Update to ES6 and Webpack

master
Vitaliy Filippov 2018-12-02 19:27:22 +03:00
parent dbe8af8a69
commit 16ec9ec90b
30 changed files with 667 additions and 593 deletions

View File

@ -1,15 +1,4 @@
{
"plugins": [
"check-es2015-constants",
"transform-es2015-arrow-functions",
"transform-es2015-block-scoping",
"transform-es2015-classes",
"transform-es2015-for-of",
"transform-es2015-computed-properties",
"transform-es2015-destructuring",
"transform-es2015-shorthand-properties",
"transform-object-rest-spread",
"transform-react-jsx",
],
"presets": [ "env", "stage-1", "react" ],
"retainLines": true
}

45
.eslintrc.js Normal file
View File

@ -0,0 +1,45 @@
module.exports = {
"parser": "babel-eslint",
"env": {
"es6": true,
"browser": true
},
"globals": {
"APPCFG": false
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"plugins": [
"react"
],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"semi": [
"error",
"always"
],
"no-control-regex": [
"off"
],
"no-empty": [
"off"
]
}
};

View File

@ -1,8 +1,16 @@
const React = require('react');
const DropDownBase = require('./DropDownBase.js');
import React from 'react';
import DropDownBase from './DropDownBase.js';
var AccountFolders = module.exports = React.createClass({
render: function()
export default class AccountFolders extends React.PureComponent
{
state = {
collapsed: this.props.collapsed,
animating: false,
h: null,
cfgPressed: false,
}
render()
{
return <div className="account">
<div className={"account-header"+(this.state.collapsed ? ' collapsed' : '')} onClick={this.onClick}>
@ -28,8 +36,9 @@ var AccountFolders = module.exports = React.createClass({
</div>
</div>
</div>
},
selectFolder: function(ev)
}
selectFolder = (ev) =>
{
var t = ev.target;
while (t && !t.getAttribute('data-i') && t != this.refs.vis)
@ -40,8 +49,9 @@ var AccountFolders = module.exports = React.createClass({
this.props.onSelect(this.props.accountIndex, i);
}
// FIXME: send select event + switch focus to message list if folder changed
},
showCfg: function(ev)
}
showCfg = (ev) =>
{
var self = this;
var i = DropDownBase.instances.account.state.items;
@ -53,12 +63,9 @@ var AccountFolders = module.exports = React.createClass({
});
self.setState({ cfgPressed: true });
ev.stopPropagation();
},
getInitialState: function()
{
return { collapsed: this.props.collapsed, animating: false, h: null, cfgPressed: false };
},
onClick: function()
}
onClick = () =>
{
var self = this;
if (this.state.animating)
@ -76,4 +83,4 @@ var AccountFolders = module.exports = React.createClass({
self.setState({ collapsed: !self.state.collapsed, animating: false, h: null });
}, this.state.collapsed ? 200 : 250);
}
});
}

View File

@ -1,11 +1,12 @@
const React = require('react');
const DropDownMenu = require('./DropDownMenu.js');
const ListSortSettingsWindow = require('./ListSortSettingsWindow.js');
const MailSettingsWindow = require('./MailSettingsWindow.js');
import React from 'react';
import DropDownMenu from './DropDownMenu.js';
import ListSortSettingsWindow from './ListSortSettingsWindow.js';
import MailSettingsWindow from './MailSettingsWindow.js';
var dropdown_account = React.createElement(
DropDownMenu, {
id: 'account',
key: 'account',
items: [ {
icon: 'mail_unread',
i16: true,
@ -23,6 +24,7 @@ var dropdown_account = React.createElement(
var dropdown_reply = React.createElement(
DropDownMenu, {
id: 'reply',
key: 'reply',
items: [ {
hotkey: 'R',
icon: 'mail_reply',
@ -41,6 +43,7 @@ var dropdown_reply = React.createElement(
var dropdown_forward = React.createElement(
DropDownMenu, {
id: 'forward',
key: 'forward',
items: [ {
hotkey: 'F',
icon: 'mail_forward',
@ -55,6 +58,7 @@ var dropdown_forward = React.createElement(
var dropdown_delete = React.createElement(
DropDownMenu, {
id: 'delete',
key: 'delete',
items: [ {
text: 'Move to Trash'
}, {
@ -67,6 +71,7 @@ var dropdown_delete = React.createElement(
var dropdown_check_send = React.createElement(
DropDownMenu, {
id: 'check-send',
key: 'check-send',
items: [ {
hotkey: 'Ctrl-K',
icon: 'mail_check',
@ -90,6 +95,7 @@ var dropdown_check_send = React.createElement(
var dropdown_threads = React.createElement(
DropDownMenu, {
id: 'threads',
key: 'threads',
items: [ {
icon: 'thread',
text: 'Show Message Thread'
@ -111,6 +117,7 @@ var dropdown_threads = React.createElement(
var dropdown_list_sort = React.createElement(
ListSortSettingsWindow, {
id: 'list-sort',
key: 'list-sort',
window: true,
folder: 'INBOX',
override: false,
@ -134,9 +141,7 @@ var dropdown_list_sort = React.createElement(
}
);
var dropdown_settings = MailSettingsWindow;
module.exports = function()
export default function()
{
return [
dropdown_account,
@ -146,6 +151,6 @@ module.exports = function()
dropdown_check_send,
dropdown_threads,
dropdown_list_sort,
dropdown_settings
<MailSettingsWindow key="mail-settings" />,
];
}

View File

@ -1,17 +1,17 @@
const React = require('react');
const ListWithSelection = require('./ListWithSelection.js');
const Util = require('./Util.js');
import React from 'react';
var AttachList = module.exports = React.createClass({
mixins: [ ListWithSelection ],
getInitialState: function()
{
return {
attachments: [],
attachScroll: 0
};
},
addAttachments: function(ev)
import ListWithSelection from './ListWithSelection.js';
import Util from './Util.js';
export default class AttachList extends ListWithSelection
{
state = {
...this.state,
attachments: [],
attachScroll: 0
}
addAttachments = (ev) =>
{
var a = this.state.attachments;
if (ev.target.files)
@ -20,36 +20,43 @@ var AttachList = module.exports = React.createClass({
this.setState({ attachments: a });
// reset file input
ev.target.innerHTML = ev.target.innerHTML;
},
scrollAttachList: function(ev)
}
scrollAttachList = (ev) =>
{
this.setState({ attachScroll: ev.target.scrollTop });
},
deleteSelected: function()
}
deleteSelected = () =>
{
for (var i = this.state.attachments.length-1; i >= 0; i--)
if (this.state.selected[i])
this.state.attachments.splice(i, 1);
this.setState({ attachments: this.state.attachments });
},
getTotalItems: function()
}
getTotalItems = () =>
{
return this.state.attachments.length;
},
getPageSize: function()
}
getPageSize = () =>
{
return this.refs.a0 ? Math.round(this.refs.scroll.offsetHeight / this.refs.a0.offsetHeight) : 1;
},
getItemOffset: function(index)
}
getItemOffset = (index) =>
{
var item = this.refs['a'+index];
return [ item.offsetTop, item.offsetHeight ];
},
getScrollPaddingTop: function()
}
getScrollPaddingTop = () =>
{
return this.refs.title.offsetHeight;
},
render: function()
}
render()
{
return <div className="attach">
<div className="no-attach" style={this.state.attachments.length ? { display: 'none' } : null}>
@ -74,4 +81,4 @@ var AttachList = module.exports = React.createClass({
</div>
</div>
}
});
}

View File

@ -1,63 +1,63 @@
const React = require('react');
const AttachList = require('./AttachList.js');
const StoreListener = require('./StoreListener.js');
import React from 'react';
import AttachList from './AttachList.js';
import StoreListener from './StoreListener.js';
var ComposeWindow = React.createClass({
getInitialState: function()
{
return {
text: ''
};
},
changeText: function(ev)
class ComposeWindow extends React.PureComponent
{
state = {
text: ''
}
changeText = (ev) =>
{
this.setState({ text: ev.target.value });
},
render: function()
}
render()
{
return <div className="compose">
<div className="actions">
<a className="button"><img src="icons/mail_send.png" />Send</a>
<a className="button"><img src="icons/delete.png" /></a>
</div>
<div className="flex">
<div className="headers">
<div className="headers-table">
<div className="header">
<div className="field">From</div>
<div className="value">
<select>
{this.props.accounts.filter(a => a.accountId).map((a, i) =>
<option key={'a'+a.accountId}>{'"'+a.name+'" <'+a.email+'>'}</option>
)}
</select>
return (<div className="compose">
<div className="actions">
<a className="button"><img src="icons/mail_send.png" />Send</a>
<a className="button"><img src="icons/delete.png" /></a>
</div>
<div className="flex">
<div className="headers">
<div className="headers-table">
<div className="header">
<div className="field">From</div>
<div className="value">
<select>
{this.props.accounts.filter(a => a.accountId).map((a, i) =>
<option key={'a'+a.accountId}>{'"'+a.name+'" <'+a.email+'>'}</option>
)}
</select>
</div>
</div>
<div className="header">
<div className="field">To</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Cc</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Bcc</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Subject</div>
<div className="value"><input /></div>
</div>
</div>
<div className="header">
<div className="field">To</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Cc</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Bcc</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Subject</div>
<div className="value"><input /></div>
</div>
<AttachList />
</div>
<div className="text">
<textarea onChange={this.changeText} defaultValue={this.state.text}></textarea>
</div>
<AttachList />
</div>
<div className="text">
<textarea onChange={this.changeText} defaultValue={this.state.text}></textarea>
</div>
</div>
</div>
</div>);
}
});
}
module.exports = StoreListener(ComposeWindow, (data) => { return { accounts: data.accounts }; });
export default StoreListener(ComposeWindow, (data) => { return { accounts: data.accounts }; });

View File

@ -1,35 +1,13 @@
function getOffset(elem)
{
if (elem.getBoundingClientRect)
{
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}
else
{
var top = 0, left = 0;
while(elem)
{
top = top + parseInt(elem.offsetTop);
left = left + parseInt(elem.offsetLeft);
elem = elem.offsetParent;
}
return { top: top, left: left };
}
}
import React from 'react';
var DropDownBase = module.exports = {
instances: {},
currentVisible: null,
componentDidMount: function()
export default class DropDownBase extends React.PureComponent
{
static instances = {};
static currentVisible = null;
state = { visible: false, top: 0, left: 0, calloutLeft: null, selectedItem: -1 };
componentDidMount()
{
if (!DropDownBase.setBodyListener)
{
@ -38,36 +16,38 @@ var DropDownBase = module.exports = {
DropDownBase.setBodyListener = true;
}
DropDownBase.instances[this.props.id] = this;
},
hideAll: function()
}
static hideAll()
{
for (var i in DropDownBase.instances)
DropDownBase.instances[i].hide();
},
repositionCurrent: function()
}
static repositionCurrent()
{
if (DropDownBase.currentVisible)
DropDownBase.currentVisible[0].showAt(DropDownBase.currentVisible[1], DropDownBase.currentVisible[0].onClose);
},
componentWillUnmount: function()
}
componentWillUnmount()
{
delete DropDownBase.instances[this.props.id];
if (DropDownBase.currentVisible[0] == this)
DropDownBase.currentVisible = null;
},
getInitialState: function()
{
return { visible: false, top: 0, left: 0, calloutLeft: null, selectedItem: -1 };
},
onClick: function(ev)
}
onClick = (ev) =>
{
ev.stopPropagation();
},
isVisible: function()
}
isVisible = () =>
{
return this.state.visible;
},
hide: function()
}
hide = () =>
{
this.setState({ visible: false });
DropDownBase.currentVisible = null;
@ -76,8 +56,9 @@ var DropDownBase = module.exports = {
this.onClose();
delete this.onClose;
}
},
showAt: function(el, onClose)
}
showAt = (el, onClose) =>
{
if (this.onClose && this.onClose != onClose)
{
@ -108,4 +89,32 @@ var DropDownBase = module.exports = {
this.refs.dd.focus();
this.onClose = onClose;
}
};
}
function getOffset(elem)
{
if (elem.getBoundingClientRect)
{
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}
else
{
var top = 0, left = 0;
while(elem)
{
top = top + parseInt(elem.offsetTop);
left = left + parseInt(elem.offsetLeft);
elem = elem.offsetParent;
}
return { top: top, left: left };
}
}

View File

@ -1,16 +1,21 @@
const React = require('react');
const DropDownBase = require('./DropDownBase.js');
import React from 'react';
var DropDownButton = module.exports = React.createClass({
componentDidUpdate: function(prevProps, prevState)
import DropDownBase from './DropDownBase.js';
export default class DropDownButton extends React.PureComponent
{
state = { pressed: false, checked: false }
componentDidUpdate(prevProps, prevState)
{
if (prevProps.hidden && !this.props.hidden &&
DropDownBase.instances[this.props.dropdownId].isVisible())
{
DropDownBase.instances[this.props.dropdownId].showAt(this.refs.btn, this.unpress);
}
},
render: function()
}
render()
{
return <a ref="btn" title={(this.state.checked ? this.props.checkedTitle : null) || this.props.title} onClick={this.onClickButton}
className={'button '+(this.props.dropdownId ? 'show-dropdown ' : '')+(this.state.checked ? 'checked ' : '')+
@ -20,12 +25,9 @@ var DropDownButton = module.exports = React.createClass({
{this.state.checked && this.props.checkedText || this.props.text || null}
{this.props.dropdownId ? <span className="down" onClick={this.onClickDown}></span> : null}
</a>
},
getInitialState: function()
{
return { pressed: false, checked: false };
},
toggle: function()
}
toggle = () =>
{
if (!this.state.pressed)
{
@ -35,12 +37,14 @@ var DropDownButton = module.exports = React.createClass({
else
DropDownBase.instances[this.props.dropdownId].hide();
this.setState({ pressed: !this.state.pressed });
},
unpress: function()
}
unpress = () =>
{
this.setState({ pressed: false });
},
onClickButton: function(ev)
}
onClickButton = (ev) =>
{
if (this.props.whole || this.props.checkable && this.state.pressed)
this.toggle();
@ -53,10 +57,11 @@ var DropDownButton = module.exports = React.createClass({
else if (this.props.onClick)
this.props.onClick();
ev.stopPropagation();
},
onClickDown: function(ev)
}
onClickDown = (ev) =>
{
this.toggle();
ev.stopPropagation();
}
});
}

View File

@ -1,13 +1,12 @@
const React = require('react');
const DropDownBase = require('./DropDownBase.js');
import React from 'react';
var DropDownMenu = module.exports = React.createClass({
mixins: [ DropDownBase ],
getInitialState: function()
{
return { items: this.props.items };
},
render: function()
import DropDownBase from './DropDownBase.js';
export default class DropDownMenu extends DropDownBase
{
state = { items: this.props.items }
render()
{
var sel = this.state.selectedItem;
return <div ref="dd" className={'dropdown'+(this.state.visible ? ' visible' : '')} id={'dropdown-'+this.props.id}
@ -24,16 +23,18 @@ var DropDownMenu = module.exports = React.createClass({
);
})}
</div>
},
onMouseOver: function(ev)
}
onMouseOver = (ev) =>
{
var t = ev.target;
while ((t && t != this.refs.dd) && (!t.className || t.className.substr(0, 4) != 'item'))
t = t.parentNode;
if (t && t != this.refs.dd)
this.setState({ selectedItem: parseInt(t.getAttribute('data-index')) });
},
onKeyDown: function(ev)
}
onKeyDown = (ev) =>
{
if (ev.keyCode == 40 || ev.keyCode == 38)
{
@ -50,14 +51,16 @@ var DropDownMenu = module.exports = React.createClass({
this.clickItem();
ev.preventDefault();
ev.stopPropagation();
},
clickItem: function(ev)
}
clickItem = (ev) =>
{
},
myOnClick: function(ev)
}
myOnClick = (ev) =>
{
if (ev.target.getAttribute('data-index'))
this.clickItem();
this.onClick(ev);
}
});
}

View File

@ -1,15 +1,18 @@
const React = require('react');
const AccountFolders = require('./AccountFolders.js');
const DropDownButton = require('./DropDownButton.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListenerClass.js');
const MailProgress = require('./MailProgress.js');
import React from 'react';
import AccountFolders from './AccountFolders.js';
import DropDownButton from './DropDownButton.js';
import Store from './Store.js';
import StoreListener from './StoreListener.js';
import MailProgress from './MailProgress.js';
var FolderList = React.createClass({
render: function()
class FolderList extends React.PureComponent
{
state = { selectedAccount: -1, selectedFolder: -1 }
render()
{
var self = this;
return <div className={"folder-list"+(self.props.progressText ? ' progress-visible' : '')}>
return (<div className={"folder-list"+(self.props.progressText ? ' progress-visible' : '')}>
<div className="top-border-gradient"></div>
<div className="bottom-border-gradient"></div>
<div className="actions">
@ -24,13 +27,15 @@ var FolderList = React.createClass({
})}
</div>
<MailProgress />
</div>
},
onClickCheckSend: function()
</div>)
}
onClickCheckSend = () =>
{
Store.startResync();
},
onSelectFolder: function(accIndex, folderIndex)
}
onSelectFolder = (accIndex, folderIndex) =>
{
var acc = this.props.accounts[accIndex];
var folder = this.props.accounts[accIndex].folders[folderIndex];
@ -42,11 +47,7 @@ var FolderList = React.createClass({
Store.loadFolder({ accountId: acc.accountId, folderType: folder.type });
}
this.setState({ selectedAccount: accIndex, selectedFolder: folderIndex });
},
getInitialState: function()
{
return { selectedAccount: -1, selectedFolder: -1 };
}
});
}
module.exports = StoreListener(FolderList, (data) => { return { accounts: data.accounts, progressText: data.progressText }; });
export default StoreListener(FolderList, (data) => { return { accounts: data.accounts, progressText: data.progressText }; });

View File

@ -1,4 +1,4 @@
module.exports = function(msg)
export default function(msg)
{
return msg;
}

View File

@ -1,7 +1,8 @@
const React = require('react');
import React from 'react';
var ListSortSettings = module.exports = React.createClass({
render: function()
export default class ListSortSettings extends React.PureComponent
{
render()
{
return <div className={this.props.className} value={this.props.sort.sortby}>
<select className="sortby">
@ -19,4 +20,4 @@ var ListSortSettings = module.exports = React.createClass({
<label><input type="checkbox" checked={this.props.sort.threaded} /> Threaded</label>
</div>
}
});
}

View File

@ -1,10 +1,13 @@
const React = require('react');
const DropDownBase = require('./DropDownBase.js');
const ListSortSettings = require('./ListSortSettings.js');
import React from 'react';
var ListSortSettingsWindow = module.exports = React.createClass({
mixins: [ DropDownBase ],
render: function()
import DropDownBase from './DropDownBase.js';
import ListSortSettings from './ListSortSettings.js';
export default class ListSortSettingsWindow extends DropDownBase
{
state = { checksVisible: false }
render()
{
var sort = this.props.override ? this.props.sorting : this.props.defaultSorting;
return <div ref="dd" onClick={this.onClick} className={'dropdown window list-sort'+(this.state.visible ? ' visible' : '')}
@ -25,13 +28,10 @@ var ListSortSettingsWindow = module.exports = React.createClass({
<label><input type="checkbox" checked={this.props.show.dups ? "checked" : null} /> Show Duplicates</label>
</div>
</div>
},
getInitialState: function()
{
return { checksVisible: false };
},
expandChecks: function()
}
expandChecks = () =>
{
this.setState({ checksVisible: !this.state.checksVisible });
}
});
}

View File

@ -1,18 +1,24 @@
// Common selection mixin
var ListWithSelection = module.exports = {
import React from 'react';
// Common "list with selection" component
export default class ListWithSelection extends React.PureComponent
{
// requires to override methods: this.deleteSelected(), this.getPageSize(), this.getItemOffset(index), this.getTotalItems()
getInitialState: function()
constructor(props)
{
return {
selected: {}
};
},
isSelected: function(i)
super(props);
this.state = this.state||{};
this.state.selected = {};
}
isSelected(i)
{
return this.state.selected[i] || this.state.selected.begin !== undefined &&
this.state.selected.begin <= i && this.state.selected.end >= i;
},
onListKeyDown: function(ev)
}
onListKeyDown = (ev) =>
{
if (!this.getTotalItems())
return;
@ -74,8 +80,9 @@ var ListWithSelection = module.exports = {
else
this.selectOne(nsel);
}
},
selectTo: function(ns)
}
selectTo(ns)
{
if (this.lastSel === undefined)
return this.selectOne(ns);
@ -93,8 +100,9 @@ var ListWithSelection = module.exports = {
this.curSel = ns;
if (this.onSelectCurrent)
this.onSelectCurrent(ns);
},
selectOne: function(ns)
}
selectOne(ns)
{
var sel = {};
sel[ns] = true;
@ -103,8 +111,9 @@ var ListWithSelection = module.exports = {
this.curSel = ns;
if (this.onSelectCurrent)
this.onSelectCurrent(ns);
},
onListItemClick: function(ev)
}
onListItemClick = (ev) =>
{
var t = ev.target;
while (t && !t.getAttribute('data-i'))
@ -127,4 +136,4 @@ var ListWithSelection = module.exports = {
this.selectOne(ns);
}
}
};
}

View File

@ -1,7 +1,7 @@
const StoreListener = require('./StoreListenerClass.js');
const ProgressBar = require('./ProgressBar.js');
import StoreListener from './StoreListener.js';
import ProgressBar from './ProgressBar.js';
module.exports = StoreListener(ProgressBar, function(data)
export default StoreListener(ProgressBar, function(data)
{
return { text: data.progressText, progress: data.progressPercent };
});

View File

@ -1,12 +1,12 @@
const React = require('react');
const DropDownBase = require('./DropDownBase.js');
const ListSortSettings = require('./ListSortSettings.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
import React from 'react';
import DropDownBase from './DropDownBase.js';
import ListSortSettings from './ListSortSettings.js';
import Store from './Store.js';
import StoreListener from './StoreListener.js';
var MailSettingsWindow = React.createClass({
mixins: [ DropDownBase ],
render: function()
class MailSettingsWindow extends DropDownBase
{
render()
{
return <div ref="dd" onClick={this.onClick} className={'dropdown window'+(this.state.visible ? ' visible' : '')}
id={'dropdown-'+this.props.id} tabIndex="1" style={{ top: this.state.top, left: this.state.left }}>
@ -35,20 +35,22 @@ var MailSettingsWindow = React.createClass({
</select>
</div>
</div>
},
switchLayout: function(ev)
}
switchLayout = (ev) =>
{
var t = ev.target.nodeName == 'A' ? ev.target : ev.target.parentNode;
var l = / mail-(\S+)/.exec(t.className)[1];
Store.set('layout', l);
},
showQuickReply: function()
}
showQuickReply = () =>
{
Store.set('quickReply', !this.props.quickReply);
}
});
}
module.exports = StoreListener(
export default StoreListener(
MailSettingsWindow,
(data) => { return { layout: data.layout, quickReply: data.quickReply }; },
{

View File

@ -1,13 +1,15 @@
const React = require('react');
const DropDownButton = require('./DropDownButton.js');
const ListWithSelection = require('./ListWithSelection.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
const Util = require('./Util.js');
import React from 'react';
import DropDownButton from './DropDownButton.js';
import ListWithSelection from './ListWithSelection.js';
import Store from './Store.js';
import StoreListener from './StoreListener.js';
import Util from './Util.js';
var MessageInList = React.createClass({
msgClasses: { unread: 'unread', unseen: 'unseen', answered: 'replied', flagged: 'pinned', sent: 'sent' },
render: function()
class MessageInList extends React.PureComponent
{
msgClasses = { unread: 'unread', unseen: 'unseen', answered: 'replied', flagged: 'pinned', sent: 'sent' }
render()
{
var msg = this.props.msg;
return <div data-i={this.props.i} className={'message'+
@ -29,26 +31,26 @@ var MessageInList = React.createClass({
</div>
}
});
}
// TODO: expand/collapse days
var MessageList = React.createClass({
mixins: [ ListWithSelection ],
_preloadSize: 20,
_pageSize: 50,
getInitialState: function()
{
return {
firstDayTop: 0,
firstDay: this.props.groups && this.props.groups[0] && this.props.groups[0].name || null
};
},
componentWillReceiveProps: function(nextProps)
class MessageList extends ListWithSelection
{
_preloadSize = 20
_pageSize = 50
state = {
...this.state,
firstDayTop: 0,
firstDay: this.props.groups && this.props.groups[0] && this.props.groups[0].name || null
}
componentWillReceiveProps(nextProps)
{
this.setFirstDayFromProps(nextProps);
},
}
// Main virtual scroll detector method
setFirstDayFromProps: function(props)
setFirstDayFromProps(props)
{
var groups = props.groups;
var messages = props.messages;
@ -118,16 +120,19 @@ var MessageList = React.createClass({
Store.loadMessages(loadFirst, loadFirstEnd-loadFirst);
if (loadFirstEnd < loadLastStart && loadLastStart < loadLast)
Store.loadMessages(loadLastStart, loadLast-loadLastStart);
},
changeFirstDay: function(ev)
}
changeFirstDay = (ev) =>
{
this.setFirstDayFromProps(this.props);
},
deleteSelected: function()
}
deleteSelected = () =>
{
},
onSelectCurrent: function(index)
}
onSelectCurrent = (index) =>
{
var self = this;
var total = 0, p, msg, idx;
@ -155,8 +160,9 @@ var MessageList = React.createClass({
break;
}
}
},
getTotalItems: function()
}
getTotalItems = () =>
{
var total = -1; // do not count first-day as item
for (var i = 0; i < (this.props.groups||[]).length; i++)
@ -164,12 +170,14 @@ var MessageList = React.createClass({
total += 1+this.props.groups[i].messageCount;
}
return total;
},
getPageSize: function()
}
getPageSize = () =>
{
return Math.floor(this.refs.scroll.offsetHeight / (this.props.layout == 'message-on-right' ? 60 : 30));
},
getItemOffset: function(index)
}
getItemOffset = (index) =>
{
var n = 0, top = 0, p;
var h = (this.props.layout == 'message-on-right' ? 60 : 30);
@ -185,28 +193,32 @@ var MessageList = React.createClass({
top += (i > 0 ? 30 : 0) + h*this.props.groups[i].messageCount;
}
return [ top, index == p && i > 0 ? 30 : h ];
},
getScrollPaddingTop: function()
}
getScrollPaddingTop = () =>
{
return this.refs.title.offsetHeight;
},
getMessages: function(grp, start, end)
}
getMessages = (grp, start, end) =>
{
var a = this.props.messages.slice(grp.start+start, grp.start+end);
for (var i = 0; i < end-start; i++)
if (!a[i])
a[i] = null;
return a;
},
onSearchTextChange: function(event)
}
onSearchTextChange = (event) =>
{
var s = event.target.value;
this.setState({ searchText: s });
if (this._searchTimeout)
clearTimeout(this._searchTimeout);
this._searchTimeout = setTimeout(function() { Store.search(s) }, 300);
},
render: function()
}
render()
{
var self = this;
var total = 0;
@ -270,18 +282,20 @@ var MessageList = React.createClass({
})}
</div>
</div>
},
componentDidMount: function()
}
componentDidMount()
{
window.addEventListener('resize', this.changeFirstDay);
},
componentWillUnmount: function()
}
componentWillUnmount()
{
window.removeEventListener('resize', this.changeFirstDay);
}
});
}
module.exports = StoreListener(MessageList, function(data)
export default StoreListener(MessageList, function(data)
{
return {
threads: data.threads,

View File

@ -1,11 +1,14 @@
const React = require('react');
const DropDownButton = require('./DropDownButton.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
const Util = require('./Util.js');
import React from 'react';
import DropDownButton from './DropDownButton.js';
import Store from './Store.js';
import StoreListener from './StoreListener.js';
import Util from './Util.js';
var MessageView = React.createClass({
formatLongDate: function(dt)
class MessageView extends React.PureComponent
{
state = { showImages: false }
formatLongDate(dt)
{
if (!(dt instanceof Date))
dt = new Date(dt.replace(' ', 'T'));
@ -14,8 +17,9 @@ var MessageView = React.createClass({
var s = dt.getSeconds();
return Util.WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Util.Months[dt.getMonth()]+' '+dt.getFullYear()+' '+(h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m+':'+(s < 10 ? '0' : '')+s
//return dt.toLocaleString();
},
componentWillReceiveProps: function(nextProps)
}
componentWillReceiveProps(nextProps)
{
if (nextProps.msg != this.props.msg)
{
@ -26,16 +30,14 @@ var MessageView = React.createClass({
}
this.setState(ns);
}
},
showImages: function()
}
showImages = () =>
{
this.setState({ showImages: true });
},
getInitialState: function()
{
return { showImages: false };
},
render: function()
}
render()
{
var showImages = this.state.showImages;
var msg = this.props.msg;
@ -148,6 +150,6 @@ var MessageView = React.createClass({
] : null}
</div>
}
});
}
module.exports = StoreListener(MessageView, (data) => { return { layout: data.layout, quickReply: data.quickReply, msg: data.msg }; });
export default StoreListener(MessageView, (data) => { return { layout: data.layout, quickReply: data.quickReply, msg: data.msg }; });

View File

@ -1,7 +1,10 @@
const React = require('react');
import React from 'react';
var ProgressBar = module.exports = React.createClass({
render: function()
export default class ProgressBar extends React.PureComponent
{
state = { width: '' }
render()
{
return <div className="progress-bar" ref="pbar" style={{ display: this.props.text ? '' : 'none' }}>
<div className="pending" style={{ width: this.state.width }}>{this.props.text} ({this.props.progress||0}%)</div>
@ -9,29 +12,29 @@ var ProgressBar = module.exports = React.createClass({
<div className="done" ref="pdone" style={{ width: this.state.width }}>{this.props.text} ({this.props.progress||0}%)</div>
</div>
</div>
},
componentDidUpdate: function(prevProps, prevState)
}
componentDidUpdate(prevProps, prevState)
{
if (!prevState.width)
{
setTimeout(this.onResize, 50);
}
},
getInitialState: function()
{
return { width: '' };
},
onResize: function()
}
onResize = () =>
{
this.setState({ width: this.refs.pbar.offsetWidth });
},
componentDidMount: function()
}
componentDidMount()
{
window.addEventListener('resize', this.onResize);
this.onResize();
},
componentWillUnmount: function()
}
componentWillUnmount()
{
window.removeEventListener('resize', this.onResize);
}
});
}

View File

@ -1,10 +1,10 @@
const superagent = require('superagent');
const socket_io = require('socket.io-client');
import superagent from 'superagent';
import socket_io from 'socket.io-client';
const _ = require('./I18n.js');
const Util = require('./Util.js');
import _ from './I18n.js';
import Util from './Util.js';
var Store = module.exports = {
const Store = {
data: {
layout: 'message-on-right',
quickReply: true,
@ -195,3 +195,5 @@ var Store = module.exports = {
};
Store.startIo();
export default Store;

View File

@ -1,19 +1,23 @@
const React = require('react');
const Store = require('./Store.js');
import React from 'react';
import Store from './Store.js';
// "react-redux connect()"-like example
var StoreListener = React.createClass({
componentDidMount: function()
class StoreListener extends React.PureComponent
{
componentDidMount()
{
Store.on(this.update);
},
componentWillUnmount: function()
}
componentWillUnmount()
{
Store.un(this.update);
},
update: function()
}
update = () =>
{
var newState = this.props.mapStateToProps(Store.data);
var newState = this.mapStateToProps(Store.data);
for (var i in newState)
{
if (this.state[i] != newState[i])
@ -22,18 +26,28 @@ var StoreListener = React.createClass({
return;
}
}
},
getInitialState: function()
{
return { ...this.props.initial, ...this.props.mapStateToProps(Store.data) };
},
render: function()
{
return React.createElement(this.props.wrappedComponent, this.state);
}
});
module.exports = function(component, map, initial)
render()
{
var props = { ...this.initial, ...this.props, ...this.state };
return React.createElement(this.wrappedComponent, props);
}
}
export default function(component, map, initial)
{
return React.createElement(StoreListener, { wrappedComponent: component, mapStateToProps: map, initial: initial||{} });
var cl = class extends StoreListener
{
constructor(props, context, updater)
{
super(props, context, updater);
this.wrappedComponent = component;
this.mapStateToProps = map;
this.initial = initial;
this.state = map(Store.data);
this.update = this.update.bind(this);
}
};
return cl;
};

View File

@ -1,52 +0,0 @@
const React = require('react');
const Store = require('./Store.js');
// "react-redux connect()"-like example
class StoreListener extends React.Component
{
componentDidMount()
{
Store.on(this.update);
}
componentWillUnmount()
{
Store.un(this.update);
}
update()
{
var newState = this.mapStateToProps(Store.data);
for (var i in newState)
{
if (this.state[i] != newState[i])
{
this.setState(newState);
return;
}
}
}
render()
{
var props = { ...this.initial, ...this.props, ...this.state };
return React.createElement(this.wrappedComponent, props);
}
}
module.exports = function(component, map, initial)
{
var cl = class extends StoreListener
{
constructor(props, context, updater)
{
super(props, context, updater);
this.wrappedComponent = component;
this.mapStateToProps = map;
this.initial = initial;
this.state = map(Store.data);
this.update = this.update.bind(this);
}
};
return cl;
};

View File

@ -1,50 +0,0 @@
// НЕ РАБОТАЕТ!
const React = require('react');
const Store = require('./Store.js');
// "react-redux connect()"-like example
var StoreListener = React.createClass({
componentDidMount: function()
{
Store.on(this.update);
},
componentWillUnmount: function()
{
Store.un(this.update);
},
update: function()
{
var newState = this.mapStateToProps(Store.data);
for (var i in newState)
{
if (this.state[i] != newState[i])
{
this.setState(newState);
return;
}
}
},
getInitialState: function()
{
return { ...this.initial, ...this.mapStateToProps(Store.data) };
},
render: function()
{
var props = { ...this.initial, ...this.props, ...this.state };
return React.createElement(this.wrappedComponent, props);
}
});
module.exports = function(component, map, initial)
{
var fn = function(props, context, updater)
{
StoreListener.call(this, props, context, updater);
this.wrappedComponent = component;
this.mapStateToProps = map;
this.initial = initial||{};
};
fn.prototype = Object.create(StoreListener);
fn.prototype.constructor = fn;
return fn;
};

View File

@ -1,7 +1,10 @@
const React = require('react');
import React from 'react';
var TabPanel = module.exports = React.createClass({
render: function()
export default class TabPanel extends React.PureComponent
{
state = { selected: 0, tabs: this.props.tabs }
render()
{
var bar = [];
var body = [];
@ -26,21 +29,20 @@ var TabPanel = module.exports = React.createClass({
<div className="tab-bar">{bar}</div>
{body}
</div>
},
componentWillReceiveProps: function(nextProps, nextContent)
}
componentWillReceiveProps(nextProps, nextContent)
{
// FIXME: Do not own tabs?
this.setState({ selected: this.state.selected % nextProps.tabs.length, tabs: nextProps.tabs });
},
getInitialState: function()
{
return { selected: 0, tabs: this.props.tabs };
},
switchTab: function(ev)
}
switchTab = (ev) =>
{
this.setState({ selected: ev.target.id.substr(5) });
},
closeTab: function(ev)
}
closeTab = (ev) =>
{
var self = this;
var tab = ev.target.parentNode;
@ -59,4 +61,4 @@ var TabPanel = module.exports = React.createClass({
}, 200);
ev.stopPropagation();
}
});
}

99
Util.js
View File

@ -1,58 +1,61 @@
var WeekDays = module.exports.WeekDays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ];
var Months = module.exports.Months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
module.exports.formatBytes = function(s)
export default class Util
{
if (!s) return '';
if (s < 1024) return s+' B';
else if (s < 1024*1024) return (Math.round(s*10/1024)/10)+' KB';
else if (s < 1024*1024*1024) return (Math.round(s*10/1024/1024)/10)+' MB';
return (Math.round(s*10/1024/1024/1024)/10)+' GB';
}
static WeekDays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ];
static Months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
module.exports.formatDate = function(dt)
{
if (!(dt instanceof Date))
dt = new Date(dt.replace(' ', 'T'));
var tod = new Date();
tod.setHours(0);
tod.setMinutes(0);
tod.setSeconds(0);
tod.setMilliseconds(0);
var prevweek = tod;
prevweek = prevweek.getTime() - (7 + (prevweek.getDay()+6)%7)*86400000;
if (dt.getTime() < prevweek)
static formatBytes(s)
{
var d = dt.getDate();
var m = dt.getMonth()+1;
return (d < 10 ? '0' : '')+d+'.'+(m < 10 ? '0' : '')+m+'.'+dt.getFullYear();
if (!s) return '';
if (s < 1024) return s+' B';
else if (s < 1024*1024) return (Math.round(s*10/1024)/10)+' KB';
else if (s < 1024*1024*1024) return (Math.round(s*10/1024/1024)/10)+' MB';
return (Math.round(s*10/1024/1024/1024)/10)+' GB';
}
else if (dt.getTime() < tod.getTime())
{
return WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Months[dt.getMonth()];
}
var h = dt.getHours();
var m = dt.getMinutes();
return (h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m;
}
module.exports.getGroupName = function(k)
{
if (k == 't')
static formatDate(dt)
{
return 'Today';
if (!(dt instanceof Date))
dt = new Date(dt.replace(' ', 'T'));
var tod = new Date();
tod.setHours(0);
tod.setMinutes(0);
tod.setSeconds(0);
tod.setMilliseconds(0);
var prevweek = tod;
prevweek = prevweek.getTime() - (7 + (prevweek.getDay()+6)%7)*86400000;
if (dt.getTime() < prevweek)
{
var d = dt.getDate();
var m = dt.getMonth()+1;
return (d < 10 ? '0' : '')+d+'.'+(m < 10 ? '0' : '')+m+'.'+dt.getFullYear();
}
else if (dt.getTime() < tod.getTime())
{
return WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Months[dt.getMonth()];
}
var h = dt.getHours();
var m = dt.getMinutes();
return (h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m;
}
else if (k[0] == 'd')
static getGroupName(k)
{
return WeekDays[k.substr(1)%7];
if (k == 't')
{
return 'Today';
}
else if (k[0] == 'd')
{
return WeekDays[k.substr(1)%7];
}
else if (k == 'pw')
{
return 'Last Week';
}
else if (k[0] == 'm')
{
return Months[k.substr(1)-1];
}
return k;
}
else if (k == 'pw')
{
return 'Last Week';
}
else if (k[0] == 'm')
{
return Months[k.substr(1)-1];
}
return k;
}

View File

@ -17,6 +17,15 @@ html, body
user-select: none;
}
#app
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.clear
{
clear: both;

28
mail.js
View File

@ -1,13 +1,13 @@
const React = require('react');
const ReactDOM = require('react-dom');
const ComposeWindow = require('./ComposeWindow.js');
const FolderList = require('./FolderList.js');
const MessageList = require('./MessageList.js');
const MessageView = require('./MessageView.js');
const TabPanel = require('./TabPanel.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
const AllDropdowns = require('./AllDropdowns.js');
import React from 'react';
import ReactDOM from 'react-dom';
import ComposeWindow from './ComposeWindow.js';
import FolderList from './FolderList.js';
import MessageList from './MessageList.js';
import MessageView from './MessageView.js';
import TabPanel from './TabPanel.js';
import Store from './Store.js';
import StoreListener from './StoreListener.js';
import AllDropdowns from './AllDropdowns.js';
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame;
@ -19,13 +19,13 @@ var AllTabs = StoreListener(TabPanel, function(data)
noclose: true,
icon: 'mail_unread',
title: 'Unread (64)',
children: [ MessageList, MessageView ]
children: [ <MessageList key="1" />, <MessageView key="2" /> ]
},
{
icon: 'mail_drafts',
i16: true,
title: 'Compose Message',
children: [ ComposeWindow ]
children: [ <ComposeWindow key="1" /> ]
}
] }
});
@ -34,9 +34,9 @@ ReactDOM.render(
<div>
{AllDropdowns()}
<FolderList />
{AllTabs}
<AllTabs />
</div>,
document.body
document.getElementById('app')
);
Store.loadAccounts();

View File

@ -5,13 +5,7 @@
<link rel="stylesheet" type="text/css" href="mail.css" />
</head>
<body>
<!-- <script src="https://fb.me/react-15.0.1.js"></script>
<script src="https://fb.me/react-dom-15.1.0.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.js"></script>-->
<!-- <script src="react-15.0.1.js"></script>
<script src="react-dom-15.1.0.js"></script>
<script src="browser.js"></script>
<script type="text/babel" src="mail.js"></script>-->
<div id="app"></div>
<script type="text/javascript" src="mail.c.js"></script>
</body>
</html>

View File

@ -6,33 +6,29 @@
"url": "http://yourcmc.ru/wiki/"
},
"description": "LikeOperaMail",
"dependencies": {
},
"dependencies": {},
"devDependencies": {
"browserify": "latest",
"babelify": "latest",
"watchify": "latest",
"babel-plugin-check-es2015-constants": "latest",
"babel-plugin-transform-es2015-arrow-functions": "latest",
"babel-plugin-transform-es2015-block-scoping": "latest",
"babel-plugin-transform-es2015-classes": "latest",
"babel-plugin-transform-es2015-computed-properties": "latest",
"babel-plugin-transform-es2015-for-of": "latest",
"babel-plugin-transform-es2015-destructuring": "latest",
"babel-plugin-transform-es2015-shorthand-properties": "latest",
"babel-plugin-transform-object-rest-spread": "latest",
"babel-plugin-transform-react-jsx": "latest",
"react": "latest",
"react-dom": "latest",
"uglifyjs": "latest",
"uglifyify": "latest",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.1.5",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-1": "^6.24.1",
"eslint": "latest",
"eslint-plugin-react": "^7.7.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"superagent": "latest",
"socket.io-client": "latest"
"socket.io-client": "latest",
"webpack": "^3.12.0",
"webpack-bundle-analyzer": "^2.13.1"
},
"scripts": {
"compile": "browserify -t babelify -t uglifyify mail.js | uglifyjs -cm > mail.c.js",
"watch-dev": "watchify -t babelify mail.js -o mail.c.js",
"watch": "watchify -t babelify -t uglifyify mail.js -o 'uglifyjs -cm > mail.c.js'"
"lint": "eslint *.js",
"compile": "webpack --optimize-minimize",
"stats": "NODE_ENV=production webpack --optimize-minimize --profile --json > stats.json; webpack-bundle-analyzer stats.json -h 0.0.0.0",
"watch-dev": "NODE_ENV=development webpack -w",
"watch": "NODE_ENV=production webpack -w --optimize-minimize"
}
}

54
webpack.config.js Normal file
View File

@ -0,0 +1,54 @@
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: {
main: [ "babel-polyfill", './mail.js' ]
},
context: __dirname,
output: {
path: __dirname,
filename: './mail.c.js'
},
devtool: 'cheap-module-source-map',
module: {
rules: [
{
test: /.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{
loader: "style-loader",
options: {
singleton: true
}
},
{
loader: "css-loader",
options: {
modules: true, // default is false
sourceMap: true,
importLoaders: 1,
localIdentName: "[name]--[local]--[hash:base64:8]"
}
}
]
}
]
},
plugins: [
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || "production")
}
})
],
performance: {
maxEntrypointSize: 5000000,
maxAssetSize: 5000000
}
};