From 7ed3ae738055f44a0e87fd870b778f905251f710 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sun, 11 Sep 2016 15:49:57 +0300 Subject: [PATCH] Use CommonJS modules, browserify, babelify, and watchify --- .babelrc | 14 + AccountFolders.js | 80 ++ AttachList.js | 77 ++ ComposeWindow.js | 60 + DropDownBase.js | 107 ++ DropDownButton.js | 47 + DropDownMenu.js | 63 + FolderList.js | 53 + ListSortSettings.js | 22 + ListSortSettingsWindow.js | 37 + ListWithSelection.js | 130 ++ MailSettingsWindow.js | 70 + MessageList.js | 206 +++ MessageView.js | 112 ++ Store.js | 26 + TabPanel.js | 62 + Util.js | 34 + mail.js | 1171 +---------------- mail.js.htm | 5 +- package.json | 33 + browser.js => raw/browser.js | 0 mail-raw.css => raw/mail-raw.css | 0 mail.htm => raw/mail.htm | 2 +- react-15.0.1.js => raw/react-15.0.1.js | 0 .../react-dom-15.1.0.js | 0 25 files changed, 1249 insertions(+), 1162 deletions(-) create mode 100644 .babelrc create mode 100644 AccountFolders.js create mode 100644 AttachList.js create mode 100644 ComposeWindow.js create mode 100644 DropDownBase.js create mode 100644 DropDownButton.js create mode 100644 DropDownMenu.js create mode 100644 FolderList.js create mode 100644 ListSortSettings.js create mode 100644 ListSortSettingsWindow.js create mode 100644 ListWithSelection.js create mode 100644 MailSettingsWindow.js create mode 100644 MessageList.js create mode 100644 MessageView.js create mode 100644 Store.js create mode 100644 TabPanel.js create mode 100644 Util.js create mode 100644 package.json rename browser.js => raw/browser.js (100%) rename mail-raw.css => raw/mail-raw.css (100%) rename mail.htm => raw/mail.htm (99%) rename react-15.0.1.js => raw/react-15.0.1.js (100%) rename react-dom-15.1.0.js => raw/react-dom-15.1.0.js (100%) diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..b770454 --- /dev/null +++ b/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + "check-es2015-constants", + "transform-es2015-arrow-functions", + "transform-es2015-block-scoping", + "transform-es2015-classes", + "transform-es2015-computed-properties", + "transform-es2015-destructuring", + "transform-es2015-shorthand-properties", + "transform-object-rest-spread", + "transform-react-jsx", + ], + "retainLines": true +} diff --git a/AccountFolders.js b/AccountFolders.js new file mode 100644 index 0000000..e653eb6 --- /dev/null +++ b/AccountFolders.js @@ -0,0 +1,80 @@ +const React = require('react'); +const DropDownBase = require('./DropDownBase.js'); + +var AccountFolders = module.exports = React.createClass({ + render: function() + { + return
+
+ {this.props.name} +
{this.props.unreadCount}
+ {this.props.accountId ? [ +
, +
, +
+ ] : null} +
+
+
+ {this.props.folders.map((f, i) => +
0 ? ' with-unread' : '')+ + (this.state.selected == i ? ' selected' : '')} onClick={this.selectFolder}> + {f.icon ? : null} + {' '} + {f.name} + {f.unreadCount > 0 ?
{f.unreadCount}
: null} +
+ )} +
+
+
+ }, + 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); + } +}); diff --git a/AttachList.js b/AttachList.js new file mode 100644 index 0000000..1db8c3d --- /dev/null +++ b/AttachList.js @@ -0,0 +1,77 @@ +const React = require('react'); +const ListWithSelection = require('./ListWithSelection.js'); +const Util = require('./Util.js'); + +var AttachList = module.exports = 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
+
+ +
+
+
+
Attachment + + +
+
Size
+
+ {this.state.attachments.map((a, i) => +
+
{a.name}
+
{Util.formatBytes(a.size)}
+
+ )} +
+
+ } +}); diff --git a/ComposeWindow.js b/ComposeWindow.js new file mode 100644 index 0000000..64cde34 --- /dev/null +++ b/ComposeWindow.js @@ -0,0 +1,60 @@ +const React = require('react'); +const AttachList = require('./AttachList.js'); + +var ComposeWindow = module.exports = React.createClass({ + getInitialState: function() + { + return { + text: '' + }; + }, + changeText: function(ev) + { + this.setState({ text: ev.target.value }); + }, + render: function() + { + return
+
+ Send + +
+
+
+
+
+
From
+
+ +
+
+
+
To
+
+
+
+
Cc
+
+
+
+
Bcc
+
+
+
+
Subject
+
+
+
+ +
+
+ +
+
+
+ } +}); diff --git a/DropDownBase.js b/DropDownBase.js new file mode 100644 index 0000000..f5a3669 --- /dev/null +++ b/DropDownBase.js @@ -0,0 +1,107 @@ +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 DropDownBase = module.exports = { + 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; + } +}; diff --git a/DropDownButton.js b/DropDownButton.js new file mode 100644 index 0000000..48f1e96 --- /dev/null +++ b/DropDownButton.js @@ -0,0 +1,47 @@ +const React = require('react'); +const DropDownBase = require('./DropDownBase.js'); + +var DropDownButton = module.exports = React.createClass({ + render: function() + { + return + {this.props.icon ? : null} + {this.state.checked && this.props.checkedText || this.props.text || null} + {this.props.dropdownId ? : null} + + }, + 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(); + } +}); diff --git a/DropDownMenu.js b/DropDownMenu.js new file mode 100644 index 0000000..e063b26 --- /dev/null +++ b/DropDownMenu.js @@ -0,0 +1,63 @@ +const React = require('react'); +const DropDownBase = require('./DropDownBase.js'); + +var DropDownMenu = module.exports = React.createClass({ + mixins: [ DropDownBase ], + getInitialState: function() + { + return { items: this.props.items }; + }, + render: function() + { + var sel = this.state.selectedItem; + return
+ {this.state.items.map(function(i, index) { + return (i.split + ?
: +
+ {i.hotkey ?
{i.hotkey}
: null} + {i.icon ? : null} + {i.text} +
+ ); + })} +
+ }, + 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); + } +}); diff --git a/FolderList.js b/FolderList.js new file mode 100644 index 0000000..205ee62 --- /dev/null +++ b/FolderList.js @@ -0,0 +1,53 @@ +const React = require('react'); +const AccountFolders = require('./AccountFolders.js'); +const DropDownButton = require('./DropDownButton.js'); + +var FolderList = module.exports = React.createClass({ + render: function() + { + var self = this; + return
+
+
+
+ Compose + +
+ // TODO: keyboard navigation +
+ {this.props.accounts.map(function(account) { + return + })} +
+
+
Loading database ({this.props.progress||0}%)
+
+
Loading database ({this.props.progress||0}%)
+
+
+
+ }, + 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); + } +}); diff --git a/ListSortSettings.js b/ListSortSettings.js new file mode 100644 index 0000000..f356d58 --- /dev/null +++ b/ListSortSettings.js @@ -0,0 +1,22 @@ +const React = require('react'); + +var ListSortSettings = module.exports = React.createClass({ + render: function() + { + return
+ + + + +
+ } +}); diff --git a/ListSortSettingsWindow.js b/ListSortSettingsWindow.js new file mode 100644 index 0000000..393bdf5 --- /dev/null +++ b/ListSortSettingsWindow.js @@ -0,0 +1,37 @@ +const React = require('react'); +const DropDownBase = require('./DropDownBase.js'); +const ListSortSettings = require('./ListSortSettings.js'); + +var ListSortSettingsWindow = module.exports = React.createClass({ + mixins: [ DropDownBase ], + render: function() + { + var sort = this.props.override ? this.props.sorting : this.props.defaultSorting; + return
+
+
Sorting for {this.props.folder}
+ + +
+ Show +
+
+ + + + + + +
+
+ }, + getInitialState: function() + { + return { checksVisible: false }; + }, + expandChecks: function() + { + this.setState({ checksVisible: !this.state.checksVisible }); + } +}); diff --git a/ListWithSelection.js b/ListWithSelection.js new file mode 100644 index 0000000..a02aaf8 --- /dev/null +++ b/ListWithSelection.js @@ -0,0 +1,130 @@ +// Common selection mixin +var ListWithSelection = module.exports = { + // 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); + } + } +}; diff --git a/MailSettingsWindow.js b/MailSettingsWindow.js new file mode 100644 index 0000000..2ae2ade --- /dev/null +++ b/MailSettingsWindow.js @@ -0,0 +1,70 @@ +const React = require('react'); +const DropDownBase = require('./DropDownBase.js'); +const ListSortSettings = require('./ListSortSettings.js'); +const Store = require('./Store.js'); + +var MailSettingsWindow = module.exports = React.createClass({ + mixins: [ DropDownBase ], + render: function() + { + return
+
+
Mail Layout
+
+ + + +
+
+
Default List Sorting
+ +
+ +
+
+
Mark as Read
+
+ +
+
+ }, + 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); + } +}); diff --git a/MessageList.js b/MessageList.js new file mode 100644 index 0000000..28d780b --- /dev/null +++ b/MessageList.js @@ -0,0 +1,206 @@ +const React = require('react'); +const DropDownButton = require('./DropDownButton.js'); +const ListWithSelection = require('./ListWithSelection.js'); +const Store = require('./Store.js'); +const Util = require('./Util.js'); + +var MessageInList = React.createClass({ + msgClasses: [ 'unread', 'unseen', 'replied', 'pinned', 'sent', 'unloaded' ], + render: function() + { + var msg = this.props.msg; + return
(msg[c] ? ' '+c : '')).join(''))+ + (this.props.selected ? ' selected' : '')+(msg.thread && Store.threads ? ' thread0' : '')} onMouseDown={this.props.onClick}> +
+
{msg.subject}
+ {msg.thread && Store.threads ?
: null} +
+
{(msg.sent || msg.outgoing ? 'To '+msg.to : msg.from)}
+
{Util.formatBytes(msg.size)}
+
+
{Util.formatDate(msg.time)}
+
+ } +}); + +// TODO: expand/collapse days +var MessageList = module.exports = 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 = this.refs.title.offsetHeight, 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
+
+
+
+
+ +
+ + + +
+
+
+
{this.state.firstDay}
+ {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 ?
{grp.name}
: null, +
+ {(self.state.firstGrp > i || self.state.lastGrp < i + ?
+ : [ + (self.state.firstGrp == i + ?
+ : 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 + ? [ + , + /*(msg.thread && Store.threads ? +
+ {msg.thread.map(reply => )} +
+ : null)*/ + ] + :
+ )), + (self.state.lastGrp == i + ?
+ : null) + ] + )} +
+ ]; + total += grp.messageCount; + return r; + })} +
+
+ } +}); diff --git a/MessageView.js b/MessageView.js new file mode 100644 index 0000000..4e8df12 --- /dev/null +++ b/MessageView.js @@ -0,0 +1,112 @@ +const React = require('react'); +const DropDownButton = require('./DropDownButton.js'); +const Store = require('./Store.js'); + +var MessageView = module.exports = 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
+
+ + Reply All + + + + + Label + +
+
+
+
+ +
No message selected
+
+
+
+ {msg ? [ +
+
+
+
{this.formatLongDate(msg.time)}
+
{msg.subject}
+
+
+
+
From
+ +
+
+
To
+ +
+ {msg.cc ? +
+
Cc
+
+ {msg.cc.map(cc => {cc})} +
+
+ : null} + {msg.replyto ? +
+
Reply-to
+ +
+ : null} +
+
, + (msg.blockedImages ? +
+ This message contains blocked images. + Load Images + +
+ : null), +
, + this.state.quickReply ? +
+ +
+ Quick Reply + Quick Reply All +
+
+ : null + ] : null} +
+ } +}); diff --git a/Store.js b/Store.js new file mode 100644 index 0000000..48f8b28 --- /dev/null +++ b/Store.js @@ -0,0 +1,26 @@ +var Store = module.exports = { + 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()); + } +}; diff --git a/TabPanel.js b/TabPanel.js new file mode 100644 index 0000000..94afc49 --- /dev/null +++ b/TabPanel.js @@ -0,0 +1,62 @@ +const React = require('react'); + +var TabPanel = module.exports = 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( +
+ {t.noclose ? null : } + {t.title} +
+ ); + body.push( +
+ {t.children} +
+ ); + } + return
+
{bar}
+ {body} +
+ }, + 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(); + } +}); diff --git a/Util.js b/Util.js new file mode 100644 index 0000000..c7c299e --- /dev/null +++ b/Util.js @@ -0,0 +1,34 @@ +module.exports.formatBytes = function(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'; +} + +module.exports.formatDate = function(dt) +{ + if (!(dt instanceof Date)) + dt = new Date(dt.replace(' ', 'T')); + var tod = new Date(); + tod.setHours(0); + tod.setMinutes(0); + tod.setSeconds(0); + tod.setMilliseconds(0); + var prevweek = tod; + prevweek = prevweek.getTime() - (7 + (prevweek.getDay()+6)%7)*86400000; + if (dt.getTime() < prevweek) + { + 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; +} diff --git a/mail.js b/mail.js index 30f5d5f..ac8d27a 100644 --- a/mail.js +++ b/mail.js @@ -1,249 +1,20 @@ +const React = require('react'); +const ReactDOM = require('react-dom'); +const ComposeWindow = require('./ComposeWindow.js'); +const DropDownMenu = require('./DropDownMenu.js'); +const FolderList = require('./FolderList.js'); +const ListSortSettingsWindow = require('./ListSortSettingsWindow.js'); +const MailSettingsWindow = require('./MailSettingsWindow.js'); +const MessageList = require('./MessageList.js'); +const MessageView = require('./MessageView.js'); +const TabPanel = require('./TabPanel.js'); +const Store = require('./Store.js'); + 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
- {this.state.items.map(function(i, index) { - return (i.split - ?
: -
- {i.hotkey ?
{i.hotkey}
: null} - {i.icon ? : null} - {i.text} -
- ); - })} -
- }, - 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 - {this.props.icon ? : null} - {this.state.checked && this.props.checkedText || this.props.text || null} - {this.props.dropdownId ? : null} - - }, - 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', @@ -349,127 +120,6 @@ var dropdown_threads = React.createElement( } ); -var ListSortSettings = React.createClass({ - render: function() - { - return
- - - - -
- } -}); - -var ListSortSettingsWindow = React.createClass({ - mixins: [ DropDownBase ], - render: function() - { - var sort = this.props.override ? this.props.sorting : this.props.defaultSorting; - return
-
-
Sorting for {this.props.folder}
- - -
- Show -
-
- - - - - - -
-
- }, - getInitialState: function() - { - return { checksVisible: false }; - }, - expandChecks: function() - { - this.setState({ checksVisible: !this.state.checksVisible }); - } -}); - -var MailSettingsWindow = React.createClass({ - mixins: [ DropDownBase ], - render: function() - { - return
-
-
Mail Layout
-
- - - -
-
-
Default List Sorting
- -
- -
-
-
Mark as Read
-
- -
-
- }, - 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', @@ -512,803 +162,6 @@ var dropdown_settings = React.createElement( } ); -var AccountFolders = React.createClass({ - render: function() - { - return
-
- {this.props.name} -
{this.props.unreadCount}
- {this.props.accountId ? [ -
, -
, -
- ] : null} -
-
-
- {this.props.folders.map((f, i) => -
0 ? ' with-unread' : '')+ - (this.state.selected == i ? ' selected' : '')} onClick={this.selectFolder}> - {f.icon ? : null} - {' '} - {f.name} - {f.unreadCount > 0 ?
{f.unreadCount}
: null} -
- )} -
-
-
- }, - 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
-
-
-
- Compose - -
- // TODO: keyboard navigation -
- {this.props.accounts.map(function(account) { - return - })} -
-
-
Loading database ({this.props.progress||0}%)
-
-
Loading database ({this.props.progress||0}%)
-
-
-
- }, - 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( -
- {t.noclose ? null : } - {t.title} -
- ); - body.push( -
- {t.children} -
- ); - } - return
-
{bar}
- {body} -
- }, - 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
-
- -
-
-
-
Attachment - - -
-
Size
-
- {this.state.attachments.map((a, i) => -
-
{a.name}
-
{formatBytes(a.size)}
-
- )} -
-
- } -}); - -var ComposeWindow = React.createClass({ - getInitialState: function() - { - return { - text: '' - }; - }, - changeText: function(ev) - { - this.setState({ text: ev.target.value }); - }, - render: function() - { - return
-
- Send - -
-
-
-
-
-
From
-
- -
-
-
-
To
-
-
-
-
Cc
-
-
-
-
Bcc
-
-
-
-
Subject
-
-
-
- -
-
- -
-
-
- } -}); - -var MessageInList = React.createClass({ - msgClasses: [ 'unread', 'unseen', 'replied', 'pinned', 'sent', 'unloaded' ], - render: function() - { - var msg = this.props.msg; - return
(msg[c] ? ' '+c : '')).join(''))+ - (this.props.selected ? ' selected' : '')+(msg.thread && Store.threads ? ' thread0' : '')} onMouseDown={this.props.onClick}> -
-
{msg.subject}
- {msg.thread && Store.threads ?
: null} -
-
{(msg.sent || msg.outgoing ? 'To '+msg.to : msg.from)}
-
{formatBytes(msg.size)}
-
-
{formatDate(msg.time)}
-
- } -}); - -// TODO: expand/collapse days -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 = this.refs.title.offsetHeight, 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
-
-
-
-
- -
- - - -
-
-
-
{this.state.firstDay}
- {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 ?
{grp.name}
: null, -
- {(self.state.firstGrp > i || self.state.lastGrp < i - ?
- : [ - (self.state.firstGrp == i - ?
- : 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 - ? [ - , - /*(msg.thread && Store.threads ? -
- {msg.thread.map(reply => )} -
- : null)*/ - ] - :
- )), - (self.state.lastGrp == i - ?
- : null) - ] - )} -
- ]; - total += grp.messageCount; - return r; - })} -
-
- } -}); - -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
-
- - Reply All - - - - - Label - -
-
-
-
- -
No message selected
-
-
-
- {msg ? [ -
-
-
-
{this.formatLongDate(msg.time)}
-
{msg.subject}
-
-
-
-
From
- -
-
-
To
- -
- {msg.cc ? -
-
Cc
-
- {msg.cc.map(cc => {cc})} -
-
- : null} - {msg.replyto ? -
-
Reply-to
- -
- : null} -
-
, - (msg.blockedImages ? -
- This message contains blocked images. - Load Images - -
- : null), -
, - this.state.quickReply ? -
- -
- Quick Reply - Quick Reply All -
-
- : null - ] : null} -
- } -}); - var accounts = [ { name: 'All Messages', diff --git a/mail.js.htm b/mail.js.htm index 9701435..9abdf39 100644 --- a/mail.js.htm +++ b/mail.js.htm @@ -8,9 +8,10 @@ - + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..7d77f10 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "likeopera", + "author": { + "name": "Vitaliy Filippov", + "email": "vitalif@yourcmc.ru", + "url": "http://yourcmc.ru/wiki/" + }, + "description": "LikeOperaMail", + "dependencies": { + }, + "devDependencies": { + "browserify": "latest", + "babelify": "latest", + "watchify": "latest", + "babel-plugin-check-es2015-constants": "latest", + "babel-plugin-transform-es2015-arrow-functions": "latest", + "babel-plugin-transform-es2015-block-scoping": "latest", + "babel-plugin-transform-es2015-classes": "latest", + "babel-plugin-transform-es2015-computed-properties": "latest", + "babel-plugin-transform-es2015-destructuring": "latest", + "babel-plugin-transform-es2015-shorthand-properties": "latest", + "babel-plugin-transform-object-rest-spread": "latest", + "babel-plugin-transform-react-jsx": "latest", + "react": "latest", + "react-dom": "latest", + "uglifyify": "latest", + "eslint": "latest" + }, + "scripts": { + "compile": "browserify -t babelify -t uglifyify mail.js > mail.c.js", + "watch": "watchify -t babelify -t uglifyify mail.js -o mail.c.js" + } +} diff --git a/browser.js b/raw/browser.js similarity index 100% rename from browser.js rename to raw/browser.js diff --git a/mail-raw.css b/raw/mail-raw.css similarity index 100% rename from mail-raw.css rename to raw/mail-raw.css diff --git a/mail.htm b/raw/mail.htm similarity index 99% rename from mail.htm rename to raw/mail.htm index ad16630..f5f8900 100644 --- a/mail.htm +++ b/raw/mail.htm @@ -2,7 +2,7 @@ - + diff --git a/react-15.0.1.js b/raw/react-15.0.1.js similarity index 100% rename from react-15.0.1.js rename to raw/react-15.0.1.js diff --git a/react-dom-15.1.0.js b/raw/react-dom-15.1.0.js similarity index 100% rename from react-dom-15.1.0.js rename to raw/react-dom-15.1.0.js