Remove Redux-like superglobal Store, remove all var's and self's

master
Vitaliy Filippov 2019-05-14 15:49:20 +03:00
parent 5433f05542
commit c2b841d757
19 changed files with 520 additions and 639 deletions

View File

@ -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);
}
}

View File

@ -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" />,
];
}

View File

@ -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 ];
}

View File

@ -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 }; });

View File

@ -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);

View File

@ -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);

View File

@ -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 }; });

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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 };
});

View File

@ -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
}
}
}
);

View File

@ -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
};
});

View File

@ -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
View File

@ -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;

View File

@ -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;
};

View File

@ -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
View File

@ -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
View File

@ -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'));