Use react-redux like store connect

master
Vitaliy Filippov 2016-10-01 17:21:32 +03:00
parent eec9c21fa7
commit 5ea8744e83
12 changed files with 232 additions and 180 deletions

View File

@ -4,6 +4,7 @@
"transform-es2015-arrow-functions",
"transform-es2015-block-scoping",
"transform-es2015-classes",
"transform-es2015-for-of",
"transform-es2015-computed-properties",
"transform-es2015-destructuring",
"transform-es2015-shorthand-properties",

View File

@ -6,7 +6,7 @@ var AccountFolders = module.exports = React.createClass({
{
return <div className="account">
<div className={"account-header"+(this.state.collapsed ? ' collapsed' : '')} onClick={this.onClick}>
{this.props.name}
{this.props.email || 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>,
@ -46,7 +46,7 @@ var AccountFolders = module.exports = React.createClass({
{
var self = this;
var i = DropDownBase.instances.account.state.items;
i[0].text = 'Read '+this.props.name;
i[0].text = 'Read '+(this.props.email||this.props.name);
DropDownBase.instances.account.setState({ items: i });
DropDownBase.instances.account.showAt(ev.target, function()
{

View File

@ -134,21 +134,7 @@ var dropdown_list_sort = React.createElement(
}
);
var dropdown_settings = React.createElement(
MailSettingsWindow, {
id: 'settings',
window: true,
markDelay: -1,
defaultSorting: {
sort: {
sortby: 'sent date',
group: 'date',
ascending: false,
threaded: false
}
}
}
);
var dropdown_settings = MailSettingsWindow;
module.exports = function()
{

View File

@ -1,7 +1,8 @@
const React = require('react');
const AttachList = require('./AttachList.js');
const StoreListener = require('./StoreListener.js');
var ComposeWindow = module.exports = React.createClass({
var ComposeWindow = React.createClass({
getInitialState: function()
{
return {
@ -58,3 +59,5 @@ var ComposeWindow = module.exports = React.createClass({
</div>
}
});
module.exports = StoreListener(ComposeWindow, (data) => { return { accounts: data.accounts }; });

4
I18n.js Normal file
View File

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

View File

@ -2,8 +2,9 @@ const React = require('react');
const DropDownBase = require('./DropDownBase.js');
const ListSortSettings = require('./ListSortSettings.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
var MailSettingsWindow = module.exports = React.createClass({
var MailSettingsWindow = React.createClass({
mixins: [ DropDownBase ],
render: function()
{
@ -12,15 +13,15 @@ var MailSettingsWindow = module.exports = React.createClass({
<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>
<a onClick={this.switchLayout} className={'button mail-message-on-right'+(this.props.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.props.layout == 'message-on-bottom' ? ' selected' : '')} title="List and Message Below"><span></span></a>
<a onClick={this.switchLayout} className={'button mail-message-invisible'+(this.props.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>
<label><input type="checkbox" checked={this.props.showQuickReply} onClick={this.showQuickReply} /> Show Quick Reply</label>
</div>
<div className="split"><i></i></div>
<div className="text">Mark as Read</div>
@ -35,36 +36,32 @@ var MailSettingsWindow = module.exports = React.createClass({
</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);
}
});
module.exports = StoreListener(
MailSettingsWindow,
(data) => { return { layout: data.layout, showQuickReply: data.showQuickReply }; },
{
id: 'settings',
window: true,
markDelay: -1,
defaultSorting: {
sort: {
sortby: 'sent date',
group: 'date',
ascending: false,
threaded: false
}
}
}
);

View File

@ -1,7 +1,7 @@
const React = require('react');
const DropDownButton = require('./DropDownButton.js');
const ListWithSelection = require('./ListWithSelection.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
const Util = require('./Util.js');
var MessageInList = React.createClass({
@ -10,10 +10,10 @@ var MessageInList = React.createClass({
{
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}>
(this.props.selected ? ' selected' : '')+(msg.thread && this.props.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}
{msg.thread && this.props.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">{Util.formatBytes(msg.size)}</div>
@ -24,17 +24,19 @@ var MessageInList = React.createClass({
});
// TODO: expand/collapse days
var MessageList = module.exports = React.createClass({
var MessageList = React.createClass({
mixins: [ ListWithSelection ],
getInitialState: function()
{
return { firstDayTop: 0, firstDay: this.props.groups[0].name, groups: this.props.groups /*FIXME*/ };
return { firstDayTop: 0, firstDay: this.props.groups && this.props.groups[0] && this.props.groups[0].name || '', groups: this.props.groups||[] /*FIXME*/ };
},
changeFirstDay: function(ev)
{
if (!this.state.groups.length)
return;
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 itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
var i;
for (i = 0; i < this.state.groups.length; i++)
{
@ -104,7 +106,7 @@ var MessageList = module.exports = React.createClass({
},
getPageSize: function()
{
return Math.round(this.refs.scroll.offsetHeight / (Store.layout == 'message-on-right' ? 60 : 30));
return Math.round(this.refs.scroll.offsetHeight / (this.props.layout == 'message-on-right' ? 60 : 30));
},
getItemOffset: function(index)
{
@ -122,12 +124,12 @@ var MessageList = module.exports = React.createClass({
if (index < n)
{
if (index > p)
top += (i > 0 ? 30 : 0) + (Store.layout == 'message-on-right' ? 60 : 30)*(index-p-1);
top += (i > 0 ? 30 : 0) + (this.props.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;
top += (i > 0 ? 30 : 0) + (this.props.layout == 'message-on-right' ? 60 : 30)*this.state.groups[i].messageCount;
}
return [ top, (Store.layout == 'message-on-right' && (index == 0 || index != p) ? 60 : 30) ];
return [ top, (this.props.layout == 'message-on-right' && (index == 0 || index != p) ? 60 : 30) ];
},
getScrollPaddingTop: function()
{
@ -145,7 +147,7 @@ var MessageList = module.exports = React.createClass({
{
var self = this;
var total = 0;
var itemH = (Store.layout == 'message-on-right' ? 60 : 30);
var itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
return <div className="message-list">
<div className="top-border-gradient"></div>
<div className="bottom-border-gradient"></div>
@ -163,7 +165,7 @@ var MessageList = module.exports = React.createClass({
</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) {
{(this.props.groups||[]).map(function(grp, i) {
if (i > 0)
total++;
var start = total+(self.state.firstGrp == i ? self.state.firstMsg : 0);
@ -181,7 +183,7 @@ var MessageList = module.exports = React.createClass({
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} />,
<MessageInList threads={this.props.threads} 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} />)}
@ -204,3 +206,12 @@ var MessageList = module.exports = React.createClass({
</div>
}
});
module.exports = StoreListener(MessageList, function(data)
{
return {
threads: data.threads,
layout: data.layout,
groups: data.listGroups
};
});

View File

@ -1,9 +1,10 @@
const React = require('react');
const DropDownButton = require('./DropDownButton.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
const Util = require('./Util.js');
var MessageView = module.exports = React.createClass({
var MessageView = React.createClass({
formatLongDate: function(dt)
{
if (!(dt instanceof Date))
@ -14,32 +15,10 @@ var MessageView = module.exports = React.createClass({
return Util.WeekDays[dt.getDay()]+' '+dt.getDate()+' '+Util.Months[dt.getMonth()]+' '+dt.getFullYear()+' '+(h < 10 ? '0' : '')+h+':'+(m < 10 ? '0' : '')+m+':'+(s < 10 ? '0' : '')+s
//return dt.toLocaleString();
},
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' : '')}>
var msg = this.props.msg;
return <div className={'message-view'+(!msg ? ' no-mail-shown' : '')+(!this.props.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>
@ -98,7 +77,7 @@ var MessageView = module.exports = React.createClass({
</div>
: null),
<div className="text" dangerouslySetInnerHTML={{ __html: msg.body }}></div>,
this.state.quickReply ?
this.props.quickReply ?
<div className="quick-reply">
<textarea></textarea>
<div className="btns">
@ -111,3 +90,5 @@ var MessageView = module.exports = React.createClass({
</div>
}
});
module.exports = StoreListener(MessageView, (data) => { return { quickReply: data.quickReply, msg: data.msg }; });

112
Store.js
View File

@ -1,26 +1,102 @@
var Store = module.exports = {
layout: 'message-on-right',
quickReply: true,
msg: null,
threads: false,
const superagent = require('superagent');
const _ = require('./I18n.js');
listeners: {},
on: function(ev, cb)
{
this.listeners[ev] = this.listeners[ev] || [];
this.listeners[ev].push(cb);
var Store = module.exports = {
data: {
layout: 'message-on-right',
quickReply: true,
msg: null,
threads: false,
accounts: [],
},
un: function(ev, cb)
listeners: [],
on: function(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);
this.listeners.push(cb);
},
un: function(cb)
{
for (var i = this.listeners.length; i >= 0; i--)
if (this.listeners[i] == cb)
this.listeners.splice(i, 1);
},
get: function(k)
{
return this.data[k];
},
set: function(k, v)
{
this[k] = v;
(this.listeners[k] || []).map(i => i());
this.data[k] = v;
(this.listeners || []).map(i => i());
},
loadAccounts: function()
{
superagent.get('backend/folders').end(function(err, res)
{
var ixOfAll = {
received: 1,
outbox: 3,
sent: 4,
drafts: 5,
spam: 6,
trash: 7
};
var accounts = [ {
name: _('All Messages'),
accountId: null,
unreadCount: 0,
folders: [
{ name: _('Unread'), icon: 'mail_unread', unreadCount: 0, type: 'unread' },
{ name: _('Received'), icon: 'mail_received', unreadCount: 0, type: 'inbox' },
{ name: _('Pinned'), icon: 'mail_pinned', unreadCount: 0, type: 'pinned' },
{ name: _('Outbox'), icon: 'mail_outbox', unreadCount: 0, type: 'outbox' },
{ name: _('Sent'), icon: 'mail_sent', unreadCount: 0, type: 'sent' },
{ name: _('Drafts'), icon: 'mail_drafts', unreadCount: 0, type: 'drafts' },
{ name: _('Spam'), icon: 'mail_spam', unreadCount: 0, type: 'spam' },
{ name: _('Trash'), icon: 'mail_trash', unreadCount: 0, type: 'trash' },
],
} ];
for (let a of res.body.accounts)
{
let account = {
name: a.name,
email: a.email,
accountId: a.id,
unreadCount: 0,
warning: false,
folders: [
{ name: _('Unread'), icon: 'mail_unread', unreadCount: 0, type: 'unread' },
{ name: _('Pinned'), icon: 'mail_pinned', unreadCount: a.pinned_unread_count, type: 'pinned' },
],
folderMap: a.foldermap,
folderTypes: {}
};
if (!account.folderMap.received)
{
account.folderMap.received = 'INBOX';
}
for (let f in account.folderMap)
{
account.folderTypes[account.folderMap[f]] = f;
}
for (let f of a.folders)
{
let icon = (account.folderTypes[f.name] ? 'mail_'+account.folderTypes[f.name] : 'folder');
account.folders.push({ name: f.name, icon: icon, unreadCount: f.unread_count-0, folderId: f.id });
account.folders[0].unreadCount -= -f.unread_count;
if (account.folderTypes[f.name])
{
accounts[0].folders[ixOfAll[account.folderTypes[f.name]]].unreadCount -= -f.unread_count;
}
}
accounts.push(account);
accounts[0].unreadCount += account.unreadCount;
accounts[0].folders[0].unreadCount += account.unreadCount;
accounts[0].folders[2].unreadCount += account.folders[1].unreadCount;
}
Store.set('accounts', accounts);
});
}
};

39
StoreListener.js Normal file
View File

@ -0,0 +1,39 @@
const React = require('react');
const Store = require('./Store.js');
// "react-redux connect()"-like example
var StoreListener = React.createClass({
componentDidMount: function()
{
Store.on(this.update);
},
componentWillUnmount: function()
{
Store.un(this.update);
},
update: function()
{
var newState = this.props.mapStateToProps(Store.data);
for (var i in newState)
{
if (this.state[i] != newState[i])
{
this.setState(newState);
return;
}
}
},
getInitialState: function()
{
return { ...this.props.initial, ...this.props.mapStateToProps(Store.data) };
},
render: function()
{
return React.createElement(this.props.wrappedComponent, this.state);
}
});
module.exports = function(component, map, initial)
{
return React.createElement(StoreListener, { wrappedComponent: component, mapStateToProps: map, initial: initial||{} });
};

105
mail.js
View File

@ -6,67 +6,11 @@ const MessageList = require('./MessageList.js');
const MessageView = require('./MessageView.js');
const TabPanel = require('./TabPanel.js');
const Store = require('./Store.js');
const StoreListener = require('./StoreListener.js');
const AllDropdowns = require('./AllDropdowns.js');
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame;
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: 'кошку хочешь?))',
@ -87,7 +31,7 @@ msg2[5] = {
} ]
};
var listGroups = [ {
Store.listGroups = [ {
name: 'TODAY',
messageCount: 10,
messages: [ {
@ -109,7 +53,26 @@ Best regards,<br />\
messages: msg2
} ];
var AllTabs = React.createClass({
var AllTabs = StoreListener(TabPanel, function(data)
{
return { tabs: [
{
className: data.layout,
noclose: true,
icon: 'mail_unread',
title: 'Unread (64)',
children: [ MessageList, MessageView ]
},
{
icon: 'mail_drafts',
i16: true,
title: 'Compose Message',
children: [ ComposeWindow ]
}
] }
});
/*React.createClass({
componentDidMount: function()
{
Store.on('layout', this.setLayout);
@ -150,29 +113,17 @@ var AllTabs = React.createClass({
},
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} />
}
] });
return React.createElement();
}
});
});*/
ReactDOM.render(
<div>
{AllDropdowns()}
<FolderList accounts={accounts} progress="33" />
<AllTabs />
{StoreListener(FolderList, (data) => { return { accounts: data.accounts }; }, { progress: 33 })}
{AllTabs}
</div>,
document.body
);
Store.loadAccounts();

View File

@ -17,6 +17,7 @@
"babel-plugin-transform-es2015-block-scoping": "latest",
"babel-plugin-transform-es2015-classes": "latest",
"babel-plugin-transform-es2015-computed-properties": "latest",
"babel-plugin-transform-es2015-for-of": "latest",
"babel-plugin-transform-es2015-destructuring": "latest",
"babel-plugin-transform-es2015-shorthand-properties": "latest",
"babel-plugin-transform-object-rest-spread": "latest",
@ -25,10 +26,12 @@
"react-dom": "latest",
"uglifyjs": "latest",
"uglifyify": "latest",
"eslint": "latest"
"eslint": "latest",
"superagent": "latest"
},
"scripts": {
"compile": "browserify -t babelify -t uglifyify mail.js | uglifyjs -cm > mail.c.js",
"watch-dev": "watchify -t babelify mail.js -o mail.c.js",
"watch": "watchify -t babelify -t uglifyify mail.js -o 'uglifyjs -cm > mail.c.js'"
}
}