Improve message rendering
This commit is contained in:
parent
ede8e19722
commit
072daa64f2
File diff suppressed because one or more lines are too long
@ -4,59 +4,59 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.4.5",
|
||||
"babel-core": "^6.5.2",
|
||||
"babel-eslint": "^5.0.0-beta6",
|
||||
"babel-loader": "^6.2.2",
|
||||
"babel-plugin-react-transform": "^2.0.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.4.0",
|
||||
"babel-plugin-transform-react-inline-elements": "^6.4.0",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"babel-preset-stage-0": "^6.3.13",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.5.0",
|
||||
"babel-plugin-transform-react-inline-elements": "^6.5.0",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"css-loader": "^0.23.1",
|
||||
"eslint": "^1.10.3",
|
||||
"eslint-config-airbnb": "^5.0.0",
|
||||
"eslint-config-airbnb": "^5.0.1",
|
||||
"eslint-loader": "^1.2.1",
|
||||
"eslint-plugin-react": "^3.16.1",
|
||||
"express": "^4.13.4",
|
||||
"express-http-proxy": "^0.6.0",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "3.1.0",
|
||||
"gulp-cached": "^1.1.0",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
"gulp-cssnano": "^2.1.1",
|
||||
"gulp-gzip": "1.2.0",
|
||||
"gulp-htmlmin": "^1.3.0",
|
||||
"gulp-util": "^3.0.7",
|
||||
"ify-loader": "^1.0.3",
|
||||
"react-transform-catch-errors": "^1.0.2",
|
||||
"react-transform-hmr": "^1.0.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-log-monitor": "^1.0.4",
|
||||
"style-loader": "^0.13.0",
|
||||
"webpack": "^1.12.13",
|
||||
"webpack-dev-middleware": "^1.5.1",
|
||||
"webpack-hot-middleware": "^2.6.4"
|
||||
"webpack-hot-middleware": "^2.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"autolinker": "^0.22.0",
|
||||
"autolinker": "^0.23.0",
|
||||
"backo": "^1.1.0",
|
||||
"base64-arraybuffer": "^0.1.5",
|
||||
"eventemitter2": "^0.4.14",
|
||||
"history": "^2.0.0",
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.2.1",
|
||||
"lodash": "^4.3.0",
|
||||
"pure-render-decorator": "^0.2.0",
|
||||
"react": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-infinite": "0.8.0",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^2.0.0-rc5",
|
||||
"react-router": "^2.0.0",
|
||||
"react-router-redux": "^3.0.0",
|
||||
"react-virtualized": "^4.8.1",
|
||||
"redux": "^3.3.0",
|
||||
"react-virtualized": "^4.10.0",
|
||||
"redux": "^3.3.1",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"reselect": "^2.0.3"
|
||||
}
|
||||
|
@ -396,19 +396,19 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
|
||||
bottom: 50px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-channel .messagebox {
|
||||
right: 200px;
|
||||
}
|
||||
|
||||
.messagebox-scrollable {
|
||||
padding: 10px 15px;
|
||||
overflow-y: auto !important;
|
||||
.VirtualScroll {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.message {
|
||||
white-space: nowrap;
|
||||
padding: 3px 15px;
|
||||
}
|
||||
|
||||
.message-info {
|
||||
|
@ -43,5 +43,6 @@ export const TAB_HISTORY_POP = 'TAB_HISTORY_POP';
|
||||
export const TOGGLE_MENU = 'TOGGLE_MENU';
|
||||
export const TOGGLE_SEARCH = 'TOGGLE_SEARCH';
|
||||
export const TOGGLE_USERLIST = 'TOGGLE_USERLIST';
|
||||
export const UPDATE_MESSAGE_HEIGHT = 'UPDATE_MESSAGE_HEIGHT';
|
||||
export const UPLOAD_CERT = 'UPLOAD_CERT';
|
||||
export const WHOIS = 'WHOIS';
|
||||
|
@ -1,9 +1,42 @@
|
||||
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) {
|
||||
return (dispatch, getState) => dispatch({
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
dispatch(initMessage({
|
||||
type: actions.SEND_MESSAGE,
|
||||
from: getState().servers.getIn([server, 'nick']),
|
||||
from: state.servers.getIn([server, 'nick']),
|
||||
message,
|
||||
to,
|
||||
server,
|
||||
@ -12,25 +45,31 @@ export function sendMessage(message, to, server) {
|
||||
type: 'chat',
|
||||
data: { message, to, server }
|
||||
}
|
||||
});
|
||||
}, state));
|
||||
};
|
||||
}
|
||||
|
||||
export function addMessage(message) {
|
||||
message.time = new Date();
|
||||
|
||||
return {
|
||||
return (dispatch, getState) => dispatch({
|
||||
type: actions.ADD_MESSAGE,
|
||||
message
|
||||
};
|
||||
message: initMessage(message, getState())
|
||||
});
|
||||
}
|
||||
|
||||
export function addMessages(messages) {
|
||||
const now = new Date();
|
||||
messages.forEach(message => message.time = now);
|
||||
|
||||
return {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
messages.forEach(message => initMessage(message, state).time = now);
|
||||
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGES,
|
||||
messages
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import Autolinker from 'autolinker';
|
||||
import pure from 'pure-render-decorator';
|
||||
import { timestamp } from '../util';
|
||||
|
||||
export default class MessageHeader extends Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.message.lines[0] !== this.props.message.lines[0];
|
||||
}
|
||||
|
||||
@pure
|
||||
export default class Message extends Component {
|
||||
handleSenderClick = () => {
|
||||
const { message, openPrivateChat, select } = this.props;
|
||||
|
||||
@ -16,7 +14,7 @@ export default class MessageHeader extends Component {
|
||||
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
const line = Autolinker.link(message.lines[0], { stripPrefix: false });
|
||||
const content = Autolinker.link(message.message, { stripPrefix: false });
|
||||
const classes = ['message'];
|
||||
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 (
|
||||
<p className={classes.join(' ')}>
|
||||
<p className={classes.join(' ')} style={style}>
|
||||
<span className="message-time">{timestamp(message.time)}</span>
|
||||
{sender}
|
||||
<span dangerouslySetInnerHTML={{ __html: ` ${line}` }}></span>
|
||||
<span dangerouslySetInnerHTML={{ __html: ` ${content}` }}></span>
|
||||
</p>
|
||||
);
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import Infinite from 'react-infinite';
|
||||
import { VirtualScroll } from 'react-virtualized';
|
||||
import pure from 'pure-render-decorator';
|
||||
import MessageHeader from './MessageHeader';
|
||||
import MessageLine from './MessageLine';
|
||||
import Message from './Message';
|
||||
|
||||
@pure
|
||||
export default class MessageBox extends Component {
|
||||
@ -15,16 +14,22 @@ export default class MessageBox extends Component {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
const el = this.refs.list.refs.scrollable;
|
||||
componentWillReceiveProps() {
|
||||
const el = this.refs.list.refs.scrollingContainer;
|
||||
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
if (nextProps.messages !== this.props.messages) {
|
||||
this.refs.list.recomputeRowHeights();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
setTimeout(this.updateWidth, 0);
|
||||
this.updateWidth();
|
||||
|
||||
if (this.autoScroll) {
|
||||
const el = this.refs.list.refs.scrollable;
|
||||
const el = this.refs.list.refs.scrollingContainer;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}
|
||||
@ -33,57 +38,71 @@ export default class MessageBox extends Component {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
updateWidth = () => {
|
||||
const { setWrapWidth } = this.props;
|
||||
getRowHeight = index => {
|
||||
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;
|
||||
if (list) {
|
||||
const width = list.refs.scrollable.offsetWidth - 30;
|
||||
let width = list.refs.scrollingContainer.clientWidth - 30;
|
||||
|
||||
if (isChannel) {
|
||||
width += 200;
|
||||
}
|
||||
|
||||
if (this.width !== width) {
|
||||
this.width = width;
|
||||
setWrapWidth(width);
|
||||
|
||||
if (resize) {
|
||||
updateMessageHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleResize = () => {
|
||||
this.updateWidth();
|
||||
this.updateWidth(true);
|
||||
this.setState({ height: window.innerHeight - 100 });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tab, messages, select, openPrivateChat } = this.props;
|
||||
const dest = tab.channel || tab.user || tab.server;
|
||||
const lines = [];
|
||||
renderMessage = index => {
|
||||
const { messages } = this.props;
|
||||
|
||||
messages.forEach((message, j) => {
|
||||
const key = message.server + dest + j;
|
||||
lines.push(
|
||||
<MessageHeader
|
||||
key={key}
|
||||
if (index === 0 || index === messages.size + 1) {
|
||||
return <span style={{ height: '7px' }}></span>;
|
||||
}
|
||||
|
||||
const { select, openPrivateChat } = this.props;
|
||||
const message = messages.get(index - 1);
|
||||
|
||||
return (
|
||||
<Message
|
||||
message={message}
|
||||
select={select}
|
||||
openPrivateChat={openPrivateChat}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
for (let i = 1; i < message.lines.length; i++) {
|
||||
lines.push(
|
||||
<MessageLine key={`${key}-${i}`} type={message.type} line={message.lines[i]} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="messagebox">
|
||||
<Infinite
|
||||
<VirtualScroll
|
||||
ref="list"
|
||||
className="messagebox-scrollable"
|
||||
containerHeight={this.state.height}
|
||||
elementHeight={24}
|
||||
displayBottomUpwards={false}
|
||||
>
|
||||
{lines}
|
||||
</Infinite>
|
||||
height={this.state.height}
|
||||
rowsCount={this.props.messages.size + 2}
|
||||
rowHeight={this.getRowHeight}
|
||||
rowRenderer={this.renderMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -13,6 +13,12 @@ export default class UserList extends Component {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
if (nextProps.users.size === this.props.users.size) {
|
||||
this.refs.list.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
@ -25,9 +31,7 @@ export default class UserList extends Component {
|
||||
return 24;
|
||||
};
|
||||
|
||||
handleResize = () => {
|
||||
this.setState({ height: window.innerHeight - 100 });
|
||||
};
|
||||
handleResize = () => this.setState({ height: window.innerHeight - 100 });
|
||||
|
||||
renderUser = index => {
|
||||
const { users } = this.props;
|
||||
@ -61,6 +65,7 @@ export default class UserList extends Component {
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<VirtualScroll
|
||||
ref="list"
|
||||
height={this.state.height}
|
||||
rowsCount={this.props.users.size + 2}
|
||||
rowHeight={this.getRowHeight}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
import { List, Map } from 'immutable';
|
||||
import pure from 'pure-render-decorator';
|
||||
import ChatTitle from '../components/ChatTitle';
|
||||
@ -13,10 +13,10 @@ import { part } from '../actions/channel';
|
||||
import { openPrivateChat, closePrivateChat } from '../actions/privateChat';
|
||||
import { searchMessages, toggleSearch } from '../actions/search';
|
||||
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 { setWrapWidth, setCharWidth } from '../actions/environment';
|
||||
import { stringWidth, wrapMessages } from '../util';
|
||||
import { stringWidth } from '../util';
|
||||
import { toggleUserList } from '../actions/ui';
|
||||
import * as inputHistoryActions from '../actions/inputHistory';
|
||||
|
||||
@ -64,7 +64,8 @@ class Chat extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tab, channel, search, history, dispatch } = this.props;
|
||||
const { title, tab, channel, search, history,
|
||||
messages, users, showUserList, inputActions } = this.props;
|
||||
|
||||
let chatClass;
|
||||
if (tab.channel) {
|
||||
@ -77,21 +78,43 @@ class Chat extends Component {
|
||||
|
||||
return (
|
||||
<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}
|
||||
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
|
||||
tab={tab}
|
||||
channel={channel}
|
||||
history={history}
|
||||
runCommand={this.props.runCommand}
|
||||
sendMessage={this.props.sendMessage}
|
||||
history={history}
|
||||
{...bindActionCreators(inputHistoryActions, dispatch)}
|
||||
{...inputActions}
|
||||
/>
|
||||
<UserList
|
||||
users={users}
|
||||
tab={tab}
|
||||
showUserList={showUserList}
|
||||
select={this.props.select}
|
||||
openPrivateChat={this.props.openPrivateChat}
|
||||
/>
|
||||
<UserList {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -99,6 +122,12 @@ class Chat extends Component {
|
||||
|
||||
const tabSelector = state => state.tab.selected;
|
||||
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(
|
||||
tabSelector,
|
||||
@ -106,40 +135,33 @@ const selectedMessagesSelector = createSelector(
|
||||
(tab, messages) => messages.getIn([tab.server, tab.channel || tab.user || tab.server], List())
|
||||
);
|
||||
|
||||
const wrapWidthSelector = state => state.environment.get('wrapWidth');
|
||||
const charWidthSelector = state => state.environment.get('charWidth');
|
||||
|
||||
const wrappedMessagesSelector = createSelector(
|
||||
selectedMessagesSelector,
|
||||
wrapWidthSelector,
|
||||
charWidthSelector,
|
||||
(messages, width, charWidth) => wrapMessages(messages, width, charWidth, 6 * charWidth)
|
||||
const selectedChannelSelector = createSelector(
|
||||
tabSelector,
|
||||
channelSelector,
|
||||
(tab, channels) => channels.getIn([tab.server, tab.channel], Map())
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const tab = state.tab.selected;
|
||||
const channel = state.channels.getIn([tab.server, tab.channel], Map());
|
||||
const usersSelector = createSelector(
|
||||
selectedChannelSelector,
|
||||
channel => channel.get('users', List())
|
||||
);
|
||||
|
||||
let title;
|
||||
if (tab.channel) {
|
||||
title = tab.channel;
|
||||
} else if (tab.user) {
|
||||
title = tab.user;
|
||||
} else {
|
||||
title = state.servers.getIn([tab.server, 'name']);
|
||||
}
|
||||
const titleSelector = createSelector(
|
||||
tabSelector,
|
||||
serverSelector,
|
||||
(tab, servers) => tab.channel || tab.user || servers.getIn([tab.server, 'name'])
|
||||
);
|
||||
|
||||
return {
|
||||
title,
|
||||
search: state.search,
|
||||
users: channel.get('users', List()),
|
||||
history: state.input.index === -1 ? null : state.input.history.get(state.input.index),
|
||||
messages: wrappedMessagesSelector(state),
|
||||
showUserList: state.ui.showUserList,
|
||||
channel,
|
||||
tab
|
||||
};
|
||||
}
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
title: titleSelector,
|
||||
tab: tabSelector,
|
||||
channel: selectedChannelSelector,
|
||||
messages: selectedMessagesSelector,
|
||||
users: usersSelector,
|
||||
showUserList: showUserListSelector,
|
||||
search: searchSelector,
|
||||
history: historySelector
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
@ -155,8 +177,10 @@ function mapDispatchToProps(dispatch) {
|
||||
disconnect,
|
||||
openPrivateChat,
|
||||
closePrivateChat,
|
||||
setWrapWidth
|
||||
}, dispatch)
|
||||
setWrapWidth,
|
||||
updateMessageHeight
|
||||
}, dispatch),
|
||||
inputActions: bindActionCreators(inputHistoryActions, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { List, Map, Record } from 'immutable';
|
||||
import createReducer from '../util/createReducer';
|
||||
import { messageHeight } from '../util';
|
||||
import * as actions from '../actions';
|
||||
|
||||
const Message = Record({
|
||||
@ -10,15 +11,11 @@ const Message = Record({
|
||||
message: '',
|
||||
time: null,
|
||||
type: null,
|
||||
lines: []
|
||||
height: 0,
|
||||
channel: false
|
||||
});
|
||||
|
||||
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) {
|
||||
const from = message.from;
|
||||
message.from = null;
|
||||
@ -26,7 +23,8 @@ function addMessage(state, message) {
|
||||
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(), {
|
||||
@ -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) {
|
||||
return state.delete(action.server);
|
||||
},
|
||||
@ -64,5 +54,17 @@ export default createReducer(Map(), {
|
||||
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))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import padStart from 'lodash/padStart';
|
||||
|
||||
export wrapMessages from './wrapMessages';
|
||||
export messageHeight from './messageHeight';
|
||||
|
||||
export function normalizeChannel(channel) {
|
||||
if (channel.indexOf('#') !== 0) {
|
||||
|
39
client/src/js/util/messageHeight.js
Normal file
39
client/src/js/util/messageHeight.js
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
@ -18,7 +18,8 @@ module.exports = {
|
||||
],
|
||||
loaders: [
|
||||
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ },
|
||||
{ test: /\.css$/, loader: 'style!css' }
|
||||
{ test: /\.css$/, loader: 'style!css' },
|
||||
{ test: /node_modules/, loader: 'ify' }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
|
@ -16,7 +16,8 @@ module.exports = {
|
||||
],
|
||||
loaders: [
|
||||
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ },
|
||||
{ test: /\.css$/, loader: 'style!css' }
|
||||
{ test: /\.css$/, loader: 'style!css' },
|
||||
{ test: /node_modules/, loader: 'ify' }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
|
Loading…
Reference in New Issue
Block a user