likeopera-frontend/mail.js

1485 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame;
var WeekDays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ];
var Months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
function getOffset(elem)
{
if (elem.getBoundingClientRect)
{
var box = elem.getBoundingClientRect();
var body = document.body;
var docElem = document.documentElement;
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
var clientTop = docElem.clientTop || body.clientTop || 0;
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}
else
{
var top = 0, left = 0;
while(elem)
{
top = top + parseInt(elem.offsetTop);
left = left + parseInt(elem.offsetLeft);
elem = elem.offsetParent;
}
return { top: top, left: left };
}
}
var Store = {
layout: 'message-on-right',
quickReply: true,
msg: null,
threads: false,
listeners: {},
on: function(ev, cb)
{
this.listeners[ev] = this.listeners[ev] || [];
this.listeners[ev].push(cb);
},
un: function(ev, cb)
{
if (!this.listeners[ev])
return;
for (var i = this.listeners[ev].length; i >= 0; i--)
if (this.listeners[ev] == cb)
this.listeners[ev].splice(i, 1);
},
set: function(k, v)
{
this[k] = v;
(this.listeners[k] || []).map(i => i());
}
};
var DropDownBase = {
instances: {},
currentVisible: null,
componentDidMount: function()
{
if (!DropDownBase.setBodyListener)
{
window.addEventListener('click', DropDownBase.hideAll);
window.addEventListener('resize', DropDownBase.repositionCurrent);
DropDownBase.setBodyListener = true;
}
DropDownBase.instances[this.props.id] = this;
},
hideAll: function()
{
for (var i in DropDownBase.instances)
DropDownBase.instances[i].hide();
},
repositionCurrent: function()
{
if (DropDownBase.currentVisible)
DropDownBase.currentVisible[0].showAt(DropDownBase.currentVisible[1], DropDownBase.currentVisible[0].onClose);
},
componentWillUnmount: function()
{
delete DropDownBase.instances[this.props.id];
if (DropDownBase.currentVisible[0] == this)
DropDownBase.currentVisible = null;
},
getInitialState: function()
{
return { visible: false, top: 0, left: 0, calloutLeft: null, selectedItem: -1 };
},
onClick: function(ev)
{
ev.stopPropagation();
},
hide: function()
{
this.setState({ visible: false });
DropDownBase.currentVisible = null;
if (this.onClose)
{
this.onClose();
delete this.onClose;
}
},
showAt: function(el, onClose)
{
if (this.onClose && this.onClose != onClose)
{
this.onClose();
delete this.onClose;
}
DropDownBase.currentVisible = [ this, el ];
var p = getOffset(el);
var 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)
{
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;
if (left + this.refs.dd.offsetWidth > ww)
{
left = ww-this.refs.dd.offsetWidth;
calloutLeft = Math.round(p.left+el.offsetWidth/2-left);
}
if (top + this.refs.dd.offsetHeight > wh)
top = wh-this.refs.dd.offsetHeight;
this.refs.dd.style.display = '';
this.setState({ visible: true, top: top, left: left, calloutLeft: calloutLeft });
this.refs.dd.focus();
this.onClose = onClose;
}
};
var DropDownMenu = React.createClass({
mixins: [ DropDownBase ],
getInitialState: function()
{
return { items: this.props.items };
},
render: function()
{
var 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}>
{this.state.items.map(function(i, index) {
return (i.split
? <div key={index} className="split"><i></i></div> :
<div key={index} data-index={index} className={'item'+(i.i16 ? ' i16' : '')+(i.disabled ? ' disabled' : (sel == index ? ' over' : ''))}>
{i.hotkey ? <div className="hotkey">{i.hotkey}</div> : null}
{i.icon ? <img src={'icons/'+i.icon+'.png'} /> : null}
<span>{i.text}</span>
</div>
);
})}
</div>
},
onMouseOver: function(ev)
{
var t = ev.target;
while ((t && t != this.refs.dd) && (!t.className || t.className.substr(0, 4) != 'item'))
t = t.parentNode;
if (t && t != this.refs.dd)
this.setState({ selectedItem: parseInt(t.getAttribute('data-index')) });
},
onKeyDown: function(ev)
{
if (ev.keyCode == 40 || ev.keyCode == 38)
{
var a = ev.keyCode == 40 ? 1 : this.state.items.length-1;
var sel = this.state.selectedItem;
do
{
sel = ((sel+a) % this.state.items.length);
}
while (this.state.items[sel].split && sel != this.state.selectedItem);
this.setState({ selectedItem: sel });
}
else if (ev.keyCode == 10 || ev.keyCode == 13)
this.clickItem();
ev.preventDefault();
ev.stopPropagation();
},
clickItem: function(ev)
{
},
myOnClick: function(ev)
{
if (ev.target.getAttribute('data-index'))
this.clickItem();
this.onClick(ev);
}
});
var DropDownButton = React.createClass({
render: function()
{
return <a ref="btn" title={(this.state.checked ? this.props.checkedTitle : null) || this.props.title} onClick={this.onClickButton}
className={'button '+(this.props.dropdownId ? 'show-dropdown ' : '')+(this.state.checked ? 'checked ' : '')+
(this.state.pressed ? 'pressed ' : '')+(this.props.className || '')}>
{this.props.icon ? <img src={'icons/'+(this.state.checked && this.props.checkedIcon || this.props.icon)+'.png'} /> : null}
{this.state.checked && this.props.checkedText || this.props.text || null}
{this.props.dropdownId ? <span className="down" onClick={this.onClickDown}></span> : null}
</a>
},
getInitialState: function()
{
return { pressed: false, checked: false };
},
toggle: function()
{
if (!this.state.pressed)
{
DropDownBase.hideAll();
DropDownBase.instances[this.props.dropdownId].showAt(this.refs.btn, this.unpress);
}
else
DropDownBase.instances[this.props.dropdownId].hide();
this.setState({ pressed: !this.state.pressed });
},
unpress: function()
{
this.setState({ pressed: false });
},
onClickButton: function(ev)
{
if (this.props.whole || this.props.checkable && this.state.pressed)
this.toggle();
if (this.props.checkable)
this.setState({ checked: !this.state.checked });
ev.stopPropagation();
},
onClickDown: function(ev)
{
this.toggle();
ev.stopPropagation();
}
});
var dropdown_account = React.createElement(
DropDownMenu, {
id: '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',
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',
items: [ {
hotkey: 'F',
icon: 'mail_forward',
text: 'Reply'
}, {
hotkey: 'D',
text: 'Redirect'
} ]
}
);
var dropdown_delete = React.createElement(
DropDownMenu, {
id: 'delete',
items: [ {
text: 'Move to Trash'
}, {
icon: 'delete',
text: 'Delete Permanently'
} ]
}
);
var dropdown_check_send = React.createElement(
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'
} ]
}
);
var dropdown_threads = React.createElement(
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'
} ]
}
);
var ListSortSettings = React.createClass({
render: function()
{
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>
})}
</select>
<select className="group" value={this.props.sort.group}>
<option value="">Do not group</option>
<option value="read">Group by read status</option>
<option value="pinned">Group by pinned status</option>
<option value="date">Group by date</option>
</select>
<label><input type="checkbox" checked={this.props.sort.ascending} /> Sort ascending</label>
<label><input type="checkbox" checked={this.props.sort.threaded} /> Threaded</label>
</div>
}
});
var ListSortSettingsWindow = React.createClass({
mixins: [ DropDownBase ],
render: function()
{
var sort = this.props.override ? this.props.sorting : this.props.defaultSorting;
return <div ref="dd" onClick={this.onClick} className={'dropdown window list-sort'+(this.state.visible ? ' visible' : '')}
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>
<div className="title">Sorting for {this.props.folder}</div>
<label><input type="checkbox" checked={this.props.override ? "checked" : null} /> Override default sorting</label>
<ListSortSettings className="sorting" sort={sort} />
<div className="show">
<a className="button" onClick={this.expandChecks}><span className={this.state.checksVisible ? 'collapse' : 'expand'}></span> Show</a>
</div>
<div className="show-checks" style={{ display: this.state.checksVisible ? null : 'none' }}>
<label><input type="checkbox" checked={this.props.show.read ? "checked" : null} /> Show Read</label>
<label><input type="checkbox" checked={this.props.show.trash ? "checked" : null} /> Show Trash</label>
<label><input type="checkbox" checked={this.props.show.spam ? "checked" : null} /> Show Spam</label>
<label><input type="checkbox" checked={this.props.show.lists ? "checked" : null} /> Show Mailing Lists</label>
<label><input type="checkbox" checked={this.props.show.sent ? "checked" : null} /> Show Sent</label>
<label><input type="checkbox" checked={this.props.show.dups ? "checked" : null} /> Show Duplicates</label>
</div>
</div>
},
getInitialState: function()
{
return { checksVisible: false };
},
expandChecks: function()
{
this.setState({ checksVisible: !this.state.checksVisible });
}
});
var MailSettingsWindow = React.createClass({
mixins: [ DropDownBase ],
render: function()
{
return <div ref="dd" onClick={this.onClick} className={'dropdown window'+(this.state.visible ? ' visible' : '')}
id={'dropdown-'+this.props.id} tabIndex="1" style={{ top: this.state.top, left: this.state.left }}>
<div ref="callout" className="callout-top" style={{ left: this.state.calloutLeft }}></div>
<div className="text">Mail Layout</div>
<div className="layouts">
<a onClick={this.switchLayout} className={'button mail-message-on-right'+(this.state.layout == 'message-on-right' ? ' selected' : '')} title="List and Message on Right"><span></span></a>
<a onClick={this.switchLayout} className={'button mail-message-on-bottom'+(this.state.layout == 'message-on-bottom' ? ' selected' : '')} title="List and Message Below"><span></span></a>
<a onClick={this.switchLayout} className={'button mail-message-invisible'+(this.state.layout == 'message-invisible' ? ' selected' : '')} title="List Only"><span></span></a>
</div>
<div className="split"><i></i></div>
<div className="text">Default List Sorting</div>
<ListSortSettings className="fields" sort={this.props.defaultSorting} />
<div className="fields">
<label><input type="checkbox" checked={this.state.showQuickReply} onClick={this.showQuickReply} /> Show Quick Reply</label>
</div>
<div className="split"><i></i></div>
<div className="text">Mark as Read</div>
<div className="fields">
<select className="sortby" defaultValue={this.props.markDelay}>
<option value="-1">Manually</option>
<option value="0">On Select</option>
<option value="1">After 1 second</option>
<option value="3">After 3 seconds</option>
<option value="5">After 5 seconds</option>
</select>
</div>
</div>
},
componentDidMount: function()
{
Store.on('layout', this.setLayout);
Store.on('quickReply', this.setQuickReply);
},
componentWillUnmount: function()
{
Store.un('layout', this.setLayout);
Store.un('quickReply', this.setQuickReply);
},
setLayout: function()
{
this.setState({ layout: Store.layout });
},
setQuickReply: function()
{
this.setState({ showQuickReply: Store.quickReply });
},
switchLayout: function(ev)
{
var t = ev.target.nodeName == 'A' ? ev.target : ev.target.parentNode;
var l = / mail-(\S+)/.exec(t.className)[1];
Store.set('layout', l);
},
getInitialState: function()
{
return { showQuickReply: Store.quickReply, layout: Store.layout };
},
showQuickReply: function()
{
Store.set('quickReply', !this.state.showQuickReply);
}
});
var dropdown_list_sort = React.createElement(
ListSortSettingsWindow, {
id: '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
}
}
);
var dropdown_settings = React.createElement(
MailSettingsWindow, {
id: 'settings',
window: true,
markDelay: -1,
defaultSorting: {
sort: {
sortby: 'sent date',
group: 'date',
ascending: false,
threaded: false
}
}
}
);
var AccountFolders = React.createClass({
render: function()
{
return <div className="account">
<div className={"account-header"+(this.state.collapsed ? ' collapsed' : '')} onClick={this.onClick}>
{this.props.name}
<div key="n" className="msg-count">{this.props.unreadCount}</div>
{this.props.accountId ? [
<div key="load" className="loading icon" style={{display: this.props.loading ? '' : 'none'}}></div>,
<div key="warn" className="warning icon" style={{display: this.props.warning ? '' : 'none'}}></div>,
<div key="cfg" className={'cfg'+(this.state.cfgPressed ? ' pressed' : '')} onClick={this.showCfg}></div>
] : null}
</div>
<div className={"account-folders"+(this.state.collapsed ? ' collapsed' : '')} style={{ height: this.state.h }}>
<div ref="vis" className={'visible-part'+(this.state.animating ? ' animating' : '')}>
{this.props.folders.map((f, i) =>
<div key={'f'+i} data-i={i} className={'folder'+(f.unreadCount > 0 ? ' with-unread' : '')+
(this.state.selected == i ? ' selected' : '')} onClick={this.selectFolder}>
{f.icon ? <img src={'icons/'+f.icon+'.png'} /> : null}
{' '}
<span>{f.name}</span>
{f.unreadCount > 0 ? <div className="msg-count">{f.unreadCount}</div> : null}
</div>
)}
</div>
</div>
</div>
},
selectFolder: function(ev)
{
var 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');
this.setState({ selected: i });
this.props.onSelect(this, i);
}
// FIXME: send select event + switch focus to message list if folder changed
},
showCfg: function(ev)
{
var self = this;
var i = DropDownBase.instances.account.state.items;
i[0].text = 'Read '+this.props.name;
DropDownBase.instances.account.setState({ items: i });
DropDownBase.instances.account.showAt(ev.target, function()
{
self.setState({ cfgPressed: false });
});
self.setState({ cfgPressed: true });
ev.stopPropagation();
},
getInitialState: function()
{
return { collapsed: this.props.collapsed, animating: false, h: null, cfgPressed: false, selected: -1 };
},
onClick: function()
{
var self = this;
if (this.state.animating)
return;
this.setState({ animating: true, h: this.refs.vis.offsetHeight });
if (!this.state.collapsed)
{
setTimeout(function()
{
self.setState({ h: 0 });
}, 50);
}
setTimeout(function()
{
self.setState({ collapsed: !self.state.collapsed, animating: false, h: null });
}, this.state.collapsed ? 200 : 250);
}
});
var FolderList = React.createClass({
render: function()
{
var self = this;
return <div className={"folder-list"+(this.props.progress !== undefined ? ' 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" />
</div>
// TODO: keyboard navigation
<div className="listview" tabIndex="1">
{this.props.accounts.map(function(account) {
return <AccountFolders key={'a'+account.accountId} onSelect={self.onSelectFolder} {...account} />
})}
</div>
<div className="progress-bar" ref="pbar">
<div className="pending" style={{ width: this.state.pbarWidth }}>Loading database ({this.props.progress||0}%)</div>
<div className="clip" style={{ width: (this.props.progress||0)+'%', overflow: 'hidden' }}>
<div className="done" ref="pdone" style={{ width: this.state.pbarWidth }}>Loading database ({this.props.progress||0}%)</div>
</div>
</div>
</div>
},
onSelectFolder: function(folders, i)
{
if (this.prevSelected && this.prevSelected != folders)
this.prevSelected.setState({ selected: -1 });
this.prevSelected = folders;
},
getInitialState: function()
{
return { pbarWidth: '' };
},
onResize: function()
{
this.setState({ pbarWidth: this.refs.pbar.offsetWidth });
},
componentDidMount: function()
{
window.addEventListener('resize', this.onResize);
this.onResize();
},
componentWillUnmount: function()
{
window.removeEventListener('resize', this.onResize);
}
});
var TabPanel = React.createClass({
render: function()
{
var bar = [];
var body = [];
for (var i = 0; i < this.state.tabs.length; i++)
{
var 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}>
{t.noclose ? null : <a className="close" onClick={this.closeTab}></a>}
<img src={'icons/'+t.icon+'.png'} className={t.i16 ? 'i16' : null} /> {t.title}
</div>
);
body.push(
<div key={'c'+i} className={'tab-content'+(t.className ? ' '+t.className : '')}
id={'tab'+i} style={{ display: i == this.state.selected ? null : 'none' }}>
{t.children}
</div>
);
}
return <div className="tab-panel">
<div className="tab-bar">{bar}</div>
{body}
</div>
},
componentWillReceiveProps: function(nextProps, nextContent)
{
// FIXME: Do not own tabs?
this.setState({ selected: this.state.selected % nextProps.tabs.length, tabs: nextProps.tabs });
},
getInitialState: function()
{
return { selected: 0, tabs: this.props.tabs };
},
switchTab: function(ev)
{
this.setState({ selected: ev.target.id.substr(5) });
},
closeTab: function(ev)
{
var self = this;
var 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()
{
var t = self.state.tabs;
t.splice(tab.id.substr(5), 1);
var s = self.state.selected;
if ('t-tab'+s == tab.id)
s = self.state.selected-1;
this.setState({ tabs: t, selected: s });
}, 200);
ev.stopPropagation();
}
});
function formatBytes(s)
{
if (!s) return '';
if (s < 1024) return s+' B';
else if (s < 1024*1024) return (Math.round(s*10/1024)/10)+' KB';
else if (s < 1024*1024*1024) return (Math.round(s*10/1024/1024)/10)+' MB';
return (Math.round(s*10/1024/1024/1024)/10)+' GB';
}
function formatDate(dt)
{
if (!(dt instanceof Date))
dt = new Date(dt.replace(' ', 'T'));
var tod = new Date();
tod.setHours(0);
tod.setMinutes(0);
tod.setSeconds(0);
tod.setMilliseconds(0);
var prevweek = tod;
prevweek = prevweek.getTime() - (7 + (prevweek.getDay()+6)%7)*86400000;
if (dt.getTime() < prevweek)
{
var d = dt.getDate();
var m = dt.getMonth()+1;
return (d < 10 ? '0' : '')+d+' '+(m < 10 ? '0' : '')+m+' '.dt.getFullYear();
}
else if (dt.getTime() < tod.getTime())
{
return WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Months[dt.getMonth()];
}
var h = dt.getHours();
var m = dt.getMinutes();
return (h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m;
}
// Common selection mixin
var ListWithSelection = {
// requires to override methods: this.deleteSelected(), this.getPageSize(), this.getItemOffset(index), this.getTotalItems()
getInitialState: function()
{
return {
selected: {}
};
},
isSelected: function(i)
{
return this.state.selected[i] || this.state.selected.begin !== undefined &&
this.state.selected.begin <= i && this.state.selected.end >= i;
},
onListKeyDown: function(ev)
{
if (!this.getTotalItems())
return;
if (ev.keyCode == 46) // delete
{
this.deleteSelected();
this.setState({ selected: {} });
ev.stopPropagation();
}
else if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 33 || ev.keyCode == 34) // up, down, pgup, pgdown
{
var 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();
if (nsel < 0)
nsel = 0;
if (nsel >= n)
nsel = n-1;
if (sel != nsel)
{
if (ev.shiftKey)
this.selectTo(nsel);
else
this.selectOne(nsel);
var 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())
this.refs.scroll.scrollTop = pos[0] - this.getScrollPaddingTop();
ev.stopPropagation();
}
ev.preventDefault(); // prevent scroll
}
}
else if (ev.keyCode == 36) // home
{
if (ev.shiftKey)
{
this.selectTo(0);
this.refs.scroll.scrollTop = pos[0] - this.getScrollPaddingTop();
}
else
this.selectOne(0);
}
else if (ev.keyCode == 35) // end
{
var nsel = this.getTotalItems()-1;
if (ev.shiftKey)
{
this.selectTo(nsel);
var pos = this.getItemOffset(nsel);
this.refs.scroll.scrollTop = pos[0] + pos[1] - this.refs.scroll.offsetHeight;
}
else
this.selectOne(nsel);
}
},
selectTo: function(ns)
{
if (this.lastSel === undefined)
return this.selectOne(ns);
var sel = {};
var n = this.getTotalItems();
if (this.lastSel >= n)
this.lastSel = n-1;
if (ns < this.lastSel)
sel = { begin: ns, end: this.lastSel };
else if (ns > this.lastSel)
sel = { begin: this.lastSel, end: ns };
else
sel[ns] = true;
this.setState({ selected: sel });
this.curSel = ns;
if (this.onSelectCurrent)
this.onSelectCurrent(ns);
},
selectOne: function(ns)
{
var sel = {};
sel[ns] = true;
this.setState({ selected: sel });
this.lastSel = ns;
this.curSel = ns;
if (this.onSelectCurrent)
this.onSelectCurrent(ns);
},
onListItemClick: function(ev)
{
var t = ev.target;
while (t && !t.getAttribute('data-i'))
t = t.parentNode;
if (t)
{
var ns = parseInt(t.getAttribute('data-i'));
if (ev.shiftKey)
this.selectTo(ns);
else if (ev.ctrlKey)
{
this.state.selected[ns] = true;
this.curSel = ns;
if (this.onSelectCurrent)
this.onSelectCurrent(ns);
this.lastSel = this.lastSel === undefined ? ns : this.lastSel;
this.setState({ selected: this.state.selected });
}
else
this.selectOne(ns);
}
}
};
var AttachList = React.createClass({
mixins: [ ListWithSelection ],
getInitialState: function()
{
return {
attachments: [],
attachScroll: 0
};
},
addAttachments: function(ev)
{
var a = this.state.attachments;
if (ev.target.files)
for (var i = 0; i < ev.target.files.length; i++)
a.push(ev.target.files[i]);
this.setState({ attachments: a });
// reset file input
ev.target.innerHTML = ev.target.innerHTML;
},
scrollAttachList: function(ev)
{
this.setState({ attachScroll: ev.target.scrollTop });
},
deleteSelected: function()
{
for (var i = this.state.attachments.length-1; i >= 0; i--)
if (this.state.selected[i])
this.state.attachments.splice(i, 1);
this.setState({ attachments: this.state.attachments });
},
getTotalItems: function()
{
return this.state.attachments.length;
},
getPageSize: function()
{
return this.refs.a0 ? Math.round(this.refs.scroll.offsetHeight / this.refs.a0.offsetHeight) : 1;
},
getItemOffset: function(index)
{
var item = this.refs['a'+index];
return [ item.offsetTop, item.offsetHeight ];
},
getScrollPaddingTop: function()
{
return this.refs.title.offsetHeight;
},
render: function()
{
return <div className="attach">
<div className="no-attach" style={this.state.attachments.length ? { display: 'none' } : null}>
<input type="file" multiple="multiple" onChange={this.addAttachments} />
</div>
<div ref="scroll" className="attach-list" tabIndex="1" onScroll={this.scrollAttachList} onKeyDown={this.onListKeyDown}
style={this.state.attachments.length ? null : { display: 'none' }}>
<div ref="title" className="title" style={{ top: this.state.attachScroll }}>
<div className="name">Attachment <a className="button">
<img src="icons/attach.png" />
<input type="file" multiple="multiple" onChange={this.addAttachments} />
</a></div>
<div className="size">Size</div>
</div>
{this.state.attachments.map((a, i) =>
<div ref={'a'+i} title={a.name+' ('+formatBytes(a.size)+')'} key={'a'+i} data-i={i}
className={'attachment'+(this.isSelected(i) ? ' selected' : '')} onMouseDown={this.onListItemClick}>
<div className="name">{a.name}</div>
<div className="size">{formatBytes(a.size)}</div>
</div>
)}
</div>
</div>
}
});
var ComposeWindow = React.createClass({
getInitialState: function()
{
return {
text: ''
};
},
changeText: function(ev)
{
this.setState({ text: ev.target.value });
},
render: function()
{
return <div className="compose">
<div className="actions">
<a className="button"><img src="icons/mail_send.png" />Send</a>
<a className="button"><img src="icons/delete.png" /></a>
</div>
<div className="flex">
<div className="headers">
<div className="headers-table">
<div className="header">
<div className="field">From</div>
<div className="value">
<select>
{this.props.accounts.map((a, i) =>
<option key={'a'+i}>{'"'+a.name+'" <'+a.email+'>'}</option>
)}
</select>
</div>
</div>
<div className="header">
<div className="field">To</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Cc</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Bcc</div>
<div className="value"><input /></div>
</div>
<div className="header">
<div className="field">Subject</div>
<div className="value"><input /></div>
</div>
</div>
<AttachList />
</div>
<div className="text">
<textarea onChange={this.changeText} defaultValue={this.state.text}></textarea>
</div>
</div>
</div>
}
});
var MessageInList = React.createClass({
msgClasses: [ 'unread', 'unseen', 'replied', 'pinned', 'sent', 'unloaded' ],
render: function()
{
var msg = this.props.msg;
return <div data-i={this.props.i} className={'message'+(this.msgClasses.map(c => (msg[c] ? ' '+c : '')).join(''))+
(this.props.selected ? ' selected' : '')+(msg.thread && Store.threads ? ' thread0' : '')} onMouseDown={this.props.onClick}>
<div className="icon" style={{ width: (20+10*(msg.level||0)), backgroundPosition: (10*(msg.level||0))+'px 7px' }}></div>
<div className="subject" style={{ paddingLeft: (20+10*(msg.level||0)) }}>{msg.subject}</div>
{msg.thread && Store.threads ? <div className={'expand'+(msg.collapsed ? '' : ' collapse')}></div> : null}
<div className="bullet"></div>
<div className="from" style={{ left: (21+10*(msg.level||0)) }}>{(msg.sent || msg.outgoing ? 'To '+msg.to : msg.from)}</div>
<div className="size">{formatBytes(msg.size)}</div>
<div className="attach" style={msg.attach ? null : { display: 'none' }}></div>
<div className="time">{formatDate(msg.time)}</div>
</div>
}
});
// TODO: expand/collapse days, virtual scroll
var MessageList = React.createClass({
mixins: [ ListWithSelection ],
getInitialState: function()
{
return { firstDayTop: 0, firstDay: this.props.groups[0].name, groups: this.props.groups /*FIXME*/ };
},
changeFirstDay: function(ev)
{
var scrollTop = ev.target.scrollTop, scrollSize = ev.target.offsetHeight - this.getScrollPaddingTop();
var top = 0, p, firstVisibleGrp, firstVisible, lastVisibleGrp, lastVisible;
var itemH = (Store.layout == 'message-on-right' ? 60 : 30);
var i;
for (i = 0; i < this.state.groups.length; i++)
{
p = top;
top += (i > 0 ? 30 : 0) + itemH*this.state.groups[i].messageCount;
if (firstVisibleGrp === undefined && scrollTop < top)
{
firstVisibleGrp = i;
if (i > 0 && scrollTop < p+30)
firstVisible = 0;
else
firstVisible = Math.floor((scrollTop - p - (i > 0 ? 30 : 0))/itemH);
}
if (lastVisibleGrp === undefined && scrollTop+scrollSize < top)
{
lastVisibleGrp = i;
if (i > 0 && scrollTop+scrollSize < p+30)
lastVisible = 0;
else
lastVisible = Math.floor((scrollTop+scrollSize - p - (i > 0 ? 30 : 0))/itemH);
if (lastVisible >= this.state.groups[i].messageCount)
lastVisible = this.state.groups[i].messageCount-1;
}
if (firstVisibleGrp !== undefined && lastVisibleGrp !== undefined)
break;
}
if (lastVisibleGrp === undefined)
{
lastVisibleGrp = this.state.groups.length-1;
lastVisible = this.state.groups[lastVisibleGrp].messageCount-1;
}
this.setState({
firstDayTop: scrollTop,
firstDay: this.state.groups[firstVisibleGrp].name,
firstGrp: firstVisibleGrp,
firstMsg: firstVisible,
lastGrp: lastVisibleGrp,
lastMsg: lastVisible
});
},
deleteSelected: function()
{
},
onSelectCurrent: function(index)
{
var total = 0, p;
for (var i = 0; i < this.state.groups.length; i++)
{
p = total;
total += (i > 0 ? 1 : 0)+this.state.groups[i].messageCount;
if (index < total)
{
Store.set('msg', this.state.groups[i].messages[index-p-(i > 0 ? 1 : 0)]);
break;
}
}
},
getTotalItems: function()
{
var total = -1; // do not count first-day as item
for (var i = 0; i < this.state.groups.length; i++)
{
total += 1+this.state.groups[i].messageCount;
}
return total;
},
getPageSize: function()
{
return Math.round(this.refs.scroll.offsetHeight / (Store.layout == 'message-on-right' ? 60 : 30));
},
getItemOffset: function(index)
{
/*var c, gmin = 0, gmax = this.state.groups.length;
while (gmin != gmax-1)
{
c = Math.floor((gmax+gmin)/2);
if (this.state.groups[c]
}*/
var n = 0, top = 0, p;
for (var i = 0; i < this.state.groups.length; i++)
{
p = n;
n += (i > 0 ? 1 : 0)+this.state.groups[i].messageCount;
if (index < n)
{
if (index > p)
top += (i > 0 ? 30 : 0) + (Store.layout == 'message-on-right' ? 60 : 30)*(index-p-1);
break;
}
top += (i > 0 ? 30 : 0) + (Store.layout == 'message-on-right' ? 60 : 30)*this.state.groups[i].messageCount;
}
return [ top, (Store.layout == 'message-on-right' && (index == 0 || index != p) ? 60 : 30) ];
},
getScrollPaddingTop: function()
{
return this.refs.title.offsetHeight;
},
getMessages: function(grp, start, end)
{
var a = grp.messages.slice(start, end);
for (var i = 0; i < end-start; i++)
if (!a[i])
a[i] = null;
return a;
},
render: function()
{
var self = this;
var total = 0;
var itemH = (Store.layout == 'message-on-right' ? 60 : 30);
return <div className="message-list">
<div className="top-border-gradient"></div>
<div className="bottom-border-gradient"></div>
<div className="actions">
<div className="searchbox">
<input type="text" placeholder="Search mail" />
</div>
<DropDownButton dropdownId="threads" className="threads"
title="Show Message Thread" checkedTitle="Hide Message Thread" icon="thread" checkedIcon="thread_selected" checkable="1" />
<DropDownButton ref="setBtn" dropdownId="settings" className="settings" whole="1"
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={this.changeFirstDay} onKeyDown={this.onListKeyDown}>
<div ref="title" className="day first-day" style={{ top: this.state.firstDayTop }}>{this.state.firstDay}</div>
{this.props.groups.map(function(grp, i) {
if (i > 0)
total++;
var start = total+(self.state.firstGrp == i ? self.state.firstMsg : 0);
var r = [
i > 0 ? <div className="day" data-i={total-1}>{grp.name}</div> : null,
<div className="day-list">
{(self.state.firstGrp > i || self.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>
: null),
self.getMessages(grp,
self.state.firstGrp == i ? self.state.firstMsg : 0,
self.state.lastGrp == i ? self.state.lastMsg+1 : grp.messageCount
).map((msg, j) => (msg
? [
<MessageInList msg={msg} i={start+j} selected={self.isSelected(start+j)} onClick={self.onListItemClick} />,
/*(msg.thread && Store.threads ?
<div className="thread">
{msg.thread.map(reply => <MessageInList msg={reply} i={0} onClick={self.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>
: null)
]
)}
</div>
];
total += grp.messageCount;
return r;
})}
</div>
</div>
}
});
var MessageView = React.createClass({
formatLongDate: function(dt)
{
if (!(dt instanceof Date))
dt = new Date(dt.replace(' ', 'T'));
var h = dt.getHours();
var m = dt.getMinutes();
var s = dt.getSeconds();
return WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Months[dt.getMonth()]+' '+dt.getFullYear()+' '+(h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m+':'+(s < 10 ? '0' : '')+s
//return dt.toLocaleString();
},
componentDidMount: function()
{
Store.on('quickReply', this.setQuickReply);
Store.on('msg', this.setMsg);
},
componentWillUnmount: function()
{
Store.un('quickReply', this.setQuickReply);
Store.un('msg', this.setMsg);
},
setQuickReply: function()
{
this.setState({ quickReply: Store.quickReply });
},
setMsg: function()
{
this.setState({ msg: Store.msg });
},
getInitialState: function()
{
return { quickReply: Store.quickReply };
},
render: function()
{
var msg = this.state.msg;
return <div className={'message-view'+(!msg ? ' no-mail-shown' : '')+(!this.state.quickReply ? ' no-quick' : '')}>
<div className="actions">
<DropDownButton dropdownId="reply" icon="mail_reply" text="Reply" />
<a className="button"><img src="icons/mail_reply_all.png" />Reply All</a>
<DropDownButton dropdownId="forward" icon="mail_forward" text="Forward" />
<DropDownButton className="action-read" icon="read" checkedIcon="read_selected" checkable="1" text="Read" checkedText="Unread" />
<DropDownButton className="action-spam" icon="spam" checkedIcon="spam_selected" checkable="1" text="Spam" checkedText="Not Spam" />
<DropDownButton dropdownId="delete" className="action-delete" icon="delete" checkedIcon="delete_selected" checkable="1" text="Delete" checkedText="Undelete" />
<a className="button show-dropdown"><img src="icons/label.png" />Label <span className="down"></span></a>
<DropDownButton ref="setBtn" dropdownId="settings" whole="1" icon="config" text="Settings" />
</div>
<div className="nomail">
<div className="flex1"></div>
<div className="middle">
<img src="icons/no_mail.png" />
<div className="txt">No message selected</div>
</div>
<div className="flex1"></div>
</div>
{msg ? [
<div className="headers">
<div className="top">
<div className="pin"></div>
<div className="time">{this.formatLongDate(msg.time)}</div>
<div className="subject">{msg.subject}</div>
</div>
<div className="header-table">
<div className="header">
<div className="field">From</div>
<div className="value"><a className="button">{msg.from+' <'+msg.fromEmail+'>'}</a></div>
</div>
<div className="header">
<div className="field">To</div>
<div className="value"><a className="button">{msg.to}</a></div>
</div>
{msg.cc ?
<div className="header">
<div className="field">Cc</div>
<div className="value">
{msg.cc.map(cc => <a className="button">{cc}</a>)}
</div>
</div>
: null}
{msg.replyto ?
<div className="header">
<div className="field">Reply-to</div>
<div className="value"><a className="button">{msg.replyto}</a></div>
</div>
: null}
</div>
</div>,
(msg.blockedImages ?
<div className="blocked-images">
<img src="icons/block.png" /> This message contains blocked images.
<a className="button raised">Load Images</a>
<label><input type="checkbox" /> Always load from {msg.from}</label>
</div>
: null),
<div className="text" dangerouslySetInnerHTML={{ __html: msg.body }}></div>,
this.state.quickReply ?
<div className="quick-reply">
<textarea></textarea>
<div className="btns">
<a className="button"><img src="icons/mail_send.png" /> Quick Reply</a>
<a className="button"><img src="icons/mail_reply_all.png" /> Quick Reply All</a>
</div>
</div>
: null
] : null}
</div>
}
});
var accounts = [
{
name: 'All Messages',
accountId: null,
unreadCount: 65,
folders: [
{ name: 'Unread', icon: 'mail_unread', unreadCount: 65 },
{ name: 'Received', icon: 'mail_received', unreadCount: 65 },
{ name: 'Pinned', icon: 'mail_pinned' },
{ name: 'Outbox', icon: 'mail_outbox' },
{ name: 'Sent', icon: 'mail_sent' },
{ name: 'Drafts', icon: 'mail_drafts', unreadCount: 507 },
{ name: 'Spam', icon: 'mail_spam' },
{ name: 'Trash', icon: 'mail_trash', unreadCount: 423 },
],
},
{
name: 'vitalif@mail.ru',
accountId: 1,
unreadCount: 48,
warning: true,
folders: [
{ name: 'Unread', icon: 'mail_unread', unreadCount: 48 },
{ name: 'INBOX', icon: 'folder', unreadCount: 48 },
{ name: 'TODO', icon: 'folder' },
{ name: 'Архив', icon: 'folder' },
{ name: 'Корзина', icon: 'mail_trash' },
{ name: 'Отправленные', icon: 'mail_sent' },
{ name: 'Спам', icon: 'mail_spam' },
{ name: 'Черновики', icon: 'mail_drafts' },
{ name: 'Pinned', icon: 'mail_pinned' },
],
},
{
name: 'vitalif@yourcmc.ru',
accountId: 2,
unreadCount: 16,
loading: true,
folders: [
{ name: 'Unread', icon: 'mail_unread', unreadCount: 16 },
{ name: 'Drafts', icon: 'mail_drafts' },
{ name: 'HAM', icon: 'folder' },
{ name: 'INBOX', icon: 'folder', unreadCount: 16 },
{ name: 'intermedia_su', icon: 'folder' },
{ name: 'Sent', icon: 'mail_sent' },
{ name: 'SPAM', icon: 'mail_spam' },
{ name: 'TRASH', icon: 'mail_trash' },
{ name: 'Pinned', icon: 'mail_pinned' },
],
}
];
var composeAccounts = [
{ name: 'Виталий Филиппов', email: 'vitalif@mail.ru' },
{ name: 'Vitaliy Filippov', email: 'vitalif@yourcmc.ru' }
];
var msg2 = [];
msg2[5] = {
subject: 'кошку хочешь?))',
sent: true,
from: 'Виталий Филиппов',
fromEmail: 'vitalif@mail.ru',
to: 'Margarita Philippova',
time: '2016-06-14 21:24',
body: 'к нам тут пришла какая-то и к нам в дом рвётся, симпатишная) потеряшка какая-то, но явно домашняя, не боится людей))',
thread: [ {
subject: 'Re: кошку хочешь?))',
from: 'Margarita Philippova',
fromEmail: 'philippova.marga@gmail.com',
to: 'Виталий Филиппов',
time: '2016-06-14 22:31',
level: 1,
body: 'Нет, конечно, не хочу. Я уеду в Баку, а кошка как? Уеду в Астану - а кошка как? Наташа меня Сухорукова приглашает в Барселону - а кошку куда? Я только испытала радость облегчения от того, что у меня больше нет горшочных цветов - и кошку мне? НЕт! Папику предложи - у него уже много зверья, одной кошкой больше - одной меньше - какая разница?'
} ]
};
var listGroups = [ {
name: 'TODAY',
messageCount: 10,
messages: [ {
subject: 'The Glossary of Happiness By Emily Anthes , May 12, 2016',
from: 'Margarita Philippova',
fromEmail: 'philippova.marga@gmail.com',
to: 'Виталий Филиппов',
time: '2016-06-20 00:11:18',
unread: true,
body: '<pre><a href="http://www.newyorker.com/tech/elements/the-glossary-of-happiness">http://www.newyorker.com/tech/elements/the-glossary-of-happiness</a><br />\
<br />\
--<br />\
Best regards,<br />\
Margarita Philippova</pre>'
} ]
}, {
name: 'LAST WEEK',
messageCount: 15,
messages: msg2
} ];
var AllTabs = React.createClass({
componentDidMount: function()
{
Store.on('layout', this.setLayout);
},
componentWillUnmount: function()
{
Store.un('layout', this.setLayout);
},
setLayout: function()
{
var oldLayout = this.state.layout;
this.setState({ layout: Store.layout });
window.scrollBy(0, 0);
var self = this;
requestAnimationFrame(function()
{
var btn;
if (oldLayout == 'message-on-right' &&
Store.layout != 'message-on-right')
{
btn = self.refs.list.refs.setBtn;
}
else if (oldLayout != 'message-on-right' &&
Store.layout == 'message-on-right')
{
btn = self.refs.view.refs.setBtn;
}
if (btn)
{
btn.setState({ pressed: false });
btn.toggle();
}
});
},
getInitialState: function()
{
return { layout: Store.layout };
},
render: function()
{
return React.createElement(TabPanel, { tabs: [
{
className: this.state.layout,
noclose: true,
icon: 'mail_unread',
title: 'Unread (64)',
children: [ <MessageList ref="list" groups={listGroups} />, <MessageView ref="view" /> ]
},
{
icon: 'mail_drafts',
i16: true,
title: 'Compose Message',
children: <ComposeWindow accounts={composeAccounts} />
}
] });
}
});
ReactDOM.render(
<div>
{dropdown_account}
{dropdown_reply}
{dropdown_forward}
{dropdown_delete}
{dropdown_check_send}
{dropdown_threads}
{dropdown_list_sort}
{dropdown_settings}
<FolderList accounts={accounts} progress="33" />
<AllTabs />
</div>,
document.body
);