Colocate reducers, actions and selectors
This commit is contained in:
parent
1e7d4c3fe4
commit
889e3b88b7
File diff suppressed because one or more lines are too long
@ -1,55 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
import { updateSelection } from './tab';
|
|
||||||
|
|
||||||
export function join(channels, server) {
|
|
||||||
return {
|
|
||||||
type: actions.JOIN,
|
|
||||||
channels,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'join',
|
|
||||||
data: { channels, server }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function part(channels, server) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({
|
|
||||||
type: actions.PART,
|
|
||||||
channels,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'part',
|
|
||||||
data: { channels, server }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dispatch(updateSelection());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invite(user, channel, server) {
|
|
||||||
return {
|
|
||||||
type: actions.INVITE,
|
|
||||||
user,
|
|
||||||
channel,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'invite',
|
|
||||||
data: { user, channel, server }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function kick(user, channel, server) {
|
|
||||||
return {
|
|
||||||
type: actions.KICK,
|
|
||||||
user,
|
|
||||||
channel,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'kick',
|
|
||||||
data: { user, channel, server }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
export function setEnvironment(key, value) {
|
|
||||||
return {
|
|
||||||
type: actions.SET_ENVIRONMENT,
|
|
||||||
key,
|
|
||||||
value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setWrapWidth(width) {
|
|
||||||
return setEnvironment('wrapWidth', width);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCharWidth(width) {
|
|
||||||
return setEnvironment('charWidth', width);
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
export function addInputHistory(line) {
|
|
||||||
return {
|
|
||||||
type: actions.INPUT_HISTORY_ADD,
|
|
||||||
line
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetInputHistory() {
|
|
||||||
return {
|
|
||||||
type: actions.INPUT_HISTORY_RESET
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function incrementInputHistory() {
|
|
||||||
return {
|
|
||||||
type: actions.INPUT_HISTORY_INCREMENT
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decrementInputHistory() {
|
|
||||||
return {
|
|
||||||
type: actions.INPUT_HISTORY_DECREMENT
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
import { updateSelection } from './tab';
|
|
||||||
|
|
||||||
export function openPrivateChat(server, nick) {
|
|
||||||
return {
|
|
||||||
type: actions.OPEN_PRIVATE_CHAT,
|
|
||||||
server,
|
|
||||||
nick
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closePrivateChat(server, nick) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({
|
|
||||||
type: actions.CLOSE_PRIVATE_CHAT,
|
|
||||||
server,
|
|
||||||
nick
|
|
||||||
});
|
|
||||||
dispatch(updateSelection());
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
export function searchMessages(server, channel, phrase) {
|
|
||||||
return {
|
|
||||||
type: actions.SEARCH_MESSAGES,
|
|
||||||
server,
|
|
||||||
channel,
|
|
||||||
phrase,
|
|
||||||
socket: {
|
|
||||||
type: 'search',
|
|
||||||
data: { server, channel, phrase }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleSearch() {
|
|
||||||
return {
|
|
||||||
type: actions.TOGGLE_SEARCH
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
import { updateSelection } from './tab';
|
|
||||||
|
|
||||||
export function connect(server, nick, options) {
|
|
||||||
let host = server;
|
|
||||||
const i = server.indexOf(':');
|
|
||||||
if (i > 0) {
|
|
||||||
host = server.slice(0, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: actions.CONNECT,
|
|
||||||
host,
|
|
||||||
nick,
|
|
||||||
options,
|
|
||||||
socket: {
|
|
||||||
type: 'connect',
|
|
||||||
data: {
|
|
||||||
server,
|
|
||||||
nick,
|
|
||||||
username: options.username || nick,
|
|
||||||
password: options.password,
|
|
||||||
realname: options.realname || nick,
|
|
||||||
tls: options.tls || false,
|
|
||||||
name: options.name || server
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function disconnect(server) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({
|
|
||||||
type: actions.DISCONNECT,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'quit',
|
|
||||||
data: { server }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dispatch(updateSelection());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function whois(user, server) {
|
|
||||||
return {
|
|
||||||
type: actions.WHOIS,
|
|
||||||
user,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'whois',
|
|
||||||
data: { user, server }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function away(message, server) {
|
|
||||||
return {
|
|
||||||
type: actions.AWAY,
|
|
||||||
message,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'away',
|
|
||||||
data: { message, server }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setNick(nick, server) {
|
|
||||||
return {
|
|
||||||
type: actions.SET_NICK,
|
|
||||||
nick,
|
|
||||||
server,
|
|
||||||
socket: {
|
|
||||||
type: 'nick',
|
|
||||||
data: {
|
|
||||||
new: nick,
|
|
||||||
server
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import base64 from 'base64-arraybuffer';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
export function setCertError(message) {
|
|
||||||
return {
|
|
||||||
type: actions.SET_CERT_ERROR,
|
|
||||||
message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uploadCert() {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const { settings } = getState();
|
|
||||||
if (settings.has('cert') && settings.has('key')) {
|
|
||||||
dispatch({
|
|
||||||
type: actions.UPLOAD_CERT,
|
|
||||||
socket: {
|
|
||||||
type: 'cert',
|
|
||||||
data: {
|
|
||||||
cert: settings.get('cert'),
|
|
||||||
key: settings.get('key')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch(setCertError('Missing certificate or key'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCert(fileName, cert) {
|
|
||||||
return {
|
|
||||||
type: actions.SET_CERT,
|
|
||||||
fileName,
|
|
||||||
cert: base64.encode(cert)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setKey(fileName, key) {
|
|
||||||
return {
|
|
||||||
type: actions.SET_KEY,
|
|
||||||
fileName,
|
|
||||||
key: base64.encode(key)
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
import { push, replace } from '../util/router';
|
|
||||||
|
|
||||||
export function select(server, name, doReplace) {
|
|
||||||
const navigate = doReplace ? replace : push;
|
|
||||||
if (name) {
|
|
||||||
return navigate(`/${server}/${encodeURIComponent(name)}`);
|
|
||||||
}
|
|
||||||
return navigate(`/${server}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateSelection() {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const state = getState();
|
|
||||||
const history = state.tab.history;
|
|
||||||
const { servers } = state;
|
|
||||||
const { server } = state.tab.selected;
|
|
||||||
|
|
||||||
if (servers.size === 0) {
|
|
||||||
dispatch(replace('/connect'));
|
|
||||||
} else if (history.size > 0) {
|
|
||||||
const tab = history.last();
|
|
||||||
dispatch(select(tab.server, tab.name, true));
|
|
||||||
} else if (servers.has(server)) {
|
|
||||||
dispatch(select(server, null, true));
|
|
||||||
} else {
|
|
||||||
dispatch(select(servers.keySeq().first(), null, true));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSelectedTab(server, name = null) {
|
|
||||||
return {
|
|
||||||
type: actions.SELECT_TAB,
|
|
||||||
server,
|
|
||||||
name
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
export function hideMenu() {
|
|
||||||
return {
|
|
||||||
type: actions.HIDE_MENU
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleMenu() {
|
|
||||||
return {
|
|
||||||
type: actions.TOGGLE_MENU
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleUserList() {
|
|
||||||
return {
|
|
||||||
type: actions.TOGGLE_USERLIST
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
import createCommandMiddleware from './middleware/command';
|
import createCommandMiddleware from './middleware/command';
|
||||||
import { COMMAND } from './actions';
|
import { COMMAND } from './state/actions';
|
||||||
import { setNick, disconnect, whois, away } from './actions/server';
|
import { join, part, invite, kick } from './state/channels';
|
||||||
import { join, part, invite, kick } from './actions/channel';
|
import { sendMessage, addMessage, raw } from './state/messages';
|
||||||
import { select } from './actions/tab';
|
import { setNick, disconnect, whois, away } from './state/servers';
|
||||||
import { sendMessage, addMessage, raw } from './actions/message';
|
import { select } from './state/tab';
|
||||||
|
|
||||||
const help = [
|
const help = [
|
||||||
'/join <channel> - Join a channel',
|
'/join <channel> - Join a channel',
|
||||||
|
@ -6,8 +6,8 @@ export default class MessageInput extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleKey = e => {
|
handleKey = e => {
|
||||||
const { tab, runCommand, sendMessage, addInputHistory, incrementInputHistory,
|
const { tab, runCommand, sendMessage,
|
||||||
decrementInputHistory, resetInputHistory, history } = this.props;
|
add, reset, increment, decrement, currentHistoryEntry } = this.props;
|
||||||
|
|
||||||
if (e.key === 'Enter' && e.target.value) {
|
if (e.key === 'Enter' && e.target.value) {
|
||||||
if (e.target.value[0] === '/') {
|
if (e.target.value[0] === '/') {
|
||||||
@ -16,17 +16,17 @@ export default class MessageInput extends PureComponent {
|
|||||||
sendMessage(e.target.value, tab.name, tab.server);
|
sendMessage(e.target.value, tab.name, tab.server);
|
||||||
}
|
}
|
||||||
|
|
||||||
addInputHistory(e.target.value);
|
add(e.target.value);
|
||||||
resetInputHistory();
|
reset();
|
||||||
this.setState({ value: '' });
|
this.setState({ value: '' });
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
incrementInputHistory();
|
increment();
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowDown') {
|
||||||
decrementInputHistory();
|
decrement();
|
||||||
} else if (history) {
|
} else if (currentHistoryEntry) {
|
||||||
this.setState({ value: e.target.value });
|
this.setState({ value: e.target.value });
|
||||||
resetInputHistory();
|
reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,14 +35,14 @@ export default class MessageInput extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { nick } = this.props;
|
const { nick, currentHistoryEntry } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="message-input-wrap">
|
<div className="message-input-wrap">
|
||||||
<span className="message-input-nick">{nick}</span>
|
<span className="message-input-nick">{nick}</span>
|
||||||
<input
|
<input
|
||||||
className="message-input"
|
className="message-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={this.props.history || this.state.value}
|
value={currentHistoryEntry || this.state.value}
|
||||||
onKeyDown={this.handleKey}
|
onKeyDown={this.handleKey}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toggleMenu } from '../actions/ui';
|
import { toggleMenu } from '../state/ui';
|
||||||
|
|
||||||
class Navicon extends PureComponent {
|
class Navicon extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
|
@ -35,7 +35,7 @@ export default class TabList extends PureComponent {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if (privateChats.has(address) && privateChats.get(address).size > 0) {
|
if (privateChats.has(address) && privateChats.get(address).size > 0) {
|
||||||
tabs.push(<div className="tab-label">Private messages</div>);
|
tabs.push(<div key={`${address}-pm}`} className="tab-label">Private messages</div>);
|
||||||
|
|
||||||
privateChats.get(address).forEach(nick => tabs.push(
|
privateChats.get(address).forEach(nick => tabs.push(
|
||||||
<TabListItem
|
<TabListItem
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { createStructuredSelector } from 'reselect';
|
||||||
import { push } from '../util/router';
|
import { push } from '../util/router';
|
||||||
import Route from './Route';
|
import Route from './Route';
|
||||||
import Chat from './Chat';
|
import Chat from './Chat';
|
||||||
import Connect from './Connect';
|
import Connect from './Connect';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
import TabList from '../components/TabList';
|
import TabList from '../components/TabList';
|
||||||
import { select } from '../actions/tab';
|
import { getChannels } from '../state/channels';
|
||||||
import { hideMenu } from '../actions/ui';
|
import { getPrivateChats } from '../state/privateChats';
|
||||||
|
import { getServers } from '../state/servers';
|
||||||
|
import { getSelectedTab, select } from '../state/tab';
|
||||||
|
import { getShowTabList, hideMenu } from '../state/ui';
|
||||||
|
|
||||||
class App extends PureComponent {
|
class App extends PureComponent {
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
@ -33,14 +37,12 @@ class App extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
const mapState = createStructuredSelector({
|
||||||
return {
|
channels: getChannels,
|
||||||
servers: state.servers,
|
privateChats: getPrivateChats,
|
||||||
channels: state.channels,
|
servers: getServers,
|
||||||
privateChats: state.privateChats,
|
showTabList: getShowTabList,
|
||||||
showTabList: state.ui.showTabList,
|
tab: getSelectedTab
|
||||||
tab: state.tab.selected
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, { pushPath: push, select, hideMenu })(App);
|
export default connect(mapState, { pushPath: push, select, hideMenu })(App);
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector, createStructuredSelector } from 'reselect';
|
import { createStructuredSelector } from 'reselect';
|
||||||
import { List, Map } from 'immutable';
|
|
||||||
import ChatTitle from '../components/ChatTitle';
|
import ChatTitle from '../components/ChatTitle';
|
||||||
import Search from '../components/Search';
|
import Search from '../components/Search';
|
||||||
import MessageBox from '../components/MessageBox';
|
import MessageBox from '../components/MessageBox';
|
||||||
import MessageInput from '../components/MessageInput';
|
import MessageInput from '../components/MessageInput';
|
||||||
import UserList from '../components/UserList';
|
import UserList from '../components/UserList';
|
||||||
import { part } from '../actions/channel';
|
import { getSelectedTabTitle } from '../state';
|
||||||
import { openPrivateChat, closePrivateChat } from '../actions/privateChat';
|
import { getSelectedChannel, getSelectedChannelUsers, part } from '../state/channels';
|
||||||
import { searchMessages, toggleSearch } from '../actions/search';
|
import { getCurrentInputHistoryEntry, addInputHistory, resetInputHistory,
|
||||||
import { select } from '../actions/tab';
|
incrementInputHistory, decrementInputHistory } from '../state/input';
|
||||||
import { runCommand, sendMessage, fetchMessages } from '../actions/message';
|
import { getSelectedMessages, getHasMoreMessages,
|
||||||
import { disconnect } from '../actions/server';
|
runCommand, sendMessage, fetchMessages } from '../state/messages';
|
||||||
import { toggleUserList } from '../actions/ui';
|
import { openPrivateChat, closePrivateChat } from '../state/privateChats';
|
||||||
import * as inputHistoryActions from '../actions/inputHistory';
|
import { getSearch, searchMessages, toggleSearch } from '../state/search';
|
||||||
import { getSelectedTab } from '../reducers/tab';
|
import { getCurrentNick, disconnect } from '../state/servers';
|
||||||
import { getSelectedMessages } from '../reducers/messages';
|
import { getSelectedTab, select } from '../state/tab';
|
||||||
import { getCurrentNick } from '../reducers/servers';
|
import { getShowUserList, toggleUserList } from '../state/ui';
|
||||||
|
|
||||||
class Chat extends PureComponent {
|
class Chat extends PureComponent {
|
||||||
handleSearch = phrase => {
|
handleSearch = phrase => {
|
||||||
@ -38,7 +37,7 @@ class Chat extends PureComponent {
|
|||||||
handleFetchMore = () => this.props.dispatch(fetchMessages());
|
handleFetchMore = () => this.props.dispatch(fetchMessages());
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { title, tab, channel, search, history,
|
const { title, tab, channel, search, currentInputHistoryEntry,
|
||||||
messages, hasMoreMessages, users, showUserList, nick, inputActions } = this.props;
|
messages, hasMoreMessages, users, showUserList, nick, inputActions } = this.props;
|
||||||
|
|
||||||
let chatClass;
|
let chatClass;
|
||||||
@ -76,7 +75,7 @@ class Chat extends PureComponent {
|
|||||||
<MessageInput
|
<MessageInput
|
||||||
tab={tab}
|
tab={tab}
|
||||||
channel={channel}
|
channel={channel}
|
||||||
history={history}
|
currentHistoryEntry={currentInputHistoryEntry}
|
||||||
nick={nick}
|
nick={nick}
|
||||||
runCommand={this.props.runCommand}
|
runCommand={this.props.runCommand}
|
||||||
sendMessage={this.props.sendMessage}
|
sendMessage={this.props.sendMessage}
|
||||||
@ -94,73 +93,41 @@ class Chat extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverSelector = state => state.servers;
|
const mapState = createStructuredSelector({
|
||||||
const channelSelector = state => state.channels;
|
channel: getSelectedChannel,
|
||||||
const searchSelector = state => state.search;
|
currentInputHistoryEntry: getCurrentInputHistoryEntry,
|
||||||
const showUserListSelector = state => state.ui.showUserList;
|
|
||||||
const historySelector = state => {
|
|
||||||
if (state.input.index === -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.input.history.get(state.input.index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedChannelSelector = createSelector(
|
|
||||||
getSelectedTab,
|
|
||||||
channelSelector,
|
|
||||||
(tab, channels) => channels.getIn([tab.server, tab.name], Map())
|
|
||||||
);
|
|
||||||
|
|
||||||
const usersSelector = createSelector(
|
|
||||||
selectedChannelSelector,
|
|
||||||
channel => channel.get('users', List())
|
|
||||||
);
|
|
||||||
|
|
||||||
const titleSelector = createSelector(
|
|
||||||
getSelectedTab,
|
|
||||||
serverSelector,
|
|
||||||
(tab, servers) => tab.name || servers.getIn([tab.server, 'name'])
|
|
||||||
);
|
|
||||||
|
|
||||||
const getHasMoreMessages = createSelector(
|
|
||||||
getSelectedMessages,
|
|
||||||
messages => {
|
|
||||||
const first = messages.get(0);
|
|
||||||
return first && first.next;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
title: titleSelector,
|
|
||||||
tab: getSelectedTab,
|
|
||||||
channel: selectedChannelSelector,
|
|
||||||
messages: getSelectedMessages,
|
|
||||||
hasMoreMessages: getHasMoreMessages,
|
hasMoreMessages: getHasMoreMessages,
|
||||||
users: usersSelector,
|
messages: getSelectedMessages,
|
||||||
showUserList: showUserListSelector,
|
nick: getCurrentNick,
|
||||||
search: searchSelector,
|
search: getSearch,
|
||||||
history: historySelector,
|
showUserList: getShowUserList,
|
||||||
nick: getCurrentNick
|
tab: getSelectedTab,
|
||||||
|
title: getSelectedTabTitle,
|
||||||
|
users: getSelectedChannelUsers
|
||||||
});
|
});
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatch(dispatch) {
|
||||||
return {
|
return {
|
||||||
dispatch,
|
dispatch,
|
||||||
...bindActionCreators({
|
...bindActionCreators({
|
||||||
select,
|
closePrivateChat,
|
||||||
toggleSearch,
|
|
||||||
toggleUserList,
|
|
||||||
searchMessages,
|
|
||||||
runCommand,
|
|
||||||
sendMessage,
|
|
||||||
part,
|
|
||||||
disconnect,
|
disconnect,
|
||||||
openPrivateChat,
|
openPrivateChat,
|
||||||
closePrivateChat
|
part,
|
||||||
|
runCommand,
|
||||||
|
searchMessages,
|
||||||
|
select,
|
||||||
|
sendMessage,
|
||||||
|
toggleSearch,
|
||||||
|
toggleUserList
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
inputActions: bindActionCreators(inputHistoryActions, dispatch)
|
inputActions: bindActionCreators({
|
||||||
|
add: addInputHistory,
|
||||||
|
reset: resetInputHistory,
|
||||||
|
increment: incrementInputHistory,
|
||||||
|
decrement: decrementInputHistory
|
||||||
|
}, dispatch)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Chat);
|
export default connect(mapState, mapDispatch)(Chat);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { createStructuredSelector } from 'reselect';
|
||||||
import Navicon from '../components/Navicon';
|
import Navicon from '../components/Navicon';
|
||||||
import * as serverActions from '../actions/server';
|
import { join } from '../state/channels';
|
||||||
import { join } from '../actions/channel';
|
import { getConnectDefaults } from '../state/environment';
|
||||||
import { select } from '../actions/tab';
|
import { connect as connectServer } from '../state/servers';
|
||||||
|
import { select } from '../state/tab';
|
||||||
|
|
||||||
class Connect extends PureComponent {
|
class Connect extends PureComponent {
|
||||||
state = {
|
state = {
|
||||||
@ -33,7 +35,7 @@ class Connect extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (address.indexOf('.') > 0 && nick) {
|
if (address.indexOf('.') > 0 && nick) {
|
||||||
dispatch(serverActions.connect(address, nick, opts));
|
dispatch(connectServer(address, nick, opts));
|
||||||
|
|
||||||
const i = address.indexOf(':');
|
const i = address.indexOf(':');
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
@ -102,10 +104,8 @@ class Connect extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
const mapState = createStructuredSelector({
|
||||||
return {
|
defaults: getConnectDefaults
|
||||||
defaults: state.environment.get('connect_defaults')
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Connect);
|
export default connect(mapState)(Connect);
|
||||||
|
@ -10,8 +10,8 @@ const Route = ({ route, name, children }) => {
|
|||||||
|
|
||||||
const getRoute = state => state.router.route;
|
const getRoute = state => state.router.route;
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapState = createStructuredSelector({
|
||||||
route: getRoute
|
route: getRoute
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Route);
|
export default connect(mapState)(Route);
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { createStructuredSelector } from 'reselect';
|
||||||
import Navicon from '../components/Navicon';
|
import Navicon from '../components/Navicon';
|
||||||
import FileInput from '../components/FileInput';
|
import FileInput from '../components/FileInput';
|
||||||
import { setCert, setKey, uploadCert } from '../actions/settings';
|
import { getSettings, setCert, setKey, uploadCert } from '../state/settings';
|
||||||
|
|
||||||
class Settings extends PureComponent {
|
class Settings extends PureComponent {
|
||||||
handleCertChange = (name, data) => this.props.dispatch(setCert(name, data));
|
handleCertChange = (name, data) => this.props.dispatch(setCert(name, data));
|
||||||
@ -40,6 +41,8 @@ class Settings extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(state => ({
|
const mapState = createStructuredSelector({
|
||||||
settings: state.settings
|
settings: getSettings
|
||||||
}))(Settings);
|
});
|
||||||
|
|
||||||
|
export default connect(mapState)(Settings);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { inform } from '../actions/message';
|
import { inform } from '../state/messages';
|
||||||
|
|
||||||
const notFound = 'commandNotFound';
|
const notFound = 'commandNotFound';
|
||||||
|
|
||||||
|
@ -1,28 +1,23 @@
|
|||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import observe from '../util/observe';
|
import { getRouter } from '../state';
|
||||||
import { getCurrentServerName } from '../reducers/servers';
|
import { getCurrentServerName } from '../state/servers';
|
||||||
|
import { observe } from '../util/observe';
|
||||||
const getRouter = state => state.router;
|
|
||||||
|
|
||||||
export default function documentTitle({ store }) {
|
export default function documentTitle({ store }) {
|
||||||
observe(
|
observe(store, [getRouter, getCurrentServerName], (router, serverName) => {
|
||||||
store,
|
let title;
|
||||||
[getRouter, getCurrentServerName],
|
|
||||||
(router, serverName) => {
|
|
||||||
let title;
|
|
||||||
|
|
||||||
if (router.route === 'chat') {
|
if (router.route === 'chat') {
|
||||||
const { name } = router.params;
|
const { name } = router.params;
|
||||||
if (name) {
|
if (name) {
|
||||||
title = `${name} @ ${serverName}`;
|
title = `${name} @ ${serverName}`;
|
||||||
} else {
|
|
||||||
title = serverName;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
title = capitalize(router.route);
|
title = serverName;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
document.title = `${title} | Dispatch`;
|
title = capitalize(router.route);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
document.title = `${title} | Dispatch`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
22
client/src/js/modules/fonts.js
Normal file
22
client/src/js/modules/fonts.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import FontFaceObserver from 'fontfaceobserver';
|
||||||
|
import { setCharWidth } from '../state/environment';
|
||||||
|
import { stringWidth } from '../util';
|
||||||
|
|
||||||
|
export default function fonts({ store }) {
|
||||||
|
let charWidth = localStorage.charWidth;
|
||||||
|
if (charWidth) {
|
||||||
|
store.dispatch(setCharWidth(parseFloat(charWidth)));
|
||||||
|
}
|
||||||
|
|
||||||
|
new FontFaceObserver('Roboto Mono').load().then(() => {
|
||||||
|
if (!charWidth) {
|
||||||
|
charWidth = stringWidth(' ', '16px Roboto Mono');
|
||||||
|
store.dispatch(setCharWidth(charWidth));
|
||||||
|
localStorage.charWidth = charWidth;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new FontFaceObserver('Montserrat').load();
|
||||||
|
new FontFaceObserver('Montserrat', { weight: 700 }).load();
|
||||||
|
new FontFaceObserver('Roboto Mono', { weight: 700 }).load();
|
||||||
|
}
|
@ -1,12 +1,16 @@
|
|||||||
import documentTitle from './documentTitle';
|
import documentTitle from './documentTitle';
|
||||||
import handleSocket from './handleSocket';
|
import fonts from './fonts';
|
||||||
import initialState from './initialState';
|
import initialState from './initialState';
|
||||||
|
import socket from './socket';
|
||||||
import storage from './storage';
|
import storage from './storage';
|
||||||
|
import widthUpdates from './widthUpdates';
|
||||||
|
|
||||||
export default function runModules(ctx) {
|
export default function runModules(ctx) {
|
||||||
|
fonts(ctx);
|
||||||
initialState(ctx);
|
initialState(ctx);
|
||||||
|
|
||||||
documentTitle(ctx);
|
documentTitle(ctx);
|
||||||
handleSocket(ctx);
|
socket(ctx);
|
||||||
storage(ctx);
|
storage(ctx);
|
||||||
|
widthUpdates(ctx);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import Cookie from 'js-cookie';
|
import Cookie from 'js-cookie';
|
||||||
import { setEnvironment } from '../actions/environment';
|
import { socket as socketActions } from '../state/actions';
|
||||||
import { addMessages } from '../actions/message';
|
import { getWrapWidth, setEnvironment } from '../state/environment';
|
||||||
import { select, updateSelection } from '../actions/tab';
|
import { addMessages } from '../state/messages';
|
||||||
|
import { select, updateSelection } from '../state/tab';
|
||||||
import { find } from '../util';
|
import { find } from '../util';
|
||||||
import { initWidthUpdates } from '../util/messageHeight';
|
import { when } from '../util/observe';
|
||||||
import { replace } from '../util/router';
|
import { replace } from '../util/router';
|
||||||
|
|
||||||
export default function initialState({ store }) {
|
export default function initialState({ store }) {
|
||||||
@ -13,7 +14,7 @@ export default function initialState({ store }) {
|
|||||||
|
|
||||||
if (env.servers) {
|
if (env.servers) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'SOCKET_SERVERS',
|
type: socketActions.SERVERS,
|
||||||
data: env.servers
|
data: env.servers
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,19 +38,21 @@ export default function initialState({ store }) {
|
|||||||
|
|
||||||
if (env.channels) {
|
if (env.channels) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'SOCKET_CHANNELS',
|
type: socketActions.CHANNELS,
|
||||||
data: env.channels
|
data: env.channels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.users) {
|
if (env.users) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: 'SOCKET_USERS',
|
type: socketActions.USERS,
|
||||||
...env.users
|
...env.users
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initWidthUpdates(store, () => {
|
// Wait until wrapWidth gets initialized so that height calculations
|
||||||
|
// only happen once for these messages
|
||||||
|
when(store, getWrapWidth, () => {
|
||||||
if (env.messages) {
|
if (env.messages) {
|
||||||
const { messages, server, to, next } = env.messages;
|
const { messages, server, to, next } = env.messages;
|
||||||
store.dispatch(addMessages(messages, server, to, false, next));
|
store.dispatch(addMessages(messages, server, to, false, next));
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { broadcast, inform, addMessage, addMessages } from '../actions/message';
|
import { socketAction } from '../state/actions';
|
||||||
import { select } from '../actions/tab';
|
import { broadcast, inform, addMessage, addMessages } from '../state/messages';
|
||||||
import { replace } from '../util/router';
|
import { select } from '../state/tab';
|
||||||
import { normalizeChannel } from '../util';
|
import { normalizeChannel } from '../util';
|
||||||
|
import { replace } from '../util/router';
|
||||||
|
|
||||||
function withReason(message, reason) {
|
function withReason(message, reason) {
|
||||||
return message + (reason ? ` (${reason})` : '');
|
return message + (reason ? ` (${reason})` : '');
|
||||||
@ -97,7 +98,7 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
|
|||||||
handlers[type](data);
|
handlers[type](data);
|
||||||
}
|
}
|
||||||
|
|
||||||
type = `SOCKET_${type.toUpperCase()}`;
|
type = socketAction(type);
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
dispatch({ type, data });
|
dispatch({ type, data });
|
||||||
} else {
|
} else {
|
@ -1,7 +1,7 @@
|
|||||||
import Cookie from 'js-cookie';
|
import Cookie from 'js-cookie';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import observe from '../util/observe';
|
import { observe } from '../util/observe';
|
||||||
import { getSelectedTab } from '../reducers/tab';
|
import { getSelectedTab } from '../state/tab';
|
||||||
|
|
||||||
const saveTab = debounce(tab =>
|
const saveTab = debounce(tab =>
|
||||||
Cookie.set('tab', tab.toString(), { expires: 30 })
|
Cookie.set('tab', tab.toString(), { expires: 30 })
|
||||||
|
41
client/src/js/modules/widthUpdates.js
Normal file
41
client/src/js/modules/widthUpdates.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { when } from '../util/observe';
|
||||||
|
import { measureScrollBarWidth } from '../util';
|
||||||
|
import { getCharWidth } from '../state/environment';
|
||||||
|
import { updateMessageHeight } from '../state/messages';
|
||||||
|
|
||||||
|
const menuWidth = 200;
|
||||||
|
const messagePadding = 30;
|
||||||
|
const smallScreen = 600;
|
||||||
|
|
||||||
|
export default function widthUpdates({ store }) {
|
||||||
|
when(store, getCharWidth, charWidth => {
|
||||||
|
window.messageIndent = 6 * charWidth;
|
||||||
|
const scrollBarWidth = measureScrollBarWidth();
|
||||||
|
let prevWrapWidth;
|
||||||
|
|
||||||
|
function updateWidth() {
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
let wrapWidth = windowWidth - scrollBarWidth - messagePadding;
|
||||||
|
if (windowWidth > smallScreen) {
|
||||||
|
wrapWidth -= menuWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrapWidth !== prevWrapWidth) {
|
||||||
|
prevWrapWidth = wrapWidth;
|
||||||
|
store.dispatch(updateMessageHeight(wrapWidth, charWidth, windowWidth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizeRAF;
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
if (resizeRAF) {
|
||||||
|
window.cancelAnimationFrame(resizeRAF);
|
||||||
|
}
|
||||||
|
resizeRAF = window.requestAnimationFrame(updateWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidth();
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
});
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
import { Map } from 'immutable';
|
|
||||||
import createReducer from '../util/createReducer';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
export default createReducer(Map(), {
|
|
||||||
[actions.SET_ENVIRONMENT](state, action) {
|
|
||||||
return state.set(action.key, action.value);
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.UPDATE_MESSAGE_HEIGHT](state, action) {
|
|
||||||
return state
|
|
||||||
.set('wrapWidth', action.wrapWidth)
|
|
||||||
.set('charWidth', action.charWidth);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,71 +0,0 @@
|
|||||||
import { List, Map, Record } from 'immutable';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createReducer from '../util/createReducer';
|
|
||||||
import { messageHeight } from '../util';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
import { getSelectedTab } from './tab';
|
|
||||||
|
|
||||||
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())
|
|
||||||
);
|
|
||||||
|
|
||||||
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 }) {
|
|
||||||
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))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,18 +0,0 @@
|
|||||||
import { List, Record } from 'immutable';
|
|
||||||
import createReducer from '../util/createReducer';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
const State = Record({
|
|
||||||
show: false,
|
|
||||||
results: List()
|
|
||||||
});
|
|
||||||
|
|
||||||
export default createReducer(new State(), {
|
|
||||||
[actions.SOCKET_SEARCH](state, action) {
|
|
||||||
return state.set('results', List(action.results));
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.TOGGLE_SEARCH](state) {
|
|
||||||
return state.set('show', !state.show);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,74 +0,0 @@
|
|||||||
import { Map, Record } from 'immutable';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createReducer from '../util/createReducer';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
import { getSelectedTab } from './tab';
|
|
||||||
|
|
||||||
const Server = Record({
|
|
||||||
nick: null,
|
|
||||||
name: null,
|
|
||||||
connected: false
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getServers = state => state.servers;
|
|
||||||
|
|
||||||
export const getCurrentNick = createSelector(
|
|
||||||
getServers,
|
|
||||||
getSelectedTab,
|
|
||||||
(servers, tab) => servers.getIn([tab.server, 'nick'], '')
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getCurrentServerName = createSelector(
|
|
||||||
getServers,
|
|
||||||
getSelectedTab,
|
|
||||||
(servers, tab) => servers.getIn([tab.server, 'name'], '')
|
|
||||||
);
|
|
||||||
|
|
||||||
export default createReducer(Map(), {
|
|
||||||
[actions.CONNECT](state, action) {
|
|
||||||
const { host, nick, options } = action;
|
|
||||||
|
|
||||||
if (!state.has(host)) {
|
|
||||||
return state.set(host, new Server({
|
|
||||||
nick,
|
|
||||||
name: options.name || host
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.DISCONNECT](state, action) {
|
|
||||||
return state.delete(action.server);
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SOCKET_NICK](state, action) {
|
|
||||||
const { server, old } = action;
|
|
||||||
if (!old || old === state.get(server).nick) {
|
|
||||||
return state.update(server, s => s.set('nick', action.new));
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SOCKET_SERVERS](state, action) {
|
|
||||||
if (!action.data) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.withMutations(s => {
|
|
||||||
action.data.forEach(server => {
|
|
||||||
s.set(server.host, new Server(server));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SOCKET_CONNECTION_UPDATE](state, action) {
|
|
||||||
return state.withMutations(s =>
|
|
||||||
Object.keys(action).forEach(server => {
|
|
||||||
if (s.has(server)) {
|
|
||||||
s.setIn([server, 'connected'], action[server]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,41 +0,0 @@
|
|||||||
import { Map } from 'immutable';
|
|
||||||
import createReducer from '../util/createReducer';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
|
|
||||||
export default createReducer(Map(), {
|
|
||||||
[actions.UPLOAD_CERT](state) {
|
|
||||||
return state.set('uploadingCert', true);
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SOCKET_CERT_SUCCESS]() {
|
|
||||||
return Map({ uploadingCert: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SOCKET_CERT_FAIL](state, action) {
|
|
||||||
return state.merge({
|
|
||||||
uploadingCert: false,
|
|
||||||
certError: action.message
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SET_CERT_ERROR](state, action) {
|
|
||||||
return state.merge({
|
|
||||||
uploadingCert: false,
|
|
||||||
certError: action.message
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SET_CERT](state, action) {
|
|
||||||
return state.merge({
|
|
||||||
certFile: action.fileName,
|
|
||||||
cert: action.cert
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.SET_KEY](state, action) {
|
|
||||||
return state.merge({
|
|
||||||
keyFile: action.fileName,
|
|
||||||
key: action.key
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,26 +0,0 @@
|
|||||||
import { Record } from 'immutable';
|
|
||||||
import createReducer from '../util/createReducer';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
import { LOCATION_CHANGED } from '../util/router';
|
|
||||||
|
|
||||||
const State = Record({
|
|
||||||
showTabList: false,
|
|
||||||
showUserList: false
|
|
||||||
});
|
|
||||||
|
|
||||||
function hideMenu(state) {
|
|
||||||
return state.set('showTabList', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createReducer(new State(), {
|
|
||||||
[actions.TOGGLE_MENU](state) {
|
|
||||||
return state.update('showTabList', show => !show);
|
|
||||||
},
|
|
||||||
|
|
||||||
[actions.HIDE_MENU]: hideMenu,
|
|
||||||
[LOCATION_CHANGED]: hideMenu,
|
|
||||||
|
|
||||||
[actions.TOGGLE_USERLIST](state) {
|
|
||||||
return state.update('showUserList', show => !show);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,7 +1,7 @@
|
|||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import reducer from '../reducers/channels';
|
import reducer from '../channels';
|
||||||
|
import { connect } from '../servers';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import { connect } from '../actions/server';
|
|
||||||
|
|
||||||
describe('reducers/channels', () => {
|
describe('reducers/channels', () => {
|
||||||
it('removes channels on PART', () => {
|
it('removes channels on PART', () => {
|
||||||
@ -36,7 +36,7 @@ describe('reducers/channels', () => {
|
|||||||
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
||||||
|
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SOCKET_PART,
|
type: actions.socket.PART,
|
||||||
server: 'srv',
|
server: 'srv',
|
||||||
channel: 'chan1',
|
channel: 'chan1',
|
||||||
user: 'nick2'
|
user: 'nick2'
|
||||||
@ -78,7 +78,7 @@ describe('reducers/channels', () => {
|
|||||||
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
||||||
|
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SOCKET_QUIT,
|
type: actions.socket.QUIT,
|
||||||
server: 'srv',
|
server: 'srv',
|
||||||
user: 'nick2'
|
user: 'nick2'
|
||||||
});
|
});
|
||||||
@ -103,7 +103,7 @@ describe('reducers/channels', () => {
|
|||||||
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
||||||
|
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SOCKET_NICK,
|
type: actions.socket.NICK,
|
||||||
server: 'srv',
|
server: 'srv',
|
||||||
old: 'nick1',
|
old: 'nick1',
|
||||||
new: 'nick3'
|
new: 'nick3'
|
||||||
@ -128,7 +128,7 @@ describe('reducers/channels', () => {
|
|||||||
|
|
||||||
it('handles SOCKET_USERS', () => {
|
it('handles SOCKET_USERS', () => {
|
||||||
const state = reducer(undefined, {
|
const state = reducer(undefined, {
|
||||||
type: actions.SOCKET_USERS,
|
type: actions.socket.USERS,
|
||||||
server: 'srv',
|
server: 'srv',
|
||||||
channel: 'chan1',
|
channel: 'chan1',
|
||||||
users: [
|
users: [
|
||||||
@ -157,7 +157,7 @@ describe('reducers/channels', () => {
|
|||||||
|
|
||||||
it('handles SOCKET_TOPIC', () => {
|
it('handles SOCKET_TOPIC', () => {
|
||||||
const state = reducer(undefined, {
|
const state = reducer(undefined, {
|
||||||
type: actions.SOCKET_TOPIC,
|
type: actions.socket.TOPIC,
|
||||||
server: 'srv',
|
server: 'srv',
|
||||||
channel: 'chan1',
|
channel: 'chan1',
|
||||||
topic: 'the topic'
|
topic: 'the topic'
|
||||||
@ -218,7 +218,7 @@ describe('reducers/channels', () => {
|
|||||||
|
|
||||||
it('handles SOCKET_CHANNELS', () => {
|
it('handles SOCKET_CHANNELS', () => {
|
||||||
const state = reducer(undefined, {
|
const state = reducer(undefined, {
|
||||||
type: actions.SOCKET_CHANNELS,
|
type: actions.socket.CHANNELS,
|
||||||
data: [
|
data: [
|
||||||
{ server: 'srv', name: 'chan1', topic: 'the topic' },
|
{ server: 'srv', name: 'chan1', topic: 'the topic' },
|
||||||
{ server: 'srv', name: 'chan2' },
|
{ server: 'srv', name: 'chan2' },
|
||||||
@ -239,7 +239,7 @@ describe('reducers/channels', () => {
|
|||||||
|
|
||||||
it('handles SOCKET_SERVERS', () => {
|
it('handles SOCKET_SERVERS', () => {
|
||||||
const state = reducer(undefined, {
|
const state = reducer(undefined, {
|
||||||
type: actions.SOCKET_SERVERS,
|
type: actions.socket.SERVERS,
|
||||||
data: [
|
data: [
|
||||||
{ host: '127.0.0.1' },
|
{ host: '127.0.0.1' },
|
||||||
{ host: 'thehost' }
|
{ host: 'thehost' }
|
||||||
@ -279,7 +279,7 @@ describe('reducers/channels', () => {
|
|||||||
|
|
||||||
function socket_join(server, channel, user) {
|
function socket_join(server, channel, user) {
|
||||||
return {
|
return {
|
||||||
type: 'SOCKET_JOIN',
|
type: actions.socket.JOIN,
|
||||||
server, user,
|
server, user,
|
||||||
channels: [channel]
|
channels: [channel]
|
||||||
};
|
};
|
||||||
@ -287,7 +287,7 @@ function socket_join(server, channel, user) {
|
|||||||
|
|
||||||
function socket_mode(server, channel, user, add, remove) {
|
function socket_mode(server, channel, user, add, remove) {
|
||||||
return {
|
return {
|
||||||
type: 'SOCKET_MODE',
|
type: actions.socket.MODE,
|
||||||
server, channel, user, add, remove
|
server, channel, user, add, remove
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { Map, fromJS } from 'immutable';
|
import { Map, fromJS } from 'immutable';
|
||||||
import reducer from '../reducers/messages';
|
import reducer, { broadcast } from '../messages';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import { broadcast } from '../actions/message';
|
|
||||||
|
|
||||||
describe('reducers/messages', () => {
|
describe('reducers/messages', () => {
|
||||||
it('adds the message on ADD_MESSAGE', () => {
|
it('adds the message on ADD_MESSAGE', () => {
|
@ -1,7 +1,6 @@
|
|||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import reducer from '../reducers/servers';
|
import reducer, { connect } from '../servers';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import { connect } from '../actions/server';
|
|
||||||
|
|
||||||
describe('reducers/servers', () => {
|
describe('reducers/servers', () => {
|
||||||
it('adds the server on CONNECT', () => {
|
it('adds the server on CONNECT', () => {
|
||||||
@ -62,7 +61,7 @@ describe('reducers/servers', () => {
|
|||||||
it('updates the nick on SOCKET_NICK', () => {
|
it('updates the nick on SOCKET_NICK', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SOCKET_NICK,
|
type: actions.socket.NICK,
|
||||||
server: '127.0.0.1',
|
server: '127.0.0.1',
|
||||||
old: 'nick',
|
old: 'nick',
|
||||||
new: 'nick2'
|
new: 'nick2'
|
||||||
@ -79,7 +78,7 @@ describe('reducers/servers', () => {
|
|||||||
|
|
||||||
it('adds the servers on SOCKET_SERVERS', () => {
|
it('adds the servers on SOCKET_SERVERS', () => {
|
||||||
let state = reducer(undefined, {
|
let state = reducer(undefined, {
|
||||||
type: 'SOCKET_SERVERS',
|
type: actions.socket.SERVERS,
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
@ -113,7 +112,7 @@ describe('reducers/servers', () => {
|
|||||||
it('updates connection status on SOCKET_CONNECTION_UPDATE', () => {
|
it('updates connection status on SOCKET_CONNECTION_UPDATE', () => {
|
||||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
type: actions.SOCKET_CONNECTION_UPDATE,
|
type: actions.socket.CONNECTION_UPDATE,
|
||||||
'127.0.0.1': true
|
'127.0.0.1': true
|
||||||
});
|
});
|
||||||
|
|
@ -1,7 +1,6 @@
|
|||||||
import reducer from '../reducers/tab';
|
import reducer, { setSelectedTab } from '../tab';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import { setSelectedTab } from '../actions/tab';
|
import { locationChanged } from '../../util/router';
|
||||||
import { locationChanged } from '../util/router';
|
|
||||||
|
|
||||||
describe('reducers/tab', () => {
|
describe('reducers/tab', () => {
|
||||||
it('selects the tab and adds it to history', () => {
|
it('selects the tab and adds it to history', () => {
|
@ -1,48 +1,71 @@
|
|||||||
export const ADD_MESSAGE = 'ADD_MESSAGE';
|
export const INVITE = 'INVITE';
|
||||||
export const ADD_MESSAGES = 'ADD_MESSAGES';
|
export const JOIN = 'JOIN';
|
||||||
export const AWAY = 'AWAY';
|
export const KICK = 'KICK';
|
||||||
export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT';
|
export const PART = 'PART';
|
||||||
export const COMMAND = 'COMMAND';
|
|
||||||
export const CONNECT = 'CONNECT';
|
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';
|
||||||
export const DISCONNECT = 'DISCONNECT';
|
|
||||||
export const FETCH_MESSAGES = 'FETCH_MESSAGES';
|
|
||||||
export const HIDE_MENU = 'HIDE_MENU';
|
|
||||||
export const INPUT_HISTORY_ADD = 'INPUT_HISTORY_ADD';
|
export const INPUT_HISTORY_ADD = 'INPUT_HISTORY_ADD';
|
||||||
export const INPUT_HISTORY_DECREMENT = 'INPUT_HISTORY_DECREMENT';
|
export const INPUT_HISTORY_DECREMENT = 'INPUT_HISTORY_DECREMENT';
|
||||||
export const INPUT_HISTORY_INCREMENT = 'INPUT_HISTORY_INCREMENT';
|
export const INPUT_HISTORY_INCREMENT = 'INPUT_HISTORY_INCREMENT';
|
||||||
export const INPUT_HISTORY_RESET = 'INPUT_HISTORY_RESET';
|
export const INPUT_HISTORY_RESET = 'INPUT_HISTORY_RESET';
|
||||||
export const INVITE = 'INVITE';
|
|
||||||
export const JOIN = 'JOIN';
|
export const ADD_MESSAGE = 'ADD_MESSAGE';
|
||||||
export const KICK = 'KICK';
|
export const ADD_MESSAGES = 'ADD_MESSAGES';
|
||||||
export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT';
|
export const COMMAND = 'COMMAND';
|
||||||
export const PART = 'PART';
|
export const FETCH_MESSAGES = 'FETCH_MESSAGES';
|
||||||
export const RAW = 'RAW';
|
export const RAW = 'RAW';
|
||||||
|
export const UPDATE_MESSAGE_HEIGHT = 'UPDATE_MESSAGE_HEIGHT';
|
||||||
|
|
||||||
|
export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT';
|
||||||
|
export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT';
|
||||||
|
|
||||||
export const SEARCH_MESSAGES = 'SEARCH_MESSAGES';
|
export const SEARCH_MESSAGES = 'SEARCH_MESSAGES';
|
||||||
export const SELECT_TAB = 'SELECT_TAB';
|
export const TOGGLE_SEARCH = 'TOGGLE_SEARCH';
|
||||||
|
|
||||||
|
export const AWAY = 'AWAY';
|
||||||
|
export const CONNECT = 'CONNECT';
|
||||||
|
export const DISCONNECT = 'DISCONNECT';
|
||||||
|
export const SET_NICK = 'SET_NICK';
|
||||||
|
export const WHOIS = 'WHOIS';
|
||||||
|
|
||||||
export const SET_CERT = 'SET_CERT';
|
export const SET_CERT = 'SET_CERT';
|
||||||
export const SET_CERT_ERROR = 'SET_CERT_ERROR';
|
export const SET_CERT_ERROR = 'SET_CERT_ERROR';
|
||||||
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';
|
|
||||||
export const SET_KEY = 'SET_KEY';
|
export const SET_KEY = 'SET_KEY';
|
||||||
export const SET_NICK = 'SET_NICK';
|
|
||||||
export const SOCKET_CERT_FAIL = 'SOCKET_CERT_FAIL';
|
|
||||||
export const SOCKET_CERT_SUCCESS = 'SOCKET_CERT_SUCCESS';
|
|
||||||
export const SOCKET_CHANNELS = 'SOCKET_CHANNELS';
|
|
||||||
export const SOCKET_CONNECTION_UPDATE = 'SOCKET_CONNECTION_UPDATE';
|
|
||||||
export const SOCKET_JOIN = 'SOCKET_JOIN';
|
|
||||||
export const SOCKET_MESSAGE = 'SOCKET_MESSAGE';
|
|
||||||
export const SOCKET_MODE = 'SOCKET_MODE';
|
|
||||||
export const SOCKET_NICK = 'SOCKET_NICK';
|
|
||||||
export const SOCKET_PART = 'SOCKET_PART';
|
|
||||||
export const SOCKET_PM = 'SOCKET_PM';
|
|
||||||
export const SOCKET_QUIT = 'SOCKET_QUIT';
|
|
||||||
export const SOCKET_SEARCH = 'SOCKET_SEARCH';
|
|
||||||
export const SOCKET_SERVERS = 'SOCKET_SERVERS';
|
|
||||||
export const SOCKET_TOPIC = 'SOCKET_TOPIC';
|
|
||||||
export const SOCKET_USERS = 'SOCKET_USERS';
|
|
||||||
export const TAB_HISTORY_POP = 'TAB_HISTORY_POP';
|
|
||||||
export const TOGGLE_MENU = 'TOGGLE_MENU';
|
|
||||||
export const TOGGLE_SEARCH = 'TOGGLE_SEARCH';
|
|
||||||
export const TOGGLE_USERLIST = 'TOGGLE_USERLIST';
|
|
||||||
export const UPDATE_MESSAGE_HEIGHT = 'UPDATE_MESSAGE_HEIGHT';
|
|
||||||
export const UPLOAD_CERT = 'UPLOAD_CERT';
|
export const UPLOAD_CERT = 'UPLOAD_CERT';
|
||||||
export const WHOIS = 'WHOIS';
|
|
||||||
|
export const SELECT_TAB = 'SELECT_TAB';
|
||||||
|
|
||||||
|
export const HIDE_MENU = 'HIDE_MENU';
|
||||||
|
export const TOGGLE_MENU = 'TOGGLE_MENU';
|
||||||
|
export const TOGGLE_USERLIST = 'TOGGLE_USERLIST';
|
||||||
|
|
||||||
|
export function socketAction(type) {
|
||||||
|
return `SOCKET_${type.toUpperCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSocketActions(types) {
|
||||||
|
const actions = {};
|
||||||
|
types.forEach(type => {
|
||||||
|
actions[type.toUpperCase()] = socketAction(type);
|
||||||
|
});
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const socket = createSocketActions([
|
||||||
|
'cert_fail',
|
||||||
|
'cert_success',
|
||||||
|
'channels',
|
||||||
|
'connection_update',
|
||||||
|
'join',
|
||||||
|
'message',
|
||||||
|
'mode',
|
||||||
|
'nick',
|
||||||
|
'part',
|
||||||
|
'pm',
|
||||||
|
'quit',
|
||||||
|
'search',
|
||||||
|
'servers',
|
||||||
|
'topic',
|
||||||
|
'users'
|
||||||
|
]);
|
@ -1,6 +1,8 @@
|
|||||||
import { Map, List, Record } from 'immutable';
|
import { Map, List, Record } from 'immutable';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
import createReducer from '../util/createReducer';
|
import createReducer from '../util/createReducer';
|
||||||
import * as actions from '../actions';
|
import { getSelectedTab, updateSelection } from './tab';
|
||||||
|
import * as actions from './actions';
|
||||||
|
|
||||||
const User = Record({
|
const User = Record({
|
||||||
nick: null,
|
nick: null,
|
||||||
@ -74,6 +76,19 @@ function compareUsers(a, b) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getChannels = state => state.channels;
|
||||||
|
|
||||||
|
export const getSelectedChannel = createSelector(
|
||||||
|
getSelectedTab,
|
||||||
|
getChannels,
|
||||||
|
(tab, channels) => channels.getIn([tab.server, tab.name], Map())
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getSelectedChannelUsers = createSelector(
|
||||||
|
getSelectedChannel,
|
||||||
|
channel => channel.get('users', List())
|
||||||
|
);
|
||||||
|
|
||||||
export default createReducer(Map(), {
|
export default createReducer(Map(), {
|
||||||
[actions.PART](state, action) {
|
[actions.PART](state, action) {
|
||||||
const { channels, server } = action;
|
const { channels, server } = action;
|
||||||
@ -82,14 +97,14 @@ export default createReducer(Map(), {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_JOIN](state, action) {
|
[actions.socket.JOIN](state, action) {
|
||||||
const { server, channels, user } = action;
|
const { server, channels, user } = action;
|
||||||
return state.updateIn([server, channels[0], 'users'], List(), users =>
|
return state.updateIn([server, channels[0], 'users'], List(), users =>
|
||||||
users.push(createUser(user)).sort(compareUsers)
|
users.push(createUser(user)).sort(compareUsers)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_PART](state, action) {
|
[actions.socket.PART](state, action) {
|
||||||
const { server, channel, user } = action;
|
const { server, channel, user } = action;
|
||||||
if (state.hasIn([server, channel])) {
|
if (state.hasIn([server, channel])) {
|
||||||
return state.updateIn([server, channel, 'users'], users =>
|
return state.updateIn([server, channel, 'users'], users =>
|
||||||
@ -99,7 +114,7 @@ export default createReducer(Map(), {
|
|||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_QUIT](state, action) {
|
[actions.socket.QUIT](state, action) {
|
||||||
const { server, user } = action;
|
const { server, user } = action;
|
||||||
return state.withMutations(s => {
|
return state.withMutations(s => {
|
||||||
s.get(server).forEach((v, channel) => {
|
s.get(server).forEach((v, channel) => {
|
||||||
@ -108,7 +123,7 @@ export default createReducer(Map(), {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_NICK](state, action) {
|
[actions.socket.NICK](state, action) {
|
||||||
const { server } = action;
|
const { server } = action;
|
||||||
return state.withMutations(s => {
|
return state.withMutations(s => {
|
||||||
s.get(server).forEach((v, channel) => {
|
s.get(server).forEach((v, channel) => {
|
||||||
@ -126,18 +141,18 @@ export default createReducer(Map(), {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_USERS](state, action) {
|
[actions.socket.USERS](state, action) {
|
||||||
const { server, channel, users } = action;
|
const { server, channel, users } = action;
|
||||||
return state.setIn([server, channel, 'users'],
|
return state.setIn([server, channel, 'users'],
|
||||||
List(users.map(user => loadUser(user)).sort(compareUsers)));
|
List(users.map(user => loadUser(user)).sort(compareUsers)));
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_TOPIC](state, action) {
|
[actions.socket.TOPIC](state, action) {
|
||||||
const { server, channel, topic } = action;
|
const { server, channel, topic } = action;
|
||||||
return state.setIn([server, channel, 'topic'], topic);
|
return state.setIn([server, channel, 'topic'], topic);
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_MODE](state, action) {
|
[actions.socket.MODE](state, action) {
|
||||||
const { server, channel, user, remove, add } = action;
|
const { server, channel, user, remove, add } = action;
|
||||||
|
|
||||||
return state.updateIn([server, channel, 'users'], users => {
|
return state.updateIn([server, channel, 'users'], users => {
|
||||||
@ -158,7 +173,7 @@ export default createReducer(Map(), {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_CHANNELS](state, action) {
|
[actions.socket.CHANNELS](state, action) {
|
||||||
if (!action.data) {
|
if (!action.data) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -173,7 +188,7 @@ export default createReducer(Map(), {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_SERVERS](state, action) {
|
[actions.socket.SERVERS](state, action) {
|
||||||
if (!action.data) {
|
if (!action.data) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -201,3 +216,56 @@ export default createReducer(Map(), {
|
|||||||
return state.delete(action.server);
|
return state.delete(action.server);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function join(channels, server) {
|
||||||
|
return {
|
||||||
|
type: actions.JOIN,
|
||||||
|
channels,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'join',
|
||||||
|
data: { channels, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function part(channels, server) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: actions.PART,
|
||||||
|
channels,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'part',
|
||||||
|
data: { channels, server }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dispatch(updateSelection());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invite(user, channel, server) {
|
||||||
|
return {
|
||||||
|
type: actions.INVITE,
|
||||||
|
user,
|
||||||
|
channel,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'invite',
|
||||||
|
data: { user, channel, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function kick(user, channel, server) {
|
||||||
|
return {
|
||||||
|
type: actions.KICK,
|
||||||
|
user,
|
||||||
|
channel,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'kick',
|
||||||
|
data: { user, channel, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
40
client/src/js/state/environment.js
Normal file
40
client/src/js/state/environment.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Map } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
export const getEnvironment = state => state.environment;
|
||||||
|
|
||||||
|
export const getWrapWidth = state => state.environment.get('wrapWidth');
|
||||||
|
export const getCharWidth = state => state.environment.get('charWidth');
|
||||||
|
export const getWindowWidth = state => state.environment.get('windowWidth');
|
||||||
|
|
||||||
|
export const getConnectDefaults = state => state.environment.get('connect_defaults');
|
||||||
|
|
||||||
|
export default createReducer(Map(), {
|
||||||
|
[actions.SET_ENVIRONMENT](state, action) {
|
||||||
|
return state.set(action.key, action.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.UPDATE_MESSAGE_HEIGHT](state, action) {
|
||||||
|
return state
|
||||||
|
.set('wrapWidth', action.wrapWidth)
|
||||||
|
.set('charWidth', action.charWidth)
|
||||||
|
.set('windowWidth', action.windowWidth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function setEnvironment(key, value) {
|
||||||
|
return {
|
||||||
|
type: actions.SET_ENVIRONMENT,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWrapWidth(width) {
|
||||||
|
return setEnvironment('wrapWidth', width);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCharWidth(width) {
|
||||||
|
return setEnvironment('charWidth', width);
|
||||||
|
}
|
@ -10,6 +10,9 @@ import settings from './settings';
|
|||||||
import tab from './tab';
|
import tab from './tab';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
|
|
||||||
|
export * from './selectors';
|
||||||
|
export const getRouter = state => state.router;
|
||||||
|
|
||||||
export default function createReducer(router) {
|
export default function createReducer(router) {
|
||||||
return combineReducers({
|
return combineReducers({
|
||||||
router,
|
router,
|
@ -1,6 +1,6 @@
|
|||||||
import { List, Record } from 'immutable';
|
import { List, Record } from 'immutable';
|
||||||
import createReducer from '../util/createReducer';
|
import createReducer from '../util/createReducer';
|
||||||
import * as actions from '../actions';
|
import * as actions from './actions';
|
||||||
|
|
||||||
const HISTORY_MAX_LENGTH = 128;
|
const HISTORY_MAX_LENGTH = 128;
|
||||||
|
|
||||||
@ -9,6 +9,14 @@ const State = Record({
|
|||||||
index: 0
|
index: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getCurrentInputHistoryEntry = state => {
|
||||||
|
if (state.input.index === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.input.history.get(state.input.index);
|
||||||
|
};
|
||||||
|
|
||||||
export default createReducer(new State(), {
|
export default createReducer(new State(), {
|
||||||
[actions.INPUT_HISTORY_ADD](state, action) {
|
[actions.INPUT_HISTORY_ADD](state, action) {
|
||||||
const { line } = action;
|
const { line } = action;
|
||||||
@ -43,3 +51,28 @@ export default createReducer(new State(), {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function addInputHistory(line) {
|
||||||
|
return {
|
||||||
|
type: actions.INPUT_HISTORY_ADD,
|
||||||
|
line
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetInputHistory() {
|
||||||
|
return {
|
||||||
|
type: actions.INPUT_HISTORY_RESET
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementInputHistory() {
|
||||||
|
return {
|
||||||
|
type: actions.INPUT_HISTORY_INCREMENT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrementInputHistory() {
|
||||||
|
return {
|
||||||
|
type: actions.INPUT_HISTORY_DECREMENT
|
||||||
|
};
|
||||||
|
}
|
@ -1,10 +1,87 @@
|
|||||||
import * as actions from '../actions';
|
import { List, Map, Record } from 'immutable';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import { getWrapWidth, getCharWidth, getWindowWidth } from './environment';
|
||||||
|
import { getSelectedTab } from './tab';
|
||||||
import { findBreakpoints, messageHeight, linkify, timestamp } from '../util';
|
import { findBreakpoints, messageHeight, linkify, timestamp } from '../util';
|
||||||
import { getSelectedMessages } from '../reducers/messages';
|
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())
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getHasMoreMessages = createSelector(
|
||||||
|
getSelectedMessages,
|
||||||
|
messages => {
|
||||||
|
const first = messages.get(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))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let nextID = 0;
|
let nextID = 0;
|
||||||
|
|
||||||
function initMessage(message, server, tab, state) {
|
function initMessage(message, tab, state) {
|
||||||
if (message.time) {
|
if (message.time) {
|
||||||
message.time = timestamp(new Date(message.time * 1000));
|
message.time = timestamp(new Date(message.time * 1000));
|
||||||
} else {
|
} else {
|
||||||
@ -30,12 +107,13 @@ function initMessage(message, server, tab, state) {
|
|||||||
message.content = from + message.content.slice(7, -1);
|
message.content = from + message.content.slice(7, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const charWidth = state.environment.get('charWidth');
|
const wrapWidth = getWrapWidth(state);
|
||||||
const wrapWidth = state.environment.get('wrapWidth');
|
const charWidth = getCharWidth(state);
|
||||||
|
const windowWidth = getWindowWidth(state);
|
||||||
|
|
||||||
message.length = message.content.length;
|
message.length = message.content.length;
|
||||||
message.breakpoints = findBreakpoints(message.content);
|
message.breakpoints = findBreakpoints(message.content);
|
||||||
message.height = messageHeight(message, wrapWidth, charWidth, 6 * charWidth);
|
message.height = messageHeight(message, wrapWidth, charWidth, 6 * charWidth, windowWidth);
|
||||||
message.content = linkify(message.content);
|
message.content = linkify(message.content);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
@ -74,11 +152,12 @@ export function fetchMessages() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateMessageHeight(wrapWidth, charWidth) {
|
export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
|
||||||
return {
|
return {
|
||||||
type: actions.UPDATE_MESSAGE_HEIGHT,
|
type: actions.UPDATE_MESSAGE_HEIGHT,
|
||||||
wrapWidth,
|
wrapWidth,
|
||||||
charWidth
|
charWidth,
|
||||||
|
windowWidth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +172,7 @@ export function sendMessage(content, to, server) {
|
|||||||
message: initMessage({
|
message: initMessage({
|
||||||
from: state.servers.getIn([server, 'nick']),
|
from: state.servers.getIn([server, 'nick']),
|
||||||
content
|
content
|
||||||
}, server, to, state),
|
}, to, state),
|
||||||
socket: {
|
socket: {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
data: { content, to, server }
|
data: { content, to, server }
|
||||||
@ -109,7 +188,7 @@ export function addMessage(message, server, to) {
|
|||||||
type: actions.ADD_MESSAGE,
|
type: actions.ADD_MESSAGE,
|
||||||
server,
|
server,
|
||||||
tab,
|
tab,
|
||||||
message: initMessage(message, server, tab, getState())
|
message: initMessage(message, tab, getState())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +203,7 @@ export function addMessages(messages, server, to, prepend, next) {
|
|||||||
messages[0].next = true;
|
messages[0].next = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.forEach(message => initMessage(message, server, message.tab || tab, state));
|
messages.forEach(message => initMessage(message, message.tab || tab, state));
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: actions.ADD_MESSAGES,
|
type: actions.ADD_MESSAGES,
|
@ -1,6 +1,9 @@
|
|||||||
import { Set, Map } from 'immutable';
|
import { Set, Map } from 'immutable';
|
||||||
import createReducer from '../util/createReducer';
|
import createReducer from '../util/createReducer';
|
||||||
import * as actions from '../actions';
|
import { updateSelection } from './tab';
|
||||||
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
export const getPrivateChats = state => state.privateChats;
|
||||||
|
|
||||||
function open(state, server, nick) {
|
function open(state, server, nick) {
|
||||||
return state.update(server, Set(), chats => chats.add(nick));
|
return state.update(server, Set(), chats => chats.add(nick));
|
||||||
@ -15,7 +18,7 @@ export default createReducer(Map(), {
|
|||||||
return state.update(action.server, chats => chats.delete(action.nick));
|
return state.update(action.server, chats => chats.delete(action.nick));
|
||||||
},
|
},
|
||||||
|
|
||||||
[actions.SOCKET_PM](state, action) {
|
[actions.socket.PM](state, action) {
|
||||||
if (action.from.indexOf('.') === -1) {
|
if (action.from.indexOf('.') === -1) {
|
||||||
return open(state, action.server, action.from);
|
return open(state, action.server, action.from);
|
||||||
}
|
}
|
||||||
@ -27,3 +30,22 @@ export default createReducer(Map(), {
|
|||||||
return state.delete(action.server);
|
return state.delete(action.server);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function openPrivateChat(server, nick) {
|
||||||
|
return {
|
||||||
|
type: actions.OPEN_PRIVATE_CHAT,
|
||||||
|
server,
|
||||||
|
nick
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closePrivateChat(server, nick) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: actions.CLOSE_PRIVATE_CHAT,
|
||||||
|
server,
|
||||||
|
nick
|
||||||
|
});
|
||||||
|
dispatch(updateSelection());
|
||||||
|
};
|
||||||
|
}
|
39
client/src/js/state/search.js
Normal file
39
client/src/js/state/search.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { List, Record } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
const State = Record({
|
||||||
|
show: false,
|
||||||
|
results: List()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getSearch = state => state.search;
|
||||||
|
|
||||||
|
export default createReducer(new State(), {
|
||||||
|
[actions.socket.SEARCH](state, action) {
|
||||||
|
return state.set('results', List(action.results));
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.TOGGLE_SEARCH](state) {
|
||||||
|
return state.set('show', !state.show);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function searchMessages(server, channel, phrase) {
|
||||||
|
return {
|
||||||
|
type: actions.SEARCH_MESSAGES,
|
||||||
|
server,
|
||||||
|
channel,
|
||||||
|
phrase,
|
||||||
|
socket: {
|
||||||
|
type: 'search',
|
||||||
|
data: { server, channel, phrase }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleSearch() {
|
||||||
|
return {
|
||||||
|
type: actions.TOGGLE_SEARCH
|
||||||
|
};
|
||||||
|
}
|
10
client/src/js/state/selectors.js
Normal file
10
client/src/js/state/selectors.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { getServers } from './servers';
|
||||||
|
import { getSelectedTab } from './tab';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const getSelectedTabTitle = createSelector(
|
||||||
|
getSelectedTab,
|
||||||
|
getServers,
|
||||||
|
(tab, servers) => tab.name || servers.getIn([tab.server, 'name'])
|
||||||
|
);
|
154
client/src/js/state/servers.js
Normal file
154
client/src/js/state/servers.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { Map, Record } from 'immutable';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import { getSelectedTab, updateSelection } from './tab';
|
||||||
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
const Server = Record({
|
||||||
|
nick: null,
|
||||||
|
name: null,
|
||||||
|
connected: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getServers = state => state.servers;
|
||||||
|
|
||||||
|
export const getCurrentNick = createSelector(
|
||||||
|
getServers,
|
||||||
|
getSelectedTab,
|
||||||
|
(servers, tab) => servers.getIn([tab.server, 'nick'], '')
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getCurrentServerName = createSelector(
|
||||||
|
getServers,
|
||||||
|
getSelectedTab,
|
||||||
|
(servers, tab) => servers.getIn([tab.server, 'name'], '')
|
||||||
|
);
|
||||||
|
|
||||||
|
export default createReducer(Map(), {
|
||||||
|
[actions.CONNECT](state, action) {
|
||||||
|
const { host, nick, options } = action;
|
||||||
|
|
||||||
|
if (!state.has(host)) {
|
||||||
|
return state.set(host, new Server({
|
||||||
|
nick,
|
||||||
|
name: options.name || host
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.DISCONNECT](state, action) {
|
||||||
|
return state.delete(action.server);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.socket.NICK](state, action) {
|
||||||
|
const { server, old } = action;
|
||||||
|
if (!old || old === state.get(server).nick) {
|
||||||
|
return state.update(server, s => s.set('nick', action.new));
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.socket.SERVERS](state, action) {
|
||||||
|
if (!action.data) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.withMutations(s => {
|
||||||
|
action.data.forEach(server => {
|
||||||
|
s.set(server.host, new Server(server));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.socket.CONNECTION_UPDATE](state, action) {
|
||||||
|
return state.withMutations(s =>
|
||||||
|
Object.keys(action).forEach(server => {
|
||||||
|
if (s.has(server)) {
|
||||||
|
s.setIn([server, 'connected'], action[server]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function connect(server, nick, options) {
|
||||||
|
let host = server;
|
||||||
|
const i = server.indexOf(':');
|
||||||
|
if (i > 0) {
|
||||||
|
host = server.slice(0, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: actions.CONNECT,
|
||||||
|
host,
|
||||||
|
nick,
|
||||||
|
options,
|
||||||
|
socket: {
|
||||||
|
type: 'connect',
|
||||||
|
data: {
|
||||||
|
server,
|
||||||
|
nick,
|
||||||
|
username: options.username || nick,
|
||||||
|
password: options.password,
|
||||||
|
realname: options.realname || nick,
|
||||||
|
tls: options.tls || false,
|
||||||
|
name: options.name || server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disconnect(server) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: actions.DISCONNECT,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'quit',
|
||||||
|
data: { server }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dispatch(updateSelection());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function whois(user, server) {
|
||||||
|
return {
|
||||||
|
type: actions.WHOIS,
|
||||||
|
user,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'whois',
|
||||||
|
data: { user, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function away(message, server) {
|
||||||
|
return {
|
||||||
|
type: actions.AWAY,
|
||||||
|
message,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'away',
|
||||||
|
data: { message, server }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNick(nick, server) {
|
||||||
|
return {
|
||||||
|
type: actions.SET_NICK,
|
||||||
|
nick,
|
||||||
|
server,
|
||||||
|
socket: {
|
||||||
|
type: 'nick',
|
||||||
|
data: {
|
||||||
|
new: nick,
|
||||||
|
server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
87
client/src/js/state/settings.js
Normal file
87
client/src/js/state/settings.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Map } from 'immutable';
|
||||||
|
import base64 from 'base64-arraybuffer';
|
||||||
|
import createReducer from '../util/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);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.socket.CERT_SUCCESS]() {
|
||||||
|
return Map({ uploadingCert: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.socket.CERT_FAIL](state, action) {
|
||||||
|
return state.merge({
|
||||||
|
uploadingCert: false,
|
||||||
|
certError: action.message
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SET_CERT_ERROR](state, action) {
|
||||||
|
return state.merge({
|
||||||
|
uploadingCert: false,
|
||||||
|
certError: action.message
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SET_CERT](state, action) {
|
||||||
|
return state.merge({
|
||||||
|
certFile: action.fileName,
|
||||||
|
cert: action.cert
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.SET_KEY](state, action) {
|
||||||
|
return state.merge({
|
||||||
|
keyFile: action.fileName,
|
||||||
|
key: action.key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function setCertError(message) {
|
||||||
|
return {
|
||||||
|
type: actions.SET_CERT_ERROR,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadCert() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { settings } = getState();
|
||||||
|
if (settings.has('cert') && settings.has('key')) {
|
||||||
|
dispatch({
|
||||||
|
type: actions.UPLOAD_CERT,
|
||||||
|
socket: {
|
||||||
|
type: 'cert',
|
||||||
|
data: {
|
||||||
|
cert: settings.get('cert'),
|
||||||
|
key: settings.get('key')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch(setCertError('Missing certificate or key'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCert(fileName, cert) {
|
||||||
|
return {
|
||||||
|
type: actions.SET_CERT,
|
||||||
|
fileName,
|
||||||
|
cert: base64.encode(cert)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setKey(fileName, key) {
|
||||||
|
return {
|
||||||
|
type: actions.SET_KEY,
|
||||||
|
fileName,
|
||||||
|
key: base64.encode(key)
|
||||||
|
};
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { Record, List } from 'immutable';
|
import { Record, List } from 'immutable';
|
||||||
import { LOCATION_CHANGED } from '../util/router';
|
import { push, replace, LOCATION_CHANGED } from '../util/router';
|
||||||
import createReducer from '../util/createReducer';
|
import createReducer from '../util/createReducer';
|
||||||
import * as actions from '../actions';
|
import * as actions from './actions';
|
||||||
|
|
||||||
const TabRecord = Record({
|
const TabRecord = Record({
|
||||||
server: null,
|
server: null,
|
||||||
@ -64,3 +64,39 @@ export default createReducer(new State(), {
|
|||||||
return state.set('selected', new Tab());
|
return state.set('selected', new Tab());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function select(server, name, doReplace) {
|
||||||
|
const navigate = doReplace ? replace : push;
|
||||||
|
if (name) {
|
||||||
|
return navigate(`/${server}/${encodeURIComponent(name)}`);
|
||||||
|
}
|
||||||
|
return navigate(`/${server}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSelection() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const history = state.tab.history;
|
||||||
|
const { servers } = state;
|
||||||
|
const { server } = state.tab.selected;
|
||||||
|
|
||||||
|
if (servers.size === 0) {
|
||||||
|
dispatch(replace('/connect'));
|
||||||
|
} else if (history.size > 0) {
|
||||||
|
const tab = history.last();
|
||||||
|
dispatch(select(tab.server, tab.name, true));
|
||||||
|
} else if (servers.has(server)) {
|
||||||
|
dispatch(select(server, null, true));
|
||||||
|
} else {
|
||||||
|
dispatch(select(servers.keySeq().first(), null, true));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSelectedTab(server, name = null) {
|
||||||
|
return {
|
||||||
|
type: actions.SELECT_TAB,
|
||||||
|
server,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
}
|
47
client/src/js/state/ui.js
Normal file
47
client/src/js/state/ui.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Record } from 'immutable';
|
||||||
|
import createReducer from '../util/createReducer';
|
||||||
|
import { LOCATION_CHANGED } from '../util/router';
|
||||||
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
const State = Record({
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createReducer(new State(), {
|
||||||
|
[actions.TOGGLE_MENU](state) {
|
||||||
|
return state.update('showTabList', show => !show);
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.HIDE_MENU]: setMenuHidden,
|
||||||
|
[LOCATION_CHANGED]: setMenuHidden,
|
||||||
|
|
||||||
|
[actions.TOGGLE_USERLIST](state) {
|
||||||
|
return state.update('showUserList', show => !show);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function hideMenu() {
|
||||||
|
return {
|
||||||
|
type: actions.HIDE_MENU
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleMenu() {
|
||||||
|
return {
|
||||||
|
type: actions.TOGGLE_MENU
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleUserList() {
|
||||||
|
return {
|
||||||
|
type: actions.TOGGLE_USERLIST
|
||||||
|
};
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import { createStore, applyMiddleware, compose } from 'redux';
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import createReducer from '../reducers';
|
import createReducer from './state';
|
||||||
import { routeReducer, routeMiddleware } from '../util/router';
|
import { routeReducer, routeMiddleware } from './util/router';
|
||||||
import createSocketMiddleware from '../middleware/socket';
|
import createSocketMiddleware from './middleware/socket';
|
||||||
import commands from '../commands';
|
import commands from './commands';
|
||||||
|
|
||||||
export default function configureStore(socket) {
|
export default function configureStore(socket) {
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
@ -1,64 +1,6 @@
|
|||||||
import FontFaceObserver from 'fontfaceobserver';
|
|
||||||
import { stringWidth, measureScrollBarWidth } from './index';
|
|
||||||
import { updateMessageHeight } from '../actions/message';
|
|
||||||
|
|
||||||
const lineHeight = 24;
|
const lineHeight = 24;
|
||||||
const menuWidth = 200;
|
|
||||||
const userListWidth = 200;
|
const userListWidth = 200;
|
||||||
const messagePadding = 30;
|
|
||||||
const smallScreen = 600;
|
const smallScreen = 600;
|
||||||
let windowWidth;
|
|
||||||
|
|
||||||
function init(store, charWidth, done) {
|
|
||||||
window.messageIndent = 6 * charWidth;
|
|
||||||
const scrollBarWidth = measureScrollBarWidth();
|
|
||||||
let prevWrapWidth;
|
|
||||||
|
|
||||||
function updateWidth() {
|
|
||||||
windowWidth = window.innerWidth;
|
|
||||||
let wrapWidth = windowWidth - scrollBarWidth - messagePadding;
|
|
||||||
if (windowWidth > smallScreen) {
|
|
||||||
wrapWidth -= menuWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wrapWidth !== prevWrapWidth) {
|
|
||||||
prevWrapWidth = wrapWidth;
|
|
||||||
store.dispatch(updateMessageHeight(wrapWidth, charWidth));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let resizeRAF;
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
if (resizeRAF) {
|
|
||||||
window.cancelAnimationFrame(resizeRAF);
|
|
||||||
}
|
|
||||||
resizeRAF = window.requestAnimationFrame(updateWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateWidth();
|
|
||||||
done();
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initWidthUpdates(store, done) {
|
|
||||||
let charWidth = localStorage.charWidth;
|
|
||||||
if (charWidth) {
|
|
||||||
init(store, parseFloat(charWidth), done);
|
|
||||||
}
|
|
||||||
|
|
||||||
new FontFaceObserver('Roboto Mono').load().then(() => {
|
|
||||||
if (!charWidth) {
|
|
||||||
charWidth = stringWidth(' ', '16px Roboto Mono');
|
|
||||||
init(store, charWidth, done);
|
|
||||||
localStorage.charWidth = charWidth;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new FontFaceObserver('Montserrat').load();
|
|
||||||
new FontFaceObserver('Montserrat', { weight: 700 }).load();
|
|
||||||
new FontFaceObserver('Roboto Mono', { weight: 700 }).load();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findBreakpoints(text) {
|
export function findBreakpoints(text) {
|
||||||
const breakpoints = [];
|
const breakpoints = [];
|
||||||
@ -76,15 +18,15 @@ export function findBreakpoints(text) {
|
|||||||
return breakpoints;
|
return breakpoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function messageHeight(message, width, charWidth, indent = 0) {
|
export function messageHeight(message, wrapWidth, charWidth, indent = 0, windowWidth) {
|
||||||
let pad = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
|
let pad = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
|
||||||
let height = lineHeight + 8;
|
let height = lineHeight + 8;
|
||||||
|
|
||||||
if (message.channel && windowWidth > smallScreen) {
|
if (message.channel && windowWidth > smallScreen) {
|
||||||
width -= userListWidth;
|
wrapWidth -= userListWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pad + (message.length * charWidth) < width) {
|
if (pad + (message.length * charWidth) < wrapWidth) {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +35,7 @@ export function messageHeight(message, width, charWidth, indent = 0) {
|
|||||||
let prevPos = 0;
|
let prevPos = 0;
|
||||||
|
|
||||||
for (let i = 0; i < breaks.length; i++) {
|
for (let i = 0; i < breaks.length; i++) {
|
||||||
if (pad + ((breaks[i].end - prevBreak) * charWidth) >= width) {
|
if (pad + ((breaks[i].end - prevBreak) * charWidth) >= wrapWidth) {
|
||||||
prevBreak = prevPos;
|
prevBreak = prevPos;
|
||||||
pad = indent;
|
pad = indent;
|
||||||
height += lineHeight;
|
height += lineHeight;
|
||||||
@ -102,7 +44,7 @@ export function messageHeight(message, width, charWidth, indent = 0) {
|
|||||||
prevPos = breaks[i].next;
|
prevPos = breaks[i].next;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pad + ((message.length - prevBreak) * charWidth) >= width) {
|
if (pad + ((message.length - prevBreak) * charWidth) >= wrapWidth) {
|
||||||
height += lineHeight;
|
height += lineHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,11 @@
|
|||||||
function subscribe(store, selector, handler) {
|
function subscribeArray(store, selectors, handler, init) {
|
||||||
let prev = selector(store.getState());
|
|
||||||
handler(prev);
|
|
||||||
|
|
||||||
store.subscribe(() => {
|
|
||||||
const next = selector(store.getState());
|
|
||||||
if (next !== prev) {
|
|
||||||
handler(next);
|
|
||||||
prev = next;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeArray(store, selectors, handler) {
|
|
||||||
let state = store.getState();
|
let state = store.getState();
|
||||||
let prev = selectors.map(selector => selector(state));
|
let prev = selectors.map(selector => selector(state));
|
||||||
handler(...prev);
|
if (init) {
|
||||||
|
handler(...prev);
|
||||||
|
}
|
||||||
|
|
||||||
store.subscribe(() => {
|
return store.subscribe(() => {
|
||||||
state = store.getState();
|
state = store.getState();
|
||||||
const next = [];
|
const next = [];
|
||||||
let changed = false;
|
let changed = false;
|
||||||
@ -35,10 +24,88 @@ function subscribeArray(store, selectors, handler) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function observe(store, selector, handler) {
|
function subscribe(store, selector, handler, init) {
|
||||||
if (Array.isArray(selector)) {
|
if (Array.isArray(selector)) {
|
||||||
subscribeArray(store, selector, handler);
|
return subscribeArray(store, selector, handler, init);
|
||||||
} else {
|
|
||||||
subscribe(store, selector, handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let prev = selector(store.getState());
|
||||||
|
if (init) {
|
||||||
|
handler(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.subscribe(() => {
|
||||||
|
const next = selector(store.getState());
|
||||||
|
if (next !== prev) {
|
||||||
|
handler(next);
|
||||||
|
prev = next;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Handler gets called every time the selector(s) change
|
||||||
|
//
|
||||||
|
export function observe(store, selector, handler) {
|
||||||
|
return subscribe(store, selector, handler, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Handler gets called once the next time the selector(s) change
|
||||||
|
//
|
||||||
|
export function once(store, selector, handler) {
|
||||||
|
let done = false;
|
||||||
|
const unsubscribe = subscribe(store, selector, (...args) => {
|
||||||
|
if (!done) {
|
||||||
|
done = true;
|
||||||
|
handler(...args);
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Handler gets called once when the predicate returns true, the predicate gets passed
|
||||||
|
// the result of the selector(s), if no predicate is set it defaults to checking if the
|
||||||
|
// selector(s) return something truthy
|
||||||
|
//
|
||||||
|
export function when(store, selector, predicate, handler) {
|
||||||
|
if (arguments.length === 3) {
|
||||||
|
handler = predicate;
|
||||||
|
|
||||||
|
if (Array.isArray(selector)) {
|
||||||
|
predicate = (...args) => {
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (!args[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
predicate = o => o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
if (Array.isArray(selector)) {
|
||||||
|
const val = selector.map(s => s(state));
|
||||||
|
if (predicate(...val)) {
|
||||||
|
return handler(...val);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const val = selector(state);
|
||||||
|
if (predicate(val)) {
|
||||||
|
return handler(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let done = false;
|
||||||
|
const unsubscribe = subscribe(store, selector, (...args) => {
|
||||||
|
if (!done && predicate(...args)) {
|
||||||
|
done = true;
|
||||||
|
handler(...args);
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user