Replace Babel with TypeScript compiler, switch entire app to TypeScript 🎉 (#1404)

* Switch to using Typescript instead of Babel

* Switch all es6 / jsx file extensions to ts / tsx

* Convert Utils to a TS module from module.exports style module

* Move everything from module.exports to typescript exports

* Define .d.ts files for mailspring-exports and component kit… Yes it seems this is the best option :(

* Load up on those @types

* Synthesize TS types from PropTypes for standard components

* Add types to Model classes and move constructor constants to instance vars

* 9800 => 7700 TS errors

* 7700 => 5600 TS errors

* 5600 => 5330 TS errors

* 5330 => 4866 TS errors

* 4866 => 4426 TS errors

* 4426 => 2411 TS errors

* 2411 > 1598 TS errors

* 1598 > 769 TS errors

* 769 > 129 TS errors

* 129 > 22 TS errors

* Fix runtime errors

* More runtime error fixes

* Remove support for custom .es6 file extension

* Remove a few odd remaining references to Nylas

* Don’t ship Typescript support in the compiled app for now

* Fix issues in compiled app - module resolution in TS is case sensitive?

* README updates

* Fix a few more TS errors

* Make “No Signature” option clickable + selectable

* Remove flicker when saving file and reloading keymaps

* Fix mail rule item height in preferences

* Fix missing spacing in thread sharing popover

* Fix scrollbar ticks being nested incorrectly

* Add Japanese as a manually reviewed language

* Prevent the thread list from “sticking”

* Re-use Sheet when switching root tabs, prevent sidebar from resetting

* Ensure specs run

* Update package configuration to avoid shpping types

* Turn eslint back on - we will opt-in to the TS rules one by one
This commit is contained in:
Ben Gotow 2019-03-04 11:03:12 -08:00 committed by GitHub
parent 2057ca3023
commit 149b389508
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
754 changed files with 13130 additions and 17656 deletions

View file

@ -1,11 +0,0 @@
{
"presets": [
"react"
],
"plugins": [
"transform-class-properties",
"transform-es2015-modules-commonjs",
"transform-object-rest-spread"
],
"sourceMaps": "inline"
}

View file

@ -1,14 +1,8 @@
{
"parser": "babel-eslint",
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"modules": true,
"jsx": true
}
"project": "./tsconfig.json"
},
"extends": ["react-app", "prettier", "prettier/react"],
"globals": {
"AppEnv": false,
"$m": false,
@ -24,10 +18,25 @@
"node": true,
"jasmine": true
},
"plugins": ["prettier"],
"plugins": ["@typescript-eslint"],
"extends": ["plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"],
"rules": {
"no-cond-assign": 0,
"prettier/prettier": "error"
"@typescript-eslint/explicit-member-accessibility": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/prefer-interface": 0,
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/no-namespace": 0,
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/interface-name-prefix": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/class-name-casing": 0,
"@typescript-eslint/prefer-namespace-keyword": 0,
"@typescript-eslint/no-object-literal-type-assertion": 0,
"@typescript-eslint/array-type": 0
},
"settings": {
"import/core-modules": [
@ -36,9 +45,6 @@
"electron",
"mailspring-store",
"mailspring-observables"
],
"import/resolver": {
"node": { "extensions": [".es6", ".jsx", ".json", ".js"] }
}
]
}
}

View file

@ -1,5 +1,11 @@
# Mailspring Changelog
### 1.6.0 (TBD)
Development:
**Mailspring now uses the TypeScript compiler instead of Babel, and the entire project (92,000 LOC!) has been converted to TypeScript. 🎉** This took an enormous amount of effort - 9,800 TypeScript errors were resolved by hand - but will make the project more stable, easier to maintain, and easier to contribute to in the future.
### 1.5.7 (2/25/2019)
Fixes:

View file

@ -6,7 +6,7 @@
**Mailspring is a new version of Nylas Mail maintained by one of the original authors. It's faster, leaner, and shipping today!** It replaces the JavaScript sync code in Nylas Mail with a new C++ sync engine based on [Mailcore2](https://github.com/MailCore/mailcore2). It uses roughly half the RAM and CPU of Nylas Mail and idles with almost zero "CPU Wakes", which translates to great battery life. It also has an entirely revamped composer and other great new features.
Mailspring's UI is open source (GPLv3) and written in JavaScript with [Electron](https://github.com/atom/electron) and [React](https://facebook.github.io/react/) - it's built on a plugin architecture and was designed to be easy to extend. Check out [CONTRIBUTING.md](https://github.com/Foundry376/Mailspring/blob/master/CONTRIBUTING.md) to get started!
Mailspring's UI is open source (GPLv3) and written in TypeScript with [Electron](https://github.com/atom/electron) and [React](https://facebook.github.io/react/) - it's built on a plugin architecture and was designed to be easy to extend. Check out [CONTRIBUTING.md](https://github.com/Foundry376/Mailspring/blob/master/CONTRIBUTING.md) to get started!
Mailspring's sync engine is spawned by the Electron application and runs locally on your computer. It will be open-sourced in the future but is [currently closed source.](https://github.com/Foundry376/Mailspring/blob/master/ROADMAP.md#why-is-mailsync-closed-source) When you set up your development environment, Mailspring uses the latest version of the sync process we've shipped for your platform so you don't need to pull sources or install its compile-time dependencies.

View file

@ -1,11 +0,0 @@
{
"presets": [
"react"
],
"plugins": [
"transform-class-properties",
"transform-es2015-modules-commonjs",
"transform-object-rest-spread"
],
"sourceMaps": "inline"
}

View file

@ -27,19 +27,18 @@ module.exports = grunt => {
outputDir: path.join(appDir, 'dist'),
appJSON: grunt.file.readJSON(path.join(appDir, 'package.json')),
'source:es6': [
'internal_packages/**/*.ts',
'internal_packages/**/*.tsx',
'internal_packages/**/*.jsx',
'internal_packages/**/*.es6',
'internal_packages/**/*.es',
'dot-nylas/**/*.es6',
'dot-nylas/**/*.es',
'src/**/*.es6',
'src/**/*.es',
'src/**/*.ts',
'src/**/*.tsx',
'src/**/*.jsx',
'!src/**/node_modules/**/*.es6',
'!src/**/node_modules/**/*.es',
'!src/**/node_modules/**/*.ts',
'!src/**/node_modules/**/*.tsx',
'!src/**/node_modules/**/*.jsx',
'!internal_packages/**/node_modules/**/*.es6',
'!internal_packages/**/node_modules/**/*.es',
'!internal_packages/**/node_modules/**/*.ts',
'!internal_packages/**/node_modules/**/*.tsx',
'!internal_packages/**/node_modules/**/*.jsx',
],
});
@ -49,7 +48,7 @@ module.exports = grunt => {
grunt.registerTask('docs', ['docs-build', 'docs-render']);
grunt.registerTask('lint', ['eslint', 'lesslint', 'nylaslint', 'csslint']);
grunt.registerTask('lint', ['eslint', 'lesslint', 'csslint']);
if (grunt.option('platform') === 'win32') {
grunt.registerTask('build-client', [

View file

@ -88,24 +88,6 @@ module.exports = function(grunt) {
path.join(cjsxOutputDir, path.basename(file).slice(0, -3 || undefined) + 'js'),
transformed.code
);
} else if (path.extname(file) === '.es6') {
console.log(file);
let fileStr = grunt.file.read(file);
let transformed = require('babel-core').transform(fileStr, {
plugins: ['transform-class-properties', 'transform-function-bind'],
presets: ['react', 'electron'],
});
if (transformed.code.indexOf('class ') > 0) {
grunt.log.writeln('Found class in file: ' + file);
grunt.file.write(
path.join(cjsxOutputDir, path.basename(file).slice(0, -3 || undefined) + 'js'),
transformed.code
);
}
} else if (path.extname(file) === '.js') {
let dest_path = path.join(cjsxOutputDir, path.basename(file));
console.log('Copying ' + file + ' to ' + dest_path);

View file

@ -10,10 +10,6 @@ module.exports = grunt => {
},
target: grunt.config('source:es6'),
},
eslintFixer: {
src: grunt.config('source:es6'),
},
});
grunt.registerMultiTask('eslint', 'Validate files with ESLint', function task() {

View file

@ -1,101 +0,0 @@
const path = require('path');
const fs = require('fs-plus');
module.exports = grunt => {
grunt.config.merge({
nylaslint: {
src: grunt.config('source:es6'),
},
});
grunt.registerMultiTask(
'nylaslint',
'Check requires for file extensions compiled away',
function nylaslint() {
const done = this.async();
// Enable once path errors are fixed.
if (process.platform === 'win32') {
done();
return;
}
for (const fileset of this.files) {
grunt.log.writeln(`Nylinting ${fileset.src.length} files.`);
const esExtensions = {
'.es6': true,
'.es': true,
'.jsx': true,
};
const errors = [];
const esExport = {};
const esNoExport = {};
const esExportDefault = {};
// Temp TODO. Fix spec files
for (const f of fileset.src) {
if (!esExtensions[path.extname(f)]) {
continue;
}
if (!/-spec/.test(f)) {
continue;
}
const content = fs.readFileSync(f, { encoding: 'utf8' });
// https://regex101.com/r/rQ3eD0/1
// Matches only the first describe block
const describeRe = /[\n]describe\(['"](.*?)['"], ?\(\) ?=> ?/m;
if (describeRe.test(content)) {
errors.push(`${f}: Spec has to start with function`);
}
}
// Build the list of ES6 files that export things and categorize
for (const f of fileset.src) {
if (!esExtensions[path.extname(f)]) {
continue;
}
const lookupPath = `${path.dirname(f)}/${path.basename(f, path.extname(f))}`;
const content = fs.readFileSync(f, { encoding: 'utf8' });
if (/^export/gim.test(content)) {
if (/^export default/gim.test(content)) {
esExportDefault[lookupPath] = true;
} else {
esExport[lookupPath] = true;
}
} else {
esNoExport[lookupPath] = true;
}
}
if (errors.length > 0) {
for (const err of errors) {
grunt.log.error(err);
}
const error = `
Please fix the ${errors.length} linter errors above. These are the issues we're looking for:
ISSUES WITH ES6 FILES:
4. Don't use module.exports in ES6:
You sholudn't manually assign module.exports anymore. Use proper ES6 module syntax like "export default" or "export const FOO".
5. Don't destructure default export:
If you're using "import {FOO} from './bar'" in ES6 files, it's important that "./bar" does NOT export a "default". Instead, in './bar', do "export const FOO = 'foo'"
6. Spec has to start with function
Top-level "describe" blocks can no longer use the "() => {}" function syntax. This will incorrectly bind "this" to the "window" object instead of the jasmine object. The top-level "describe" block must use the "function describeName() {}" syntax
`;
done(new Error(error));
}
}
done(null);
}
);
};

View file

@ -6,14 +6,13 @@ const util = require('util');
const tmpdir = path.resolve(require('os').tmpdir(), 'nylas-build');
const fs = require('fs-plus');
const glob = require('glob');
const babel = require('babel-core');
const TypeScript = require('typescript');
const { execSync } = require('child_process');
const symlinkedPackages = [];
module.exports = grunt => {
const packageJSON = grunt.config('appJSON');
const babelPath = path.join(grunt.config('rootDir'), '.babelrc');
const babelOptions = JSON.parse(fs.readFileSync(babelPath));
const { compilerOptions } = require(path.join(grunt.config('appDir'), 'tsconfig.json'));
function runCopyPlatformSpecificResources(buildPath, electronVersion, platform, arch, callback) {
// these files (like nylas-mailto-default.reg) go alongside the ASAR,
@ -71,29 +70,19 @@ module.exports = grunt => {
}
function runTranspilers(buildPath, electronVersion, platform, arch, callback) {
console.log('---> Running Babel');
console.log('---> Running TypeScript Compiler');
grunt.config('source:es6').forEach(pattern => {
glob.sync(pattern, { cwd: buildPath }).forEach(relPath => {
const es6Path = path.join(buildPath, relPath);
if (/(node_modules|\.js$)/.test(es6Path)) return;
const outPath = es6Path.replace(path.extname(es6Path), '.js');
console.log(` ---> Compiling ${es6Path.slice(es6Path.indexOf('/app') + 4)}`);
const res = babel.transformFileSync(
es6Path,
Object.assign(babelOptions, {
sourceMaps: true,
sourceRoot: '/',
sourceMapTarget: path.relative(buildPath, outPath),
sourceFileName: path.relative(buildPath, es6Path),
})
);
grunt.file.write(
outPath,
`${res.code}\n//# sourceMappingURL=${path.basename(outPath)}.map\n`
);
grunt.file.write(`${outPath}.map`, JSON.stringify(res.map));
fs.unlinkSync(es6Path);
const tsPath = path.join(buildPath, relPath);
const tsCode = fs.readFileSync(tsPath).toString();
if (/(node_modules|\.js$)/.test(tsPath)) return;
if (tsPath.endsWith('.d.ts')) return;
const outPath = tsPath.replace(path.extname(tsPath), '.js');
console.log(` ---> Compiling ${tsPath.slice(tsPath.indexOf('/app') + 4)}`);
const res = TypeScript.transpileModule(tsCode, { compilerOptions, fileName: tsPath });
grunt.file.write(outPath, res.outputText);
fs.unlinkSync(tsPath);
});
});
@ -186,7 +175,6 @@ module.exports = grunt => {
/\.pdb$/,
/\.h$/,
/\.cc$/,
/\.ts$/,
/\.flow$/,
/\.gyp/,
/\.mk/,

View file

@ -3,11 +3,16 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const { Actions, MenuHelpers } = require('mailspring-exports');
import { Actions, Account, MenuHelpers } from 'mailspring-exports';
let _commandsDisposable = null;
function _isSelected(account, sidebarAccountIds) {
interface IAccountMenuItem extends Electron.MenuItemConstructorOptions {
command?: string;
account?: boolean;
}
export function _isSelected(account, sidebarAccountIds) {
if (sidebarAccountIds.length > 1) {
return account instanceof Array;
} else if (sidebarAccountIds.length === 1) {
@ -17,8 +22,12 @@ function _isSelected(account, sidebarAccountIds) {
}
}
function menuItem(account, idx, { isSelected, clickHandlers } = {}) {
const item = {
export function menuItem(
account: Account,
idx: number,
{ isSelected, clickHandlers }: { isSelected?: boolean; clickHandlers?: boolean } = {}
) {
const item: IAccountMenuItem = {
label: account.label != null ? account.label : 'All Accounts',
command: `window:select-account-${idx}`,
account: true,
@ -35,7 +44,11 @@ function menuItem(account, idx, { isSelected, clickHandlers } = {}) {
return item;
}
function menuTemplate(accounts, sidebarAccountIds, { clickHandlers } = {}) {
export function menuTemplate(
accounts,
sidebarAccountIds,
{ clickHandlers }: { clickHandlers?: boolean } = {}
) {
let isSelected;
let template = [];
const multiAccount = accounts.length > 1;
@ -56,14 +69,14 @@ function menuTemplate(accounts, sidebarAccountIds, { clickHandlers } = {}) {
return template;
}
function _focusAccounts(accounts) {
export function _focusAccounts(accounts) {
Actions.focusDefaultMailboxPerspectiveForAccounts(accounts);
if (!AppEnv.isVisible()) {
AppEnv.show();
}
}
function registerCommands(accounts) {
export function registerCommands(accounts) {
if (_commandsDisposable != null) {
_commandsDisposable.dispose();
}
@ -84,7 +97,7 @@ function registerCommands(accounts) {
_commandsDisposable = AppEnv.commands.add(document.body, commands);
}
function registerMenuItems(accounts, sidebarAccountIds) {
export function registerMenuItems(accounts: Account[], sidebarAccountIds: string[]) {
const windowMenu = AppEnv.menu.template.find(
({ label }) => MenuHelpers.normalizeLabel(label) === 'Window'
);
@ -92,7 +105,7 @@ function registerMenuItems(accounts, sidebarAccountIds) {
return;
}
const submenu = windowMenu.submenu.filter(item => !item.account);
const submenu = windowMenu.submenu.filter(item => !(item as any).account);
if (!submenu) {
return;
}
@ -108,15 +121,7 @@ function registerMenuItems(accounts, sidebarAccountIds) {
AppEnv.menu.update();
}
function register(accounts, sidebarAccountIds) {
export function register(accounts: Account[], sidebarAccountIds: string[]) {
registerCommands(accounts);
registerMenuItems(accounts, sidebarAccountIds);
}
module.exports = {
register,
registerCommands,
registerMenuItems,
menuTemplate,
menuItem,
};

View file

@ -1,10 +1,18 @@
const React = require('react');
const { Utils, AccountStore } = require('mailspring-exports');
const { OutlineView, ScrollRegion, Flexbox } = require('mailspring-component-kit');
const AccountSwitcher = require('./account-switcher');
const SidebarStore = require('../sidebar-store');
import React from 'react';
import { Utils, Account, AccountStore } from 'mailspring-exports';
import { OutlineView, ScrollRegion, Flexbox } from 'mailspring-component-kit';
import AccountSwitcher from './account-switcher';
import SidebarStore from '../sidebar-store';
import { ISidebarSection, ISidebarItem } from '../types';
class AccountSidebar extends React.Component {
interface AccountSidebarState {
accounts: Account[];
sidebarAccountIds: string[];
userSections: ISidebarSection[];
standardSection: ISidebarSection;
}
export default class AccountSidebar extends React.Component<{}, AccountSidebarState> {
static displayName = 'AccountSidebar';
static containerRequired = false;
@ -13,6 +21,8 @@ class AccountSidebar extends React.Component {
maxWidth: 250,
};
unsubscribers = [];
constructor(props) {
super(props);
@ -20,7 +30,6 @@ class AccountSidebar extends React.Component {
}
componentDidMount() {
this.unsubscribers = [];
this.unsubscribers.push(SidebarStore.listen(this._onStoreChange));
return this.unsubscribers.push(AccountStore.listen(this._onStoreChange));
}
@ -66,5 +75,3 @@ class AccountSidebar extends React.Component {
);
}
}
module.exports = AccountSidebar;

View file

@ -1,8 +1,13 @@
const { localized, Actions, React, PropTypes } = require('mailspring-exports');
const { RetinaImg } = require('mailspring-component-kit');
const AccountCommands = require('../account-commands');
import React from 'react';
import { Account, localized, Actions, PropTypes } from 'mailspring-exports';
import { RetinaImg } from 'mailspring-component-kit';
import { ipcRenderer, remote } from 'electron';
import * as AccountCommands from '../account-commands';
class AccountSwitcher extends React.Component {
export default class AccountSwitcher extends React.Component<{
accounts: Account[];
sidebarAccountIds: string[];
}> {
static displayName = 'AccountSwitcher';
static propTypes = {
@ -25,8 +30,7 @@ class AccountSwitcher extends React.Component {
// Handlers
_onAddAccount = () => {
const ipc = require('electron').ipcRenderer;
ipc.send('command', 'application:add-account');
ipcRenderer.send('command', 'application:add-account');
};
_onManageAccounts = () => {
@ -35,9 +39,7 @@ class AccountSwitcher extends React.Component {
};
_onShowMenu = () => {
const { remote } = require('electron');
const { Menu } = remote;
const menu = Menu.buildFromTemplate(this._makeMenuTemplate());
const menu = remote.Menu.buildFromTemplate(this._makeMenuTemplate());
menu.popup({});
};
@ -53,5 +55,3 @@ class AccountSwitcher extends React.Component {
);
}
}
module.exports = AccountSwitcher;

View file

@ -1,16 +0,0 @@
const AccountSidebar = require('./components/account-sidebar');
const { ComponentRegistry, WorkspaceStore } = require('mailspring-exports');
module.exports = {
item: null, // The DOM item the main React component renders into
activate(state) {
this.state = state;
ComponentRegistry.register(AccountSidebar, { location: WorkspaceStore.Location.RootSidebar });
},
deactivate(state) {
this.state = state;
ComponentRegistry.unregister(AccountSidebar);
},
};

View file

@ -0,0 +1,10 @@
import { ComponentRegistry, WorkspaceStore } from 'mailspring-exports';
import AccountSidebar from './components/account-sidebar';
export function activate(state) {
ComponentRegistry.register(AccountSidebar, { location: WorkspaceStore.Location.RootSidebar });
}
export function deactivate(state) {
ComponentRegistry.unregister(AccountSidebar);
}

View file

@ -5,11 +5,8 @@
*/
const Reflux = require('reflux');
const Actions = ['focusAccounts', 'setKeyCollapsed'];
export const focusAccounts = Reflux.createAction('focusAccounts');
focusAccounts.sync = true;
for (let idx of Array.from(Actions)) {
Actions[idx] = Reflux.createAction(Actions[idx]);
Actions[idx].sync = true;
}
module.exports = Actions;
export const setKeyCollapsed = Reflux.createAction('setKeyCollapsed');
setKeyCollapsed.sync = true;

View file

@ -1,7 +1,7 @@
const _ = require('underscore');
const _str = require('underscore.string');
const { OutlineViewItem } = require('mailspring-component-kit');
const {
import _ from 'underscore';
import _str from 'underscore.string';
import { OutlineViewItem } from 'mailspring-component-kit';
import {
MailboxPerspective,
FocusedPerspectiveStore,
SyncbackCategoryTask,
@ -9,9 +9,10 @@ const {
CategoryStore,
Actions,
RegExpUtils,
} = require('mailspring-exports');
} from 'mailspring-exports';
const SidebarActions = require('./sidebar-actions');
import * as SidebarActions from './sidebar-actions';
import { ISidebarItem } from './types';
const idForCategories = categories => _.pluck(categories, 'id').join('-');
@ -95,8 +96,8 @@ const onEditItem = function(item, value) {
);
};
class SidebarItem {
static forPerspective(id, perspective, opts = {}) {
export default class SidebarItem {
static forPerspective(id, perspective, opts: Partial<ISidebarItem> = {}): ISidebarItem {
let counterStyle;
if (perspective.isInbox()) {
counterStyle = OutlineViewItem.CounterStyles.Alt;
@ -121,7 +122,7 @@ class SidebarItem {
onCollapseToggled: toggleItemCollapsed,
onDrop(item, event) {
const jsonString = event.dataTransfer.getData('nylas-threads-data');
const jsonString = event.dataTransfer.getData('mailspring-threads-data');
let jsonData = null;
try {
jsonData = JSON.parse(jsonString);
@ -137,7 +138,7 @@ class SidebarItem {
shouldAcceptDrop(item, event) {
const target = item.perspective;
const current = FocusedPerspectiveStore.current();
if (!event.dataTransfer.types.includes('nylas-threads-data')) {
if (!event.dataTransfer.types.includes('mailspring-threads-data')) {
return false;
}
if (target.isEqual(current)) {
@ -146,8 +147,8 @@ class SidebarItem {
// We can't inspect the drag payload until drop, so we use a dataTransfer
// type to encode the account IDs of threads currently being dragged.
const accountsType = event.dataTransfer.types.find(t => t.startsWith('nylas-accounts='));
const accountIds = (accountsType || '').replace('nylas-accounts=', '').split(',');
const accountsType = event.dataTransfer.types.find(t => t.startsWith('mailspring-accounts='));
const accountIds = (accountsType || '').replace('mailspring-accounts=', '').split(',');
return target.canReceiveThreadsFromAccountIds(accountIds);
},
@ -159,7 +160,7 @@ class SidebarItem {
);
}
static forCategories(categories = [], opts = {}) {
static forCategories(categories = [], opts: Partial<ISidebarItem> = {}) {
const id = idForCategories(categories);
const contextMenuLabel = _str.capitalize(
categories[0] != null ? categories[0].displayType() : undefined
@ -176,7 +177,7 @@ class SidebarItem {
return this.forPerspective(id, perspective, opts);
}
static forStarred(accountIds, opts = {}) {
static forStarred(accountIds, opts: Partial<ISidebarItem> = {}) {
const perspective = MailboxPerspective.forStarred(accountIds);
let id = 'Starred';
if (opts.name) {
@ -185,7 +186,7 @@ class SidebarItem {
return this.forPerspective(id, perspective, opts);
}
static forUnread(accountIds, opts = {}) {
static forUnread(accountIds, opts: Partial<ISidebarItem> = {}) {
let categories = accountIds.map(accId => {
return CategoryStore.getCategoryByRole(accId, 'inbox');
});
@ -205,11 +206,9 @@ class SidebarItem {
return this.forPerspective(id, perspective, opts);
}
static forDrafts(accountIds, opts = {}) {
static forDrafts(accountIds, opts: Partial<ISidebarItem> = {}) {
const perspective = MailboxPerspective.forDrafts(accountIds);
const id = `Drafts-${opts.name}`;
return this.forPerspective(id, perspective, opts);
}
}
module.exports = SidebarItem;

View file

@ -4,19 +4,21 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const _ = require('underscore');
const {
import _ from 'underscore';
import {
Actions,
Account,
SyncbackCategoryTask,
CategoryStore,
Label,
ExtensionRegistry,
RegExpUtils,
localized,
} = require('mailspring-exports');
} from 'mailspring-exports';
const SidebarItem = require('./sidebar-item');
const SidebarActions = require('./sidebar-actions');
import SidebarItem from './sidebar-item';
import * as SidebarActions from './sidebar-actions';
import { ISidebarSection } from './types';
function isSectionCollapsed(title) {
if (AppEnv.savedState.sidebarKeysCollapsed[title] !== undefined) {
@ -34,14 +36,14 @@ function toggleSectionCollapsed(section) {
}
class SidebarSection {
static empty(title) {
static empty(title): ISidebarSection {
return {
title,
items: [],
};
}
static standardSectionForAccount(account) {
static standardSectionForAccount(account): ISidebarSection {
if (!account) {
throw new Error('standardSectionForAccount: You must pass an account.');
}
@ -81,7 +83,7 @@ class SidebarSection {
};
}
static standardSectionForAccounts(accounts) {
static standardSectionForAccounts(accounts?: Account[]): ISidebarSection {
let children;
if (!accounts || accounts.length === 0) {
return this.empty(localized('All Accounts'));
@ -115,7 +117,7 @@ class SidebarSection {
// eslint-disable-next-line
accounts.forEach(acc => {
const cat = _.first(
_.compact(names.map(name => CategoryStore.getCategoryByRole(acc, name)))
_.compact((names as string[]).map(name => CategoryStore.getCategoryByRole(acc, name)))
);
if (!cat) {
return;
@ -174,7 +176,10 @@ class SidebarSection {
};
}
static forUserCategories(account, { title, collapsible } = {}) {
static forUserCategories(
account,
{ title, collapsible }: { title?: string; collapsible?: boolean } = {}
): ISidebarSection {
let onCollapseToggled;
if (!account) {
return;
@ -256,4 +261,4 @@ class SidebarSection {
}
}
module.exports = SidebarSection;
export default SidebarSection;

View file

@ -1,18 +1,21 @@
const _ = require('underscore');
const MailspringStore = require('mailspring-store').default;
const {
import _ from 'underscore';
import MailspringStore from 'mailspring-store';
import {
Actions,
Account,
AccountStore,
ThreadCountsStore,
WorkspaceStore,
OutboxStore,
FocusedPerspectiveStore,
CategoryStore,
} = require('mailspring-exports');
} from 'mailspring-exports';
const SidebarSection = require('./sidebar-section');
const SidebarActions = require('./sidebar-actions');
const AccountCommands = require('./account-commands');
import SidebarSection from './sidebar-section';
import * as SidebarActions from './sidebar-actions';
import * as AccountCommands from './account-commands';
import { Disposable } from 'event-kit';
import { ISidebarItem, ISidebarSection } from './types';
const Sections = {
Standard: 'Standard',
@ -20,16 +23,21 @@ const Sections = {
};
class SidebarStore extends MailspringStore {
_sections: {
Standard: ISidebarSection;
User: ISidebarSection[];
} = {
Standard: { title: '', items: [] },
User: [],
};
configSubscription: Disposable;
constructor() {
super();
if (AppEnv.savedState.sidebarKeysCollapsed == null) {
AppEnv.savedState.sidebarKeysCollapsed = {};
}
this._sections = {};
this._sections[Sections.Standard] = {};
this._sections[Sections.User] = [];
this._registerCommands();
this._registerMenuItems();
this._registerListeners();
@ -45,11 +53,11 @@ class SidebarStore extends MailspringStore {
}
standardSection() {
return this._sections[Sections.Standard];
return this._sections.Standard;
}
userSections() {
return this._sections[Sections.User];
return this._sections.User;
}
_registerListeners() {
@ -68,7 +76,7 @@ class SidebarStore extends MailspringStore {
);
}
_onSetCollapsedByKey = (itemKey, collapsed) => {
_onSetCollapsedByKey = (itemKey: string, collapsed: boolean) => {
const currentValue = AppEnv.savedState.sidebarKeysCollapsed[itemKey];
if (currentValue !== collapsed) {
AppEnv.savedState.sidebarKeysCollapsed[itemKey] = collapsed;
@ -76,8 +84,8 @@ class SidebarStore extends MailspringStore {
}
};
_onSetCollapsedByName = (itemName, collapsed) => {
let item = _.findWhere(this.standardSection().items, { name: itemName });
_onSetCollapsedByName = (itemName: string, collapsed: boolean) => {
let item = this.standardSection().items.find(i => i.name === itemName);
if (!item) {
for (let section of this.userSections()) {
item = _.findWhere(section.items, { name: itemName });
@ -92,14 +100,14 @@ class SidebarStore extends MailspringStore {
this._onSetCollapsedByKey(item.id, collapsed);
};
_registerCommands = accounts => {
_registerCommands = (accounts: Account[] = null) => {
if (accounts == null) {
accounts = AccountStore.accounts();
}
AccountCommands.registerCommands(accounts);
};
_registerMenuItems = accounts => {
_registerMenuItems = (accounts: Account[] = null) => {
if (accounts == null) {
accounts = AccountStore.accounts();
}
@ -144,7 +152,7 @@ class SidebarStore extends MailspringStore {
this._sections[Sections.Standard] = SidebarSection.standardSectionForAccounts(accounts);
this._sections[Sections.User] = accounts.map(function(acc) {
const opts = {};
const opts: { title?: string; collapsible?: boolean } = {};
if (multiAccount) {
opts.title = acc.label;
opts.collapsible = true;
@ -155,4 +163,4 @@ class SidebarStore extends MailspringStore {
};
}
module.exports = new SidebarStore();
export default new SidebarStore();

View file

@ -0,0 +1,32 @@
import { MailboxPerspective } from 'mailspring-exports';
export interface ISidebarItem {
id: string;
name: string;
contextMenuLabel: string;
count: number;
iconName: string;
children: ISidebarItem[];
perspective: MailboxPerspective;
selected: boolean;
collapsed: boolean;
counterStyle: string;
onDelete?: () => void;
onEdited?: () => void;
onCollapseToggled: () => void;
onDrop: (item, event) => void;
shouldAcceptDrop: (item, event) => void;
onSelect: (item) => void;
deletable?: boolean;
editable?: boolean;
}
export interface ISidebarSection {
title: string;
items: ISidebarItem[];
iconName?: string;
collapsed?: boolean;
onCollapseToggled?: () => void;
onItemCreated?: (displayName) => void;
}

View file

@ -6,7 +6,7 @@ describe('sidebar-item', function sidebarItemSpec() {
spyOn(Actions, 'queueTask');
const categories = [new Folder({ path: 'a.b/c', accountId: window.TEST_ACCOUNT_ID })];
AppEnv.savedState.sidebarKeysCollapsed = {};
const item = SidebarItem.forCategories(categories);
const item = SidebarItem.forCategories(categories) as any;
item.onEdited(item, 'd');
const task = Actions.queueTask.calls[0].args[0];
@ -19,7 +19,7 @@ describe('sidebar-item', function sidebarItemSpec() {
const categories = [new Folder({ path: 'a', accountId: window.TEST_ACCOUNT_ID })];
AppEnv.savedState.sidebarKeysCollapsed = {};
const item = SidebarItem.forCategories(categories);
item.onEdited(item, 'b');
item.onEdited(item, 'b') as any;
const task = Actions.queueTask.calls[0].args[0];
const { existingPath, path } = task;

View file

@ -1,9 +0,0 @@
import Reflux from 'reflux';
const ActivityActions = Reflux.createActions(['markViewed']);
for (const key of Object.keys(ActivityActions)) {
ActivityActions[key].sync = true;
}
export default ActivityActions;

View file

@ -0,0 +1,4 @@
import Reflux from 'reflux';
export const markViewed = Reflux.createAction('markViewed');
markViewed.sync = true;

View file

@ -2,10 +2,13 @@ import { Rx, Message, DatabaseStore } from 'mailspring-exports';
import { OPEN_TRACKING_ID, LINK_TRACKING_ID } from './plugin-helpers';
export default class ActivityDataSource {
observable: Rx.Observable<Message[]>;
buildObservable({ messageLimit }) {
const query = DatabaseStore.findAll(Message)
const query = DatabaseStore.findAll<Message>(Message)
.order(Message.attributes.date.descending())
.where(Message.attributes.pluginMetadata.contains(OPEN_TRACKING_ID, LINK_TRACKING_ID))
.where(Message.attributes.pluginMetadata.containsAny([OPEN_TRACKING_ID, LINK_TRACKING_ID]))
.distinct()
.limit(messageLimit);
this.observable = Rx.Observable.fromQuery(query);
return this.observable;

View file

@ -3,12 +3,13 @@ import {
localized,
Actions,
Thread,
Message,
DatabaseStore,
NativeNotifications,
FocusedPerspectiveStore,
} from 'mailspring-exports';
import ActivityActions from './activity-actions';
import * as ActivityActions from './activity-actions';
import ActivityDataSource from './activity-data-source';
import { configForPluginId, LINK_TRACKING_ID, OPEN_TRACKING_ID } from './plugin-helpers';
@ -22,14 +23,16 @@ export function pluckByEmail(recipients, email) {
}
class ActivityEventStore extends MailspringStore {
_throttlingTimestamps = {};
_actions = [];
_unreadCount = 0;
_messages?: Message[];
_subscription: Rx.IDisposable;
activate() {
this.listenTo(ActivityActions.markViewed, this._onMarkViewed);
this.listenTo(FocusedPerspectiveStore, this._onUpdateActivity);
this._throttlingTimestamps = {};
this._actions = [];
this._unreadCount = 0;
const start = () => {
this._subscription = new ActivityDataSource()
.buildObservable({
@ -78,10 +81,10 @@ class ActivityEventStore extends MailspringStore {
focusThread(threadId) {
AppEnv.displayWindow();
Actions.closePopover();
DatabaseStore.find(Thread, threadId).then(thread => {
DatabaseStore.find<Thread>(Thread, threadId).then(thread => {
if (!thread) {
AppEnv.reportError(
new Error(`ActivityEventStore::focusThread: Can't find thread`, { threadId })
new Error(`ActivityEventStore::focusThread: Can't find thread: ${threadId}`)
);
AppEnv.showErrorDialog(localized(`Can't find the selected thread in your mailbox`));
return;

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { RetinaImg } from 'mailspring-component-kit';
export default class LoadingCover extends React.Component {
export default class LoadingCover extends React.Component<{ active: boolean }> {
static propTypes = {
active: PropTypes.bool,
};

View file

@ -50,8 +50,11 @@ export function activate() {
location: WorkspaceStore.Location.ActivityContent,
});
const { perspective } = AppEnv.savedState || {};
if (perspective && perspective.type === 'ActivityMailboxPerspective') {
if (
AppEnv.savedState &&
AppEnv.savedState.perspective &&
AppEnv.savedState.perspective.type === 'ActivityMailboxPerspective'
) {
Actions.selectRootSheet(WorkspaceStore.Sheet.Activity);
}
}

View file

@ -1,8 +1,9 @@
import React from 'react';
import { RetinaImg } from 'mailspring-component-kit';
import { localized, isRTL } from 'mailspring-exports';
import { SubjectStatsEntry } from './root';
export class MetricContainer extends React.Component {
export class MetricContainer extends React.Component<{ name: string }> {
render() {
return (
<div className="metric-container">
@ -13,7 +14,14 @@ export class MetricContainer extends React.Component {
}
}
export class MetricStat extends React.Component {
export class MetricStat extends React.Component<{
name: string;
units: string;
value: number;
loading: boolean;
}> {
_el: HTMLDivElement;
render() {
const { value, units, name } = this.props;
@ -42,7 +50,14 @@ export class MetricStat extends React.Component {
}
}
export class MetricHistogram extends React.Component {
export class MetricHistogram extends React.Component<{
loading: boolean;
values: number[];
left: React.ReactChild;
right: React.ReactChild;
}> {
_el: HTMLDivElement;
componentDidMount() {
if (!this.props.loading) {
window.requestAnimationFrame(() => this._el && this._el.classList.add('visible'));
@ -82,7 +97,9 @@ export class MetricHistogram extends React.Component {
}
}
export class MetricGraph extends React.Component {
export class MetricGraph extends React.Component<{ loading: boolean; values: number[] }> {
_el: HTMLDivElement;
componentDidMount() {
if (!this.props.loading) {
window.setTimeout(() => this._el && this._el.classList.add('visible'), 50);
@ -147,7 +164,7 @@ export class MetricGraph extends React.Component {
}
}
export class MetricsBySubjectTable extends React.Component {
export class MetricsBySubjectTable extends React.Component<{ data: SubjectStatsEntry[] }> {
render() {
const { data } = this.props;

View file

@ -26,11 +26,57 @@ import { LINK_TRACKING_ID, OPEN_TRACKING_ID } from '../plugin-helpers';
import { DEFAULT_TIMESPAN_ID, getTimespanStartEnd } from './timespan';
import TimespanSelector from './timespan-selector';
import LoadingCover from './loading-cover';
import { Moment } from 'moment';
const CHUNK_SIZE = 500;
const MINIMUM_THINKING_TIME = 2000;
class RootWithTimespan extends React.Component {
export interface Timespan {
id: string;
startDate: Moment;
endDate: Moment;
days: number;
}
export interface ThreadStatEntry {
outbound: boolean;
subject?: string;
tracked?: boolean;
hasReply?: boolean;
opened?: boolean;
clicked?: boolean;
}
export interface SubjectStatsEntry {
subject: string;
count: number;
opens: number;
clicks: number;
replies: number;
}
interface RootState {
loading: boolean;
version: number;
metricsBySubjectLine: SubjectStatsEntry[];
metrics: {
receivedByDay: number[];
receivedTimeOfDay: number[];
sentByDay: number[];
percentUsingTracking: number;
percentOpened: number;
percentLinkClicked: number;
percentReplied: number;
};
}
class RootWithTimespan extends React.Component<
{
timespan: Timespan;
accountIds: string[];
},
RootState
> {
static displayName = 'ActivityDashboardRootWithTimespan';
static propTypes = {
@ -38,6 +84,8 @@ class RootWithTimespan extends React.Component {
accountIds: PropTypes.arrayOf(PropTypes.string),
};
_mounted: boolean = false;
constructor(props) {
super(props);
this.state = this.getLoadingState(props);
@ -110,7 +158,7 @@ class RootWithTimespan extends React.Component {
let openTrackingTriggered = 0;
let linkTrackingEnabled = 0;
let linkTrackingTriggered = 0;
const threadStats = {};
const threadStats: { [threadId: string]: ThreadStatEntry } = {};
await this._forEachMessageIn(accountIds, startUnix, endUnix, (message, messageUnix) => {
const dayIdx = Math.floor((messageUnix - startUnix) / dayUnix);
@ -183,7 +231,7 @@ class RootWithTimespan extends React.Component {
}
// Aggregate open/link tracking of outbound threads by subject line
let bySubject = {};
let bySubject: { [subject: string]: SubjectStatsEntry } = {};
for (const stats of outboundThreadStats) {
if (!stats.tracked) {
continue;
@ -238,9 +286,9 @@ class RootWithTimespan extends React.Component {
};
_onFetchChunk(accountIds, startUnix, endUnix) {
return new Promise(resolve => {
return new Promise<Message[]>(resolve => {
window.requestAnimationFrame(() => {
DatabaseStore.findAll(Message)
DatabaseStore.findAll<Message>(Message)
.background()
.where(Message.attributes.accountId.in(accountIds))
.where(Message.attributes.date.greaterThan(startUnix))
@ -441,7 +489,7 @@ class RootWithTimespan extends React.Component {
}
}
class Root extends React.Component {
class Root extends React.Component<{ accountIds: string[] }, { timespan: Timespan }> {
static displayName = 'ActivityDashboardRoot';
static propTypes = {

View file

@ -39,7 +39,10 @@ function buildShareHTML(htmlEl, styleEl) {
`;
}
export default class ShareButton extends React.Component {
export default class ShareButton extends React.Component<{}, { link: string; loading: boolean }> {
_mounted: boolean = false;
_linkEl: HTMLInputElement;
constructor(props) {
super(props);
this.state = {

View file

@ -4,8 +4,12 @@ import PropTypes from 'prop-types';
import { localized } from 'mailspring-exports';
import { DropdownMenu, Menu } from 'mailspring-component-kit';
import { getTimespanOptions } from './timespan';
import { Timespan } from './root';
export default class TimespanSelector extends React.Component {
export default class TimespanSelector extends React.Component<{
timespan: Timespan;
onChange: (id: string) => void;
}> {
static propTypes = {
timespan: PropTypes.object,
onChange: PropTypes.func,

View file

@ -5,11 +5,13 @@ import { RetinaImg } from 'mailspring-component-kit';
import ActivityList from './activity-list';
import ActivityEventStore from '../activity-event-store';
class ActivityListButton extends React.Component {
class ActivityListButton extends React.Component<{}, { unreadCount: number | string }> {
static displayName = 'ActivityListButton';
constructor() {
super();
_unsub: () => void;
constructor(props) {
super(props);
this.state = this._getStateFromStores();
}
@ -22,7 +24,7 @@ class ActivityListButton extends React.Component {
}
onClick = () => {
const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
const buttonRect = (ReactDOM.findDOMNode(this) as HTMLElement).getBoundingClientRect();
Actions.openPopover(<ActivityList />, { originRect: buttonRect, direction: 'down' });
};

View file

@ -6,7 +6,7 @@ import { localized, DateUtils } from 'mailspring-exports';
import ActivityEventStore from '../activity-event-store';
import { configForPluginId } from '../plugin-helpers';
class ActivityListItemContainer extends React.Component {
class ActivityListItemContainer extends React.Component<{ group: any }, { collapsed: boolean }> {
static displayName = 'ActivityListItemContainer';
static propTypes = {

View file

@ -4,16 +4,21 @@ import { localized, Actions, FocusedPerspectiveStore } from 'mailspring-exports'
import { Flexbox, ScrollRegion, RetinaImg } from 'mailspring-component-kit';
import ActivityEventStore from '../activity-event-store';
import ActivityActions from '../activity-actions';
import * as ActivityActions from '../activity-actions';
import ActivityMailboxPerspective from '../activity-mailbox-perspective';
import ActivityListItemContainer from './activity-list-item-container';
import ActivityListEmptyState from './activity-list-empty-state';
class ActivityList extends React.Component {
class ActivityList extends React.Component<
{},
{ collapsedToggles: {}; empty: boolean; actions: any[] }
> {
static displayName = 'ActivityList';
constructor() {
super();
_unsub: () => void;
constructor(props) {
super(props);
this.state = this._getStateFromStores();
}
@ -31,7 +36,7 @@ class ActivityList extends React.Component {
};
_onViewSummary = () => {
if (document.activeElement) {
if (document.activeElement && document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
const aids = FocusedPerspectiveStore.sidebarAccountIds();
@ -90,7 +95,7 @@ class ActivityList extends React.Component {
empty: empty,
});
return (
<Flexbox direction="column" height="none" className={classes} tabIndex="-1">
<Flexbox direction="column" height="none" className={classes} tabIndex={-1}>
<ScrollRegion style={{ height: '100%' }}>{this.renderActions()}</ScrollRegion>
{!empty && (
<a className="activity-summary-cta" onClick={this._onViewSummary}>

View file

@ -46,8 +46,11 @@ export function activate() {
location: WorkspaceStore.Location.ActivityContent,
});
const { perspective } = AppEnv.savedState || {};
if (perspective && perspective.type === 'ActivityMailboxPerspective') {
if (
AppEnv.savedState &&
AppEnv.savedState.perspective &&
AppEnv.savedState.perspective.type === 'ActivityMailboxPerspective'
) {
Actions.selectRootSheet(WorkspaceStore.Sheet.Activity);
}

View file

@ -3,6 +3,8 @@ export default class TestDataSource {
return this;
}
onNext: (result: any) => void;
manuallyTrigger = (messages = []) => {
this.onNext(messages);
};
@ -10,9 +12,7 @@ export default class TestDataSource {
subscribe(onNext) {
this.onNext = onNext;
this.manuallyTrigger();
const dispose = () => {
this._unsub();
};
const dispose = () => {};
return { dispose };
}
}

View file

@ -125,7 +125,9 @@ describe('ActivityList', function activityList() {
beforeEach(() => {
this.testSource = new TestDataSource();
spyOn(ActivityEventStore, '_dataSource').andReturn(this.testSource);
spyOn(FocusedPerspectiveStore, 'sidebarAccountIds').andReturn(['0000000000000000000000000']);
spyOn(FocusedPerspectiveStore, 'sidebarAccountIds').andReturn([
'0000000000000000000000000',
]);
spyOn(DatabaseStore, 'run').andCallFake(query => {
if (query._klass === Thread) {
const thread = new Thread({

View file

@ -1,9 +1,19 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Actions, Utils, AttachmentStore } from 'mailspring-exports';
import { Actions, Utils, AttachmentStore, File } from 'mailspring-exports';
import { AttachmentItem, ImageAttachmentItem } from 'mailspring-component-kit';
class MessageAttachments extends Component {
interface MessageAttachmentsProps {
files: File[];
downloads: object;
headerMessageId: string;
filePreviewPaths: {
[fileId: string]: string;
};
canRemoveAttachments: boolean;
}
class MessageAttachments extends Component<MessageAttachmentsProps> {
static displayName = 'MessageAttachments';
static containerRequired = false;
@ -42,7 +52,7 @@ class MessageAttachments extends Component {
};
renderAttachment(AttachmentRenderer, file) {
const { canRemoveAttachments, downloads, filePreviewPaths, headerMessageId } = this.props;
const { canRemoveAttachments, downloads, filePreviewPaths } = this.props;
const download = downloads[file.id];
const filePath = AttachmentStore.pathForFile(file);
const fileIconName = `file-${file.displayExtension()}.png`;
@ -66,9 +76,7 @@ class MessageAttachments extends Component {
onOpenAttachment={() => this.onOpenAttachment(file)}
onDownloadAttachment={() => this.onDownloadAttachment(file)}
onAbortDownload={() => this.onAbortDownload(file)}
onRemoveAttachment={
canRemoveAttachments ? () => this.onRemoveAttachment(headerMessageId, file) : null
}
onRemoveAttachment={canRemoveAttachments ? () => this.onRemoveAttachment(file) : null}
/>
);
}

View file

@ -1,3 +1,4 @@
import React from 'react';
import utf7 from 'utf7';
import {
RetinaImg,
@ -5,9 +6,30 @@ import {
LabelColorizer,
BoldedSearchResult,
} from 'mailspring-component-kit';
import { localized, Label, Utils, React, PropTypes } from 'mailspring-exports';
import { localized, Label, Utils, PropTypes } from 'mailspring-exports';
export default class CategorySelection extends React.Component {
interface CategorySelectionProps {
accountUsesLabels: boolean;
all: CategoryItem[];
current: CategoryItem;
onSelect: (item: CategoryItem) => void;
}
interface CategorySelectionState {
searchValue: string;
}
type CategoryItem = {
backgroundColor?: string;
empty?: boolean;
path?: string;
name?: string;
};
export default class CategorySelection extends React.Component<
CategorySelectionProps,
CategorySelectionState
> {
static propTypes = {
accountUsesLabels: PropTypes.bool,
all: PropTypes.array,
@ -15,15 +37,13 @@ export default class CategorySelection extends React.Component {
onSelect: PropTypes.func,
};
constructor(props) {
super(props);
this._categories = [];
this.state = {
searchValue: '',
};
}
_categories = [];
_itemsForCategories() {
state = {
searchValue: '',
};
_itemsForCategories(): CategoryItem[] {
return this.props.all
.sort((a, b) => {
var pathA = utf7.imap.decode(a.path).toUpperCase();
@ -47,7 +67,7 @@ export default class CategorySelection extends React.Component {
this.setState({ searchValue: event.target.value });
};
_renderItem = (item = { empty: true }) => {
_renderItem = (item: CategoryItem = { empty: true }) => {
let icon;
if (item.empty) {
icon = <div className="empty-icon" />;
@ -82,7 +102,7 @@ export default class CategorySelection extends React.Component {
const headerComponents = [
<input
type="text"
tabIndex="-1"
tabIndex={-1}
key="textfield"
className="search"
placeholder={placeholder}

View file

@ -1,7 +1,7 @@
import React from 'react';
import {
AccountStore,
CategoryStore,
React,
Category,
Actions,
ChangeRoleMappingTask,
@ -11,9 +11,22 @@ import CategorySelection from './category-selection';
const SELECTABLE_ROLES = ['inbox', 'sent', 'drafts', 'spam', 'archive', 'trash'];
export default class PreferencesCategoryMapper extends React.Component {
constructor() {
super();
interface State {
assignments: {
[accountId: string]: {
[role: string]: Category;
};
};
all: {
[accountId: string]: Category[];
};
}
export default class PreferencesCategoryMapper extends React.Component<{}, State> {
_unlisten?: () => void;
constructor(props) {
super(props);
this.state = this._getStateFromStores();
}

View file

@ -1,5 +1,5 @@
/* eslint jsx-a11y/tabindex-no-positive: 0 */
import React, { Component } from 'react';
import React, { Component, CSSProperties } from 'react';
import PropTypes from 'prop-types';
import { Menu, RetinaImg, LabelColorizer, BoldedSearchResult } from 'mailspring-component-kit';
import {
@ -8,20 +8,38 @@ import {
Actions,
TaskQueue,
Label,
Account,
SyncbackCategoryTask,
ChangeLabelsTask,
Thread,
} from 'mailspring-exports';
import { Categories } from 'mailspring-observables';
import { CategoryData } from './types';
export default class LabelPickerPopover extends Component {
interface LabelPickerPopoverProps {
threads: Thread[];
account: Account;
}
interface LabelPickerPopoverState {
searchValue: string;
categoryData: CategoryData[];
}
export default class LabelPickerPopover extends Component<
LabelPickerPopoverProps,
LabelPickerPopoverState
> {
static propTypes = {
threads: PropTypes.array.isRequired,
account: PropTypes.object.isRequired,
};
_labels: Label[] = [];
disposables: Rx.Disposable[];
constructor(props) {
super(props);
this._labels = [];
this.state = this._recalculateState(this.props, { searchValue: '' });
}
@ -62,7 +80,7 @@ export default class LabelPickerPopover extends Component {
const categoryData = this._labels
.filter(label => Utils.wordSearchRegExp(searchValue).test(label.displayName))
.map(label => {
.map<CategoryData>(label => {
return {
id: label.id,
category: label,
@ -147,7 +165,7 @@ export default class LabelPickerPopover extends Component {
};
_renderCheckbox = item => {
const styles = {};
const styles: CSSProperties = {};
let checkStatus;
styles.backgroundColor = item.backgroundColor;
@ -222,7 +240,7 @@ export default class LabelPickerPopover extends Component {
const headerComponents = [
<input
type="text"
tabIndex="1"
tabIndex={1}
key="textfield"
className="search"
placeholder={localized('Label as...')}

View file

@ -1,12 +0,0 @@
const ToolbarCategoryPicker = require('./toolbar-category-picker');
const { ComponentRegistry } = require('mailspring-exports');
module.exports = {
activate() {
ComponentRegistry.register(ToolbarCategoryPicker, { role: 'ThreadActionsToolbarButton' });
},
deactivate() {
ComponentRegistry.unregister(ToolbarCategoryPicker);
},
};

View file

@ -0,0 +1,10 @@
import ToolbarCategoryPicker from './toolbar-category-picker';
import { ComponentRegistry } from 'mailspring-exports';;
export function activate() {
ComponentRegistry.register(ToolbarCategoryPicker, { role: 'ThreadActionsToolbarButton' });
}
export function deactivate() {
ComponentRegistry.unregister(ToolbarCategoryPicker);
}

View file

@ -7,6 +7,8 @@ import {
localized,
Actions,
TaskQueue,
Thread,
Account,
CategoryStore,
Folder,
SyncbackCategoryTask,
@ -15,17 +17,33 @@ import {
FocusedPerspectiveStore,
} from 'mailspring-exports';
import { Categories } from 'mailspring-observables';
import { CategoryData } from './types';
export default class MovePickerPopover extends Component {
interface MovePickerPopoverProps {
threads: Thread[];
account: Account;
}
interface MovePickerPopoverState {
searchValue: string;
categoryData: CategoryData[];
}
export default class MovePickerPopover extends Component<
MovePickerPopoverProps,
MovePickerPopoverState
> {
static propTypes = {
threads: PropTypes.array.isRequired,
account: PropTypes.object.isRequired,
};
_standardFolders = [];
_userCategories = [];
disposables: Rx.Disposable[];
constructor(props) {
super(props);
this._standardFolders = [];
this._userCategories = [];
this.state = this._recalculateState(this.props, { searchValue: '' });
}
@ -121,7 +139,7 @@ export default class MovePickerPopover extends Component {
}
if (item.newCategoryItem) {
this._onCreateCategory(item);
this._onCreateCategory();
} else {
this._onMoveToCategory(item);
}
@ -225,7 +243,7 @@ export default class MovePickerPopover extends Component {
const headerComponents = [
<input
type="text"
tabIndex="1"
tabIndex={1}
key="textfield"
className="search"
placeholder={localized('Move to...')}

View file

@ -1,24 +1,30 @@
const {
import React from 'react';
import {
localized,
Actions,
React,
Account,
PropTypes,
AccountStore,
WorkspaceStore,
} = require('mailspring-exports');
const { RetinaImg, KeyCommandsRegion } = require('mailspring-component-kit');
Thread,
} from 'mailspring-exports';
import { RetinaImg, KeyCommandsRegion } from 'mailspring-component-kit';
const MovePickerPopover = require('./move-picker-popover').default;
const LabelPickerPopover = require('./label-picker-popover').default;
import MovePickerPopover from './move-picker-popover';
import LabelPickerPopover from './label-picker-popover';
// This sets the folder / label on one or more threads.
class MovePicker extends React.Component {
class MovePicker extends React.Component<{ items: Thread[] }> {
static displayName = 'MovePicker';
static containerRequired = false;
static propTypes = { items: PropTypes.array };
static contextTypes = { sheetDepth: PropTypes.number };
_account: Account;
_labelEl: HTMLElement;
_moveEl: HTMLButtonElement;
constructor(props) {
super(props);
@ -101,4 +107,4 @@ class MovePicker extends React.Component {
}
}
module.exports = MovePicker;
export default MovePicker;

View file

@ -0,0 +1,15 @@
import { Category } from 'mailspring-exports';
export type CategoryData =
| {
searchValue: string;
newCategoryItem: boolean;
id: string;
}
| {
category: Category;
displayName: string;
backgroundColor: string;
usage: number;
numThreads: number;
};

View file

@ -1,9 +1,9 @@
const React = require('react');
const ReactDOM = require('react-dom');
const ReactTestUtils = require('react-dom/test-utils');
const MovePickerPopover = require('../lib/move-picker-popover').default;
import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import MovePickerPopover from '../lib/move-picker-popover';
const {
import {
Category,
Folder,
Thread,
@ -17,9 +17,9 @@ const {
MailboxPerspective,
MailspringTestUtils,
TaskQueue,
} = require('mailspring-exports');
} from 'mailspring-exports';
const { Categories } = require('mailspring-observables');
import { Categories } from 'mailspring-observables';
describe('MovePickerPopover', function() {
beforeEach(() => (CategoryStore._categoryCache = {}));

View file

@ -1,12 +1,16 @@
import { remote } from 'electron';
import React from 'react';
import {
React,
localized,
AccountStore,
SignatureStore,
Actions,
FocusedPerspectiveStore,
Utils,
ISignatureSet,
ISignature,
IDefaultSignatures,
IAliasSet,
} from 'mailspring-exports';
import { Flexbox, EditableList } from 'mailspring-component-kit';
@ -16,7 +20,15 @@ import SignatureTemplatePicker from './signature-template-picker';
import SignaturePhotoPicker from './signature-photo-picker';
import Templates from './templates';
class SignatureEditor extends React.Component {
interface SignatureEditorProps {
signature: ISignature;
defaults: IDefaultSignatures;
accountsAndAliases: IAliasSet;
}
interface SignatureEditorState {}
class SignatureEditor extends React.Component<SignatureEditorProps, SignatureEditorState> {
_onBaseFieldChange = event => {
const { id, value } = event.target;
const sig = this.props.signature;
@ -68,7 +80,12 @@ class SignatureEditor extends React.Component {
let signature = this.props.signature;
let empty = false;
if (!signature) {
signature = { data: { templateName: Templates[0].name } };
signature = {
id: '',
body: '',
title: '',
data: { title: '', templateName: Templates[0].name },
};
empty = true;
}
const data = signature.data || {};
@ -145,11 +162,20 @@ class SignatureEditor extends React.Component {
}
}
export default class PreferencesSignatures extends React.Component {
interface PreferencesSignaturesState {
signatures: ISignatureSet;
selectedSignature: ISignature;
defaults: IDefaultSignatures;
accountsAndAliases: IAliasSet;
}
export default class PreferencesSignatures extends React.Component<{}, PreferencesSignaturesState> {
static displayName = 'PreferencesSignatures';
constructor() {
super();
unsubscribers = [];
constructor(props) {
super(props);
this.state = this._getStateFromStores();
}

View file

@ -1,13 +1,23 @@
import { localized, React, PropTypes, Actions } from 'mailspring-exports';
import React from 'react';
import {
localized,
PropTypes,
Actions,
IAliasSet,
ISignature,
IDefaultSignatures,
} from 'mailspring-exports';
import { MultiselectDropdown } from 'mailspring-component-kit';
export default class SignatureAccountDefaultPicker extends React.Component {
static propTypes = {
defaults: PropTypes.object,
signature: PropTypes.object,
accountsAndAliases: PropTypes.array,
};
interface SignatureAccountDefaultPickerProps {
defaults: IDefaultSignatures;
signature: ISignature;
accountsAndAliases: IAliasSet;
}
export default class SignatureAccountDefaultPicker extends React.Component<
SignatureAccountDefaultPickerProps
> {
_onToggleAccount = account => {
Actions.toggleAccount(account.email);
};
@ -29,7 +39,6 @@ export default class SignatureAccountDefaultPicker extends React.Component {
itemChecked={isChecked}
itemContent={accountOrAlias => accountOrAlias.email}
itemKey={a => a.id}
current={signature}
attachment={'right'}
buttonText={`${checked.length} ${noun}`}
onToggleItem={this._onToggleAccount}

View file

@ -1,9 +1,29 @@
import { localized, React, Actions, PropTypes, SignatureStore } from 'mailspring-exports';
import React from 'react';
import {
localized,
Actions,
PropTypes,
SignatureStore,
Message,
DraftEditingSession,
Account,
ISignatureSet,
} from 'mailspring-exports';
import { Menu, RetinaImg, ButtonDropdown } from 'mailspring-component-kit';
import { applySignature, currentSignatureId } from './signature-utils';
export default class SignatureComposerDropdown extends React.Component {
const MenuItem = Menu.Item;
export default class SignatureComposerDropdown extends React.Component<
{
draft: Message;
draftFromEmail: string;
session: DraftEditingSession;
accounts: Account[];
},
{ signatures: ISignatureSet }
> {
static displayName = 'SignatureComposerDropdown';
static containerRequired = false;
@ -15,27 +35,32 @@ export default class SignatureComposerDropdown extends React.Component {
accounts: PropTypes.array,
};
_staticIcon = (
<RetinaImg
className="signature-button"
name="top-signature-dropdown.png"
mode={RetinaImg.Mode.ContentIsMask}
/>
);
_staticFooterItems = [
<div
key="edit"
className="item item-edit"
onMouseDown={() => {
Actions.switchPreferencesTab('Signatures');
Actions.openPreferences();
}}
>
<span>{localized('Edit Signatures...')}</span>
</div>,
];
unsubscribers: Array<() => void>;
constructor(props) {
super(props);
this.state = this._getStateFromStores();
this._staticIcon = (
<RetinaImg
className="signature-button"
name="top-signature-dropdown.png"
mode={RetinaImg.Mode.ContentIsMask}
/>
);
this._staticHeaderItems = [
<div className="item item-none" key="none" onMouseDown={this._onClickNoSignature}>
<span>{localized('No signature')}</span>
</div>,
];
this._staticFooterItems = [
<div className="item item-edit" key="edit" onMouseDown={this._onClickEditSignatures}>
<span>{localized('Edit Signatures...')}</span>
</div>,
];
}
componentDidMount = () => {
@ -75,21 +100,19 @@ export default class SignatureComposerDropdown extends React.Component {
this.props.session.changes.add({ body });
};
_onClickNoSignature = () => {
this._onChangeSignature(null);
};
_onClickEditSignatures() {
Actions.switchPreferencesTab('Signatures');
Actions.openPreferences();
}
_renderSignatures() {
// note: these are using onMouseDown to avoid clearing focus in the composer (I think)
return (
<Menu
headerComponents={this._staticHeaderItems}
headerComponents={[
<MenuItem
key={'none'}
onMouseDown={() => this._onChangeSignature(null)}
checked={!currentSignatureId(this.props.draft.body)}
content={localized('No signature')}
/>,
]}
footerComponents={this._staticFooterItems}
items={Object.values(this.state.signatures)}
itemKey={sig => sig.id}

View file

@ -1,9 +1,21 @@
import { localized, React, PropTypes, MailspringAPIRequest } from 'mailspring-exports';
import React from 'react';
import { localized, PropTypes, MailspringAPIRequest } from 'mailspring-exports';
import { RetinaImg, DropZone } from 'mailspring-component-kit';
const MAX_IMAGE_RES = 250;
export default class SignaturePhotoPicker extends React.Component {
export default class SignaturePhotoPicker extends React.Component<
{
id: string;
data: any;
resolvedURL: string;
onChange: (e: { target: { value: string; id?: string } }) => void;
},
{
isDropping?: boolean;
isUploading?: boolean;
}
> {
static propTypes = {
id: PropTypes.string,
data: PropTypes.object,
@ -11,6 +23,8 @@ export default class SignaturePhotoPicker extends React.Component {
onChange: PropTypes.func,
};
_isMounted: boolean;
constructor(props) {
super(props);
this.state = {
@ -61,7 +75,7 @@ export default class SignaturePhotoPicker extends React.Component {
img.onload = () => {
let scale = Math.min(MAX_IMAGE_RES / img.width, MAX_IMAGE_RES / img.height, 1);
let scaleDesired = scale;
let source = img;
let source: any = img;
let times = 0;
while (true) {
@ -160,7 +174,7 @@ export default class SignaturePhotoPicker extends React.Component {
onClick={this._onChooseImage}
onDragStateChange={({ isDropping }) => this.setState({ isDropping })}
onDrop={e => this._onChooseImageFilePath(e.dataTransfer.files[0].path)}
shouldAcceptDrop={e => e.dataTransfer.types.includes('Files')}
shouldAcceptDrop={e => (e as any).dataTransfer.types.includes('Files')}
style={{
backgroundImage: !isUploading && `url(${resolvedURL || emptyPlaceholderURL})`,
}}

View file

@ -1,12 +1,18 @@
import { localized, React, PropTypes } from 'mailspring-exports';
import React from 'react';
import { localized, PropTypes } from 'mailspring-exports';
import Templates from './templates';
export default class SignatureTemplatePicker extends React.Component {
export default class SignatureTemplatePicker extends React.Component<{
resolvedData: any;
onChange: (e: { target: { id: string; value: string } }) => void;
}> {
static propTypes = {
resolvedData: PropTypes.object,
onChange: PropTypes.func,
};
_el: HTMLElement;
_onClickItem = event => {
const value = event.currentTarget.dataset.value;
this.props.onChange({ target: { id: 'templateName', value } });
@ -21,7 +27,7 @@ export default class SignatureTemplatePicker extends React.Component {
}
ensureSelectionVisible() {
const item = this._el.querySelector('.active');
const item = this._el.querySelector('.active') as HTMLElement;
if (item) {
const left = item.offsetLeft - 5;
const right = item.offsetLeft + item.clientWidth + 5;

View file

@ -1,4 +1,4 @@
import { React } from 'mailspring-exports';
import React from 'react';
import querystring from 'querystring';
// Static components
@ -28,11 +28,14 @@ const LINKEDIN_SHARE = (
/>
);
function widthAndHeightForPhotoURL(photoURL, { maxWidth, maxHeight } = {}) {
function widthAndHeightForPhotoURL(
photoURL,
{ maxWidth, maxHeight }: { maxWidth?: number; maxHeight?: number } = {}
) {
if (!photoURL) {
return {};
}
let q = {};
let q: any = {};
try {
q = querystring.parse(photoURL.split('?').pop());
} catch (err) {
@ -71,7 +74,7 @@ const PrefixStyles = {
},
};
function GenericInfoBlock(props, prefixStyle = PrefixStyles.None) {
function GenericInfoBlock(props, prefixStyle: any = PrefixStyles.None) {
return (
<div>
{props.email && (
@ -199,7 +202,7 @@ const Templates = [
<table cellPadding={0} cellSpacing={0}>
<tbody>
<tr>
<td colSpan="2">
<td colSpan={2}>
<div style={{ paddingBottom: 15 }}>
{props.name && (
<div>
@ -365,7 +368,7 @@ const Templates = [
</td>
</tr>
<tr>
<td colSpan="2">
<td colSpan={2}>
<div
style={{
fontSize: '0.9em',

View file

@ -18,8 +18,8 @@ const SIGNATURES = {
};
const DEFAULTS = {
'one@nylas.com': '1',
'two@nylas.com': '2',
'one@mailspring.com': '1',
'two@mailspring.com': '2',
};
const makeComponent = (props = {}) => {

View file

@ -4,7 +4,7 @@ import React from 'react';
import ReactTestUtils from 'react-dom/test-utils';
import { SignatureStore } from 'mailspring-exports';
import SignatureComposerDropdown from '../lib/signature-composer-dropdown';
import { renderIntoDocument } from '../../../spec/mailspring-test-utils';
import MTestUtils from '../../../spec/mailspring-test-utils';
const SIGNATURES = {
'1': {
@ -31,7 +31,7 @@ describe('SignatureComposerDropdown', function signatureComposerDropdown() {
this.draft = {
body: 'draft body',
};
this.button = renderIntoDocument(
this.button = MTestUtils.renderIntoDocument(
<SignatureComposerDropdown draft={this.draft} session={this.session} />
);
});

View file

@ -25,13 +25,13 @@ describe('SignatureComposerExtension', function signatureComposerExtension() {
it('should insert the signature at the end of the message or before the first quoted text block and have a newline', () => {
const a = new Message({
draft: true,
from: ['one@nylas.com'],
from: ['one@mailspring.com'],
accountId: TEST_ACCOUNT_ID,
body: 'This is a test! <div class="gmail_quote">Hello world</div>',
});
const b = new Message({
draft: true,
from: ['one@nylas.com'],
from: ['one@mailspring.com'],
accountId: TEST_ACCOUNT_ID,
body: 'This is a another test.',
});
@ -79,7 +79,7 @@ describe('SignatureComposerExtension', function signatureComposerExtension() {
})`, () => {
const message = new Message({
draft: true,
from: ['one@nylas.com'],
from: ['one@mailspring.com'],
body: scenario.body,
accountId: TEST_ACCOUNT_ID,
});

View file

@ -57,7 +57,7 @@ describe('SignatureStore', function signatureStore() {
it('should reset selectedSignatureId to a different signature', () => {
const toRemove = SIGNATURES[SignatureStore.selectedSignatureId];
SignatureStore._onRemoveSignature(toRemove);
expect(SignatureStore.selectedSignatureId).toNotEqual('1');
expect(SignatureStore.selectedSignatureId).not.toEqual('1');
});
});
});

View file

@ -1,13 +1,29 @@
import fs from 'fs';
import React from 'react';
import ReactDOM from 'react-dom';
import { Flexbox, EditableList, ComposerEditor, ComposerSupport } from 'mailspring-component-kit';
import { Actions, localized, localizedReactFragment, React, ReactDOM } from 'mailspring-exports';
import { Actions, localized, localizedReactFragment } from 'mailspring-exports';
import { shell } from 'electron';
import { Value } from 'slate';
import TemplateStore from './template-store';
interface ITemplate {
path: string;
name: string;
}
const { Conversion: { convertFromHTML, convertToHTML } } = ComposerSupport;
class TemplateEditor extends React.Component {
interface TemplateEditorProps {
template: ITemplate;
onEditTitle: (title: string) => void;
}
class TemplateEditor extends React.Component<
TemplateEditorProps,
{ readOnly: boolean; editorState: Value }
> {
_composer: ComposerEditor;
constructor(props) {
super(props);
@ -57,7 +73,6 @@ class TemplateEditor extends React.Component {
<div className="section editor" onClick={this._onFocusEditor}>
<ComposerEditor
ref={c => (this._composer = c)}
readOnly={readOnly}
value={editorState}
propsForPlugins={{ inTemplateEditor: true }}
onChange={change => this.setState({ editorState: change.value })}
@ -77,11 +92,16 @@ class TemplateEditor extends React.Component {
}
}
export default class PreferencesTemplates extends React.Component {
export default class PreferencesTemplates extends React.Component<
{},
{ selected: ITemplate; templates: ITemplate[] }
> {
static displayName = 'PreferencesTemplates';
constructor() {
super();
unsubscribers: Array<() => void>;
constructor(props) {
super(props);
this.state = this._getStateFromStores();
}

View file

@ -1,22 +1,23 @@
/* eslint jsx-a11y/tabindex-no-positive: 0 */
import { localized, React, ReactDOM, PropTypes, Actions } from 'mailspring-exports';
import React from 'react';
import ReactDOM from 'react-dom';
import { localized, PropTypes, Actions } from 'mailspring-exports';
import { Menu, RetinaImg } from 'mailspring-component-kit';
import TemplateStore from './template-store';
class TemplatePopover extends React.Component {
class TemplatePopover extends React.Component<{ headerMessageId: string }> {
static displayName = 'TemplatePopover';
static propTypes = {
headerMessageId: PropTypes.string,
};
constructor() {
super();
this.state = {
searchValue: '',
templates: TemplateStore.items(),
};
}
unsubscribe?: () => void;
state = {
searchValue: '',
templates: TemplateStore.items(),
};
componentDidMount() {
this.unsubscribe = TemplateStore.listen(() => {
@ -62,18 +63,13 @@ class TemplatePopover extends React.Component {
Actions.createTemplate({ headerMessageId: this.props.headerMessageId });
};
_onClickButton = () => {
const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
Actions.openPopover(this._renderPopover(), { originRect: buttonRect, direction: 'up' });
};
render() {
const filteredTemplates = this._filteredTemplates();
const headerComponents = [
<input
type="text"
tabIndex="1"
tabIndex={1}
key="textfield"
className="search"
value={this.state.searchValue}
@ -105,7 +101,7 @@ class TemplatePopover extends React.Component {
}
}
class TemplatePicker extends React.Component {
class TemplatePicker extends React.Component<{ headerMessageId: string }> {
static displayName = 'TemplatePicker';
static propTypes = {
@ -113,7 +109,7 @@ class TemplatePicker extends React.Component {
};
_onClickButton = () => {
const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
const buttonRect = (ReactDOM.findDOMNode(this) as HTMLElement).getBoundingClientRect();
Actions.openPopover(<TemplatePopover headerMessageId={this.props.headerMessageId} />, {
originRect: buttonRect,
direction: 'up',

View file

@ -1,8 +1,15 @@
import { localized, React, PropTypes } from 'mailspring-exports';
import React from 'react';
import { localized, PropTypes, Message } from 'mailspring-exports';
class TemplateStatusBar extends React.Component {
class TemplateStatusBar extends React.Component<{ draft: Message }> {
static displayName = 'TemplateStatusBar';
static containerStyles = {
textAlign: 'center',
width: 580,
margin: 'auto',
};
static propTypes = {
draft: PropTypes.object.isRequired,
};
@ -29,10 +36,4 @@ class TemplateStatusBar extends React.Component {
}
}
TemplateStatusBar.containerStyles = {
textAlign: 'center',
width: 580,
margin: 'auto',
};
export default TemplateStatusBar;

View file

@ -17,6 +17,10 @@ import fs from 'fs';
const INVALID_TEMPLATE_NAME_REGEX = /[^a-zA-Z\u00C0-\u017F0-9_\- ]+/g;
class TemplateStore extends MailspringStore {
private _items = [];
private _templatesDir = path.join(AppEnv.getConfigDirPath(), 'templates');
private _watcher = null;
constructor() {
super();
@ -26,11 +30,8 @@ class TemplateStore extends MailspringStore {
this.listenTo(Actions.deleteTemplate, this._onDeleteTemplate);
this.listenTo(Actions.renameTemplate, this._onRenameTemplate);
this._items = [];
this._templatesDir = path.join(AppEnv.getConfigDirPath(), 'templates');
this._welcomeName = 'Welcome to Templates.html';
this._welcomePath = path.join(__dirname, '..', 'assets', this._welcomeName);
this._watcher = null;
const welcomeName = 'Welcome to Templates.html';
const welcomePath = path.join(__dirname, '..', 'assets', welcomeName);
// I know this is a bit of pain but don't do anything that
// could possibly slow down app launch
@ -40,8 +41,8 @@ class TemplateStore extends MailspringStore {
this.watch();
} else {
fs.mkdir(this._templatesDir, () => {
fs.readFile(this._welcomePath, (err, welcome) => {
fs.writeFile(path.join(this._templatesDir, this._welcomeName), welcome, () => {
fs.readFile(welcomePath, (err, welcome) => {
fs.writeFile(path.join(this._templatesDir, welcomeName), welcome, () => {
this.watch();
});
});
@ -105,7 +106,11 @@ class TemplateStore extends MailspringStore {
});
}
_onCreateTemplate({ headerMessageId, name, contents } = {}) {
_onCreateTemplate({
headerMessageId,
name,
contents,
}: { headerMessageId?: string; name?: string; contents?: string } = {}) {
if (headerMessageId) {
this._onCreateTemplateFromDraft(headerMessageId);
return;
@ -261,7 +266,10 @@ class TemplateStore extends MailspringStore {
});
}
_onInsertTemplateId({ templateId, headerMessageId } = {}) {
_onInsertTemplateId({
templateId,
headerMessageId,
}: { templateId?: string; headerMessageId?: string } = {}) {
const template = this._items.find(t => t.id === templateId);
const templateBody = fs.readFileSync(template.path).toString();
DraftStore.sessionForClientId(headerMessageId).then(session => {

View file

@ -6,14 +6,16 @@
// TranslateButton is a simple React component that allows you to select
// a language from a popup menu and translates draft text into that language.
import React from 'react';
import ReactDOM from 'react-dom';
import {
React,
ReactDOM,
PropTypes,
ComponentRegistry,
QuotedHTMLTransformer,
localized,
Actions,
Message,
DraftEditingSession,
} from 'mailspring-exports';
import { Menu, RetinaImg } from 'mailspring-component-kit';
@ -34,7 +36,7 @@ const YandexLanguages = {
Korean: 'ko',
};
class TranslateButton extends React.Component {
class TranslateButton extends React.Component<{ draft: Message; session: DraftEditingSession }> {
// Adding a `displayName` makes debugging React easier
static displayName = 'TranslateButton';
@ -96,7 +98,7 @@ class TranslateButton extends React.Component {
};
_onClickTranslateButton = () => {
const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
const buttonRect = (ReactDOM.findDOMNode(this) as HTMLElement).getBoundingClientRect();
Actions.openPopover(this._renderPopover(), { originRect: buttonRect, direction: 'up' });
};

View file

@ -1,10 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { localized, AccountStore, ContactStore } from 'mailspring-exports';
import {
localized,
AccountStore,
ContactStore,
Account,
Contact,
DraftEditingSession,
Message,
} from 'mailspring-exports';
import { Menu, ButtonDropdown, InjectedComponentSet } from 'mailspring-component-kit';
export default class AccountContactField extends React.Component {
interface AccountContactFieldProps {
accounts: Account[];
value: Contact;
session: DraftEditingSession;
draft: Message;
onChange: (val: { from: Contact[]; cc: Contact[]; bcc: Contact[] }) => void;
}
export default class AccountContactField extends React.Component<AccountContactFieldProps> {
static displayName = 'AccountContactField';
static propTypes = {
@ -15,6 +30,8 @@ export default class AccountContactField extends React.Component {
onChange: PropTypes.func.isRequired,
};
_dropdownComponent: ButtonDropdown;
_onChooseContact = async contact => {
const { draft, session, onChange } = this.props;
const { autoaddress } = AccountStore.accountForEmail(contact.email);
@ -96,7 +113,7 @@ export default class AccountContactField extends React.Component {
draft,
session,
accounts,
draftFromEmail: (draft.from[0] || {}).email,
draftFromEmail: draft.from[0] ? draft.from[0].email : undefined,
}}
/>
);

View file

@ -1,10 +1,18 @@
import classnames from 'classnames';
import { React, PropTypes, ComponentRegistry } from 'mailspring-exports';
import React from 'react';
import { PropTypes, ComponentRegistry, Message, DraftEditingSession } from 'mailspring-exports';
import { InjectedComponentSet } from 'mailspring-component-kit';
const ROLE = 'Composer:ActionButton';
export default class ActionBarPlugins extends React.Component {
export default class ActionBarPlugins extends React.Component<
{
draft: Message;
session: DraftEditingSession;
isValidDraft: () => boolean;
},
{ pluginsLoaded: boolean }
> {
static displayName = 'ActionBarPlugins';
static propTypes = {
@ -13,6 +21,8 @@ export default class ActionBarPlugins extends React.Component {
isValidDraft: PropTypes.func,
};
_usub: () => void;
constructor(props) {
super(props);
this.state = this._getStateFromStores();

View file

@ -4,7 +4,12 @@ import { localized, Actions } from 'mailspring-exports';
import { RetinaImg } from 'mailspring-component-kit';
import Fields from './fields';
export default class ComposerHeaderActions extends React.Component {
interface ComposerHeaderActionsProps {
headerMessageId: string;
enabledFields: string[];
onShowAndFocusField: (f: string) => void;
}
export default class ComposerHeaderActions extends React.Component<ComposerHeaderActionsProps> {
static displayName = 'ComposerHeaderActions';
static propTypes = {

View file

@ -1,4 +1,13 @@
import { localized, React, ReactDOM, PropTypes, Actions, AccountStore } from 'mailspring-exports';
import React from 'react';
import ReactDOM from 'react-dom';
import {
localized,
PropTypes,
Actions,
AccountStore,
Message,
DraftEditingSession,
} from 'mailspring-exports';
import {
KeyCommandsRegion,
ParticipantsTextField,
@ -19,7 +28,18 @@ const ScopedFromField = ListensToFluxStore(AccountContactField, {
},
});
export default class ComposerHeader extends React.Component {
interface ComposerHeaderProps {
draft: Message;
session: DraftEditingSession;
}
interface ComposerHeaderState {
enabledFields: string[];
}
export default class ComposerHeader extends React.Component<
ComposerHeaderProps,
ComposerHeaderState
> {
static displayName = 'ComposerHeader';
static propTypes = {
@ -31,7 +51,11 @@ export default class ComposerHeader extends React.Component {
parentTabGroup: PropTypes.object,
};
constructor(props = {}) {
private _els: {
participantsContainer?: KeyCommandsRegion;
} = {};
constructor(props) {
super(props);
this._els = {};
this.state = this._initialStateForDraft(this.props.draft, props);

View file

@ -1,13 +1,15 @@
import { remote } from 'electron';
import React from 'react';
import ReactDOM from 'react-dom';
import {
React,
ReactDOM,
localized,
PropTypes,
Utils,
Actions,
DraftStore,
AttachmentStore,
Message,
DraftEditingSession,
} from 'mailspring-exports';
import {
DropZone,
@ -34,10 +36,20 @@ const {
removeQuotedText,
} = ComposerSupport.BaseBlockPlugins;
interface ComposerViewProps {
draft: Message & { bodyEditorState: any };
session: DraftEditingSession;
className?: string;
}
interface ComposerViewState {
quotedTextHidden: boolean;
quotedTextPresent: boolean;
isDropping: boolean;
}
// The ComposerView is a unique React component because it (currently) is a
// singleton. Normally, the React way to do things would be to re-render the
// Composer with new props.
export default class ComposerView extends React.Component {
export default class ComposerView extends React.Component<ComposerViewProps, ComposerViewState> {
static displayName = 'ComposerView';
static propTypes = {
@ -46,22 +58,30 @@ export default class ComposerView extends React.Component {
className: PropTypes.string,
};
_mounted: boolean = false;
_mouseDownTarget: HTMLElement = null;
_dropzone: DropZone;
_els: { [key: string]: any } = {
composerWrap: null,
sendActionButton: SendActionButton,
[Fields.Body]: ComposerEditor,
header: ComposerHeader,
};
_keymapHandlers = {
'composer:send-message': () => this._onPrimarySend(),
'composer:delete-empty-draft': () => this.props.draft.pristine && this._onDestroyDraft(),
'composer:show-and-focus-bcc': () => this._els.header.showAndFocusField(Fields.Bcc),
'composer:show-and-focus-cc': () => this._els.header.showAndFocusField(Fields.Cc),
'composer:focus-to': () => this._els.header.showAndFocusField(Fields.To),
'composer:show-and-focus-from': () => {},
'composer:select-attachment': () => this._onSelectAttachment(),
};
constructor(props) {
super(props);
this._els = {};
this._mounted = false;
this._keymapHandlers = {
'composer:send-message': () => this._onPrimarySend(),
'composer:delete-empty-draft': () => this.props.draft.pristine && this._onDestroyDraft(),
'composer:show-and-focus-bcc': () => this._els.header.showAndFocusField(Fields.Bcc),
'composer:show-and-focus-cc': () => this._els.header.showAndFocusField(Fields.Cc),
'composer:focus-to': () => this._els.header.showAndFocusField(Fields.To),
'composer:show-and-focus-from': () => {},
'composer:select-attachment': () => this._onSelectAttachment(),
};
const draft = props.session.draft();
this.state = {
isDropping: false,
@ -78,9 +98,10 @@ export default class ComposerView extends React.Component {
}
});
const isBrandNew = Date.now() - this.props.draft.date < 3 * 1000;
const d = this.props.draft.date;
const isBrandNew = Date.now() - (d instanceof Date ? d.getTime() : Number(d)) < 3 * 1000;
if (isBrandNew) {
ReactDOM.findDOMNode(this).scrollIntoView(false);
(ReactDOM.findDOMNode(this) as HTMLElement).scrollIntoView(false);
window.requestAnimationFrame(() => {
this.focus();
});
@ -154,7 +175,6 @@ export default class ComposerView extends React.Component {
}}
draft={this.props.draft}
session={this.props.session}
initiallyFocused={this.props.draft.to.length === 0}
/>
<div
className="compose-body"
@ -191,7 +211,7 @@ export default class ComposerView extends React.Component {
<a
className="quoted-text-control"
onMouseDown={e => {
if (e.target.closest('.remove-quoted-text')) return;
if (e.currentTarget.closest('.remove-quoted-text')) return;
e.preventDefault();
e.stopPropagation();
this.setState({ quotedTextHidden: false });
@ -276,7 +296,7 @@ export default class ComposerView extends React.Component {
draggable={false}
filePath={AttachmentStore.pathForFile(file)}
displayName={file.filename}
fileIconName={`file-${file.extension}.png`}
fileIconName={`file-${file.displayExtension()}.png`}
onRemoveAttachment={() => Actions.removeAttachment(headerMessageId, file)}
/>
));
@ -384,7 +404,10 @@ export default class ComposerView extends React.Component {
if (event.target === this._mouseDownTarget && !this._inFooterRegion(event.target)) {
// We don't set state directly here because we want the native
// contenteditable focus behavior. When the contenteditable gets focused
const bodyRect = ReactDOM.findDOMNode(this._els[Fields.Body]).getBoundingClientRect();
const bodyRect = (ReactDOM.findDOMNode(
this._els[Fields.Body]
) as HTMLElement).getBoundingClientRect();
if (event.pageY < bodyRect.top) {
this._els[Fields.Body].focus();
} else {
@ -409,8 +432,8 @@ export default class ComposerView extends React.Component {
};
_nonNativeFilePathForDrop = event => {
if (event.dataTransfer.types.includes('text/nylas-file-url')) {
const downloadURL = event.dataTransfer.getData('text/nylas-file-url');
if (event.dataTransfer.types.includes('text/mailspring-file-url')) {
const downloadURL = event.dataTransfer.getData('text/mailspring-file-url');
const downloadFilePath = downloadURL.split('file://')[1];
if (downloadFilePath) {
return downloadFilePath;
@ -430,7 +453,7 @@ export default class ComposerView extends React.Component {
_onDrop = event => {
// Accept drops of real files from other applications
for (const file of Array.from(event.dataTransfer.files)) {
this._onFileReceived(file.path);
this._onFileReceived((file as any).path);
event.preventDefault();
}
@ -466,7 +489,7 @@ export default class ComposerView extends React.Component {
});
};
_isValidDraft = (options = {}) => {
_isValidDraft = (options: { force?: boolean } = {}) => {
// We need to check the `DraftStore` because the `DraftStore` is
// immediately and synchronously updated as soon as this function
// fires. Since `setState` is asynchronous, if we used that as our only
@ -530,7 +553,7 @@ export default class ComposerView extends React.Component {
this._els.composerWrap = el;
}
}}
tabIndex="-1"
tabIndex={-1}
>
<TabGroupRegion className="composer-inner-wrap">
<DropZone

View file

@ -5,6 +5,8 @@ const Fields = {
From: 'fromField',
Subject: 'textFieldSubject',
Body: 'contentBody',
ParticipantFields: [],
Order: {},
};
Fields.ParticipantFields = [Fields.To, Fields.Cc, Fields.Bcc];

View file

@ -14,10 +14,16 @@ import ComposerView from './composer-view';
const ComposerViewForDraftClientId = InflatesDraftClientId(ComposerView);
class ComposerWithWindowProps extends React.Component {
class ComposerWithWindowProps extends React.Component<
{},
{ headerMessageId: string; errorMessage?: string; errorDetail?: string }
> {
static displayName = 'ComposerWithWindowProps';
static containerRequired = false;
_usub?: () => void;
_composerComponent?: any;
constructor(props) {
super(props);
@ -27,7 +33,7 @@ class ComposerWithWindowProps extends React.Component {
if (!draftJSON) {
throw new Error('Initialize popout composer windows with valid draftJSON');
}
const draft = new Message().fromJSON(draftJSON);
const draft = new Message({}).fromJSON(draftJSON);
DraftStore._createSession(headerMessageId, draft);
this.state = windowProps;
}
@ -100,7 +106,7 @@ export function activate() {
const i = document.createElement('i');
i.className = 'fa fa-list';
i.style.position = 'absolute';
i.style.top = 0;
i.style.top = '0';
document.body.appendChild(i);
}, 1000);
}

View file

@ -1,24 +1,24 @@
import React from 'react';
import {
localized,
React,
PropTypes,
ISendAction,
Actions,
SendActionsStore,
SoundRegistry,
Message,
} from 'mailspring-exports';
import { Menu, RetinaImg, ButtonDropdown, ListensToFluxStore } from 'mailspring-component-kit';
class SendActionButton extends React.Component {
interface SendActionButtonProps {
draft: Message;
isValidDraft: () => boolean;
sendActions: ISendAction[];
}
class SendActionButton extends React.Component<SendActionButtonProps> {
static displayName = 'SendActionButton';
static containerRequired = false;
static propTypes = {
draft: PropTypes.object,
isValidDraft: PropTypes.func,
sendActions: PropTypes.array,
};
/* This component is re-rendered constantly because `draft` changes in random ways.
We only use the draft prop when you click send, so update with more discretion. */
shouldComponentUpdate(nextProps) {
@ -46,8 +46,8 @@ class SendActionButton extends React.Component {
};
_renderSendActionItem = ({ iconUrl }) => {
let plusHTML = '';
let additionalImg = false;
let plusHTML: React.ReactChild = '';
let additionalImg: React.ReactChild = null;
if (iconUrl) {
plusHTML = <span>&nbsp;+&nbsp;</span>;
@ -134,6 +134,6 @@ Object.assign(EnhancedSendActionButton.prototype, {
},
});
EnhancedSendActionButton.UndecoratedSendActionButton = SendActionButton;
(EnhancedSendActionButton as any).UndecoratedSendActionButton = SendActionButton;
export default EnhancedSendActionButton;

View file

@ -1,9 +1,9 @@
const React = require('react');
const ReactDOM = require('react-dom');
const ComposerHeaderActions = require('../lib/composer-header-actions').default;
const Fields = require('../lib/fields').default;
const ReactTestUtils = require('react-dom/test-utils');
const { Actions } = require('mailspring-exports');
import React from 'react';
import ReactDOM from 'react-dom';
import ComposerHeaderActions from '../lib/composer-header-actions';
import Fields from '../lib/fields';
import ReactTestUtils from 'react-dom/test-utils';
import { Actions } from 'mailspring-exports';;
describe('ComposerHeaderActions', function() {
const makeField = function(props) {

View file

@ -17,7 +17,7 @@ describe('ComposerHeader', function composerHeader() {
},
};
this.component = ReactTestUtils.renderIntoDocument(
<ComposerHeader draft={draft} initiallyFocused={false} session={session} />
<ComposerHeader draft={draft} session={session} />
);
};
advanceClock();

View file

@ -42,7 +42,7 @@ const ContentsColumn = new ListTabular.Column({
name: 'Contents',
flex: 4,
resolver: draft => {
let attachments = [];
let attachments: JSX.Element[] | JSX.Element = [];
if (draft.files && draft.files.length > 0) {
attachments = <div className="thread-icon thread-icon-attachment" />;
}
@ -71,6 +71,4 @@ const StatusColumn = new ListTabular.Column({
},
});
module.exports = {
Wide: [ParticipantsColumn, ContentsColumn, StatusColumn],
};
export const Wide = [ParticipantsColumn, ContentsColumn, StatusColumn];

View file

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { DateUtils } from 'mailspring-exports';
import { DateUtils, Message } from 'mailspring-exports';
import { Flexbox } from 'mailspring-component-kit';
import SendingProgressBar from './sending-progress-bar';
export default class DraftListSendStatus extends Component {
export default class DraftListSendStatus extends Component<{ draft: Message }> {
static displayName = 'DraftListSendStatus';
static propTypes = {
@ -15,16 +15,17 @@ export default class DraftListSendStatus extends Component {
render() {
const { draft } = this.props;
if (draft.uploadTaskId) {
return (
<Flexbox style={{ width: 150, whiteSpace: 'nowrap' }}>
<SendingProgressBar
style={{ flex: 1, marginRight: 10 }}
progress={draft.uploadProgress * 100}
/>
</Flexbox>
);
}
// TODO BG
// if (draft.uploadTaskId) {
// return (
// <Flexbox style={{ width: 150, whiteSpace: 'nowrap' }}>
// <SendingProgressBar
// style={{ flex: 1, marginRight: 10 }}
// progress={draft.uploadProgress * 100}
// />
// </Flexbox>
// );
// }
return <span className="timestamp">{DateUtils.shortTimeString(draft.date)}</span>;
}
}

View file

@ -1,5 +1,5 @@
const MailspringStore = require('mailspring-store').default;
const {
import MailspringStore from 'mailspring-store';
import {
Rx,
Message,
OutboxStore,
@ -9,8 +9,9 @@ const {
ObservableListDataSource,
FocusedPerspectiveStore,
DatabaseStore,
} = require('mailspring-exports');
const { ListTabular } = require('mailspring-component-kit');
QueryResultSet,
} from 'mailspring-exports';
import { ListTabular, ListDataSource } from 'mailspring-component-kit';
class DraftListStore extends MailspringStore {
constructor() {
@ -19,6 +20,8 @@ class DraftListStore extends MailspringStore {
this._createListDataSource();
}
_dataSource: ListDataSource;
dataSource = () => {
return this._dataSource;
};
@ -43,8 +46,8 @@ class DraftListStore extends MailspringStore {
this._dataSource = null;
}
if (mailboxPerspective.drafts) {
const query = DatabaseStore.findAll(Message)
if ((mailboxPerspective as any).drafts) {
const query = DatabaseStore.findAll<Message>(Message)
.include(Message.attributes.body)
.order(Message.attributes.date.descending())
.where({ draft: true })
@ -57,10 +60,12 @@ class DraftListStore extends MailspringStore {
}
const subscription = new MutableQuerySubscription(query, { emitResultSet: true });
let $resultSet = Rx.Observable.fromNamedQuerySubscription('draft-list', subscription);
$resultSet = Rx.Observable.combineLatest(
[$resultSet, Rx.Observable.fromStore(OutboxStore)],
(resultSet, outbox) => {
const $resultSet = Rx.Observable.combineLatest(
[
Rx.Observable.fromNamedQuerySubscription('draft-list', subscription),
Rx.Observable.fromStore(OutboxStore) as any,
],
(resultSet: QueryResultSet<Message>, outbox) => {
// 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.
@ -69,7 +74,7 @@ class DraftListStore extends MailspringStore {
// TODO BG modelWithId: task.headerMessageId does not work
mailboxPerspective.accountIds.forEach(aid => {
OutboxStore.itemsForAccount(aid).forEach(task => {
let draft = resultSet.modelWithId(task.headerMessageId);
let draft = resultSet.modelWithId(task.headerMessageId) as any;
if (draft) {
draft = draft.clone();
draft.uploadTaskId = task.id;
@ -92,4 +97,4 @@ class DraftListStore extends MailspringStore {
};
}
module.exports = new DraftListStore();
export default new DraftListStore();

View file

@ -7,6 +7,7 @@ import {
import PropTypes from 'prop-types';
import DraftListStore from './draft-list-store';
import { Message } from 'mailspring-exports';
function getObservable() {
return DraftListStore.selectionObservable();
@ -19,7 +20,7 @@ function getStateFromObservable(items) {
return { items };
}
class DraftListToolbar extends Component {
class DraftListToolbar extends Component<{ items: Message[] }> {
static displayName = 'DraftListToolbar';
static propTypes = {

View file

@ -7,7 +7,7 @@ import {
MultiselectList,
} from 'mailspring-component-kit';
import DraftListStore from './draft-list-store';
import DraftListColumns from './draft-list-columns';
import * as DraftListColumns from './draft-list-columns';
class DraftList extends React.Component {
static displayName = 'DraftList';
@ -36,7 +36,7 @@ class DraftList extends React.Component {
);
}
_itemPropsProvider = draft => {
const props = {};
const props: any = {};
if (draft.uploadTaskId) {
props.className = 'sending';
}
@ -64,4 +64,4 @@ class DraftList extends React.Component {
};
}
module.exports = DraftList;
export default DraftList;

View file

@ -1,7 +1,8 @@
import React from 'react';
import { RetinaImg } from 'mailspring-component-kit';
import { localized, React, PropTypes, Actions } from 'mailspring-exports';
import { localized, PropTypes, Actions } from 'mailspring-exports';
class DraftDeleteButton extends React.Component {
export class DraftDeleteButton extends React.Component<{ selection: any }> {
static displayName = 'DraftDeleteButton';
static containerRequired = false;
@ -30,5 +31,3 @@ class DraftDeleteButton extends React.Component {
return;
};
}
module.exports = { DraftDeleteButton };

View file

@ -1,7 +1,8 @@
const { React, PropTypes, Actions } = require('mailspring-exports');
const { RetinaImg } = require('mailspring-component-kit');
import React from 'react';
import { PropTypes, Actions } from 'mailspring-exports';
import { RetinaImg } from 'mailspring-component-kit';
class SendingCancelButton extends React.Component {
class SendingCancelButton extends React.Component<{ taskId: string }, { cancelling: boolean }> {
static displayName = 'SendingCancelButton';
static propTypes = { taskId: PropTypes.string.isRequired };
@ -35,4 +36,4 @@ class SendingCancelButton extends React.Component {
};
}
module.exports = SendingCancelButton;
export default SendingCancelButton;

View file

@ -1,10 +1,11 @@
const { React, PropTypes, Utils } = require('mailspring-exports');
import React from 'react';
import { PropTypes, Utils } from 'mailspring-exports';
class SendingProgressBar extends React.Component {
class SendingProgressBar extends React.Component<{ progress: number }> {
static propTypes = { progress: PropTypes.number.isRequired };
render() {
const otherProps = Utils.fastOmit(this.props, Object.keys(this.constructor.propTypes));
const otherProps = Utils.fastOmit(this.props, Object.keys(SendingProgressBar.propTypes));
if (0 < this.props.progress && this.props.progress < 99) {
return (
<div className="sending-progress" {...otherProps}>
@ -20,4 +21,4 @@ class SendingProgressBar extends React.Component {
}
}
}
module.exports = SendingProgressBar;
export default SendingProgressBar;

View file

@ -1,22 +1,29 @@
const { RetinaImg } = require('mailspring-component-kit');
const {
import { RetinaImg } from 'mailspring-component-kit';
import React from 'react';
import {
Actions,
localized,
React,
PropTypes,
DateUtils,
Message,
Event,
EventRSVPTask,
DatabaseStore,
} = require('mailspring-exports');
} from 'mailspring-exports';
const moment = require('moment-timezone');
class EventHeader extends React.Component {
class EventHeader extends React.Component<
{
message: Message;
},
{ event: Event }
> {
static displayName = 'EventHeader';
static propTypes = { message: PropTypes.instanceOf(Message).isRequired };
_unlisten: () => void;
constructor(props) {
super(props);
this.state = { event: this.props.message.events[0] };
@ -26,7 +33,7 @@ class EventHeader extends React.Component {
if (!this.state.event) {
return;
}
DatabaseStore.find(Event, this.state.event.id).then(event => {
DatabaseStore.find<Event>(Event, this.state.event.id).then(event => {
if (!event) {
return;
}
@ -109,7 +116,7 @@ class EventHeader extends React.Component {
return (
<div className="event-actions">
{actions.mapx(([status, label]) => {
{actions.map(([status, label]) => {
let classes = 'btn-rsvp ';
if (me.status === status) {
classes += status;
@ -130,4 +137,4 @@ class EventHeader extends React.Component {
};
}
module.exports = EventHeader;
export default EventHeader;

Some files were not shown because too many files have changed in this diff Show more