likeopera-frontend/MessageList.js

302 lines
12 KiB
JavaScript

import React from 'react';
import DropDownButton from './DropDownButton.js';
import ListWithSelection from './ListWithSelection.js';
import Util from './Util.js';
class MessageInList extends React.PureComponent
{
msgClasses = { unread: 'unread', unseen: 'unseen', answered: 'replied', flagged: 'pinned', sent: 'sent' }
render()
{
let msg = this.props.msg;
return <div data-i={this.props.i} className={'message'+
(msg.body_text || msg.body_html ? '' : ' unloaded')+
(msg.flags.map(c => (this.msgClasses[c] ? ' '+this.msgClasses[c] : '')).join(''))+
(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 && 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.props.sent ? 'To '+(msg.props.to[0][0]||msg.props.to[0][1]) : (msg.props.from ? msg.props.from[0]||msg.props.from[1] : ''))}
</div>
<div className="size">{Util.formatBytes(msg.size||0)}</div>
<div className="attach" style={msg.props.attachments && msg.props.attachments.length ? null : { display: 'none' }}></div>
<div className="time">{Util.formatDate(msg.time)}</div>
</div>
}
}
// TODO: expand/collapse days
export default class MessageList extends ListWithSelection
{
_preloadSize = 20
_pageSize = 50
state = {
...this.state,
firstDayTop: 0,
firstDay: this.props.groups && this.props.groups[0] && this.props.groups[0].name || null
}
componentWillReceiveProps(nextProps)
{
this.setFirstDayFromProps(nextProps);
}
// Main virtual scroll detector method
setFirstDayFromProps(props)
{
let groups = props.groups;
let messages = props.messages;
if (!groups || !groups.length)
return;
let scrollTop = this.refs.scroll.scrollTop, scrollSize = this.refs.scroll.offsetHeight - this.getScrollPaddingTop();
let top = 0, p, firstVisibleGrp, firstVisible, lastVisibleGrp, lastVisible;
let itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
let i;
for (i = 0; i < groups.length; i++)
{
p = top;
top += (i > 0 ? 30 : 0) + itemH*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 >= groups[i].messageCount)
lastVisible = groups[i].messageCount-1;
}
if (firstVisibleGrp !== undefined && lastVisibleGrp !== undefined)
break;
}
if (firstVisibleGrp === undefined || firstVisibleGrp >= groups.length)
{
this.refs.scroll.scrollTop = 0;
return;
}
if (lastVisibleGrp === undefined)
{
lastVisibleGrp = groups.length-1;
lastVisible = groups[lastVisibleGrp].messageCount-1;
}
this.setState({
firstDayTop: scrollTop,
firstDay: firstVisibleGrp !== undefined ? groups[firstVisibleGrp].name : null,
firstGrp: firstVisibleGrp,
firstMsg: firstVisible,
lastGrp: lastVisibleGrp,
lastMsg: lastVisible
});
let loadFirst = groups[firstVisibleGrp].start+firstVisible-this._preloadSize;
let loadLast = groups[lastVisibleGrp].start+lastVisible+1+this._preloadSize;
let total = groups[groups.length-1].messageCount+groups[groups.length-1].start;
loadLast = Math.floor((loadLast + this._pageSize - 1) / this._pageSize) * this._pageSize;
loadLast = loadLast < total ? loadLast : total;
loadFirst = loadFirst < 0 ? 0 : loadFirst;
loadFirst = loadFirst - (loadFirst % this._pageSize);
let loadFirstEnd;
for (loadFirstEnd = loadFirst; loadFirstEnd < loadLast && messages[loadFirstEnd] === undefined; loadFirstEnd++)
messages[loadFirstEnd] = null;
let loadLastStart;
for (loadLastStart = loadLast; loadLastStart > loadFirst && messages[loadLastStart-1] === undefined; loadLastStart--)
messages[loadLastStart-1] = null;
if (loadFirstEnd > loadFirst)
this.props.loadMessages(loadFirst, loadFirstEnd-loadFirst);
if (loadFirstEnd < loadLastStart && loadLastStart < loadLast)
this.props.loadMessages(loadLastStart, loadLast-loadLastStart);
}
changeFirstDay = (ev) =>
{
this.setFirstDayFromProps(this.props);
}
deleteSelected = () =>
{
}
onSelectCurrent = (index) =>
{
let total = 0, p, msg, idx;
for (let i = 0; i < (this.props.groups||[]).length; i++)
{
p = total;
total += (i > 0 ? 1 : 0)+this.props.groups[i].messageCount;
if (index < total)
{
idx = this.props.groups[i].start+index-p-(i > 0 ? 1 : 0);
msg = this.props.messages[idx];
if (msg && !msg.body_text && !msg.body_html)
{
this.props.loadMessage(msg.id, (newMsg) =>
{
this.props.setMessage(newMsg);
if (this.props.messages[idx] == msg)
{
this.props.messages[idx] = newMsg;
}
});
}
else
{
this.props.setMessage(msg);
}
break;
}
}
}
getTotalItems = () =>
{
let total = -1; // do not count first-day as item
for (let i = 0; i < (this.props.groups||[]).length; i++)
{
total += 1+this.props.groups[i].messageCount;
}
return total;
}
getPageSize = () =>
{
return Math.floor(this.refs.scroll.offsetHeight / (this.props.layout == 'message-on-right' ? 60 : 30));
}
getItemOffset = (index) =>
{
let n = 0, top = 0, p, i;
let h = (this.props.layout == 'message-on-right' ? 60 : 30);
for (i = 0; i < (this.props.groups||[]).length; i++)
{
p = n;
n += (i > 0 ? 1 : 0)+this.props.groups[i].messageCount;
if (index < n)
{
top += (index > p && i > 0 ? 30+h*(index-p-1) : h*(index-p));
break;
}
top += (i > 0 ? 30 : 0) + h*this.props.groups[i].messageCount;
}
return [ top, index == p && i > 0 ? 30 : h ];
}
getScrollPaddingTop = () =>
{
return this.refs.title.offsetHeight;
}
getMessages = (grp, start, end) =>
{
let a = this.props.messages.slice(grp.start+start, grp.start+end);
for (let i = 0; i < end-start; i++)
if (!a[i])
a[i] = null;
return a;
}
onSearchTextChange = (event) =>
{
let s = event.target.value;
this.setState({ searchText: s });
if (this._searchTimeout)
{
clearTimeout(this._searchTimeout);
}
this._searchTimeout = setTimeout(() => this.props.search(s), 300);
}
renderGroup = (grp, i) =>
{
if (i > 0)
{
this.total++;
}
let start = this.total+(this.state.firstGrp == i ? this.state.firstMsg : 0);
let r = [
i > 0 ? <div className="day" data-i={this.total-1}>{grp.name.toUpperCase()}</div> : null,
<div className="day-list">
{(this.state.firstGrp > i || this.state.lastGrp < i
? <div className="placeholder" style={{ height: this.itemH*grp.messageCount }}></div>
: [
(this.state.firstGrp == i
? <div className="placeholder" style={{ height: this.itemH*this.state.firstMsg }}></div>
: null),
this.getMessages(grp,
this.state.firstGrp == i ? this.state.firstMsg : 0,
this.state.lastGrp == i ? this.state.lastMsg+1 : grp.messageCount
).map((msg, j) => (msg
? [
<MessageInList threads={this.props.threads} msg={msg} i={start+j} selected={this.isSelected(start+j)} onClick={this.onListItemClick} />,
/*(msg.thread && this.props.threads ?
<div className="thread">
{msg.thread.map(reply => <MessageInList msg={reply} i={0} onClick={this.onListItemClick} />)}
</div>
: null)*/
]
: <div data-i={start+j} className={'message'+(this.isSelected(start+j) ? ' selected' : '')} onMouseDown={this.onListItemClick}></div>
)),
(this.state.lastGrp == i
? <div className="placeholder" style={{ height: this.itemH*(grp.messageCount-this.state.lastMsg-1) }}></div>
: null)
]
)}
</div>
];
this.total += grp.messageCount;
return r;
}
render()
{
this.itemH = (this.props.layout == 'message-on-right' ? 60 : 30);
this.total = 0;
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" value={this.state.searchText} onChange={this.onSearchTextChange} />
</div>
<DropDownButton dropdownId="threads" className="threads"
title="Show Message Thread" checkedTitle="Hide Message Thread" icon="thread" checkedIcon="thread_selected" checkable="1" />
<DropDownButton dropdownId="settings" className="settings" whole="1" hidden={this.props.layout == 'message-on-right'}
title="Settings for this folder" icon="config" />
<DropDownButton dropdownId="list-sort" className="list-sort" whole="1"
title="Sorting settings" icon="list_sort" />
<div className="clear"></div>
</div>
<div ref="scroll" className="listview" tabIndex="1" onScroll={this.changeFirstDay} onKeyDown={this.onListKeyDown}>
<div ref="title" className="day first-day"
style={{ top: this.state.firstDayTop, display: this.state.firstDay ? '' : 'none' }}>
{(this.state.firstDay||'').toUpperCase()}
</div>
{(this.props.groups||[]).map(this.renderGroup)}
</div>
</div>);
}
componentDidMount()
{
window.addEventListener('resize', this.changeFirstDay);
}
componentWillUnmount()
{
window.removeEventListener('resize', this.changeFirstDay);
}
}