328 lines
7.1 KiB
JavaScript
328 lines
7.1 KiB
JavaScript
import { createSelector } from 'reselect';
|
|
import get from 'lodash/get';
|
|
import sortBy from 'lodash/sortBy';
|
|
import createReducer from 'utils/createReducer';
|
|
import { trimPrefixChar, find, findIndex } from 'utils';
|
|
import { getSelectedTab, updateSelection } from './tab';
|
|
import * as actions from './actions';
|
|
|
|
const modePrefixes = [
|
|
{ mode: 'q', prefix: '~' }, // Owner
|
|
{ mode: 'a', prefix: '&' }, // Admin
|
|
{ mode: 'o', prefix: '@' }, // Op
|
|
{ mode: 'h', prefix: '%' }, // Halfop
|
|
{ mode: 'v', prefix: '+' } // Voice
|
|
];
|
|
|
|
function getRenderName(user) {
|
|
for (let i = 0; i < modePrefixes.length; i++) {
|
|
if (user.mode.indexOf(modePrefixes[i].mode) !== -1) {
|
|
return `${modePrefixes[i].prefix}${user.nick}`;
|
|
}
|
|
}
|
|
|
|
return user.nick;
|
|
}
|
|
|
|
function createUser(nick, mode) {
|
|
const user = {
|
|
nick,
|
|
mode: mode || ''
|
|
};
|
|
user.renderName = getRenderName(user);
|
|
return user;
|
|
}
|
|
|
|
function loadUser(nick) {
|
|
let mode;
|
|
|
|
for (let i = 0; i < modePrefixes.length; i++) {
|
|
if (nick[0] === modePrefixes[i].prefix) {
|
|
({ mode } = modePrefixes[i]);
|
|
}
|
|
}
|
|
|
|
if (mode) {
|
|
return createUser(nick.slice(1), mode);
|
|
}
|
|
|
|
return createUser(nick);
|
|
}
|
|
|
|
function removeUser(users, nick) {
|
|
const i = findIndex(users, u => u.nick === nick);
|
|
if (i !== -1) {
|
|
users.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
function init(state, network, channel) {
|
|
if (!state[network]) {
|
|
state[network] = {};
|
|
}
|
|
if (channel && !state[network][channel]) {
|
|
state[network][channel] = {
|
|
name: channel,
|
|
users: [],
|
|
joined: false
|
|
};
|
|
}
|
|
return state[network][channel];
|
|
}
|
|
|
|
export function compareUsers(a, b) {
|
|
a = a.renderName.toLowerCase();
|
|
b = b.renderName.toLowerCase();
|
|
|
|
for (let i = 0; i < modePrefixes.length; i++) {
|
|
const { prefix } = modePrefixes[i];
|
|
|
|
if (a[0] === prefix && b[0] !== prefix) {
|
|
return -1;
|
|
}
|
|
if (b[0] === prefix && a[0] !== prefix) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (a < b) {
|
|
return -1;
|
|
}
|
|
if (a > b) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export const getChannels = state => state.channels;
|
|
|
|
export const getSortedChannels = createSelector(getChannels, channels =>
|
|
sortBy(
|
|
Object.keys(channels).map(network => ({
|
|
address: network,
|
|
channels: sortBy(channels[network], channel =>
|
|
trimPrefixChar(channel.name, '#').toLowerCase()
|
|
)
|
|
})),
|
|
network => network.address.toLowerCase()
|
|
)
|
|
);
|
|
|
|
export const getSelectedChannel = createSelector(
|
|
getSelectedTab,
|
|
getChannels,
|
|
(tab, channels) => get(channels, [tab.network, tab.name])
|
|
);
|
|
|
|
export const getSelectedChannelUsers = createSelector(
|
|
getSelectedChannel,
|
|
channel => {
|
|
if (channel) {
|
|
return channel.users.concat().sort(compareUsers);
|
|
}
|
|
return [];
|
|
}
|
|
);
|
|
|
|
export default createReducer(
|
|
{},
|
|
{
|
|
[actions.JOIN](state, { network, channels }) {
|
|
channels.forEach(channel => init(state, network, channel));
|
|
},
|
|
|
|
[actions.PART](state, { network, channels }) {
|
|
channels.forEach(channel => delete state[network][channel]);
|
|
},
|
|
|
|
[actions.socket.JOIN](state, { network, channels, user }) {
|
|
const channel = channels[0];
|
|
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.network, action.new);
|
|
delete state[action.network][action.old];
|
|
},
|
|
|
|
[actions.socket.PART](state, { network, channel, user }) {
|
|
if (state[network][channel]) {
|
|
removeUser(state[network][channel].users, user);
|
|
}
|
|
},
|
|
|
|
[actions.socket.QUIT](state, { network, user }) {
|
|
Object.keys(state[network]).forEach(channel => {
|
|
removeUser(state[network][channel].users, user);
|
|
});
|
|
},
|
|
|
|
[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[network][channel].users,
|
|
u => u.nick === oldNick
|
|
);
|
|
if (user) {
|
|
user.nick = newNick;
|
|
user.renderName = getRenderName(user);
|
|
}
|
|
});
|
|
},
|
|
|
|
[actions.socket.USERS](state, { network, channel, users }) {
|
|
state[network][channel].users = users.map(nick => loadUser(nick));
|
|
},
|
|
|
|
[actions.socket.TOPIC](state, { network, channel, topic }) {
|
|
state[network][channel].topic = topic;
|
|
},
|
|
|
|
[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;
|
|
while (j--) {
|
|
u.mode = u.mode.replace(remove[j], '');
|
|
}
|
|
}
|
|
|
|
if (add) {
|
|
u.mode += add;
|
|
}
|
|
|
|
u.renderName = getRenderName(u);
|
|
}
|
|
},
|
|
|
|
[actions.socket.CHANNELS](state, { data }) {
|
|
if (data) {
|
|
data.forEach(({ network, name, topic }) => {
|
|
const chan = init(state, network, name);
|
|
chan.joined = true;
|
|
chan.topic = topic;
|
|
});
|
|
}
|
|
},
|
|
|
|
[actions.socket.NETWORKS](state, { data }) {
|
|
if (data) {
|
|
data.forEach(({ host }) => init(state, host));
|
|
}
|
|
},
|
|
|
|
[actions.CONNECT](state, { host }) {
|
|
init(state, host);
|
|
},
|
|
|
|
[actions.DISCONNECT](state, { network }) {
|
|
delete state[network];
|
|
}
|
|
}
|
|
);
|
|
|
|
export function join(channels, network) {
|
|
return {
|
|
type: actions.JOIN,
|
|
channels,
|
|
network,
|
|
socket: {
|
|
type: 'join',
|
|
data: { channels, network }
|
|
}
|
|
};
|
|
}
|
|
|
|
export function part(channels, network) {
|
|
return (dispatch, getState) => {
|
|
const action = {
|
|
type: actions.PART,
|
|
channels,
|
|
network
|
|
};
|
|
|
|
const state = getState().channels[network];
|
|
const joined = channels.filter(c => state[c] && state[c].joined);
|
|
|
|
if (joined.length > 0) {
|
|
action.socket = {
|
|
type: 'part',
|
|
data: {
|
|
channels: joined,
|
|
network
|
|
}
|
|
};
|
|
}
|
|
|
|
dispatch(action);
|
|
dispatch(updateSelection());
|
|
};
|
|
}
|
|
|
|
export function invite(user, channel, network) {
|
|
return {
|
|
type: actions.INVITE,
|
|
user,
|
|
channel,
|
|
network,
|
|
socket: {
|
|
type: 'invite',
|
|
data: { user, channel, network }
|
|
}
|
|
};
|
|
}
|
|
|
|
export function kick(user, channel, network) {
|
|
return {
|
|
type: actions.KICK,
|
|
user,
|
|
channel,
|
|
network,
|
|
socket: {
|
|
type: 'kick',
|
|
data: { user, channel, network }
|
|
}
|
|
};
|
|
}
|
|
|
|
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,
|
|
network,
|
|
socket: {
|
|
type: 'topic',
|
|
data: { topic, channel, network }
|
|
}
|
|
};
|
|
}
|