mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 07:16:08 +08:00
Upgrade to Electron 8, improve TS usage and TS errors outside calendar [requires re- npm install] (#2284)
* Shfit away from default exports and PropTypes for better TS support * localize strings and expand use of types in WeekView, create new EventOccurence distinct from Event * Remove calendar wrap, use TS enum for view type + consistent prop interface * Bump Typescript to 3.8.3 and improve query / attribute / search typings * Re-use the Autolinker for calendar event descriptions with aggressive phone detection * Clean up WeekView and the editing popover, lots of cruft here * Update ScrollRegion to initialize scrollbar provided by external ref * Expose ScrollRegion’s resizeObserver to clean up tick interval tracking * Simply tickGenerator and move it to a helper * Bump to Electron 8.x for Chrome 75+ CSS features * Bump Handlebars dep to fix annoying npm audit noise * Remove electron-remote from electron-spellchecker * Explicitly add node-gyp, why is this necessary? * Fix lesslint issues * Bump eslint and let it fix 133 issues * Satisfy remaining eslint@2020 errors by hand * Add tsc-watch npm script and fix all TS errors outside calendar * Configure appveyor to publish all the pdb files it gets * Log sync exit codes and signals for easier triage on Windows * Upgrade npm, mark that the build process supports Node 11+ not just Node 11 * Resolve more errors * Upgrade sqlite to be a context-aware native module * Fix: Tab key no longer navigating into contenteditable because tabIndex not inferred * Fix: Bad print styles because Chrome now adds more CSS of it’s own when doctype is missing * Fix: before-navigate is now called after beforeunload
This commit is contained in:
parent
3b07b0767f
commit
cff437e900
|
@ -32,7 +32,7 @@ build_script:
|
|||
- cmd: node app/build/create-signed-windows-installer.js
|
||||
|
||||
before_deploy:
|
||||
- cmd: 7z -ttar a dummy %APPVEYOR_BUILD_FOLDER%\app\dist\*.dll %APPVEYOR_BUILD_FOLDER%\app\dist\mailsync.exe -so | 7z -si -tgzip a .\app\dist\mailsync.tar.gz
|
||||
- cmd: 7z -ttar a dummy %APPVEYOR_BUILD_FOLDER%\app\dist\*.dll %APPVEYOR_BUILD_FOLDER%\app\dist\*.pdb %APPVEYOR_BUILD_FOLDER%\app\dist\mailsync.exe -so | 7z -si -tgzip a .\app\dist\mailsync.tar.gz
|
||||
- ps: Get-ChildItem .\app\dist\*.tar.gz | % { Push-AppveyorArtifact $_.FullName -FileName "win-ia32/$($_.Name)" -DeploymentName s3-deployment }
|
||||
- ps: Get-ChildItem .\app\dist\MailspringSetup.exe | % { Push-AppveyorArtifact $_.FullName -FileName "win-ia32/$($_.Name)" -DeploymentName s3-deployment }
|
||||
- ps: Get-ChildItem .\app\dist\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName "win-ia32/$($_.Name)" -DeploymentName s3-deployment }
|
||||
|
|
|
@ -6,7 +6,7 @@ git:
|
|||
language: node_js
|
||||
|
||||
node_js:
|
||||
- '11'
|
||||
- '12'
|
||||
|
||||
addons:
|
||||
artifacts:
|
||||
|
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -29,5 +29,6 @@
|
|||
"git.ignoreLimitWarning": true,
|
||||
"files.exclude": {
|
||||
"**/*.dll": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
|
11
.vscode/tasks.json
vendored
Normal file
11
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"command": "./node_modules/.bin/tsc",
|
||||
"type": "shell",
|
||||
"args": ["-w", "-p", "./app", "--noEmit"],
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"isBackground": true,
|
||||
"problemMatcher": "$tsc-watch"
|
||||
}
|
|
@ -52,7 +52,7 @@ const onDeleteItem = function(item) {
|
|||
return;
|
||||
}
|
||||
|
||||
const chosen = remote.dialog.showMessageBox({
|
||||
const response = remote.dialog.showMessageBoxSync({
|
||||
type: 'info',
|
||||
message: localized('Are you sure?'),
|
||||
detail: localized(
|
||||
|
@ -62,7 +62,7 @@ const onDeleteItem = function(item) {
|
|||
defaultId: 0,
|
||||
});
|
||||
|
||||
if (chosen !== 0) {
|
||||
if (response !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
|
||||
import SidebarItem from './sidebar-item';
|
||||
import * as SidebarActions from './sidebar-actions';
|
||||
import { ISidebarSection } from './types';
|
||||
import { ISidebarSection, ISidebarItem } from './types';
|
||||
|
||||
function isSectionCollapsed(title) {
|
||||
if (AppEnv.savedState.sidebarKeysCollapsed[title] !== undefined) {
|
||||
|
@ -106,8 +106,8 @@ class SidebarSection {
|
|||
];
|
||||
const items = [];
|
||||
|
||||
for (let names of standardNames) {
|
||||
names = Array.isArray(names) ? names : [names];
|
||||
for (const nameOrNames of standardNames) {
|
||||
const names = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames];
|
||||
const categories = CategoryStore.getCategoriesWithRoles(accounts, ...names);
|
||||
if (categories.length === 0) {
|
||||
continue;
|
||||
|
@ -192,15 +192,16 @@ class SidebarSection {
|
|||
// Inbox.FolderA.FolderB
|
||||
// Inbox.FolderB
|
||||
//
|
||||
const items = [];
|
||||
const seenItems = {};
|
||||
const items: ISidebarItem[] = [];
|
||||
const seenItems: { [key: string]: ISidebarItem } = {};
|
||||
for (const category of CategoryStore.userCategories(account)) {
|
||||
// https://regex101.com/r/jK8cC2/1
|
||||
let item, parentKey;
|
||||
let item: ISidebarItem = null;
|
||||
const re = RegExpUtils.subcategorySplitRegex();
|
||||
const itemKey = category.displayName.replace(re, '/');
|
||||
|
||||
let parent = null;
|
||||
let parentKey: string = null;
|
||||
const parentComponents = itemKey.split('/');
|
||||
for (let i = parentComponents.length; i >= 1; i--) {
|
||||
parentKey = parentComponents.slice(0, i).join('/');
|
||||
|
|
|
@ -12,7 +12,7 @@ export interface ISidebarItem {
|
|||
collapsed: boolean;
|
||||
counterStyle: string;
|
||||
onDelete?: () => void;
|
||||
onEdited?: () => void;
|
||||
onEdited?: (item, name: string) => void;
|
||||
onCollapseToggled: () => void;
|
||||
onDrop: (item, event) => void;
|
||||
shouldAcceptDrop: (item, event) => void;
|
||||
|
|
|
@ -3,25 +3,25 @@ import SidebarItem from '../lib/sidebar-item';
|
|||
|
||||
describe('sidebar-item', function sidebarItemSpec() {
|
||||
it('preserves nested labels on rename', () => {
|
||||
spyOn(Actions, 'queueTask');
|
||||
const categories = [new Folder({ path: 'a.b/c', accountId: window.TEST_ACCOUNT_ID })];
|
||||
const queueTask = spyOn(Actions, 'queueTask');
|
||||
const categories = [new Folder({ path: 'a.b/c', accountId: TEST_ACCOUNT_ID })];
|
||||
AppEnv.savedState.sidebarKeysCollapsed = {};
|
||||
const item = SidebarItem.forCategories(categories) as any;
|
||||
item.onEdited(item, 'd');
|
||||
|
||||
const task = Actions.queueTask.calls[0].args[0];
|
||||
const task = queueTask.calls[0].args[0];
|
||||
const { existingPath, path } = task;
|
||||
expect(existingPath).toBe('a.b/c');
|
||||
expect(path).toBe('a.b/d');
|
||||
});
|
||||
it('preserves labels on rename', () => {
|
||||
spyOn(Actions, 'queueTask');
|
||||
const categories = [new Folder({ path: 'a', accountId: window.TEST_ACCOUNT_ID })];
|
||||
const queueTask = spyOn(Actions, 'queueTask');
|
||||
const categories = [new Folder({ path: 'a', accountId: TEST_ACCOUNT_ID })];
|
||||
AppEnv.savedState.sidebarKeysCollapsed = {};
|
||||
const item = SidebarItem.forCategories(categories);
|
||||
item.onEdited(item, 'b') as any;
|
||||
|
||||
const task = Actions.queueTask.calls[0].args[0];
|
||||
const task = queueTask.calls[0].args[0];
|
||||
const { existingPath, path } = task;
|
||||
expect(existingPath).toBe('a');
|
||||
expect(path).toBe('b');
|
||||
|
|
|
@ -5,6 +5,7 @@ import { RetinaImg } from 'mailspring-component-kit';
|
|||
|
||||
function buildShareHTML(htmlEl, styleEl) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
|
@ -57,7 +57,7 @@ class SignatureEditor extends React.Component<SignatureEditorProps, SignatureEdi
|
|||
t => sig.body === RenderSignatureData({ ...sig.data, templateName: t.name })
|
||||
);
|
||||
if (!htmlMatchesATemplate) {
|
||||
const idx = remote.dialog.showMessageBox({
|
||||
const idx = remote.dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
buttons: [localized('Cancel'), localized('Continue')],
|
||||
message: localized('Revert custom HTML?'),
|
||||
|
|
|
@ -155,7 +155,7 @@ class TemplateStore extends MailspringStore {
|
|||
|
||||
_displayDialog(title, message, buttons) {
|
||||
return (
|
||||
remote.dialog.showMessageBox({
|
||||
remote.dialog.showMessageBoxSync({
|
||||
title: title,
|
||||
message: title,
|
||||
detail: message,
|
||||
|
|
|
@ -62,7 +62,9 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
|
|||
dropzone = React.createRef<DropZone>();
|
||||
sendButton = React.createRef<SendActionButton>();
|
||||
focusContainer = React.createRef<KeyCommandsRegion & HTMLDivElement>();
|
||||
editor = React.createRef<ComposerEditor>();
|
||||
editor:
|
||||
| React.RefObject<ComposerEditorPlaintext>
|
||||
| React.RefObject<ComposerEditor> = React.createRef<any>();
|
||||
header = React.createRef<ComposerHeader>();
|
||||
|
||||
_keymapHandlers = {
|
||||
|
@ -158,7 +160,7 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
|
|||
<div className="composer-body-wrap">
|
||||
{draft.plaintext ? (
|
||||
<ComposerEditorPlaintext
|
||||
ref={this.editor}
|
||||
ref={this.editor as React.RefObject<ComposerEditorPlaintext>}
|
||||
value={draft.body}
|
||||
propsForPlugins={{ draft, session }}
|
||||
onFileReceived={this._onFileReceived}
|
||||
|
@ -170,7 +172,7 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
|
|||
) : (
|
||||
<>
|
||||
<ComposerEditor
|
||||
ref={this.editor}
|
||||
ref={this.editor as React.RefObject<ComposerEditor>}
|
||||
value={draft.bodyEditorState}
|
||||
className={quotedTextHidden && 'hiding-quoted-text'}
|
||||
propsForPlugins={{ draft, session }}
|
||||
|
@ -364,7 +366,7 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
|
|||
}
|
||||
|
||||
if (warnings.length > 0 && !options.force) {
|
||||
const response = dialog.showMessageBox({
|
||||
const response = dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
buttons: [localized('Send Anyway'), localized('Cancel')],
|
||||
message: localized('Are you sure?'),
|
||||
|
|
|
@ -385,7 +385,11 @@ body.platform-win32 {
|
|||
.composer-body-wrap {
|
||||
padding: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.composer-header {
|
||||
.key-commands-region {
|
||||
height: initial;
|
||||
}
|
||||
}
|
||||
.composer-header-actions {
|
||||
position: relative;
|
||||
float: right;
|
||||
|
|
|
@ -14,7 +14,12 @@ class FoundInMailEnabledBarWithData extends React.Component<FoundInMailEnabledBa
|
|||
static displayName = 'FoundInMailEnabledBar';
|
||||
|
||||
_onToggle = () => {
|
||||
const accountId = this.props.perspective.accountId;
|
||||
const { perspective } = this.props;
|
||||
if (!perspective || perspective.type !== 'found-in-mail') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const accountId = perspective.accountId;
|
||||
let disabled = AppEnv.config.get(CONFIG_KEY);
|
||||
if (disabled.includes(accountId)) {
|
||||
disabled = disabled.filter(i => i !== accountId);
|
||||
|
|
|
@ -10,7 +10,7 @@ export const showGPeopleReadonlyNotice = (accountId: string) => {
|
|||
acct.provider === 'gmail' &&
|
||||
(!acct.authedAt || acct.authedAt < CONTACTS_OAUTH_SCOPE_ADDED)
|
||||
) {
|
||||
remote.dialog.showMessageBox({
|
||||
remote.dialog.showMessageBoxSync({
|
||||
message: localized(`Please re-authenticate with Google`),
|
||||
detail: localized(
|
||||
`To make changes to contacts in this account, you'll need to re-authorize Mailspring to access your data.\n\n` +
|
||||
|
|
|
@ -61,11 +61,12 @@ class DraftListStore extends MailspringStore {
|
|||
|
||||
const subscription = new MutableQuerySubscription(query, { emitResultSet: true });
|
||||
const $resultSet = Rx.Observable.combineLatest(
|
||||
[
|
||||
Rx.Observable.fromNamedQuerySubscription('draft-list', subscription),
|
||||
Rx.Observable.fromStore(OutboxStore) as any,
|
||||
],
|
||||
(resultSet: QueryResultSet<Message>, outbox) => {
|
||||
Rx.Observable.fromNamedQuerySubscription('draft-list', subscription),
|
||||
Rx.Observable.fromStore(OutboxStore),
|
||||
(resultSet, outboxStore) => {
|
||||
if (!(resultSet instanceof QueryResultSet)) {
|
||||
throw 'Set emitResultSet=true and did not receive a QueryResultSet';
|
||||
}
|
||||
// Generate a new result set that includes additional information on
|
||||
// the draft objects. This is similar to what we do in the thread-list,
|
||||
// where we set thread.__messages to the message array.
|
||||
|
@ -73,7 +74,7 @@ class DraftListStore extends MailspringStore {
|
|||
|
||||
// TODO BG modelWithId: task.headerMessageId does not work
|
||||
mailboxPerspective.accountIds.forEach(aid => {
|
||||
OutboxStore.itemsForAccount(aid).forEach(task => {
|
||||
outboxStore.itemsForAccount(aid).forEach(task => {
|
||||
let draft = resultSet.modelWithId(task.headerMessageId) as any;
|
||||
if (draft) {
|
||||
draft = draft.clone();
|
||||
|
|
|
@ -82,7 +82,7 @@ class GithubUserStore extends MailspringStore {
|
|||
`https://api.github.com/search/repositories?q=user:${profile.login}&sort=stars&order=desc`
|
||||
);
|
||||
// Sort the repositories by their stars (`-` for descending order)
|
||||
profile.repos = _.sortBy(repos.items, repo => -repo.stargazers_count);
|
||||
profile.repos = _.sortBy(repos.items, repo => -repo['stargazers_count']);
|
||||
// Trigger so that our React components refresh their state and display
|
||||
// the updated data.
|
||||
this.trigger(this);
|
||||
|
|
|
@ -33,7 +33,7 @@ class SubmitLocalizationsBar extends React.Component {
|
|||
json: true,
|
||||
});
|
||||
if (status === 'success') {
|
||||
remote.dialog.showMessageBox({
|
||||
remote.dialog.showMessageBoxSync({
|
||||
type: 'info',
|
||||
buttons: [localized('OK')],
|
||||
message: localized('Thank you!'),
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
import { Actions, DestroyModelTask, Event } from 'mailspring-exports';
|
||||
import React from 'react';
|
||||
import { remote } from 'electron';
|
||||
|
||||
import { KeyCommandsRegion } from 'mailspring-component-kit';
|
||||
import CalendarDataSource from './core/calendar-data-source';
|
||||
import CalendarEventPopover from './core/calendar-event-popover';
|
||||
import MailspringCalendar from './core/mailspring-calendar';
|
||||
|
||||
export default class CalendarWrapper extends React.Component<{}, { selectedEvents: Event[] }> {
|
||||
static displayName = 'CalendarWrapper';
|
||||
static containerRequired = false;
|
||||
|
||||
_dataSource = new CalendarDataSource();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { selectedEvents: [] };
|
||||
}
|
||||
|
||||
_openEventPopover(eventModel: Event) {
|
||||
const eventEl = document.getElementById(eventModel.id);
|
||||
if (!eventEl) {
|
||||
return;
|
||||
}
|
||||
const eventRect = eventEl.getBoundingClientRect();
|
||||
|
||||
Actions.openPopover(<CalendarEventPopover event={eventModel} />, {
|
||||
originRect: eventRect,
|
||||
direction: 'right',
|
||||
fallbackDirection: 'left',
|
||||
});
|
||||
}
|
||||
|
||||
_onEventClick = (e: React.MouseEvent, event: Event) => {
|
||||
let next = [...this.state.selectedEvents];
|
||||
|
||||
if (e.shiftKey || e.metaKey) {
|
||||
const idx = next.findIndex(({ id }) => event.id === id);
|
||||
if (idx === -1) {
|
||||
next.push(event);
|
||||
} else {
|
||||
next.splice(idx, 1);
|
||||
}
|
||||
} else {
|
||||
next = [event];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedEvents: next,
|
||||
});
|
||||
};
|
||||
|
||||
_onEventDoubleClick = (eventModel: Event) => {
|
||||
this._openEventPopover(eventModel);
|
||||
};
|
||||
|
||||
_onEventFocused = (eventModel: Event) => {
|
||||
this._openEventPopover(eventModel);
|
||||
};
|
||||
|
||||
_onDeleteSelectedEvents = () => {
|
||||
if (this.state.selectedEvents.length === 0) {
|
||||
return;
|
||||
}
|
||||
const response = remote.dialog.showMessageBox({
|
||||
type: 'warning',
|
||||
buttons: ['Delete', 'Cancel'],
|
||||
message: 'Delete or decline these events?',
|
||||
detail: `Are you sure you want to delete or decline invitations for the selected event(s)?`,
|
||||
});
|
||||
if (response === 0) {
|
||||
// response is button array index
|
||||
for (const event of this.state.selectedEvents) {
|
||||
const task = new DestroyModelTask({
|
||||
modelId: event.id,
|
||||
modelName: event.constructor.name,
|
||||
endpoint: '/events',
|
||||
accountId: event.accountId,
|
||||
});
|
||||
Actions.queueTask(task);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<KeyCommandsRegion
|
||||
className="main-calendar"
|
||||
localHandlers={{
|
||||
'core:remove-from-view': this._onDeleteSelectedEvents,
|
||||
}}
|
||||
>
|
||||
<MailspringCalendar
|
||||
dataSource={this._dataSource}
|
||||
onEventClick={this._onEventClick}
|
||||
onEventDoubleClick={this._onEventDoubleClick}
|
||||
onEventFocused={this._onEventFocused}
|
||||
selectedEvents={this.state.selectedEvents}
|
||||
/>
|
||||
</KeyCommandsRegion>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export const DAY_VIEW = 'day';
|
||||
export const WEEK_VIEW = 'week';
|
||||
export const MONTH_VIEW = 'month';
|
||||
export const YEAR_VIEW = 'year';
|
||||
export enum CalendarView {
|
||||
DAY = 'day',
|
||||
WEEK = 'week',
|
||||
MONTH = 'month',
|
||||
}
|
||||
|
|
|
@ -2,18 +2,32 @@ import Rx from 'rx-lite';
|
|||
import { Event, Matcher, DatabaseStore } from 'mailspring-exports';
|
||||
import IcalExpander from 'ical-expander';
|
||||
|
||||
export default class CalendarDataSource {
|
||||
observable: Rx.Observable<{ events: Event[] }>;
|
||||
export interface EventOccurrence {
|
||||
start: number; // unix
|
||||
end: number; // unix
|
||||
id: string;
|
||||
accountId: string;
|
||||
calendarId: string;
|
||||
title: string;
|
||||
location: string;
|
||||
description: string;
|
||||
isAllDay: boolean;
|
||||
organizer: { email: string } | null;
|
||||
attendees: { email: string; name: string }[];
|
||||
}
|
||||
|
||||
buildObservable({ startTime, endTime, disabledCalendars }) {
|
||||
export class CalendarDataSource {
|
||||
observable: Rx.Observable<{ events: EventOccurrence[] }>;
|
||||
|
||||
buildObservable({ startUnix, endUnix, disabledCalendars }) {
|
||||
const end = Event.attributes.recurrenceEnd;
|
||||
const start = Event.attributes.recurrenceStart;
|
||||
|
||||
const matcher = new Matcher.Or([
|
||||
new Matcher.And([start.lte(endTime), end.gte(startTime)]),
|
||||
new Matcher.And([start.lte(endTime), start.gte(startTime)]),
|
||||
new Matcher.And([end.gte(startTime), end.lte(endTime)]),
|
||||
new Matcher.And([end.gte(endTime), start.lte(startTime)]),
|
||||
new Matcher.And([start.lte(endUnix), end.gte(startUnix)]),
|
||||
new Matcher.And([start.lte(endUnix), start.gte(startUnix)]),
|
||||
new Matcher.And([end.gte(startUnix), end.lte(endUnix)]),
|
||||
new Matcher.And([end.gte(endUnix), start.lte(startUnix)]),
|
||||
]);
|
||||
|
||||
if (disabledCalendars && disabledCalendars.length) {
|
||||
|
@ -21,35 +35,9 @@ export default class CalendarDataSource {
|
|||
}
|
||||
|
||||
const query = DatabaseStore.findAll<Event>(Event).where(matcher);
|
||||
this.observable = Rx.Observable.fromQuery(query).flatMapLatest(results => {
|
||||
const events = [];
|
||||
results.forEach(result => {
|
||||
const icalExpander = new IcalExpander({ ics: result.ics, maxIterations: 100 });
|
||||
const expanded = icalExpander.between(new Date(startTime * 1000), new Date(endTime * 1000));
|
||||
|
||||
[...expanded.events, ...expanded.occurrences].forEach((e, idx) => {
|
||||
const start = e.startDate.toJSDate().getTime() / 1000;
|
||||
const end = e.endDate.toJSDate().getTime() / 1000;
|
||||
const item = e.item || e;
|
||||
events.push({
|
||||
start,
|
||||
end,
|
||||
id: `${result.id}-e${idx}`,
|
||||
calendarId: result.calendarId,
|
||||
title: item.summary,
|
||||
displayTitle: item.summary,
|
||||
description: item.description,
|
||||
isAllDay: end - start >= 86400 - 1,
|
||||
organizer: item.organizer ? { email: item.organizer } : null,
|
||||
attendees: item.attendees.map(a => ({
|
||||
...a.jCal[1],
|
||||
email: a.getFirstValue(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
});
|
||||
return Rx.Observable.from([{ events }]);
|
||||
});
|
||||
this.observable = Rx.Observable.fromQuery(query).flatMapLatest(results =>
|
||||
Rx.Observable.from([{ events: occurrencesForEvents(results, { startUnix, endUnix }) }])
|
||||
);
|
||||
return this.observable;
|
||||
}
|
||||
|
||||
|
@ -57,3 +45,39 @@ export default class CalendarDataSource {
|
|||
return this.observable.subscribe(callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function occurrencesForEvents(
|
||||
results: Event[],
|
||||
{ startUnix, endUnix }: { startUnix: number; endUnix: number }
|
||||
) {
|
||||
const occurences: EventOccurrence[] = [];
|
||||
|
||||
results.forEach(result => {
|
||||
const icalExpander = new IcalExpander({ ics: result.ics, maxIterations: 100 });
|
||||
const expanded = icalExpander.between(new Date(startUnix * 1000), new Date(endUnix * 1000));
|
||||
|
||||
[...expanded.events, ...expanded.occurrences].forEach((e, idx) => {
|
||||
const start = e.startDate.toJSDate().getTime() / 1000;
|
||||
const end = e.endDate.toJSDate().getTime() / 1000;
|
||||
const item = e.item || e;
|
||||
occurences.push({
|
||||
start,
|
||||
end,
|
||||
id: `${result.id}-e${idx}`,
|
||||
accountId: result.accountId,
|
||||
calendarId: result.calendarId,
|
||||
title: item.summary,
|
||||
location: item.location,
|
||||
description: item.description,
|
||||
isAllDay: end - start >= 86400 - 1,
|
||||
organizer: item.organizer ? { email: item.organizer } : null,
|
||||
attendees: item.attendees.map(a => ({
|
||||
...a.jCal[1],
|
||||
email: a.getFirstValue(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return occurences;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
import moment from 'moment';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class CalendarEventContainer extends React.Component {
|
||||
import { EventOccurrence } from './calendar-data-source';
|
||||
|
||||
export interface CalendarEventArgs {
|
||||
event?: EventOccurrence;
|
||||
time: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
mouseIsDown: boolean;
|
||||
}
|
||||
|
||||
interface CalendarEventContainerProps {
|
||||
onCalendarMouseDown: (args: CalendarEventArgs) => void;
|
||||
onCalendarMouseMove: (args: CalendarEventArgs) => void;
|
||||
onCalendarMouseUp: (args: CalendarEventArgs) => void;
|
||||
}
|
||||
|
||||
export class CalendarEventContainer extends React.Component<CalendarEventContainerProps> {
|
||||
static displayName = 'CalendarEventContainer';
|
||||
|
||||
static propTypes = {
|
||||
onCalendarMouseUp: PropTypes.func,
|
||||
onCalendarMouseDown: PropTypes.func,
|
||||
onCalendarMouseMove: PropTypes.func,
|
||||
};
|
||||
|
||||
_DOMCache: {
|
||||
eventColumn?: any;
|
||||
gridWrap?: any;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import moment from 'moment';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import {
|
||||
PropTypes,
|
||||
Event,
|
||||
Actions,
|
||||
DatabaseStore,
|
||||
DateUtils,
|
||||
SyncbackEventTask,
|
||||
localized,
|
||||
RegExpUtils,
|
||||
Autolink,
|
||||
} from 'mailspring-exports';
|
||||
import {
|
||||
DatePicker,
|
||||
|
@ -15,10 +15,12 @@ import {
|
|||
TabGroupRegion,
|
||||
TimePicker,
|
||||
} from 'mailspring-component-kit';
|
||||
import EventAttendeesInput from './event-attendees-input';
|
||||
import { EventAttendeesInput } from './event-attendees-input';
|
||||
import { EventOccurrence } from './calendar-data-source';
|
||||
import { EventTimerangePicker } from './event-timerange-picker';
|
||||
|
||||
interface CalendarEventPopoverProps {
|
||||
event: Event;
|
||||
event: EventOccurrence;
|
||||
}
|
||||
|
||||
interface CalendarEventPopoverState {
|
||||
|
@ -31,36 +33,28 @@ interface CalendarEventPopoverState {
|
|||
title: string;
|
||||
}
|
||||
|
||||
export default class CalendarEventPopover extends React.Component<
|
||||
export class CalendarEventPopover extends React.Component<
|
||||
CalendarEventPopoverProps,
|
||||
CalendarEventPopoverState
|
||||
> {
|
||||
static propTypes = {
|
||||
event: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { description, start, end, location, attendees } = this.props.event;
|
||||
const { description, start, end, location, attendees, title } = this.props.event;
|
||||
|
||||
this.state = {
|
||||
description,
|
||||
start,
|
||||
end,
|
||||
location,
|
||||
title: this.props.event.displayTitle,
|
||||
title,
|
||||
editing: false,
|
||||
attendees: attendees || [],
|
||||
attendees,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps = nextProps => {
|
||||
const { description, start, end, location, attendees } = nextProps.event;
|
||||
this.setState({ description, start, end, location });
|
||||
this.setState({
|
||||
attendees: attendees || [],
|
||||
title: nextProps.event.displayTitle,
|
||||
});
|
||||
const { description, start, end, location, attendees, title } = nextProps.event;
|
||||
this.setState({ description, start, end, location, attendees, title });
|
||||
};
|
||||
|
||||
onEdit = () => {
|
||||
|
@ -89,34 +83,7 @@ export default class CalendarEventPopover extends React.Component<
|
|||
Actions.queueTask(task);
|
||||
};
|
||||
|
||||
extractNotesFromDescription(node) {
|
||||
const els = node.querySelectorAll('meta[itemprop=description]');
|
||||
let notes: string = null;
|
||||
if (els.length) {
|
||||
notes = Array.from(els)
|
||||
.map((el: any) => el.content)
|
||||
.join('\n');
|
||||
} else {
|
||||
notes = node.innerText;
|
||||
}
|
||||
while (true) {
|
||||
const nextNotes = notes.replace('\n\n', '\n');
|
||||
if (nextNotes === notes) {
|
||||
break;
|
||||
}
|
||||
notes = nextNotes;
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
// If on the hour, formats as "3 PM", else formats as "3:15 PM"
|
||||
formatTime(momentTime) {
|
||||
const min = momentTime.minutes();
|
||||
if (min === 0) {
|
||||
return momentTime.format('h A');
|
||||
}
|
||||
return momentTime.format('h:mm A');
|
||||
}
|
||||
|
||||
updateAttendees = attendees => {
|
||||
this.setState({ attendees });
|
||||
|
@ -128,86 +95,10 @@ export default class CalendarEventPopover extends React.Component<
|
|||
this.setState(updates);
|
||||
};
|
||||
|
||||
_onChangeDay = newTimestamp => {
|
||||
const newDay = moment(newTimestamp);
|
||||
const start = this.getStartMoment();
|
||||
const end = this.getEndMoment();
|
||||
start.year(newDay.year());
|
||||
end.year(newDay.year());
|
||||
start.dayOfYear(newDay.dayOfYear());
|
||||
end.dayOfYear(newDay.dayOfYear());
|
||||
this.setState({ start: start.unix(), end: end.unix() });
|
||||
};
|
||||
|
||||
_onChangeStartTime = newTimestamp => {
|
||||
const newStart = moment(newTimestamp);
|
||||
let newEnd = this.getEndMoment();
|
||||
if (newEnd.isSameOrBefore(newStart)) {
|
||||
const leftInDay = moment(newStart)
|
||||
.endOf('day')
|
||||
.diff(newStart);
|
||||
const move = Math.min(leftInDay, moment.duration(1, 'hour').asMilliseconds());
|
||||
newEnd = moment(newStart).add(move, 'ms');
|
||||
}
|
||||
this.setState({ start: newStart.unix(), end: newEnd.unix() });
|
||||
};
|
||||
|
||||
_onChangeEndTime = newTimestamp => {
|
||||
const newEnd = moment(newTimestamp);
|
||||
let newStart = this.getStartMoment();
|
||||
if (newStart.isSameOrAfter(newEnd)) {
|
||||
const sinceDay = moment(newEnd).diff(newEnd.startOf('day'));
|
||||
const move = Math.min(sinceDay, moment.duration(1, 'hour').asMilliseconds());
|
||||
newStart = moment(newEnd).subtract(move, 'ms');
|
||||
}
|
||||
this.setState({ end: newEnd.unix(), start: newStart.unix() });
|
||||
};
|
||||
|
||||
renderTime() {
|
||||
const startMoment = this.getStartMoment();
|
||||
const endMoment = this.getEndMoment();
|
||||
const date = startMoment.format('dddd, MMMM D'); // e.g. Tuesday, February 22
|
||||
const timeRange = `${this.formatTime(startMoment)} - ${this.formatTime(endMoment)}`;
|
||||
return (
|
||||
<div>
|
||||
{date}
|
||||
<br />
|
||||
{timeRange}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditableTime() {
|
||||
const startVal = this.state.start * 1000;
|
||||
const endVal = this.state.end * 1000;
|
||||
return (
|
||||
<div className="row time">
|
||||
<RetinaImg name="ic-eventcard-time@2x.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
<span>
|
||||
<TimePicker value={startVal} onChange={this._onChangeStartTime} />
|
||||
to
|
||||
<TimePicker value={endVal} onChange={this._onChangeEndTime} />
|
||||
<span className="timezone">
|
||||
{moment()
|
||||
.tz(DateUtils.timeZone)
|
||||
.format('z')}
|
||||
</span>
|
||||
on
|
||||
<DatePicker value={startVal} onChange={this._onChangeDay} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditable = () => {
|
||||
const { title, description, start, end, location, attendees } = this.state;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
const descriptionRoot = document.createElement('root');
|
||||
fragment.appendChild(descriptionRoot);
|
||||
descriptionRoot.innerHTML = description;
|
||||
|
||||
const notes = this.extractNotesFromDescription(descriptionRoot);
|
||||
const notes = extractNotesFromDescription(description);
|
||||
|
||||
return (
|
||||
<div className="calendar-event-popover" tabIndex={0}>
|
||||
|
@ -230,9 +121,15 @@ export default class CalendarEventPopover extends React.Component<
|
|||
this.updateField('location', e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className="section">{this.renderEditableTime()}</div>
|
||||
<div className="section">
|
||||
<div className="label">Invitees: </div>
|
||||
<EventTimerangePicker
|
||||
start={start}
|
||||
end={end}
|
||||
onChange={({ start, end }) => this.setState({ start, end })}
|
||||
/>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="label">{localized(`Invitees`)}:</div>
|
||||
<EventAttendeesInput
|
||||
className="event-participant-field"
|
||||
attendees={attendees}
|
||||
|
@ -242,7 +139,7 @@ export default class CalendarEventPopover extends React.Component<
|
|||
/>
|
||||
</div>
|
||||
<div className="section">
|
||||
<div className="label">Notes: </div>
|
||||
<div className="label">{localized(`Notes`)}:</div>
|
||||
<input
|
||||
type="text"
|
||||
value={notes}
|
||||
|
@ -251,8 +148,8 @@ export default class CalendarEventPopover extends React.Component<
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<span onClick={this.saveEdits}>Save</span>
|
||||
<span onClick={() => Actions.closePopover()}>Cancel</span>
|
||||
<span onClick={this.saveEdits}>{localized(`Save`)}</span>
|
||||
<span onClick={() => Actions.closePopover()}>{localized(`Cancel`)}</span>
|
||||
</TabGroupRegion>
|
||||
</div>
|
||||
);
|
||||
|
@ -262,14 +159,56 @@ export default class CalendarEventPopover extends React.Component<
|
|||
if (this.state.editing) {
|
||||
return this.renderEditable();
|
||||
}
|
||||
const { title, description, location, attendees } = this.state;
|
||||
return (
|
||||
<CalendarEventPopoverUnenditable
|
||||
{...this.props}
|
||||
onEdit={() => this.setState({ editing: true })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
const descriptionRoot = document.createElement('root');
|
||||
fragment.appendChild(descriptionRoot);
|
||||
descriptionRoot.innerHTML = description;
|
||||
class CalendarEventPopoverUnenditable extends React.Component<{
|
||||
event: EventOccurrence;
|
||||
onEdit: () => void;
|
||||
}> {
|
||||
descriptionRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
const notes = this.extractNotesFromDescription(descriptionRoot);
|
||||
renderTime() {
|
||||
const startMoment = moment(this.props.event.start * 1000);
|
||||
const endMoment = moment(this.props.event.end * 1000);
|
||||
const date = startMoment.format('dddd, MMMM D'); // e.g. Tuesday, February 22
|
||||
const timeRange = `${formatTime(startMoment)} - ${formatTime(endMoment)}`;
|
||||
return (
|
||||
<div>
|
||||
{date}
|
||||
<br />
|
||||
{timeRange}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.autolink();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.autolink();
|
||||
}
|
||||
|
||||
autolink() {
|
||||
if (!this.descriptionRef.current) return;
|
||||
Autolink(this.descriptionRef.current, {
|
||||
async: false,
|
||||
telAggressiveMatch: true,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { event, onEdit } = this.props;
|
||||
const { title, description, location, attendees } = event;
|
||||
|
||||
const notes = extractNotesFromDescription(description);
|
||||
|
||||
return (
|
||||
<div className="calendar-event-popover" tabIndex={0}>
|
||||
|
@ -280,22 +219,67 @@ export default class CalendarEventPopover extends React.Component<
|
|||
name="edit-icon.png"
|
||||
title="Edit Item"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
onClick={this.onEdit}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
</div>
|
||||
<div className="location">{location}</div>
|
||||
{location && (
|
||||
<div className="location">
|
||||
{location.startsWith('http') || location.startsWith('tel:') ? (
|
||||
<a href={location}>{location}</a>
|
||||
) : (
|
||||
location
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="section">{this.renderTime()}</div>
|
||||
<ScrollRegion className="section invitees">
|
||||
<div className="label">Invitees: </div>
|
||||
<div>{attendees.map((a, idx) => <div key={idx}> {a.cn} </div>)}</div>
|
||||
<div className="label">{localized(`Invitees`)}: </div>
|
||||
<div>
|
||||
{attendees.map((a, idx) => (
|
||||
<div key={idx}> {a.cn}</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollRegion>
|
||||
<ScrollRegion className="section description">
|
||||
<div className="description">
|
||||
<div className="label">Notes: </div>
|
||||
<div>{notes}</div>
|
||||
<div className="label">{localized(`Notes`)}: </div>
|
||||
<div ref={this.descriptionRef}>{notes}</div>
|
||||
</div>
|
||||
</ScrollRegion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function extractNotesFromDescription(description: string) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const descriptionRoot = document.createElement('root');
|
||||
fragment.appendChild(descriptionRoot);
|
||||
descriptionRoot.innerHTML = description;
|
||||
|
||||
const els = descriptionRoot.querySelectorAll('meta[itemprop=description]');
|
||||
let notes: string = null;
|
||||
if (els.length) {
|
||||
notes = Array.from(els)
|
||||
.map((el: any) => el.content)
|
||||
.join('\n');
|
||||
} else {
|
||||
notes = descriptionRoot.innerText;
|
||||
}
|
||||
while (true) {
|
||||
const nextNotes = notes.replace('\n\n', '\n');
|
||||
if (nextNotes === notes) {
|
||||
break;
|
||||
}
|
||||
notes = nextNotes;
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
function formatTime(momentTime: Moment) {
|
||||
const min = momentTime.minutes();
|
||||
if (min === 0) {
|
||||
return momentTime.format('h A');
|
||||
}
|
||||
return momentTime.format('h:mm A');
|
||||
}
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Event } from 'mailspring-exports';
|
||||
import { InjectedComponentSet } from 'mailspring-component-kit';
|
||||
import { EventOccurrence } from './calendar-data-source';
|
||||
import { calcColor } from './calendar-helpers';
|
||||
|
||||
interface CalendarEventProps {
|
||||
event: Event;
|
||||
event: EventOccurrence;
|
||||
order: number;
|
||||
selected?: boolean;
|
||||
selected: boolean;
|
||||
scopeEnd: number;
|
||||
scopeStart: number;
|
||||
direction: 'horizontal' | 'vertical';
|
||||
fixedSize: number;
|
||||
focused?: boolean;
|
||||
focused: boolean;
|
||||
concurrentEvents: number;
|
||||
onClick: (e: React.MouseEvent<any>, event: Event) => void;
|
||||
onDoubleClick: (event: Event) => void;
|
||||
onFocused: (event: Event) => void;
|
||||
|
||||
onClick: (e: React.MouseEvent<any>, event: EventOccurrence) => void;
|
||||
onDoubleClick: (event: EventOccurrence) => void;
|
||||
onFocused: (event: EventOccurrence) => void;
|
||||
}
|
||||
|
||||
export default class CalendarEvent extends React.Component<CalendarEventProps> {
|
||||
export class CalendarEvent extends React.Component<CalendarEventProps> {
|
||||
static displayName = 'CalendarEvent';
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -116,7 +117,7 @@ export default class CalendarEvent extends React.Component<CalendarEventProps> {
|
|||
onDoubleClick={() => onDoubleClick(event)}
|
||||
>
|
||||
<span className="default-header" style={{ order: 0 }}>
|
||||
{event.displayTitle}
|
||||
{event.title}
|
||||
</span>
|
||||
<InjectedComponentSet
|
||||
className="event-injected-components"
|
||||
|
|
|
@ -4,7 +4,7 @@ export function calcColor(calendarId) {
|
|||
let bgColor = AppEnv.config.get(`calendar.colors.${calendarId}`);
|
||||
if (!bgColor) {
|
||||
const hue = Utils.hueForString(calendarId);
|
||||
bgColor = `hsla(${hue}, 50%, 45%, 0.35)`;
|
||||
bgColor = `hsla(${hue}, 80%, 45%, 0.55)`;
|
||||
}
|
||||
return bgColor;
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
import _ from 'underscore';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { calcColor } from './calendar-helpers';
|
||||
import { Calendar, Account } from 'mailspring-exports';
|
||||
|
||||
const DISABLED_CALENDARS = 'mailspring.disabledCalendars';
|
||||
|
||||
function renderCalendarToggles(calendars, disabledCalendars) {
|
||||
function renderCalendarToggles(calendars: Calendar[], disabledCalendars: string[]) {
|
||||
return calendars.map(calendar => {
|
||||
const calendarId = calendar.id;
|
||||
const onClick = () => {
|
||||
|
@ -43,7 +43,12 @@ function renderCalendarToggles(calendars, disabledCalendars) {
|
|||
});
|
||||
}
|
||||
|
||||
export default function CalendarSourceList(props) {
|
||||
interface CalendarSourceListProps {
|
||||
accounts: Account[];
|
||||
calendars: Calendar[];
|
||||
disabledCalendars: string[];
|
||||
}
|
||||
export function CalendarSourceList(props: CalendarSourceListProps) {
|
||||
const calsByAccountId = _.groupBy(props.calendars, 'accountId');
|
||||
const accountSections = [];
|
||||
for (const accountId of Object.keys(calsByAccountId)) {
|
||||
|
@ -61,9 +66,3 @@ export default function CalendarSourceList(props) {
|
|||
}
|
||||
return <div className="calendar-source-list-wrap">{accountSections}</div>;
|
||||
}
|
||||
|
||||
CalendarSourceList.propTypes = {
|
||||
accounts: PropTypes.array,
|
||||
calendars: PropTypes.array,
|
||||
disabledCalendars: PropTypes.array,
|
||||
};
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import Moment from 'moment';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class CurrentTimeIndicator extends React.Component<
|
||||
{
|
||||
gridHeight: number;
|
||||
numColumns: number;
|
||||
todayColumnIdx: number;
|
||||
visible: boolean;
|
||||
},
|
||||
interface CurrentTimeIndicatorProps {
|
||||
gridHeight: number;
|
||||
numColumns: number;
|
||||
todayColumnIdx: number;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export class CurrentTimeIndicator extends React.Component<
|
||||
CurrentTimeIndicatorProps,
|
||||
{ msecIntoDay: number }
|
||||
> {
|
||||
_movementTimer = null;
|
||||
|
@ -48,7 +49,7 @@ export default class CurrentTimeIndicator extends React.Component<
|
|||
|
||||
const todayMarker =
|
||||
todayColumnIdx !== -1 ? (
|
||||
<div style={{ left: `${Math.round(todayColumnIdx * 100 / numColumns)}%` }} />
|
||||
<div style={{ left: `${Math.round((todayColumnIdx * 100) / numColumns)}%` }} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import _ from 'underscore';
|
||||
import { remote, clipboard } from 'electron';
|
||||
import { PropTypes, Utils, Contact, ContactStore, RegExpUtils } from 'mailspring-exports';
|
||||
import { Utils, Contact, ContactStore, RegExpUtils, localized } from 'mailspring-exports';
|
||||
import { TokenizingTextField, Menu, InjectedComponentSet } from 'mailspring-component-kit';
|
||||
|
||||
const TokenRenderer = props => {
|
||||
const TokenRenderer = (props: { token: Contact }) => {
|
||||
const { email, cn } = props.token;
|
||||
let chipText = email;
|
||||
if (cn && cn.length > 0 && cn !== email) {
|
||||
|
@ -23,10 +23,6 @@ const TokenRenderer = props => {
|
|||
);
|
||||
};
|
||||
|
||||
TokenRenderer.propTypes = {
|
||||
token: PropTypes.object,
|
||||
};
|
||||
|
||||
interface EventAttendeesInputProps {
|
||||
attendees: any[];
|
||||
change: (next: any[]) => void;
|
||||
|
@ -35,7 +31,7 @@ interface EventAttendeesInputProps {
|
|||
onFocus?: () => void;
|
||||
}
|
||||
|
||||
export default class EventAttendeesInput extends React.Component<EventAttendeesInputProps> {
|
||||
export class EventAttendeesInput extends React.Component<EventAttendeesInputProps> {
|
||||
static displayName = 'EventAttendeesInput';
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
@ -123,7 +119,7 @@ export default class EventAttendeesInput extends React.Component<EventAttendeesI
|
|||
const menu = new MenuClass();
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: `Copy ${participant.email}`,
|
||||
label: `${localized(`Copy`)} ${participant.email}`,
|
||||
click: () => clipboard.writeText(participant.email),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { PropTypes, Utils } from 'mailspring-exports';
|
||||
import { Utils } from 'mailspring-exports';
|
||||
import { tickGenerator } from './week-view-helpers';
|
||||
|
||||
interface EventGridBackgroundProps {
|
||||
height: number;
|
||||
numColumns: number;
|
||||
tickGenerator: (arg: { type: string }) => Array<{ yPos }>;
|
||||
intervalHeight: number;
|
||||
}
|
||||
|
||||
export default class EventGridBackground extends React.Component<EventGridBackgroundProps> {
|
||||
export class EventGridBackground extends React.Component<EventGridBackgroundProps> {
|
||||
static displayName = 'EventGridBackground';
|
||||
|
||||
_lastHoverRect: { x?; y?; width?; height? } = {};
|
||||
|
@ -36,9 +36,9 @@ export default class EventGridBackground extends React.Component<EventGridBackgr
|
|||
const doStroke = (type, strokeStyle) => {
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.beginPath();
|
||||
for (const { yPos } of this.props.tickGenerator({ type })) {
|
||||
ctx.moveTo(0, yPos);
|
||||
ctx.lineTo(canvas.width, yPos);
|
||||
for (const { y } of tickGenerator(type, this.props.intervalHeight)) {
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(canvas.width, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
|
@ -47,7 +47,11 @@ export default class EventGridBackground extends React.Component<EventGridBackgr
|
|||
doStroke('major', '#e0e0e0'); // Major ticks
|
||||
}
|
||||
|
||||
mouseMove({ x, y, width }) {
|
||||
onMouseMove(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
const width = e.currentTarget.clientWidth;
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
if (!width || x == null || y == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -77,7 +81,7 @@ export default class EventGridBackground extends React.Component<EventGridBackgr
|
|||
height: this.props.height,
|
||||
};
|
||||
return (
|
||||
<div className="event-grid-bg-wrap">
|
||||
<div className="event-grid-bg-wrap" onMouseMove={this.onMouseMove}>
|
||||
<div ref="cursor" className="cursor" />
|
||||
<canvas ref="canvas" className="event-grid-bg" style={styles} />
|
||||
</div>
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Event, DatabaseStore } from 'mailspring-exports';
|
||||
import { SearchBar } from 'mailspring-component-kit';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Event, DatabaseStore, localized } from 'mailspring-exports';
|
||||
import { EventOccurrence, occurrencesForEvents } from './calendar-data-source';
|
||||
import moment from 'moment';
|
||||
|
||||
class EventSearchBar extends Component<
|
||||
{
|
||||
disabledCalendars: string[];
|
||||
onSelectEvent: (event: Event) => void;
|
||||
},
|
||||
{ query: string; suggestions: Event[] }
|
||||
interface EventSearchBarProps {
|
||||
disabledCalendars: string[];
|
||||
onSelectEvent: (event: EventOccurrence) => void;
|
||||
}
|
||||
|
||||
export class EventSearchBar extends Component<
|
||||
EventSearchBarProps,
|
||||
{ query: string; suggestions: EventOccurrence[] }
|
||||
> {
|
||||
static displayName = 'EventSearchBar';
|
||||
|
||||
static defaultProps = {
|
||||
disabledCalendars: [],
|
||||
onSelectEvent: (event: Event) => {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -40,7 +37,16 @@ class EventSearchBar extends Component<
|
|||
.search(query)
|
||||
.limit(10)
|
||||
.then(events => {
|
||||
this.setState({ suggestions: events });
|
||||
this.setState({
|
||||
suggestions: occurrencesForEvents(events, {
|
||||
startUnix: moment()
|
||||
.add(-2, 'years')
|
||||
.unix(),
|
||||
endUnix: moment()
|
||||
.add(2, 'years')
|
||||
.unix(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -69,20 +75,18 @@ class EventSearchBar extends Component<
|
|||
// TODO BG
|
||||
return <span />;
|
||||
|
||||
return (
|
||||
<SearchBar
|
||||
query={query}
|
||||
suggestions={suggestions}
|
||||
placeholder="Search all events"
|
||||
suggestionKey={event => event.id}
|
||||
suggestionRenderer={this.renderEvent}
|
||||
onSearchQueryChanged={this.onSearchQueryChanged}
|
||||
onSelectSuggestion={this.onSelectEvent}
|
||||
onClearSearchQuery={this.onClearSearchQuery}
|
||||
onClearSearchSuggestions={this.onClearSearchSuggestions}
|
||||
/>
|
||||
);
|
||||
// return (
|
||||
// <SearchBar
|
||||
// query={query}
|
||||
// suggestions={suggestions}
|
||||
// placeholder={localized('Search all events')}
|
||||
// suggestionKey={event => event.id}
|
||||
// suggestionRenderer={this.renderEvent}
|
||||
// onSearchQueryChanged={this.onSearchQueryChanged}
|
||||
// onSelectSuggestion={this.onSelectEvent}
|
||||
// onClearSearchQuery={this.onClearSearchQuery}
|
||||
// onClearSearchSuggestions={this.onClearSearchSuggestions}
|
||||
// />
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
export default EventSearchBar;
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { DateUtils } from 'mailspring-exports';
|
||||
import { DatePicker, RetinaImg, TimePicker } from 'mailspring-component-kit';
|
||||
|
||||
export const EventTimerangePicker: React.FunctionComponent<{
|
||||
start: number;
|
||||
end: number;
|
||||
onChange: ({ start, end }) => void;
|
||||
}> = ({ start, end, onChange }) => {
|
||||
const onChangeStartTime = newTimestamp => {
|
||||
const newStart = moment(newTimestamp);
|
||||
let newEnd = moment(end);
|
||||
if (newEnd.isSameOrBefore(newStart)) {
|
||||
const leftInDay = moment(newStart)
|
||||
.endOf('day')
|
||||
.diff(newStart);
|
||||
const move = Math.min(leftInDay, moment.duration(1, 'hour').asMilliseconds());
|
||||
newEnd = moment(newStart).add(move, 'ms');
|
||||
}
|
||||
onChange({ start: newStart.unix(), end: newEnd.unix() });
|
||||
};
|
||||
|
||||
const onChangeEndTime = (newTimestamp: number) => {
|
||||
const newEnd = moment(newTimestamp);
|
||||
let newStart = moment(start);
|
||||
if (newStart.isSameOrAfter(newEnd)) {
|
||||
const sinceDay = moment(newEnd).diff(newEnd.startOf('day'));
|
||||
const move = Math.min(sinceDay, moment.duration(1, 'hour').asMilliseconds());
|
||||
newStart = moment(newEnd).subtract(move, 'ms');
|
||||
}
|
||||
onChange({ end: newEnd.unix(), start: newStart.unix() });
|
||||
};
|
||||
|
||||
const onChangeDay = (newTimestamp: number) => {
|
||||
const newDay = moment(newTimestamp);
|
||||
|
||||
const newStart = moment(start);
|
||||
newStart.year(newDay.year());
|
||||
newStart.dayOfYear(newDay.dayOfYear());
|
||||
|
||||
const newEnd = moment(end);
|
||||
newEnd.year(newDay.year());
|
||||
newEnd.dayOfYear(newDay.dayOfYear());
|
||||
|
||||
this.setState({ start: newStart.unix(), end: newEnd.unix() });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row time">
|
||||
<RetinaImg name="ic-eventcard-time@2x.png" mode={RetinaImg.Mode.ContentPreserve} />
|
||||
<span>
|
||||
<TimePicker value={start * 1000} onChange={onChangeStartTime} />
|
||||
to
|
||||
<TimePicker value={end * 1000} onChange={onChangeEndTime} />
|
||||
<span className="timezone">
|
||||
{moment()
|
||||
.tz(DateUtils.timeZone)
|
||||
.format('z')}
|
||||
</span>
|
||||
on
|
||||
<DatePicker value={start * 1000} onChange={onChangeDay} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
import React from 'react';
|
||||
import { PropTypes, Utils } from 'mailspring-exports';
|
||||
|
||||
export default class FooterControls extends React.Component<{
|
||||
footerComponents: React.ReactChildren;
|
||||
}> {
|
||||
static displayName = 'FooterControls';
|
||||
|
||||
static defaultProps = {
|
||||
footerComponents: false,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.footerComponents) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<div className="footer-controls">
|
||||
<div className="spacer" style={{ order: 0, flex: 1 }}>
|
||||
|
||||
</div>
|
||||
{this.props.footerComponents}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,18 +2,13 @@ import React from 'react';
|
|||
import { Utils } from 'mailspring-exports';
|
||||
import { RetinaImg } from 'mailspring-component-kit';
|
||||
|
||||
export default class HeaderControls extends React.Component<{
|
||||
export class HeaderControls extends React.Component<{
|
||||
title: string;
|
||||
headerComponents: React.ReactNode;
|
||||
nextAction: () => void;
|
||||
prevAction: () => void;
|
||||
}> {
|
||||
static displayName = 'HeaderControls';
|
||||
|
||||
static defaultProps = {
|
||||
headerComonents: false,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);
|
||||
}
|
||||
|
@ -48,7 +43,7 @@ export default class HeaderControls extends React.Component<{
|
|||
<span className="title">{this.props.title}</span>
|
||||
{this._renderNextAction()}
|
||||
</div>
|
||||
{this.props.headerComponents}
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,90 +1,74 @@
|
|||
import moment, { Moment } from 'moment';
|
||||
import React from 'react';
|
||||
import { Rx, DatabaseStore, AccountStore, Calendar, Account, Event } from 'mailspring-exports';
|
||||
import { ScrollRegion, ResizableRegion } from 'mailspring-component-kit';
|
||||
import WeekView from './week-view';
|
||||
import MonthView from './month-view';
|
||||
import EventSearchBar from './event-search-bar';
|
||||
import CalendarSourceList from './calendar-source-list';
|
||||
import CalendarDataSource from './calendar-data-source';
|
||||
import { WEEK_VIEW, MONTH_VIEW } from './calendar-constants';
|
||||
import MiniMonthView from './mini-month-view';
|
||||
import {
|
||||
Rx,
|
||||
DatabaseStore,
|
||||
AccountStore,
|
||||
Calendar,
|
||||
Account,
|
||||
Actions,
|
||||
localized,
|
||||
DestroyModelTask,
|
||||
} from 'mailspring-exports';
|
||||
import {
|
||||
ScrollRegion,
|
||||
ResizableRegion,
|
||||
KeyCommandsRegion,
|
||||
MiniMonthView,
|
||||
} from 'mailspring-component-kit';
|
||||
import { WeekView } from './week-view';
|
||||
import { MonthView } from './month-view';
|
||||
import { EventSearchBar } from './event-search-bar';
|
||||
import { CalendarSourceList } from './calendar-source-list';
|
||||
import { CalendarDataSource, EventOccurrence } from './calendar-data-source';
|
||||
import { CalendarView } from './calendar-constants';
|
||||
import { Disposable } from 'rx-core';
|
||||
import { CalendarEventArgs } from './calendar-event-container';
|
||||
import { CalendarEventPopover } from './calendar-event-popover';
|
||||
import { remote } from 'electron';
|
||||
|
||||
const DISABLED_CALENDARS = 'mailspring.disabledCalendars';
|
||||
|
||||
const VIEWS = {
|
||||
[CalendarView.WEEK]: WeekView,
|
||||
[CalendarView.MONTH]: MonthView,
|
||||
};
|
||||
|
||||
export interface EventRendererProps {
|
||||
focusedEvent: EventOccurrence;
|
||||
selectedEvents: EventOccurrence[];
|
||||
onEventClick: (e: React.MouseEvent<any>, event: EventOccurrence) => void;
|
||||
onEventDoubleClick: (event: EventOccurrence) => void;
|
||||
onEventFocused: (event: EventOccurrence) => void;
|
||||
}
|
||||
|
||||
export interface MailspringCalendarViewProps extends EventRendererProps {
|
||||
dataSource: CalendarDataSource;
|
||||
disabledCalendars: string[];
|
||||
focusedMoment: Moment;
|
||||
onChangeView: (view: CalendarView) => void;
|
||||
onChangeFocusedMoment: (moment: Moment) => void;
|
||||
onCalendarMouseUp: (args: CalendarEventArgs) => void;
|
||||
onCalendarMouseDown: (args: CalendarEventArgs) => void;
|
||||
onCalendarMouseMove: (args: CalendarEventArgs) => void;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mailspring Calendar
|
||||
*/
|
||||
interface MailspringCalendarProps {
|
||||
/*
|
||||
* The data source that powers all of the views of the MailspringCalendar
|
||||
*/
|
||||
dataSource: CalendarDataSource;
|
||||
|
||||
currentMoment?: Moment;
|
||||
|
||||
/*
|
||||
* Any extra info you want to display on the top banner of calendar
|
||||
* components
|
||||
*/
|
||||
bannerComponents?: {
|
||||
day: React.ReactChild;
|
||||
week: React.ReactChild;
|
||||
month: React.ReactChild;
|
||||
year: React.ReactChild;
|
||||
};
|
||||
|
||||
/*
|
||||
* Any extra header components for each of the supported View types of
|
||||
* the MailspringCalendar
|
||||
*/
|
||||
headerComponents?: {
|
||||
day: React.ReactChild;
|
||||
week: React.ReactChild;
|
||||
month: React.ReactChild;
|
||||
year: React.ReactChild;
|
||||
};
|
||||
|
||||
/*
|
||||
* Any extra footer components for each of the supported View types of
|
||||
* the MailspringCalendar
|
||||
*/
|
||||
footerComponents?: {
|
||||
day: React.ReactChild;
|
||||
week: React.ReactChild;
|
||||
month: React.ReactChild;
|
||||
year: React.ReactChild;
|
||||
};
|
||||
|
||||
/*
|
||||
* The following are a set of supported interaction handlers.
|
||||
*
|
||||
* These are passed a custom set of arguments in a single object that
|
||||
* includes the `currentView` as well as things like the `time` at the
|
||||
* click coordinate.
|
||||
*/
|
||||
onCalendarMouseUp?: () => void;
|
||||
onCalendarMouseDown?: () => void;
|
||||
onCalendarMouseMove?: () => void;
|
||||
|
||||
onEventClick: (e: React.MouseEvent, event: Event) => void;
|
||||
onEventDoubleClick: (event: Event) => void;
|
||||
onEventFocused: (event: Event) => void;
|
||||
|
||||
selectedEvents: Event[];
|
||||
}
|
||||
interface MailspringCalendarProps {}
|
||||
|
||||
interface MailspringCalendarState {
|
||||
currentView: string;
|
||||
focusedEvent: Event | null;
|
||||
view: CalendarView;
|
||||
selectedEvents: EventOccurrence[];
|
||||
focusedEvent: EventOccurrence | null;
|
||||
accounts?: Account[];
|
||||
calendars: Calendar[];
|
||||
currentMoment: Moment;
|
||||
focusedMoment: Moment;
|
||||
disabledCalendars: string[];
|
||||
}
|
||||
|
||||
export default class MailspringCalendar extends React.Component<
|
||||
export class MailspringCalendar extends React.Component<
|
||||
MailspringCalendarProps,
|
||||
MailspringCalendarState
|
||||
> {
|
||||
|
@ -92,26 +76,21 @@ export default class MailspringCalendar extends React.Component<
|
|||
|
||||
static WeekView = WeekView;
|
||||
|
||||
static defaultProps = {
|
||||
bannerComponents: { day: false, week: false, month: false, year: false },
|
||||
headerComponents: { day: false, week: false, month: false, year: false },
|
||||
footerComponents: { day: false, week: false, month: false, year: false },
|
||||
selectedEvents: [],
|
||||
};
|
||||
|
||||
static containerStyles = {
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
_disposable?: Disposable;
|
||||
_dataSource = new CalendarDataSource();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
calendars: [],
|
||||
focusedEvent: null,
|
||||
currentView: WEEK_VIEW,
|
||||
currentMoment: props.currentMoment || this._now(),
|
||||
selectedEvents: [],
|
||||
view: CalendarView.WEEK,
|
||||
focusedMoment: moment(),
|
||||
disabledCalendars: AppEnv.config.get(DISABLED_CALENDARS) || [],
|
||||
};
|
||||
}
|
||||
|
@ -129,49 +108,109 @@ export default class MailspringCalendar extends React.Component<
|
|||
const calQueryObs = Rx.Observable.fromQuery(calQuery);
|
||||
const accQueryObs = Rx.Observable.fromStore(AccountStore);
|
||||
const configObs = Rx.Observable.fromConfig<string[] | undefined>(DISABLED_CALENDARS);
|
||||
return Rx.Observable.combineLatest<any>([calQueryObs, accQueryObs, configObs]).subscribe(
|
||||
([calendars, accountStore, disabledCalendars]: [Calendar[], any, string[] | undefined]) => {
|
||||
|
||||
return Rx.Observable.combineLatest(calQueryObs, accQueryObs, configObs).subscribe(
|
||||
([calendars, accountStore, disabledCalendars]) => {
|
||||
this.setState({
|
||||
accounts: accountStore.accounts() as Account[],
|
||||
calendars: calendars,
|
||||
accounts: accountStore.accounts(),
|
||||
disabledCalendars: disabledCalendars || [],
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_now() {
|
||||
return moment();
|
||||
onChangeView = (view: CalendarView) => {
|
||||
this.setState({ view });
|
||||
};
|
||||
|
||||
onChangeFocusedMoment = (focusedMoment: Moment) => {
|
||||
this.setState({ focusedMoment, focusedEvent: null });
|
||||
};
|
||||
|
||||
_focusEvent = (event: EventOccurrence) => {
|
||||
this.setState({ focusedMoment: moment(event.start * 1000), focusedEvent: event });
|
||||
};
|
||||
|
||||
_openEventPopover(eventModel: EventOccurrence) {
|
||||
const eventEl = document.getElementById(eventModel.id);
|
||||
if (!eventEl) {
|
||||
return;
|
||||
}
|
||||
Actions.openPopover(<CalendarEventPopover event={eventModel} />, {
|
||||
originRect: eventEl.getBoundingClientRect(),
|
||||
direction: 'right',
|
||||
fallbackDirection: 'left',
|
||||
});
|
||||
}
|
||||
|
||||
_getCurrentViewComponent() {
|
||||
const components = {};
|
||||
components[WEEK_VIEW] = WeekView;
|
||||
components[MONTH_VIEW] = MonthView;
|
||||
return components[this.state.currentView];
|
||||
}
|
||||
_onEventClick = (e: React.MouseEvent, event: EventOccurrence) => {
|
||||
let next = [...this.state.selectedEvents];
|
||||
|
||||
_changeCurrentView = currentView => {
|
||||
this.setState({ currentView });
|
||||
if (e.shiftKey || e.metaKey) {
|
||||
const idx = next.findIndex(({ id }) => event.id === id);
|
||||
if (idx === -1) {
|
||||
next.push(event);
|
||||
} else {
|
||||
next.splice(idx, 1);
|
||||
}
|
||||
} else {
|
||||
next = [event];
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedEvents: next,
|
||||
});
|
||||
};
|
||||
|
||||
_changeCurrentMoment = currentMoment => {
|
||||
this.setState({ currentMoment, focusedEvent: null });
|
||||
_onEventDoubleClick = (occurrence: EventOccurrence) => {
|
||||
this._openEventPopover(occurrence);
|
||||
};
|
||||
|
||||
_changeCurrentMomentFromValue = value => {
|
||||
this.setState({ currentMoment: moment(value), focusedEvent: null });
|
||||
_onEventFocused = (occurrence: EventOccurrence) => {
|
||||
this._openEventPopover(occurrence);
|
||||
};
|
||||
|
||||
_focusEvent = event => {
|
||||
const value = event.start * 1000;
|
||||
this.setState({ currentMoment: moment(value), focusedEvent: event });
|
||||
_onDeleteSelectedEvents = () => {
|
||||
if (this.state.selectedEvents.length === 0) {
|
||||
return;
|
||||
}
|
||||
const response = remote.dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
buttons: [localized('Delete'), localized('Cancel')],
|
||||
message: localized('Delete or decline these events?'),
|
||||
detail: localized(
|
||||
`Are you sure you want to delete or decline invitations for the selected event(s)?`
|
||||
),
|
||||
});
|
||||
if (response === 0) {
|
||||
// response is button array index
|
||||
for (const event of this.state.selectedEvents) {
|
||||
const task = new DestroyModelTask({
|
||||
modelId: event.id,
|
||||
modelName: event.constructor.name,
|
||||
endpoint: '/events',
|
||||
accountId: event.accountId,
|
||||
});
|
||||
Actions.queueTask(task);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onCalendarMouseDown = () => {};
|
||||
_onCalendarMouseMove = () => {};
|
||||
_onCalendarMouseUp = () => {};
|
||||
|
||||
render() {
|
||||
const CurrentView = this._getCurrentViewComponent();
|
||||
const CurrentView = VIEWS[this.state.view];
|
||||
|
||||
return (
|
||||
<div className="mailspring-calendar">
|
||||
<KeyCommandsRegion
|
||||
className="mailspring-calendar"
|
||||
localHandlers={{
|
||||
'core:remove-from-view': this._onDeleteSelectedEvents,
|
||||
}}
|
||||
>
|
||||
<ResizableRegion
|
||||
className="calendar-source-list"
|
||||
initialWidth={200}
|
||||
|
@ -192,31 +231,25 @@ export default class MailspringCalendar extends React.Component<
|
|||
/>
|
||||
</ScrollRegion>
|
||||
<div style={{ width: '100%' }}>
|
||||
<MiniMonthView
|
||||
value={this.state.currentMoment.valueOf()}
|
||||
onChange={this._changeCurrentMomentFromValue}
|
||||
/>
|
||||
<MiniMonthView value={this.state.focusedMoment} onChange={this.onChangeFocusedMoment} />
|
||||
</div>
|
||||
</ResizableRegion>
|
||||
<CurrentView
|
||||
dataSource={this.props.dataSource}
|
||||
currentMoment={this.state.currentMoment}
|
||||
dataSource={this._dataSource}
|
||||
focusedMoment={this.state.focusedMoment}
|
||||
focusedEvent={this.state.focusedEvent}
|
||||
bannerComponents={this.props.bannerComponents[this.state.currentView]}
|
||||
headerComponents={this.props.headerComponents[this.state.currentView]}
|
||||
footerComponents={this.props.footerComponents[this.state.currentView]}
|
||||
changeCurrentView={this._changeCurrentView}
|
||||
selectedEvents={this.state.selectedEvents}
|
||||
disabledCalendars={this.state.disabledCalendars}
|
||||
changeCurrentMoment={this._changeCurrentMoment}
|
||||
onCalendarMouseUp={this.props.onCalendarMouseUp}
|
||||
onCalendarMouseDown={this.props.onCalendarMouseDown}
|
||||
onCalendarMouseMove={this.props.onCalendarMouseMove}
|
||||
selectedEvents={this.props.selectedEvents}
|
||||
onEventClick={this.props.onEventClick}
|
||||
onEventDoubleClick={this.props.onEventDoubleClick}
|
||||
onEventFocused={this.props.onEventFocused}
|
||||
onChangeView={this.onChangeView}
|
||||
onChangeFocusedMoment={this.onChangeFocusedMoment}
|
||||
onCalendarMouseUp={this._onCalendarMouseUp}
|
||||
onCalendarMouseDown={this._onCalendarMouseDown}
|
||||
onCalendarMouseMove={this._onCalendarMouseMove}
|
||||
onEventClick={this._onEventClick}
|
||||
onEventDoubleClick={this._onEventDoubleClick}
|
||||
onEventFocused={this._onEventFocused}
|
||||
/>
|
||||
</div>
|
||||
</KeyCommandsRegion>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,138 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class MiniMonthView extends React.Component<
|
||||
{ value: number; onChange: (val: number) => void },
|
||||
{ shownYear: number; shownMonth: number }
|
||||
> {
|
||||
static displayName = 'MiniMonthView';
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: moment().valueOf(),
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
today = moment();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this._stateFromProps(props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
this.setState(this._stateFromProps(newProps));
|
||||
}
|
||||
|
||||
_stateFromProps(props) {
|
||||
const m = props.value ? moment(props.value) : moment();
|
||||
return {
|
||||
shownYear: m.year(),
|
||||
shownMonth: m.month(),
|
||||
};
|
||||
}
|
||||
|
||||
_shownMonthMoment() {
|
||||
return moment([this.state.shownYear, this.state.shownMonth]);
|
||||
}
|
||||
|
||||
_changeMonth = by => {
|
||||
const newMonth = this.state.shownMonth + by;
|
||||
const newMoment = this._shownMonthMoment().month(newMonth);
|
||||
this.setState({
|
||||
shownYear: newMoment.year(),
|
||||
shownMonth: newMoment.month(),
|
||||
});
|
||||
};
|
||||
|
||||
_renderLegend() {
|
||||
const weekdayGen = moment([2016]);
|
||||
const legendEls = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const dayStr = weekdayGen.weekday(i).format('dd'); // Locale aware!
|
||||
legendEls.push(
|
||||
<span key={i} className="weekday">
|
||||
{dayStr}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <div className="legend">{legendEls}</div>;
|
||||
}
|
||||
|
||||
_onClickDay = event => {
|
||||
if (!event.target.dataset.timestamp) {
|
||||
return;
|
||||
}
|
||||
const newVal = moment(parseInt(event.target.dataset.timestamp, 10)).valueOf();
|
||||
this.props.onChange(newVal);
|
||||
};
|
||||
|
||||
_isSameDay(m1, m2) {
|
||||
return m1.dayOfYear() === m2.dayOfYear() && m1.year() === m2.year();
|
||||
}
|
||||
|
||||
_renderDays() {
|
||||
const dayIter = this._shownMonthMoment().date(1);
|
||||
const startWeek = dayIter.week();
|
||||
const curMonth = this.state.shownMonth;
|
||||
const endWeek = moment(dayIter)
|
||||
.date(dayIter.daysInMonth())
|
||||
.week();
|
||||
const weekEls = [];
|
||||
const valDay = moment(this.props.value);
|
||||
for (let week = startWeek; week <= endWeek; week++) {
|
||||
dayIter.week(week); // Locale aware!
|
||||
const dayEls = [];
|
||||
for (let weekday = 0; weekday < 7; weekday++) {
|
||||
dayIter.weekday(weekday); // Locale aware!
|
||||
const dayStr = dayIter.format('D');
|
||||
const className = classnames({
|
||||
day: true,
|
||||
today: this._isSameDay(dayIter, this.today),
|
||||
'cur-day': this._isSameDay(dayIter, valDay),
|
||||
'cur-month': dayIter.month() === curMonth,
|
||||
});
|
||||
dayEls.push(
|
||||
<div className={className} key={`${week}-${weekday}`} data-timestamp={dayIter.valueOf()}>
|
||||
{dayStr}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
weekEls.push(
|
||||
<div className="week" key={week}>
|
||||
{dayEls}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="day-grid" onClick={this._onClickDay}>
|
||||
{weekEls}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mini-month-view">
|
||||
<div className="header">
|
||||
<div className="btn btn-icon" onClick={_.partial(this._changeMonth, -1)}>
|
||||
‹
|
||||
</div>
|
||||
<span className="month-title">{this._shownMonthMoment().format('MMMM YYYY')}</span>
|
||||
<div className="btn btn-icon" onClick={_.partial(this._changeMonth, 1)}>
|
||||
›
|
||||
</div>
|
||||
</div>
|
||||
{this._renderLegend()}
|
||||
{this._renderDays()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MailspringCalendarViewProps } from './mailspring-calendar';
|
||||
import { CalendarView } from './calendar-constants';
|
||||
|
||||
export default class MonthView extends React.Component<{ changeView: (view: string) => void }> {
|
||||
export class MonthView extends React.Component<MailspringCalendarViewProps> {
|
||||
static displayName = 'MonthView';
|
||||
|
||||
_onClick = () => {
|
||||
this.props.changeView('WeekView');
|
||||
this.props.onChangeView(CalendarView.WEEK);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class TopBanner extends React.Component<{ bannerComponents: React.ReactNode }> {
|
||||
static displayName = 'TopBanner';
|
||||
|
||||
static propTypes = {
|
||||
bannerComponents: PropTypes.node,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.bannerComponents) {
|
||||
return false;
|
||||
}
|
||||
return <div className="top-banner">{this.props.bannerComponents}</div>;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Event, Utils } from 'mailspring-exports';
|
||||
import CalendarEvent from './calendar-event';
|
||||
import { CalendarEvent } from './calendar-event';
|
||||
import { EventOccurrence } from './calendar-data-source';
|
||||
import { OverlapByEventId } from './week-view-helpers';
|
||||
import { EventRendererProps } from './mailspring-calendar';
|
||||
|
||||
/*
|
||||
* Displays the all day events across the top bar of the week event view.
|
||||
|
@ -9,16 +12,16 @@ import CalendarEvent from './calendar-event';
|
|||
* we can use `shouldComponentUpdate` to selectively re-render these
|
||||
* events.
|
||||
*/
|
||||
interface WeekViewAllDayEventsProps {
|
||||
interface WeekViewAllDayEventsProps extends EventRendererProps {
|
||||
end: number;
|
||||
start: number;
|
||||
height: number;
|
||||
minorDim: number;
|
||||
allDayEvents: Event[];
|
||||
allDayOverlap: any;
|
||||
allDayEvents: EventOccurrence[];
|
||||
allDayOverlap: OverlapByEventId;
|
||||
}
|
||||
|
||||
export default class WeekViewAllDayEvents extends React.Component<WeekViewAllDayEventsProps> {
|
||||
export class WeekViewAllDayEvents extends React.Component<WeekViewAllDayEventsProps> {
|
||||
static displayName = 'WeekViewAllDayEvents';
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
@ -26,23 +29,27 @@ export default class WeekViewAllDayEvents extends React.Component<WeekViewAllDay
|
|||
}
|
||||
|
||||
render() {
|
||||
const eventComponents = this.props.allDayEvents.map(e => {
|
||||
return (
|
||||
<CalendarEvent
|
||||
event={e}
|
||||
order={this.props.allDayOverlap[e.id].order}
|
||||
key={e.id}
|
||||
scopeStart={this.props.start}
|
||||
scopeEnd={this.props.end}
|
||||
direction="horizontal"
|
||||
fixedSize={this.props.minorDim}
|
||||
concurrentEvents={this.props.allDayOverlap[e.id].concurrentEvents}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const { height, allDayEvents, allDayOverlap, selectedEvents, focusedEvent } = this.props;
|
||||
|
||||
return (
|
||||
<div className="all-day-events" style={{ height: this.props.height }}>
|
||||
{eventComponents}
|
||||
<div className="all-day-events" style={{ height: height }}>
|
||||
{allDayEvents.map(e => (
|
||||
<CalendarEvent
|
||||
event={e}
|
||||
order={allDayOverlap[e.id].order}
|
||||
key={e.id}
|
||||
selected={selectedEvents.includes(e)}
|
||||
focused={focusedEvent ? focusedEvent.id === e.id : false}
|
||||
scopeStart={this.props.start}
|
||||
scopeEnd={this.props.end}
|
||||
direction="horizontal"
|
||||
fixedSize={this.props.minorDim}
|
||||
concurrentEvents={allDayOverlap[e.id].concurrentEvents}
|
||||
onClick={this.props.onEventClick}
|
||||
onDoubleClick={this.props.onEventDoubleClick}
|
||||
onFocused={this.props.onEventFocused}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import moment, { Moment } from 'moment';
|
||||
import classnames from 'classnames';
|
||||
import { PropTypes, Utils, Event } from 'mailspring-exports';
|
||||
import CalendarEvent from './calendar-event';
|
||||
import { Utils, Event } from 'mailspring-exports';
|
||||
import { CalendarEvent } from './calendar-event';
|
||||
import { EventOccurrence } from './calendar-data-source';
|
||||
import { overlapForEvents } from './week-view-helpers';
|
||||
|
||||
/*
|
||||
* This display a single column of events in the Week View.
|
||||
|
@ -11,71 +13,63 @@ import CalendarEvent from './calendar-event';
|
|||
* column-by-column basis.
|
||||
*/
|
||||
interface WeekViewEventColumnProps {
|
||||
events: Event[];
|
||||
events: EventOccurrence[];
|
||||
day: Moment;
|
||||
dayEnd: number;
|
||||
focusedEvent: Event;
|
||||
eventOverlap: any;
|
||||
onEventClick: () => void;
|
||||
onEventDoubleClick: () => void;
|
||||
onEventFocused: () => void;
|
||||
selectedEvents: Event[];
|
||||
focusedEvent: EventOccurrence;
|
||||
onEventClick: (e: React.MouseEvent<any>, event: EventOccurrence) => void;
|
||||
onEventDoubleClick: (event: EventOccurrence) => void;
|
||||
onEventFocused: (event: EventOccurrence) => void;
|
||||
selectedEvents: EventOccurrence[];
|
||||
}
|
||||
|
||||
export default class WeekViewEventColumn extends React.Component<WeekViewEventColumnProps> {
|
||||
export class WeekViewEventColumn extends React.Component<WeekViewEventColumnProps> {
|
||||
static displayName = 'WeekViewEventColumn';
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);
|
||||
}
|
||||
|
||||
renderEvents() {
|
||||
render() {
|
||||
const {
|
||||
events,
|
||||
focusedEvent,
|
||||
selectedEvents,
|
||||
eventOverlap,
|
||||
dayEnd,
|
||||
day,
|
||||
onEventClick,
|
||||
onEventDoubleClick,
|
||||
onEventFocused,
|
||||
} = this.props;
|
||||
return events.map(e => (
|
||||
<CalendarEvent
|
||||
ref={`event-${e.id}`}
|
||||
event={e}
|
||||
selected={selectedEvents.includes(e)}
|
||||
order={eventOverlap[e.id].order}
|
||||
focused={focusedEvent ? focusedEvent.id === e.id : false}
|
||||
key={e.id}
|
||||
scopeEnd={dayEnd}
|
||||
scopeStart={day.unix()}
|
||||
concurrentEvents={eventOverlap[e.id].concurrentEvents}
|
||||
onClick={onEventClick}
|
||||
onDoubleClick={onEventDoubleClick}
|
||||
onFocused={onEventFocused}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const className = classnames({
|
||||
'event-column': true,
|
||||
weekend: this.props.day.day() === 0 || this.props.day.day() === 6,
|
||||
weekend: day.day() === 0 || day.day() === 6,
|
||||
});
|
||||
const end = moment(this.props.day)
|
||||
const overlap = overlapForEvents(events);
|
||||
const end = moment(day)
|
||||
.add(1, 'day')
|
||||
.subtract(1, 'millisecond')
|
||||
.valueOf();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
key={this.props.day.valueOf()}
|
||||
data-start={this.props.day.valueOf()}
|
||||
data-end={end}
|
||||
>
|
||||
{this.renderEvents()}
|
||||
<div className={className} key={day.valueOf()} data-start={day.valueOf()} data-end={end}>
|
||||
{events.map(e => (
|
||||
<CalendarEvent
|
||||
ref={`event-${e.id}`}
|
||||
event={e}
|
||||
selected={selectedEvents.includes(e)}
|
||||
order={overlap[e.id].order}
|
||||
focused={focusedEvent ? focusedEvent.id === e.id : false}
|
||||
key={e.id}
|
||||
scopeEnd={dayEnd}
|
||||
scopeStart={day.unix()}
|
||||
concurrentEvents={overlap[e.id].concurrentEvents}
|
||||
onClick={onEventClick}
|
||||
onDoubleClick={onEventDoubleClick}
|
||||
onFocused={onEventFocused}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import { EventOccurrence } from './calendar-data-source';
|
||||
import moment, { Moment } from 'moment';
|
||||
import { Utils } from 'mailspring-exports';
|
||||
|
||||
// This pre-fetches from Utils to prevent constant disc access
|
||||
const overlapsBounds = Utils.overlapsBounds;
|
||||
|
||||
export interface OverlapByEventId {
|
||||
[id: string]: { concurrentEvents: number; order: null | number };
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes the overlap between a set of events in not O(n^2).
|
||||
*
|
||||
* Returns a hash keyed by event id whose value is an object:
|
||||
* - concurrentEvents: number of concurrent events
|
||||
* - order: the order in that series of concurrent events
|
||||
*/
|
||||
export function overlapForEvents(events: EventOccurrence[]) {
|
||||
const eventsByTime: { [unix: number]: EventOccurrence[] } = {};
|
||||
|
||||
for (const event of events) {
|
||||
if (!eventsByTime[event.start]) {
|
||||
eventsByTime[event.start] = [];
|
||||
}
|
||||
if (!eventsByTime[event.end]) {
|
||||
eventsByTime[event.end] = [];
|
||||
}
|
||||
eventsByTime[event.start].push(event);
|
||||
eventsByTime[event.end].push(event);
|
||||
}
|
||||
const sortedTimes = Object.keys(eventsByTime)
|
||||
.map(Number)
|
||||
.sort();
|
||||
|
||||
const overlapById: OverlapByEventId = {};
|
||||
let ongoingEvents: EventOccurrence[] = [];
|
||||
|
||||
for (const t of sortedTimes) {
|
||||
// Process all event start/ends during this time to keep our
|
||||
// "ongoingEvents" set correct.
|
||||
for (const e of eventsByTime[t]) {
|
||||
if (e.start === t) {
|
||||
overlapById[e.id] = { concurrentEvents: 1, order: null };
|
||||
ongoingEvents.push(e);
|
||||
}
|
||||
if (e.end === t) {
|
||||
ongoingEvents = ongoingEvents.filter(o => o.id !== e.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Write concurrency for all the events currently ongoing if they haven't
|
||||
// been assigned values already
|
||||
for (const e of ongoingEvents) {
|
||||
const numEvents = findMaxConcurrent(ongoingEvents, overlapById);
|
||||
overlapById[e.id].concurrentEvents = numEvents;
|
||||
if (overlapById[e.id].order === null) {
|
||||
// Don't re-assign the order.
|
||||
const order = findAvailableOrder(ongoingEvents, overlapById);
|
||||
overlapById[e.id].order = order;
|
||||
}
|
||||
}
|
||||
}
|
||||
return overlapById;
|
||||
}
|
||||
|
||||
export function findMaxConcurrent(ongoing: EventOccurrence[], overlapById: OverlapByEventId) {
|
||||
return Math.max(1, ongoing.length, ...ongoing.map(e => overlapById[e.id].concurrentEvents));
|
||||
}
|
||||
|
||||
export function findAvailableOrder(ongoing: EventOccurrence[], overlapById: OverlapByEventId) {
|
||||
const orders = ongoing.map(e => overlapById[e.id].order);
|
||||
let order = 1;
|
||||
while (true) {
|
||||
if (!orders.includes(order)) {
|
||||
return order;
|
||||
}
|
||||
order += 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function maxConcurrentEvents(eventOverlap: OverlapByEventId) {
|
||||
return Math.max(-1, ...Object.values(eventOverlap).map(o => o.concurrentEvents));
|
||||
}
|
||||
|
||||
export function eventsGroupedByDay(events: EventOccurrence[], days: Moment[]) {
|
||||
const map: { allDay: EventOccurrence[]; [dayUnix: string]: EventOccurrence[] } = { allDay: [] };
|
||||
|
||||
const unixDays = days.map(d => d.unix());
|
||||
unixDays.forEach(day => {
|
||||
map[`${day}`] = [];
|
||||
});
|
||||
|
||||
events.forEach(event => {
|
||||
if (event.isAllDay) {
|
||||
map.allDay.push(event);
|
||||
} else {
|
||||
for (const day of unixDays) {
|
||||
const bounds = {
|
||||
start: day,
|
||||
end: day + 24 * 60 * 60 - 1,
|
||||
};
|
||||
if (overlapsBounds(bounds, event)) {
|
||||
map[`${day}`].push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export const DAY_DUR = 24 * 60 * 60;
|
||||
export const TICK_STEP = 30 * 60;
|
||||
export const TICKS_PER_DAY = DAY_DUR / TICK_STEP;
|
||||
|
||||
export function* tickGenerator(type: 'major' | 'minor', tickHeight: number) {
|
||||
const step = TICK_STEP * 2;
|
||||
const skip = TICK_STEP * 2;
|
||||
const stepStart = type === 'minor' ? TICK_STEP : 0;
|
||||
|
||||
// We only use a moment object so we can properly localize the "time"
|
||||
// part. The day is irrelevant. We just need to make sure we're
|
||||
// picking a non-DST boundary day.
|
||||
const time = moment([2015, 1, 1]).add(stepStart, 'seconds');
|
||||
|
||||
for (let tsec = stepStart; tsec <= DAY_DUR; tsec += step) {
|
||||
const y = (tsec / TICK_STEP) * tickHeight;
|
||||
yield { time, y };
|
||||
time.add(skip, 'seconds');
|
||||
}
|
||||
}
|
|
@ -3,65 +3,43 @@ import _ from 'underscore';
|
|||
import moment, { Moment } from 'moment-timezone';
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { PropTypes, Utils, Event } from 'mailspring-exports';
|
||||
import { ScrollRegion } from 'mailspring-component-kit';
|
||||
import TopBanner from './top-banner';
|
||||
import HeaderControls from './header-controls';
|
||||
import FooterControls from './footer-controls';
|
||||
import CalendarDataSource from './calendar-data-source';
|
||||
import EventGridBackground from './event-grid-background';
|
||||
import WeekViewEventColumn from './week-view-event-column';
|
||||
import WeekViewAllDayEvents from './week-view-all-day-events';
|
||||
import CalendarEventContainer from './calendar-event-container';
|
||||
import CurrentTimeIndicator from './current-time-indicator';
|
||||
import { ScrollRegion, InjectedComponentSet } from 'mailspring-component-kit';
|
||||
import { HeaderControls } from './header-controls';
|
||||
import { EventOccurrence } from './calendar-data-source';
|
||||
import { EventGridBackground } from './event-grid-background';
|
||||
import { WeekViewEventColumn } from './week-view-event-column';
|
||||
import { WeekViewAllDayEvents } from './week-view-all-day-events';
|
||||
import { CalendarEventContainer } from './calendar-event-container';
|
||||
import { CurrentTimeIndicator } from './current-time-indicator';
|
||||
import { Disposable } from 'rx-core';
|
||||
import {
|
||||
overlapForEvents,
|
||||
maxConcurrentEvents,
|
||||
eventsGroupedByDay,
|
||||
TICKS_PER_DAY,
|
||||
tickGenerator,
|
||||
} from './week-view-helpers';
|
||||
import { MailspringCalendarViewProps } from './mailspring-calendar';
|
||||
|
||||
const BUFFER_DAYS = 7; // in each direction
|
||||
const DAYS_IN_VIEW = 7;
|
||||
const MIN_INTERVAL_HEIGHT = 21;
|
||||
const DAY_DUR = moment.duration(1, 'day').as('seconds');
|
||||
const INTERVAL_TIME = moment.duration(30, 'minutes').as('seconds');
|
||||
const DAY_PORTION_SHOWN_VERTICALLY = 11 / 24;
|
||||
|
||||
// This pre-fetches from Utils to prevent constant disc access
|
||||
const overlapsBounds = Utils.overlapsBounds;
|
||||
|
||||
interface WeekViewProps {
|
||||
dataSource: CalendarDataSource;
|
||||
currentMoment: Moment;
|
||||
focusedEvent: Event;
|
||||
bannerComponents: React.ReactChildren;
|
||||
headerComponents: React.ReactChildren;
|
||||
footerComponents: React.ReactChildren;
|
||||
disabledCalendars: string[];
|
||||
changeCurrentView: () => void;
|
||||
changeCurrentMoment: (moment: Moment) => void;
|
||||
onCalendarMouseUp: () => void;
|
||||
onCalendarMouseDown: () => void;
|
||||
onCalendarMouseMove: () => void;
|
||||
onEventClick: () => void;
|
||||
onEventDoubleClick: () => void;
|
||||
onEventFocused: () => void;
|
||||
selectedEvents: Event[];
|
||||
}
|
||||
|
||||
export default class WeekView extends React.Component<
|
||||
WeekViewProps,
|
||||
{ intervalHeight: number; events: Event[] }
|
||||
export class WeekView extends React.Component<
|
||||
MailspringCalendarViewProps,
|
||||
{ intervalHeight: number; events: EventOccurrence[] }
|
||||
> {
|
||||
static displayName = 'WeekView';
|
||||
|
||||
static defaultProps = {
|
||||
changeCurrentView: () => {},
|
||||
bannerComponents: false,
|
||||
headerComponents: false,
|
||||
footerComponents: false,
|
||||
};
|
||||
|
||||
_waitingForShift = 0;
|
||||
_mounted = false;
|
||||
_scrollbar = React.createRef<any>();
|
||||
_sub?: Disposable;
|
||||
_lastWrapHeight: number;
|
||||
|
||||
_legendWrapEl = React.createRef<HTMLDivElement>();
|
||||
_calendarWrapEl = React.createRef<HTMLDivElement>();
|
||||
_gridScrollRegion = React.createRef<ScrollRegion>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -74,20 +52,23 @@ export default class WeekView extends React.Component<
|
|||
componentDidMount() {
|
||||
this._mounted = true;
|
||||
this._centerScrollRegion();
|
||||
this._setIntervalHeight();
|
||||
window.addEventListener('resize', this._setIntervalHeight, true);
|
||||
const wrap = ReactDOM.findDOMNode(this.refs.calendarAreaWrap) as HTMLElement;
|
||||
|
||||
// Shift ourselves right by a week because we preload 7 days on either side
|
||||
const wrap = this._calendarWrapEl.current;
|
||||
wrap.scrollLeft += wrap.clientWidth;
|
||||
|
||||
this.updateSubscription();
|
||||
this._setIntervalHeight();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this._setIntervalHeight();
|
||||
const wrap = ReactDOM.findDOMNode(this.refs.calendarAreaWrap) as HTMLElement;
|
||||
wrap.scrollLeft += this._waitingForShift;
|
||||
this._waitingForShift = 0;
|
||||
if (this._waitingForShift) {
|
||||
const wrap = this._calendarWrapEl.current;
|
||||
wrap.scrollLeft += this._waitingForShift;
|
||||
this._waitingForShift = 0;
|
||||
}
|
||||
if (
|
||||
prevProps.currentMoment !== this.props.currentMoment ||
|
||||
prevProps.focusedMoment !== this.props.focusedMoment ||
|
||||
prevProps.disabledCalendars !== this.props.disabledCalendars
|
||||
) {
|
||||
this.updateSubscription();
|
||||
|
@ -96,8 +77,7 @@ export default class WeekView extends React.Component<
|
|||
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
this._sub.dispose();
|
||||
window.removeEventListener('resize', this._setIntervalHeight);
|
||||
this._sub && this._sub.dispose();
|
||||
}
|
||||
|
||||
// Indirection for testing purposes
|
||||
|
@ -106,17 +86,14 @@ export default class WeekView extends React.Component<
|
|||
}
|
||||
|
||||
updateSubscription() {
|
||||
if (this._sub) {
|
||||
this._sub.dispose();
|
||||
}
|
||||
|
||||
const { start, end } = this._calculateMomentRange();
|
||||
const { bufferedStart, bufferedEnd } = this._calculateMomentRange();
|
||||
|
||||
this._sub && this._sub.dispose();
|
||||
this._sub = this.props.dataSource
|
||||
.buildObservable({
|
||||
disabledCalendars: this.props.disabledCalendars,
|
||||
startTime: start.unix(),
|
||||
endTime: end.unix(),
|
||||
startUnix: bufferedStart.unix(),
|
||||
endUnix: bufferedEnd.unix(),
|
||||
})
|
||||
.subscribe(state => {
|
||||
this.setState(state);
|
||||
|
@ -124,37 +101,39 @@ export default class WeekView extends React.Component<
|
|||
}
|
||||
|
||||
_calculateMomentRange() {
|
||||
const { currentMoment } = this.props;
|
||||
let start;
|
||||
const { focusedMoment } = this.props;
|
||||
|
||||
// NOTE: Since we initialize a new time from one of the properties of
|
||||
// the props.currentMomet, we need to check for the timezone!
|
||||
// the props.focusedMoment, we need to check for the timezone!
|
||||
//
|
||||
// Other relative operations (like adding or subtracting time) are
|
||||
// independent of a timezone.
|
||||
const tz = currentMoment.tz();
|
||||
if (tz) {
|
||||
start = moment.tz([currentMoment.year()], tz);
|
||||
} else {
|
||||
start = moment([currentMoment.year()]);
|
||||
}
|
||||
|
||||
start = start
|
||||
const tz = focusedMoment.tz();
|
||||
const start = (tz ? moment.tz([focusedMoment.year()], tz) : moment([focusedMoment.year()]))
|
||||
.weekday(0)
|
||||
.week(currentMoment.week())
|
||||
.subtract(BUFFER_DAYS, 'days');
|
||||
.week(focusedMoment.week());
|
||||
|
||||
const end = moment(start)
|
||||
.add(BUFFER_DAYS * 2 + DAYS_IN_VIEW, 'days')
|
||||
const end = start
|
||||
.clone()
|
||||
.add(DAYS_IN_VIEW, 'days')
|
||||
.subtract(1, 'millisecond');
|
||||
|
||||
return { start, end };
|
||||
return {
|
||||
visibleStart: start,
|
||||
visibleEnd: end,
|
||||
|
||||
bufferedStart: start.clone().subtract(BUFFER_DAYS, 'days'),
|
||||
bufferedEnd: moment(end)
|
||||
.add(BUFFER_DAYS, 'days')
|
||||
.subtract(1, 'millisecond'),
|
||||
};
|
||||
}
|
||||
|
||||
_renderDateLabel = (day, idx) => {
|
||||
_renderDateLabel = (day: Moment, idx: number) => {
|
||||
const className = classnames({
|
||||
'day-label-wrap': true,
|
||||
'is-today': this._isToday(day),
|
||||
'is-hard-stop': day.weekday() == 1,
|
||||
});
|
||||
return (
|
||||
<div className={className} key={idx}>
|
||||
|
@ -171,246 +150,79 @@ export default class WeekView extends React.Component<
|
|||
return todayDayOfYear === day.dayOfYear() && todayYear === day.year();
|
||||
}
|
||||
|
||||
_renderEventColumn = (eventsByDay, day) => {
|
||||
const dayUnix = day.unix();
|
||||
const events = eventsByDay[dayUnix];
|
||||
return (
|
||||
<WeekViewEventColumn
|
||||
day={day}
|
||||
dayEnd={dayUnix + DAY_DUR - 1}
|
||||
key={day.valueOf()}
|
||||
events={events}
|
||||
eventOverlap={this._eventOverlap(events)}
|
||||
focusedEvent={this.props.focusedEvent}
|
||||
selectedEvents={this.props.selectedEvents}
|
||||
onEventClick={this.props.onEventClick}
|
||||
onEventDoubleClick={this.props.onEventDoubleClick}
|
||||
onEventFocused={this.props.onEventFocused}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_allDayEventHeight(allDayOverlap) {
|
||||
if (_.size(allDayOverlap) === 0) {
|
||||
return 0;
|
||||
}
|
||||
return this._maxConcurrentEvents(allDayOverlap) * MIN_INTERVAL_HEIGHT + 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes the overlap between a set of events in not O(n^2).
|
||||
*
|
||||
* Returns a hash keyed by event id whose value is an object:
|
||||
* - concurrentEvents: number of concurrent events
|
||||
* - order: the order in that series of concurrent events
|
||||
*/
|
||||
_eventOverlap(events) {
|
||||
const times = {};
|
||||
for (const event of events) {
|
||||
if (!times[event.start]) {
|
||||
times[event.start] = [];
|
||||
}
|
||||
if (!times[event.end]) {
|
||||
times[event.end] = [];
|
||||
}
|
||||
times[event.start].push(event);
|
||||
times[event.end].push(event);
|
||||
}
|
||||
const sortedTimes = Object.keys(times)
|
||||
.map(k => parseInt(k, 10))
|
||||
.sort();
|
||||
const overlapById = {};
|
||||
let startedEvents = [];
|
||||
for (const t of sortedTimes) {
|
||||
for (const e of times[t]) {
|
||||
if (e.start === t) {
|
||||
overlapById[e.id] = { concurrentEvents: 1, order: null };
|
||||
startedEvents.push(e);
|
||||
}
|
||||
if (e.end === t) {
|
||||
startedEvents = _.reject(startedEvents, o => o.id === e.id);
|
||||
}
|
||||
}
|
||||
for (const e of startedEvents) {
|
||||
if (!overlapById[e.id]) {
|
||||
overlapById[e.id] = {};
|
||||
}
|
||||
const numEvents = this._findMaxConcurrent(startedEvents, overlapById);
|
||||
overlapById[e.id].concurrentEvents = numEvents;
|
||||
if (overlapById[e.id].order === null) {
|
||||
// Dont' re-assign the order.
|
||||
const order = this._findAvailableOrder(startedEvents, overlapById);
|
||||
overlapById[e.id].order = order;
|
||||
}
|
||||
}
|
||||
}
|
||||
return overlapById;
|
||||
}
|
||||
|
||||
_findMaxConcurrent(startedEvents, overlapById) {
|
||||
let max = 1;
|
||||
for (const e of startedEvents) {
|
||||
max = Math.max(overlapById[e.id].concurrentEvents || 1, max);
|
||||
}
|
||||
return Math.max(max, startedEvents.length);
|
||||
}
|
||||
|
||||
_findAvailableOrder(startedEvents, overlapById) {
|
||||
const orders = startedEvents.map(e => overlapById[e.id].order);
|
||||
let order = 1;
|
||||
while (true) {
|
||||
if (orders.indexOf(order) === -1) {
|
||||
return order;
|
||||
}
|
||||
order += 1;
|
||||
}
|
||||
}
|
||||
|
||||
_maxConcurrentEvents(eventOverlap) {
|
||||
let maxConcurrent = -1;
|
||||
_.each(eventOverlap, ({ concurrentEvents }) => {
|
||||
maxConcurrent = Math.max(concurrentEvents, maxConcurrent);
|
||||
});
|
||||
return maxConcurrent;
|
||||
}
|
||||
|
||||
_daysInView() {
|
||||
const { start } = this._calculateMomentRange();
|
||||
const days = [];
|
||||
const { bufferedStart } = this._calculateMomentRange();
|
||||
const days: Moment[] = [];
|
||||
for (let i = 0; i < DAYS_IN_VIEW + BUFFER_DAYS * 2; i++) {
|
||||
// moment::weekday is locale aware since some weeks start on diff
|
||||
// days. See http://momentjs.com/docs/#/get-set/weekday/
|
||||
days.push(moment(start).weekday(i));
|
||||
days.push(moment(bufferedStart).weekday(i));
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
_headerComponents() {
|
||||
const left = (
|
||||
<button
|
||||
key="today"
|
||||
className="btn"
|
||||
ref="todayBtn"
|
||||
onClick={this._onClickToday}
|
||||
style={{ position: 'absolute', left: 10 }}
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
);
|
||||
const right = false;
|
||||
return [left, right, this.props.headerComponents];
|
||||
}
|
||||
|
||||
_onClickToday = () => {
|
||||
this.props.changeCurrentMoment(this._now());
|
||||
this.props.onChangeFocusedMoment(this._now());
|
||||
};
|
||||
|
||||
_onClickNextWeek = () => {
|
||||
const newMoment = moment(this.props.currentMoment).add(1, 'week');
|
||||
this.props.changeCurrentMoment(newMoment);
|
||||
const newMoment = moment(this.props.focusedMoment).add(1, 'week');
|
||||
this.props.onChangeFocusedMoment(newMoment);
|
||||
};
|
||||
|
||||
_onClickPrevWeek = () => {
|
||||
const newMoment = moment(this.props.currentMoment).subtract(1, 'week');
|
||||
this.props.changeCurrentMoment(newMoment);
|
||||
const newMoment = moment(this.props.focusedMoment).subtract(1, 'week');
|
||||
this.props.onChangeFocusedMoment(newMoment);
|
||||
};
|
||||
|
||||
_gridHeight() {
|
||||
return DAY_DUR / INTERVAL_TIME * this.state.intervalHeight;
|
||||
}
|
||||
|
||||
_centerScrollRegion() {
|
||||
const wrap = ReactDOM.findDOMNode(this.refs.eventGridWrap) as HTMLElement;
|
||||
wrap.scrollTop = this._gridHeight() / 2 - wrap.getBoundingClientRect().height / 2;
|
||||
}
|
||||
|
||||
// This generates the ticks used mark the event grid and the
|
||||
// corresponding legend in the week view.
|
||||
*_tickGenerator({ type }) {
|
||||
const height = this._gridHeight();
|
||||
|
||||
let step = INTERVAL_TIME;
|
||||
let stepStart = 0;
|
||||
|
||||
// We only use a moment object so we can properly localize the "time"
|
||||
// part. The day is irrelevant. We just need to make sure we're
|
||||
// picking a non-DST boundary day.
|
||||
const start = moment([2015, 1, 1]);
|
||||
|
||||
let duration = INTERVAL_TIME;
|
||||
if (type === 'major') {
|
||||
step = INTERVAL_TIME * 2;
|
||||
duration += INTERVAL_TIME;
|
||||
} else if (type === 'minor') {
|
||||
step = INTERVAL_TIME * 2;
|
||||
stepStart = INTERVAL_TIME;
|
||||
duration += INTERVAL_TIME;
|
||||
start.add(INTERVAL_TIME, 'seconds');
|
||||
}
|
||||
|
||||
const curTime = moment(start);
|
||||
for (let tsec = stepStart; tsec <= DAY_DUR; tsec += step) {
|
||||
const y = tsec / DAY_DUR * height;
|
||||
yield { time: curTime, yPos: y };
|
||||
curTime.add(duration, 'seconds');
|
||||
}
|
||||
const wrap = this._gridScrollRegion.current.viewportEl;
|
||||
wrap.scrollTop = wrap.scrollHeight / 2 - wrap.clientHeight / 2;
|
||||
}
|
||||
|
||||
_setIntervalHeight = () => {
|
||||
if (!this._mounted) {
|
||||
return;
|
||||
} // Resize unmounting is delayed in tests
|
||||
const wrap = ReactDOM.findDOMNode(this.refs.eventGridWrap) as HTMLElement;
|
||||
const wrapHeight = wrap.getBoundingClientRect().height;
|
||||
if (this._lastWrapHeight === wrapHeight) {
|
||||
return;
|
||||
}
|
||||
this._lastWrapHeight = wrapHeight;
|
||||
const numIntervals = Math.floor(DAY_DUR / INTERVAL_TIME);
|
||||
(ReactDOM.findDOMNode(
|
||||
this.refs.eventGridLegendWrap
|
||||
) as HTMLElement).style.height = `${wrapHeight}px`;
|
||||
const viewportHeight = this._gridScrollRegion.current.viewportEl.clientHeight;
|
||||
this._legendWrapEl.current.style.height = `${viewportHeight}px`;
|
||||
|
||||
this.setState({
|
||||
intervalHeight: Math.max(wrapHeight / numIntervals, MIN_INTERVAL_HEIGHT),
|
||||
intervalHeight: Math.max(
|
||||
viewportHeight / (TICKS_PER_DAY * DAY_PORTION_SHOWN_VERTICALLY),
|
||||
MIN_INTERVAL_HEIGHT
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
_onScrollGrid = event => {
|
||||
(ReactDOM.findDOMNode(this.refs.eventGridLegendWrap) as HTMLElement).scrollTop =
|
||||
event.target.scrollTop;
|
||||
};
|
||||
_onScrollCalendarArea = (event: React.UIEvent) => {
|
||||
console.log(event.currentTarget.scrollLeft);
|
||||
// if (!event.currentTarget.scrollLeft || this._waitingForShift) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
_onScrollCalendarArea = event => {
|
||||
if (!event.currentTarget.scrollLeft || this._waitingForShift) {
|
||||
return;
|
||||
}
|
||||
// const edgeWidth = (event.currentTarget.clientWidth / DAYS_IN_VIEW) * 2;
|
||||
|
||||
const edgeWidth = event.currentTarget.clientWidth / DAYS_IN_VIEW * 2;
|
||||
|
||||
if (event.currentTarget.scrollLeft < edgeWidth) {
|
||||
this._waitingForShift = event.currentTarget.clientWidth;
|
||||
this._onClickPrevWeek();
|
||||
} else if (
|
||||
event.currentTarget.scrollLeft >
|
||||
event.currentTarget.scrollWidth - event.currentTarget.clientWidth - edgeWidth
|
||||
) {
|
||||
this._waitingForShift = -event.currentTarget.clientWidth;
|
||||
this._onClickNextWeek();
|
||||
}
|
||||
// if (event.currentTarget.scrollLeft < edgeWidth) {
|
||||
// this._waitingForShift = event.currentTarget.clientWidth;
|
||||
// this._onClickPrevWeek();
|
||||
// } else if (
|
||||
// event.currentTarget.scrollLeft >
|
||||
// event.currentTarget.scrollWidth - event.currentTarget.clientWidth - edgeWidth
|
||||
// ) {
|
||||
// this._waitingForShift = -event.currentTarget.clientWidth;
|
||||
// this._onClickNextWeek();
|
||||
// }
|
||||
};
|
||||
|
||||
_renderEventGridLabels() {
|
||||
const labels = [];
|
||||
let centering = 0;
|
||||
for (const { time, yPos } of this._tickGenerator({ type: 'major' })) {
|
||||
const hr = time.format('LT'); // Locale time. 2:00 pm or 14:00
|
||||
const style = { top: yPos - centering };
|
||||
for (const { time, y } of tickGenerator('major', this.state.intervalHeight)) {
|
||||
labels.push(
|
||||
<span className="legend-text" key={yPos} style={style}>
|
||||
{hr}
|
||||
<span className="legend-text" key={y} style={{ top: y === 0 ? y : y - 8 }}>
|
||||
{time.format('LT')}
|
||||
</span>
|
||||
);
|
||||
centering = 8; // center all except the 1st one.
|
||||
}
|
||||
return labels.slice(0, labels.length - 1);
|
||||
}
|
||||
|
@ -419,75 +231,57 @@ export default class WeekView extends React.Component<
|
|||
return (BUFFER_DAYS * 2 + DAYS_IN_VIEW) / DAYS_IN_VIEW;
|
||||
}
|
||||
|
||||
// We calculate events by days so we only need to iterate through all
|
||||
// events in the span once.
|
||||
_eventsByDay(days) {
|
||||
const map = { allDay: [] };
|
||||
const unixDays = days.map(d => d.unix());
|
||||
unixDays.forEach(d => {
|
||||
map[d] = [];
|
||||
return;
|
||||
});
|
||||
for (const event of this.state.events) {
|
||||
if (event.isAllDay) {
|
||||
map.allDay.push(event);
|
||||
} else {
|
||||
for (const day of unixDays) {
|
||||
const bounds = {
|
||||
start: day,
|
||||
end: day + DAY_DUR - 1,
|
||||
};
|
||||
if (overlapsBounds(bounds, event)) {
|
||||
map[day].push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
render() {
|
||||
const days = this._daysInView();
|
||||
const eventsByDay = eventsGroupedByDay(this.state.events, days);
|
||||
const todayColumnIdx = days.findIndex(d => this._isToday(d));
|
||||
const eventsByDay = this._eventsByDay(days);
|
||||
const allDayOverlap = this._eventOverlap(eventsByDay.allDay);
|
||||
const tickGen = this._tickGenerator.bind(this);
|
||||
const gridHeight = this._gridHeight();
|
||||
const totalHeight = TICKS_PER_DAY * this.state.intervalHeight;
|
||||
|
||||
const { start: startMoment, end: endMoment } = this._calculateMomentRange();
|
||||
const range = this._calculateMomentRange();
|
||||
|
||||
const start = moment(startMoment).add(BUFFER_DAYS, 'days');
|
||||
const end = moment(endMoment).subtract(BUFFER_DAYS, 'days');
|
||||
const headerText = `${start.format('MMMM D')} - ${end.format('MMMM D YYYY')}`;
|
||||
const headerText = [
|
||||
range.visibleStart.format('MMMM D'),
|
||||
range.visibleEnd.format('MMMM D YYYY'),
|
||||
].join(' - ');
|
||||
|
||||
const allDayOverlap = overlapForEvents(eventsByDay.allDay);
|
||||
const allDayBarHeight = eventsByDay.allDay.length
|
||||
? maxConcurrentEvents(allDayOverlap) * MIN_INTERVAL_HEIGHT + 1
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="calendar-view week-view">
|
||||
<CalendarEventContainer
|
||||
ref="calendarEventContainer"
|
||||
onCalendarMouseUp={this.props.onCalendarMouseUp}
|
||||
onCalendarMouseDown={this.props.onCalendarMouseDown}
|
||||
onCalendarMouseMove={this.props.onCalendarMouseMove}
|
||||
>
|
||||
<TopBanner bannerComponents={this.props.bannerComponents} />
|
||||
<div className="top-banner">
|
||||
<InjectedComponentSet matching={{ role: 'Calendar:Week:Banner' }} direction="row" />
|
||||
</div>
|
||||
|
||||
<HeaderControls
|
||||
title={headerText}
|
||||
ref="headerControls"
|
||||
headerComponents={this._headerComponents()}
|
||||
nextAction={this._onClickNextWeek}
|
||||
prevAction={this._onClickPrevWeek}
|
||||
/>
|
||||
>
|
||||
<button
|
||||
key="today"
|
||||
className="btn"
|
||||
onClick={this._onClickToday}
|
||||
style={{ position: 'absolute', left: 10 }}
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
</HeaderControls>
|
||||
|
||||
<div className="calendar-body-wrap">
|
||||
<div className="calendar-legend">
|
||||
<div
|
||||
className="date-label-legend"
|
||||
style={{ height: this._allDayEventHeight(allDayOverlap) + 75 + 1 }}
|
||||
>
|
||||
<div className="date-label-legend" style={{ height: allDayBarHeight + 75 + 1 }}>
|
||||
<span className="legend-text">All Day</span>
|
||||
</div>
|
||||
<div className="event-grid-legend-wrap" ref="eventGridLegendWrap">
|
||||
<div className="event-grid-legend" style={{ height: gridHeight }}>
|
||||
<div className="event-grid-legend-wrap" ref={this._legendWrapEl}>
|
||||
<div className="event-grid-legend" style={{ height: totalHeight }}>
|
||||
{this._renderEventGridLabels()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -495,56 +289,69 @@ export default class WeekView extends React.Component<
|
|||
|
||||
<div
|
||||
className="calendar-area-wrap"
|
||||
ref="calendarAreaWrap"
|
||||
onWheel={this._onScrollCalendarArea}
|
||||
ref={this._calendarWrapEl}
|
||||
onScroll={this._onScrollCalendarArea}
|
||||
>
|
||||
<div className="week-header" style={{ width: `${this._bufferRatio() * 100}%` }}>
|
||||
<div className="date-labels">{days.map(this._renderDateLabel)}</div>
|
||||
|
||||
<WeekViewAllDayEvents
|
||||
ref="weekViewAllDayEvents"
|
||||
minorDim={MIN_INTERVAL_HEIGHT}
|
||||
end={endMoment.unix()}
|
||||
height={this._allDayEventHeight(allDayOverlap)}
|
||||
start={startMoment.unix()}
|
||||
height={allDayBarHeight}
|
||||
start={range.bufferedStart.unix()}
|
||||
end={range.bufferedEnd.unix()}
|
||||
allDayEvents={eventsByDay.allDay}
|
||||
allDayOverlap={allDayOverlap}
|
||||
focusedEvent={this.props.focusedEvent}
|
||||
selectedEvents={this.props.selectedEvents}
|
||||
onEventClick={this.props.onEventClick}
|
||||
onEventDoubleClick={this.props.onEventDoubleClick}
|
||||
onEventFocused={this.props.onEventFocused}
|
||||
/>
|
||||
</div>
|
||||
<ScrollRegion
|
||||
className="event-grid-wrap"
|
||||
ref="eventGridWrap"
|
||||
getScrollbar={() => this.refs.scrollbar}
|
||||
onScroll={this._onScrollGrid}
|
||||
ref={this._gridScrollRegion}
|
||||
scrollbarRef={this._scrollbar}
|
||||
onScroll={event => (this._legendWrapEl.current.scrollTop = event.target.scrollTop)}
|
||||
onViewportResize={this._setIntervalHeight}
|
||||
style={{ width: `${this._bufferRatio() * 100}%` }}
|
||||
>
|
||||
<div className="event-grid" style={{ height: gridHeight }}>
|
||||
{days.map(_.partial(this._renderEventColumn, eventsByDay))}
|
||||
<div className="event-grid" style={{ height: totalHeight }}>
|
||||
{days.map(day => (
|
||||
<WeekViewEventColumn
|
||||
day={day}
|
||||
dayEnd={day.unix() + 24 * 60 * 60 - 1}
|
||||
key={day.valueOf()}
|
||||
events={eventsByDay[day.unix()]}
|
||||
focusedEvent={this.props.focusedEvent}
|
||||
selectedEvents={this.props.selectedEvents}
|
||||
onEventClick={this.props.onEventClick}
|
||||
onEventDoubleClick={this.props.onEventDoubleClick}
|
||||
onEventFocused={this.props.onEventFocused}
|
||||
/>
|
||||
))}
|
||||
<CurrentTimeIndicator
|
||||
visible={
|
||||
todayColumnIdx > BUFFER_DAYS && todayColumnIdx <= BUFFER_DAYS + DAYS_IN_VIEW
|
||||
}
|
||||
gridHeight={gridHeight}
|
||||
gridHeight={totalHeight}
|
||||
numColumns={BUFFER_DAYS * 2 + DAYS_IN_VIEW}
|
||||
todayColumnIdx={todayColumnIdx}
|
||||
/>
|
||||
<EventGridBackground
|
||||
height={gridHeight}
|
||||
height={totalHeight}
|
||||
intervalHeight={this.state.intervalHeight}
|
||||
numColumns={BUFFER_DAYS * 2 + DAYS_IN_VIEW}
|
||||
ref="eventGridBg"
|
||||
tickGenerator={tickGen}
|
||||
/>
|
||||
</div>
|
||||
</ScrollRegion>
|
||||
</div>
|
||||
<ScrollRegion.Scrollbar
|
||||
ref="scrollbar"
|
||||
getScrollRegion={() => this.refs.eventGridWrap}
|
||||
ref={this._scrollbar}
|
||||
getScrollRegion={() => this._gridScrollRegion.current}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FooterControls footerComponents={this.props.footerComponents} />
|
||||
</CalendarEventContainer>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { EventedIFrame } from 'mailspring-component-kit';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { PropTypes, Utils } from 'mailspring-exports';
|
||||
import { Utils } from 'mailspring-exports';
|
||||
|
||||
export default class EmailFrame extends React.Component<{ content: string }> {
|
||||
export class EmailFrame extends React.Component<{ content: string }> {
|
||||
static displayName = 'EmailFrame';
|
||||
|
||||
static propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
_mounted = false;
|
||||
_unlisten?: () => void;
|
||||
_iframeComponent: EventedIFrame;
|
||||
|
@ -54,7 +50,7 @@ export default class EmailFrame extends React.Component<{ content: string }> {
|
|||
);
|
||||
doc.close();
|
||||
|
||||
// autolink(doc, {async: true});
|
||||
// autolink(doc.body, {async: true});
|
||||
// autoscaleImages(doc);
|
||||
// addInlineDownloadPrompts(doc);
|
||||
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import React from 'react';
|
||||
import { WorkspaceStore, ComponentRegistry } from 'mailspring-exports';
|
||||
import CalendarWrapper from './calendar-wrapper';
|
||||
import QuickEventButton from './quick-event-button';
|
||||
import { QuickEventButton } from './quick-event-button';
|
||||
import { MailspringCalendar } from './core/mailspring-calendar';
|
||||
|
||||
const Notice = () =>
|
||||
AppEnv.inDevMode() ? (
|
||||
<span />
|
||||
) : (
|
||||
<div className="preview-notice">
|
||||
Calendar is launching later this year! This preview is read-only and only supports Google
|
||||
calendar.
|
||||
</div>
|
||||
);
|
||||
|
||||
const Notice = () => (
|
||||
<div className="preview-notice">
|
||||
Calendar is launching later this year! This preview is read-only and only supports Google
|
||||
calendar.
|
||||
</div>
|
||||
);
|
||||
Notice.displayName = 'Notice';
|
||||
|
||||
export function activate() {
|
||||
ComponentRegistry.register(CalendarWrapper, {
|
||||
ComponentRegistry.register(MailspringCalendar, {
|
||||
location: WorkspaceStore.Location.Center,
|
||||
});
|
||||
ComponentRegistry.register(Notice, {
|
||||
|
@ -24,6 +28,6 @@ export function activate() {
|
|||
}
|
||||
|
||||
export function deactivate() {
|
||||
ComponentRegistry.unregister(CalendarWrapper);
|
||||
ComponentRegistry.unregister(MailspringCalendar);
|
||||
ComponentRegistry.unregister(QuickEventButton);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Actions } from 'mailspring-exports';
|
||||
import QuickEventPopover from './quick-event-popover';
|
||||
import { QuickEventPopover } from './quick-event-popover';
|
||||
|
||||
export default class QuickEventButton extends React.Component {
|
||||
export class QuickEventButton extends React.Component<{}> {
|
||||
static displayName = 'QuickEventButton';
|
||||
|
||||
onClick = event => {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Actions, Calendar, DatabaseStore, DateUtils, Event } from 'mailspring-exports';
|
||||
import { Actions, Calendar, DatabaseStore, DateUtils, Event, localized } from 'mailspring-exports';
|
||||
import { Moment } from 'moment';
|
||||
|
||||
interface QuickEventPopoverSttae {
|
||||
interface QuickEventPopoverState {
|
||||
start: Moment | null;
|
||||
end: Moment | null;
|
||||
leftoverText: string | null;
|
||||
}
|
||||
export default class QuickEventPopover extends React.Component<{}, QuickEventPopoverSttae> {
|
||||
|
||||
export class QuickEventPopover extends React.Component<{}, QuickEventPopoverState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -18,7 +19,10 @@ export default class QuickEventPopover extends React.Component<{}, QuickEventPop
|
|||
}
|
||||
|
||||
onInputKeyDown = event => {
|
||||
const { key, target: { value } } = event;
|
||||
const {
|
||||
key,
|
||||
target: { value },
|
||||
} = event;
|
||||
if (value.length > 0 && ['Enter', 'Return'].includes(key)) {
|
||||
// This prevents onInputChange from being fired
|
||||
event.stopPropagation();
|
||||
|
@ -31,24 +35,29 @@ export default class QuickEventPopover extends React.Component<{}, QuickEventPop
|
|||
this.setState(DateUtils.parseDateString(event.target.value));
|
||||
};
|
||||
|
||||
createEvent = async ({ leftoverText, start, end }) => {
|
||||
createEvent = async ({
|
||||
leftoverText,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
leftoverText: string;
|
||||
start: Moment;
|
||||
end: Moment;
|
||||
}) => {
|
||||
const allCalendars = await DatabaseStore.findAll<Calendar>(Calendar);
|
||||
if (allCalendars.length === 0) {
|
||||
throw new Error("Can't create an event, you have no calendars");
|
||||
}
|
||||
const cals = allCalendars.filter(c => !c.readOnly);
|
||||
if (cals.length === 0) {
|
||||
const editableCals = allCalendars.filter(c => !c.readOnly);
|
||||
if (editableCals.length === 0) {
|
||||
AppEnv.showErrorDialog(
|
||||
"This account has no editable calendars. We can't " +
|
||||
'create an event for you. Please make sure you have an editable calendar ' +
|
||||
'with your account provider.'
|
||||
localized(
|
||||
"This account has no editable calendars. We can't create an event for you. Please make sure you have an editable calendar with your account provider."
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new Event({
|
||||
calendarId: cals[0].id,
|
||||
accountId: cals[0].accountId,
|
||||
calendarId: editableCals[0].id,
|
||||
accountId: editableCals[0].accountId,
|
||||
start: start.unix(),
|
||||
end: end.unix(),
|
||||
when: {
|
||||
|
@ -86,7 +95,7 @@ export default class QuickEventPopover extends React.Component<{}, QuickEventPop
|
|||
<input
|
||||
tabIndex={0}
|
||||
type="text"
|
||||
placeholder="Coffee next Monday at 9AM'"
|
||||
placeholder={localized("Coffee next Monday at 9AM'")}
|
||||
onKeyDown={this.onInputKeyDown}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
@import 'ui-variables';
|
||||
@import 'ui-mixins';
|
||||
|
||||
.main-calendar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fixed-popover .calendar-event-popover {
|
||||
color: fadeout(@text-color, 20%);
|
||||
background-color: @background-primary;
|
||||
|
|
|
@ -91,7 +91,7 @@ body.platform-win32 {
|
|||
.default-header {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 4px 5px 5px 6px;
|
||||
margin: 4px 1px 3px 4px;
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
flex: 1;
|
||||
|
@ -141,6 +141,7 @@ body.platform-win32 {
|
|||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
|
@ -181,14 +182,14 @@ body.platform-win32 {
|
|||
.legend-text {
|
||||
font-size: 11px;
|
||||
position: absolute;
|
||||
color: #bfbfbf;
|
||||
color: @text-color-very-subtle;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.date-label-legend {
|
||||
width: @legend-width;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
box-shadow: 0 1px 2.5px rgba(0, 0, 0, 0.15);
|
||||
z-index: 3;
|
||||
.legend-text {
|
||||
|
@ -201,6 +202,10 @@ body.platform-win32 {
|
|||
text-align: center;
|
||||
flex: 1;
|
||||
box-shadow: inset 1px 0 0 rgba(177, 177, 177, 0.15);
|
||||
scroll-snap-align: start;
|
||||
&.is-hard-stop {
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
&.is-today {
|
||||
.date-label {
|
||||
color: @accent-primary;
|
||||
|
@ -214,7 +219,7 @@ body.platform-win32 {
|
|||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
color: #808080;
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
.weekday-label {
|
||||
display: block;
|
||||
|
@ -222,7 +227,7 @@ body.platform-win32 {
|
|||
text-transform: uppercase;
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
color: #ccd4d8;
|
||||
color: @text-color-subtle;
|
||||
}
|
||||
|
||||
.event-grid-wrap {
|
||||
|
@ -277,6 +282,7 @@ body.platform-win32 {
|
|||
}
|
||||
|
||||
.month-view {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.current-time-indicator {
|
||||
|
@ -304,7 +310,7 @@ body.platform-win32 {
|
|||
|
||||
.top-banner {
|
||||
color: rgba(33, 99, 146, 0.6);
|
||||
background: #e0eff6;
|
||||
background: @gray-lighter;
|
||||
font-size: 12px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
|
@ -314,7 +320,7 @@ body.platform-win32 {
|
|||
.header-controls {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
color: #808080;
|
||||
color: @text-color-subtle;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
box-shadow: inset 0 -1px 1px rgba(191, 191, 191, 0.12);
|
||||
flex-shrink: 0;
|
||||
|
@ -338,99 +344,9 @@ body.platform-win32 {
|
|||
}
|
||||
}
|
||||
|
||||
.footer-controls {
|
||||
padding: 10px;
|
||||
min-height: 45px;
|
||||
display: flex;
|
||||
color: #808080;
|
||||
background: @background-primary;
|
||||
border-top: @border-color-divider;
|
||||
box-shadow: 0 -3px 16px rgba(0, 0, 0, 0.11);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.center-controls {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
order: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-month-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: @background-primary;
|
||||
border: 1px solid @border-color-divider;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
background: @background-secondary;
|
||||
padding: 4px 0 3px 0;
|
||||
.month-title {
|
||||
padding-top: 3px;
|
||||
color: @text-color;
|
||||
flex: 1;
|
||||
}
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
.btn.btn-icon {
|
||||
line-height: 27px;
|
||||
height: 27px;
|
||||
margin-top: -1px;
|
||||
margin-right: 0;
|
||||
&:active {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
.weekday {
|
||||
flex: 1;
|
||||
}
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
}
|
||||
|
||||
.day-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
.week {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 28px;
|
||||
}
|
||||
.day {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-height: 28px;
|
||||
color: @text-color-very-subtle;
|
||||
&.cur-month {
|
||||
color: @text-color;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
&.today {
|
||||
border: 1px solid @accent-primary;
|
||||
}
|
||||
&.cur-day {
|
||||
background: @accent-primary;
|
||||
color: @text-color-inverse;
|
||||
&:hover {
|
||||
background: darken(@accent-primary, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { EventedIFrame } from 'mailspring-component-kit';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { PropTypes, Utils, QuotedHTMLTransformer, MessageStore, Message } from 'mailspring-exports';
|
||||
import { autolink } from './autolinker';
|
||||
import {
|
||||
PropTypes,
|
||||
Utils,
|
||||
QuotedHTMLTransformer,
|
||||
MessageStore,
|
||||
Message,
|
||||
Autolink,
|
||||
} from 'mailspring-exports';
|
||||
import { adjustImages } from './adjust-images';
|
||||
import EmailFrameStylesStore from './email-frame-styles-store';
|
||||
|
||||
|
@ -118,7 +124,10 @@ export default class EmailFrame extends React.Component<EmailFrameProps> {
|
|||
this._iframeDocObserver.observe(iframeEl.contentDocument.firstElementChild);
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
autolink(doc, { async: true });
|
||||
Autolink(doc.body, {
|
||||
async: true,
|
||||
telAggressiveMatch: false,
|
||||
});
|
||||
adjustImages(doc);
|
||||
|
||||
for (const extension of MessageStore.extensions()) {
|
||||
|
|
|
@ -122,7 +122,7 @@ xdescribe('MessageItem', function() {
|
|||
snippet: 'snippet one...',
|
||||
subject: 'Subject One',
|
||||
threadId: 'thread_12345',
|
||||
accountId: window.TEST_ACCOUNT_ID,
|
||||
accountId: TEST_ACCOUNT_ID,
|
||||
});
|
||||
|
||||
// Generate the test component. Should be called after @message is configured
|
||||
|
|
|
@ -148,7 +148,7 @@ const CreatePageForForm = FormComponent => {
|
|||
account.settings.imap_host.includes('imap.gmail.com')
|
||||
) {
|
||||
didWarnAboutGmailIMAP = true;
|
||||
const buttonIndex = remote.dialog.showMessageBox({
|
||||
const buttonIndex = remote.dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
buttons: [localized('Go Back'), localized('Continue')],
|
||||
message: localized('Are you sure?'),
|
||||
|
|
|
@ -162,25 +162,6 @@
|
|||
"mx-match": ["mx\\.zoho\\.com", "mx[0-9]*\\.zoho\\.com"],
|
||||
"domain-match": ["zoho\\.com"]
|
||||
},
|
||||
"juno": {
|
||||
"servers": {
|
||||
"pop": [
|
||||
{
|
||||
"port": 995,
|
||||
"hostname": "pop.juno.com",
|
||||
"ssl": true
|
||||
}
|
||||
],
|
||||
"smtp": [
|
||||
{
|
||||
"port": 465,
|
||||
"hostname": "smtp.juno.com",
|
||||
"starttls": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"domain-match": ["juno\\.com"]
|
||||
},
|
||||
"mobileme": {
|
||||
"servers": {
|
||||
"imap": [
|
||||
|
@ -1109,44 +1090,6 @@
|
|||
},
|
||||
"mx-match": ["mx1\\.comcast\\.net", "mx2\\.comcast\\.net"]
|
||||
},
|
||||
"verizon": {
|
||||
"servers": {
|
||||
"pop": [
|
||||
{
|
||||
"port": 995,
|
||||
"hostname": "pop.verizon.net",
|
||||
"ssl": true
|
||||
}
|
||||
],
|
||||
"smtp": [
|
||||
{
|
||||
"port": 465,
|
||||
"hostname": "smtp.verizon.net",
|
||||
"ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"mx-match": ["relay\\.verizon\\.net"]
|
||||
},
|
||||
"rcn": {
|
||||
"servers": {
|
||||
"pop": [
|
||||
{
|
||||
"port": 110,
|
||||
"hostname": "pop.rcn.com",
|
||||
"ssl": true
|
||||
}
|
||||
],
|
||||
"smtp": [
|
||||
{
|
||||
"port": 25,
|
||||
"hostname": "smtp.rcn.com",
|
||||
"ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"mx-match": ["mx\\.rcn\\.com"]
|
||||
},
|
||||
"ukrnet": {
|
||||
"servers": {
|
||||
"imap": [
|
||||
|
|
|
@ -7,13 +7,14 @@ import url from 'url';
|
|||
|
||||
import FormErrorMessage from './form-error-message';
|
||||
import { LOCAL_SERVER_PORT } from './onboarding-helpers';
|
||||
import AccountProviders from './account-providers';
|
||||
|
||||
interface OAuthSignInPageProps {
|
||||
providerAuthPageUrl: string;
|
||||
buildAccountFromAuthResponse: (rep: any) => Account | Promise<Account>;
|
||||
onSuccess: (account: Account) => void;
|
||||
onTryAgain: () => void;
|
||||
providerConfig: object;
|
||||
providerConfig: (typeof AccountProviders)[0];
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
|
@ -79,18 +80,16 @@ export default class OAuthSignInPage extends React.Component<
|
|||
response.end('Unknown Request');
|
||||
}
|
||||
});
|
||||
this._server.listen(LOCAL_SERVER_PORT, err => {
|
||||
if (err) {
|
||||
AppEnv.showErrorDialog({
|
||||
title: localized('Unable to Start Local Server'),
|
||||
message: localized(
|
||||
`To listen for the Gmail Oauth response, Mailspring needs to start a webserver on port ${LOCAL_SERVER_PORT}. Please go back and try linking your account again. If this error persists, use the IMAP/SMTP option with a Gmail App Password.\n\n%@`,
|
||||
err
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._server.once('error', err => {
|
||||
AppEnv.showErrorDialog({
|
||||
title: localized('Unable to Start Local Server'),
|
||||
message: localized(
|
||||
`To listen for the Gmail Oauth response, Mailspring needs to start a webserver on port ${LOCAL_SERVER_PORT}. Please go back and try linking your account again. If this error persists, use the IMAP/SMTP option with a Gmail App Password.\n\n%@`,
|
||||
err
|
||||
),
|
||||
});
|
||||
});
|
||||
this._server.listen(LOCAL_SERVER_PORT);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -33,19 +33,17 @@ const PageTopBar = props => {
|
|||
backButton = null;
|
||||
}
|
||||
|
||||
const style: any = {
|
||||
top: 0,
|
||||
left: 26,
|
||||
right: 0,
|
||||
height: 27,
|
||||
zIndex: 100,
|
||||
position: 'absolute',
|
||||
WebkitAppRegion: 'drag',
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="dragRegion"
|
||||
style={{
|
||||
top: 0,
|
||||
left: 26,
|
||||
right: 0,
|
||||
height: 27,
|
||||
zIndex: 100,
|
||||
position: 'absolute',
|
||||
WebkitAppRegion: 'drag',
|
||||
}}
|
||||
>
|
||||
<div className="dragRegion" style={style}>
|
||||
{backButton}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -20,7 +20,6 @@ xdescribe('Open tracking composer extension', function openTrackingComposerExten
|
|||
beforeEach(() => {
|
||||
this.draftBodyRootNode = nodeForHTML(beforeBody);
|
||||
this.draft = new Message({
|
||||
clientId: clientId,
|
||||
accountId: accountId,
|
||||
body: beforeBody,
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ class PreferencesAccountList extends Component<PreferencesAccountListProps> {
|
|||
onRemoveAccount: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_renderAccountStateIcon(account) {
|
||||
_renderAccountStateIcon(account: Account) {
|
||||
if (account.syncState !== 'running') {
|
||||
return (
|
||||
<div className="sync-error-icon">
|
||||
|
@ -38,7 +38,7 @@ class PreferencesAccountList extends Component<PreferencesAccountListProps> {
|
|||
return null;
|
||||
}
|
||||
|
||||
_renderAccount = account => {
|
||||
_renderAccount = (account: Account) => {
|
||||
const label = account.label;
|
||||
const accountSub = `${account.name || localized('No name provided')} <${account.emailAddress}>`;
|
||||
const syncError = account.hasSyncStateError();
|
||||
|
|
|
@ -28,7 +28,7 @@ class PreferencesGeneral extends React.Component<{
|
|||
};
|
||||
|
||||
_onResetAccountsAndSettings = () => {
|
||||
const chosen = remote.dialog.showMessageBox({
|
||||
const chosen = remote.dialog.showMessageBoxSync({
|
||||
type: 'info',
|
||||
message: localized('Are you sure?'),
|
||||
buttons: [localized('Cancel'), localized('Reset')],
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
localized,
|
||||
localizedReactFragment,
|
||||
IIdentity,
|
||||
EMPTY_IDENTITY,
|
||||
} from 'mailspring-exports';
|
||||
import { OpenIdentityPageButton, BillingModal, RetinaImg } from 'mailspring-component-kit';
|
||||
import { shell } from 'electron';
|
||||
|
@ -158,7 +159,7 @@ class PreferencesIdentity extends React.Component<{}, { identity: IIdentity }> {
|
|||
|
||||
_getStateFromStores() {
|
||||
return {
|
||||
identity: IdentityStore.identity() || {},
|
||||
identity: IdentityStore.identity() || { ...EMPTY_IDENTITY },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ export default class PreferencesKeymaps extends React.Component<
|
|||
}
|
||||
|
||||
_onDeleteUserKeymap() {
|
||||
const chosen = remote.dialog.showMessageBox({
|
||||
const chosen = remote.dialog.showMessageBoxSync({
|
||||
type: 'info',
|
||||
message: localized('Are you sure?'),
|
||||
detail: localized('Delete your custom key bindings and reset to the template defaults?'),
|
||||
|
|
|
@ -26,6 +26,7 @@ export default class PrintWindow {
|
|||
.join('');
|
||||
|
||||
const content = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src * mailspring:; script-src 'self' chrome-extension://react-developer-tools; style-src * 'unsafe-inline' mailspring:; img-src * data: mailspring: file:;">
|
||||
|
@ -79,7 +80,7 @@ export default class PrintWindow {
|
|||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
this.browserWin.setMenu(null);
|
||||
this.browserWin.removeMenu();
|
||||
fs.writeFileSync(tmpMessagesPath, `window.printMessages = ${printMessages}`);
|
||||
fs.writeFileSync(this.tmpFile, content);
|
||||
}
|
||||
|
|
|
@ -7,31 +7,28 @@ win.addListener('page-title-updated', event => {
|
|||
event.preventDefault();
|
||||
});
|
||||
|
||||
global.printToPDF = () => {
|
||||
remote.dialog.showSaveDialog(
|
||||
global.printToPDF = async () => {
|
||||
const { filePath } = await remote.dialog.showSaveDialog({
|
||||
defaultPath: `${win.getTitle()}.pdf`,
|
||||
});
|
||||
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
webcontents.printToPDF(
|
||||
{
|
||||
defaultPath: `${win.getTitle()}.pdf`,
|
||||
marginsType: 0,
|
||||
pageSize: 'Letter',
|
||||
printBackground: true,
|
||||
landscape: false,
|
||||
},
|
||||
filename => {
|
||||
if (!filename) {
|
||||
(error, data) => {
|
||||
if (error) {
|
||||
remote.dialog.showErrorBox('An Error Occurred', `${error}`);
|
||||
return;
|
||||
}
|
||||
webcontents.printToPDF(
|
||||
{
|
||||
marginsType: 0,
|
||||
pageSize: 'Letter',
|
||||
printBackground: true,
|
||||
landscape: false,
|
||||
},
|
||||
(error, data) => {
|
||||
if (error) {
|
||||
remote.dialog.showErrorBox('An Error Occurred', `${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(filename, data);
|
||||
}
|
||||
);
|
||||
fs.writeFileSync(filename, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint no-irregular-whitespace: 0 */
|
||||
import fs from 'fs';
|
||||
import { removeTrackingPixels } from '../lib/main';
|
||||
import { Message } from 'mailspring-exports';
|
||||
|
||||
const readFixture = name => {
|
||||
return fs
|
||||
|
@ -19,7 +20,7 @@ describe('TrackingPixelsExtension', function trackingPixelsExtension() {
|
|||
accountId: '1234',
|
||||
isFromMe: () => true,
|
||||
};
|
||||
removeTrackingPixels(message);
|
||||
removeTrackingPixels(message as Message);
|
||||
expect(message.body).toEqual(expected);
|
||||
});
|
||||
|
||||
|
@ -32,7 +33,7 @@ describe('TrackingPixelsExtension', function trackingPixelsExtension() {
|
|||
accountId: '1234',
|
||||
isFromMe: () => false,
|
||||
};
|
||||
removeTrackingPixels(message);
|
||||
removeTrackingPixels(message as Message);
|
||||
expect(message.body).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,7 +68,7 @@ export default class SendLaterStatus extends Component<SendLaterStatusProps, Sen
|
|||
SendDraftTask,
|
||||
{ headerMessageId: draft.headerMessageId },
|
||||
{ includeCompleted: true }
|
||||
).pop(),
|
||||
).pop() as SendDraftTask,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ function getObservable() {
|
|||
return Rx.Observable.combineLatest(
|
||||
Rx.Observable.fromStore(FocusedContentStore),
|
||||
ThreadListStore.selectionObservable(),
|
||||
(store: FocusedContentStore, items: Thread[]) => ({
|
||||
(store, items) => ({
|
||||
focusedThread: store.focused('thread'),
|
||||
items,
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from 'mailspring-exports';
|
||||
|
||||
const _observableForThreadMessages = (id, initialModels) => {
|
||||
const subscription = new QuerySubscription(
|
||||
const subscription = new QuerySubscription<Message>(
|
||||
DatabaseStore.findAll<Message>(Message, { threadId: id }),
|
||||
{
|
||||
initialModels: initialModels,
|
||||
|
|
|
@ -59,7 +59,7 @@ class ThreadListParticipants extends React.Component<{ thread: ThreadWithMessage
|
|||
if (spacer) {
|
||||
accumulate('...');
|
||||
} else {
|
||||
let short = '';
|
||||
let short = contact.email;
|
||||
if (contact.name && contact.name.length > 0) {
|
||||
if (items.length > 1) {
|
||||
short = contact.displayName({
|
||||
|
@ -69,8 +69,6 @@ class ThreadListParticipants extends React.Component<{ thread: ThreadWithMessage
|
|||
} else {
|
||||
short = contact.displayName({ includeAccountLabel: false });
|
||||
}
|
||||
} else {
|
||||
short = contact.email;
|
||||
}
|
||||
if (idx < items.length - 1 && !items[idx + 1].spacer) {
|
||||
short += ', ';
|
||||
|
|
|
@ -49,7 +49,7 @@ class ThreadListStore extends MailspringStore {
|
|||
};
|
||||
|
||||
selectionObservable = () => {
|
||||
return Rx.Observable.fromListSelection(this);
|
||||
return Rx.Observable.fromListSelection<Thread>(this);
|
||||
};
|
||||
|
||||
// Inbound Events
|
||||
|
|
|
@ -191,7 +191,7 @@ describe('ThreadListParticipants', function() {
|
|||
},
|
||||
];
|
||||
|
||||
for (let scenario of scenarios) {
|
||||
for (const scenario of scenarios) {
|
||||
const thread = new Thread();
|
||||
thread.__messages = scenario.in;
|
||||
const participants = ReactTestUtils.renderIntoDocument(
|
||||
|
|
|
@ -23,26 +23,27 @@ import {
|
|||
wrapInQuotes,
|
||||
} from './search-bar-util';
|
||||
|
||||
class ThreadSearchBar extends Component<
|
||||
{
|
||||
query: string;
|
||||
isSearching: boolean;
|
||||
perspective: MailboxPerspective;
|
||||
},
|
||||
{
|
||||
suggestions: {
|
||||
token: string;
|
||||
term: string;
|
||||
description: string;
|
||||
termSuggestions: string[] | ((term: string, accountIds: string[]) => Promise<any>);
|
||||
}[];
|
||||
focused: boolean;
|
||||
selected?: {
|
||||
description: any;
|
||||
};
|
||||
selectedIdx: number;
|
||||
}
|
||||
> {
|
||||
interface ThreadSearchBarProps {
|
||||
query: string;
|
||||
isSearching: boolean;
|
||||
perspective: MailboxPerspective;
|
||||
}
|
||||
|
||||
interface ThreadSearchBarState {
|
||||
suggestions: {
|
||||
token: string;
|
||||
term: string;
|
||||
description: string;
|
||||
termSuggestions: string[] | ((term: string, accountIds: string[]) => Promise<any>);
|
||||
}[];
|
||||
focused: boolean;
|
||||
selected?: {
|
||||
description: any;
|
||||
};
|
||||
selectedIdx: number;
|
||||
}
|
||||
|
||||
class ThreadSearchBar extends Component<ThreadSearchBarProps, ThreadSearchBarState> {
|
||||
static displayName = 'ThreadSearchBar';
|
||||
|
||||
static propTypes = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import cld from '@paulcbetts/cld';
|
||||
import cld from 'cld';
|
||||
import { remote } from 'electron';
|
||||
import {
|
||||
localized,
|
||||
|
@ -237,7 +237,7 @@ export class TranslateMessageHeader extends React.Component<
|
|||
_onNeverForLanguage = () => {
|
||||
if (!this.state.detected) return;
|
||||
|
||||
const response = remote.dialog.showMessageBox({
|
||||
const response = remote.dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
buttons: [localized('Yes'), localized('Cancel')],
|
||||
message: localized('Are you sure?'),
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
Actions,
|
||||
MailspringAPIRequest,
|
||||
RegExpUtils,
|
||||
FeatureLexicon,
|
||||
} from 'mailspring-exports';
|
||||
|
||||
export const TranslatePopupOptions = {
|
||||
|
@ -118,7 +119,7 @@ export const AllLanguages = {
|
|||
ms: 'Malay',
|
||||
};
|
||||
|
||||
export const TranslationsUsedLexicon = {
|
||||
export const TranslationsUsedLexicon: FeatureLexicon = {
|
||||
headerText: localized('All Translations Used'),
|
||||
rechargeText: `${localized(
|
||||
'You can translate up to %1$@ emails each %2$@ with Mailspring Basic.'
|
||||
|
|
|
@ -7,7 +7,7 @@ function isUndoSend(block) {
|
|||
return (
|
||||
block.tasks.length === 1 &&
|
||||
block.tasks[0] instanceof SyncbackMetadataTask &&
|
||||
block.tasks[0].value.isUndoSend
|
||||
(block.tasks[0].value as any).isUndoSend
|
||||
);
|
||||
}
|
||||
|
||||
|
|
4591
app/package-lock.json
generated
4591
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -12,15 +12,15 @@
|
|||
"main": "./src/browser/main.js",
|
||||
"dependencies": {
|
||||
"@bengotow/slate-edit-list": "github:bengotow/slate-edit-list#b868e108",
|
||||
"@paulcbetts/cld": "2.4.6",
|
||||
"better-sqlite3": "bengotow/better-sqlite3#dcd5b6e73c9a5329fd72c85be3316131fcfb83ab",
|
||||
"better-sqlite3": "^7.1.2",
|
||||
"chromium-net-errors": "1.0.3",
|
||||
"chrono-node": "^1.1.2",
|
||||
"classnames": "1.2.1",
|
||||
"cld": "2.6.0",
|
||||
"collapse-whitespace": "^1.1.6",
|
||||
"debug": "github:emorikawa/debug#nylas",
|
||||
"deep-extend": "0.6.0",
|
||||
"electron-spellchecker": "github:bengotow/electron-spellchecker#de319e18db19e497a6add6cc086b243d8e0461db",
|
||||
"electron-spellchecker": "github:bengotow/electron-spellchecker#267df08",
|
||||
"emoji-data": "^0.2.0",
|
||||
"enzyme": "^3.8.0",
|
||||
"enzyme-adapter-react-16": "^1.9.0",
|
||||
|
@ -36,7 +36,7 @@
|
|||
"jasmine-react-helpers": "^0.2",
|
||||
"jasmine-reporters": "1.x.x",
|
||||
"juice": "^5.2.0",
|
||||
"keytar": "4.3.0",
|
||||
"keytar": "5.5.0",
|
||||
"less-cache": "1.1.0",
|
||||
"lru-cache": "^4.0.1",
|
||||
"mammoth": "1.4.7",
|
||||
|
@ -58,7 +58,6 @@
|
|||
"reflux": "0.1.13",
|
||||
"rimraf": "2.5.2",
|
||||
"rtlcss": "2.4.0",
|
||||
"runas": "getflywheel/node-runas#ca4f0714",
|
||||
"rx-lite": "4.0.8",
|
||||
"slate": "github:bengotow/slate#cd6f40e8",
|
||||
"slate-auto-replace": "0.12.1",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import _ from 'underscore';
|
||||
import { Model } from '../src/flux/models/model';
|
||||
import Attributes from '../src/flux/attributes';
|
||||
import * as Attributes from '../src/flux/attributes';
|
||||
import DatabaseObjectRegistry from '../src/registries/database-object-registry';
|
||||
|
||||
class GoodTest extends Model {
|
||||
|
|
7
app/spec/fixtures/db-test-model.ts
vendored
7
app/spec/fixtures/db-test-model.ts
vendored
|
@ -5,14 +5,11 @@
|
|||
*/
|
||||
import { Model } from '../../src/flux/models/model';
|
||||
import { Category } from '../../src/flux/models/category';
|
||||
import Attributes from '../../src/flux/attributes';
|
||||
import * as Attributes from '../../src/flux/attributes';
|
||||
|
||||
class TestModel extends Model {
|
||||
static attributes = {
|
||||
id: Attributes.String({
|
||||
queryable: true,
|
||||
modelKey: 'id',
|
||||
}),
|
||||
...Model.attributes,
|
||||
|
||||
clientId: Attributes.String({
|
||||
queryable: true,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import { Model } from '../../src/flux/models/model';
|
||||
import Attributes from '../../src/flux/attributes';
|
||||
import * as Attributes from '../../src/flux/attributes';
|
||||
|
||||
describe('Model', function modelSpecs() {
|
||||
describe('constructor', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint quote-props: 0 */
|
||||
import ModelQuery from '../../src/flux/models/query';
|
||||
import Attributes from '../../src/flux/attributes';
|
||||
import * as Attributes from '../../src/flux/attributes';
|
||||
import { Message } from '../../src/flux/models/message';
|
||||
import { Thread } from '../../src/flux/models/thread';
|
||||
import { Account } from '../../src/flux/models/account';
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { autolink } from '../lib/autolinker';
|
||||
import { Autolink } from '../../src/services/autolinker';
|
||||
|
||||
describe('autolink', function autolinkSpec() {
|
||||
const fixturesDir = path.join(__dirname, 'autolinker-fixtures');
|
||||
fs
|
||||
.readdirSync(fixturesDir)
|
||||
fs.readdirSync(fixturesDir)
|
||||
.filter(filename => filename.indexOf('-in.html') !== -1)
|
||||
.forEach(filename => {
|
||||
it(`should properly autolink a variety of email bodies ${filename}`, () => {
|
||||
|
@ -17,7 +16,7 @@ describe('autolink', function autolinkSpec() {
|
|||
const expected = fs.readFileSync(expectedPath).toString();
|
||||
|
||||
div.innerHTML = input;
|
||||
autolink({ body: div });
|
||||
Autolink(div);
|
||||
|
||||
expect(div.innerHTML).toEqual(expected);
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue