[#34] Add react-list & lazy-loading to Messages & Adjust store to make requests

This commit is contained in:
Jannis Mattheis 2018-04-08 17:45:25 +02:00 committed by Jannis Mattheis
parent 9ed6228013
commit ca5a832baf
4 changed files with 119 additions and 64 deletions

View File

@ -6,7 +6,6 @@ import dispatcher from '../stores/dispatcher';
export function initialLoad(resp) { export function initialLoad(resp) {
AppAction.fetchApps(); AppAction.fetchApps();
MessageAction.fetchMessages();
MessageAction.listenToWebSocket(); MessageAction.listenToWebSocket();
ClientAction.fetchClients(); ClientAction.fetchClients();
if (resp.data.admin) { if (resp.data.admin) {

View File

@ -18,6 +18,9 @@ const styles = () => ({
marginTop: -15, marginTop: -15,
marginRight: -15, marginRight: -15,
}, },
wrapperPadding: {
padding: 12,
},
messageContentWrapper: { messageContentWrapper: {
width: '100%', width: '100%',
}, },
@ -33,33 +36,34 @@ class Message extends Component {
static propTypes = { static propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
image: PropTypes.string.isRequired, image: PropTypes.string,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
content: PropTypes.string.isRequired, content: PropTypes.string.isRequired,
fDelete: PropTypes.func.isRequired, fDelete: PropTypes.func.isRequired,
}; };
render() { render() {
const {fDelete, classes, title, date, content, image} = this.props; const {fDelete, classes, title, date, content, image} = this.props;
return ( return (
<Container style={{display: 'flex'}}> <div className={classes.wrapperPadding}>
<div className={classes.imageWrapper}> <Container style={{display: 'flex'}}>
<img src={image} alt="app logo" width="70" height="70" className={classes.image}/> <div className={classes.imageWrapper}>
</div> <img src={image} alt="app logo" width="70" height="70" className={classes.image}/>
<div className={classes.messageContentWrapper}>
<div className={classes.header}>
<Typography className={classes.headerTitle} variant="headline">
{title}
</Typography>
<Typography variant="body1">
<TimeAgo date={date}/>
</Typography>
<IconButton onClick={fDelete} className={classes.trash}><Delete/></IconButton>
</div> </div>
<Typography component="p">{content}</Typography> <div className={classes.messageContentWrapper}>
</div> <div className={classes.header}>
</Container> <Typography className={classes.headerTitle} variant="headline">
{title}
</Typography>
<Typography variant="body1">
<TimeAgo date={date}/>
</Typography>
<IconButton onClick={fDelete} className={classes.trash}><Delete/></IconButton>
</div>
<Typography component="p">{content}</Typography>
</div>
</Container>
</div>
); );
} }
} }

View File

@ -6,12 +6,11 @@ import MessageStore from '../stores/MessageStore';
import AppStore from '../stores/AppStore'; import AppStore from '../stores/AppStore';
import * as MessageAction from '../actions/MessageAction'; import * as MessageAction from '../actions/MessageAction';
import DefaultPage from '../component/DefaultPage'; import DefaultPage from '../component/DefaultPage';
import ReactList from 'react-list';
import {CircularProgress} from 'material-ui/Progress';
class Messages extends Component { class Messages extends Component {
constructor() { state = {appId: -1, messages: [], name: 'unknown', hasMore: true, list: null};
super();
this.state = {appId: -1, messages: [], name: 'unknown'};
}
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.updateAllWithProps(nextProps); this.updateAllWithProps(nextProps);
@ -30,9 +29,10 @@ class Messages extends Component {
updateAllWithProps = (props) => { updateAllWithProps = (props) => {
const appId = Messages.appId(props); const appId = Messages.appId(props);
const messages = MessageStore.getForAppId(appId); this.setState({...MessageStore.get(appId), appId, name: AppStore.getName(appId)});
const name = AppStore.getName(appId); if (!MessageStore.exists(appId)) {
this.setState({appId: appId, messages, name}); MessageStore.loadNext(appId);
}
}; };
updateAll = () => this.updateAllWithProps(this.props); updateAll = () => this.updateAllWithProps(this.props);
@ -45,24 +45,57 @@ class Messages extends Component {
return match.params.id !== undefined ? parseInt(match.params.id, 10) : -1; return match.params.id !== undefined ? parseInt(match.params.id, 10) : -1;
} }
render() { renderMessage = (index, key) => {
const {name, messages, appId} = this.state; this.checkIfLoadMore();
const fDelete = appId === -1 ? MessageAction.deleteMessages : MessageAction.deleteMessagesByApp.bind(this, appId); const message = this.state.messages[index];
return (
const noMessages = ( <Message key={key}
<Grid item xs={12}><Typography variant="caption" gutterBottom align="center">No messages</Typography></Grid> fDelete={() => MessageAction.deleteMessage(message)}
title={message.title}
date={message.date}
content={message.message}
image={message.image}/>
); );
};
checkIfLoadMore() {
const {hasMore, messages, appId} = this.state;
if (hasMore) {
const [, maxRenderedIndex] = (this.list && this.list.getVisibleRange()) || [0, 0];
if (maxRenderedIndex > (messages.length - 30)) {
MessageStore.loadNext(appId);
}
}
}
label = (text) => (
<Grid item xs={12}><Typography variant="caption" gutterBottom align="center">{text}</Typography></Grid>
);
render() {
const {name, messages, hasMore, appId} = this.state;
const hasMessages = messages.length !== 0;
const deleteMessages = () => MessageAction.deleteMessagesByApp(appId);
return ( return (
<DefaultPage title={name} buttonTitle="Delete All" fButton={fDelete} buttonDisabled={messages.length === 0}> <DefaultPage title={name} buttonTitle="Delete All" fButton={deleteMessages} buttonDisabled={!hasMessages}>
{messages.length === 0 ? noMessages : messages.map((message) => { {hasMessages
return ( ? (
<Grid item xs={12} key={message.id}> <div style={{width: '100%'}}>
<Message fDelete={() => MessageAction.deleteMessage(message.id)} title={message.title} <ReactList ref={(el) => this.list = el}
date={message.date} content={message.message} image={message.image}/> itemRenderer={this.renderMessage}
</Grid> length={messages.length}
); threshold={1000}
})} pageSize={30}
type='variable'
/>
{hasMore
? <Grid item xs={12} style={{textAlign: 'center'}}><CircularProgress size={100}/></Grid>
: this.label('You\'ve reached the end')}
</div>
)
: this.label('No messages')
}
</DefaultPage> </DefaultPage>
); );
} }

View File

@ -1,59 +1,78 @@
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import dispatcher from './dispatcher'; import dispatcher from './dispatcher';
import AppStore from './AppStore'; import AppStore from './AppStore';
import * as MessageAction from '../actions/MessageAction';
class MessageStore extends EventEmitter { class MessageStore extends EventEmitter {
constructor() { constructor() {
super(); super();
this.messages = []; this.appToMessages = {};
this.messagesForApp = []; this.loading = false;
AppStore.on('change', this.updateApps); AppStore.on('change', this.updateApps);
} }
get() { loadNext(id) {
return this.messages; if (this.loading || !this.get(id).hasMore) {
return;
}
this.loading = true;
MessageAction.fetchMessagesApp(id, this.get(id).nextSince).catch(() => this.loading = false);
} }
getForAppId(id) { get(id) {
if (id === -1) { if (this.exists(id)) {
return this.messages; return this.appToMessages[id];
} else {
return {messages: [], nextSince: 0, hasMore: true};
} }
if (this.messagesForApp[id]) { }
return this.messagesForApp[id];
} exists(id) {
return []; return this.appToMessages[id] !== undefined;
} }
handle(data) { handle(data) {
if (data.type === 'UPDATE_MESSAGES') { if (data.type === 'UPDATE_MESSAGES') {
this.messages = data.payload; const payload = data.payload;
this.messagesForApp = []; if (this.exists(payload.id)) {
this.messages.forEach(function(message) { payload.messages = this.get(payload.id).messages.concat(payload.messages);
this.createIfNotExist(message.appid); }
this.messagesForApp[message.appid].push(message); this.appToMessages[payload.id] = payload;
}.bind(this));
this.updateApps(); this.updateApps();
this.loading = false;
this.emit('change'); this.emit('change');
} else if (data.type === 'ONE_MESSAGE') { } else if (data.type === 'ONE_MESSAGE') {
const {payload} = data; const {payload} = data;
this.createIfNotExist(payload.appid); this.createIfNotExist(payload.appid);
this.messagesForApp[payload.appid].unshift(payload); this.createIfNotExist(-1);
this.messages.unshift(payload); this.appToMessages[payload.appid].messages.unshift(payload);
this.appToMessages[-1].messages.unshift(payload);
this.updateApps(); this.updateApps();
this.emit('change'); this.emit('change');
} else if (data.type === 'DELETE_MESSAGE') {
Object.keys(this.appToMessages).forEach((key) => {
const appMessages = this.appToMessages[key];
const index = appMessages.messages.indexOf(data.payload);
if (index !== -1) {
appMessages.messages.splice(index, 1);
}
});
this.emit('change');
} }
} }
updateApps = () => { updateApps = () => {
const appToUrl = {}; const appToUrl = {};
AppStore.get().forEach((app) => appToUrl[app.id] = app.image); AppStore.get().forEach((app) => appToUrl[app.id] = app.image);
this.messages.forEach((msg) => msg.image = appToUrl[msg.appid]); Object.keys(this.appToMessages).forEach((key) => {
this.messagesForApp.forEach((forApp) => forApp.forEach((msg) => msg.image = appToUrl[msg.appid])); const appMessages = this.appToMessages[key];
appMessages.messages.forEach((message) => message.image = appToUrl[message.appid]);
});
}; };
createIfNotExist(id) { createIfNotExist(id) {
if (!(id in this.messagesForApp)) { if (!(id in this.appToMessages)) {
this.messagesForApp[id] = []; this.appToMessages[id] = this.get(id);
} }
} }
} }