Improve message rendering

This commit is contained in:
Ken-Håvard Lieng 2016-02-16 22:43:25 +01:00
parent ede8e19722
commit 072daa64f2
16 changed files with 303 additions and 260 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,59 +4,59 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"devDependencies": { "devDependencies": {
"babel-core": "^6.4.5", "babel-core": "^6.5.2",
"babel-eslint": "^5.0.0-beta6", "babel-eslint": "^5.0.0-beta6",
"babel-loader": "^6.2.2", "babel-loader": "^6.2.2",
"babel-plugin-react-transform": "^2.0.0", "babel-plugin-react-transform": "^2.0.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-react-constant-elements": "^6.4.0", "babel-plugin-transform-react-constant-elements": "^6.5.0",
"babel-plugin-transform-react-inline-elements": "^6.4.0", "babel-plugin-transform-react-inline-elements": "^6.5.0",
"babel-preset-es2015": "^6.3.13", "babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.3.13", "babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.3.13", "babel-preset-stage-0": "^6.5.0",
"css-loader": "^0.23.1", "css-loader": "^0.23.1",
"eslint": "^1.10.3", "eslint": "^1.10.3",
"eslint-config-airbnb": "^5.0.0", "eslint-config-airbnb": "^5.0.1",
"eslint-loader": "^1.2.1", "eslint-loader": "^1.2.1",
"eslint-plugin-react": "^3.16.1", "eslint-plugin-react": "^3.16.1",
"express": "^4.13.4", "express": "^4.13.4",
"express-http-proxy": "^0.6.0", "express-http-proxy": "^0.6.0",
"gulp": "^3.9.0", "gulp": "^3.9.1",
"gulp-autoprefixer": "3.1.0", "gulp-autoprefixer": "3.1.0",
"gulp-cached": "^1.1.0", "gulp-cached": "^1.1.0",
"gulp-concat": "^2.6.0", "gulp-concat": "^2.6.0",
"gulp-cssnano": "^2.1.0", "gulp-cssnano": "^2.1.1",
"gulp-gzip": "1.2.0", "gulp-gzip": "1.2.0",
"gulp-htmlmin": "^1.3.0", "gulp-htmlmin": "^1.3.0",
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
"ify-loader": "^1.0.3",
"react-transform-catch-errors": "^1.0.2", "react-transform-catch-errors": "^1.0.2",
"react-transform-hmr": "^1.0.2", "react-transform-hmr": "^1.0.2",
"redbox-react": "^1.2.2", "redbox-react": "^1.2.2",
"redux-devtools": "^3.1.0", "redux-devtools": "^3.1.1",
"redux-devtools-dock-monitor": "^1.0.1", "redux-devtools-dock-monitor": "^1.0.1",
"redux-devtools-log-monitor": "^1.0.4", "redux-devtools-log-monitor": "^1.0.4",
"style-loader": "^0.13.0", "style-loader": "^0.13.0",
"webpack": "^1.12.13", "webpack": "^1.12.13",
"webpack-dev-middleware": "^1.5.1", "webpack-dev-middleware": "^1.5.1",
"webpack-hot-middleware": "^2.6.4" "webpack-hot-middleware": "^2.7.1"
}, },
"dependencies": { "dependencies": {
"autolinker": "^0.22.0", "autolinker": "^0.23.0",
"backo": "^1.1.0", "backo": "^1.1.0",
"base64-arraybuffer": "^0.1.5", "base64-arraybuffer": "^0.1.5",
"eventemitter2": "^0.4.14", "eventemitter2": "^0.4.14",
"history": "^2.0.0", "history": "^2.0.0",
"immutable": "^3.7.6", "immutable": "^3.7.6",
"lodash": "^4.2.1", "lodash": "^4.3.0",
"pure-render-decorator": "^0.2.0", "pure-render-decorator": "^0.2.0",
"react": "^0.14.7", "react": "^0.14.7",
"react-dom": "^0.14.7", "react-dom": "^0.14.7",
"react-infinite": "0.8.0",
"react-redux": "^4.4.0", "react-redux": "^4.4.0",
"react-router": "^2.0.0-rc5", "react-router": "^2.0.0",
"react-router-redux": "^3.0.0", "react-router-redux": "^3.0.0",
"react-virtualized": "^4.8.1", "react-virtualized": "^4.10.0",
"redux": "^3.3.0", "redux": "^3.3.1",
"redux-thunk": "^1.0.3", "redux-thunk": "^1.0.3",
"reselect": "^2.0.3" "reselect": "^2.0.3"
} }

View File

@ -396,19 +396,19 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
bottom: 50px; bottom: 50px;
right: 0; right: 0;
z-index: 1; z-index: 1;
overflow: hidden;
} }
.chat-channel .messagebox { .chat-channel .messagebox {
right: 200px; right: 200px;
} }
.messagebox-scrollable { .VirtualScroll {
padding: 10px 15px; overflow-x: hidden !important;
overflow-y: auto !important;
} }
.message { .message {
white-space: nowrap; padding: 3px 15px;
} }
.message-info { .message-info {

View File

@ -43,5 +43,6 @@ export const TAB_HISTORY_POP = 'TAB_HISTORY_POP';
export const TOGGLE_MENU = 'TOGGLE_MENU'; export const TOGGLE_MENU = 'TOGGLE_MENU';
export const TOGGLE_SEARCH = 'TOGGLE_SEARCH'; export const TOGGLE_SEARCH = 'TOGGLE_SEARCH';
export const TOGGLE_USERLIST = 'TOGGLE_USERLIST'; export const TOGGLE_USERLIST = 'TOGGLE_USERLIST';
export const UPDATE_MESSAGE_HEIGHT = 'UPDATE_MESSAGE_HEIGHT';
export const UPLOAD_CERT = 'UPLOAD_CERT'; export const UPLOAD_CERT = 'UPLOAD_CERT';
export const WHOIS = 'WHOIS'; export const WHOIS = 'WHOIS';

View File

@ -1,36 +1,75 @@
import * as actions from '../actions'; import * as actions from '../actions';
import { messageHeight } from '../util';
function initMessage(message, state) {
message.dest = message.to || message.from || message.server;
if (message.from && message.from.indexOf('.') !== -1) {
message.dest = message.server;
}
if (message.dest.charAt(0) === '#') {
message.channel = true;
}
// Combine multiple adjacent spaces into a single one
message.message = message.message.replace(/\s\s+/g, ' ');
const charWidth = state.environment.get('charWidth');
const wrapWidth = state.environment.get('wrapWidth');
message.height = messageHeight(message, wrapWidth, charWidth, 6 * charWidth);
return message;
}
export function updateMessageHeight() {
return (dispatch, getState) => dispatch({
type: actions.UPDATE_MESSAGE_HEIGHT,
wrapWidth: getState().environment.get('wrapWidth'),
charWidth: getState().environment.get('charWidth')
});
}
export function sendMessage(message, to, server) { export function sendMessage(message, to, server) {
return (dispatch, getState) => dispatch({ return (dispatch, getState) => {
type: actions.SEND_MESSAGE, const state = getState();
from: getState().servers.getIn([server, 'nick']),
message, dispatch(initMessage({
to, type: actions.SEND_MESSAGE,
server, from: state.servers.getIn([server, 'nick']),
time: new Date(), message,
socket: { to,
type: 'chat', server,
data: { message, to, server } time: new Date(),
} socket: {
}); type: 'chat',
data: { message, to, server }
}
}, state));
};
} }
export function addMessage(message) { export function addMessage(message) {
message.time = new Date(); message.time = new Date();
return { return (dispatch, getState) => dispatch({
type: actions.ADD_MESSAGE, type: actions.ADD_MESSAGE,
message message: initMessage(message, getState())
}; });
} }
export function addMessages(messages) { export function addMessages(messages) {
const now = new Date(); const now = new Date();
messages.forEach(message => message.time = now);
return { return (dispatch, getState) => {
type: actions.ADD_MESSAGES, const state = getState();
messages
messages.forEach(message => initMessage(message, state).time = now);
dispatch({
type: actions.ADD_MESSAGES,
messages
});
}; };
} }

View File

@ -1,12 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import Autolinker from 'autolinker'; import Autolinker from 'autolinker';
import pure from 'pure-render-decorator';
import { timestamp } from '../util'; import { timestamp } from '../util';
export default class MessageHeader extends Component { @pure
shouldComponentUpdate(nextProps) { export default class Message extends Component {
return nextProps.message.lines[0] !== this.props.message.lines[0];
}
handleSenderClick = () => { handleSenderClick = () => {
const { message, openPrivateChat, select } = this.props; const { message, openPrivateChat, select } = this.props;
@ -16,7 +14,7 @@ export default class MessageHeader extends Component {
render() { render() {
const { message } = this.props; const { message } = this.props;
const line = Autolinker.link(message.lines[0], { stripPrefix: false }); const content = Autolinker.link(message.message, { stripPrefix: false });
const classes = ['message']; const classes = ['message'];
let sender = null; let sender = null;
@ -35,11 +33,16 @@ export default class MessageHeader extends Component {
); );
} }
const style = {
paddingLeft: `${window.messageIndent + 15}px`,
textIndent: `-${window.messageIndent}px`
};
return ( return (
<p className={classes.join(' ')}> <p className={classes.join(' ')} style={style}>
<span className="message-time">{timestamp(message.time)}</span> <span className="message-time">{timestamp(message.time)}</span>
{sender} {sender}
<span dangerouslySetInnerHTML={{ __html: ` ${line}` }}></span> <span dangerouslySetInnerHTML={{ __html: ` ${content}` }}></span>
</p> </p>
); );
} }

View File

@ -1,8 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import Infinite from 'react-infinite'; import { VirtualScroll } from 'react-virtualized';
import pure from 'pure-render-decorator'; import pure from 'pure-render-decorator';
import MessageHeader from './MessageHeader'; import Message from './Message';
import MessageLine from './MessageLine';
@pure @pure
export default class MessageBox extends Component { export default class MessageBox extends Component {
@ -15,16 +14,22 @@ export default class MessageBox extends Component {
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
} }
componentWillUpdate() { componentWillReceiveProps() {
const el = this.refs.list.refs.scrollable; const el = this.refs.list.refs.scrollingContainer;
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight; this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
} }
componentWillUpdate(nextProps) {
if (nextProps.messages !== this.props.messages) {
this.refs.list.recomputeRowHeights();
}
}
componentDidUpdate() { componentDidUpdate() {
setTimeout(this.updateWidth, 0); this.updateWidth();
if (this.autoScroll) { if (this.autoScroll) {
const el = this.refs.list.refs.scrollable; const el = this.refs.list.refs.scrollingContainer;
el.scrollTop = el.scrollHeight; el.scrollTop = el.scrollHeight;
} }
} }
@ -33,57 +38,71 @@ export default class MessageBox extends Component {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
} }
updateWidth = () => { getRowHeight = index => {
const { setWrapWidth } = this.props; const { messages } = this.props;
if (index === 0 || index === messages.size + 1) {
return 7;
}
return messages.get(index - 1).height;
};
updateWidth = resize => {
const { isChannel, setWrapWidth, updateMessageHeight } = this.props;
const { list } = this.refs; const { list } = this.refs;
if (list) { if (list) {
const width = list.refs.scrollable.offsetWidth - 30; let width = list.refs.scrollingContainer.clientWidth - 30;
if (isChannel) {
width += 200;
}
if (this.width !== width) { if (this.width !== width) {
this.width = width; this.width = width;
setWrapWidth(width); setWrapWidth(width);
if (resize) {
updateMessageHeight();
}
} }
} }
}; };
handleResize = () => { handleResize = () => {
this.updateWidth(); this.updateWidth(true);
this.setState({ height: window.innerHeight - 100 }); this.setState({ height: window.innerHeight - 100 });
}; };
render() { renderMessage = index => {
const { tab, messages, select, openPrivateChat } = this.props; const { messages } = this.props;
const dest = tab.channel || tab.user || tab.server;
const lines = [];
messages.forEach((message, j) => { if (index === 0 || index === messages.size + 1) {
const key = message.server + dest + j; return <span style={{ height: '7px' }}></span>;
lines.push( }
<MessageHeader
key={key}
message={message}
select={select}
openPrivateChat={openPrivateChat}
/>
);
for (let i = 1; i < message.lines.length; i++) { const { select, openPrivateChat } = this.props;
lines.push( const message = messages.get(index - 1);
<MessageLine key={`${key}-${i}`} type={message.type} line={message.lines[i]} />
);
}
});
return (
<Message
message={message}
select={select}
openPrivateChat={openPrivateChat}
/>
);
};
render() {
return ( return (
<div className="messagebox"> <div className="messagebox">
<Infinite <VirtualScroll
ref="list" ref="list"
className="messagebox-scrollable" height={this.state.height}
containerHeight={this.state.height} rowsCount={this.props.messages.size + 2}
elementHeight={24} rowHeight={this.getRowHeight}
displayBottomUpwards={false} rowRenderer={this.renderMessage}
> />
{lines}
</Infinite>
</div> </div>
); );
} }

View File

@ -1,26 +0,0 @@
import React, { Component } from 'react';
import Autolinker from 'autolinker';
import pure from 'pure-render-decorator';
@pure
export default class MessageLine extends Component {
render() {
const { line, type } = this.props;
const content = Autolinker.link(line, { stripPrefix: false });
const classes = ['message'];
if (type) {
classes.push(`message-${type}`);
}
const style = {
paddingLeft: `${window.messageIndent}px`
};
return (
<p className={classes.join(' ')} style={style}>
<span dangerouslySetInnerHTML={{ __html: content }}></span>
</p>
);
}
}

View File

@ -13,6 +13,12 @@ export default class UserList extends Component {
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
} }
componentWillUpdate(nextProps) {
if (nextProps.users.size === this.props.users.size) {
this.refs.list.forceUpdate();
}
}
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
} }
@ -25,9 +31,7 @@ export default class UserList extends Component {
return 24; return 24;
}; };
handleResize = () => { handleResize = () => this.setState({ height: window.innerHeight - 100 });
this.setState({ height: window.innerHeight - 100 });
};
renderUser = index => { renderUser = index => {
const { users } = this.props; const { users } = this.props;
@ -61,6 +65,7 @@ export default class UserList extends Component {
return ( return (
<div className={className} style={style}> <div className={className} style={style}>
<VirtualScroll <VirtualScroll
ref="list"
height={this.state.height} height={this.state.height}
rowsCount={this.props.users.size + 2} rowsCount={this.props.users.size + 2}
rowHeight={this.getRowHeight} rowHeight={this.getRowHeight}

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector, createStructuredSelector } from 'reselect';
import { List, Map } from 'immutable'; import { List, Map } from 'immutable';
import pure from 'pure-render-decorator'; import pure from 'pure-render-decorator';
import ChatTitle from '../components/ChatTitle'; import ChatTitle from '../components/ChatTitle';
@ -13,10 +13,10 @@ import { part } from '../actions/channel';
import { openPrivateChat, closePrivateChat } from '../actions/privateChat'; import { openPrivateChat, closePrivateChat } from '../actions/privateChat';
import { searchMessages, toggleSearch } from '../actions/search'; import { searchMessages, toggleSearch } from '../actions/search';
import { select, setSelectedChannel, setSelectedUser } from '../actions/tab'; import { select, setSelectedChannel, setSelectedUser } from '../actions/tab';
import { runCommand, sendMessage } from '../actions/message'; import { runCommand, sendMessage, updateMessageHeight } from '../actions/message';
import { disconnect } from '../actions/server'; import { disconnect } from '../actions/server';
import { setWrapWidth, setCharWidth } from '../actions/environment'; import { setWrapWidth, setCharWidth } from '../actions/environment';
import { stringWidth, wrapMessages } from '../util'; import { stringWidth } from '../util';
import { toggleUserList } from '../actions/ui'; import { toggleUserList } from '../actions/ui';
import * as inputHistoryActions from '../actions/inputHistory'; import * as inputHistoryActions from '../actions/inputHistory';
@ -64,7 +64,8 @@ class Chat extends Component {
}; };
render() { render() {
const { tab, channel, search, history, dispatch } = this.props; const { title, tab, channel, search, history,
messages, users, showUserList, inputActions } = this.props;
let chatClass; let chatClass;
if (tab.channel) { if (tab.channel) {
@ -77,21 +78,43 @@ class Chat extends Component {
return ( return (
<div className={chatClass}> <div className={chatClass}>
<ChatTitle {...this.props } /> <ChatTitle
title={title}
tab={tab}
channel={channel}
toggleSearch={this.props.toggleSearch}
toggleUserList={this.props.toggleUserList}
disconnect={this.props.disconnect}
part={this.props.part}
closePrivateChat={this.props.closePrivateChat}
/>
<Search <Search
search={search} search={search}
onSearch={this.handleSearch} onSearch={this.handleSearch}
/> />
<MessageBox {...this.props } /> <MessageBox
messages={messages}
isChannel={tab.channel !== null}
setWrapWidth={this.props.setWrapWidth}
updateMessageHeight={this.props.updateMessageHeight}
select={this.props.select}
openPrivateChat={this.props.openPrivateChat}
/>
<MessageInput <MessageInput
tab={tab} tab={tab}
channel={channel} channel={channel}
history={history}
runCommand={this.props.runCommand} runCommand={this.props.runCommand}
sendMessage={this.props.sendMessage} sendMessage={this.props.sendMessage}
history={history} {...inputActions}
{...bindActionCreators(inputHistoryActions, dispatch)} />
<UserList
users={users}
tab={tab}
showUserList={showUserList}
select={this.props.select}
openPrivateChat={this.props.openPrivateChat}
/> />
<UserList {...this.props} />
</div> </div>
); );
} }
@ -99,6 +122,12 @@ class Chat extends Component {
const tabSelector = state => state.tab.selected; const tabSelector = state => state.tab.selected;
const messageSelector = state => state.messages; const messageSelector = state => state.messages;
const serverSelector = state => state.servers;
const channelSelector = state => state.channels;
const searchSelector = state => state.search;
const showUserListSelector = state => state.ui.showUserList;
const historySelector = state =>
state.input.index === -1 ? null : state.input.history.get(state.input.index);
const selectedMessagesSelector = createSelector( const selectedMessagesSelector = createSelector(
tabSelector, tabSelector,
@ -106,40 +135,33 @@ const selectedMessagesSelector = createSelector(
(tab, messages) => messages.getIn([tab.server, tab.channel || tab.user || tab.server], List()) (tab, messages) => messages.getIn([tab.server, tab.channel || tab.user || tab.server], List())
); );
const wrapWidthSelector = state => state.environment.get('wrapWidth'); const selectedChannelSelector = createSelector(
const charWidthSelector = state => state.environment.get('charWidth'); tabSelector,
channelSelector,
const wrappedMessagesSelector = createSelector( (tab, channels) => channels.getIn([tab.server, tab.channel], Map())
selectedMessagesSelector,
wrapWidthSelector,
charWidthSelector,
(messages, width, charWidth) => wrapMessages(messages, width, charWidth, 6 * charWidth)
); );
function mapStateToProps(state) { const usersSelector = createSelector(
const tab = state.tab.selected; selectedChannelSelector,
const channel = state.channels.getIn([tab.server, tab.channel], Map()); channel => channel.get('users', List())
);
let title; const titleSelector = createSelector(
if (tab.channel) { tabSelector,
title = tab.channel; serverSelector,
} else if (tab.user) { (tab, servers) => tab.channel || tab.user || servers.getIn([tab.server, 'name'])
title = tab.user; );
} else {
title = state.servers.getIn([tab.server, 'name']);
}
return { const mapStateToProps = createStructuredSelector({
title, title: titleSelector,
search: state.search, tab: tabSelector,
users: channel.get('users', List()), channel: selectedChannelSelector,
history: state.input.index === -1 ? null : state.input.history.get(state.input.index), messages: selectedMessagesSelector,
messages: wrappedMessagesSelector(state), users: usersSelector,
showUserList: state.ui.showUserList, showUserList: showUserListSelector,
channel, search: searchSelector,
tab history: historySelector
}; });
}
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
@ -155,8 +177,10 @@ function mapDispatchToProps(dispatch) {
disconnect, disconnect,
openPrivateChat, openPrivateChat,
closePrivateChat, closePrivateChat,
setWrapWidth setWrapWidth,
}, dispatch) updateMessageHeight
}, dispatch),
inputActions: bindActionCreators(inputHistoryActions, dispatch)
}; };
} }

View File

@ -1,5 +1,6 @@
import { List, Map, Record } from 'immutable'; import { List, Map, Record } from 'immutable';
import createReducer from '../util/createReducer'; import createReducer from '../util/createReducer';
import { messageHeight } from '../util';
import * as actions from '../actions'; import * as actions from '../actions';
const Message = Record({ const Message = Record({
@ -10,15 +11,11 @@ const Message = Record({
message: '', message: '',
time: null, time: null,
type: null, type: null,
lines: [] height: 0,
channel: false
}); });
function addMessage(state, message) { function addMessage(state, message) {
let dest = message.to || message.from || message.server;
if (message.from && message.from.indexOf('.') !== -1) {
dest = message.server;
}
if (message.message.indexOf('\x01ACTION') === 0) { if (message.message.indexOf('\x01ACTION') === 0) {
const from = message.from; const from = message.from;
message.from = null; message.from = null;
@ -26,7 +23,8 @@ function addMessage(state, message) {
message.message = from + message.message.slice(7); message.message = from + message.message.slice(7);
} }
return state.updateIn([message.server, dest], List(), list => list.push(new Message(message))); return state.updateIn([message.server, message.dest], List(),
list => list.push(new Message(message)));
} }
export default createReducer(Map(), { export default createReducer(Map(), {
@ -45,15 +43,7 @@ export default createReducer(Map(), {
) )
); );
}, },
/*
[actions.SOCKET_MESSAGE](state, action) {
return addMessage(state, action);
},
[actions.SOCKET_PM](state, action) {
return addMessage(state, action);
},
*/
[actions.DISCONNECT](state, action) { [actions.DISCONNECT](state, action) {
return state.delete(action.server); return state.delete(action.server);
}, },
@ -64,5 +54,17 @@ export default createReducer(Map(), {
s.deleteIn([action.server, channel]) s.deleteIn([action.server, channel])
) )
); );
},
[actions.UPDATE_MESSAGE_HEIGHT](state, action) {
return state.withMutations(s =>
s.forEach((server, serverKey) =>
server.forEach((target, targetKey) =>
target.forEach((message, index) => s.setIn([serverKey, targetKey, index, 'height'],
messageHeight(message, action.wrapWidth, action.charWidth, 6 * action.charWidth))
)
)
)
);
} }
}); });

View File

@ -1,6 +1,6 @@
import padStart from 'lodash/padStart'; import padStart from 'lodash/padStart';
export wrapMessages from './wrapMessages'; export messageHeight from './messageHeight';
export function normalizeChannel(channel) { export function normalizeChannel(channel) {
if (channel.indexOf('#') !== 0) { if (channel.indexOf('#') !== 0) {

View File

@ -0,0 +1,39 @@
const lineHeight = 24;
export default function messageHeight(message, width, charWidth, indent = 0) {
let pad = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
let height = lineHeight + 4;
if (message.channel) {
width -= 200;
}
if (pad + message.message.length * charWidth < width) {
return height;
}
let prevBreak = 0;
let prevPos = 0;
for (let i = 0, len = message.message.length; i < len; i++) {
const c = message.message.charAt(i);
if (c === ' ' || c === '-') {
const end = c === ' ' ? i : i + 1;
if (pad + (end - prevBreak) * charWidth >= width) {
prevBreak = prevPos;
pad = indent;
height += lineHeight;
}
prevPos = i + 1;
} else if (i === len - 1) {
if (pad + (len - prevBreak) * charWidth >= width) {
height += lineHeight;
}
}
}
return height;
}

View File

@ -1,65 +0,0 @@
export default function wrapMessages(messages, width, charWidth, indent = 0) {
return messages.withMutations(m => {
for (let j = 0, llen = messages.size; j < llen; j++) {
const message = messages.get(j);
let lineWidth = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
if (lineWidth + message.message.length * charWidth < width) {
m.setIn([j, 'lines'], [message.message]);
continue;
}
const words = message.message.split(' ');
const wrapped = [];
let line = '';
let wordCount = 0;
let hasWrapped = false;
// Add empty line if first word after timestamp + sender wraps
if (words.length > 0 && message.from && lineWidth + words[0].length * charWidth >= width) {
wrapped.push(line);
lineWidth = 0;
}
for (let i = 0, wlen = words.length; i < wlen; i++) {
const word = words[i];
if (hasWrapped) {
hasWrapped = false;
lineWidth += indent;
}
lineWidth += word.length * charWidth;
wordCount++;
if (lineWidth >= width) {
if (wordCount !== 1) {
wrapped.push(line);
if (i !== wlen - 1) {
line = `${word} `;
lineWidth = (word.length + 1) * charWidth;
wordCount = 1;
} else {
wrapped.push(word);
}
} else {
wrapped.push(word);
lineWidth = 0;
wordCount = 0;
}
hasWrapped = true;
} else if (i !== wlen - 1) {
line += `${word} `;
lineWidth += charWidth;
} else {
line += word;
wrapped.push(line);
}
}
m.setIn([j, 'lines'], wrapped);
}
});
}

View File

@ -18,7 +18,8 @@ module.exports = {
], ],
loaders: [ loaders: [
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }, { test: /\.js$/, loader: 'babel', exclude: /node_modules/ },
{ test: /\.css$/, loader: 'style!css' } { test: /\.css$/, loader: 'style!css' },
{ test: /node_modules/, loader: 'ify' }
] ]
}, },
plugins: [ plugins: [

View File

@ -16,7 +16,8 @@ module.exports = {
], ],
loaders: [ loaders: [
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }, { test: /\.js$/, loader: 'babel', exclude: /node_modules/ },
{ test: /\.css$/, loader: 'style!css' } { test: /\.css$/, loader: 'style!css' },
{ test: /node_modules/, loader: 'ify' }
] ]
}, },
plugins: [ plugins: [