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": "",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
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}
|
||||||
|
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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) {
|
||||||
|
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: [
|
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: [
|
||||||
|
@ -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: [
|
||||||
|
Loading…
Reference in New Issue
Block a user