Add prettier

This commit is contained in:
Ken-Håvard Lieng 2018-04-06 01:46:22 +02:00
parent 0cbbc1b8ff
commit b176b79144
46 changed files with 832 additions and 544 deletions

View file

@ -4,7 +4,10 @@ import { sendMessage, raw } from 'state/messages';
import { setNick, disconnect, whois, away } from 'state/servers';
import { select } from 'state/tab';
import { find } from 'utils';
import createCommandMiddleware, { beforeHandler, notFoundHandler } from './middleware/command';
import createCommandMiddleware, {
beforeHandler,
notFoundHandler
} from './middleware/command';
const help = [
'/join <channel> - Join a channel',
@ -26,7 +29,8 @@ const help = [
const text = content => ({ content });
const error = content => ({ content, type: 'error' });
const prompt = content => ({ content, type: 'prompt' });
const findHelp = cmd => find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
const findHelp = cmd =>
find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
export default createCommandMiddleware(COMMAND, {
join({ dispatch, server }, channel) {
@ -160,10 +164,7 @@ export default createCommandMiddleware(COMMAND, {
dispatch(raw(cmd, server));
return prompt(`=> ${cmd}`);
}
return [
prompt('=> /raw'),
error('Missing message')
];
return [prompt('=> /raw'), error('Missing message')];
},
help(_, ...commands) {

View file

@ -14,14 +14,26 @@ export default class App extends Component {
};
render() {
const { connected, tab, channels, servers,
privateChats, showTabList, select, push } = this.props;
const mainClass = showTabList ? 'main-container off-canvas' : 'main-container';
const {
connected,
tab,
channels,
servers,
privateChats,
showTabList,
select,
push
} = this.props;
const mainClass = showTabList
? 'main-container off-canvas'
: 'main-container';
return (
<div className="wrap">
{!connected &&
<div className="app-info">Connection lost, attempting to reconnect...</div>
}
{!connected && (
<div className="app-info">
Connection lost, attempting to reconnect...
</div>
)}
<div className="app-container" onClick={this.handleClick}>
<TabList
tab={tab}
@ -33,9 +45,15 @@ export default class App extends Component {
push={push}
/>
<div className={mainClass}>
<Route name="chat"><Chat /></Route>
<Route name="connect"><Connect /></Route>
<Route name="settings"><Settings /></Route>
<Route name="chat">
<Chat />
</Route>
<Route name="connect">
<Connect />
</Route>
<Route name="settings">
<Settings />
</Route>
</div>
</div>
</div>

View file

@ -24,36 +24,48 @@ export default class TabList extends PureComponent {
/>
);
server.forEach((channel, name) => tabs.push(
<TabListItem
key={address + name}
server={address}
target={name}
content={name}
selected={tab.server === address && tab.name === name}
onClick={this.handleTabClick}
/>
));
if (privateChats.has(address) && privateChats.get(address).size > 0) {
tabs.push(<div key={`${address}-pm}`} className="tab-label">Private messages</div>);
privateChats.get(address).forEach(nick => tabs.push(
server.forEach((channel, name) =>
tabs.push(
<TabListItem
key={address + nick}
key={address + name}
server={address}
target={nick}
content={nick}
selected={tab.server === address && tab.name === nick}
target={name}
content={name}
selected={tab.server === address && tab.name === name}
onClick={this.handleTabClick}
/>
));
)
);
if (privateChats.has(address) && privateChats.get(address).size > 0) {
tabs.push(
<div key={`${address}-pm}`} className="tab-label">
Private messages
</div>
);
privateChats
.get(address)
.forEach(nick =>
tabs.push(
<TabListItem
key={address + nick}
server={address}
target={nick}
content={nick}
selected={tab.server === address && tab.name === nick}
onClick={this.handleTabClick}
/>
)
);
}
});
return (
<div className={className}>
<button className="button-connect" onClick={this.handleConnectClick}>Connect</button>
<button className="button-connect" onClick={this.handleConnectClick}>
Connect
</button>
<div className="tab-container">{tabs}</div>
<div className="side-buttons">
<i className="icon-user" />

View file

@ -90,10 +90,7 @@ export default class Chat extends Component {
onToggleSearch={toggleSearch}
onToggleUserList={toggleUserList}
/>
<Search
search={search}
onSearch={this.handleSearch}
/>
<Search search={search} onSearch={this.handleSearch} />
<MessageBox
hasMoreMessages={hasMoreMessages}
messages={messages}

View file

@ -7,8 +7,16 @@ import { linkify } from 'utils';
export default class ChatTitle extends PureComponent {
render() {
const { status, title, tab, channel, onTitleChange,
onToggleSearch, onToggleUserList, onCloseClick } = this.props;
const {
status,
title,
tab,
channel,
onTitleChange,
onToggleSearch,
onToggleUserList,
onCloseClick
} = this.props;
let closeTitle;
if (tab.isChannel()) {
@ -21,7 +29,9 @@ export default class ChatTitle extends PureComponent {
let serverError = null;
if (!tab.name && status.error) {
serverError = <span className="chat-topic error">Error: {status.error}</span>;
serverError = (
<span className="chat-topic error">Error: {status.error}</span>
);
}
return (
@ -38,7 +48,9 @@ export default class ChatTitle extends PureComponent {
<span className="chat-title">{title}</span>
</Editable>
<div className="chat-topic-wrap">
<span className="chat-topic">{linkify(channel.get('topic')) || null}</span>
<span className="chat-topic">
{linkify(channel.get('topic')) || null}
</span>
{serverError}
</div>
<i className="icon-search" title="Search" onClick={onToggleSearch} />
@ -51,7 +63,9 @@ export default class ChatTitle extends PureComponent {
</div>
<div className="userlist-bar">
<i className="icon-user" />
<span className="chat-usercount">{channel.get('users', List()).size}</span>
<span className="chat-usercount">
{channel.get('users', List()).size}
</span>
</div>
</div>
);

View file

@ -5,7 +5,9 @@ export default class Message extends PureComponent {
render() {
const { message } = this.props;
const className = message.type ? `message message-${message.type}` : 'message';
const className = message.type
? `message message-${message.type}`
: 'message';
const style = {
paddingLeft: `${window.messageIndent + 15}px`,
textIndent: `-${window.messageIndent}px`,
@ -15,11 +17,13 @@ export default class Message extends PureComponent {
return (
<p className={className} style={style}>
<span className="message-time">{message.time}</span>
{message.from &&
{message.from && (
<span className="message-sender" onClick={this.handleNickClick}>
{' '}{message.from}
{' '}
{message.from}
</span>
}{' '}{message.content}
)}{' '}
{message.content}
</p>
);
}

View file

@ -33,7 +33,8 @@ export default class MessageBox extends PureComponent {
if (nextProps.messages.get(0) !== this.props.messages.get(0)) {
if (nextProps.tab === this.props.tab) {
const addedMessages = nextProps.messages.size - this.props.messages.size;
const addedMessages =
nextProps.messages.size - this.props.messages.size;
let addedHeight = 0;
for (let i = 0; i < addedMessages; i++) {
addedHeight += nextProps.messages.get(i).height;
@ -126,10 +127,12 @@ export default class MessageBox extends PureComponent {
handleScroll = ({ scrollTop, clientHeight, scrollHeight }) => {
if (this.mounted) {
if (!this.loading &&
if (
!this.loading &&
this.props.hasMoreMessages &&
scrollTop <= fetchThreshold &&
scrollTop < this.prevScrollTop) {
scrollTop < this.prevScrollTop
) {
this.fetchMore();
}
@ -165,11 +168,7 @@ export default class MessageBox extends PureComponent {
if (index === 0) {
if (this.props.hasMoreMessages) {
return (
<div
key="top"
className="messagebox-top-indicator"
style={style}
>
<div key="top" className="messagebox-top-indicator" style={style}>
Loading messages...
</div>
);

View file

@ -7,8 +7,16 @@ export default class MessageInput extends PureComponent {
};
handleKey = e => {
const { tab, onCommand, onMessage,
add, reset, increment, decrement, currentHistoryEntry } = this.props;
const {
tab,
onCommand,
onMessage,
add,
reset,
increment,
decrement,
currentHistoryEntry
} = this.props;
if (e.key === 'Enter' && e.target.value) {
if (e.target.value[0] === '/') {
@ -36,7 +44,12 @@ export default class MessageInput extends PureComponent {
};
render() {
const { nick, currentHistoryEntry, onNickChange, onNickEditDone } = this.props;
const {
nick,
currentHistoryEntry,
onNickChange,
onNickEditDone
} = this.props;
return (
<div className="message-input-wrap">
<Editable

View file

@ -8,7 +8,9 @@ export default class Search extends PureComponent {
}
}
inputRef = el => { this.input = el; };
inputRef = el => {
this.input = el;
};
handleSearch = e => this.props.onSearch(e.target.value);

View file

@ -11,12 +11,14 @@ export default class SearchResult extends PureComponent {
return (
<p className="search-result" style={style}>
<span className="message-time">{timestamp(new Date(result.time * 1000))}</span>
<span className="message-time">
{timestamp(new Date(result.time * 1000))}
</span>
<span>
{' '}
<span className="message-sender">{result.from}</span>
</span>
<span>{' '}{linkify(result.content)}</span>
<span> {linkify(result.content)}</span>
</p>
);
}

View file

@ -10,7 +10,9 @@ export default class UserList extends PureComponent {
}
}
listRef = el => { this.list = el; };
listRef = el => {
this.list = el;
};
renderUser = ({ index, style, key }) => {
const { users, onNickClick } = this.props;

View file

@ -14,7 +14,10 @@ export default class Connect extends Component {
let address = e.target.address.value.trim();
const nick = e.target.nick.value.trim();
const channels = e.target.channels.value.split(',').map(s => s.trim()).filter(s => s);
const channels = e.target.channels.value
.split(',')
.map(s => s.trim())
.filter(s => s);
const opts = {
name: e.target.name.value.trim(),
tls: e.target.ssl.checked
@ -78,18 +81,32 @@ export default class Connect extends Component {
<Navicon />
<form className="connect-form" onSubmit={this.handleSubmit}>
<h1>Connect</h1>
<input name="name" type="text" placeholder="Name" defaultValue={defaults.name} />
<input name="address" type="text" placeholder="Address" defaultValue={defaults.address} />
<input
name="name"
type="text"
placeholder="Name"
defaultValue={defaults.name}
/>
<input
name="address"
type="text"
placeholder="Address"
defaultValue={defaults.address}
/>
<input name="nick" type="text" placeholder="Nick" />
<input
name="channels"
type="text"
placeholder="Channels"
defaultValue={defaults.channels ? defaults.channels.join(',') : null}
defaultValue={
defaults.channels ? defaults.channels.join(',') : null
}
/>
{optionals}
<p>
<label htmlFor="ssl"><input name="ssl" type="checkbox" defaultChecked={defaults.ssl} />SSL</label>
<label htmlFor="ssl">
<input name="ssl" type="checkbox" defaultChecked={defaults.ssl} />SSL
</label>
<i className="icon-ellipsis" onClick={this.handleShowClick} />
</p>
<input type="submit" value="Connect" />

View file

@ -26,7 +26,7 @@ const Settings = ({ settings, onCertChange, onKeyChange, uploadCert }) => {
/>
</div>
<button onClick={uploadCert}>{status}</button>
{ error ? <p className="error">{error}</p> : null }
{error ? <p className="error">{error}</p> : null}
</div>
);
};

View file

@ -30,7 +30,8 @@ export default class Editable extends PureComponent {
getInputWidth(value) {
if (this.input) {
const style = window.getComputedStyle(this.input);
const padding = parseInt(style.paddingLeft, 10) + parseInt(style.paddingRight, 10);
const padding =
parseInt(style.paddingLeft, 10) + parseInt(style.paddingRight, 10);
// Make sure the width is atleast 1px so the caret always shows
const width = stringWidth(value, style.font) || 1;
return padding + width;
@ -68,7 +69,9 @@ export default class Editable extends PureComponent {
}
};
inputRef = el => { this.input = el; }
inputRef = el => {
this.input = el;
};
render() {
const { children, className, value } = this.props;
@ -77,21 +80,21 @@ export default class Editable extends PureComponent {
width: this.state.width
};
return (
this.state.editing ?
<input
autoFocus
ref={this.inputRef}
className={className}
type="text"
value={value}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleKey}
style={style}
spellCheck={false}
/> :
<div onClick={this.startEditing}>{children}</div>
return this.state.editing ? (
<input
autoFocus
ref={this.inputRef}
className={className}
type="text"
value={value}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleKey}
style={style}
spellCheck={false}
/>
) : (
<div onClick={this.startEditing}>{children}</div>
);
}
}

View file

@ -21,7 +21,9 @@ export default class FileInput extends PureComponent {
render() {
return (
<button className="input-file" onClick={this.handleClick}>{this.props.name}</button>
<button className="input-file" onClick={this.handleClick}>
{this.props.name}
</button>
);
}
}

View file

@ -3,14 +3,35 @@ import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import Chat from 'components/pages/Chat';
import { getSelectedTabTitle } from 'state';
import { getSelectedChannel, getSelectedChannelUsers, part } from 'state/channels';
import { getCurrentInputHistoryEntry, addInputHistory, resetInputHistory,
incrementInputHistory, decrementInputHistory } from 'state/input';
import { getSelectedMessages, getHasMoreMessages,
runCommand, sendMessage, fetchMessages, addFetchedMessages } from 'state/messages';
import {
getSelectedChannel,
getSelectedChannelUsers,
part
} from 'state/channels';
import {
getCurrentInputHistoryEntry,
addInputHistory,
resetInputHistory,
incrementInputHistory,
decrementInputHistory
} from 'state/input';
import {
getSelectedMessages,
getHasMoreMessages,
runCommand,
sendMessage,
fetchMessages,
addFetchedMessages
} from 'state/messages';
import { openPrivateChat, closePrivateChat } from 'state/privateChats';
import { getSearch, searchMessages, toggleSearch } from 'state/search';
import { getCurrentNick, getCurrentServerStatus, disconnect, setNick, setServerName } from 'state/servers';
import {
getCurrentNick,
getCurrentServerStatus,
disconnect,
setNick,
setServerName
} from 'state/servers';
import { getSelectedTab, select } from 'state/tab';
import { getShowUserList, toggleUserList } from 'state/ui';
@ -29,29 +50,35 @@ const mapState = createStructuredSelector({
});
const mapDispatch = dispatch => ({
...bindActionCreators({
addFetchedMessages,
closePrivateChat,
disconnect,
fetchMessages,
openPrivateChat,
part,
runCommand,
searchMessages,
select,
sendMessage,
setNick,
setServerName,
toggleSearch,
toggleUserList
}, dispatch),
...bindActionCreators(
{
addFetchedMessages,
closePrivateChat,
disconnect,
fetchMessages,
openPrivateChat,
part,
runCommand,
searchMessages,
select,
sendMessage,
setNick,
setServerName,
toggleSearch,
toggleUserList
},
dispatch
),
inputActions: bindActionCreators({
add: addInputHistory,
reset: resetInputHistory,
increment: incrementInputHistory,
decrement: decrementInputHistory
}, dispatch)
inputActions: bindActionCreators(
{
add: addInputHistory,
reset: resetInputHistory,
increment: incrementInputHistory,
decrement: decrementInputHistory
},
dispatch
)
});
export default connect(mapState, mapDispatch)(Chat);

View file

@ -10,7 +10,9 @@ import routes from './routes';
import runModules from './modules';
const production = process.env.NODE_ENV === 'production';
const host = production ? window.location.host : `${window.location.hostname}:1337`;
const host = production
? window.location.host
: `${window.location.hostname}:1337`;
const socket = new Socket(host);
const store = configureStore(socket);

View file

@ -1,6 +1,12 @@
import { socketAction } from 'state/actions';
import { setConnected } from 'state/app';
import { broadcast, inform, print, addMessage, addMessages } from 'state/messages';
import {
broadcast,
inform,
print,
addMessage,
addMessages
} from 'state/messages';
import { reconnect } from 'state/servers';
import { select } from 'state/tab';
import { normalizeChannel } from 'utils';
@ -21,7 +27,10 @@ function findChannels(state, server, user) {
return channels;
}
export default function handleSocket({ socket, store: { dispatch, getState } }) {
export default function handleSocket({
socket,
store: { dispatch, getState }
}) {
const handlers = {
message(message) {
dispatch(addMessage(message, message.server, message.to));
@ -41,10 +50,12 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
const [joinedChannel] = channels;
if (tab.server && tab.name) {
const { nick } = state.servers.get(tab.server);
if (tab.server === server &&
if (
tab.server === server &&
nick === user &&
tab.name !== joinedChannel &&
normalizeChannel(tab.name) === normalizeChannel(joinedChannel)) {
normalizeChannel(tab.name) === normalizeChannel(joinedChannel)
) {
dispatch(select(server, joinedChannel));
}
}
@ -53,7 +64,9 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
},
part({ user, server, channel, reason }) {
dispatch(inform(withReason(`${user} left the channel`, reason), server, channel));
dispatch(
inform(withReason(`${user} left the channel`, reason), server, channel)
);
},
quit({ user, server, reason }) {
@ -63,7 +76,9 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
nick({ server, oldNick, newNick }) {
const channels = findChannels(getState(), server, oldNick);
dispatch(broadcast(`${oldNick} changed nick to ${newNick}`, server, channels));
dispatch(
broadcast(`${oldNick} changed nick to ${newNick}`, server, channels)
);
},
topic({ server, channel, topic, nick }) {
@ -84,14 +99,20 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
whois(data) {
const tab = getState().tab.selected;
dispatch(print([
`Nick: ${data.nick}`,
`Username: ${data.username}`,
`Realname: ${data.realname}`,
`Host: ${data.host}`,
`Server: ${data.server}`,
`Channels: ${data.channels}`
], tab.server, tab.name));
dispatch(
print(
[
`Nick: ${data.nick}`,
`Username: ${data.username}`,
`Realname: ${data.realname}`,
`Host: ${data.host}`,
`Server: ${data.server}`,
`Channels: ${data.channels}`
],
tab.server,
tab.name
)
);
},
print(message) {
@ -100,11 +121,17 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
},
connection_update({ server, errorType }) {
if (errorType === 'verify' &&
confirm('The server is using a self-signed certificate, continue anyway?')) {
dispatch(reconnect(server, {
skipVerify: true
}));
if (
errorType === 'verify' &&
confirm(
'The server is using a self-signed certificate, continue anyway?'
)
) {
dispatch(
reconnect(server, {
skipVerify: true
})
);
}
},

View file

@ -1,4 +1,4 @@
import { connect, setServerName } from '../servers';
import { connect, setServerName } from '../servers';
describe('connect()', () => {
it('sets host and port correctly', () => {

View file

@ -1,13 +1,15 @@
import Immutable from 'immutable';
import reducer, { compareUsers } from '../channels';
import { connect } from '../servers';
import { connect } from '../servers';
import * as actions from '../actions';
describe('channel reducer', () => {
it('removes channels on PART', () => {
let state = Immutable.fromJS({
srv1: {
chan1: {}, chan2: {}, chan3: {}
chan1: {},
chan2: {},
chan3: {}
},
srv2: {
chan1: {}
@ -45,14 +47,10 @@ describe('channel reducer', () => {
expect(state.toJS()).toEqual({
srv: {
chan1: {
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' },
]
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
@ -64,9 +62,7 @@ describe('channel reducer', () => {
expect(state.toJS()).toEqual({
srv: {
chan1: {
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' }
]
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
}
}
});
@ -86,9 +82,7 @@ describe('channel reducer', () => {
expect(state.toJS()).toEqual({
srv: {
chan1: {
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' }
]
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
},
chan2: {
users: []
@ -118,9 +112,7 @@ describe('channel reducer', () => {
]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
@ -131,13 +123,7 @@ describe('channel reducer', () => {
type: actions.socket.USERS,
server: 'srv',
channel: 'chan1',
users: [
'user3',
'user2',
'@user4',
'user1',
'+user5'
]
users: ['user3', 'user2', '@user4', 'user1', '+user5']
});
expect(state.toJS()).toEqual({
@ -152,7 +138,7 @@ describe('channel reducer', () => {
]
}
}
})
});
});
it('handles SOCKET_TOPIC', () => {
@ -188,14 +174,12 @@ describe('channel reducer', () => {
]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
state = reducer(state, socket_mode('srv', 'chan1', 'nick1' ,'v', 'o'));
state = reducer(state, socket_mode('srv', 'chan1', 'nick1', 'v', 'o'));
state = reducer(state, socket_mode('srv', 'chan1', 'nick2', 'o', ''));
state = reducer(state, socket_mode('srv', 'chan2', 'not_there', 'x', ''));
@ -208,9 +192,7 @@ describe('channel reducer', () => {
]
},
chan2: {
users: [
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
}
}
});
@ -240,10 +222,7 @@ describe('channel reducer', () => {
it('handles SOCKET_SERVERS', () => {
const state = reducer(undefined, {
type: actions.socket.SERVERS,
data: [
{ host: '127.0.0.1' },
{ host: 'thehost' }
]
data: [{ host: '127.0.0.1' }, { host: 'thehost' }]
});
expect(state.toJS()).toEqual({
@ -280,7 +259,8 @@ describe('channel reducer', () => {
function socket_join(server, channel, user) {
return {
type: actions.socket.JOIN,
server, user,
server,
user,
channels: [channel]
};
}
@ -288,29 +268,35 @@ function socket_join(server, channel, user) {
function socket_mode(server, channel, user, add, remove) {
return {
type: actions.socket.MODE,
server, channel, user, add, remove
server,
channel,
user,
add,
remove
};
}
describe('compareUsers()', () => {
it('compares users correctly', () => {
expect([
{ renderName: 'user5' },
{ renderName: '@user2' },
{ renderName: 'user3' },
{ renderName: 'user2' },
{ renderName: '+user1' },
{ renderName: '~bob' },
{ renderName: '%apples' },
{ renderName: '&cake' }
].sort(compareUsers)).toEqual([
expect(
[
{ renderName: 'user5' },
{ renderName: '@user2' },
{ renderName: 'user3' },
{ renderName: 'user2' },
{ renderName: '+user1' },
{ renderName: '~bob' },
{ renderName: '%apples' },
{ renderName: '&cake' }
].sort(compareUsers)
).toEqual([
{ renderName: '~bob' },
{ renderName: '&cake' },
{ renderName: '@user2' },
{ renderName: '%apples' },
{ renderName: '+user1' },
{ renderName: '+user1' },
{ renderName: 'user2' },
{ renderName: 'user3' },
{ renderName: 'user3' },
{ renderName: 'user5' }
]);
});

View file

@ -17,10 +17,12 @@ describe('message reducer', () => {
expect(state.toJS()).toMatchObject({
srv: {
'#chan1': [{
from: 'foo',
content: 'msg'
}]
'#chan1': [
{
from: 'foo',
content: 'msg'
}
]
}
});
});
@ -34,10 +36,12 @@ describe('message reducer', () => {
{
from: 'foo',
content: 'msg'
}, {
},
{
from: 'bar',
content: 'msg'
}, {
},
{
tab: '#chan2',
from: 'foo',
content: 'msg'
@ -51,15 +55,18 @@ describe('message reducer', () => {
{
from: 'foo',
content: 'msg'
}, {
},
{
from: 'bar',
content: 'msg'
}
],
'#chan2': [{
from: 'foo',
content: 'msg'
}]
'#chan2': [
{
from: 'foo',
content: 'msg'
}
]
}
});
});
@ -92,10 +99,9 @@ describe('message reducer', () => {
};
const thunk = broadcast('test', 'srv', ['#chan1', '#chan3']);
thunk(
action => { state.messages = reducer(undefined, action); },
() => state
);
thunk(action => {
state.messages = reducer(undefined, action);
}, () => state);
const messages = state.messages.toJS();
@ -109,18 +115,11 @@ describe('message reducer', () => {
it('deletes all messages related to server when disconnecting', () => {
let state = fromJS({
srv: {
'#chan1': [
{ content: 'msg1' },
{ content: 'msg2' }
],
'#chan2': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
'#chan2': [{ content: 'msg' }]
},
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
@ -131,9 +130,7 @@ describe('message reducer', () => {
expect(state.toJS()).toEqual({
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
});
@ -141,18 +138,11 @@ describe('message reducer', () => {
it('deletes all messages related to channel when parting', () => {
let state = fromJS({
srv: {
'#chan1': [
{ content: 'msg1' },
{ content: 'msg2' }
],
'#chan2': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
'#chan2': [{ content: 'msg' }]
},
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
@ -164,14 +154,10 @@ describe('message reducer', () => {
expect(state.toJS()).toEqual({
srv: {
'#chan2': [
{ content: 'msg' }
]
'#chan2': [{ content: 'msg' }]
},
srv2: {
'#chan1': [
{ content: 'msg' }
]
'#chan1': [{ content: 'msg' }]
}
});
});

View file

@ -32,9 +32,12 @@ describe('server reducer', () => {
}
});
state = reducer(state, connect('127.0.0.2:1337', 'nick', {
name: 'srv'
}));
state = reducer(
state,
connect('127.0.0.2:1337', 'nick', {
name: 'srv'
})
);
expect(state.toJS()).toEqual({
'127.0.0.1': {
@ -190,7 +193,7 @@ describe('server reducer', () => {
status: {
connected: false
}
},
}
]
});

View file

@ -8,9 +8,7 @@ describe('tab reducer', () => {
expect(state.toJS()).toEqual({
selected: { server: 'srv', name: '#chan' },
history: [
{ server: 'srv', name: '#chan' }
]
history: [{ server: 'srv', name: '#chan' }]
});
state = reducer(state, setSelectedTab('srv', 'user1'));
@ -62,7 +60,7 @@ describe('tab reducer', () => {
history: [
{ server: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan3' }
{ server: 'srv', name: '#chan3' }
]
});
});
@ -75,14 +73,12 @@ describe('tab reducer', () => {
state = reducer(state, {
type: actions.DISCONNECT,
server: 'srv',
server: 'srv'
});
expect(state.toJS()).toEqual({
selected: { server: 'srv', name: '#chan3' },
history: [
{ server: 'srv1', name: 'bob' },
]
history: [{ server: 'srv1', name: 'bob' }]
});
});
@ -93,14 +89,13 @@ describe('tab reducer', () => {
expect(state.toJS()).toEqual({
selected: { server: null, name: null },
history: [
{ server: 'srv', name: '#chan' }
]
history: [{ server: 'srv', name: '#chan' }]
});
});
it('selects the tab and adds it to history when navigating to a tab', () => {
const state = reducer(undefined,
const state = reducer(
undefined,
locationChanged('chat', {
server: 'srv',
name: '#chan'
@ -109,9 +104,7 @@ describe('tab reducer', () => {
expect(state.toJS()).toEqual({
selected: { server: 'srv', name: '#chan' },
history: [
{ server: 'srv', name: '#chan' }
]
history: [{ server: 'srv', name: '#chan' }]
});
});
});

View file

@ -29,11 +29,13 @@ function updateRenderName(user) {
}
function createUser(nick, mode) {
return updateRenderName(new User({
nick,
renderName: nick,
mode: mode || ''
}));
return updateRenderName(
new User({
nick,
renderName: nick,
mode: mode || ''
})
);
}
function loadUser(nick) {
@ -80,13 +82,14 @@ export const getChannels = state => state.channels;
const key = (v, k) => k.toLowerCase();
export const getSortedChannels = createSelector(
getChannels,
channels => channels.withMutations(c =>
c.forEach((server, address) =>
c.update(address, chans => chans.sortBy(key))
export const getSortedChannels = createSelector(getChannels, channels =>
channels
.withMutations(c =>
c.forEach((server, address) =>
c.update(address, chans => chans.sortBy(key))
)
)
).sortBy(key)
.sortBy(key)
);
export const getSelectedChannel = createSelector(
@ -125,7 +128,9 @@ export default createReducer(Map(), {
[actions.socket.QUIT](state, { server, user }) {
return state.withMutations(s => {
s.get(server).forEach((v, channel) => {
s.updateIn([server, channel, 'users'], users => users.filter(u => u.nick !== user));
s.updateIn([server, channel, 'users'], users =>
users.filter(u => u.nick !== user)
);
});
});
},
@ -139,8 +144,8 @@ export default createReducer(Map(), {
return users;
}
return users.update(i,
user => updateRenderName(user.set('nick', newNick))
return users.update(i, user =>
updateRenderName(user.set('nick', newNick))
);
});
});
@ -148,7 +153,8 @@ export default createReducer(Map(), {
},
[actions.socket.USERS](state, { server, channel, users }) {
return state.setIn([server, channel, 'users'],
return state.setIn(
[server, channel, 'users'],
List(users.map(user => loadUser(user)))
);
},
@ -183,10 +189,13 @@ export default createReducer(Map(), {
return state.withMutations(s => {
data.forEach(channel => {
s.setIn([channel.server, channel.name], Map({
users: List(),
topic: channel.topic
}));
s.setIn(
[channel.server, channel.name],
Map({
users: List(),
topic: channel.topic
})
);
});
});
},

View file

@ -1,6 +1,12 @@
import { List, Map, Record } from 'immutable';
import { createSelector } from 'reselect';
import { findBreakpoints, messageHeight, linkify, timestamp, isChannel } from 'utils';
import {
findBreakpoints,
messageHeight,
linkify,
timestamp,
isChannel
} from 'utils';
import createReducer from 'utils/createReducer';
import { getApp } from './app';
import { getSelectedTab } from './tab';
@ -24,7 +30,8 @@ export const getMessages = state => state.messages;
export const getSelectedMessages = createSelector(
getSelectedTab,
getMessages,
(tab, messages) => messages.getIn([tab.server, tab.name || tab.server], List())
(tab, messages) =>
messages.getIn([tab.server, tab.name || tab.server], List())
);
export const getHasMoreMessages = createSelector(
@ -37,18 +44,24 @@ export const getHasMoreMessages = createSelector(
export default createReducer(Map(), {
[actions.ADD_MESSAGE](state, { server, tab, message }) {
return state.updateIn([server, tab], List(), list => list.push(new Message(message)));
return state.updateIn([server, tab], List(), list =>
list.push(new Message(message))
);
},
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) {
return state.withMutations(s => {
if (prepend) {
for (let i = messages.length - 1; i >= 0; i--) {
s.updateIn([server, tab], List(), list => list.unshift(new Message(messages[i])));
s.updateIn([server, tab], List(), list =>
list.unshift(new Message(messages[i]))
);
}
} else {
messages.forEach(message =>
s.updateIn([server, message.tab || tab], List(), list => list.push(new Message(message)))
s.updateIn([server, message.tab || tab], List(), list =>
list.push(new Message(message))
)
);
}
});
@ -60,18 +73,28 @@ export default createReducer(Map(), {
[actions.PART](state, { server, channels }) {
return state.withMutations(s =>
channels.forEach(channel =>
s.deleteIn([server, channel])
)
channels.forEach(channel => s.deleteIn([server, channel]))
);
},
[actions.UPDATE_MESSAGE_HEIGHT](state, { wrapWidth, charWidth, windowWidth }) {
[actions.UPDATE_MESSAGE_HEIGHT](
state,
{ wrapWidth, charWidth, windowWidth }
) {
return state.withMutations(s =>
s.forEach((server, serverKey) =>
server.forEach((target, targetKey) =>
target.forEach((message, index) => s.setIn([serverKey, targetKey, index, 'height'],
messageHeight(message, wrapWidth, charWidth, 6 * charWidth, windowWidth))
target.forEach((message, index) =>
s.setIn(
[serverKey, targetKey, index, 'height'],
messageHeight(
message,
wrapWidth,
charWidth,
6 * charWidth,
windowWidth
)
)
)
)
)
@ -111,7 +134,13 @@ function initMessage(message, tab, state) {
message.length = message.content.length;
message.breakpoints = findBreakpoints(message.content);
message.height = messageHeight(message, wrapWidth, charWidth, 6 * charWidth, windowWidth);
message.height = messageHeight(
message,
wrapWidth,
charWidth,
6 * charWidth,
windowWidth
);
message.content = linkify(message.content);
return message;
@ -175,10 +204,14 @@ export function sendMessage(content, to, server) {
type: actions.ADD_MESSAGE,
server,
tab: to,
message: initMessage({
from: state.servers.getIn([server, 'nick']),
content
}, to, state),
message: initMessage(
{
from: state.servers.getIn([server, 'nick']),
content
},
to,
state
),
socket: {
type: 'message',
data: { content, to, server }
@ -190,12 +223,13 @@ export function sendMessage(content, to, server) {
export function addMessage(message, server, to) {
const tab = getMessageTab(server, to);
return (dispatch, getState) => dispatch({
type: actions.ADD_MESSAGE,
server,
tab,
message: initMessage(message, tab, getState())
});
return (dispatch, getState) =>
dispatch({
type: actions.ADD_MESSAGE,
server,
tab,
message: initMessage(message, tab, getState())
});
}
export function addMessages(messages, server, to, prepend, next) {
@ -209,7 +243,9 @@ export function addMessages(messages, server, to, prepend, next) {
messages[0].next = true;
}
messages.forEach(message => initMessage(message, message.tab || tab, state));
messages.forEach(message =>
initMessage(message, message.tab || tab, state)
);
dispatch({
type: actions.ADD_MESSAGES,
@ -222,25 +258,36 @@ export function addMessages(messages, server, to, prepend, next) {
}
export function broadcast(message, server, channels) {
return addMessages(channels.map(channel => ({
tab: channel,
content: message,
type: 'info'
})), server);
return addMessages(
channels.map(channel => ({
tab: channel,
content: message,
type: 'info'
})),
server
);
}
export function print(message, server, channel, type) {
if (Array.isArray(message)) {
return addMessages(message.map(line => ({
content: line,
type
})), server, channel);
return addMessages(
message.map(line => ({
content: line,
type
})),
server,
channel
);
}
return addMessage({
content: message,
type
}, server, channel);
return addMessage(
{
content: message,
type
},
server,
channel
);
}
export function inform(message, server, channel) {

View file

@ -10,11 +10,12 @@ const lowerCaseValue = v => v.toLowerCase();
export const getSortedPrivateChats = createSelector(
getPrivateChats,
privateChats => privateChats.withMutations(p =>
p.forEach((server, address) =>
p.update(address, chats => chats.sortBy(lowerCaseValue))
privateChats =>
privateChats.withMutations(p =>
p.forEach((server, address) =>
p.update(address, chats => chats.sortBy(lowerCaseValue))
)
)
)
);
function open(state, server, nick) {

View file

@ -45,10 +45,13 @@ export const getCurrentServerStatus = createSelector(
export default createReducer(Map(), {
[actions.CONNECT](state, { host, nick, options }) {
if (!state.has(host)) {
return state.set(host, new Server({
nick,
name: options.name || host
}));
return state.set(
host,
new Server({
nick,
name: options.name || host
})
);
}
return state;
@ -73,9 +76,8 @@ export default createReducer(Map(), {
[actions.socket.NICK](state, { server, oldNick, newNick }) {
if (!oldNick || oldNick === state.get(server).nick) {
return state.update(server, s => s
.set('nick', newNick)
.set('editedNick', null)
return state.update(server, s =>
s.set('nick', newNick).set('editedNick', null)
);
}
return state;

View file

@ -40,19 +40,29 @@ export default createReducer(new State(), {
[actions.SELECT_TAB]: selectTab,
[actions.PART](state, action) {
return state.set('history', state.history.filter(tab =>
!(tab.server === action.server && tab.name === action.channels[0])
));
return state.set(
'history',
state.history.filter(
tab =>
!(tab.server === action.server && tab.name === action.channels[0])
)
);
},
[actions.CLOSE_PRIVATE_CHAT](state, action) {
return state.set('history', state.history.filter(tab =>
!(tab.server === action.server && tab.name === action.nick)
));
return state.set(
'history',
state.history.filter(
tab => !(tab.server === action.server && tab.name === action.nick)
)
);
},
[actions.DISCONNECT](state, action) {
return state.set('history', state.history.filter(tab => tab.server !== action.server));
return state.set(
'history',
state.history.filter(tab => tab.server !== action.server)
);
},
[LOCATION_CHANGED](state, action) {

View file

@ -7,20 +7,24 @@ import createSocketMiddleware from './middleware/socket';
import commands from './commands';
export default function configureStore(socket) {
// eslint-disable-next-line no-underscore-dangle
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
/* eslint-disable no-underscore-dangle */
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const reducer = createReducer(routeReducer);
const store = createStore(reducer, composeEnhancers(
applyMiddleware(
thunk,
routeMiddleware,
createSocketMiddleware(socket),
message,
commands
const store = createStore(
reducer,
composeEnhancers(
applyMiddleware(
thunk,
routeMiddleware,
createSocketMiddleware(socket),
message,
commands
)
)
));
);
return store;
}

View file

@ -50,7 +50,7 @@ export default class Socket {
this.retry();
};
this.ws.onmessage = (e) => {
this.ws.onmessage = e => {
this.setTimeoutPing();
const msg = JSON.parse(e.data);

View file

@ -2,55 +2,40 @@ import React from 'react';
import linkify from '../linkify';
describe('linkify()', () => {
const proto = href => href.indexOf('http') !== 0 ? `http://${href}` : href;
const linkTo = href => <a href={proto(href)} rel="noopener noreferrer" target="_blank">{href}</a>;
const proto = href => (href.indexOf('http') !== 0 ? `http://${href}` : href);
const linkTo = href => (
<a href={proto(href)} rel="noopener noreferrer" target="_blank">
{href}
</a>
);
it('returns the arg when no matches are found', () => [
null,
undefined,
10,
false,
true,
'just some text',
''
].forEach(input => expect(linkify(input)).toBe(input)));
it('returns the arg when no matches are found', () =>
[null, undefined, 10, false, true, 'just some text', ''].forEach(input =>
expect(linkify(input)).toBe(input)
));
it('linkifies text', () => Object.entries({
'google.com': linkTo('google.com'),
'google.com stuff': [
linkTo('google.com'),
' stuff'
],
'cake google.com stuff': [
'cake ',
linkTo('google.com'),
' stuff'
],
'cake google.com stuff https://google.com': [
'cake ',
linkTo('google.com'),
' stuff ',
linkTo('https://google.com')
],
'cake google.com stuff pie https://google.com ': [
'cake ',
linkTo('google.com'),
' stuff pie ',
linkTo('https://google.com'),
' '
],
' google.com': [
' ',
linkTo('google.com')
],
'google.com ': [
linkTo('google.com'),
' '
],
'/google.com?': [
'/',
linkTo('google.com'),
'?'
]
}).forEach(([ input, expected ]) => expect(linkify(input)).toEqual(expected)));
it('linkifies text', () =>
Object.entries({
'google.com': linkTo('google.com'),
'google.com stuff': [linkTo('google.com'), ' stuff'],
'cake google.com stuff': ['cake ', linkTo('google.com'), ' stuff'],
'cake google.com stuff https://google.com': [
'cake ',
linkTo('google.com'),
' stuff ',
linkTo('https://google.com')
],
'cake google.com stuff pie https://google.com ': [
'cake ',
linkTo('google.com'),
' stuff pie ',
linkTo('https://google.com'),
' '
],
' google.com': [' ', linkTo('google.com')],
'google.com ': [linkTo('google.com'), ' '],
'/google.com?': ['/', linkTo('google.com'), '?']
}).forEach(([input, expected]) =>
expect(linkify(input)).toEqual(expected)
));
});

View file

@ -8,7 +8,10 @@ export function normalizeChannel(channel) {
return channel;
}
return channel.split('#').join('').toLowerCase();
return channel
.split('#')
.join('')
.toLowerCase();
}
export function isChannel(name) {

View file

@ -30,12 +30,19 @@ export default function linkify(text) {
}
result.push(
<a target="_blank" rel="noopener noreferrer" href={match.getAnchorHref()}>
<a
target="_blank"
rel="noopener noreferrer"
href={match.getAnchorHref()}
>
{match.matchedText}
</a>
);
} else if (typeof result[result.length - 1] === 'string') {
result[result.length - 1] += text.slice(pos, match.offset + match.matchedText.length);
result[result.length - 1] += text.slice(
pos,
match.offset + match.matchedText.length
);
} else {
result.push(text.slice(pos, match.offset + match.matchedText.length));
}

View file

@ -18,7 +18,13 @@ export function findBreakpoints(text) {
return breakpoints;
}
export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowWidth) {
export function messageHeight(
message,
wrapWidth,
charWidth,
indent = 0,
windowWidth
) {
let pad = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
let height = lineHeight + 8;
@ -26,7 +32,7 @@ export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowW
wrapWidth -= userListWidth;
}
if (pad + (message.length * charWidth) < wrapWidth) {
if (pad + message.length * charWidth < wrapWidth) {
return height;
}
@ -35,7 +41,7 @@ export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowW
let prevPos = 0;
for (let i = 0; i < breaks.length; i++) {
if (pad + ((breaks[i].end - prevBreak) * charWidth) >= wrapWidth) {
if (pad + (breaks[i].end - prevBreak) * charWidth >= wrapWidth) {
prevBreak = prevPos;
pad = indent;
height += lineHeight;
@ -44,7 +50,7 @@ export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowW
prevPos = breaks[i].next;
}
if (pad + ((message.length - prevBreak) * charWidth) >= wrapWidth) {
if (pad + (message.length - prevBreak) * charWidth >= wrapWidth) {
height += lineHeight;
}

View file

@ -99,7 +99,10 @@ export default function initRouter(routes, store) {
history.listen(location => {
const nextMatch = match(patterns, location);
if (nextMatch && nextMatch.location.pathname !== matched.location.pathname) {
if (
nextMatch &&
nextMatch.location.pathname !== matched.location.pathname
) {
matched = nextMatch;
store.dispatch(matched);
}