Remove Redux-like superglobal Store, remove all var's and self's
parent
5433f05542
commit
c2b841d757
|
@ -40,12 +40,12 @@ export default class AccountFolders extends React.PureComponent
|
|||
|
||||
selectFolder = (ev) =>
|
||||
{
|
||||
var t = ev.target;
|
||||
let t = ev.target;
|
||||
while (t && !t.getAttribute('data-i') && t != this.refs.vis)
|
||||
t = t.parentNode;
|
||||
if (t && t != this.refs.vis)
|
||||
{
|
||||
var i = t.getAttribute('data-i');
|
||||
let i = t.getAttribute('data-i');
|
||||
this.props.onSelect(this.props.accountIndex, i);
|
||||
}
|
||||
// FIXME: send select event + switch focus to message list if folder changed
|
||||
|
@ -53,34 +53,32 @@ export default class AccountFolders extends React.PureComponent
|
|||
|
||||
showCfg = (ev) =>
|
||||
{
|
||||
var self = this;
|
||||
var i = DropDownBase.instances.account.state.items;
|
||||
let i = DropDownBase.instances.account.state.items;
|
||||
i[0].text = 'Read '+(this.props.account.email||this.props.account.name);
|
||||
DropDownBase.instances.account.setState({ items: i });
|
||||
DropDownBase.instances.account.showAt(ev.target, function()
|
||||
DropDownBase.instances.account.showAt(ev.target, () =>
|
||||
{
|
||||
self.setState({ cfgPressed: false });
|
||||
this.setState({ cfgPressed: false });
|
||||
});
|
||||
self.setState({ cfgPressed: true });
|
||||
this.setState({ cfgPressed: true });
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
onClick = () =>
|
||||
{
|
||||
var self = this;
|
||||
if (this.state.animating)
|
||||
return;
|
||||
this.setState({ animating: true, h: this.refs.vis.offsetHeight });
|
||||
if (!this.state.collapsed)
|
||||
{
|
||||
setTimeout(function()
|
||||
setTimeout(() =>
|
||||
{
|
||||
self.setState({ h: 0 });
|
||||
this.setState({ h: 0 });
|
||||
}, 50);
|
||||
}
|
||||
setTimeout(function()
|
||||
setTimeout(() =>
|
||||
{
|
||||
self.setState({ collapsed: !self.state.collapsed, animating: false, h: null });
|
||||
this.setState({ collapsed: !this.state.collapsed, animating: false, h: null });
|
||||
}, this.state.collapsed ? 200 : 250);
|
||||
}
|
||||
}
|
||||
|
|
156
AllDropdowns.js
156
AllDropdowns.js
|
@ -1,156 +0,0 @@
|
|||
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,
|
||||
text: 'Read vitalif@mail.ru'
|
||||
}, {
|
||||
icon: 'folder',
|
||||
text: 'IMAP Folders',
|
||||
}, {
|
||||
icon: 'properties',
|
||||
text: 'Properties...'
|
||||
} ]
|
||||
}
|
||||
);
|
||||
|
||||
var dropdown_reply = React.createElement(
|
||||
DropDownMenu, {
|
||||
id: 'reply',
|
||||
key: 'reply',
|
||||
items: [ {
|
||||
hotkey: 'R',
|
||||
icon: 'mail_reply',
|
||||
text: 'Reply'
|
||||
}, {
|
||||
icon: 'mail_reply',
|
||||
text: 'Reply to Sender',
|
||||
}, {
|
||||
disabled: true,
|
||||
icon: 'mail_reply_all',
|
||||
text: 'Reply to List'
|
||||
} ]
|
||||
}
|
||||
);
|
||||
|
||||
var dropdown_forward = React.createElement(
|
||||
DropDownMenu, {
|
||||
id: 'forward',
|
||||
key: 'forward',
|
||||
items: [ {
|
||||
hotkey: 'F',
|
||||
icon: 'mail_forward',
|
||||
text: 'Reply'
|
||||
}, {
|
||||
hotkey: 'D',
|
||||
text: 'Redirect'
|
||||
} ]
|
||||
}
|
||||
);
|
||||
|
||||
var dropdown_delete = React.createElement(
|
||||
DropDownMenu, {
|
||||
id: 'delete',
|
||||
key: 'delete',
|
||||
items: [ {
|
||||
text: 'Move to Trash'
|
||||
}, {
|
||||
icon: 'delete',
|
||||
text: 'Delete Permanently'
|
||||
} ]
|
||||
}
|
||||
);
|
||||
|
||||
var dropdown_check_send = React.createElement(
|
||||
DropDownMenu, {
|
||||
id: 'check-send',
|
||||
key: 'check-send',
|
||||
items: [ {
|
||||
hotkey: 'Ctrl-K',
|
||||
icon: 'mail_check',
|
||||
text: 'Check All'
|
||||
}, {
|
||||
hotkey: 'Ctrl-Shift-K',
|
||||
icon: 'mail_send',
|
||||
text: 'Send Queued'
|
||||
}, { split: true }, {
|
||||
icon: 'mail_check',
|
||||
text: 'vitalif@mail.ru'
|
||||
}, {
|
||||
icon: 'mail_check',
|
||||
text: 'vitalif@yourcmc.ru'
|
||||
}, { split: true }, {
|
||||
text: 'Resynchronize All Messages'
|
||||
} ]
|
||||
}
|
||||
);
|
||||
|
||||
var dropdown_threads = React.createElement(
|
||||
DropDownMenu, {
|
||||
id: 'threads',
|
||||
key: 'threads',
|
||||
items: [ {
|
||||
icon: 'thread',
|
||||
text: 'Show Message Thread'
|
||||
}, {
|
||||
text: 'Follow Thread'
|
||||
}, {
|
||||
text: 'Ignore Thread'
|
||||
}, { split: true }, {
|
||||
hotkey: 'M',
|
||||
icon: 'read',
|
||||
text: 'Mark Thread as Read'
|
||||
}, { split: true }, {
|
||||
hotkey: 'N',
|
||||
text: 'Mark Thread and Go to Next Unread'
|
||||
} ]
|
||||
}
|
||||
);
|
||||
|
||||
var dropdown_list_sort = React.createElement(
|
||||
ListSortSettingsWindow, {
|
||||
id: 'list-sort',
|
||||
key: 'list-sort',
|
||||
window: true,
|
||||
folder: 'INBOX',
|
||||
override: false,
|
||||
sorting: {},
|
||||
defaultSorting: {
|
||||
sort: {
|
||||
sortby: 'sent date',
|
||||
group: 'date',
|
||||
ascending: false,
|
||||
threaded: false
|
||||
}
|
||||
},
|
||||
show: {
|
||||
read: true,
|
||||
trash: false,
|
||||
spam: false,
|
||||
lists: true,
|
||||
sent: true,
|
||||
dups: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default function()
|
||||
{
|
||||
return [
|
||||
dropdown_account,
|
||||
dropdown_reply,
|
||||
dropdown_forward,
|
||||
dropdown_delete,
|
||||
dropdown_check_send,
|
||||
dropdown_threads,
|
||||
dropdown_list_sort,
|
||||
<MailSettingsWindow key="mail-settings" />,
|
||||
];
|
||||
}
|
|
@ -13,9 +13,9 @@ export default class AttachList extends ListWithSelection
|
|||
|
||||
addAttachments = (ev) =>
|
||||
{
|
||||
var a = this.state.attachments;
|
||||
let a = this.state.attachments;
|
||||
if (ev.target.files)
|
||||
for (var i = 0; i < ev.target.files.length; i++)
|
||||
for (let i = 0; i < ev.target.files.length; i++)
|
||||
a.push(ev.target.files[i]);
|
||||
this.setState({ attachments: a });
|
||||
// reset file input
|
||||
|
@ -29,7 +29,7 @@ export default class AttachList extends ListWithSelection
|
|||
|
||||
deleteSelected = () =>
|
||||
{
|
||||
for (var i = this.state.attachments.length-1; i >= 0; i--)
|
||||
for (let 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 });
|
||||
|
@ -47,7 +47,7 @@ export default class AttachList extends ListWithSelection
|
|||
|
||||
getItemOffset = (index) =>
|
||||
{
|
||||
var item = this.refs['a'+index];
|
||||
let item = this.refs['a'+index];
|
||||
return [ item.offsetTop, item.offsetHeight ];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import AttachList from './AttachList.js';
|
||||
import StoreListener from './StoreListener.js';
|
||||
|
||||
class ComposeWindow extends React.PureComponent
|
||||
export default class ComposeWindow extends React.PureComponent
|
||||
{
|
||||
state = {
|
||||
text: ''
|
||||
|
@ -59,5 +58,3 @@ class ComposeWindow extends React.PureComponent
|
|||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
export default StoreListener(ComposeWindow, (data) => { return { accounts: data.accounts }; });
|
||||
|
|
|
@ -20,7 +20,7 @@ export default class DropDownBase extends React.PureComponent
|
|||
|
||||
static hideAll()
|
||||
{
|
||||
for (var i in DropDownBase.instances)
|
||||
for (let i in DropDownBase.instances)
|
||||
DropDownBase.instances[i].hide();
|
||||
}
|
||||
|
||||
|
@ -66,8 +66,8 @@ export default class DropDownBase extends React.PureComponent
|
|||
delete this.onClose;
|
||||
}
|
||||
DropDownBase.currentVisible = [ this, el ];
|
||||
var p = getOffset(el);
|
||||
var left = p.left, top = p.top+el.offsetHeight, calloutLeft = null;
|
||||
let p = getOffset(el);
|
||||
let left = p.left, top = p.top+el.offsetHeight, calloutLeft = null;
|
||||
this.setState({ visible: true, top: top, left: left, selectedItem: -1 });
|
||||
this.refs.dd.style.display = 'block';
|
||||
if (this.props.window)
|
||||
|
@ -75,8 +75,8 @@ export default class DropDownBase extends React.PureComponent
|
|||
left = Math.round(p.left+el.offsetWidth/2-this.refs.dd.offsetWidth/2);
|
||||
top = p.top+el.offsetHeight+3;
|
||||
}
|
||||
var ww = window.innerWidth || de.clientWidth || db.clientWidth;
|
||||
var wh = window.innerHeight || de.clientHeight || db.clientHeight;
|
||||
let ww = window.innerWidth || de.clientWidth || db.clientWidth;
|
||||
let wh = window.innerHeight || de.clientHeight || db.clientHeight;
|
||||
if (left + this.refs.dd.offsetWidth > ww)
|
||||
{
|
||||
left = ww-this.refs.dd.offsetWidth;
|
||||
|
@ -95,20 +95,20 @@ 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;
|
||||
let box = elem.getBoundingClientRect();
|
||||
let body = document.body;
|
||||
let docElem = document.documentElement;
|
||||
let scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
|
||||
let scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
|
||||
let clientTop = docElem.clientTop || body.clientTop || 0;
|
||||
let clientLeft = docElem.clientLeft || body.clientLeft || 0;
|
||||
let top = box.top + scrollTop - clientTop;
|
||||
let left = box.left + scrollLeft - clientLeft;
|
||||
return { top: Math.round(top), left: Math.round(left) };
|
||||
}
|
||||
else
|
||||
{
|
||||
var top = 0, left = 0;
|
||||
let top = 0, left = 0;
|
||||
while(elem)
|
||||
{
|
||||
top = top + parseInt(elem.offsetTop);
|
||||
|
|
|
@ -8,7 +8,7 @@ export default class DropDownMenu extends DropDownBase
|
|||
|
||||
render()
|
||||
{
|
||||
var sel = this.state.selectedItem;
|
||||
let sel = this.state.selectedItem;
|
||||
return <div ref="dd" className={'dropdown'+(this.state.visible ? ' visible' : '')} id={'dropdown-'+this.props.id}
|
||||
tabIndex="1" style={{ top: this.state.top, left: this.state.left }} onClick={this.myOnClick} onKeyDown={this.onKeyDown}
|
||||
onMouseOver={this.onMouseOver}>
|
||||
|
@ -27,7 +27,7 @@ export default class DropDownMenu extends DropDownBase
|
|||
|
||||
onMouseOver = (ev) =>
|
||||
{
|
||||
var t = ev.target;
|
||||
let 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)
|
||||
|
@ -38,8 +38,8 @@ export default class DropDownMenu extends DropDownBase
|
|||
{
|
||||
if (ev.keyCode == 40 || ev.keyCode == 38)
|
||||
{
|
||||
var a = ev.keyCode == 40 ? 1 : this.state.items.length-1;
|
||||
var sel = this.state.selectedItem;
|
||||
let a = ev.keyCode == 40 ? 1 : this.state.items.length-1;
|
||||
let sel = this.state.selectedItem;
|
||||
do
|
||||
{
|
||||
sel = ((sel+a) % this.state.items.length);
|
||||
|
|
|
@ -1,53 +1,54 @@
|
|||
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';
|
||||
import ProgressBar from './ProgressBar.js';
|
||||
|
||||
class FolderList extends React.PureComponent
|
||||
export default 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"+(this.props.progressText ? ' progress-visible' : '')}>
|
||||
<div className="top-border-gradient"></div>
|
||||
<div className="bottom-border-gradient"></div>
|
||||
<div className="actions">
|
||||
<a className="button"><img src="icons/compose.png" /> Compose</a>
|
||||
<DropDownButton dropdownId="check-send" className="check-send" icon="mail_check_send" onClick={self.onClickCheckSend} />
|
||||
<DropDownButton dropdownId="check-send" className="check-send" icon="mail_check_send" onClick={this.onClickCheckSend} />
|
||||
</div>
|
||||
// TODO: keyboard navigation
|
||||
<div className="listview" tabIndex="1">
|
||||
{self.props.accounts.map(function(account, i) {
|
||||
return <AccountFolders key={'a'+account.accountId} accountIndex={i}
|
||||
onSelect={self.onSelectFolder} selected={self.state.selectedAccount == i ? self.state.selectedFolder : -1} account={account} />
|
||||
})}
|
||||
{this.props.accounts.map((account, i) => <AccountFolders
|
||||
key={'a'+account.accountId}
|
||||
accountIndex={i}
|
||||
onSelect={this.onSelectFolder}
|
||||
selected={this.state.selectedAccount == i ? this.state.selectedFolder : -1}
|
||||
account={account}
|
||||
/>)}
|
||||
</div>
|
||||
<MailProgress />
|
||||
<ProgressBar
|
||||
text={this.props.progressText}
|
||||
progress={this.props.progressPercent}
|
||||
/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
onClickCheckSend = () =>
|
||||
{
|
||||
Store.startResync();
|
||||
this.props.startResync();
|
||||
}
|
||||
|
||||
onSelectFolder = (accIndex, folderIndex) =>
|
||||
{
|
||||
var acc = this.props.accounts[accIndex];
|
||||
var folder = this.props.accounts[accIndex].folders[folderIndex];
|
||||
let acc = this.props.accounts[accIndex];
|
||||
let folder = this.props.accounts[accIndex].folders[folderIndex];
|
||||
if (this.state.selectedAccount != accIndex || this.state.selectedFolder != folderIndex)
|
||||
{
|
||||
if (folder.folderId)
|
||||
Store.loadFolder({ folderId: folder.folderId });
|
||||
this.props.loadFolder({ folderId: folder.folderId });
|
||||
else
|
||||
Store.loadFolder({ accountId: acc.accountId, folderType: folder.type });
|
||||
this.props.loadFolder({ accountId: acc.accountId, folderType: folder.type });
|
||||
}
|
||||
this.setState({ selectedAccount: accIndex, selectedFolder: folderIndex });
|
||||
}
|
||||
}
|
||||
|
||||
export default StoreListener(FolderList, (data) => { return { accounts: data.accounts, progressText: data.progressText }; });
|
||||
|
|
|
@ -6,9 +6,9 @@ export default class ListSortSettings extends React.PureComponent
|
|||
{
|
||||
return <div className={this.props.className} value={this.props.sort.sortby}>
|
||||
<select className="sortby">
|
||||
{['sent date', 'status', 'label', 'size', 'subject'].map(function(i) {
|
||||
return <option key={'s'+i} value={i}>Sort by {i}</option>
|
||||
})}
|
||||
{['sent date', 'status', 'label', 'size', 'subject'].map((i) => (
|
||||
<option key={'s'+i} value={i}>Sort by {i}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="group" value={this.props.sort.group}>
|
||||
<option value="">Do not group</option>
|
||||
|
|
|
@ -9,7 +9,7 @@ export default class ListSortSettingsWindow extends DropDownBase
|
|||
|
||||
render()
|
||||
{
|
||||
var sort = this.props.override ? this.props.sorting : this.props.defaultSorting;
|
||||
let 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' : '')}
|
||||
id={'dropdown-'+this.props.id} tabIndex="1" style={{ top: this.state.top, left: this.state.left }}>
|
||||
<div ref="callout" className="callout-top" style={{ left: this.state.calloutLeft }}></div>
|
||||
|
|
|
@ -30,14 +30,14 @@ export default class ListWithSelection extends React.PureComponent
|
|||
}
|
||||
else if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 33 || ev.keyCode == 34) // up, down, pgup, pgdown
|
||||
{
|
||||
var sel = this.curSel, dir;
|
||||
let sel = this.curSel, dir;
|
||||
if (ev.keyCode < 35)
|
||||
dir = (ev.keyCode == 34 ? 1 : -1) * this.getPageSize();
|
||||
else
|
||||
dir = (ev.keyCode == 40 ? 1 : -1);
|
||||
if (sel !== null)
|
||||
{
|
||||
var nsel = sel+dir, n = this.getTotalItems();
|
||||
let nsel = sel+dir, n = this.getTotalItems();
|
||||
if (nsel < 0)
|
||||
nsel = 0;
|
||||
if (nsel >= n)
|
||||
|
@ -48,7 +48,7 @@ export default class ListWithSelection extends React.PureComponent
|
|||
this.selectTo(nsel);
|
||||
else
|
||||
this.selectOne(nsel);
|
||||
var pos = this.getItemOffset(nsel);
|
||||
let pos = this.getItemOffset(nsel);
|
||||
if (pos[0] + pos[1] > this.refs.scroll.scrollTop + this.refs.scroll.offsetHeight)
|
||||
this.refs.scroll.scrollTop = pos[0] + pos[1] - this.refs.scroll.offsetHeight;
|
||||
else if (pos[0] < this.refs.scroll.scrollTop + this.getScrollPaddingTop())
|
||||
|
@ -70,11 +70,11 @@ export default class ListWithSelection extends React.PureComponent
|
|||
}
|
||||
else if (ev.keyCode == 35) // end
|
||||
{
|
||||
var nsel = this.getTotalItems()-1;
|
||||
let nsel = this.getTotalItems()-1;
|
||||
if (ev.shiftKey)
|
||||
{
|
||||
this.selectTo(nsel);
|
||||
var pos = this.getItemOffset(nsel);
|
||||
let pos = this.getItemOffset(nsel);
|
||||
this.refs.scroll.scrollTop = pos[0] + pos[1] - this.refs.scroll.offsetHeight;
|
||||
}
|
||||
else
|
||||
|
@ -86,8 +86,8 @@ export default class ListWithSelection extends React.PureComponent
|
|||
{
|
||||
if (this.lastSel === undefined)
|
||||
return this.selectOne(ns);
|
||||
var sel = {};
|
||||
var n = this.getTotalItems();
|
||||
let sel = {};
|
||||
let n = this.getTotalItems();
|
||||
if (this.lastSel >= n)
|
||||
this.lastSel = n-1;
|
||||
if (ns < this.lastSel)
|
||||
|
@ -104,7 +104,7 @@ export default class ListWithSelection extends React.PureComponent
|
|||
|
||||
selectOne(ns)
|
||||
{
|
||||
var sel = {};
|
||||
let sel = {};
|
||||
sel[ns] = true;
|
||||
this.setState({ selected: sel });
|
||||
this.lastSel = ns;
|
||||
|
@ -115,12 +115,12 @@ export default class ListWithSelection extends React.PureComponent
|
|||
|
||||
onListItemClick = (ev) =>
|
||||
{
|
||||
var t = ev.target;
|
||||
let t = ev.target;
|
||||
while (t && !t.getAttribute('data-i'))
|
||||
t = t.parentNode;
|
||||
if (t)
|
||||
{
|
||||
var ns = parseInt(t.getAttribute('data-i'));
|
||||
let ns = parseInt(t.getAttribute('data-i'));
|
||||
if (ev.shiftKey)
|
||||
this.selectTo(ns);
|
||||
else if (ev.ctrlKey)
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import StoreListener from './StoreListener.js';
|
||||
import ProgressBar from './ProgressBar.js';
|
||||
|
||||
export default StoreListener(ProgressBar, function(data)
|
||||
{
|
||||
return { text: data.progressText, progress: data.progressPercent };
|
||||
});
|
|
@ -1,10 +1,8 @@
|
|||
import React from 'react';
|
||||
import DropDownBase from './DropDownBase.js';
|
||||
import ListSortSettings from './ListSortSettings.js';
|
||||
import Store from './Store.js';
|
||||
import StoreListener from './StoreListener.js';
|
||||
|
||||
class MailSettingsWindow extends DropDownBase
|
||||
export default class MailSettingsWindow extends DropDownBase
|
||||
{
|
||||
render()
|
||||
{
|
||||
|
@ -21,7 +19,7 @@ class MailSettingsWindow extends DropDownBase
|
|||
<div className="text">Default List Sorting</div>
|
||||
<ListSortSettings className="fields" sort={this.props.defaultSorting} />
|
||||
<div className="fields">
|
||||
<label><input type="checkbox" checked={this.props.quickReply} onClick={this.showQuickReply} /> Show Quick Reply</label>
|
||||
<label><input type="checkbox" checked={this.props.quickReply} onClick={this.props.toggleQuickReply} /> Show Quick Reply</label>
|
||||
</div>
|
||||
<div className="split"><i></i></div>
|
||||
<div className="text">Mark as Read</div>
|
||||
|
@ -39,31 +37,8 @@ class MailSettingsWindow extends DropDownBase
|
|||
|
||||
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 = () =>
|
||||
{
|
||||
Store.set('quickReply', !this.props.quickReply);
|
||||
let t = ev.target.nodeName == 'A' ? ev.target : ev.target.parentNode;
|
||||
let l = / mail-(\S+)/.exec(t.className)[1];
|
||||
this.props.setLayout(l);
|
||||
}
|
||||
}
|
||||
|
||||
export default StoreListener(
|
||||
MailSettingsWindow,
|
||||
(data) => { return { layout: data.layout, quickReply: data.quickReply }; },
|
||||
{
|
||||
id: 'settings',
|
||||
window: true,
|
||||
markDelay: -1,
|
||||
defaultSorting: {
|
||||
sort: {
|
||||
sortby: 'sent date',
|
||||
group: 'date',
|
||||
ascending: false,
|
||||
threaded: false
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
135
MessageList.js
135
MessageList.js
|
@ -1,8 +1,6 @@
|
|||
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';
|
||||
|
||||
class MessageInList extends React.PureComponent
|
||||
|
@ -11,7 +9,7 @@ class MessageInList extends React.PureComponent
|
|||
|
||||
render()
|
||||
{
|
||||
var msg = this.props.msg;
|
||||
let msg = this.props.msg;
|
||||
return <div data-i={this.props.i} className={'message'+
|
||||
(msg.body_text || msg.body_html ? '' : ' unloaded')+
|
||||
(msg.flags.map(c => (this.msgClasses[c] ? ' '+this.msgClasses[c] : '')).join(''))+
|
||||
|
@ -34,7 +32,7 @@ class MessageInList extends React.PureComponent
|
|||
}
|
||||
|
||||
// TODO: expand/collapse days
|
||||
class MessageList extends ListWithSelection
|
||||
export default class MessageList extends ListWithSelection
|
||||
{
|
||||
_preloadSize = 20
|
||||
_pageSize = 50
|
||||
|
@ -52,14 +50,14 @@ class MessageList extends ListWithSelection
|
|||
// Main virtual scroll detector method
|
||||
setFirstDayFromProps(props)
|
||||
{
|
||||
var groups = props.groups;
|
||||
var messages = props.messages;
|
||||
let groups = props.groups;
|
||||
let messages = props.messages;
|
||||
if (!groups || !groups.length)
|
||||
return;
|
||||
var scrollTop = this.refs.scroll.scrollTop, scrollSize = this.refs.scroll.offsetHeight - this.getScrollPaddingTop();
|
||||
var top = 0, p, firstVisibleGrp, firstVisible, lastVisibleGrp, lastVisible;
|
||||
var itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
|
||||
var i;
|
||||
let scrollTop = this.refs.scroll.scrollTop, scrollSize = this.refs.scroll.offsetHeight - this.getScrollPaddingTop();
|
||||
let top = 0, p, firstVisibleGrp, firstVisible, lastVisibleGrp, lastVisible;
|
||||
let itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
|
||||
let i;
|
||||
for (i = 0; i < groups.length; i++)
|
||||
{
|
||||
p = top;
|
||||
|
@ -103,23 +101,23 @@ class MessageList extends ListWithSelection
|
|||
lastGrp: lastVisibleGrp,
|
||||
lastMsg: lastVisible
|
||||
});
|
||||
var loadFirst = groups[firstVisibleGrp].start+firstVisible-this._preloadSize;
|
||||
var loadLast = groups[lastVisibleGrp].start+lastVisible+1+this._preloadSize;
|
||||
var total = groups[groups.length-1].messageCount+groups[groups.length-1].start;
|
||||
let loadFirst = groups[firstVisibleGrp].start+firstVisible-this._preloadSize;
|
||||
let loadLast = groups[lastVisibleGrp].start+lastVisible+1+this._preloadSize;
|
||||
let total = groups[groups.length-1].messageCount+groups[groups.length-1].start;
|
||||
loadLast = Math.floor((loadLast + this._pageSize - 1) / this._pageSize) * this._pageSize;
|
||||
loadLast = loadLast < total ? loadLast : total;
|
||||
loadFirst = loadFirst < 0 ? 0 : loadFirst;
|
||||
loadFirst = loadFirst - (loadFirst % this._pageSize);
|
||||
var loadFirstEnd;
|
||||
let loadFirstEnd;
|
||||
for (loadFirstEnd = loadFirst; loadFirstEnd < loadLast && messages[loadFirstEnd] === undefined; loadFirstEnd++)
|
||||
messages[loadFirstEnd] = null;
|
||||
var loadLastStart;
|
||||
let loadLastStart;
|
||||
for (loadLastStart = loadLast; loadLastStart > loadFirst && messages[loadLastStart-1] === undefined; loadLastStart--)
|
||||
messages[loadLastStart-1] = null;
|
||||
if (loadFirstEnd > loadFirst)
|
||||
Store.loadMessages(loadFirst, loadFirstEnd-loadFirst);
|
||||
this.props.loadMessages(loadFirst, loadFirstEnd-loadFirst);
|
||||
if (loadFirstEnd < loadLastStart && loadLastStart < loadLast)
|
||||
Store.loadMessages(loadLastStart, loadLast-loadLastStart);
|
||||
this.props.loadMessages(loadLastStart, loadLast-loadLastStart);
|
||||
}
|
||||
|
||||
changeFirstDay = (ev) =>
|
||||
|
@ -134,29 +132,30 @@ class MessageList extends ListWithSelection
|
|||
|
||||
onSelectCurrent = (index) =>
|
||||
{
|
||||
var self = this;
|
||||
var total = 0, p, msg, idx;
|
||||
for (var i = 0; i < (self.props.groups||[]).length; i++)
|
||||
let total = 0, p, msg, idx;
|
||||
for (let i = 0; i < (this.props.groups||[]).length; i++)
|
||||
{
|
||||
p = total;
|
||||
total += (i > 0 ? 1 : 0)+self.props.groups[i].messageCount;
|
||||
total += (i > 0 ? 1 : 0)+this.props.groups[i].messageCount;
|
||||
if (index < total)
|
||||
{
|
||||
idx = self.props.groups[i].start+index-p-(i > 0 ? 1 : 0);
|
||||
msg = self.props.messages[idx];
|
||||
idx = this.props.groups[i].start+index-p-(i > 0 ? 1 : 0);
|
||||
msg = this.props.messages[idx];
|
||||
if (msg && !msg.body_text && !msg.body_html)
|
||||
{
|
||||
Store.loadMessage(msg.id, function(newMsg)
|
||||
this.props.loadMessage(msg.id, (newMsg) =>
|
||||
{
|
||||
Store.set('msg', newMsg);
|
||||
if (self.props.messages[idx] == msg)
|
||||
this.props.setMessage(newMsg);
|
||||
if (this.props.messages[idx] == msg)
|
||||
{
|
||||
self.props.messages[idx] = newMsg;
|
||||
this.props.messages[idx] = newMsg;
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
Store.set('msg', msg);
|
||||
{
|
||||
this.props.setMessage(msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -164,8 +163,8 @@ class MessageList extends ListWithSelection
|
|||
|
||||
getTotalItems = () =>
|
||||
{
|
||||
var total = -1; // do not count first-day as item
|
||||
for (var i = 0; i < (this.props.groups||[]).length; i++)
|
||||
let total = -1; // do not count first-day as item
|
||||
for (let i = 0; i < (this.props.groups||[]).length; i++)
|
||||
{
|
||||
total += 1+this.props.groups[i].messageCount;
|
||||
}
|
||||
|
@ -179,9 +178,9 @@ class MessageList extends ListWithSelection
|
|||
|
||||
getItemOffset = (index) =>
|
||||
{
|
||||
var n = 0, top = 0, p;
|
||||
var h = (this.props.layout == 'message-on-right' ? 60 : 30);
|
||||
for (var i = 0; i < (this.props.groups||[]).length; i++)
|
||||
let n = 0, top = 0, p, i;
|
||||
let h = (this.props.layout == 'message-on-right' ? 60 : 30);
|
||||
for (i = 0; i < (this.props.groups||[]).length; i++)
|
||||
{
|
||||
p = n;
|
||||
n += (i > 0 ? 1 : 0)+this.props.groups[i].messageCount;
|
||||
|
@ -202,8 +201,8 @@ class MessageList extends ListWithSelection
|
|||
|
||||
getMessages = (grp, start, end) =>
|
||||
{
|
||||
var a = this.props.messages.slice(grp.start+start, grp.start+end);
|
||||
for (var i = 0; i < end-start; i++)
|
||||
let a = this.props.messages.slice(grp.start+start, grp.start+end);
|
||||
for (let i = 0; i < end-start; i++)
|
||||
if (!a[i])
|
||||
a[i] = null;
|
||||
return a;
|
||||
|
@ -211,18 +210,19 @@ class MessageList extends ListWithSelection
|
|||
|
||||
onSearchTextChange = (event) =>
|
||||
{
|
||||
var s = event.target.value;
|
||||
let s = event.target.value;
|
||||
this.setState({ searchText: s });
|
||||
if (this._searchTimeout)
|
||||
{
|
||||
clearTimeout(this._searchTimeout);
|
||||
this._searchTimeout = setTimeout(function() { Store.search(s) }, 300);
|
||||
}
|
||||
this._searchTimeout = setTimeout(() => this.props.search(s), 300);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
var self = this;
|
||||
var total = 0;
|
||||
var itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
|
||||
let total = 0;
|
||||
let itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
|
||||
return <div className="message-list">
|
||||
<div className="top-border-gradient"></div>
|
||||
<div className="bottom-border-gradient"></div>
|
||||
|
@ -232,46 +232,47 @@ class MessageList extends ListWithSelection
|
|||
</div>
|
||||
<DropDownButton dropdownId="threads" className="threads"
|
||||
title="Show Message Thread" checkedTitle="Hide Message Thread" icon="thread" checkedIcon="thread_selected" checkable="1" />
|
||||
<DropDownButton dropdownId="settings" className="settings" whole="1" hidden={self.props.layout == 'message-on-right'}
|
||||
title="Settings for self folder" icon="config" />
|
||||
<DropDownButton dropdownId="settings" className="settings" whole="1" hidden={this.props.layout == 'message-on-right'}
|
||||
title="Settings for this folder" icon="config" />
|
||||
<DropDownButton dropdownId="list-sort" className="list-sort" whole="1"
|
||||
title="Sorting settings" icon="list_sort" />
|
||||
<div className="clear"></div>
|
||||
</div>
|
||||
<div ref="scroll" className="listview" tabIndex="1" onScroll={self.changeFirstDay} onKeyDown={self.onListKeyDown}>
|
||||
<div ref="scroll" className="listview" tabIndex="1" onScroll={this.changeFirstDay} onKeyDown={this.onListKeyDown}>
|
||||
<div ref="title" className="day first-day"
|
||||
style={{ top: self.state.firstDayTop, display: self.state.firstDay ? '' : 'none' }}>
|
||||
{(self.state.firstDay||'').toUpperCase()}
|
||||
style={{ top: this.state.firstDayTop, display: this.state.firstDay ? '' : 'none' }}>
|
||||
{(this.state.firstDay||'').toUpperCase()}
|
||||
</div>
|
||||
{(self.props.groups||[]).map(function(grp, i) {
|
||||
{(this.props.groups||[]).map((grp, i) =>
|
||||
{
|
||||
if (i > 0)
|
||||
total++;
|
||||
var start = total+(self.state.firstGrp == i ? self.state.firstMsg : 0);
|
||||
var r = [
|
||||
let start = total+(this.state.firstGrp == i ? this.state.firstMsg : 0);
|
||||
let r = [
|
||||
i > 0 ? <div className="day" data-i={total-1}>{grp.name.toUpperCase()}</div> : null,
|
||||
<div className="day-list">
|
||||
{(self.state.firstGrp > i || self.state.lastGrp < i
|
||||
{(this.state.firstGrp > i || this.state.lastGrp < i
|
||||
? <div className="placeholder" style={{ height: itemH*grp.messageCount }}></div>
|
||||
: [
|
||||
(self.state.firstGrp == i
|
||||
? <div className="placeholder" style={{ height: itemH*self.state.firstMsg }}></div>
|
||||
(this.state.firstGrp == i
|
||||
? <div className="placeholder" style={{ height: itemH*this.state.firstMsg }}></div>
|
||||
: null),
|
||||
self.getMessages(grp,
|
||||
self.state.firstGrp == i ? self.state.firstMsg : 0,
|
||||
self.state.lastGrp == i ? self.state.lastMsg+1 : grp.messageCount
|
||||
).map(function(msg, j) { return (msg
|
||||
this.getMessages(grp,
|
||||
this.state.firstGrp == i ? this.state.firstMsg : 0,
|
||||
this.state.lastGrp == i ? this.state.lastMsg+1 : grp.messageCount
|
||||
).map((msg, j) => (msg
|
||||
? [
|
||||
<MessageInList threads={self.props.threads} msg={msg} i={start+j} selected={self.isSelected(start+j)} onClick={self.onListItemClick} />,
|
||||
/*(msg.thread && Store.threads ?
|
||||
<MessageInList threads={this.props.threads} msg={msg} i={start+j} selected={this.isSelected(start+j)} onClick={this.onListItemClick} />,
|
||||
/*(msg.thread && this.props.threads ?
|
||||
<div className="thread">
|
||||
{msg.thread.map(reply => <MessageInList msg={reply} i={0} onClick={self.onListItemClick} />)}
|
||||
{msg.thread.map(reply => <MessageInList msg={reply} i={0} onClick={this.onListItemClick} />)}
|
||||
</div>
|
||||
: null)*/
|
||||
]
|
||||
: <div data-i={start+j} className={'message'+(self.isSelected(start+j) ? ' selected' : '')} onMouseDown={self.onListItemClick}></div>
|
||||
); }),
|
||||
(self.state.lastGrp == i
|
||||
? <div className="placeholder" style={{ height: itemH*(grp.messageCount-self.state.lastMsg-1) }}></div>
|
||||
: <div data-i={start+j} className={'message'+(this.isSelected(start+j) ? ' selected' : '')} onMouseDown={this.onListItemClick}></div>
|
||||
)),
|
||||
(this.state.lastGrp == i
|
||||
? <div className="placeholder" style={{ height: itemH*(grp.messageCount-this.state.lastMsg-1) }}></div>
|
||||
: null)
|
||||
]
|
||||
)}
|
||||
|
@ -294,13 +295,3 @@ class MessageList extends ListWithSelection
|
|||
window.removeEventListener('resize', this.changeFirstDay);
|
||||
}
|
||||
}
|
||||
|
||||
export default StoreListener(MessageList, function(data)
|
||||
{
|
||||
return {
|
||||
threads: data.threads,
|
||||
layout: data.layout,
|
||||
groups: data.listGroups,
|
||||
messages: data.messages
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import React from 'react';
|
||||
import DropDownButton from './DropDownButton.js';
|
||||
import Store from './Store.js';
|
||||
import StoreListener from './StoreListener.js';
|
||||
import Util from './Util.js';
|
||||
|
||||
class MessageView extends React.PureComponent
|
||||
export default class MessageView extends React.PureComponent
|
||||
{
|
||||
state = { showImages: false }
|
||||
|
||||
|
@ -151,5 +149,3 @@ class MessageView extends React.PureComponent
|
|||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default StoreListener(MessageView, (data) => { return { layout: data.layout, quickReply: data.quickReply, msg: data.msg }; });
|
||||
|
|
199
Store.js
199
Store.js
|
@ -1,199 +0,0 @@
|
|||
import superagent from 'superagent';
|
||||
import socket_io from 'socket.io-client';
|
||||
|
||||
import _ from './I18n.js';
|
||||
import Util from './Util.js';
|
||||
|
||||
const Store = {
|
||||
data: {
|
||||
layout: 'message-on-right',
|
||||
quickReply: true,
|
||||
msg: null,
|
||||
threads: false,
|
||||
accounts: [],
|
||||
listGroups: [],
|
||||
messages: []
|
||||
},
|
||||
|
||||
listeners: [],
|
||||
on: function(cb)
|
||||
{
|
||||
this.listeners.push(cb);
|
||||
},
|
||||
un: function(cb)
|
||||
{
|
||||
for (var i = this.listeners.length; i >= 0; i--)
|
||||
if (this.listeners[i] == cb)
|
||||
this.listeners.splice(i, 1);
|
||||
},
|
||||
get: function(k)
|
||||
{
|
||||
return this.data[k];
|
||||
},
|
||||
set: function(k, v)
|
||||
{
|
||||
this.data[k] = v;
|
||||
(this.listeners || []).map(i => i());
|
||||
},
|
||||
setAll: function(obj)
|
||||
{
|
||||
for (var k in obj)
|
||||
this.data[k] = obj[k];
|
||||
(this.listeners || []).map(i => i());
|
||||
},
|
||||
|
||||
startIo: function()
|
||||
{
|
||||
var self = this;
|
||||
this.io = socket_io('', { path: window.location.pathname.replace(/[^\/]+$/, 'backend/socket.io') });
|
||||
this.io.on('sync', function(params)
|
||||
{
|
||||
if (params.state == 'start')
|
||||
{
|
||||
self.setAll({ progressText: 'Syncing '+params.email+' / '+params.folder, progressPercent: 0 });
|
||||
}
|
||||
else if (params.state == 'progress')
|
||||
{
|
||||
self.setAll({ progressPercent: Math.round(100*params.done/(params.total||1)) });
|
||||
}
|
||||
else if (params.state == 'finish-box')
|
||||
{
|
||||
self.setAll({ progressPercent: 100 });
|
||||
}
|
||||
else if (params.state == 'complete')
|
||||
{
|
||||
self.setAll({ progressText: '', progressPercent: 0 });
|
||||
}
|
||||
self.set('sync', params.progress);
|
||||
});
|
||||
},
|
||||
|
||||
loadAccounts: function()
|
||||
{
|
||||
superagent.get('backend/folders').end(function(err, res)
|
||||
{
|
||||
var ixOfAll = {
|
||||
received: 1,
|
||||
outbox: 3,
|
||||
sent: 4,
|
||||
drafts: 5,
|
||||
spam: 6,
|
||||
trash: 7
|
||||
};
|
||||
var accounts = [ {
|
||||
name: _('All Messages'),
|
||||
accountId: null,
|
||||
unreadCount: 0,
|
||||
folders: [
|
||||
{ name: _('Unread'), icon: 'mail_unread', unreadCount: 0, type: 'unread' },
|
||||
{ name: _('Received'), icon: 'mail_received', unreadCount: 0, type: 'inbox' },
|
||||
{ name: _('Pinned'), icon: 'mail_pinned', unreadCount: 0, type: 'pinned' },
|
||||
{ name: _('Outbox'), icon: 'mail_outbox', unreadCount: 0, type: 'outbox' },
|
||||
{ name: _('Sent'), icon: 'mail_sent', unreadCount: 0, type: 'sent' },
|
||||
{ name: _('Drafts'), icon: 'mail_drafts', unreadCount: 0, type: 'drafts' },
|
||||
{ name: _('Spam'), icon: 'mail_spam', unreadCount: 0, type: 'spam' },
|
||||
{ name: _('Trash'), icon: 'mail_trash', unreadCount: 0, type: 'trash' },
|
||||
],
|
||||
} ];
|
||||
for (let a of res.body.accounts)
|
||||
{
|
||||
let account = {
|
||||
name: a.name,
|
||||
email: a.email,
|
||||
accountId: a.id,
|
||||
unreadCount: 0,
|
||||
warning: false,
|
||||
folders: [
|
||||
{ name: _('Unread'), icon: 'mail_unread', unreadCount: 0, type: 'unread' },
|
||||
{ name: _('Pinned'), icon: 'mail_pinned', unreadCount: a.pinned_unread_count, type: 'pinned' },
|
||||
],
|
||||
folderMap: a.foldermap,
|
||||
folderTypes: {}
|
||||
};
|
||||
if (!account.folderMap.received)
|
||||
{
|
||||
account.folderMap.received = 'INBOX';
|
||||
}
|
||||
for (let f in account.folderMap)
|
||||
{
|
||||
account.folderTypes[account.folderMap[f]] = f;
|
||||
}
|
||||
for (let f of a.folders)
|
||||
{
|
||||
let icon = (account.folderTypes[f.name] ? 'mail_'+account.folderTypes[f.name] : 'folder');
|
||||
account.folders.push({ name: f.name, icon: icon, unreadCount: f.unread_count-0, folderId: f.id });
|
||||
account.folders[0].unreadCount += (f.unread_count-0);
|
||||
if (account.folderTypes[f.name])
|
||||
{
|
||||
accounts[0].folders[ixOfAll[account.folderTypes[f.name]]].unreadCount += (f.unread_count-0);
|
||||
}
|
||||
account.unreadCount += (f.unread_count-0);
|
||||
}
|
||||
accounts.push(account);
|
||||
accounts[0].unreadCount += account.unreadCount;
|
||||
accounts[0].folders[0].unreadCount += account.unreadCount;
|
||||
accounts[0].folders[2].unreadCount += account.folders[1].unreadCount;
|
||||
}
|
||||
Store.set('accounts', accounts);
|
||||
});
|
||||
},
|
||||
|
||||
loadFolder: function(folderParams)
|
||||
{
|
||||
superagent.get('backend/groups').query(folderParams).end(function(err, res)
|
||||
{
|
||||
var groups = res.body.groups.map(g => { return { name: Util.getGroupName(g.name), messageCount: g.count-0, start: 0 } });
|
||||
var start = 0;
|
||||
for (var i = 0; i < groups.length; i++)
|
||||
{
|
||||
groups[i].start = start;
|
||||
start += groups[i].messageCount;
|
||||
}
|
||||
Store.setAll({
|
||||
folderParams: folderParams,
|
||||
listGroups: groups,
|
||||
messages: []
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
search: function(text)
|
||||
{
|
||||
Store.loadFolder({ ...Store.get('folderParams'), search: text });
|
||||
},
|
||||
|
||||
loadMessages: function(start, count)
|
||||
{
|
||||
var p = { ...Store.get('folderParams') };
|
||||
p.offset = start;
|
||||
p.limit = count;
|
||||
superagent.get('backend/messages').query(p).end(function(err, res)
|
||||
{
|
||||
var msgs = Store.get('messages').slice(0);
|
||||
var par = res.body.messages;
|
||||
par.unshift(par.length);
|
||||
par.unshift(start);
|
||||
msgs.splice.apply(msgs, par);
|
||||
Store.set('messages', msgs);
|
||||
});
|
||||
},
|
||||
|
||||
loadMessage: function(msgId, callback)
|
||||
{
|
||||
superagent.get('backend/message').query({ msgId: msgId }).end(function(err, res)
|
||||
{
|
||||
callback(res.body.msg);
|
||||
});
|
||||
},
|
||||
|
||||
startResync: function()
|
||||
{
|
||||
superagent.post('backend/sync').send().end(function(err, res)
|
||||
{
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Store.startIo();
|
||||
|
||||
export default Store;
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import Store from './Store.js';
|
||||
|
||||
// "react-redux connect()"-like example
|
||||
class StoreListener extends React.PureComponent
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default 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;
|
||||
};
|
19
TabPanel.js
19
TabPanel.js
|
@ -6,11 +6,11 @@ export default class TabPanel extends React.PureComponent
|
|||
|
||||
render()
|
||||
{
|
||||
var bar = [];
|
||||
var body = [];
|
||||
for (var i = 0; i < this.state.tabs.length; i++)
|
||||
let bar = [];
|
||||
let body = [];
|
||||
for (let i = 0; i < this.state.tabs.length; i++)
|
||||
{
|
||||
var t = this.state.tabs[i];
|
||||
let t = this.state.tabs[i];
|
||||
bar.push(
|
||||
<div key={'t'+i} className={'tab'+(i == this.state.selected ? ' selected' : '')+(t.noclose ? ' noclose' : '')}
|
||||
id={'t-tab'+i} onClick={this.switchTab} style={i == this.state.closing ? { width: '1px', padding: '0', opacity: '0' } : null}>
|
||||
|
@ -44,19 +44,18 @@ export default class TabPanel extends React.PureComponent
|
|||
|
||||
closeTab = (ev) =>
|
||||
{
|
||||
var self = this;
|
||||
var tab = ev.target.parentNode;
|
||||
let tab = ev.target.parentNode;
|
||||
//this.setState({ closing: tab.id.substr(5) });
|
||||
tab.style.width = '1px';
|
||||
tab.style.padding = '0';
|
||||
tab.style.opacity = '0';
|
||||
setTimeout(function()
|
||||
setTimeout(() =>
|
||||
{
|
||||
var t = self.state.tabs;
|
||||
let t = this.state.tabs;
|
||||
t.splice(tab.id.substr(5), 1);
|
||||
var s = self.state.selected;
|
||||
let s = this.state.selected;
|
||||
if ('t-tab'+s == tab.id)
|
||||
s = self.state.selected-1;
|
||||
s = this.state.selected-1;
|
||||
this.setState({ tabs: t, selected: s });
|
||||
}, 200);
|
||||
ev.stopPropagation();
|
||||
|
|
12
Util.js
12
Util.js
|
@ -16,25 +16,25 @@ export default class Util
|
|||
{
|
||||
if (!(dt instanceof Date))
|
||||
dt = new Date(dt.replace(' ', 'T'));
|
||||
var tod = new Date();
|
||||
let tod = new Date();
|
||||
tod.setHours(0);
|
||||
tod.setMinutes(0);
|
||||
tod.setSeconds(0);
|
||||
tod.setMilliseconds(0);
|
||||
var prevweek = tod;
|
||||
let prevweek = tod;
|
||||
prevweek = prevweek.getTime() - (7 + (prevweek.getDay()+6)%7)*86400000;
|
||||
if (dt.getTime() < prevweek)
|
||||
{
|
||||
var d = dt.getDate();
|
||||
var m = dt.getMonth()+1;
|
||||
let d = dt.getDate();
|
||||
let m = dt.getMonth()+1;
|
||||
return (d < 10 ? '0' : '')+d+'.'+(m < 10 ? '0' : '')+m+'.'+dt.getFullYear();
|
||||
}
|
||||
else if (dt.getTime() < tod.getTime())
|
||||
{
|
||||
return Util.WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Util.Months[dt.getMonth()];
|
||||
}
|
||||
var h = dt.getHours();
|
||||
var m = dt.getMinutes();
|
||||
let h = dt.getHours();
|
||||
let m = dt.getMinutes();
|
||||
return (h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m;
|
||||
}
|
||||
|
||||
|
|
397
mail.js
397
mail.js
|
@ -1,42 +1,381 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import superagent from 'superagent';
|
||||
import socket_io from 'socket.io-client';
|
||||
|
||||
import ComposeWindow from './ComposeWindow.js';
|
||||
import FolderList from './FolderList.js';
|
||||
import MessageList from './MessageList.js';
|
||||
import MessageView from './MessageView.js';
|
||||
import DropDownMenu from './DropDownMenu.js';
|
||||
import ListSortSettingsWindow from './ListSortSettingsWindow.js';
|
||||
import MailSettingsWindow from './MailSettingsWindow.js';
|
||||
import TabPanel from './TabPanel.js';
|
||||
import Store from './Store.js';
|
||||
import StoreListener from './StoreListener.js';
|
||||
import AllDropdowns from './AllDropdowns.js';
|
||||
import _ from './I18n.js';
|
||||
import Util from './Util.js';
|
||||
|
||||
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame;
|
||||
|
||||
var AllTabs = StoreListener(TabPanel, function(data)
|
||||
class MainWindow extends React.PureComponent
|
||||
{
|
||||
return { tabs: [
|
||||
{
|
||||
className: data.layout,
|
||||
noclose: true,
|
||||
icon: 'mail_unread',
|
||||
title: 'Unread (64)',
|
||||
children: [ <MessageList key="1" />, <MessageView key="2" /> ]
|
||||
},
|
||||
{
|
||||
icon: 'mail_drafts',
|
||||
i16: true,
|
||||
title: 'Compose Message',
|
||||
children: [ <ComposeWindow key="1" /> ]
|
||||
}
|
||||
] }
|
||||
});
|
||||
state = {
|
||||
layout: 'message-on-right',
|
||||
quickReply: true,
|
||||
msg: null,
|
||||
threads: false,
|
||||
accounts: [],
|
||||
listGroups: [],
|
||||
messages: []
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
{AllDropdowns()}
|
||||
<FolderList />
|
||||
<AllTabs />
|
||||
</div>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
startIo = () =>
|
||||
{
|
||||
this.io = socket_io('', { path: window.location.pathname.replace(/[^\/]+$/, 'backend/socket.io') });
|
||||
this.io.on('sync', (params) =>
|
||||
{
|
||||
if (params.state == 'start')
|
||||
{
|
||||
this.setState({ progressText: 'Syncing '+params.email+' / '+params.folder, progressPercent: 0 });
|
||||
}
|
||||
else if (params.state == 'progress')
|
||||
{
|
||||
this.setState({ progressPercent: Math.round(100*params.done/(params.total||1)) });
|
||||
}
|
||||
else if (params.state == 'finish-box')
|
||||
{
|
||||
this.setState({ progressPercent: 100 });
|
||||
}
|
||||
else if (params.state == 'complete')
|
||||
{
|
||||
this.setState({ progressText: '', progressPercent: 0 });
|
||||
}
|
||||
this.setState({ sync: params.progress });
|
||||
});
|
||||
}
|
||||
|
||||
Store.loadAccounts();
|
||||
loadAccounts()
|
||||
{
|
||||
superagent.get('backend/folders').end((err, res) =>
|
||||
{
|
||||
let ixOfAll = {
|
||||
received: 1,
|
||||
outbox: 3,
|
||||
sent: 4,
|
||||
drafts: 5,
|
||||
spam: 6,
|
||||
trash: 7
|
||||
};
|
||||
let accounts = [ {
|
||||
name: _('All Messages'),
|
||||
accountId: null,
|
||||
unreadCount: 0,
|
||||
folders: [
|
||||
{ name: _('Unread'), icon: 'mail_unread', unreadCount: 0, type: 'unread' },
|
||||
{ name: _('Received'), icon: 'mail_received', unreadCount: 0, type: 'inbox' },
|
||||
{ name: _('Pinned'), icon: 'mail_pinned', unreadCount: 0, type: 'pinned' },
|
||||
{ name: _('Outbox'), icon: 'mail_outbox', unreadCount: 0, type: 'outbox' },
|
||||
{ name: _('Sent'), icon: 'mail_sent', unreadCount: 0, type: 'sent' },
|
||||
{ name: _('Drafts'), icon: 'mail_drafts', unreadCount: 0, type: 'drafts' },
|
||||
{ name: _('Spam'), icon: 'mail_spam', unreadCount: 0, type: 'spam' },
|
||||
{ name: _('Trash'), icon: 'mail_trash', unreadCount: 0, type: 'trash' },
|
||||
],
|
||||
} ];
|
||||
for (let a of res.body.accounts)
|
||||
{
|
||||
let account = {
|
||||
name: a.name,
|
||||
email: a.email,
|
||||
accountId: a.id,
|
||||
unreadCount: 0,
|
||||
warning: false,
|
||||
folders: [
|
||||
{ name: _('Unread'), icon: 'mail_unread', unreadCount: 0, type: 'unread' },
|
||||
{ name: _('Pinned'), icon: 'mail_pinned', unreadCount: a.pinned_unread_count, type: 'pinned' },
|
||||
],
|
||||
folderMap: a.foldermap,
|
||||
folderTypes: {}
|
||||
};
|
||||
if (!account.folderMap.received)
|
||||
{
|
||||
account.folderMap.received = 'INBOX';
|
||||
}
|
||||
for (let f in account.folderMap)
|
||||
{
|
||||
account.folderTypes[account.folderMap[f]] = f;
|
||||
}
|
||||
for (let f of a.folders)
|
||||
{
|
||||
let icon = (account.folderTypes[f.name] ? 'mail_'+account.folderTypes[f.name] : 'folder');
|
||||
account.folders.push({ name: f.name, icon: icon, unreadCount: f.unread_count-0, folderId: f.id });
|
||||
account.folders[0].unreadCount += (f.unread_count-0);
|
||||
if (account.folderTypes[f.name])
|
||||
{
|
||||
accounts[0].folders[ixOfAll[account.folderTypes[f.name]]].unreadCount += (f.unread_count-0);
|
||||
}
|
||||
account.unreadCount += (f.unread_count-0);
|
||||
}
|
||||
accounts.push(account);
|
||||
accounts[0].unreadCount += account.unreadCount;
|
||||
accounts[0].folders[0].unreadCount += account.unreadCount;
|
||||
accounts[0].folders[2].unreadCount += account.folders[1].unreadCount;
|
||||
}
|
||||
this.setState({ accounts });
|
||||
});
|
||||
}
|
||||
|
||||
loadFolder = (folderParams) =>
|
||||
{
|
||||
superagent.get('backend/groups').query(folderParams).end((err, res) =>
|
||||
{
|
||||
let groups = res.body.groups.map(g => ({ name: Util.getGroupName(g.name), messageCount: g.count-0, start: 0 }));
|
||||
let start = 0;
|
||||
for (let i = 0; i < groups.length; i++)
|
||||
{
|
||||
groups[i].start = start;
|
||||
start += groups[i].messageCount;
|
||||
}
|
||||
this.setState({
|
||||
folderParams: folderParams,
|
||||
listGroups: groups,
|
||||
messages: []
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
search = (text) =>
|
||||
{
|
||||
this.loadFolder({ ...this.state.folderParams, search: text });
|
||||
}
|
||||
|
||||
loadMessages = (start, count) =>
|
||||
{
|
||||
let p = { ...this.state.folderParams };
|
||||
p.offset = start;
|
||||
p.limit = count;
|
||||
superagent.get('backend/messages').query(p).end((err, res) =>
|
||||
{
|
||||
let msgs = [ ...this.state.messages ];
|
||||
let par = res.body.messages;
|
||||
par.unshift(par.length);
|
||||
par.unshift(start);
|
||||
msgs.splice.apply(msgs, par);
|
||||
this.setState({ messages: msgs });
|
||||
});
|
||||
}
|
||||
|
||||
loadMessage = (msgId, callback) =>
|
||||
{
|
||||
superagent.get('backend/message').query({ msgId: msgId }).end((err, res) =>
|
||||
{
|
||||
callback(res.body.msg);
|
||||
});
|
||||
}
|
||||
|
||||
startResync = () =>
|
||||
{
|
||||
superagent.post('backend/sync').send().end((err, res) =>
|
||||
{
|
||||
});
|
||||
}
|
||||
|
||||
setLayout = (l) =>
|
||||
{
|
||||
this.setState({ layout: l });
|
||||
}
|
||||
|
||||
toggleQuickReply = () =>
|
||||
{
|
||||
this.setState({ quickReply: !this.state.quickReply });
|
||||
}
|
||||
|
||||
setMessage = (msg) =>
|
||||
{
|
||||
this.setState({ msg });
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
return (<div>
|
||||
<DropDownMenu
|
||||
id="account"
|
||||
items={[ {
|
||||
icon: 'mail_unread',
|
||||
i16: true,
|
||||
text: 'Read vitalif@mail.ru'
|
||||
}, {
|
||||
icon: 'folder',
|
||||
text: 'IMAP Folders',
|
||||
}, {
|
||||
icon: 'properties',
|
||||
text: 'Properties...'
|
||||
} ]}
|
||||
/>
|
||||
<DropDownMenu
|
||||
id="reply"
|
||||
items={[ {
|
||||
hotkey: 'R',
|
||||
icon: 'mail_reply',
|
||||
text: 'Reply'
|
||||
}, {
|
||||
icon: 'mail_reply',
|
||||
text: 'Reply to Sender',
|
||||
}, {
|
||||
disabled: true,
|
||||
icon: 'mail_reply_all',
|
||||
text: 'Reply to List'
|
||||
} ]}
|
||||
/>
|
||||
<DropDownMenu
|
||||
id="forward"
|
||||
items={[ {
|
||||
hotkey: 'F',
|
||||
icon: 'mail_forward',
|
||||
text: 'Reply'
|
||||
}, {
|
||||
hotkey: 'D',
|
||||
text: 'Redirect'
|
||||
} ]}
|
||||
/>
|
||||
<DropDownMenu
|
||||
id="delete"
|
||||
items={[ {
|
||||
text: 'Move to Trash'
|
||||
}, {
|
||||
icon: 'delete',
|
||||
text: 'Delete Permanently'
|
||||
} ]}
|
||||
/>
|
||||
<DropDownMenu
|
||||
id="check-send"
|
||||
items={[ {
|
||||
hotkey: 'Ctrl-K',
|
||||
icon: 'mail_check',
|
||||
text: 'Check All'
|
||||
}, {
|
||||
hotkey: 'Ctrl-Shift-K',
|
||||
icon: 'mail_send',
|
||||
text: 'Send Queued'
|
||||
}, { split: true }, {
|
||||
icon: 'mail_check',
|
||||
text: 'vitalif@mail.ru'
|
||||
}, {
|
||||
icon: 'mail_check',
|
||||
text: 'vitalif@yourcmc.ru'
|
||||
}, { split: true }, {
|
||||
text: 'Resynchronize All Messages'
|
||||
} ]}
|
||||
/>
|
||||
<DropDownMenu
|
||||
id="threads"
|
||||
items={[ {
|
||||
icon: 'thread',
|
||||
text: 'Show Message Thread'
|
||||
}, {
|
||||
text: 'Follow Thread'
|
||||
}, {
|
||||
text: 'Ignore Thread'
|
||||
}, { split: true }, {
|
||||
hotkey: 'M',
|
||||
icon: 'read',
|
||||
text: 'Mark Thread as Read'
|
||||
}, { split: true }, {
|
||||
hotkey: 'N',
|
||||
text: 'Mark Thread and Go to Next Unread'
|
||||
} ]}
|
||||
/>
|
||||
<ListSortSettingsWindow
|
||||
id="list-sort"
|
||||
window={true}
|
||||
folder="INBOX"
|
||||
override={false}
|
||||
markDelay={-1}
|
||||
sorting={{}}
|
||||
defaultSorting={{
|
||||
sort: {
|
||||
sortby: 'sent date',
|
||||
group: 'date',
|
||||
ascending: false,
|
||||
threaded: false
|
||||
}
|
||||
}}
|
||||
show={{
|
||||
read: true,
|
||||
trash: false,
|
||||
spam: false,
|
||||
lists: true,
|
||||
sent: true,
|
||||
dups: true
|
||||
}}
|
||||
/>
|
||||
<MailSettingsWindow
|
||||
defaultSorting={{
|
||||
sort: {
|
||||
sortby: 'sent date',
|
||||
group: 'date',
|
||||
ascending: false,
|
||||
threaded: false
|
||||
}
|
||||
}}
|
||||
layout={this.state.layout}
|
||||
quickReply={this.state.quickReply}
|
||||
setLayout={this.setLayout}
|
||||
toggleQuickReply={this.toggleQuickReply}
|
||||
/>
|
||||
<FolderList
|
||||
accounts={this.state.accounts}
|
||||
progressText={this.state.progressText}
|
||||
startResync={this.startResync}
|
||||
loadFolder={this.loadFolder}
|
||||
/>
|
||||
<TabPanel
|
||||
tabs={[
|
||||
{
|
||||
className: this.state.layout,
|
||||
noclose: true,
|
||||
icon: 'mail_unread',
|
||||
title: 'Unread (64)',
|
||||
children: [
|
||||
<MessageList
|
||||
key="1"
|
||||
loadMessages={this.loadMessages}
|
||||
loadMessage={this.loadMessage}
|
||||
setMessage={this.setMessage}
|
||||
search={this.search}
|
||||
threads={this.state.threads}
|
||||
layout={this.state.layout}
|
||||
groups={this.state.listGroups}
|
||||
messages={this.state.messages}
|
||||
/>,
|
||||
<MessageView
|
||||
key="2"
|
||||
layout={this.state.layout}
|
||||
quickReply={this.state.quickReply}
|
||||
msg={this.state.msg}
|
||||
/>,
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: 'mail_drafts',
|
||||
i16: true,
|
||||
title: 'Compose Message',
|
||||
children: [
|
||||
<ComposeWindow
|
||||
key="1"
|
||||
accounts={this.state.accounts}
|
||||
/>
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this.loadAccounts();
|
||||
this.startIo();
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<MainWindow />, document.getElementById('app'));
|
||||
|
|
Loading…
Reference in New Issue