import { createSelector } from 'reselect'; import get from 'lodash/get'; import sortBy from 'lodash/sortBy'; import createReducer from 'utils/createReducer'; import { find, findIndex } from 'utils'; import { getSelectedTab, updateSelection } from './tab'; import * as actions from './actions'; const 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, server, channel) { if (!state[server]) { state[server] = {}; } if (channel && !state[server][channel]) { state[server][channel] = { name: channel, users: [], joined: false }; } } 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(server => ({ address: server, channels: sortBy(channels[server], channel => channel.name.toLowerCase()) })), server => server.address.toLowerCase() ) ); export const getSelectedChannel = createSelector( getSelectedTab, getChannels, (tab, channels) => get(channels, [tab.server, tab.name]) ); export const getSelectedChannelUsers = createSelector( getSelectedChannel, channel => { if (channel) { return channel.users.concat().sort(compareUsers); } return []; } ); export default createReducer( {}, { [actions.JOIN](state, { server, channels }) { channels.forEach(channel => init(state, server, channel)); }, [actions.PART](state, { server, channels }) { channels.forEach(channel => delete state[server][channel]); }, [actions.socket.JOIN](state, { server, 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)); }, [actions.socket.CHANNEL_FORWARD](state, action) { init(state, action.server, action.new); delete state[action.server][action.old]; }, [actions.socket.PART](state, { server, channel, user }) { if (state[server][channel]) { removeUser(state[server][channel].users, user); } }, [actions.socket.QUIT](state, { server, user }) { Object.keys(state[server]).forEach(channel => { removeUser(state[server][channel].users, user); }); }, [actions.socket.NICK](state, { server, oldNick, newNick }) { Object.keys(state[server]).forEach(channel => { const user = find( state[server][channel].users, u => u.nick === oldNick ); if (user) { user.nick = newNick; user.renderName = getRenderName(user); } }); }, [actions.socket.USERS](state, { server, channel, users }) { state[server][channel].users = users.map(nick => loadUser(nick)); }, [actions.socket.TOPIC](state, { server, channel, topic }) { state[server][channel].topic = topic; }, [actions.socket.MODE](state, { server, channel, user, remove, add }) { const u = find(state[server][channel].users, v => v.nick === user); if (u) { 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(({ server, name, topic }) => { init(state, server, name); state[server][name].joined = true; state[server][name].topic = topic; }); } }, [actions.socket.SERVERS](state, { data }) { if (data) { data.forEach(({ host }) => init(state, host)); } }, [actions.CONNECT](state, { host }) { init(state, host); }, [actions.DISCONNECT](state, { server }) { delete state[server]; } } ); export function join(channels, server) { return { type: actions.JOIN, channels, server, socket: { type: 'join', data: { channels, server } } }; } export function part(channels, server) { return (dispatch, getState) => { const action = { type: actions.PART, channels, server }; const state = getState().channels[server]; const joined = channels.filter(c => state[c] && state[c].joined); if (joined.length > 0) { action.socket = { type: 'part', data: { channels: joined, server } }; } dispatch(action); 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 } } }; } export function setTopic(topic, channel, server) { return { type: actions.SET_TOPIC, topic, channel, server, socket: { type: 'topic', data: { topic, channel, server } } }; }