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 { COMMAND } from './actions';
|
||||
import { setNick, disconnect, whois, away } from './actions/server';
|
||||
import { join, part, invite, kick } from './actions/channel';
|
||||
import { select } from './actions/tab';
|
||||
import { sendMessage, addMessage, raw } from './actions/message';
|
||||
import { COMMAND } from './state/actions';
|
||||
import { join, part, invite, kick } from './state/channels';
|
||||
import { sendMessage, addMessage, raw } from './state/messages';
|
||||
import { setNick, disconnect, whois, away } from './state/servers';
|
||||
import { select } from './state/tab';
|
||||
|
||||
const help = [
|
||||
'/join <channel> - Join a channel',
|
||||
|
@ -6,8 +6,8 @@ export default class MessageInput extends PureComponent {
|
||||
};
|
||||
|
||||
handleKey = e => {
|
||||
const { tab, runCommand, sendMessage, addInputHistory, incrementInputHistory,
|
||||
decrementInputHistory, resetInputHistory, history } = this.props;
|
||||
const { tab, runCommand, sendMessage,
|
||||
add, reset, increment, decrement, currentHistoryEntry } = this.props;
|
||||
|
||||
if (e.key === 'Enter' && e.target.value) {
|
||||
if (e.target.value[0] === '/') {
|
||||
@ -16,17 +16,17 @@ export default class MessageInput extends PureComponent {
|
||||
sendMessage(e.target.value, tab.name, tab.server);
|
||||
}
|
||||
|
||||
addInputHistory(e.target.value);
|
||||
resetInputHistory();
|
||||
add(e.target.value);
|
||||
reset();
|
||||
this.setState({ value: '' });
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
incrementInputHistory();
|
||||
increment();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
decrementInputHistory();
|
||||
} else if (history) {
|
||||
decrement();
|
||||
} else if (currentHistoryEntry) {
|
||||
this.setState({ value: e.target.value });
|
||||
resetInputHistory();
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
@ -35,14 +35,14 @@ export default class MessageInput extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { nick } = this.props;
|
||||
const { nick, currentHistoryEntry } = this.props;
|
||||
return (
|
||||
<div className="message-input-wrap">
|
||||
<span className="message-input-nick">{nick}</span>
|
||||
<input
|
||||
className="message-input"
|
||||
type="text"
|
||||
value={this.props.history || this.state.value}
|
||||
value={currentHistoryEntry || this.state.value}
|
||||
onKeyDown={this.handleKey}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { toggleMenu } from '../actions/ui';
|
||||
import { toggleMenu } from '../state/ui';
|
||||
|
||||
class Navicon extends PureComponent {
|
||||
render() {
|
||||
|
@ -35,7 +35,7 @@ export default class TabList extends PureComponent {
|
||||
));
|
||||
|
||||
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(
|
||||
<TabListItem
|
||||
|
@ -1,13 +1,17 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { push } from '../util/router';
|
||||
import Route from './Route';
|
||||
import Chat from './Chat';
|
||||
import Connect from './Connect';
|
||||
import Settings from './Settings';
|
||||
import TabList from '../components/TabList';
|
||||
import { select } from '../actions/tab';
|
||||
import { hideMenu } from '../actions/ui';
|
||||
import { getChannels } from '../state/channels';
|
||||
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 {
|
||||
handleClick = () => {
|
||||
@ -33,14 +37,12 @@ class App extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
servers: state.servers,
|
||||
channels: state.channels,
|
||||
privateChats: state.privateChats,
|
||||
showTabList: state.ui.showTabList,
|
||||
tab: state.tab.selected
|
||||
};
|
||||
}
|
||||
const mapState = createStructuredSelector({
|
||||
channels: getChannels,
|
||||
privateChats: getPrivateChats,
|
||||
servers: getServers,
|
||||
showTabList: getShowTabList,
|
||||
tab: getSelectedTab
|
||||
});
|
||||
|
||||
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 { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
import { List, Map } from 'immutable';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import ChatTitle from '../components/ChatTitle';
|
||||
import Search from '../components/Search';
|
||||
import MessageBox from '../components/MessageBox';
|
||||
import MessageInput from '../components/MessageInput';
|
||||
import UserList from '../components/UserList';
|
||||
import { part } from '../actions/channel';
|
||||
import { openPrivateChat, closePrivateChat } from '../actions/privateChat';
|
||||
import { searchMessages, toggleSearch } from '../actions/search';
|
||||
import { select } from '../actions/tab';
|
||||
import { runCommand, sendMessage, fetchMessages } from '../actions/message';
|
||||
import { disconnect } from '../actions/server';
|
||||
import { toggleUserList } from '../actions/ui';
|
||||
import * as inputHistoryActions from '../actions/inputHistory';
|
||||
import { getSelectedTab } from '../reducers/tab';
|
||||
import { getSelectedMessages } from '../reducers/messages';
|
||||
import { getCurrentNick } from '../reducers/servers';
|
||||
import { getSelectedTabTitle } from '../state';
|
||||
import { getSelectedChannel, getSelectedChannelUsers, part } from '../state/channels';
|
||||
import { getCurrentInputHistoryEntry, addInputHistory, resetInputHistory,
|
||||
incrementInputHistory, decrementInputHistory } from '../state/input';
|
||||
import { getSelectedMessages, getHasMoreMessages,
|
||||
runCommand, sendMessage, fetchMessages } from '../state/messages';
|
||||
import { openPrivateChat, closePrivateChat } from '../state/privateChats';
|
||||
import { getSearch, searchMessages, toggleSearch } from '../state/search';
|
||||
import { getCurrentNick, disconnect } from '../state/servers';
|
||||
import { getSelectedTab, select } from '../state/tab';
|
||||
import { getShowUserList, toggleUserList } from '../state/ui';
|
||||
|
||||
class Chat extends PureComponent {
|
||||
handleSearch = phrase => {
|
||||
@ -38,7 +37,7 @@ class Chat extends PureComponent {
|
||||
handleFetchMore = () => this.props.dispatch(fetchMessages());
|
||||
|
||||
render() {
|
||||
const { title, tab, channel, search, history,
|
||||
const { title, tab, channel, search, currentInputHistoryEntry,
|
||||
messages, hasMoreMessages, users, showUserList, nick, inputActions } = this.props;
|
||||
|
||||
let chatClass;
|
||||
@ -76,7 +75,7 @@ class Chat extends PureComponent {
|
||||
<MessageInput
|
||||
tab={tab}
|
||||
channel={channel}
|
||||
history={history}
|
||||
currentHistoryEntry={currentInputHistoryEntry}
|
||||
nick={nick}
|
||||
runCommand={this.props.runCommand}
|
||||
sendMessage={this.props.sendMessage}
|
||||
@ -94,73 +93,41 @@ class Chat extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const serverSelector = state => state.servers;
|
||||
const channelSelector = state => state.channels;
|
||||
const searchSelector = state => state.search;
|
||||
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,
|
||||
const mapState = createStructuredSelector({
|
||||
channel: getSelectedChannel,
|
||||
currentInputHistoryEntry: getCurrentInputHistoryEntry,
|
||||
hasMoreMessages: getHasMoreMessages,
|
||||
users: usersSelector,
|
||||
showUserList: showUserListSelector,
|
||||
search: searchSelector,
|
||||
history: historySelector,
|
||||
nick: getCurrentNick
|
||||
messages: getSelectedMessages,
|
||||
nick: getCurrentNick,
|
||||
search: getSearch,
|
||||
showUserList: getShowUserList,
|
||||
tab: getSelectedTab,
|
||||
title: getSelectedTabTitle,
|
||||
users: getSelectedChannelUsers
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
function mapDispatch(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
...bindActionCreators({
|
||||
select,
|
||||
toggleSearch,
|
||||
toggleUserList,
|
||||
searchMessages,
|
||||
runCommand,
|
||||
sendMessage,
|
||||
part,
|
||||
closePrivateChat,
|
||||
disconnect,
|
||||
openPrivateChat,
|
||||
closePrivateChat
|
||||
part,
|
||||
runCommand,
|
||||
searchMessages,
|
||||
select,
|
||||
sendMessage,
|
||||
toggleSearch,
|
||||
toggleUserList
|
||||
}, 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 { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import Navicon from '../components/Navicon';
|
||||
import * as serverActions from '../actions/server';
|
||||
import { join } from '../actions/channel';
|
||||
import { select } from '../actions/tab';
|
||||
import { join } from '../state/channels';
|
||||
import { getConnectDefaults } from '../state/environment';
|
||||
import { connect as connectServer } from '../state/servers';
|
||||
import { select } from '../state/tab';
|
||||
|
||||
class Connect extends PureComponent {
|
||||
state = {
|
||||
@ -33,7 +35,7 @@ class Connect extends PureComponent {
|
||||
}
|
||||
|
||||
if (address.indexOf('.') > 0 && nick) {
|
||||
dispatch(serverActions.connect(address, nick, opts));
|
||||
dispatch(connectServer(address, nick, opts));
|
||||
|
||||
const i = address.indexOf(':');
|
||||
if (i > 0) {
|
||||
@ -102,10 +104,8 @@ class Connect extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
defaults: state.environment.get('connect_defaults')
|
||||
};
|
||||
}
|
||||
const mapState = createStructuredSelector({
|
||||
defaults: getConnectDefaults
|
||||
});
|
||||
|
||||
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 mapStateToProps = createStructuredSelector({
|
||||
const mapState = createStructuredSelector({
|
||||
route: getRoute
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Route);
|
||||
export default connect(mapState)(Route);
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import Navicon from '../components/Navicon';
|
||||
import FileInput from '../components/FileInput';
|
||||
import { setCert, setKey, uploadCert } from '../actions/settings';
|
||||
import { getSettings, setCert, setKey, uploadCert } from '../state/settings';
|
||||
|
||||
class Settings extends PureComponent {
|
||||
handleCertChange = (name, data) => this.props.dispatch(setCert(name, data));
|
||||
@ -40,6 +41,8 @@ class Settings extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
settings: state.settings
|
||||
}))(Settings);
|
||||
const mapState = createStructuredSelector({
|
||||
settings: getSettings
|
||||
});
|
||||
|
||||
export default connect(mapState)(Settings);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { inform } from '../actions/message';
|
||||
import { inform } from '../state/messages';
|
||||
|
||||
const notFound = 'commandNotFound';
|
||||
|
||||
|
@ -1,14 +1,10 @@
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import observe from '../util/observe';
|
||||
import { getCurrentServerName } from '../reducers/servers';
|
||||
|
||||
const getRouter = state => state.router;
|
||||
import { getRouter } from '../state';
|
||||
import { getCurrentServerName } from '../state/servers';
|
||||
import { observe } from '../util/observe';
|
||||
|
||||
export default function documentTitle({ store }) {
|
||||
observe(
|
||||
store,
|
||||
[getRouter, getCurrentServerName],
|
||||
(router, serverName) => {
|
||||
observe(store, [getRouter, getCurrentServerName], (router, serverName) => {
|
||||
let title;
|
||||
|
||||
if (router.route === 'chat') {
|
||||
@ -23,6 +19,5 @@ export default function documentTitle({ store }) {
|
||||
}
|
||||
|
||||
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 handleSocket from './handleSocket';
|
||||
import fonts from './fonts';
|
||||
import initialState from './initialState';
|
||||
import socket from './socket';
|
||||
import storage from './storage';
|
||||
import widthUpdates from './widthUpdates';
|
||||
|
||||
export default function runModules(ctx) {
|
||||
fonts(ctx);
|
||||
initialState(ctx);
|
||||
|
||||
documentTitle(ctx);
|
||||
handleSocket(ctx);
|
||||
socket(ctx);
|
||||
storage(ctx);
|
||||
widthUpdates(ctx);
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import Cookie from 'js-cookie';
|
||||
import { setEnvironment } from '../actions/environment';
|
||||
import { addMessages } from '../actions/message';
|
||||
import { select, updateSelection } from '../actions/tab';
|
||||
import { socket as socketActions } from '../state/actions';
|
||||
import { getWrapWidth, setEnvironment } from '../state/environment';
|
||||
import { addMessages } from '../state/messages';
|
||||
import { select, updateSelection } from '../state/tab';
|
||||
import { find } from '../util';
|
||||
import { initWidthUpdates } from '../util/messageHeight';
|
||||
import { when } from '../util/observe';
|
||||
import { replace } from '../util/router';
|
||||
|
||||
export default function initialState({ store }) {
|
||||
@ -13,7 +14,7 @@ export default function initialState({ store }) {
|
||||
|
||||
if (env.servers) {
|
||||
store.dispatch({
|
||||
type: 'SOCKET_SERVERS',
|
||||
type: socketActions.SERVERS,
|
||||
data: env.servers
|
||||
});
|
||||
|
||||
@ -37,19 +38,21 @@ export default function initialState({ store }) {
|
||||
|
||||
if (env.channels) {
|
||||
store.dispatch({
|
||||
type: 'SOCKET_CHANNELS',
|
||||
type: socketActions.CHANNELS,
|
||||
data: env.channels
|
||||
});
|
||||
}
|
||||
|
||||
if (env.users) {
|
||||
store.dispatch({
|
||||
type: 'SOCKET_USERS',
|
||||
type: socketActions.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) {
|
||||
const { messages, server, to, next } = env.messages;
|
||||
store.dispatch(addMessages(messages, server, to, false, next));
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { broadcast, inform, addMessage, addMessages } from '../actions/message';
|
||||
import { select } from '../actions/tab';
|
||||
import { replace } from '../util/router';
|
||||
import { socketAction } from '../state/actions';
|
||||
import { broadcast, inform, addMessage, addMessages } from '../state/messages';
|
||||
import { select } from '../state/tab';
|
||||
import { normalizeChannel } from '../util';
|
||||
import { replace } from '../util/router';
|
||||
|
||||
function withReason(message, reason) {
|
||||
return message + (reason ? ` (${reason})` : '');
|
||||
@ -97,7 +98,7 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
|
||||
handlers[type](data);
|
||||
}
|
||||
|
||||
type = `SOCKET_${type.toUpperCase()}`;
|
||||
type = socketAction(type);
|
||||
if (Array.isArray(data)) {
|
||||
dispatch({ type, data });
|
||||
} else {
|
@ -1,7 +1,7 @@
|
||||
import Cookie from 'js-cookie';
|
||||
import debounce from 'lodash/debounce';
|
||||
import observe from '../util/observe';
|
||||
import { getSelectedTab } from '../reducers/tab';
|
||||
import { observe } from '../util/observe';
|
||||
import { getSelectedTab } from '../state/tab';
|
||||
|
||||
const saveTab = debounce(tab =>
|
||||
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 reducer from '../reducers/channels';
|
||||
import reducer from '../channels';
|
||||
import { connect } from '../servers';
|
||||
import * as actions from '../actions';
|
||||
import { connect } from '../actions/server';
|
||||
|
||||
describe('reducers/channels', () => {
|
||||
it('removes channels on PART', () => {
|
||||
@ -36,7 +36,7 @@ describe('reducers/channels', () => {
|
||||
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.SOCKET_PART,
|
||||
type: actions.socket.PART,
|
||||
server: 'srv',
|
||||
channel: 'chan1',
|
||||
user: 'nick2'
|
||||
@ -78,7 +78,7 @@ describe('reducers/channels', () => {
|
||||
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.SOCKET_QUIT,
|
||||
type: actions.socket.QUIT,
|
||||
server: 'srv',
|
||||
user: 'nick2'
|
||||
});
|
||||
@ -103,7 +103,7 @@ describe('reducers/channels', () => {
|
||||
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
|
||||
|
||||
state = reducer(state, {
|
||||
type: actions.SOCKET_NICK,
|
||||
type: actions.socket.NICK,
|
||||
server: 'srv',
|
||||
old: 'nick1',
|
||||
new: 'nick3'
|
||||
@ -128,7 +128,7 @@ describe('reducers/channels', () => {
|
||||
|
||||
it('handles SOCKET_USERS', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.SOCKET_USERS,
|
||||
type: actions.socket.USERS,
|
||||
server: 'srv',
|
||||
channel: 'chan1',
|
||||
users: [
|
||||
@ -157,7 +157,7 @@ describe('reducers/channels', () => {
|
||||
|
||||
it('handles SOCKET_TOPIC', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.SOCKET_TOPIC,
|
||||
type: actions.socket.TOPIC,
|
||||
server: 'srv',
|
||||
channel: 'chan1',
|
||||
topic: 'the topic'
|
||||
@ -218,7 +218,7 @@ describe('reducers/channels', () => {
|
||||
|
||||
it('handles SOCKET_CHANNELS', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.SOCKET_CHANNELS,
|
||||
type: actions.socket.CHANNELS,
|
||||
data: [
|
||||
{ server: 'srv', name: 'chan1', topic: 'the topic' },
|
||||
{ server: 'srv', name: 'chan2' },
|
||||
@ -239,7 +239,7 @@ describe('reducers/channels', () => {
|
||||
|
||||
it('handles SOCKET_SERVERS', () => {
|
||||
const state = reducer(undefined, {
|
||||
type: actions.SOCKET_SERVERS,
|
||||
type: actions.socket.SERVERS,
|
||||
data: [
|
||||
{ host: '127.0.0.1' },
|
||||
{ host: 'thehost' }
|
||||
@ -279,7 +279,7 @@ describe('reducers/channels', () => {
|
||||
|
||||
function socket_join(server, channel, user) {
|
||||
return {
|
||||
type: 'SOCKET_JOIN',
|
||||
type: actions.socket.JOIN,
|
||||
server, user,
|
||||
channels: [channel]
|
||||
};
|
||||
@ -287,7 +287,7 @@ function socket_join(server, channel, user) {
|
||||
|
||||
function socket_mode(server, channel, user, add, remove) {
|
||||
return {
|
||||
type: 'SOCKET_MODE',
|
||||
type: actions.socket.MODE,
|
||||
server, channel, user, add, remove
|
||||
};
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
import reducer from '../reducers/messages';
|
||||
import reducer, { broadcast } from '../messages';
|
||||
import * as actions from '../actions';
|
||||
import { broadcast } from '../actions/message';
|
||||
|
||||
describe('reducers/messages', () => {
|
||||
it('adds the message on ADD_MESSAGE', () => {
|
@ -1,7 +1,6 @@
|
||||
import Immutable from 'immutable';
|
||||
import reducer from '../reducers/servers';
|
||||
import reducer, { connect } from '../servers';
|
||||
import * as actions from '../actions';
|
||||
import { connect } from '../actions/server';
|
||||
|
||||
describe('reducers/servers', () => {
|
||||
it('adds the server on CONNECT', () => {
|
||||
@ -62,7 +61,7 @@ describe('reducers/servers', () => {
|
||||
it('updates the nick on SOCKET_NICK', () => {
|
||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
||||
state = reducer(state, {
|
||||
type: actions.SOCKET_NICK,
|
||||
type: actions.socket.NICK,
|
||||
server: '127.0.0.1',
|
||||
old: 'nick',
|
||||
new: 'nick2'
|
||||
@ -79,7 +78,7 @@ describe('reducers/servers', () => {
|
||||
|
||||
it('adds the servers on SOCKET_SERVERS', () => {
|
||||
let state = reducer(undefined, {
|
||||
type: 'SOCKET_SERVERS',
|
||||
type: actions.socket.SERVERS,
|
||||
data: [
|
||||
{
|
||||
host: '127.0.0.1',
|
||||
@ -113,7 +112,7 @@ describe('reducers/servers', () => {
|
||||
it('updates connection status on SOCKET_CONNECTION_UPDATE', () => {
|
||||
let state = reducer(undefined, connect('127.0.0.1:1337', 'nick', {}));
|
||||
state = reducer(state, {
|
||||
type: actions.SOCKET_CONNECTION_UPDATE,
|
||||
type: actions.socket.CONNECTION_UPDATE,
|
||||
'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 { setSelectedTab } from '../actions/tab';
|
||||
import { locationChanged } from '../util/router';
|
||||
import { locationChanged } from '../../util/router';
|
||||
|
||||
describe('reducers/tab', () => {
|
||||
it('selects the tab and adds it to history', () => {
|
@ -1,48 +1,71 @@
|
||||
export const ADD_MESSAGE = 'ADD_MESSAGE';
|
||||
export const ADD_MESSAGES = 'ADD_MESSAGES';
|
||||
export const AWAY = 'AWAY';
|
||||
export const CLOSE_PRIVATE_CHAT = 'CLOSE_PRIVATE_CHAT';
|
||||
export const COMMAND = 'COMMAND';
|
||||
export const CONNECT = 'CONNECT';
|
||||
export const DISCONNECT = 'DISCONNECT';
|
||||
export const FETCH_MESSAGES = 'FETCH_MESSAGES';
|
||||
export const HIDE_MENU = 'HIDE_MENU';
|
||||
export const INVITE = 'INVITE';
|
||||
export const JOIN = 'JOIN';
|
||||
export const KICK = 'KICK';
|
||||
export const PART = 'PART';
|
||||
|
||||
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';
|
||||
|
||||
export const INPUT_HISTORY_ADD = 'INPUT_HISTORY_ADD';
|
||||
export const INPUT_HISTORY_DECREMENT = 'INPUT_HISTORY_DECREMENT';
|
||||
export const INPUT_HISTORY_INCREMENT = 'INPUT_HISTORY_INCREMENT';
|
||||
export const INPUT_HISTORY_RESET = 'INPUT_HISTORY_RESET';
|
||||
export const INVITE = 'INVITE';
|
||||
export const JOIN = 'JOIN';
|
||||
export const KICK = 'KICK';
|
||||
export const OPEN_PRIVATE_CHAT = 'OPEN_PRIVATE_CHAT';
|
||||
export const PART = 'PART';
|
||||
|
||||
export const ADD_MESSAGE = 'ADD_MESSAGE';
|
||||
export const ADD_MESSAGES = 'ADD_MESSAGES';
|
||||
export const COMMAND = 'COMMAND';
|
||||
export const FETCH_MESSAGES = 'FETCH_MESSAGES';
|
||||
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 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_ERROR = 'SET_CERT_ERROR';
|
||||
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';
|
||||
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 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 { createSelector } from 'reselect';
|
||||
import createReducer from '../util/createReducer';
|
||||
import * as actions from '../actions';
|
||||
import { getSelectedTab, updateSelection } from './tab';
|
||||
import * as actions from './actions';
|
||||
|
||||
const User = Record({
|
||||
nick: null,
|
||||
@ -74,6 +76,19 @@ function compareUsers(a, b) {
|
||||
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(), {
|
||||
[actions.PART](state, 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;
|
||||
return state.updateIn([server, channels[0], 'users'], List(), users =>
|
||||
users.push(createUser(user)).sort(compareUsers)
|
||||
);
|
||||
},
|
||||
|
||||
[actions.SOCKET_PART](state, action) {
|
||||
[actions.socket.PART](state, action) {
|
||||
const { server, channel, user } = action;
|
||||
if (state.hasIn([server, channel])) {
|
||||
return state.updateIn([server, channel, 'users'], users =>
|
||||
@ -99,7 +114,7 @@ export default createReducer(Map(), {
|
||||
return state;
|
||||
},
|
||||
|
||||
[actions.SOCKET_QUIT](state, action) {
|
||||
[actions.socket.QUIT](state, action) {
|
||||
const { server, user } = action;
|
||||
return state.withMutations(s => {
|
||||
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;
|
||||
return state.withMutations(s => {
|
||||
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;
|
||||
return state.setIn([server, channel, 'users'],
|
||||
List(users.map(user => loadUser(user)).sort(compareUsers)));
|
||||
},
|
||||
|
||||
[actions.SOCKET_TOPIC](state, action) {
|
||||
[actions.socket.TOPIC](state, action) {
|
||||
const { server, channel, topic } = action;
|
||||
return state.setIn([server, channel, 'topic'], topic);
|
||||
},
|
||||
|
||||
[actions.SOCKET_MODE](state, action) {
|
||||
[actions.socket.MODE](state, action) {
|
||||
const { server, channel, user, remove, add } = action;
|
||||
|
||||
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) {
|
||||
return state;
|
||||
}
|
||||
@ -173,7 +188,7 @@ export default createReducer(Map(), {
|
||||
});
|
||||
},
|
||||
|
||||
[actions.SOCKET_SERVERS](state, action) {
|
||||
[actions.socket.SERVERS](state, action) {
|
||||
if (!action.data) {
|
||||
return state;
|
||||
}
|
||||
@ -201,3 +216,56 @@ export default createReducer(Map(), {
|
||||
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 ui from './ui';
|
||||
|
||||
export * from './selectors';
|
||||
export const getRouter = state => state.router;
|
||||
|
||||
export default function createReducer(router) {
|
||||
return combineReducers({
|
||||
router,
|
@ -1,6 +1,6 @@
|
||||
import { List, Record } from 'immutable';
|
||||
import createReducer from '../util/createReducer';
|
||||
import * as actions from '../actions';
|
||||
import * as actions from './actions';
|
||||
|
||||
const HISTORY_MAX_LENGTH = 128;
|
||||
|
||||
@ -9,6 +9,14 @@ const State = Record({
|
||||
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(), {
|
||||
[actions.INPUT_HISTORY_ADD](state, action) {
|
||||
const { line } = action;
|
||||
@ -43,3 +51,28 @@ export default createReducer(new 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 { 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;
|
||||
|
||||
function initMessage(message, server, tab, state) {
|
||||
function initMessage(message, tab, state) {
|
||||
if (message.time) {
|
||||
message.time = timestamp(new Date(message.time * 1000));
|
||||
} else {
|
||||
@ -30,12 +107,13 @@ function initMessage(message, server, tab, state) {
|
||||
message.content = from + message.content.slice(7, -1);
|
||||
}
|
||||
|
||||
const charWidth = state.environment.get('charWidth');
|
||||
const wrapWidth = state.environment.get('wrapWidth');
|
||||
const wrapWidth = getWrapWidth(state);
|
||||
const charWidth = getCharWidth(state);
|
||||
const windowWidth = getWindowWidth(state);
|
||||
|
||||
message.length = message.content.length;
|
||||
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);
|
||||
|
||||
return message;
|
||||
@ -74,11 +152,12 @@ export function fetchMessages() {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateMessageHeight(wrapWidth, charWidth) {
|
||||
export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
|
||||
return {
|
||||
type: actions.UPDATE_MESSAGE_HEIGHT,
|
||||
wrapWidth,
|
||||
charWidth
|
||||
charWidth,
|
||||
windowWidth
|
||||
};
|
||||
}
|
||||
|
||||
@ -93,7 +172,7 @@ export function sendMessage(content, to, server) {
|
||||
message: initMessage({
|
||||
from: state.servers.getIn([server, 'nick']),
|
||||
content
|
||||
}, server, to, state),
|
||||
}, to, state),
|
||||
socket: {
|
||||
type: 'message',
|
||||
data: { content, to, server }
|
||||
@ -109,7 +188,7 @@ export function addMessage(message, server, to) {
|
||||
type: actions.ADD_MESSAGE,
|
||||
server,
|
||||
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.forEach(message => initMessage(message, server, message.tab || tab, state));
|
||||
messages.forEach(message => initMessage(message, message.tab || tab, state));
|
||||
|
||||
dispatch({
|
||||
type: actions.ADD_MESSAGES,
|
@ -1,6 +1,9 @@
|
||||
import { Set, Map } from 'immutable';
|
||||
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) {
|
||||
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));
|
||||
},
|
||||
|
||||
[actions.SOCKET_PM](state, action) {
|
||||
[actions.socket.PM](state, action) {
|
||||
if (action.from.indexOf('.') === -1) {
|
||||
return open(state, action.server, action.from);
|
||||
}
|
||||
@ -27,3 +30,22 @@ export default createReducer(Map(), {
|
||||
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 { LOCATION_CHANGED } from '../util/router';
|
||||
import { push, replace, LOCATION_CHANGED } from '../util/router';
|
||||
import createReducer from '../util/createReducer';
|
||||
import * as actions from '../actions';
|
||||
import * as actions from './actions';
|
||||
|
||||
const TabRecord = Record({
|
||||
server: null,
|
||||
@ -64,3 +64,39 @@ export default createReducer(new State(), {
|
||||
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 thunk from 'redux-thunk';
|
||||
import createReducer from '../reducers';
|
||||
import { routeReducer, routeMiddleware } from '../util/router';
|
||||
import createSocketMiddleware from '../middleware/socket';
|
||||
import commands from '../commands';
|
||||
import createReducer from './state';
|
||||
import { routeReducer, routeMiddleware } from './util/router';
|
||||
import createSocketMiddleware from './middleware/socket';
|
||||
import commands from './commands';
|
||||
|
||||
export default function configureStore(socket) {
|
||||
// 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 menuWidth = 200;
|
||||
const userListWidth = 200;
|
||||
const messagePadding = 30;
|
||||
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) {
|
||||
const breakpoints = [];
|
||||
@ -76,15 +18,15 @@ export function findBreakpoints(text) {
|
||||
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 height = lineHeight + 8;
|
||||
|
||||
if (message.channel && windowWidth > smallScreen) {
|
||||
width -= userListWidth;
|
||||
wrapWidth -= userListWidth;
|
||||
}
|
||||
|
||||
if (pad + (message.length * charWidth) < width) {
|
||||
if (pad + (message.length * charWidth) < wrapWidth) {
|
||||
return height;
|
||||
}
|
||||
|
||||
@ -93,7 +35,7 @@ export function messageHeight(message, width, charWidth, indent = 0) {
|
||||
let prevPos = 0;
|
||||
|
||||
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;
|
||||
pad = indent;
|
||||
height += lineHeight;
|
||||
@ -102,7 +44,7 @@ export function messageHeight(message, width, charWidth, indent = 0) {
|
||||
prevPos = breaks[i].next;
|
||||
}
|
||||
|
||||
if (pad + ((message.length - prevBreak) * charWidth) >= width) {
|
||||
if (pad + ((message.length - prevBreak) * charWidth) >= wrapWidth) {
|
||||
height += lineHeight;
|
||||
}
|
||||
|
||||
|
@ -1,22 +1,11 @@
|
||||
function subscribe(store, selector, handler) {
|
||||
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) {
|
||||
function subscribeArray(store, selectors, handler, init) {
|
||||
let state = store.getState();
|
||||
let prev = selectors.map(selector => selector(state));
|
||||
if (init) {
|
||||
handler(...prev);
|
||||
}
|
||||
|
||||
store.subscribe(() => {
|
||||
return store.subscribe(() => {
|
||||
state = store.getState();
|
||||
const next = [];
|
||||
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)) {
|
||||
subscribeArray(store, selector, handler);
|
||||
} else {
|
||||
subscribe(store, selector, handler);
|
||||
return subscribeArray(store, selector, handler, init);
|
||||
}
|
||||
|
||||
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