mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 07:16:08 +08:00
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:
parent
2057ca3023
commit
149b389508
11
.babelrc
11
.babelrc
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"presets": [
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"transform-es2015-modules-commonjs",
|
||||
"transform-object-rest-spread"
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
}
|
34
.eslintrc
34
.eslintrc
|
@ -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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
11
app/.babelrc
11
app/.babelrc
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"presets": [
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"transform-es2015-modules-commonjs",
|
||||
"transform-object-rest-spread"
|
||||
],
|
||||
"sourceMaps": "inline"
|
||||
}
|
|
@ -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', [
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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/,
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
},
|
||||
};
|
10
app/internal_packages/account-sidebar/lib/main.ts
Normal file
10
app/internal_packages/account-sidebar/lib/main.ts
Normal 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);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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();
|
32
app/internal_packages/account-sidebar/lib/types.ts
Normal file
32
app/internal_packages/account-sidebar/lib/types.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
4
app/internal_packages/activity/lib/activity-actions.ts
Normal file
4
app/internal_packages/activity/lib/activity-actions.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Reflux from 'reflux';
|
||||
|
||||
export const markViewed = Reflux.createAction('markViewed');
|
||||
markViewed.sync = true;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -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 = {
|
|
@ -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 = {
|
|
@ -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,
|
|
@ -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' });
|
||||
};
|
||||
|
|
@ -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 = {
|
|
@ -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}>
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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...')}
|
|
@ -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);
|
||||
},
|
||||
};
|
10
app/internal_packages/category-picker/lib/main.tsx
Normal file
10
app/internal_packages/category-picker/lib/main.tsx
Normal 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);
|
||||
}
|
|
@ -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...')}
|
|
@ -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;
|
15
app/internal_packages/category-picker/lib/types.ts
Normal file
15
app/internal_packages/category-picker/lib/types.ts
Normal 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;
|
||||
};
|
|
@ -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 = {}));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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}
|
|
@ -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}
|
|
@ -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})`,
|
||||
}}
|
|
@ -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;
|
|
@ -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',
|
|
@ -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 = {}) => {
|
|
@ -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} />
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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',
|
|
@ -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;
|
|
@ -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 => {
|
|
@ -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' });
|
||||
};
|
||||
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -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();
|
|
@ -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 = {
|
|
@ -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);
|
|
@ -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
|
|
@ -5,6 +5,8 @@ const Fields = {
|
|||
From: 'fromField',
|
||||
Subject: 'textFieldSubject',
|
||||
Body: 'contentBody',
|
||||
ParticipantFields: [],
|
||||
Order: {},
|
||||
};
|
||||
|
||||
Fields.ParticipantFields = [Fields.To, Fields.Cc, Fields.Bcc];
|
|
@ -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);
|
||||
}
|
|
@ -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> + </span>;
|
||||
|
@ -134,6 +134,6 @@ Object.assign(EnhancedSendActionButton.prototype, {
|
|||
},
|
||||
});
|
||||
|
||||
EnhancedSendActionButton.UndecoratedSendActionButton = SendActionButton;
|
||||
(EnhancedSendActionButton as any).UndecoratedSendActionButton = SendActionButton;
|
||||
|
||||
export default EnhancedSendActionButton;
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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];
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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 = {
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
Loading…
Reference in a new issue