Handle kick, rename server to network

This commit is contained in:
Ken-Håvard Lieng 2020-06-15 10:58:51 +02:00
parent a33157ff84
commit 6985dd16da
65 changed files with 2650 additions and 2179 deletions

View file

@ -0,0 +1,20 @@
import { connect, setNetworkName } from '../networks';
describe('setNetworkName()', () => {
it('passes valid names to the network', () => {
const name = 'cake';
const network = 'srv';
expect(setNetworkName(name, network)).toMatchObject({
socket: {
type: 'set_network_name',
data: { name, network }
}
});
});
it('does not pass invalid names to the network', () => {
expect(setNetworkName('', 'srv').socket).toBeUndefined();
expect(setNetworkName(' ', 'srv').socket).toBeUndefined();
});
});

View file

@ -1,20 +0,0 @@
import { connect, setServerName } from '../servers';
describe('setServerName()', () => {
it('passes valid names to the server', () => {
const name = 'cake';
const server = 'srv';
expect(setServerName(name, server)).toMatchObject({
socket: {
type: 'set_server_name',
data: { name, server }
}
});
});
it('does not pass invalid names to the server', () => {
expect(setServerName('', 'srv').socket).toBeUndefined();
expect(setServerName(' ', 'srv').socket).toBeUndefined();
});
});

View file

@ -1,5 +1,5 @@
import reducer, { compareUsers, getSortedChannels } from '../channels';
import { connect } from '../servers';
import { connect } from '../networks';
import * as actions from '../actions';
describe('channel reducer', () => {
@ -17,7 +17,7 @@ describe('channel reducer', () => {
state = reducer(state, {
type: actions.PART,
server: 'srv1',
network: 'srv1',
channels: ['chan1', 'chan3']
});
@ -38,7 +38,7 @@ describe('channel reducer', () => {
state = reducer(state, {
type: actions.socket.PART,
server: 'srv',
network: 'srv',
channel: 'chan1',
user: 'nick2'
});
@ -80,7 +80,7 @@ describe('channel reducer', () => {
state = reducer(state, {
type: actions.socket.QUIT,
server: 'srv',
network: 'srv',
user: 'nick2'
});
@ -100,6 +100,67 @@ describe('channel reducer', () => {
});
});
it('handles KICKED', () => {
let state = reducer(
undefined,
connect({
host: 'srv',
nick: 'nick2'
})
);
state = reducer(state, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, socket_join('srv', 'chan1', 'nick2'));
state = reducer(state, socket_join('srv', 'chan2', 'nick2'));
state = reducer(state, {
type: actions.KICKED,
network: 'srv',
channel: 'chan2',
user: 'nick2',
self: true
});
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [
{ mode: '', nick: 'nick1', renderName: 'nick1' },
{ mode: '', nick: 'nick2', renderName: 'nick2' }
]
},
chan2: {
name: 'chan2',
joined: false,
users: []
}
}
});
state = reducer(state, {
type: actions.KICKED,
network: 'srv',
channel: 'chan1',
user: 'nick1'
});
expect(state).toEqual({
srv: {
chan1: {
name: 'chan1',
joined: true,
users: [{ mode: '', nick: 'nick2', renderName: 'nick2' }]
},
chan2: {
name: 'chan2',
joined: false,
users: []
}
}
});
});
it('handles SOCKET_NICK', () => {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, socket_join('srv', 'chan1', 'nick2'));
@ -107,7 +168,7 @@ describe('channel reducer', () => {
state = reducer(state, {
type: actions.socket.NICK,
server: 'srv',
network: 'srv',
oldNick: 'nick1',
newNick: 'nick3'
});
@ -135,7 +196,7 @@ describe('channel reducer', () => {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, {
type: actions.socket.USERS,
server: 'srv',
network: 'srv',
channel: 'chan1',
users: ['user3', 'user2', '@user4', 'user1', '+user5']
});
@ -161,7 +222,7 @@ describe('channel reducer', () => {
let state = reducer(undefined, socket_join('srv', 'chan1', 'nick1'));
state = reducer(state, {
type: actions.socket.TOPIC,
server: 'srv',
network: 'srv',
channel: 'chan1',
topic: 'the topic'
});
@ -219,9 +280,9 @@ describe('channel reducer', () => {
const state = reducer(undefined, {
type: actions.socket.CHANNELS,
data: [
{ server: 'srv', name: 'chan1', topic: 'the topic' },
{ server: 'srv', name: 'chan2' },
{ server: 'srv2', name: 'chan1' }
{ network: 'srv', name: 'chan1', topic: 'the topic' },
{ network: 'srv', name: 'chan2' },
{ network: 'srv2', name: 'chan1' }
]
});
@ -236,9 +297,9 @@ describe('channel reducer', () => {
});
});
it('handles SOCKET_SERVERS', () => {
it('handles SOCKET_NETWORKS', () => {
const state = reducer(undefined, {
type: actions.socket.SERVERS,
type: actions.socket.NETWORKS,
data: [{ host: '127.0.0.1' }, { host: 'thehost' }]
});
@ -248,7 +309,7 @@ describe('channel reducer', () => {
});
});
it('optimistically adds the server on CONNECT', () => {
it('optimistically adds the network on CONNECT', () => {
const state = reducer(
undefined,
connect({ host: '127.0.0.1', nick: 'nick' })
@ -259,7 +320,7 @@ describe('channel reducer', () => {
});
});
it('removes the server on DISCONNECT', () => {
it('removes the network on DISCONNECT', () => {
let state = {
srv: {},
srv2: {}
@ -267,7 +328,7 @@ describe('channel reducer', () => {
state = reducer(state, {
type: actions.DISCONNECT,
server: 'srv2'
network: 'srv2'
});
expect(state).toEqual({
@ -276,19 +337,19 @@ describe('channel reducer', () => {
});
});
function socket_join(server, channel, user) {
function socket_join(network, channel, user) {
return {
type: actions.socket.JOIN,
server,
network,
user,
channels: [channel]
};
}
function socket_mode(server, channel, user, add, remove) {
function socket_mode(network, channel, user, add, remove) {
return {
type: actions.socket.MODE,
server,
network,
channel,
user,
add,
@ -323,7 +384,7 @@ describe('compareUsers()', () => {
});
describe('getSortedChannels', () => {
it('sorts servers and channels', () => {
it('sorts networks and channels', () => {
expect(
getSortedChannels({
channels: {

View file

@ -7,7 +7,7 @@ describe('message reducer', () => {
it('adds the message on ADD_MESSAGE', () => {
const state = reducer(undefined, {
type: actions.ADD_MESSAGE,
server: 'srv',
network: 'srv',
tab: '#chan1',
message: {
from: 'foo',
@ -30,7 +30,7 @@ describe('message reducer', () => {
it('adds all the messages on ADD_MESSAGES', () => {
const state = reducer(undefined, {
type: actions.ADD_MESSAGES,
server: 'srv',
network: 'srv',
tab: '#chan1',
messages: [
{
@ -80,7 +80,7 @@ describe('message reducer', () => {
state = reducer(state, {
type: actions.ADD_MESSAGES,
server: 'srv',
network: 'srv',
tab: '#chan1',
prepend: true,
messages: [
@ -105,7 +105,7 @@ describe('message reducer', () => {
state = reducer(state, {
type: actions.ADD_MESSAGES,
server: 'srv',
network: 'srv',
tab: '#chan1',
prepend: true,
messages: [
@ -136,7 +136,7 @@ describe('message reducer', () => {
state = reducer(state, {
type: actions.ADD_MESSAGE,
server: 'srv',
network: 'srv',
tab: '#chan1',
message: { id: 1, date: new Date(1990, 0, 2) }
});
@ -157,7 +157,7 @@ describe('message reducer', () => {
state = reducer(state, {
type: actions.ADD_MESSAGES,
server: 'srv',
network: 'srv',
tab: '#chan1',
messages: [
{ id: 1, time: unix(new Date(1990, 0, 2)) },
@ -202,7 +202,7 @@ describe('message reducer', () => {
expect(messages.srv['#chan3'][0].content).toBe('test');
});
it('deletes all messages related to server when disconnecting', () => {
it('deletes all messages related to network when disconnecting', () => {
let state = {
srv: {
'#chan1': [{ content: 'msg1' }, { content: 'msg2' }],
@ -215,7 +215,7 @@ describe('message reducer', () => {
state = reducer(state, {
type: actions.DISCONNECT,
server: 'srv'
network: 'srv'
});
expect(state).toEqual({
@ -238,7 +238,7 @@ describe('message reducer', () => {
state = reducer(state, {
type: actions.PART,
server: 'srv',
network: 'srv',
channels: ['#chan1']
});
@ -265,7 +265,7 @@ describe('message reducer', () => {
state = reducer(state, {
type: actions.CLOSE_PRIVATE_CHAT,
server: 'srv',
network: 'srv',
nick: 'bob'
});

View file

@ -1,8 +1,8 @@
import reducer, { connect, setServerName } from '../servers';
import reducer, { connect, setNetworkName } from '../networks';
import * as actions from '../actions';
describe('server reducer', () => {
it('adds the server on CONNECT', () => {
describe('network reducer', () => {
it('adds the network on CONNECT', () => {
let state = reducer(
undefined,
connect({ host: '127.0.0.1', nick: 'nick' })
@ -13,10 +13,8 @@ describe('server reducer', () => {
name: '127.0.0.1',
nick: 'nick',
editedNick: null,
status: {
connected: false,
error: null
},
connected: false,
error: null,
features: {}
}
});
@ -28,10 +26,8 @@ describe('server reducer', () => {
name: '127.0.0.1',
nick: 'nick',
editedNick: null,
status: {
connected: false,
error: null
},
connected: false,
error: null,
features: {}
}
});
@ -46,26 +42,22 @@ describe('server reducer', () => {
name: '127.0.0.1',
nick: 'nick',
editedNick: null,
status: {
connected: false,
error: null
},
connected: false,
error: null,
features: {}
},
'127.0.0.2': {
name: 'srv',
nick: 'nick',
editedNick: null,
status: {
connected: false,
error: null
},
connected: false,
error: null,
features: {}
}
});
});
it('removes the server on DISCONNECT', () => {
it('removes the network on DISCONNECT', () => {
let state = {
srv: {},
srv2: {}
@ -73,7 +65,7 @@ describe('server reducer', () => {
state = reducer(state, {
type: actions.DISCONNECT,
server: 'srv2'
network: 'srv2'
});
expect(state).toEqual({
@ -81,14 +73,14 @@ describe('server reducer', () => {
});
});
it('handles SET_SERVER_NAME', () => {
it('handles SET_NETWORK_NAME', () => {
let state = {
srv: {
name: 'cake'
}
};
state = reducer(state, setServerName('pie', 'srv'));
state = reducer(state, setNetworkName('pie', 'srv'));
expect(state).toEqual({
srv: {
@ -104,7 +96,7 @@ describe('server reducer', () => {
);
state = reducer(state, {
type: actions.SET_NICK,
server: '127.0.0.1',
network: '127.0.0.1',
nick: 'nick2',
editing: true
});
@ -125,13 +117,13 @@ describe('server reducer', () => {
);
state = reducer(state, {
type: actions.SET_NICK,
server: '127.0.0.1',
network: '127.0.0.1',
nick: 'nick2',
editing: true
});
state = reducer(state, {
type: actions.SET_NICK,
server: '127.0.0.1',
network: '127.0.0.1',
nick: ''
});
@ -151,7 +143,7 @@ describe('server reducer', () => {
);
state = reducer(state, {
type: actions.socket.NICK,
server: '127.0.0.1',
network: '127.0.0.1',
oldNick: 'nick',
newNick: 'nick2'
});
@ -172,13 +164,13 @@ describe('server reducer', () => {
);
state = reducer(state, {
type: actions.SET_NICK,
server: '127.0.0.1',
network: '127.0.0.1',
nick: 'nick2',
editing: true
});
state = reducer(state, {
type: actions.socket.NICK_FAIL,
server: '127.0.0.1'
network: '127.0.0.1'
});
expect(state).toMatchObject({
@ -190,25 +182,21 @@ describe('server reducer', () => {
});
});
it('adds the servers on SOCKET_SERVERS', () => {
it('adds the networks on SOCKET_NETWORKS', () => {
let state = reducer(undefined, {
type: actions.socket.SERVERS,
type: actions.socket.NETWORKS,
data: [
{
host: '127.0.0.1',
name: 'stuff',
nick: 'nick',
status: {
connected: true
}
connected: true
},
{
host: '127.0.0.2',
name: 'stuffz',
nick: 'nick2',
status: {
connected: false
}
connected: false
}
]
});
@ -218,18 +206,14 @@ describe('server reducer', () => {
name: 'stuff',
nick: 'nick',
editedNick: null,
status: {
connected: true
},
connected: true,
features: {}
},
'127.0.0.2': {
name: 'stuffz',
nick: 'nick2',
editedNick: null,
status: {
connected: false
},
connected: false,
features: {}
}
});
@ -242,7 +226,7 @@ describe('server reducer', () => {
);
state = reducer(state, {
type: actions.socket.CONNECTION_UPDATE,
server: '127.0.0.1',
network: '127.0.0.1',
connected: true
});
@ -251,16 +235,14 @@ describe('server reducer', () => {
name: '127.0.0.1',
nick: 'nick',
editedNick: null,
status: {
connected: true
},
connected: true,
features: {}
}
});
state = reducer(state, {
type: actions.socket.CONNECTION_UPDATE,
server: '127.0.0.1',
network: '127.0.0.1',
connected: false,
error: 'Bad stuff happened'
});
@ -270,10 +252,8 @@ describe('server reducer', () => {
name: '127.0.0.1',
nick: 'nick',
editedNick: null,
status: {
connected: false,
error: 'Bad stuff happened'
},
connected: false,
error: 'Bad stuff happened',
features: {}
}
});

View file

@ -7,17 +7,17 @@ describe('tab reducer', () => {
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
expect(state).toEqual({
selected: { server: 'srv', name: '#chan' },
history: [{ server: 'srv', name: '#chan' }]
selected: { network: 'srv', name: '#chan' },
history: [{ network: 'srv', name: '#chan' }]
});
state = reducer(state, setSelectedTab('srv', 'user1'));
expect(state).toEqual({
selected: { server: 'srv', name: 'user1' },
selected: { network: 'srv', name: 'user1' },
history: [
{ server: 'srv', name: '#chan' },
{ server: 'srv', name: 'user1' }
{ network: 'srv', name: '#chan' },
{ network: 'srv', name: 'user1' }
]
});
});
@ -30,15 +30,15 @@ describe('tab reducer', () => {
state = reducer(state, {
type: actions.PART,
server: 'srv',
network: 'srv',
channels: ['#chan']
});
expect(state).toEqual({
selected: { server: 'srv', name: '#chan3' },
selected: { network: 'srv', name: '#chan3' },
history: [
{ server: 'srv1', name: 'bob' },
{ server: 'srv', name: '#chan3' }
{ network: 'srv1', name: 'bob' },
{ network: 'srv', name: '#chan3' }
]
});
});
@ -51,21 +51,21 @@ describe('tab reducer', () => {
state = reducer(state, {
type: actions.CLOSE_PRIVATE_CHAT,
server: 'srv1',
network: 'srv1',
nick: 'bob'
});
expect(state).toEqual({
selected: { server: 'srv', name: '#chan3' },
selected: { network: 'srv', name: '#chan3' },
history: [
{ server: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan' },
{ server: 'srv', name: '#chan3' }
{ network: 'srv', name: '#chan' },
{ network: 'srv', name: '#chan' },
{ network: 'srv', name: '#chan3' }
]
});
});
it('removes all tabs related to server from history on DISCONNECT', () => {
it('removes all tabs related to network from history on DISCONNECT', () => {
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
state = reducer(state, setSelectedTab('srv1', 'bob'));
state = reducer(state, setSelectedTab('srv', '#chan'));
@ -73,12 +73,12 @@ describe('tab reducer', () => {
state = reducer(state, {
type: actions.DISCONNECT,
server: 'srv'
network: 'srv'
});
expect(state).toEqual({
selected: { server: 'srv', name: '#chan3' },
history: [{ server: 'srv1', name: 'bob' }]
selected: { network: 'srv', name: '#chan3' },
history: [{ network: 'srv1', name: 'bob' }]
});
});
@ -89,7 +89,7 @@ describe('tab reducer', () => {
expect(state).toEqual({
selected: {},
history: [{ server: 'srv', name: '#chan' }]
history: [{ network: 'srv', name: '#chan' }]
});
});
@ -99,7 +99,7 @@ describe('tab reducer', () => {
locationChanged(
'chat',
{
server: 'srv',
network: 'srv',
name: '#chan'
},
{}
@ -107,8 +107,8 @@ describe('tab reducer', () => {
);
expect(state).toEqual({
selected: { server: 'srv', name: '#chan' },
history: [{ server: 'srv', name: '#chan' }]
selected: { network: 'srv', name: '#chan' },
history: [{ network: 'srv', name: '#chan' }]
});
});
});

View file

@ -3,6 +3,7 @@ export const APP_SET = 'APP_SET';
export const INVITE = 'INVITE';
export const JOIN = 'JOIN';
export const KICK = 'KICK';
export const KICKED = 'KICKED';
export const PART = 'PART';
export const SET_TOPIC = 'SET_TOPIC';
@ -36,7 +37,7 @@ export const CONNECT = 'CONNECT';
export const DISCONNECT = 'DISCONNECT';
export const RECONNECT = 'RECONNECT';
export const SET_NICK = 'SET_NICK';
export const SET_SERVER_NAME = 'SET_SERVER_NAME';
export const SET_NETWORK_NAME = 'SET_NETWORK_NAME';
export const WHOIS = 'WHOIS';
export const SET_CERT = 'SET_CERT';
@ -83,7 +84,7 @@ export const socket = createSocketActions([
'pm',
'quit',
'search',
'servers',
'networks',
'topic',
'users'
]);

View file

@ -8,7 +8,7 @@ const initialState = {
};
export default createReducer(initialState, {
[actions.socket.CHANNEL_SEARCH](state, { results, start, server, q }) {
[actions.socket.CHANNEL_SEARCH](state, { results, start, network, q }) {
if (results) {
state.end = false;
@ -18,7 +18,7 @@ export default createReducer(initialState, {
state.results = results;
if (!q) {
state.topCache[server] = results;
state.topCache[network] = results;
}
}
} else {
@ -34,14 +34,14 @@ export default createReducer(initialState, {
}
});
export function searchChannels(server, q, start) {
export function searchChannels(network, q, start) {
return {
type: actions.CHANNEL_SEARCH,
server,
network,
q,
socket: {
type: 'channel_search',
data: { server, q, start }
data: { network, q, start }
}
};
}

View file

@ -56,13 +56,18 @@ function removeUser(users, nick) {
}
}
function init(state, server, channel) {
if (!state[server]) {
state[server] = {};
function init(state, network, channel) {
if (!state[network]) {
state[network] = {};
}
if (channel && !state[server][channel]) {
state[server][channel] = { name: channel, users: [], joined: false };
if (channel && !state[network][channel]) {
state[network][channel] = {
name: channel,
users: [],
joined: false
};
}
return state[network][channel];
}
export function compareUsers(a, b) {
@ -93,18 +98,18 @@ export const getChannels = state => state.channels;
export const getSortedChannels = createSelector(getChannels, channels =>
sortBy(
Object.keys(channels).map(server => ({
address: server,
channels: sortBy(channels[server], channel => channel.name.toLowerCase())
Object.keys(channels).map(network => ({
address: network,
channels: sortBy(channels[network], channel => channel.name.toLowerCase())
})),
server => server.address.toLowerCase()
network => network.address.toLowerCase()
)
);
export const getSelectedChannel = createSelector(
getSelectedTab,
getChannels,
(tab, channels) => get(channels, [tab.server, tab.name])
(tab, channels) => get(channels, [tab.network, tab.name])
);
export const getSelectedChannelUsers = createSelector(
@ -120,43 +125,53 @@ export const getSelectedChannelUsers = createSelector(
export default createReducer(
{},
{
[actions.JOIN](state, { server, channels }) {
channels.forEach(channel => init(state, server, channel));
[actions.JOIN](state, { network, channels }) {
channels.forEach(channel => init(state, network, channel));
},
[actions.PART](state, { server, channels }) {
channels.forEach(channel => delete state[server][channel]);
[actions.PART](state, { network, channels }) {
channels.forEach(channel => delete state[network][channel]);
},
[actions.socket.JOIN](state, { server, channels, user }) {
[actions.socket.JOIN](state, { network, channels, user }) {
const channel = channels[0];
init(state, server, channel);
state[server][channel].name = channel;
state[server][channel].joined = true;
state[server][channel].users.push(createUser(user));
const chan = init(state, network, channel);
chan.name = channel;
chan.joined = true;
chan.users.push(createUser(user));
},
[actions.socket.CHANNEL_FORWARD](state, action) {
init(state, action.server, action.new);
delete state[action.server][action.old];
init(state, action.network, action.new);
delete state[action.network][action.old];
},
[actions.socket.PART](state, { server, channel, user }) {
if (state[server][channel]) {
removeUser(state[server][channel].users, user);
[actions.socket.PART](state, { network, channel, user }) {
if (state[network][channel]) {
removeUser(state[network][channel].users, user);
}
},
[actions.socket.QUIT](state, { server, user }) {
Object.keys(state[server]).forEach(channel => {
removeUser(state[server][channel].users, user);
[actions.socket.QUIT](state, { network, user }) {
Object.keys(state[network]).forEach(channel => {
removeUser(state[network][channel].users, user);
});
},
[actions.socket.NICK](state, { server, oldNick, newNick }) {
Object.keys(state[server]).forEach(channel => {
[actions.KICKED](state, { network, channel, user, self }) {
const chan = state[network][channel];
if (self) {
chan.joined = false;
chan.users = [];
} else {
removeUser(chan.users, user);
}
},
[actions.socket.NICK](state, { network, oldNick, newNick }) {
Object.keys(state[network]).forEach(channel => {
const user = find(
state[server][channel].users,
state[network][channel].users,
u => u.nick === oldNick
);
if (user) {
@ -166,16 +181,16 @@ export default createReducer(
});
},
[actions.socket.USERS](state, { server, channel, users }) {
state[server][channel].users = users.map(nick => loadUser(nick));
[actions.socket.USERS](state, { network, channel, users }) {
state[network][channel].users = users.map(nick => loadUser(nick));
},
[actions.socket.TOPIC](state, { server, channel, topic }) {
state[server][channel].topic = topic;
[actions.socket.TOPIC](state, { network, channel, topic }) {
state[network][channel].topic = topic;
},
[actions.socket.MODE](state, { server, channel, user, remove, add }) {
const u = find(state[server][channel].users, v => v.nick === user);
[actions.socket.MODE](state, { network, channel, user, remove, add }) {
const u = find(state[network][channel].users, v => v.nick === user);
if (u) {
if (remove) {
let j = remove.length;
@ -194,15 +209,15 @@ export default createReducer(
[actions.socket.CHANNELS](state, { data }) {
if (data) {
data.forEach(({ server, name, topic }) => {
init(state, server, name);
state[server][name].joined = true;
state[server][name].topic = topic;
data.forEach(({ network, name, topic }) => {
const chan = init(state, network, name);
chan.joined = true;
chan.topic = topic;
});
}
},
[actions.socket.SERVERS](state, { data }) {
[actions.socket.NETWORKS](state, { data }) {
if (data) {
data.forEach(({ host }) => init(state, host));
}
@ -212,33 +227,33 @@ export default createReducer(
init(state, host);
},
[actions.DISCONNECT](state, { server }) {
delete state[server];
[actions.DISCONNECT](state, { network }) {
delete state[network];
}
}
);
export function join(channels, server) {
export function join(channels, network) {
return {
type: actions.JOIN,
channels,
server,
network,
socket: {
type: 'join',
data: { channels, server }
data: { channels, network }
}
};
}
export function part(channels, server) {
export function part(channels, network) {
return (dispatch, getState) => {
const action = {
type: actions.PART,
channels,
server
network
};
const state = getState().channels[server];
const state = getState().channels[network];
const joined = channels.filter(c => state[c] && state[c].joined);
if (joined.length > 0) {
@ -246,7 +261,7 @@ export function part(channels, server) {
type: 'part',
data: {
channels: joined,
server
network
}
};
}
@ -256,41 +271,55 @@ export function part(channels, server) {
};
}
export function invite(user, channel, server) {
export function invite(user, channel, network) {
return {
type: actions.INVITE,
user,
channel,
server,
network,
socket: {
type: 'invite',
data: { user, channel, server }
data: { user, channel, network }
}
};
}
export function kick(user, channel, server) {
export function kick(user, channel, network) {
return {
type: actions.KICK,
user,
channel,
server,
network,
socket: {
type: 'kick',
data: { user, channel, server }
data: { user, channel, network }
}
};
}
export function setTopic(topic, channel, server) {
export function kicked(network, channel, user) {
return (dispatch, getState) => {
const nick = getState().networks[network]?.nick;
dispatch({
type: actions.KICKED,
network,
channel,
user,
self: nick === user
});
};
}
export function setTopic(topic, channel, network) {
return {
type: actions.SET_TOPIC,
topic,
channel,
server,
network,
socket: {
type: 'topic',
data: { topic, channel, server }
data: { topic, channel, network }
}
};
}

View file

@ -5,9 +5,9 @@ import channelSearch from './channelSearch';
import input from './input';
import messages from './messages';
import modals from './modals';
import networks from './networks';
import privateChats from './privateChats';
import search from './search';
import servers from './servers';
import settings from './settings';
import tab from './tab';
import ui from './ui';
@ -24,9 +24,9 @@ export default function createReducer(router) {
input,
messages,
modals,
networks,
privateChats,
search,
servers,
settings,
tab,
ui

View file

@ -23,9 +23,9 @@ export const getSelectedMessages = createSelector(
getSelectedTab,
getMessages,
(tab, messages) => {
const target = tab.name || tab.server;
if (has(messages, [tab.server, target])) {
return messages[tab.server][target];
const target = tab.name || tab.network;
if (has(messages, [tab.network, target])) {
return messages[tab.network][target];
}
return [];
}
@ -39,12 +39,12 @@ export const getHasMoreMessages = createSelector(
}
);
function init(state, server, tab) {
if (!state[server]) {
state[server] = {};
function init(state, network, tab) {
if (!state[network]) {
state[network] = {};
}
if (!state[server][tab]) {
state[server][tab] = [];
if (!state[network][tab]) {
state[network][tab] = [];
}
}
@ -117,6 +117,13 @@ function renderEvents(events) {
return [renderNick(oldNick), ' changed nick to ', renderNick(newNick)];
}
if (first.type === 'kick') {
const [kicked, by] = first.params;
return [renderNick(by), ' kicked ', renderNick(kicked)];
}
if (first.type === 'topic') {
const [nick, newTopic] = first.params;
const topic = colorify(linkify(newTopic));
@ -176,14 +183,14 @@ let nextID = 0;
function initMessage(
state,
message,
server,
network,
tab,
wrapWidth,
charWidth,
windowWidth,
prepend
) {
const messages = state[server][tab];
const messages = state[network][tab];
if (messages.length > 0 && !prepend) {
const lastMessage = messages[messages.length - 1];
@ -280,7 +287,7 @@ function isSameDay(d1, d2) {
function reducerPrependMessages(
state,
messages,
server,
network,
tab,
wrapWidth,
charWidth,
@ -293,7 +300,7 @@ function reducerPrependMessages(
initMessage(
state,
message,
server,
network,
tab,
wrapWidth,
charWidth,
@ -307,7 +314,7 @@ function reducerPrependMessages(
msgs.push(message);
}
const m = state[server][tab];
const m = state[network][tab];
if (m.length > 0) {
const lastNewMessage = msgs[msgs.length - 1];
@ -323,8 +330,8 @@ function reducerPrependMessages(
m.unshift(...msgs);
}
function reducerAddMessage(message, server, tab, state) {
const messages = state[server][tab];
function reducerAddMessage(message, network, tab, state) {
const messages = state[network][tab];
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
@ -341,34 +348,34 @@ export default createReducer(
{
[actions.ADD_MESSAGE](
state,
{ server, tab, message, wrapWidth, charWidth, windowWidth }
{ network, tab, message, wrapWidth, charWidth, windowWidth }
) {
init(state, server, tab);
init(state, network, tab);
const shouldAdd = initMessage(
state,
message,
server,
network,
tab,
wrapWidth,
charWidth,
windowWidth
);
if (shouldAdd) {
reducerAddMessage(message, server, tab, state);
reducerAddMessage(message, network, tab, state);
}
},
[actions.ADD_MESSAGES](
state,
{ server, tab, messages, prepend, wrapWidth, charWidth, windowWidth }
{ network, tab, messages, prepend, wrapWidth, charWidth, windowWidth }
) {
if (prepend) {
init(state, server, tab);
init(state, network, tab);
reducerPrependMessages(
state,
messages,
server,
network,
tab,
wrapWidth,
charWidth,
@ -376,45 +383,45 @@ export default createReducer(
);
} else {
if (!messages[0].tab) {
init(state, server, tab);
init(state, network, tab);
}
messages.forEach(message => {
if (message.tab) {
init(state, server, message.tab);
init(state, network, message.tab);
}
const shouldAdd = initMessage(
state,
message,
server,
network,
message.tab || tab,
wrapWidth,
charWidth,
windowWidth
);
if (shouldAdd) {
reducerAddMessage(message, server, message.tab || tab, state);
reducerAddMessage(message, network, message.tab || tab, state);
}
});
}
},
[actions.DISCONNECT](state, { server }) {
delete state[server];
[actions.DISCONNECT](state, { network }) {
delete state[network];
},
[actions.PART](state, { server, channels }) {
channels.forEach(channel => delete state[server][channel]);
[actions.PART](state, { network, channels }) {
channels.forEach(channel => delete state[network][channel]);
},
[actions.CLOSE_PRIVATE_CHAT](state, { server, nick }) {
delete state[server][nick];
[actions.CLOSE_PRIVATE_CHAT](state, { network, nick }) {
delete state[network][nick];
},
[actions.socket.CHANNEL_FORWARD](state, { server, old }) {
if (state[server]) {
delete state[server][old];
[actions.socket.CHANNEL_FORWARD](state, { network, old }) {
if (state[network]) {
delete state[network][old];
}
},
@ -422,9 +429,9 @@ export default createReducer(
state,
{ wrapWidth, charWidth, windowWidth }
) {
Object.keys(state).forEach(server =>
Object.keys(state[server]).forEach(target =>
state[server][target].forEach(message => {
Object.keys(state).forEach(network =>
Object.keys(state[network]).forEach(target =>
state[network][target].forEach(message => {
if (message.type === 'date') {
return;
}
@ -441,7 +448,7 @@ export default createReducer(
);
},
[actions.socket.SERVERS](state, { data }) {
[actions.socket.NETWORKS](state, { data }) {
if (data) {
data.forEach(({ host }) => {
state[host] = {};
@ -451,9 +458,9 @@ export default createReducer(
}
);
export function getMessageTab(server, to) {
export function getMessageTab(network, to) {
if (!to || to === '*' || (!isChannel(to) && to.indexOf('.') !== -1)) {
return server;
return network;
}
return to;
}
@ -474,7 +481,7 @@ export function fetchMessages() {
socket: {
type: 'fetch_messages',
data: {
server: tab.server,
network: tab.network,
channel: tab.name,
next: first.id
}
@ -484,10 +491,10 @@ export function fetchMessages() {
};
}
export function addFetchedMessages(server, tab) {
export function addFetchedMessages(network, tab) {
return {
type: actions.ADD_FETCHED_MESSAGES,
server,
network,
tab
};
}
@ -501,17 +508,17 @@ export function updateMessageHeight(wrapWidth, charWidth, windowWidth) {
};
}
export function sendMessage(content, to, server) {
export function sendMessage(content, to, network) {
return (dispatch, getState) => {
const state = getState();
const { wrapWidth, charWidth, windowWidth } = getApp(state);
dispatch({
type: actions.ADD_MESSAGE,
server,
network,
tab: to,
message: {
from: state.servers[server].nick,
from: state.networks[network].nick,
content
},
wrapWidth,
@ -519,21 +526,21 @@ export function sendMessage(content, to, server) {
windowWidth,
socket: {
type: 'message',
data: { content, to, server }
data: { content, to, network }
}
});
};
}
export function addMessage(message, server, to) {
const tab = getMessageTab(server, to);
export function addMessage(message, network, to) {
const tab = getMessageTab(network, to);
return (dispatch, getState) => {
const { wrapWidth, charWidth, windowWidth } = getApp(getState());
dispatch({
type: actions.ADD_MESSAGE,
server,
network,
tab,
message,
wrapWidth,
@ -543,8 +550,8 @@ export function addMessage(message, server, to) {
};
}
export function addMessages(messages, server, to, prepend, next) {
const tab = getMessageTab(server, to);
export function addMessages(messages, network, to, prepend, next) {
const tab = getMessageTab(network, to);
return (dispatch, getState) => {
const state = getState();
@ -558,7 +565,7 @@ export function addMessages(messages, server, to, prepend, next) {
dispatch({
type: actions.ADD_MESSAGES,
server,
network,
tab,
messages,
prepend,
@ -569,7 +576,7 @@ export function addMessages(messages, server, to, prepend, next) {
};
}
export function addEvent(server, tab, type, ...params) {
export function addEvent(network, tab, type, ...params) {
return addMessage(
{
type: 'info',
@ -581,12 +588,12 @@ export function addEvent(server, tab, type, ...params) {
}
]
},
server,
network,
tab
);
}
export function broadcastEvent(server, channels, type, ...params) {
export function broadcastEvent(network, channels, type, ...params) {
const now = unix();
return addMessages(
@ -601,29 +608,29 @@ export function broadcastEvent(server, channels, type, ...params) {
}
]
})),
server
network
);
}
export function broadcast(message, server, channels) {
export function broadcast(message, network, channels) {
return addMessages(
channels.map(channel => ({
tab: channel,
content: message,
type: 'info'
})),
server
network
);
}
export function print(message, server, channel, type) {
export function print(message, network, channel, type) {
if (Array.isArray(message)) {
return addMessages(
message.map(line => ({
content: line,
type
})),
server,
network,
channel
);
}
@ -633,32 +640,32 @@ export function print(message, server, channel, type) {
content: message,
type
},
server,
network,
channel
);
}
export function inform(message, server, channel) {
return print(message, server, channel, 'info');
export function inform(message, network, channel) {
return print(message, network, channel, 'info');
}
export function runCommand(command, channel, server) {
export function runCommand(command, channel, network) {
return {
type: actions.COMMAND,
command,
channel,
server
network
};
}
export function raw(message, server) {
export function raw(message, network) {
return {
type: actions.RAW,
message,
server,
network,
socket: {
type: 'raw',
data: { message, server }
data: { message, network }
}
};
}

229
client/js/state/networks.js Normal file
View file

@ -0,0 +1,229 @@
import { createSelector } from 'reselect';
import get from 'lodash/get';
import createReducer from 'utils/createReducer';
import { getSelectedTab, updateSelection } from './tab';
import * as actions from './actions';
export const getNetworks = state => state.networks;
export const getCurrentNick = createSelector(
getNetworks,
getSelectedTab,
(networks, tab) => {
if (!networks[tab.network]) {
return;
}
const { editedNick } = networks[tab.network];
if (editedNick === null) {
return networks[tab.network].nick;
}
return editedNick;
}
);
export const getCurrentNetworkName = createSelector(
getNetworks,
getSelectedTab,
(networks, tab) => get(networks, [tab.network, 'name'])
);
export const getCurrentNetworkError = createSelector(
getNetworks,
getSelectedTab,
(networks, tab) => get(networks, [tab.network, 'error'], null)
);
export default createReducer(
{},
{
[actions.CONNECT](state, { host, nick, name }) {
if (!state[host]) {
state[host] = {
nick,
editedNick: null,
name: name || host,
connected: false,
error: null,
features: {}
};
}
},
[actions.DISCONNECT](state, { network }) {
delete state[network];
},
[actions.SET_NETWORK_NAME](state, { network, name }) {
state[network].name = name;
},
[actions.SET_NICK](state, { network, nick, editing }) {
if (editing) {
state[network].editedNick = nick;
} else if (nick === '') {
state[network].editedNick = null;
}
},
[actions.socket.NICK](state, { network, oldNick, newNick }) {
if (!oldNick || oldNick === state[network].nick) {
state[network].nick = newNick;
state[network].editedNick = null;
}
},
[actions.socket.NICK_FAIL](state, { network }) {
state[network].editedNick = null;
},
[actions.socket.NETWORKS](state, { data }) {
if (data) {
data.forEach(
({ host, name = host, nick, connected, error, features = {} }) => {
state[host] = {
name,
nick,
connected,
error,
features,
editedNick: null
};
}
);
}
},
[actions.socket.CONNECTION_UPDATE](state, { network, connected, error }) {
if (state[network]) {
state[network].connected = connected;
state[network].error = error;
}
},
[actions.socket.FEATURES](state, { network, features }) {
const srv = state[network];
if (srv) {
srv.features = features;
if (features.NETWORK && srv.name === network) {
srv.name = features.NETWORK;
}
}
}
}
);
export function connect(config) {
return {
type: actions.CONNECT,
...config,
socket: {
type: 'connect',
data: config
}
};
}
export function disconnect(network) {
return dispatch => {
dispatch({
type: actions.DISCONNECT,
network,
socket: {
type: 'quit',
data: { network }
}
});
dispatch(updateSelection());
};
}
export function reconnect(network, settings) {
return {
type: actions.RECONNECT,
network,
settings,
socket: {
type: 'reconnect',
data: {
...settings,
network
}
}
};
}
export function whois(user, network) {
return {
type: actions.WHOIS,
user,
network,
socket: {
type: 'whois',
data: { user, network }
}
};
}
export function away(message, network) {
return {
type: actions.AWAY,
message,
network,
socket: {
type: 'away',
data: { message, network }
}
};
}
export function setNick(nick, network, editing) {
nick = nick.trim().replace(' ', '');
const action = {
type: actions.SET_NICK,
nick,
network,
editing
};
if (!editing && nick !== '') {
action.socket = {
type: 'nick',
data: {
newNick: nick,
network
}
};
}
return action;
}
export function isValidNetworkName(name) {
return name.trim() !== '';
}
export function setNetworkName(name, network) {
const action = {
type: actions.SET_NETWORK_NAME,
name,
network
};
if (isValidNetworkName(name)) {
action.socket = {
type: 'set_network_name',
data: {
name,
network
},
debounce: {
delay: 500,
key: `network_name:${network}`
}
};
}
return action;
}

View file

@ -5,13 +5,13 @@ import * as actions from './actions';
export const getPrivateChats = state => state.privateChats;
function open(state, server, nick) {
if (!state[server]) {
state[server] = [];
function open(state, network, nick) {
if (!state[network]) {
state[network] = [];
}
if (!state[server].includes(nick)) {
state[server].push(nick);
state[server] = sortBy(state[server], v => v.toLowerCase());
if (!state[network].includes(nick)) {
state[network].push(nick);
state[network] = sortBy(state[network], v => v.toLowerCase());
}
}
@ -19,63 +19,63 @@ export default createReducer(
{},
{
[actions.OPEN_PRIVATE_CHAT](state, action) {
open(state, action.server, action.nick);
open(state, action.network, action.nick);
},
[actions.CLOSE_PRIVATE_CHAT](state, { server, nick }) {
const i = state[server]?.findIndex(n => n === nick);
[actions.CLOSE_PRIVATE_CHAT](state, { network, nick }) {
const i = state[network]?.findIndex(n => n === nick);
if (i !== -1) {
state[server].splice(i, 1);
state[network].splice(i, 1);
}
},
[actions.PRIVATE_CHATS](state, { privateChats }) {
privateChats.forEach(({ server, name }) => {
if (!state[server]) {
state[server] = [];
privateChats.forEach(({ network, name }) => {
if (!state[network]) {
state[network] = [];
}
state[server].push(name);
state[network].push(name);
});
},
[actions.socket.PM](state, action) {
if (action.from.indexOf('.') === -1) {
open(state, action.server, action.from);
open(state, action.network, action.from);
}
},
[actions.DISCONNECT](state, { server }) {
delete state[server];
[actions.DISCONNECT](state, { network }) {
delete state[network];
}
}
);
export function openPrivateChat(server, nick) {
export function openPrivateChat(network, nick) {
return (dispatch, getState) => {
if (!getState().privateChats[server]?.includes(nick)) {
if (!getState().privateChats[network]?.includes(nick)) {
dispatch({
type: actions.OPEN_PRIVATE_CHAT,
server,
network,
nick,
socket: {
type: 'open_dm',
data: { server, name: nick }
data: { network, name: nick }
}
});
}
};
}
export function closePrivateChat(server, nick) {
export function closePrivateChat(network, nick) {
return dispatch => {
dispatch({
type: actions.CLOSE_PRIVATE_CHAT,
server,
network,
nick,
socket: {
type: 'close_dm',
data: { server, name: nick }
data: { network, name: nick }
}
});
dispatch(updateSelection());

View file

@ -18,15 +18,15 @@ export default createReducer(initialState, {
}
});
export function searchMessages(server, channel, phrase) {
export function searchMessages(network, channel, phrase) {
return {
type: actions.SEARCH_MESSAGES,
server,
network,
channel,
phrase,
socket: {
type: 'search',
data: { server, channel, phrase }
data: { network, channel, phrase }
}
};
}

View file

@ -1,11 +1,11 @@
import { createSelector } from 'reselect';
import get from 'lodash/get';
import { getServers } from './servers';
import { getNetworks } from './networks';
import { getSelectedTab } from './tab';
// eslint-disable-next-line import/prefer-default-export
export const getSelectedTabTitle = createSelector(
getSelectedTab,
getServers,
(tab, servers) => tab.name || get(servers, [tab.server, 'name'])
getNetworks,
(tab, networks) => tab.name || get(networks, [tab.network, 'name'])
);

View file

@ -1,222 +0,0 @@
import { createSelector } from 'reselect';
import get from 'lodash/get';
import createReducer from 'utils/createReducer';
import { getSelectedTab, updateSelection } from './tab';
import * as actions from './actions';
export const getServers = state => state.servers;
export const getCurrentNick = createSelector(
getServers,
getSelectedTab,
(servers, tab) => {
if (!servers[tab.server]) {
return;
}
const { editedNick } = servers[tab.server];
if (editedNick === null) {
return servers[tab.server].nick;
}
return editedNick;
}
);
export const getCurrentServerName = createSelector(
getServers,
getSelectedTab,
(servers, tab) => get(servers, [tab.server, 'name'])
);
export const getCurrentServerStatus = createSelector(
getServers,
getSelectedTab,
(servers, tab) => get(servers, [tab.server, 'status'], {})
);
export default createReducer(
{},
{
[actions.CONNECT](state, { host, nick, name }) {
if (!state[host]) {
state[host] = {
nick,
editedNick: null,
name: name || host,
status: {
connected: false,
error: null
},
features: {}
};
}
},
[actions.DISCONNECT](state, { server }) {
delete state[server];
},
[actions.SET_SERVER_NAME](state, { server, name }) {
state[server].name = name;
},
[actions.SET_NICK](state, { server, nick, editing }) {
if (editing) {
state[server].editedNick = nick;
} else if (nick === '') {
state[server].editedNick = null;
}
},
[actions.socket.NICK](state, { server, oldNick, newNick }) {
if (!oldNick || oldNick === state[server].nick) {
state[server].nick = newNick;
state[server].editedNick = null;
}
},
[actions.socket.NICK_FAIL](state, { server }) {
state[server].editedNick = null;
},
[actions.socket.SERVERS](state, { data }) {
if (data) {
data.forEach(({ host, name = host, nick, status, features = {} }) => {
state[host] = { name, nick, status, features, editedNick: null };
});
}
},
[actions.socket.CONNECTION_UPDATE](state, { server, connected, error }) {
if (state[server]) {
state[server].status.connected = connected;
state[server].status.error = error;
}
},
[actions.socket.FEATURES](state, { server, features }) {
const srv = state[server];
if (srv) {
srv.features = features;
if (features.NETWORK && srv.name === server) {
srv.name = features.NETWORK;
}
}
}
}
);
export function connect(config) {
return {
type: actions.CONNECT,
...config,
socket: {
type: 'connect',
data: config
}
};
}
export function disconnect(server) {
return dispatch => {
dispatch({
type: actions.DISCONNECT,
server,
socket: {
type: 'quit',
data: { server }
}
});
dispatch(updateSelection());
};
}
export function reconnect(server, settings) {
return {
type: actions.RECONNECT,
server,
settings,
socket: {
type: 'reconnect',
data: {
...settings,
server
}
}
};
}
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, editing) {
nick = nick.trim().replace(' ', '');
const action = {
type: actions.SET_NICK,
nick,
server,
editing
};
if (!editing && nick !== '') {
action.socket = {
type: 'nick',
data: {
newNick: nick,
server
}
};
}
return action;
}
export function isValidServerName(name) {
return name.trim() !== '';
}
export function setServerName(name, server) {
const action = {
type: actions.SET_SERVER_NAME,
name,
server
};
if (isValidServerName(name)) {
action.socket = {
type: 'set_server_name',
data: {
name,
server
},
debounce: {
delay: 500,
key: `server_name:${server}`
}
};
}
return action;
}

View file

@ -12,7 +12,7 @@ const initialState = {
function selectTab(state, action) {
state.selected = {
server: action.server,
network: action.network,
name: action.name
};
state.history.push(state.selected);
@ -25,18 +25,19 @@ export default createReducer(initialState, {
[actions.PART](state, action) {
state.history = state.history.filter(
tab => !(tab.server === action.server && tab.name === action.channels[0])
tab =>
!(tab.network === action.network && tab.name === action.channels[0])
);
},
[actions.CLOSE_PRIVATE_CHAT](state, action) {
state.history = state.history.filter(
tab => !(tab.server === action.server && tab.name === action.nick)
tab => !(tab.network === action.network && tab.name === action.nick)
);
},
[actions.DISCONNECT](state, action) {
state.history = state.history.filter(tab => tab.server !== action.server);
state.history = state.history.filter(tab => tab.network !== action.network);
},
[LOCATION_CHANGED](state, action) {
@ -49,30 +50,30 @@ export default createReducer(initialState, {
}
});
export function select(server, name, doReplace) {
export function select(network, name, doReplace) {
const navigate = doReplace ? replace : push;
if (name) {
return navigate(`/${server}/${encodeURIComponent(name)}`);
return navigate(`/${network}/${encodeURIComponent(name)}`);
}
return navigate(`/${server}`);
return navigate(`/${network}`);
}
export function tabExists(
{ server, name },
{ servers, channels, privateChats }
{ network, name },
{ networks, channels, privateChats }
) {
return (
(name && get(channels, [server, name])) ||
(!name && server && servers[server]) ||
(name && find(privateChats[server], nick => nick === name))
(name && get(channels, [network, name])) ||
(!name && network && networks[network]) ||
(name && find(privateChats[network], nick => nick === name))
);
}
function parseTabCookie() {
const cookie = Cookie.get('tab');
if (cookie) {
const [server, name = null] = cookie.split(/;(.+)/);
return { server, name };
const [network, name = null] = cookie.split(/;(.+)/);
return { network, name };
}
return null;
}
@ -88,35 +89,35 @@ export function updateSelection(tryCookie) {
if (tryCookie) {
const tab = parseTabCookie();
if (tab && tabExists(tab, state)) {
return dispatch(select(tab.server, tab.name, true));
return dispatch(select(tab.network, tab.name, true));
}
}
const { servers } = state;
const { networks } = state;
const { history } = state.tab;
const { server } = state.tab.selected;
const serverAddrs = Object.keys(servers);
const { network } = state.tab.selected;
const networkAddrs = Object.keys(networks);
if (serverAddrs.length === 0) {
if (networkAddrs.length === 0) {
dispatch(replace('/connect'));
} else if (
history.length > 0 &&
tabExists(history[history.length - 1], state)
) {
const tab = history[history.length - 1];
dispatch(select(tab.server, tab.name, true));
} else if (servers[server]) {
dispatch(select(server, null, true));
dispatch(select(tab.network, tab.name, true));
} else if (networks[network]) {
dispatch(select(network, null, true));
} else {
dispatch(select(serverAddrs.sort()[0], null, true));
dispatch(select(networkAddrs.sort()[0], null, true));
}
};
}
export function setSelectedTab(server, name = null) {
export function setSelectedTab(network, name = null) {
return {
type: actions.SELECT_TAB,
server,
network,
name
};
}