Forward irc errors to the client, improve command validation and feedback, handle topic changes
This commit is contained in:
parent
993d29242e
commit
aa59e71745
17 changed files with 328 additions and 96 deletions
|
@ -428,11 +428,23 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
|
|||
color: #999;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
color: #F6546A;
|
||||
}
|
||||
|
||||
.message-prompt {
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
color: #6BB758;
|
||||
}
|
||||
|
||||
.message-action {
|
||||
color: #FF6698;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import createCommandMiddleware from './middleware/command';
|
||||
import createCommandMiddleware, { beforeHandler, notFoundHandler } from './middleware/command';
|
||||
import { COMMAND } from './state/actions';
|
||||
import { join, part, invite, kick } from './state/channels';
|
||||
import { sendMessage, addMessage, raw } from './state/messages';
|
||||
import { join, part, invite, kick, setTopic } from './state/channels';
|
||||
import { sendMessage, raw } from './state/messages';
|
||||
import { setNick, disconnect, whois, away } from './state/servers';
|
||||
import { select } from './state/tab';
|
||||
import { find } from './util';
|
||||
|
||||
const help = [
|
||||
'/join <channel> - Join a channel',
|
||||
|
@ -11,35 +12,50 @@ const help = [
|
|||
'/nick <nick> - Change nick',
|
||||
'/quit - Disconnect from the current server',
|
||||
'/me <message> - Send action message',
|
||||
'/topic - Show topic for the current channel',
|
||||
'/topic [topic] - Show or set topic in the current channel',
|
||||
'/msg <target> <message> - Send message to the specified channel or user',
|
||||
'/say <message> - Send message to the current chat',
|
||||
'/invite <user> [channel] - Invite user to the current or specified channel',
|
||||
'/kick <user> - Kick user from the current channel',
|
||||
'/whois <user> - Get information about user',
|
||||
'/invite <nick> [channel] - Invite user to the current or specified channel',
|
||||
'/kick <nick> - Kick user from the current channel',
|
||||
'/whois <nick> - Get information about user',
|
||||
'/away [message] - Set or clear away message',
|
||||
'/raw [message] - Send raw IRC message to the current server'
|
||||
'/raw [message] - Send raw IRC message to the current server',
|
||||
'/help [command]... - Print help for all or the specified command(s)'
|
||||
];
|
||||
|
||||
const text = content => ({ content });
|
||||
const error = content => ({ content, type: 'error' });
|
||||
const prompt = content => ({ content, type: 'prompt' });
|
||||
const findHelp = cmd => find(help, line => line.slice(1, line.indexOf(' ')) === cmd);
|
||||
|
||||
export default createCommandMiddleware(COMMAND, {
|
||||
join({ dispatch, server }, channel) {
|
||||
if (channel) {
|
||||
if (channel[0] !== '#') {
|
||||
return error('Bad channel name');
|
||||
}
|
||||
dispatch(join([channel], server));
|
||||
dispatch(select(server, channel));
|
||||
} else {
|
||||
return error('Missing channel');
|
||||
}
|
||||
},
|
||||
|
||||
part({ dispatch, server, channel }, partChannel) {
|
||||
part({ dispatch, server, channel, isChannel }, partChannel) {
|
||||
if (partChannel) {
|
||||
dispatch(part([partChannel], server));
|
||||
} else {
|
||||
} else if (isChannel) {
|
||||
dispatch(part([channel], server));
|
||||
} else {
|
||||
return error('This is not a channel');
|
||||
}
|
||||
},
|
||||
|
||||
nick({ dispatch, server }, nick) {
|
||||
if (nick) {
|
||||
dispatch(setNick(nick, server));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -47,74 +63,127 @@ export default createCommandMiddleware(COMMAND, {
|
|||
dispatch(disconnect(server));
|
||||
},
|
||||
|
||||
me({ dispatch, server, channel }, ...params) {
|
||||
if (params.length > 0) {
|
||||
dispatch(sendMessage(`\x01ACTION ${params.join(' ')}\x01`, channel, server));
|
||||
me({ dispatch, server, channel }, ...message) {
|
||||
const msg = message.join(' ');
|
||||
if (msg !== '') {
|
||||
dispatch(sendMessage(`\x01ACTION ${msg}\x01`, channel, server));
|
||||
} else {
|
||||
return error('Messages can not be empty');
|
||||
}
|
||||
},
|
||||
|
||||
topic({ dispatch, getState, server, channel }) {
|
||||
const topic = getState().channels.getIn([server, channel, 'topic']);
|
||||
if (topic) {
|
||||
dispatch(addMessage({
|
||||
server,
|
||||
to: channel,
|
||||
content: topic
|
||||
}));
|
||||
topic({ dispatch, getState, server, channel }, ...newTopic) {
|
||||
if (newTopic.length > 0) {
|
||||
dispatch(setTopic(newTopic.join(' '), channel, server));
|
||||
} else {
|
||||
const topic = getState().channels.getIn([server, channel, 'topic']);
|
||||
if (topic) {
|
||||
return text(topic);
|
||||
}
|
||||
return 'No topic set';
|
||||
}
|
||||
},
|
||||
|
||||
msg({ dispatch, server }, target, ...message) {
|
||||
if (target && message) {
|
||||
if (!target) {
|
||||
return error('Missing nick/channel');
|
||||
}
|
||||
|
||||
const msg = message.join(' ');
|
||||
if (msg !== '') {
|
||||
dispatch(sendMessage(message.join(' '), target, server));
|
||||
dispatch(select(server, target));
|
||||
} else {
|
||||
return error('Messages can not be empty');
|
||||
}
|
||||
},
|
||||
|
||||
say({ dispatch, server, channel }, ...message) {
|
||||
if (channel && message) {
|
||||
if (!channel) {
|
||||
return error('Messages can only be sent to channels or users');
|
||||
}
|
||||
|
||||
const msg = message.join(' ');
|
||||
if (msg !== '') {
|
||||
dispatch(sendMessage(message.join(' '), channel, server));
|
||||
} else {
|
||||
return error('Messages can not be empty');
|
||||
}
|
||||
},
|
||||
|
||||
invite({ dispatch, server, channel }, user, inviteChannel) {
|
||||
invite({ dispatch, server, channel, isChannel }, user, inviteChannel) {
|
||||
if (!inviteChannel && !isChannel) {
|
||||
return error('This is not a channel');
|
||||
}
|
||||
|
||||
if (user && inviteChannel) {
|
||||
dispatch(invite(user, inviteChannel, server));
|
||||
} else if (user && channel) {
|
||||
dispatch(invite(user, channel, server));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
kick({ dispatch, server, channel }, user) {
|
||||
if (user && channel) {
|
||||
kick({ dispatch, server, channel, isChannel }, user) {
|
||||
if (!isChannel) {
|
||||
return error('This is not a channel');
|
||||
}
|
||||
|
||||
if (user) {
|
||||
dispatch(kick(user, channel, server));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
whois({ dispatch, server }, user) {
|
||||
if (user) {
|
||||
dispatch(whois(user, server));
|
||||
} else {
|
||||
return error('Missing nick');
|
||||
}
|
||||
},
|
||||
|
||||
away({ dispatch, server }, message) {
|
||||
dispatch(away(message, server));
|
||||
away({ dispatch, server }, ...message) {
|
||||
const msg = message.join(' ');
|
||||
dispatch(away(msg, server));
|
||||
if (msg !== '') {
|
||||
return 'Away message set';
|
||||
}
|
||||
return 'Away message cleared';
|
||||
},
|
||||
|
||||
raw({ dispatch, server }, ...message) {
|
||||
if (message) {
|
||||
const cmd = message.join(' ');
|
||||
if (message.length > 0 && message[0] !== '') {
|
||||
const cmd = `${message[0].toUpperCase()} ${message.slice(1).join(' ')}`;
|
||||
dispatch(raw(cmd, server));
|
||||
return `=> ${cmd}`;
|
||||
return prompt(`=> ${cmd}`);
|
||||
}
|
||||
return [
|
||||
prompt('=> /raw'),
|
||||
error('Missing message')
|
||||
];
|
||||
},
|
||||
|
||||
help(_, ...commands) {
|
||||
if (commands.length > 0) {
|
||||
const cmdHelp = commands.filter(findHelp).map(findHelp);
|
||||
if (cmdHelp.length > 0) {
|
||||
return text(cmdHelp);
|
||||
}
|
||||
return error('Unable to find any help :(');
|
||||
}
|
||||
return text(help);
|
||||
},
|
||||
|
||||
[beforeHandler](_, command, ...params) {
|
||||
if (command !== 'raw') {
|
||||
return prompt(`=> /${command} ${params.join(' ')}`);
|
||||
}
|
||||
},
|
||||
|
||||
help() {
|
||||
return help;
|
||||
},
|
||||
|
||||
commandNotFound(_, command) {
|
||||
return `The command /${command} was not found`;
|
||||
[notFoundHandler](_, command) {
|
||||
return error(`=> /${command}: No such command`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,9 +1,26 @@
|
|||
import { inform } from '../state/messages';
|
||||
import { addMessages, inform, print } from '../state/messages';
|
||||
import { isChannel } from '../util';
|
||||
|
||||
const notFound = 'commandNotFound';
|
||||
export const beforeHandler = '_before';
|
||||
export const notFoundHandler = 'commandNotFound';
|
||||
|
||||
function createContext({ dispatch, getState }, { server, channel }) {
|
||||
return { dispatch, getState, server, channel };
|
||||
return { dispatch, getState, server, channel, isChannel: isChannel(channel) };
|
||||
}
|
||||
|
||||
// TODO: Pull this out as convenience action
|
||||
function process({ dispatch, server, channel }, result) {
|
||||
if (typeof result === 'string') {
|
||||
dispatch(inform(result, server, channel));
|
||||
} else if (Array.isArray(result)) {
|
||||
if (typeof result[0] === 'string') {
|
||||
dispatch(inform(result, server, channel));
|
||||
} else if (typeof result[0] === 'object') {
|
||||
dispatch(addMessages(result, server, channel));
|
||||
}
|
||||
} else if (typeof result === 'object' && result) {
|
||||
dispatch(print(result.content, server, channel, result.type));
|
||||
}
|
||||
}
|
||||
|
||||
export default function createCommandMiddleware(type, handlers) {
|
||||
|
@ -13,16 +30,15 @@ export default function createCommandMiddleware(type, handlers) {
|
|||
const command = words[0];
|
||||
const params = words.slice(1);
|
||||
|
||||
let result;
|
||||
|
||||
if (command in handlers) {
|
||||
result = handlers[command](createContext(store, action), ...params);
|
||||
} else if (notFound in handlers) {
|
||||
result = handlers[notFound](createContext(store, action), command);
|
||||
}
|
||||
|
||||
if (typeof result === 'string' || Array.isArray(result)) {
|
||||
store.dispatch(inform(result, action.server, action.channel));
|
||||
const ctx = createContext(store, action);
|
||||
if (beforeHandler in handlers) {
|
||||
process(ctx, handlers[beforeHandler](ctx, command, ...params));
|
||||
}
|
||||
process(ctx, handlers[command](ctx, ...params));
|
||||
} else if (notFoundHandler in handlers) {
|
||||
const ctx = createContext(store, action);
|
||||
process(ctx, handlers[notFoundHandler](ctx, command));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { socketAction } from '../state/actions';
|
||||
import { broadcast, inform, addMessage, addMessages } from '../state/messages';
|
||||
import { broadcast, inform, print, addMessage, addMessages } from '../state/messages';
|
||||
import { select } from '../state/tab';
|
||||
import { normalizeChannel } from '../util';
|
||||
import { replace } from '../util/router';
|
||||
|
@ -71,25 +71,39 @@ export default function handleSocket({ socket, store: { dispatch, getState } })
|
|||
dispatch(broadcast(`${data.old} changed nick to ${data.new}`, data.server, channels));
|
||||
},
|
||||
|
||||
topic({ server, channel, topic, nick }) {
|
||||
if (nick) {
|
||||
if (topic) {
|
||||
dispatch(inform(`${nick} changed the topic to:`, server, channel));
|
||||
dispatch(print(topic, server, channel));
|
||||
} else {
|
||||
dispatch(inform(`${nick} cleared the topic`, server, channel));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
motd({ content, server }) {
|
||||
dispatch(addMessages(content.map(line => ({ content: line })), server));
|
||||
},
|
||||
|
||||
whois(data) {
|
||||
const tab = getState().tab.selected;
|
||||
if (data.nick) {
|
||||
const tab = getState().tab.selected;
|
||||
|
||||
dispatch(inform([
|
||||
`Nick: ${data.nick}`,
|
||||
`Username: ${data.username}`,
|
||||
`Realname: ${data.realname}`,
|
||||
`Host: ${data.host}`,
|
||||
`Server: ${data.server}`,
|
||||
`Channels: ${data.channels}`
|
||||
], tab.server, tab.name));
|
||||
dispatch(print([
|
||||
`Nick: ${data.nick}`,
|
||||
`Username: ${data.username}`,
|
||||
`Realname: ${data.realname}`,
|
||||
`Host: ${data.host}`,
|
||||
`Server: ${data.server}`,
|
||||
`Channels: ${data.channels}`
|
||||
], tab.server, tab.name));
|
||||
}
|
||||
},
|
||||
|
||||
print({ server, message }) {
|
||||
dispatch(inform(message, server));
|
||||
print(message) {
|
||||
const tab = getState().tab.selected;
|
||||
dispatch(addMessage(message, tab.server, tab.name));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ export const INVITE = 'INVITE';
|
|||
export const JOIN = 'JOIN';
|
||||
export const KICK = 'KICK';
|
||||
export const PART = 'PART';
|
||||
export const SET_TOPIC = 'SET_TOPIC';
|
||||
|
||||
export const SET_ENVIRONMENT = 'SET_ENVIRONMENT';
|
||||
|
||||
|
|
|
@ -269,3 +269,16 @@ export function kick(user, channel, server) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function setTopic(topic, channel, server) {
|
||||
return {
|
||||
type: actions.SET_TOPIC,
|
||||
topic,
|
||||
channel,
|
||||
server,
|
||||
socket: {
|
||||
type: 'topic',
|
||||
data: { topic, channel, server }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -223,20 +223,24 @@ export function broadcast(message, server, channels) {
|
|||
})), server);
|
||||
}
|
||||
|
||||
export function inform(message, server, channel) {
|
||||
export function print(message, server, channel, type) {
|
||||
if (Array.isArray(message)) {
|
||||
return addMessages(message.map(line => ({
|
||||
content: line,
|
||||
type: 'info'
|
||||
type
|
||||
})), server, channel);
|
||||
}
|
||||
|
||||
return addMessage({
|
||||
content: message,
|
||||
type: 'info'
|
||||
type
|
||||
}, server, channel);
|
||||
}
|
||||
|
||||
export function inform(message, server, channel) {
|
||||
return print(message, server, channel, 'info');
|
||||
}
|
||||
|
||||
export function runCommand(command, channel, server) {
|
||||
return {
|
||||
type: actions.COMMAND,
|
||||
|
|
|
@ -11,6 +11,11 @@ export function normalizeChannel(channel) {
|
|||
return channel.split('#').join('').toLowerCase();
|
||||
}
|
||||
|
||||
export function isChannel(name) {
|
||||
// TODO: Handle other channel types
|
||||
return typeof name === 'string' && name[0] === '#';
|
||||
}
|
||||
|
||||
export function timestamp(date = new Date()) {
|
||||
const h = padStart(date.getHours(), 2, '0');
|
||||
const m = padStart(date.getMinutes(), 2, '0');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue