Use immer
This commit is contained in:
parent
7f755d2a83
commit
4f72e164d7
File diff suppressed because one or more lines are too long
@ -8,7 +8,12 @@
|
||||
}
|
||||
],
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-stage-0"
|
||||
[
|
||||
"@babel/preset-stage-0",
|
||||
{
|
||||
"decoratorsLegacy": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
|
@ -8,12 +8,12 @@
|
||||
"ie 11"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0-beta.44",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.0.0-beta.44",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.0.0-beta.44",
|
||||
"@babel/preset-env": "^7.0.0-beta.44",
|
||||
"@babel/preset-react": "^7.0.0-beta.44",
|
||||
"@babel/preset-stage-0": "^7.0.0-beta.44",
|
||||
"@babel/core": "^7.0.0-beta.46",
|
||||
"@babel/plugin-transform-react-constant-elements": "^7.0.0-beta.46",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.0.0-beta.46",
|
||||
"@babel/preset-env": "^7.0.0-beta.46",
|
||||
"@babel/preset-react": "^7.0.0-beta.46",
|
||||
"@babel/preset-stage-0": "^7.0.0-beta.46",
|
||||
"autoprefixer": "^8.2.0",
|
||||
"babel-core": "^7.0.0-0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
@ -32,18 +32,18 @@
|
||||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"express": "^4.14.1",
|
||||
"express-http-proxy": "^1.0.1",
|
||||
"express-http-proxy": "^1.2.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-util": "^3.0.8",
|
||||
"jest": "^22.4.3",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"postcss-loader": "^2.1.3",
|
||||
"prettier": "1.12.1",
|
||||
"style-loader": "^0.20.3",
|
||||
"style-loader": "^0.21.0",
|
||||
"through2": "^2.0.3",
|
||||
"webpack": "^4.1.1",
|
||||
"webpack-dev-middleware": "^3.0.1",
|
||||
"webpack-hot-middleware": "^2.17.0"
|
||||
"webpack-hot-middleware": "^2.22.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"autolinker": "^1.4.3",
|
||||
@ -51,12 +51,12 @@
|
||||
"base64-arraybuffer": "^0.1.5",
|
||||
"fontfaceobserver": "^2.0.9",
|
||||
"history": "4.5.1",
|
||||
"immutable": "^3.8.1",
|
||||
"immer": "^1.2.1",
|
||||
"js-cookie": "^2.1.4",
|
||||
"lodash": "^4.17.4",
|
||||
"lodash": "^4.17.10",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-hot-loader": "^4.0.0",
|
||||
"react-hot-loader": "^4.1.2",
|
||||
"react-redux": "^5.0.2",
|
||||
"react-virtualized": "^9.3.0",
|
||||
"redux": "^4.0.0",
|
||||
@ -65,7 +65,7 @@
|
||||
"url-pattern": "^1.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"prettier": "prettier --write {.*,*.js,src/css/*.css}",
|
||||
"prettier": "prettier --write {.*,*.js,src/css/*.css,src/**/*.test.js}",
|
||||
"prettier:all": "prettier --write {.*,*.js,src/**/*.js,src/css/*.css}",
|
||||
"test": "jest",
|
||||
"test:verbose": "jest --verbose",
|
||||
|
@ -79,13 +79,13 @@ export default createCommandMiddleware(COMMAND, {
|
||||
topic({ dispatch, getState, server, channel }, ...newTopic) {
|
||||
if (newTopic.length > 0) {
|
||||
dispatch(setTopic(newTopic.join(' '), channel, server));
|
||||
} else {
|
||||
const topic = getState().channels.getIn([server, channel, 'topic']);
|
||||
} else if (channel) {
|
||||
const { topic } = getState().channels[server][channel];
|
||||
if (topic) {
|
||||
return text(topic);
|
||||
}
|
||||
return 'No topic set';
|
||||
}
|
||||
return 'No topic set';
|
||||
},
|
||||
|
||||
msg({ dispatch, server }, target, ...message) {
|
||||
|
@ -11,20 +11,21 @@ export default class TabList extends PureComponent {
|
||||
const className = showTabList ? 'tablist off-canvas' : 'tablist';
|
||||
const tabs = [];
|
||||
|
||||
channels.forEach((server, address) => {
|
||||
const srv = servers.get(address);
|
||||
channels.forEach(server => {
|
||||
const { address } = server;
|
||||
const srv = servers[address];
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address}
|
||||
server={address}
|
||||
content={srv.name}
|
||||
selected={tab.server === address && tab.name === null}
|
||||
selected={tab.server === address && !tab.name}
|
||||
connected={srv.status.connected}
|
||||
onClick={this.handleTabClick}
|
||||
/>
|
||||
);
|
||||
|
||||
server.forEach((channel, name) =>
|
||||
server.channels.forEach(name =>
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address + name}
|
||||
@ -37,27 +38,25 @@ export default class TabList extends PureComponent {
|
||||
)
|
||||
);
|
||||
|
||||
if (privateChats.has(address) && privateChats.get(address).size > 0) {
|
||||
if (privateChats[address] && privateChats[address].length > 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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
privateChats[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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { isChannel } from 'utils';
|
||||
import ChatTitle from './ChatTitle';
|
||||
import Search from './Search';
|
||||
import MessageBox from './MessageBox';
|
||||
@ -9,7 +10,7 @@ export default class Chat extends Component {
|
||||
handleCloseClick = () => {
|
||||
const { tab, part, closePrivateChat, disconnect } = this.props;
|
||||
|
||||
if (tab.isChannel()) {
|
||||
if (isChannel(tab)) {
|
||||
part([tab.name], tab.server);
|
||||
} else if (tab.name) {
|
||||
closePrivateChat(tab.server, tab.name);
|
||||
@ -20,7 +21,7 @@ export default class Chat extends Component {
|
||||
|
||||
handleSearch = phrase => {
|
||||
const { tab, searchMessages } = this.props;
|
||||
if (tab.isChannel()) {
|
||||
if (isChannel(tab)) {
|
||||
searchMessages(tab.server, tab.name, phrase);
|
||||
}
|
||||
};
|
||||
@ -70,7 +71,7 @@ export default class Chat extends Component {
|
||||
} = this.props;
|
||||
|
||||
let chatClass;
|
||||
if (tab.isChannel()) {
|
||||
if (isChannel(tab)) {
|
||||
chatClass = 'chat-channel';
|
||||
} else if (tab.name) {
|
||||
chatClass = 'chat-private';
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { List } from 'immutable';
|
||||
import Navicon from 'containers/Navicon';
|
||||
import Editable from 'components/ui/Editable';
|
||||
import { isValidServerName } from 'state/servers';
|
||||
import { linkify } from 'utils';
|
||||
import { isChannel, linkify } from 'utils';
|
||||
|
||||
export default class ChatTitle extends PureComponent {
|
||||
render() {
|
||||
@ -19,7 +18,7 @@ export default class ChatTitle extends PureComponent {
|
||||
} = this.props;
|
||||
|
||||
let closeTitle;
|
||||
if (tab.isChannel()) {
|
||||
if (isChannel(tab)) {
|
||||
closeTitle = 'Leave';
|
||||
} else if (tab.name) {
|
||||
closeTitle = 'Close';
|
||||
@ -49,7 +48,7 @@ export default class ChatTitle extends PureComponent {
|
||||
</Editable>
|
||||
<div className="chat-topic-wrap">
|
||||
<span className="chat-topic">
|
||||
{linkify(channel.get('topic')) || null}
|
||||
{channel && linkify(channel.topic)}
|
||||
</span>
|
||||
{serverError}
|
||||
</div>
|
||||
@ -64,7 +63,7 @@ export default class ChatTitle extends PureComponent {
|
||||
<div className="userlist-bar">
|
||||
<i className="icon-user" />
|
||||
<span className="chat-usercount">
|
||||
{channel.get('users', List()).size}
|
||||
{channel && channel.users.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,13 +31,13 @@ export default class MessageBox extends PureComponent {
|
||||
this.bottom = false;
|
||||
}
|
||||
|
||||
if (nextProps.messages.get(0) !== this.props.messages.get(0)) {
|
||||
if (nextProps.messages[0] !== this.props.messages[0]) {
|
||||
if (nextProps.tab === this.props.tab) {
|
||||
const addedMessages =
|
||||
nextProps.messages.size - this.props.messages.size;
|
||||
nextProps.messages.length - this.props.messages.length;
|
||||
let addedHeight = 0;
|
||||
for (let i = 0; i < addedMessages; i++) {
|
||||
addedHeight += nextProps.messages.get(i).height;
|
||||
addedHeight += nextProps.messages[i].height;
|
||||
}
|
||||
|
||||
this.nextScrollTop = addedHeight + this.container.scrollTop;
|
||||
@ -57,7 +57,7 @@ export default class MessageBox extends PureComponent {
|
||||
this.container.scrollTop = this.nextScrollTop;
|
||||
this.nextScrollTop = 0;
|
||||
} else if (this.bottom) {
|
||||
this.list.scrollToRow(this.props.messages.size);
|
||||
this.list.scrollToRow(this.props.messages.length);
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ export default class MessageBox extends PureComponent {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return this.props.messages.get(index - 1).height;
|
||||
return this.props.messages[index - 1].height;
|
||||
};
|
||||
|
||||
listRef = el => {
|
||||
@ -101,7 +101,7 @@ export default class MessageBox extends PureComponent {
|
||||
} else {
|
||||
this.bottom = true;
|
||||
if (scroll) {
|
||||
this.list.scrollToRow(this.props.messages.size);
|
||||
this.list.scrollToRow(this.props.messages.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -177,7 +177,7 @@ export default class MessageBox extends PureComponent {
|
||||
}
|
||||
|
||||
const { messages, onNickClick } = this.props;
|
||||
const message = messages.get(index - 1);
|
||||
const message = messages[index - 1];
|
||||
|
||||
return (
|
||||
<Message
|
||||
@ -192,7 +192,7 @@ export default class MessageBox extends PureComponent {
|
||||
render() {
|
||||
const props = {};
|
||||
if (this.bottom) {
|
||||
props.scrollToIndex = this.props.messages.size;
|
||||
props.scrollToIndex = this.props.messages.length;
|
||||
} else if (this.scrollTop >= 0) {
|
||||
props.scrollTop = this.scrollTop;
|
||||
}
|
||||
@ -209,7 +209,7 @@ export default class MessageBox extends PureComponent {
|
||||
ref={this.listRef}
|
||||
width={width}
|
||||
height={height - 14}
|
||||
rowCount={this.props.messages.size + 1}
|
||||
rowCount={this.props.messages.length + 1}
|
||||
rowHeight={this.getRowHeight}
|
||||
rowRenderer={this.renderMessage}
|
||||
onScroll={this.handleScroll}
|
||||
|
@ -20,7 +20,7 @@ export default class UserList extends PureComponent {
|
||||
return (
|
||||
<UserListItem
|
||||
key={key}
|
||||
user={users.get(index)}
|
||||
user={users[index]}
|
||||
style={style}
|
||||
onClick={onNickClick}
|
||||
/>
|
||||
@ -39,7 +39,7 @@ export default class UserList extends PureComponent {
|
||||
ref={this.listRef}
|
||||
width={200}
|
||||
height={height - 20}
|
||||
rowCount={users.size}
|
||||
rowCount={users.length}
|
||||
rowHeight={24}
|
||||
rowRenderer={this.renderUser}
|
||||
className="rvlist-users"
|
||||
|
@ -1,6 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import Navicon from 'containers/Navicon';
|
||||
|
||||
const getSortedDefaultChannels = createSelector(
|
||||
defaults => defaults.channels,
|
||||
channels => channels.concat().sort()
|
||||
);
|
||||
|
||||
export default class Connect extends Component {
|
||||
state = {
|
||||
showOptionals: false,
|
||||
@ -90,7 +96,9 @@ export default class Connect extends Component {
|
||||
{defaults.showDetails && (
|
||||
<div className="connect-details">
|
||||
<h2>{defaults.address}</h2>
|
||||
{defaults.channels.sort().map(channel => <p>{channel}</p>)}
|
||||
{getSortedDefaultChannels(defaults).map(channel => (
|
||||
<p>{channel}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<input name="nick" type="text" placeholder="Nick" />
|
||||
|
@ -3,8 +3,8 @@ import Navicon from 'containers/Navicon';
|
||||
import FileInput from 'components/ui/FileInput';
|
||||
|
||||
const Settings = ({ settings, onCertChange, onKeyChange, uploadCert }) => {
|
||||
const status = settings.get('uploadingCert') ? 'Uploading...' : 'Upload';
|
||||
const error = settings.get('certError');
|
||||
const status = settings.uploadingCert ? 'Uploading...' : 'Upload';
|
||||
const error = settings.certError;
|
||||
|
||||
return (
|
||||
<div className="settings">
|
||||
@ -14,14 +14,14 @@ const Settings = ({ settings, onCertChange, onKeyChange, uploadCert }) => {
|
||||
<div>
|
||||
<p>Certificate</p>
|
||||
<FileInput
|
||||
name={settings.get('certFile') || 'Select Certificate'}
|
||||
name={settings.certFile || 'Select Certificate'}
|
||||
onChange={onCertChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>Private Key</p>
|
||||
<FileInput
|
||||
name={settings.get('keyFile') || 'Select Key'}
|
||||
name={settings.keyFile || 'Select Key'}
|
||||
onChange={onKeyChange}
|
||||
/>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@ import { createStructuredSelector } from 'reselect';
|
||||
import App from 'components/App';
|
||||
import { getConnected } from 'state/app';
|
||||
import { getSortedChannels } from 'state/channels';
|
||||
import { getSortedPrivateChats } from 'state/privateChats';
|
||||
import { getPrivateChats } from 'state/privateChats';
|
||||
import { getServers } from 'state/servers';
|
||||
import { getSelectedTab, select } from 'state/tab';
|
||||
import { getShowTabList, hideMenu } from 'state/ui';
|
||||
@ -12,7 +12,7 @@ import { push } from 'utils/router';
|
||||
const mapState = createStructuredSelector({
|
||||
channels: getSortedChannels,
|
||||
connected: getConnected,
|
||||
privateChats: getSortedPrivateChats,
|
||||
privateChats: getPrivateChats,
|
||||
servers: getServers,
|
||||
showTabList: getShowTabList,
|
||||
tab: getSelectedTab
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
} from 'state/messages';
|
||||
import { reconnect } from 'state/servers';
|
||||
import { select } from 'state/tab';
|
||||
import { normalizeChannel } from 'utils';
|
||||
import { find, normalizeChannel } from 'utils';
|
||||
|
||||
function withReason(message, reason) {
|
||||
return message + (reason ? ` (${reason})` : '');
|
||||
@ -18,9 +18,9 @@ function withReason(message, reason) {
|
||||
function findChannels(state, server, user) {
|
||||
const channels = [];
|
||||
|
||||
state.channels.get(server).forEach((channel, channelName) => {
|
||||
if (channel.get('users').find(u => u.nick === user)) {
|
||||
channels.push(channelName);
|
||||
Object.keys(state.channels[server]).forEach(channel => {
|
||||
if (find(state.channels[server][channel].users, u => u.nick === user)) {
|
||||
channels.push(channel);
|
||||
}
|
||||
});
|
||||
|
||||
@ -49,7 +49,7 @@ export default function handleSocket({
|
||||
const tab = state.tab.selected;
|
||||
const [joinedChannel] = channels;
|
||||
if (tab.server && tab.name) {
|
||||
const { nick } = state.servers.get(tab.server);
|
||||
const { nick } = state.servers[tab.server];
|
||||
if (
|
||||
tab.server === server &&
|
||||
nick === user &&
|
||||
|
@ -1,16 +1,17 @@
|
||||
import Cookie from 'js-cookie';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { getSelectedTab } from 'state/tab';
|
||||
import { isChannel, stringifyTab } from 'utils';
|
||||
import { observe } from 'utils/observe';
|
||||
|
||||
const saveTab = debounce(
|
||||
tab => Cookie.set('tab', tab.toString(), { expires: 30 }),
|
||||
tab => Cookie.set('tab', stringifyTab(tab), { expires: 30 }),
|
||||
1000
|
||||
);
|
||||
|
||||
export default function storage({ store }) {
|
||||
observe(store, getSelectedTab, tab => {
|
||||
if (tab.isChannel() || (tab.server && !tab.name)) {
|
||||
if (isChannel(tab) || (tab.server && !tab.name)) {
|
||||
saveTab(tab);
|
||||
}
|
||||
});
|
||||
|
@ -1,11 +1,10 @@
|
||||
import Immutable from 'immutable';
|
||||
import reducer, { compareUsers } from '../channels';
|
||||
import reducer, { compareUsers, getSortedChannels } from '../channels';
|
||||
import { connect } from '../servers';
|
||||
import * as actions from '../actions';
|
||||
|
||||
describe('channel reducer', () => {
|
||||
it('removes channels on PART', () => {
|
||||
let state = Immutable.fromJS({
|
||||
let state = {
|
||||
srv1: {
|
||||
chan1: {},
|
||||
chan2: {},
|
||||
@ -14,7 +13,7 @@ describe('channel reducer', () => {
|
||||
srv2: {
|
||||
chan1: {}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.PART,
|
||||
@ -22,7 +21,7 @@ describe('channel reducer', () => {
|
||||
channels: ['chan1', 'chan3']
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv1: {
|
||||
chan2: {}
|
||||
},
|
||||
@ -44,7 +43,7 @@ describe('channel reducer', () => {
|
||||
user: 'nick2'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
|
||||
@ -59,7 +58,7 @@ describe('channel reducer', () => {
|
||||
it('handles SOCKET_JOIN', () => {
|
||||
const state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
|
||||
@ -79,7 +78,7 @@ describe('channel reducer', () => {
|
||||
user: 'nick2'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
users: [{ mode: '', nick: 'nick1', renderName: 'nick1' }]
|
||||
@ -103,7 +102,7 @@ describe('channel reducer', () => {
|
||||
newNick: 'nick3'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
users: [
|
||||
@ -126,7 +125,7 @@ describe('channel reducer', () => {
|
||||
users: ['user3', 'user2', '@user4', 'user1', '+user5']
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
users: [
|
||||
@ -149,10 +148,11 @@ describe('channel reducer', () => {
|
||||
topic: 'the topic'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
topic: 'the topic'
|
||||
topic: 'the topic',
|
||||
users: []
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -165,7 +165,7 @@ describe('channel reducer', () => {
|
||||
|
||||
state = reducer(state, socket_mode('srv', 'chan1', 'nick1', 'o', ''));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
users: [
|
||||
@ -183,7 +183,7 @@ describe('channel reducer', () => {
|
||||
state = reducer(state, socket_mode('srv', 'chan1', 'nick2', 'o', ''));
|
||||
state = reducer(state, socket_mode('srv', 'chan2', 'not_there', 'x', ''));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: {
|
||||
users: [
|
||||
@ -208,7 +208,7 @@ describe('channel reducer', () => {
|
||||
]
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
chan1: { topic: 'the topic', users: [] },
|
||||
chan2: { users: [] }
|
||||
@ -225,7 +225,7 @@ describe('channel reducer', () => {
|
||||
data: [{ host: '127.0.0.1' }, { host: 'thehost' }]
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {},
|
||||
thehost: {}
|
||||
});
|
||||
@ -234,23 +234,23 @@ describe('channel reducer', () => {
|
||||
it('optimistically adds the server on CONNECT', () => {
|
||||
const state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {}
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the server on DISCONNECT', () => {
|
||||
let state = Immutable.fromJS({
|
||||
let state = {
|
||||
srv: {},
|
||||
srv2: {}
|
||||
});
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.DISCONNECT,
|
||||
server: 'srv2'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {}
|
||||
});
|
||||
});
|
||||
@ -301,3 +301,32 @@ describe('compareUsers()', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortedChannels', () => {
|
||||
it('sorts servers and channels', () => {
|
||||
expect(
|
||||
getSortedChannels({
|
||||
channels: {
|
||||
'bob.com': {},
|
||||
'127.0.0.1': {
|
||||
'#chan1': {
|
||||
users: [],
|
||||
topic: 'cake'
|
||||
},
|
||||
'#pie': {},
|
||||
'##apples': {}
|
||||
}
|
||||
}
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
address: '127.0.0.1',
|
||||
channels: ['##apples', '#chan1', '#pie']
|
||||
},
|
||||
{
|
||||
address: 'bob.com',
|
||||
channels: []
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
import reducer, { broadcast, getMessageTab } from '../messages';
|
||||
import * as actions from '../actions';
|
||||
import appReducer from '../app';
|
||||
@ -15,7 +14,7 @@ describe('message reducer', () => {
|
||||
}
|
||||
});
|
||||
|
||||
expect(state.toJS()).toMatchObject({
|
||||
expect(state).toMatchObject({
|
||||
srv: {
|
||||
'#chan1': [
|
||||
{
|
||||
@ -49,7 +48,7 @@ describe('message reducer', () => {
|
||||
]
|
||||
});
|
||||
|
||||
expect(state.toJS()).toMatchObject({
|
||||
expect(state).toMatchObject({
|
||||
srv: {
|
||||
'#chan1': [
|
||||
{
|
||||
@ -72,11 +71,11 @@ describe('message reducer', () => {
|
||||
});
|
||||
|
||||
it('handles prepending of messages on ADD_MESSAGES', () => {
|
||||
let state = fromJS({
|
||||
let state = {
|
||||
srv: {
|
||||
'#chan1': [{ id: 0 }]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.ADD_MESSAGES,
|
||||
@ -86,7 +85,7 @@ describe('message reducer', () => {
|
||||
messages: [{ id: 1 }, { id: 2 }]
|
||||
});
|
||||
|
||||
expect(state.toJS()).toMatchObject({
|
||||
expect(state).toMatchObject({
|
||||
srv: {
|
||||
'#chan1': [{ id: 1 }, { id: 2 }, { id: 0 }]
|
||||
}
|
||||
@ -103,7 +102,7 @@ describe('message reducer', () => {
|
||||
state.messages = reducer(undefined, action);
|
||||
}, () => state);
|
||||
|
||||
const messages = state.messages.toJS();
|
||||
const messages = state.messages;
|
||||
|
||||
expect(messages.srv).not.toHaveProperty('srv');
|
||||
expect(messages.srv['#chan1']).toHaveLength(1);
|
||||
@ -113,7 +112,7 @@ describe('message reducer', () => {
|
||||
});
|
||||
|
||||
it('deletes all messages related to server when disconnecting', () => {
|
||||
let state = fromJS({
|
||||
let state = {
|
||||
srv: {
|
||||
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
|
||||
'#chan2': [{ content: 'msg' }]
|
||||
@ -121,14 +120,14 @@ describe('message reducer', () => {
|
||||
srv2: {
|
||||
'#chan1': [{ content: 'msg' }]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.DISCONNECT,
|
||||
server: 'srv'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv2: {
|
||||
'#chan1': [{ content: 'msg' }]
|
||||
}
|
||||
@ -136,7 +135,7 @@ describe('message reducer', () => {
|
||||
});
|
||||
|
||||
it('deletes all messages related to channel when parting', () => {
|
||||
let state = fromJS({
|
||||
let state = {
|
||||
srv: {
|
||||
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
|
||||
'#chan2': [{ content: 'msg' }]
|
||||
@ -144,7 +143,7 @@ describe('message reducer', () => {
|
||||
srv2: {
|
||||
'#chan1': [{ content: 'msg' }]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.PART,
|
||||
@ -152,7 +151,7 @@ describe('message reducer', () => {
|
||||
channels: ['#chan1']
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
'#chan2': [{ content: 'msg' }]
|
||||
},
|
||||
|
@ -1,4 +1,3 @@
|
||||
import Immutable from 'immutable';
|
||||
import reducer, { connect, setServerName } from '../servers';
|
||||
import * as actions from '../actions';
|
||||
|
||||
@ -6,7 +5,7 @@ describe('server reducer', () => {
|
||||
it('adds the server on CONNECT', () => {
|
||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
@ -20,7 +19,7 @@ describe('server reducer', () => {
|
||||
|
||||
state = reducer(state, connect('127.0.0.1:1337', 'nick', {}));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
@ -39,7 +38,7 @@ describe('server reducer', () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
@ -62,31 +61,31 @@ describe('server reducer', () => {
|
||||
});
|
||||
|
||||
it('removes the server on DISCONNECT', () => {
|
||||
let state = Immutable.fromJS({
|
||||
let state = {
|
||||
srv: {},
|
||||
srv2: {}
|
||||
});
|
||||
};
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.DISCONNECT,
|
||||
server: 'srv2'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('handles SET_SERVER_NAME', () => {
|
||||
let state = Immutable.fromJS({
|
||||
let state = {
|
||||
srv: {
|
||||
name: 'cake'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
state = reducer(state, setServerName('pie', 'srv'));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
srv: {
|
||||
name: 'pie'
|
||||
}
|
||||
@ -102,7 +101,7 @@ describe('server reducer', () => {
|
||||
editing: true
|
||||
});
|
||||
|
||||
expect(state.toJS()).toMatchObject({
|
||||
expect(state).toMatchObject({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
@ -125,7 +124,7 @@ describe('server reducer', () => {
|
||||
nick: ''
|
||||
});
|
||||
|
||||
expect(state.toJS()).toMatchObject({
|
||||
expect(state).toMatchObject({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
@ -143,7 +142,7 @@ describe('server reducer', () => {
|
||||
newNick: 'nick2'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toMatchObject({
|
||||
expect(state).toMatchObject({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick2',
|
||||
@ -165,7 +164,7 @@ describe('server reducer', () => {
|
||||
server: '127.0.0.1'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toMatchObject({
|
||||
expect(state).toMatchObject({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
@ -197,23 +196,19 @@ describe('server reducer', () => {
|
||||
]
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {
|
||||
name: 'stuff',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: true,
|
||||
error: null
|
||||
connected: true
|
||||
}
|
||||
},
|
||||
'127.0.0.2': {
|
||||
name: 'stuffz',
|
||||
nick: 'nick2',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: false,
|
||||
error: null
|
||||
connected: false
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -227,14 +222,13 @@ describe('server reducer', () => {
|
||||
connected: true
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
editedNick: null,
|
||||
status: {
|
||||
connected: true,
|
||||
error: null
|
||||
connected: true
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -246,7 +240,7 @@ describe('server reducer', () => {
|
||||
error: 'Bad stuff happened'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
'127.0.0.1': {
|
||||
name: '127.0.0.1',
|
||||
nick: 'nick',
|
||||
|
@ -6,14 +6,14 @@ describe('tab reducer', () => {
|
||||
it('selects the tab and adds it to history', () => {
|
||||
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan' },
|
||||
history: [{ server: 'srv', name: '#chan' }]
|
||||
});
|
||||
|
||||
state = reducer(state, setSelectedTab('srv', 'user1'));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: 'user1' },
|
||||
history: [
|
||||
{ server: 'srv', name: '#chan' },
|
||||
@ -34,7 +34,7 @@ describe('tab reducer', () => {
|
||||
channels: ['#chan']
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan3' },
|
||||
history: [
|
||||
{ server: 'srv1', name: 'bob' },
|
||||
@ -55,7 +55,7 @@ describe('tab reducer', () => {
|
||||
nick: 'bob'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan3' },
|
||||
history: [
|
||||
{ server: 'srv', name: '#chan' },
|
||||
@ -76,7 +76,7 @@ describe('tab reducer', () => {
|
||||
server: 'srv'
|
||||
});
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan3' },
|
||||
history: [{ server: 'srv1', name: 'bob' }]
|
||||
});
|
||||
@ -87,8 +87,8 @@ describe('tab reducer', () => {
|
||||
|
||||
state = reducer(state, locationChanged('settings'));
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
selected: { server: null, name: null },
|
||||
expect(state).toEqual({
|
||||
selected: {},
|
||||
history: [{ server: 'srv', name: '#chan' }]
|
||||
});
|
||||
});
|
||||
@ -102,7 +102,7 @@ describe('tab reducer', () => {
|
||||
})
|
||||
);
|
||||
|
||||
expect(state.toJS()).toEqual({
|
||||
expect(state).toEqual({
|
||||
selected: { server: 'srv', name: '#chan' },
|
||||
history: [{ server: 'srv', name: '#chan' }]
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Record } from 'immutable';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import * as actions from './actions';
|
||||
|
||||
@ -9,34 +8,31 @@ export const getCharWidth = state => state.app.charWidth;
|
||||
export const getWindowWidth = state => state.app.windowWidth;
|
||||
export const getConnectDefaults = state => state.app.connectDefaults;
|
||||
|
||||
const ConnectDefaults = Record({
|
||||
name: '',
|
||||
address: '',
|
||||
channels: [],
|
||||
ssl: false,
|
||||
password: false,
|
||||
readonly: false,
|
||||
showDetails: false
|
||||
});
|
||||
|
||||
const App = Record({
|
||||
const initialState = {
|
||||
connected: true,
|
||||
wrapWidth: 0,
|
||||
charWidth: 0,
|
||||
windowWidth: 0,
|
||||
connectDefaults: new ConnectDefaults()
|
||||
});
|
||||
connectDefaults: {
|
||||
name: '',
|
||||
address: '',
|
||||
channels: [],
|
||||
ssl: false,
|
||||
password: false,
|
||||
readonly: false,
|
||||
showDetails: false
|
||||
}
|
||||
};
|
||||
|
||||
export default createReducer(new App(), {
|
||||
[actions.APP_SET](state, action) {
|
||||
return state.set(action.key, action.value);
|
||||
export default createReducer(initialState, {
|
||||
[actions.APP_SET](state, { key, value }) {
|
||||
state[key] = value;
|
||||
},
|
||||
|
||||
[actions.UPDATE_MESSAGE_HEIGHT](state, action) {
|
||||
return state
|
||||
.set('wrapWidth', action.wrapWidth)
|
||||
.set('charWidth', action.charWidth)
|
||||
.set('windowWidth', action.windowWidth);
|
||||
state.wrapWidth = action.wrapWidth;
|
||||
state.charWidth = action.charWidth;
|
||||
state.windowWidth = action.windowWidth;
|
||||
}
|
||||
});
|
||||
|
||||
@ -57,5 +53,5 @@ export function setCharWidth(width) {
|
||||
}
|
||||
|
||||
export function setConnectDefaults(defaults) {
|
||||
return appSet('connectDefaults', new ConnectDefaults(defaults));
|
||||
return appSet('connectDefaults', defaults);
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { Map, List, Record } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import get from 'lodash/get';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { find, findIndex } from 'utils';
|
||||
import { getSelectedTab, updateSelection } from './tab';
|
||||
import * as actions from './actions';
|
||||
|
||||
const User = Record({
|
||||
nick: null,
|
||||
renderName: null,
|
||||
mode: ''
|
||||
});
|
||||
|
||||
const modePrefixes = [
|
||||
{ mode: 'q', prefix: '~' }, // Owner
|
||||
{ mode: 'a', prefix: '&' }, // Admin
|
||||
@ -18,24 +14,23 @@ const modePrefixes = [
|
||||
{ mode: 'v', prefix: '+' } // Voice
|
||||
];
|
||||
|
||||
function updateRenderName(user) {
|
||||
function getRenderName(user) {
|
||||
for (let i = 0; i < modePrefixes.length; i++) {
|
||||
if (user.mode.indexOf(modePrefixes[i].mode) !== -1) {
|
||||
return user.set('renderName', `${modePrefixes[i].prefix}${user.nick}`);
|
||||
return `${modePrefixes[i].prefix}${user.nick}`;
|
||||
}
|
||||
}
|
||||
|
||||
return user.set('renderName', user.nick);
|
||||
return user.nick;
|
||||
}
|
||||
|
||||
function createUser(nick, mode) {
|
||||
return updateRenderName(
|
||||
new User({
|
||||
nick,
|
||||
renderName: nick,
|
||||
mode: mode || ''
|
||||
})
|
||||
);
|
||||
const user = {
|
||||
nick,
|
||||
mode: mode || ''
|
||||
};
|
||||
user.renderName = getRenderName(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
function loadUser(nick) {
|
||||
@ -54,6 +49,22 @@ function loadUser(nick) {
|
||||
return createUser(nick);
|
||||
}
|
||||
|
||||
function removeUser(users, nick) {
|
||||
const i = findIndex(users, u => u.nick === nick);
|
||||
if (i !== -1) {
|
||||
users.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function init(state, server, channel) {
|
||||
if (!state[server]) {
|
||||
state[server] = {};
|
||||
}
|
||||
if (channel && !state[server][channel]) {
|
||||
state[server][channel] = { users: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export function compareUsers(a, b) {
|
||||
a = a.renderName.toLowerCase();
|
||||
b = b.renderName.toLowerCase();
|
||||
@ -80,152 +91,119 @@ export function compareUsers(a, b) {
|
||||
|
||||
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))
|
||||
sortBy(
|
||||
Object.keys(channels).map(server => ({
|
||||
address: server,
|
||||
channels: sortBy(Object.keys(channels[server]), channel =>
|
||||
channel.toLowerCase()
|
||||
)
|
||||
)
|
||||
.sortBy(key)
|
||||
})),
|
||||
server => server.address.toLowerCase()
|
||||
)
|
||||
);
|
||||
|
||||
export const getSelectedChannel = createSelector(
|
||||
getSelectedTab,
|
||||
getChannels,
|
||||
(tab, channels) => channels.getIn([tab.server, tab.name], Map())
|
||||
(tab, channels) => get(channels, [tab.server, tab.name])
|
||||
);
|
||||
|
||||
export const getSelectedChannelUsers = createSelector(
|
||||
getSelectedChannel,
|
||||
channel => channel.get('users', List()).sort(compareUsers)
|
||||
channel => {
|
||||
if (channel) {
|
||||
return channel.users.concat().sort(compareUsers);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
);
|
||||
|
||||
export default createReducer(Map(), {
|
||||
[actions.PART](state, { server, channels }) {
|
||||
return state.withMutations(s => {
|
||||
channels.forEach(channel => s.deleteIn([server, channel]));
|
||||
});
|
||||
},
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.PART](state, { server, channels }) {
|
||||
channels.forEach(channel => delete state[server][channel]);
|
||||
},
|
||||
|
||||
[actions.socket.JOIN](state, { server, channels, user }) {
|
||||
return state.updateIn([server, channels[0], 'users'], List(), users =>
|
||||
users.push(createUser(user))
|
||||
);
|
||||
},
|
||||
[actions.socket.JOIN](state, { server, channels, user }) {
|
||||
const channel = channels[0];
|
||||
init(state, server, channel);
|
||||
state[server][channel].users.push(createUser(user));
|
||||
},
|
||||
|
||||
[actions.socket.PART](state, { server, channel, user }) {
|
||||
if (state.hasIn([server, channel])) {
|
||||
return state.updateIn([server, channel, 'users'], users =>
|
||||
users.filter(u => u.nick !== user)
|
||||
);
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
[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)
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
[actions.socket.NICK](state, { server, oldNick, newNick }) {
|
||||
return state.withMutations(s => {
|
||||
s.get(server).forEach((v, channel) => {
|
||||
s.updateIn([server, channel, 'users'], users => {
|
||||
const i = users.findIndex(user => user.nick === oldNick);
|
||||
if (i < 0) {
|
||||
return users;
|
||||
}
|
||||
|
||||
return users.update(i, user =>
|
||||
updateRenderName(user.set('nick', newNick))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
[actions.socket.USERS](state, { server, channel, users }) {
|
||||
return state.setIn(
|
||||
[server, channel, 'users'],
|
||||
List(users.map(user => loadUser(user)))
|
||||
);
|
||||
},
|
||||
|
||||
[actions.socket.TOPIC](state, { server, channel, topic }) {
|
||||
return state.setIn([server, channel, 'topic'], topic);
|
||||
},
|
||||
|
||||
[actions.socket.MODE](state, { server, channel, user, remove, add }) {
|
||||
return state.updateIn([server, channel, 'users'], users => {
|
||||
const i = users.findIndex(u => u.nick === user);
|
||||
if (i < 0) {
|
||||
return users;
|
||||
[actions.socket.PART](state, { server, channel, user }) {
|
||||
if (state[server][channel]) {
|
||||
removeUser(state[server][channel].users, user);
|
||||
}
|
||||
},
|
||||
|
||||
return users.update(i, u => {
|
||||
let { mode } = u;
|
||||
[actions.socket.QUIT](state, { server, user }) {
|
||||
Object.keys(state[server]).forEach(channel => {
|
||||
removeUser(state[server][channel].users, user);
|
||||
});
|
||||
},
|
||||
|
||||
[actions.socket.NICK](state, { server, oldNick, newNick }) {
|
||||
Object.keys(state[server]).forEach(channel => {
|
||||
const user = find(
|
||||
state[server][channel].users,
|
||||
u => u.nick === oldNick
|
||||
);
|
||||
if (user) {
|
||||
user.nick = newNick;
|
||||
user.renderName = getRenderName(user);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
[actions.socket.USERS](state, { server, channel, users }) {
|
||||
init(state, server, channel);
|
||||
state[server][channel].users = users.map(nick => loadUser(nick));
|
||||
},
|
||||
|
||||
[actions.socket.TOPIC](state, { server, channel, topic }) {
|
||||
init(state, server, channel);
|
||||
state[server][channel].topic = topic;
|
||||
},
|
||||
|
||||
[actions.socket.MODE](state, { server, channel, user, remove, add }) {
|
||||
const u = find(state[server][channel].users, v => v.nick === user);
|
||||
if (u) {
|
||||
let j = remove.length;
|
||||
while (j--) {
|
||||
mode = mode.replace(remove[j], '');
|
||||
u.mode = u.mode.replace(remove[j], '');
|
||||
}
|
||||
|
||||
return updateRenderName(u.set('mode', mode + add));
|
||||
});
|
||||
});
|
||||
},
|
||||
u.mode += add;
|
||||
u.renderName = getRenderName(u);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.CHANNELS](state, { data }) {
|
||||
if (!data) {
|
||||
return state;
|
||||
[actions.socket.CHANNELS](state, { data }) {
|
||||
if (data) {
|
||||
data.forEach(({ server, name, topic }) => {
|
||||
init(state, server, name);
|
||||
state[server][name].topic = topic;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.SERVERS](state, { data }) {
|
||||
if (data) {
|
||||
data.forEach(({ host }) => init(state, host));
|
||||
}
|
||||
},
|
||||
|
||||
[actions.CONNECT](state, { host }) {
|
||||
init(state, host);
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
delete state[server];
|
||||
}
|
||||
|
||||
return state.withMutations(s => {
|
||||
data.forEach(channel => {
|
||||
s.setIn(
|
||||
[channel.server, channel.name],
|
||||
Map({
|
||||
users: List(),
|
||||
topic: channel.topic
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
[actions.socket.SERVERS](state, { data }) {
|
||||
if (!data) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return state.withMutations(s => {
|
||||
data.forEach(server => {
|
||||
if (!s.has(server.host)) {
|
||||
s.set(server.host, Map());
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
[actions.CONNECT](state, { host }) {
|
||||
if (!state.has(host)) {
|
||||
return state.set(host, Map());
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
return state.delete(server);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export function join(channels, server) {
|
||||
return {
|
||||
|
@ -1,54 +1,45 @@
|
||||
import { List, Record } from 'immutable';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import * as actions from './actions';
|
||||
|
||||
const HISTORY_MAX_LENGTH = 128;
|
||||
|
||||
const State = Record({
|
||||
history: List(),
|
||||
const initialState = {
|
||||
history: [],
|
||||
index: 0
|
||||
});
|
||||
};
|
||||
|
||||
export const getCurrentInputHistoryEntry = state => {
|
||||
if (state.input.index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return state.input.history.get(state.input.index);
|
||||
return state.input.history[state.input.index];
|
||||
};
|
||||
|
||||
export default createReducer(new State(), {
|
||||
[actions.INPUT_HISTORY_ADD](state, action) {
|
||||
const { line } = action;
|
||||
if (line.trim() && line !== state.history.get(0)) {
|
||||
export default createReducer(initialState, {
|
||||
[actions.INPUT_HISTORY_ADD](state, { line }) {
|
||||
if (line.trim() && line !== state.history[0]) {
|
||||
if (history.length === HISTORY_MAX_LENGTH) {
|
||||
return state.set('history', state.history.unshift(line).pop());
|
||||
state.history.pop();
|
||||
}
|
||||
|
||||
return state.set('history', state.history.unshift(line));
|
||||
state.history.unshift(line);
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.INPUT_HISTORY_RESET](state) {
|
||||
return state.set('index', -1);
|
||||
state.index = -1;
|
||||
},
|
||||
|
||||
[actions.INPUT_HISTORY_INCREMENT](state) {
|
||||
if (state.index < state.history.size - 1) {
|
||||
return state.set('index', state.index + 1);
|
||||
if (state.index < state.history.length - 1) {
|
||||
state.index++;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.INPUT_HISTORY_DECREMENT](state) {
|
||||
if (state.index >= 0) {
|
||||
return state.set('index', state.index - 1);
|
||||
state.index--;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { List, Map, Record } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import has from 'lodash/has';
|
||||
import {
|
||||
findBreakpoints,
|
||||
messageHeight,
|
||||
@ -12,95 +12,85 @@ import { getApp } from './app';
|
||||
import { getSelectedTab } from './tab';
|
||||
import * as actions from './actions';
|
||||
|
||||
const Message = Record({
|
||||
id: null,
|
||||
from: null,
|
||||
content: '',
|
||||
time: null,
|
||||
type: null,
|
||||
channel: false,
|
||||
next: false,
|
||||
height: 0,
|
||||
length: 0,
|
||||
breakpoints: null
|
||||
});
|
||||
|
||||
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) => {
|
||||
const target = tab.name || tab.server;
|
||||
if (has(messages, [tab.server, target])) {
|
||||
return messages[tab.server][target];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
);
|
||||
|
||||
export const getHasMoreMessages = createSelector(
|
||||
getSelectedMessages,
|
||||
messages => {
|
||||
const first = messages.get(0);
|
||||
const first = messages[0];
|
||||
return first && first.next;
|
||||
}
|
||||
);
|
||||
|
||||
export default createReducer(Map(), {
|
||||
[actions.ADD_MESSAGE](state, { server, tab, 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]))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
messages.forEach(message =>
|
||||
s.updateIn([server, message.tab || tab], List(), list =>
|
||||
list.push(new Message(message))
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
return state.delete(server);
|
||||
},
|
||||
|
||||
[actions.PART](state, { server, channels }) {
|
||||
return state.withMutations(s =>
|
||||
channels.forEach(channel => s.deleteIn([server, channel]))
|
||||
);
|
||||
},
|
||||
|
||||
[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
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
function init(state, server, tab) {
|
||||
if (!state[server]) {
|
||||
state[server] = {};
|
||||
}
|
||||
});
|
||||
if (!state[server][tab]) {
|
||||
state[server][tab] = [];
|
||||
}
|
||||
}
|
||||
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.ADD_MESSAGE](state, { server, tab, message }) {
|
||||
init(state, server, tab);
|
||||
state[server][tab].push(message);
|
||||
},
|
||||
|
||||
[actions.ADD_MESSAGES](state, { server, tab, messages, prepend }) {
|
||||
if (prepend) {
|
||||
init(state, server, tab);
|
||||
state[server][tab].unshift(...messages);
|
||||
} else {
|
||||
messages.forEach(message => {
|
||||
init(state, server, message.tab || tab);
|
||||
state[server][message.tab || tab].push(message);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
delete state[server];
|
||||
},
|
||||
|
||||
[actions.PART](state, { server, channels }) {
|
||||
channels.forEach(channel => delete state[server][channel]);
|
||||
},
|
||||
|
||||
[actions.UPDATE_MESSAGE_HEIGHT](
|
||||
state,
|
||||
{ wrapWidth, charWidth, windowWidth }
|
||||
) {
|
||||
Object.keys(state).forEach(server =>
|
||||
Object.keys(state[server]).forEach(target =>
|
||||
state[server][target].forEach(message => {
|
||||
message.height = messageHeight(
|
||||
message,
|
||||
wrapWidth,
|
||||
charWidth,
|
||||
6 * charWidth,
|
||||
windowWidth
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let nextID = 0;
|
||||
|
||||
@ -156,14 +146,14 @@ export function getMessageTab(server, to) {
|
||||
export function fetchMessages() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const first = getSelectedMessages(state).get(0);
|
||||
const first = getSelectedMessages(state)[0];
|
||||
|
||||
if (!first) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = state.tab.selected;
|
||||
if (tab.isChannel()) {
|
||||
if (isChannel(tab)) {
|
||||
dispatch({
|
||||
type: actions.FETCH_MESSAGES,
|
||||
socket: {
|
||||
@ -206,7 +196,7 @@ export function sendMessage(content, to, server) {
|
||||
tab: to,
|
||||
message: initMessage(
|
||||
{
|
||||
from: state.servers.getIn([server, 'nick']),
|
||||
from: state.servers[server].nick,
|
||||
content
|
||||
},
|
||||
to,
|
||||
|
@ -1,48 +1,46 @@
|
||||
import { Set, Map } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { findIndex } from 'utils';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { updateSelection } from './tab';
|
||||
import * as actions from './actions';
|
||||
|
||||
export const getPrivateChats = state => state.privateChats;
|
||||
|
||||
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))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
function open(state, server, nick) {
|
||||
return state.update(server, Set(), chats => chats.add(nick));
|
||||
if (!state[server]) {
|
||||
state[server] = [];
|
||||
}
|
||||
if (findIndex(state[server], n => n === nick) === -1) {
|
||||
state[server].push(nick);
|
||||
state[server] = sortBy(state[server], v => v.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
export default createReducer(Map(), {
|
||||
[actions.OPEN_PRIVATE_CHAT](state, action) {
|
||||
return open(state, action.server, action.nick);
|
||||
},
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.OPEN_PRIVATE_CHAT](state, action) {
|
||||
open(state, action.server, action.nick);
|
||||
},
|
||||
|
||||
[actions.CLOSE_PRIVATE_CHAT](state, action) {
|
||||
return state.update(action.server, chats => chats.delete(action.nick));
|
||||
},
|
||||
[actions.CLOSE_PRIVATE_CHAT](state, { server, nick }) {
|
||||
const i = findIndex(state[server], n => n === nick);
|
||||
if (i !== -1) {
|
||||
state[server].splice(i, 1);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.PM](state, action) {
|
||||
if (action.from.indexOf('.') === -1) {
|
||||
return open(state, action.server, action.from);
|
||||
[actions.socket.PM](state, action) {
|
||||
if (action.from.indexOf('.') === -1) {
|
||||
open(state, action.server, action.from);
|
||||
}
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
delete state[server];
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, action) {
|
||||
return state.delete(action.server);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export function openPrivateChat(server, nick) {
|
||||
return {
|
||||
|
@ -1,21 +1,20 @@
|
||||
import { List, Record } from 'immutable';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import * as actions from './actions';
|
||||
|
||||
const State = Record({
|
||||
const initialState = {
|
||||
show: false,
|
||||
results: List()
|
||||
});
|
||||
results: []
|
||||
};
|
||||
|
||||
export const getSearch = state => state.search;
|
||||
|
||||
export default createReducer(new State(), {
|
||||
[actions.socket.SEARCH](state, action) {
|
||||
return state.set('results', List(action.results));
|
||||
export default createReducer(initialState, {
|
||||
[actions.socket.SEARCH](state, { results }) {
|
||||
state.results = results;
|
||||
},
|
||||
|
||||
[actions.TOGGLE_SEARCH](state) {
|
||||
return state.set('show', !state.show);
|
||||
state.show = !state.show;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import get from 'lodash/get';
|
||||
import { getServers } from './servers';
|
||||
import { getSelectedTab } from './tab';
|
||||
|
||||
@ -6,5 +7,5 @@ import { getSelectedTab } from './tab';
|
||||
export const getSelectedTabTitle = createSelector(
|
||||
getSelectedTab,
|
||||
getServers,
|
||||
(tab, servers) => tab.name || servers.getIn([tab.server, 'name'])
|
||||
(tab, servers) => tab.name || get(servers, [tab.server, 'name'])
|
||||
);
|
||||
|
@ -1,30 +1,21 @@
|
||||
import { Map, Record } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import get from 'lodash/get';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { getSelectedTab, updateSelection } from './tab';
|
||||
import * as actions from './actions';
|
||||
|
||||
const Status = Record({
|
||||
connected: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const Server = Record({
|
||||
nick: '',
|
||||
editedNick: null,
|
||||
name: '',
|
||||
status: new Status()
|
||||
});
|
||||
|
||||
export const getServers = state => state.servers;
|
||||
|
||||
export const getCurrentNick = createSelector(
|
||||
getServers,
|
||||
getSelectedTab,
|
||||
(servers, tab) => {
|
||||
const editedNick = servers.getIn([tab.server, 'editedNick']);
|
||||
if (editedNick === null) {
|
||||
return servers.getIn([tab.server, 'nick']);
|
||||
if (!servers[tab.server]) {
|
||||
return;
|
||||
}
|
||||
const { editedNick } = servers[tab.server];
|
||||
if (!editedNick) {
|
||||
return servers[tab.server].nick;
|
||||
}
|
||||
return editedNick;
|
||||
}
|
||||
@ -33,80 +24,77 @@ export const getCurrentNick = createSelector(
|
||||
export const getCurrentServerName = createSelector(
|
||||
getServers,
|
||||
getSelectedTab,
|
||||
(servers, tab) => servers.getIn([tab.server, 'name'])
|
||||
(servers, tab) => get(servers, [tab.server, 'name'])
|
||||
);
|
||||
|
||||
export const getCurrentServerStatus = createSelector(
|
||||
getServers,
|
||||
getSelectedTab,
|
||||
(servers, tab) => servers.getIn([tab.server, 'status'])
|
||||
(servers, tab) => get(servers, [tab.server, 'status'], {})
|
||||
);
|
||||
|
||||
export default createReducer(Map(), {
|
||||
[actions.CONNECT](state, { host, nick, options }) {
|
||||
if (!state.has(host)) {
|
||||
return state.set(
|
||||
host,
|
||||
new Server({
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.CONNECT](state, { host, nick, options }) {
|
||||
if (!state[host]) {
|
||||
state[host] = {
|
||||
nick,
|
||||
name: options.name || host
|
||||
})
|
||||
);
|
||||
}
|
||||
editedNick: null,
|
||||
name: options.name || host,
|
||||
status: {
|
||||
connected: false,
|
||||
error: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
return state.delete(server);
|
||||
},
|
||||
|
||||
[actions.SET_SERVER_NAME](state, { server, name }) {
|
||||
return state.setIn([server, 'name'], name);
|
||||
},
|
||||
|
||||
[actions.SET_NICK](state, { server, nick, editing }) {
|
||||
if (editing) {
|
||||
return state.setIn([server, 'editedNick'], nick);
|
||||
} else if (nick === '') {
|
||||
return state.setIn([server, 'editedNick'], null);
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
[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;
|
||||
},
|
||||
|
||||
[actions.socket.NICK_FAIL](state, { server }) {
|
||||
return state.setIn([server, 'editedNick'], null);
|
||||
},
|
||||
|
||||
[actions.socket.SERVERS](state, { data }) {
|
||||
if (!data) {
|
||||
return state;
|
||||
}
|
||||
},
|
||||
|
||||
return state.withMutations(s => {
|
||||
data.forEach(server => {
|
||||
server.status = new Status(server.status);
|
||||
s.set(server.host, new Server(server));
|
||||
});
|
||||
});
|
||||
},
|
||||
[actions.DISCONNECT](state, { server }) {
|
||||
delete state[server];
|
||||
},
|
||||
|
||||
[actions.socket.CONNECTION_UPDATE](state, action) {
|
||||
if (state.has(action.server)) {
|
||||
return state.setIn([action.server, 'status'], new Status(action));
|
||||
[actions.SET_SERVER_NAME](state, { server, name }) {
|
||||
state[server].name = name;
|
||||
},
|
||||
|
||||
[actions.SET_NICK](state, { server, nick, editing }) {
|
||||
if (editing) {
|
||||
state[server].editedNick = nick;
|
||||
} else if (nick === '') {
|
||||
state[server].editedNick = null;
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.NICK](state, { server, oldNick, newNick }) {
|
||||
if (!oldNick || oldNick === state[server].nick) {
|
||||
state[server].nick = newNick;
|
||||
state[server].editedNick = null;
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.NICK_FAIL](state, { server }) {
|
||||
state[server].editedNick = null;
|
||||
},
|
||||
|
||||
[actions.socket.SERVERS](state, { data }) {
|
||||
if (data) {
|
||||
data.forEach(({ host, name, nick, status }) => {
|
||||
state[host] = { name, nick, status };
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[actions.socket.CONNECTION_UPDATE](state, { server, connected, error }) {
|
||||
if (state[server]) {
|
||||
state[server].status.connected = connected;
|
||||
state[server].status.error = error;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export function connect(server, nick, options) {
|
||||
let host = server;
|
||||
|
@ -1,47 +1,45 @@
|
||||
import { Map } from 'immutable';
|
||||
import base64 from 'base64-arraybuffer';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import * as actions from './actions';
|
||||
|
||||
export const getSettings = state => state.settings;
|
||||
|
||||
export default createReducer(Map(), {
|
||||
[actions.UPLOAD_CERT](state) {
|
||||
return state.set('uploadingCert', true);
|
||||
},
|
||||
export default createReducer(
|
||||
{},
|
||||
{
|
||||
[actions.UPLOAD_CERT](state) {
|
||||
state.uploadingCert = true;
|
||||
},
|
||||
|
||||
[actions.socket.CERT_SUCCESS]() {
|
||||
return Map({ uploadingCert: false });
|
||||
},
|
||||
[actions.socket.CERT_SUCCESS](state) {
|
||||
state.uploadingCert = false;
|
||||
delete state.certFile;
|
||||
delete state.cert;
|
||||
delete state.keyFile;
|
||||
delete state.key;
|
||||
},
|
||||
|
||||
[actions.socket.CERT_FAIL](state, action) {
|
||||
return state.merge({
|
||||
uploadingCert: false,
|
||||
certError: action.message
|
||||
});
|
||||
},
|
||||
[actions.socket.CERT_FAIL](state, action) {
|
||||
state.uploadingCert = false;
|
||||
state.certError = action.message;
|
||||
},
|
||||
|
||||
[actions.SET_CERT_ERROR](state, action) {
|
||||
return state.merge({
|
||||
uploadingCert: false,
|
||||
certError: action.message
|
||||
});
|
||||
},
|
||||
[actions.SET_CERT_ERROR](state, action) {
|
||||
state.uploadingCert = false;
|
||||
state.certError = action.message;
|
||||
},
|
||||
|
||||
[actions.SET_CERT](state, action) {
|
||||
return state.merge({
|
||||
certFile: action.fileName,
|
||||
cert: action.cert
|
||||
});
|
||||
},
|
||||
[actions.SET_CERT](state, action) {
|
||||
state.certFile = action.fileName;
|
||||
state.cert = action.cert;
|
||||
},
|
||||
|
||||
[actions.SET_KEY](state, action) {
|
||||
return state.merge({
|
||||
keyFile: action.fileName,
|
||||
key: action.key
|
||||
});
|
||||
[actions.SET_KEY](state, action) {
|
||||
state.keyFile = action.fileName;
|
||||
state.key = action.key;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export function setCertError(message) {
|
||||
return {
|
||||
@ -53,14 +51,14 @@ export function setCertError(message) {
|
||||
export function uploadCert() {
|
||||
return (dispatch, getState) => {
|
||||
const { settings } = getState();
|
||||
if (settings.has('cert') && settings.has('key')) {
|
||||
if (settings.cert && settings.key) {
|
||||
dispatch({
|
||||
type: actions.UPLOAD_CERT,
|
||||
socket: {
|
||||
type: 'cert',
|
||||
data: {
|
||||
cert: settings.get('cert'),
|
||||
key: settings.get('key')
|
||||
cert: settings.cert,
|
||||
key: settings.key
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,77 +1,48 @@
|
||||
import { Record, List } from 'immutable';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { push, replace, LOCATION_CHANGED } from 'utils/router';
|
||||
import * as actions from './actions';
|
||||
|
||||
const TabRecord = Record({
|
||||
server: null,
|
||||
name: null
|
||||
});
|
||||
|
||||
class Tab extends TabRecord {
|
||||
isChannel() {
|
||||
return this.name && this.name.charAt(0) === '#';
|
||||
}
|
||||
|
||||
toString() {
|
||||
let str = this.server;
|
||||
if (this.name) {
|
||||
str += `;${this.name}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
const State = Record({
|
||||
selected: new Tab(),
|
||||
history: List()
|
||||
});
|
||||
const initialState = {
|
||||
selected: {},
|
||||
history: []
|
||||
};
|
||||
|
||||
function selectTab(state, action) {
|
||||
const tab = new Tab(action);
|
||||
return state
|
||||
.set('selected', tab)
|
||||
.update('history', history => history.push(tab));
|
||||
state.selected = {
|
||||
server: action.server,
|
||||
name: action.name
|
||||
};
|
||||
state.history.push(state.selected);
|
||||
}
|
||||
|
||||
export const getSelectedTab = state => state.tab.selected;
|
||||
|
||||
export default createReducer(new State(), {
|
||||
export default createReducer(initialState, {
|
||||
[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])
|
||||
)
|
||||
state.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)
|
||||
)
|
||||
state.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)
|
||||
);
|
||||
state.history = state.history.filter(tab => tab.server !== action.server);
|
||||
},
|
||||
|
||||
[LOCATION_CHANGED](state, action) {
|
||||
const { route, params } = action;
|
||||
if (route === 'chat') {
|
||||
return selectTab(state, params);
|
||||
selectTab(state, params);
|
||||
} else {
|
||||
state.selected = {};
|
||||
}
|
||||
|
||||
return state.set('selected', new Tab());
|
||||
}
|
||||
});
|
||||
|
||||
@ -89,16 +60,17 @@ export function updateSelection() {
|
||||
const { history } = state.tab;
|
||||
const { servers } = state;
|
||||
const { server } = state.tab.selected;
|
||||
const serverAddrs = Object.keys(servers);
|
||||
|
||||
if (servers.size === 0) {
|
||||
if (serverAddrs.length === 0) {
|
||||
dispatch(replace('/connect'));
|
||||
} else if (history.size > 0) {
|
||||
const tab = history.last();
|
||||
} else if (history.length > 0) {
|
||||
const tab = history[history.length - 1];
|
||||
dispatch(select(tab.server, tab.name, true));
|
||||
} else if (servers.has(server)) {
|
||||
} else if (servers[server]) {
|
||||
dispatch(select(server, null, true));
|
||||
} else {
|
||||
dispatch(select(servers.keySeq().first(), null, true));
|
||||
dispatch(select(serverAddrs.sort()[0], null, true));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,30 +1,29 @@
|
||||
import { Record } from 'immutable';
|
||||
import createReducer from 'utils/createReducer';
|
||||
import { LOCATION_CHANGED } from 'utils/router';
|
||||
import * as actions from './actions';
|
||||
|
||||
const State = Record({
|
||||
const initialState = {
|
||||
showTabList: false,
|
||||
showUserList: false
|
||||
});
|
||||
};
|
||||
|
||||
export const getShowTabList = state => state.ui.showTabList;
|
||||
export const getShowUserList = state => state.ui.showUserList;
|
||||
|
||||
function setMenuHidden(state) {
|
||||
return state.set('showTabList', false);
|
||||
state.showTabList = false;
|
||||
}
|
||||
|
||||
export default createReducer(new State(), {
|
||||
export default createReducer(initialState, {
|
||||
[actions.TOGGLE_MENU](state) {
|
||||
return state.update('showTabList', show => !show);
|
||||
state.showTabList = !state.showTabList;
|
||||
},
|
||||
|
||||
[actions.HIDE_MENU]: setMenuHidden,
|
||||
[LOCATION_CHANGED]: setMenuHidden,
|
||||
|
||||
[actions.TOGGLE_USERLIST](state) {
|
||||
return state.update('showUserList', show => !show);
|
||||
state.showUserList = !state.showUserList;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,19 @@
|
||||
import React from 'react';
|
||||
import { isChannel } from '..';
|
||||
import linkify from '../linkify';
|
||||
|
||||
describe('isChannel()', () => {
|
||||
it('it handles strings', () => {
|
||||
expect(isChannel('#cake')).toBe(true);
|
||||
expect(isChannel('cake')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles tab objects', () => {
|
||||
expect(isChannel({ name: '#cake' })).toBe(true);
|
||||
expect(isChannel({ name: 'cake' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('linkify()', () => {
|
||||
const proto = href => (href.indexOf('http') !== 0 ? `http://${href}` : href);
|
||||
const linkTo = href => (
|
||||
|
@ -1,7 +1,10 @@
|
||||
import produce from 'immer';
|
||||
import has from 'lodash/has';
|
||||
|
||||
export default function createReducer(initialState, handlers) {
|
||||
return function reducer(state = initialState, action) {
|
||||
if (Object.prototype.hasOwnProperty.call(handlers, action.type)) {
|
||||
return handlers[action.type](state, action);
|
||||
if (has(handlers, action.type)) {
|
||||
return produce(state, draft => handlers[action.type](draft, action));
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
@ -16,9 +16,25 @@ export function normalizeChannel(channel) {
|
||||
|
||||
export function isChannel(name) {
|
||||
// TODO: Handle other channel types
|
||||
if (typeof name === 'object') {
|
||||
({ name } = name);
|
||||
}
|
||||
return typeof name === 'string' && name[0] === '#';
|
||||
}
|
||||
|
||||
export function stringifyTab(server, name) {
|
||||
if (typeof server === 'object') {
|
||||
if (server.name) {
|
||||
return `${server.server};${server.name}`;
|
||||
}
|
||||
return server.server;
|
||||
}
|
||||
if (name) {
|
||||
return `${server};${name}`;
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
export function timestamp(date = new Date()) {
|
||||
const h = padStart(date.getHours(), 2, '0');
|
||||
const m = padStart(date.getMinutes(), 2, '0');
|
||||
@ -54,14 +70,24 @@ export function measureScrollBarWidth() {
|
||||
return widthNoScroll - widthWithScroll;
|
||||
}
|
||||
|
||||
export function find(arr, pred) {
|
||||
export function findIndex(arr, pred) {
|
||||
if (!arr) {
|
||||
return null;
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (pred(arr[i])) {
|
||||
return arr[i];
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function find(arr, pred) {
|
||||
const i = findIndex(arr, pred);
|
||||
if (i !== -1) {
|
||||
return arr[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
1140
client/yarn.lock
1140
client/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user