2021-03-02 16:46:48 -05:00
|
|
|
|
import * as irc from "../lib/irc.js";
|
|
|
|
|
import Client from "../lib/client.js";
|
|
|
|
|
import Buffer from "./buffer.js";
|
|
|
|
|
import BufferList from "./buffer-list.js";
|
|
|
|
|
import BufferHeader from "./buffer-header.js";
|
|
|
|
|
import MemberList from "./member-list.js";
|
2021-03-09 07:25:31 -05:00
|
|
|
|
import ConnectForm from "./connect-form.js";
|
|
|
|
|
import JoinForm from "./join-form.js";
|
2021-03-08 11:05:48 -05:00
|
|
|
|
import Help from "./help.js";
|
2021-03-08 12:15:04 -05:00
|
|
|
|
import NetworkForm from "./network-form.js";
|
2021-03-02 16:46:48 -05:00
|
|
|
|
import Composer from "./composer.js";
|
|
|
|
|
import ScrollManager from "./scroll-manager.js";
|
2021-03-08 10:23:16 -05:00
|
|
|
|
import Dialog from "./dialog.js";
|
2021-03-02 16:46:48 -05:00
|
|
|
|
import { html, Component, createRef } from "../lib/index.js";
|
|
|
|
|
import { strip as stripANSI } from "../lib/ansi.js";
|
2021-06-04 12:03:03 -04:00
|
|
|
|
import { SERVER_BUFFER, BufferType, ReceiptType, ServerStatus, Unread, State } from "../state.js";
|
2021-03-02 16:46:48 -05:00
|
|
|
|
import commands from "../commands.js";
|
|
|
|
|
import { setup as setupKeybindings } from "../keybindings.js";
|
2021-05-26 12:43:11 -04:00
|
|
|
|
import * as store from "../store.js";
|
2020-06-26 04:35:38 -04:00
|
|
|
|
|
2021-05-26 17:17:23 -04:00
|
|
|
|
const configPromise = fetch("./config.json")
|
2021-05-25 06:33:22 -04:00
|
|
|
|
.then((resp) => {
|
|
|
|
|
if (resp.ok) {
|
|
|
|
|
return resp.json();
|
|
|
|
|
}
|
|
|
|
|
if (resp.status !== 404) {
|
|
|
|
|
console.error("Failed to fetch config: HTTP error:", resp.status, resp.statusText);
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error("Failed to fetch config:", err);
|
|
|
|
|
return {};
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
const CHATHISTORY_MAX_SIZE = 4000;
|
|
|
|
|
|
2020-06-28 09:13:06 -04:00
|
|
|
|
var messagesCount = 0;
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
function parseQueryString() {
|
|
|
|
|
var query = window.location.search.substring(1);
|
|
|
|
|
var params = {};
|
|
|
|
|
query.split('&').forEach((s) => {
|
|
|
|
|
if (!s) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var pair = s.split('=');
|
|
|
|
|
params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
|
|
|
|
|
});
|
|
|
|
|
return params;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-31 11:57:17 -04:00
|
|
|
|
function fillConnectParams(params) {
|
2021-05-31 12:11:33 -04:00
|
|
|
|
var host = window.location.host || "localhost:8080";
|
|
|
|
|
var proto = "wss:";
|
|
|
|
|
if (window.location.protocol != "https:") {
|
|
|
|
|
proto = "ws:";
|
|
|
|
|
}
|
|
|
|
|
var path = window.location.pathname || "/";
|
|
|
|
|
if (!window.location.host) {
|
|
|
|
|
path = "/";
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-31 11:57:17 -04:00
|
|
|
|
params = { ...params };
|
2021-05-31 12:11:33 -04:00
|
|
|
|
if (!params.url) {
|
|
|
|
|
params.url = proto + "//" + host + path + "socket";
|
|
|
|
|
}
|
|
|
|
|
if (params.url.startsWith("/")) {
|
|
|
|
|
params.url = proto + "//" + host + params.url;
|
|
|
|
|
}
|
2021-05-31 11:57:17 -04:00
|
|
|
|
if (!params.username) {
|
|
|
|
|
params.username = params.nick;
|
|
|
|
|
}
|
|
|
|
|
if (!params.realname) {
|
|
|
|
|
params.realname = params.nick;
|
|
|
|
|
}
|
|
|
|
|
return params;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-29 03:06:47 -04:00
|
|
|
|
/* Insert a message in an immutable list of sorted messages. */
|
|
|
|
|
function insertMessage(list, msg) {
|
|
|
|
|
if (list.length == 0) {
|
|
|
|
|
return [msg];
|
|
|
|
|
} else if (list[list.length - 1].tags.time <= msg.tags.time) {
|
|
|
|
|
return list.concat(msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var insertBefore = -1;
|
|
|
|
|
for (var i = 0; i < list.length; i++) {
|
|
|
|
|
var other = list[i];
|
|
|
|
|
if (msg.tags.time < other.tags.time) {
|
|
|
|
|
insertBefore = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.assert(insertBefore >= 0, "");
|
|
|
|
|
|
|
|
|
|
list = [ ...list ];
|
|
|
|
|
list.splice(insertBefore, 0, msg);
|
|
|
|
|
return list;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
function debounce(f, delay) {
|
|
|
|
|
var timeout = null;
|
|
|
|
|
return (...args) => {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
|
timeout = null;
|
|
|
|
|
f(...args);
|
|
|
|
|
}, delay);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-09 06:04:12 -05:00
|
|
|
|
function isServerBuffer(buf) {
|
|
|
|
|
return buf.type == BufferType.SERVER;
|
|
|
|
|
}
|
2021-01-22 04:37:58 -05:00
|
|
|
|
|
2021-03-09 06:04:12 -05:00
|
|
|
|
/* Returns 1 if a should appear after b, -1 if a should appear before b, or
|
|
|
|
|
* 0 otherwise. */
|
|
|
|
|
function compareBuffers(a, b) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
if (a.server != b.server) {
|
|
|
|
|
return a.server > b.server ? 1 : -1;
|
2020-08-03 09:43:20 -04:00
|
|
|
|
}
|
2021-03-09 06:04:12 -05:00
|
|
|
|
if (isServerBuffer(a) != isServerBuffer(b)) {
|
|
|
|
|
return isServerBuffer(b) ? 1 : -1;
|
2020-08-03 09:43:20 -04:00
|
|
|
|
}
|
2021-03-09 06:04:12 -05:00
|
|
|
|
if (a.name != b.name) {
|
|
|
|
|
return a.name > b.name ? 1 : -1;
|
2020-08-03 09:43:20 -04:00
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
export default class App extends Component {
|
|
|
|
|
state = {
|
|
|
|
|
connectParams: {
|
2021-05-31 12:04:02 -04:00
|
|
|
|
url: null,
|
|
|
|
|
pass: null,
|
2020-06-18 08:23:08 -04:00
|
|
|
|
username: null,
|
|
|
|
|
realname: null,
|
|
|
|
|
nick: null,
|
|
|
|
|
saslPlain: null,
|
2020-06-24 12:14:46 -04:00
|
|
|
|
autoconnect: false,
|
2020-06-18 08:23:08 -04:00
|
|
|
|
autojoin: [],
|
|
|
|
|
},
|
2021-06-03 05:46:50 -04:00
|
|
|
|
servers: new Map(),
|
2020-06-18 08:23:08 -04:00
|
|
|
|
buffers: new Map(),
|
2021-01-22 15:01:03 -05:00
|
|
|
|
bouncerNetworks: new Map(),
|
2020-06-18 08:23:08 -04:00
|
|
|
|
activeBuffer: null,
|
2021-03-08 10:23:16 -05:00
|
|
|
|
dialog: null,
|
2020-08-08 00:08:51 -04:00
|
|
|
|
error: null,
|
2021-05-27 10:35:33 -04:00
|
|
|
|
openPanels: {
|
|
|
|
|
bufferList: false,
|
|
|
|
|
memberList: false,
|
|
|
|
|
},
|
2020-06-18 08:23:08 -04:00
|
|
|
|
};
|
2021-05-31 06:26:47 -04:00
|
|
|
|
config = {};
|
2021-01-22 11:36:53 -05:00
|
|
|
|
clients = new Map();
|
2020-06-29 03:06:47 -04:00
|
|
|
|
endOfHistory = new Map();
|
2020-07-15 12:21:09 -04:00
|
|
|
|
receipts = new Map();
|
2020-06-25 06:03:05 -04:00
|
|
|
|
buffer = createRef();
|
2020-06-18 08:23:08 -04:00
|
|
|
|
composer = createRef();
|
2021-06-03 05:46:50 -04:00
|
|
|
|
lastServerID = 0;
|
2021-01-21 14:41:44 -05:00
|
|
|
|
lastBufferID = 0;
|
2021-05-25 06:40:33 -04:00
|
|
|
|
switchToChannel = null;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
|
|
this.handleConnectSubmit = this.handleConnectSubmit.bind(this);
|
2021-03-08 10:23:16 -05:00
|
|
|
|
this.handleJoinSubmit = this.handleJoinSubmit.bind(this);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
this.handleBufferListClick = this.handleBufferListClick.bind(this);
|
2021-05-27 10:35:33 -04:00
|
|
|
|
this.toggleBufferList = this.toggleBufferList.bind(this);
|
|
|
|
|
this.toggleMemberList = this.toggleMemberList.bind(this);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
this.handleComposerSubmit = this.handleComposerSubmit.bind(this);
|
2021-05-31 22:39:35 -04:00
|
|
|
|
this.handleChannelClick = this.handleChannelClick.bind(this);
|
2020-06-25 12:45:41 -04:00
|
|
|
|
this.handleNickClick = this.handleNickClick.bind(this);
|
2020-06-29 06:36:17 -04:00
|
|
|
|
this.autocomplete = this.autocomplete.bind(this);
|
2020-06-29 03:06:47 -04:00
|
|
|
|
this.handleBufferScrollTop = this.handleBufferScrollTop.bind(this);
|
2021-03-08 10:23:16 -05:00
|
|
|
|
this.handleDialogDismiss = this.handleDialogDismiss.bind(this);
|
2021-03-08 12:15:04 -05:00
|
|
|
|
this.handleAddNetworkClick = this.handleAddNetworkClick.bind(this);
|
2021-03-09 13:10:22 -05:00
|
|
|
|
this.handleNetworkSubmit = this.handleNetworkSubmit.bind(this);
|
|
|
|
|
this.handleNetworkRemove = this.handleNetworkRemove.bind(this);
|
2020-08-08 00:08:51 -04:00
|
|
|
|
this.dismissError = this.dismissError.bind(this);
|
2020-06-24 12:14:46 -04:00
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.saveReceipts = debounce(this.saveReceipts.bind(this), 500);
|
|
|
|
|
|
2021-05-26 12:43:11 -04:00
|
|
|
|
this.receipts = store.receipts.load();
|
2020-07-01 06:25:57 -04:00
|
|
|
|
|
2021-05-25 06:33:22 -04:00
|
|
|
|
configPromise.then((config) => {
|
|
|
|
|
this.handleConfig(config);
|
|
|
|
|
return config;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-27 04:36:03 -04:00
|
|
|
|
/**
|
|
|
|
|
* Handle configuration data and populate the connection parameters.
|
|
|
|
|
*
|
|
|
|
|
* The priority order is:
|
|
|
|
|
*
|
|
|
|
|
* - URL params
|
|
|
|
|
* - Saved parameters in local storage
|
|
|
|
|
* - Configuration data (fetched from the config.json file)
|
|
|
|
|
* - Default server URL constructed from the current URL location
|
|
|
|
|
*/
|
2021-05-25 06:33:22 -04:00
|
|
|
|
handleConfig(config) {
|
2021-05-27 12:17:41 -04:00
|
|
|
|
this.config = config;
|
|
|
|
|
|
2021-05-31 12:11:33 -04:00
|
|
|
|
var connectParams = {};
|
2021-05-25 06:33:22 -04:00
|
|
|
|
|
|
|
|
|
if (config.server) {
|
2021-05-31 12:04:02 -04:00
|
|
|
|
connectParams.url = config.server.url;
|
2021-05-25 06:33:22 -04:00
|
|
|
|
if (Array.isArray(config.server.autojoin)) {
|
|
|
|
|
connectParams.autojoin = config.server.autojoin;
|
2020-07-01 06:25:57 -04:00
|
|
|
|
} else {
|
2021-05-25 06:33:22 -04:00
|
|
|
|
connectParams.autojoin = [config.server.autojoin];
|
2020-06-24 12:14:46 -04:00
|
|
|
|
}
|
2021-05-25 06:33:22 -04:00
|
|
|
|
}
|
2020-06-24 12:14:46 -04:00
|
|
|
|
|
2021-05-27 04:36:03 -04:00
|
|
|
|
var autoconnect = store.autoconnect.load();
|
|
|
|
|
if (autoconnect) {
|
|
|
|
|
connectParams = {
|
|
|
|
|
...connectParams,
|
|
|
|
|
...autoconnect,
|
|
|
|
|
autoconnect: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-25 06:33:22 -04:00
|
|
|
|
var queryParams = parseQueryString();
|
|
|
|
|
if (queryParams.server) {
|
2021-05-31 12:11:33 -04:00
|
|
|
|
connectParams.url = queryParams.server;
|
2020-06-24 12:14:46 -04:00
|
|
|
|
}
|
2021-05-27 06:32:22 -04:00
|
|
|
|
if (queryParams.nick) {
|
|
|
|
|
connectParams.nick = queryParams.nick;
|
|
|
|
|
}
|
2021-05-25 06:33:22 -04:00
|
|
|
|
if (queryParams.channels) {
|
|
|
|
|
connectParams.autojoin = queryParams.channels.split(",");
|
2020-07-15 12:21:09 -04:00
|
|
|
|
}
|
2021-05-25 06:33:22 -04:00
|
|
|
|
|
2021-05-26 16:57:21 -04:00
|
|
|
|
if (window.location.hash) {
|
|
|
|
|
connectParams.autojoin = window.location.hash.split(",");
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-25 06:33:22 -04:00
|
|
|
|
this.setState((state) => {
|
|
|
|
|
return { connectParams: { ...state.connectParams, ...connectParams } };
|
|
|
|
|
});
|
2021-05-27 04:36:03 -04:00
|
|
|
|
|
|
|
|
|
if (connectParams.autoconnect) {
|
|
|
|
|
this.connect(connectParams);
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-08 00:08:51 -04:00
|
|
|
|
dismissError(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
this.setState({ error: null });
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
setServerState(id, updater, callback) {
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.setState((state) => {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
return State.updateServer(state, id, updater);
|
2021-01-21 13:01:50 -05:00
|
|
|
|
}, callback);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
setBufferState(id, updater, callback) {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
this.setState((state) => {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
return State.updateBuffer(state, id, updater);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}, callback);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
createBuffer(serverID, name, callback) {
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var id = null;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
this.setState((state) => {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
if (State.getBuffer(state, { server: serverID, name })) {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.lastBufferID++;
|
|
|
|
|
id = this.lastBufferID;
|
|
|
|
|
|
2020-06-26 04:35:38 -04:00
|
|
|
|
var type;
|
|
|
|
|
if (name == SERVER_BUFFER) {
|
|
|
|
|
type = BufferType.SERVER;
|
|
|
|
|
} else if (this.isChannel(name)) {
|
|
|
|
|
type = BufferType.CHANNEL;
|
|
|
|
|
} else {
|
|
|
|
|
type = BufferType.NICK;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(serverID);
|
2021-05-27 09:17:18 -04:00
|
|
|
|
var cm = client ? client.cm : irc.CaseMapping.RFC1459;
|
|
|
|
|
|
2020-08-03 09:43:20 -04:00
|
|
|
|
var bufferList = Array.from(state.buffers.values());
|
|
|
|
|
bufferList.push({
|
2021-01-21 14:41:44 -05:00
|
|
|
|
id,
|
2020-06-26 04:35:38 -04:00
|
|
|
|
name,
|
|
|
|
|
type,
|
2021-06-03 05:46:50 -04:00
|
|
|
|
server: serverID,
|
2020-06-26 06:08:14 -04:00
|
|
|
|
serverInfo: null, // if server
|
2020-06-26 06:00:10 -04:00
|
|
|
|
topic: null, // if channel
|
2021-05-27 09:17:18 -04:00
|
|
|
|
members: new irc.CaseMapMap(null, cm), // if channel
|
2020-06-26 06:00:10 -04:00
|
|
|
|
who: null, // if nick
|
2020-07-13 06:51:09 -04:00
|
|
|
|
offline: false, // if nick
|
2020-06-18 08:23:08 -04:00
|
|
|
|
messages: [],
|
2020-06-24 10:56:28 -04:00
|
|
|
|
unread: Unread.NONE,
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
2020-08-03 09:43:20 -04:00
|
|
|
|
bufferList = bufferList.sort(compareBuffers);
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var buffers = new Map(bufferList.map((buf) => [buf.id, buf]));
|
2020-06-18 08:23:08 -04:00
|
|
|
|
return { buffers };
|
2021-01-21 14:41:44 -05:00
|
|
|
|
}, () => {
|
|
|
|
|
if (callback) {
|
|
|
|
|
callback(id);
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 16:15:33 -05:00
|
|
|
|
switchBuffer(id) {
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var buf;
|
|
|
|
|
this.setState((state) => {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
buf = State.getBuffer(state, id);
|
2021-01-21 14:41:44 -05:00
|
|
|
|
if (!buf) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
return { activeBuffer: buf.id };
|
|
|
|
|
}, () => {
|
2021-01-21 16:15:33 -05:00
|
|
|
|
if (!buf) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var lastReadReceipt = this.getReceipt(buf.name, ReceiptType.READ);
|
|
|
|
|
// TODO: only mark as read if user scrolled at the bottom
|
|
|
|
|
this.setBufferState(buf.id, {
|
|
|
|
|
unread: Unread.NONE,
|
|
|
|
|
lastReadReceipt,
|
|
|
|
|
});
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
if (this.composer.current) {
|
|
|
|
|
this.composer.current.focus();
|
|
|
|
|
}
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2021-01-21 16:15:33 -05:00
|
|
|
|
if (buf.messages.length == 0) {
|
2020-07-15 12:21:09 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var lastMsg = buf.messages[buf.messages.length - 1];
|
2021-01-21 16:15:33 -05:00
|
|
|
|
this.setReceipt(buf.name, ReceiptType.READ, lastMsg);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveReceipts() {
|
2021-05-26 12:43:11 -04:00
|
|
|
|
store.receipts.put(this.receipts);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getReceipt(target, type) {
|
|
|
|
|
var receipts = this.receipts.get(target);
|
|
|
|
|
if (!receipts) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
return receipts[type];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasReceipt(target, type, msg) {
|
|
|
|
|
var receipt = this.getReceipt(target, type);
|
|
|
|
|
return receipt && msg.tags.time <= receipt.time;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setReceipt(target, type, msg) {
|
|
|
|
|
var receipt = this.getReceipt(target, type);
|
|
|
|
|
if (this.hasReceipt(target, type, msg)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-06-03 05:04:32 -04:00
|
|
|
|
// TODO: this doesn't trigger a redraw
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.receipts.set(target, {
|
|
|
|
|
...this.receipts.get(target),
|
|
|
|
|
[type]: { time: msg.tags.time },
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.saveReceipts();
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-18 10:53:52 -04:00
|
|
|
|
latestReceipt(type) {
|
|
|
|
|
var last = null;
|
|
|
|
|
this.receipts.forEach((receipts, target) => {
|
|
|
|
|
var delivery = receipts[type];
|
|
|
|
|
if (target == "*" || !delivery || !delivery.time) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!last || delivery.time > last.time) {
|
|
|
|
|
last = delivery;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return last;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
addMessage(serverID, bufName, msg) {
|
|
|
|
|
var client = this.clients.get(serverID);
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2020-06-28 09:13:06 -04:00
|
|
|
|
msg.key = messagesCount;
|
|
|
|
|
messagesCount++;
|
|
|
|
|
|
2021-01-22 11:36:53 -05:00
|
|
|
|
msg.isHighlight = irc.isHighlight(msg, client.nick);
|
2020-08-13 05:58:36 -04:00
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
if (!msg.tags) {
|
|
|
|
|
msg.tags = {};
|
|
|
|
|
}
|
2020-06-29 03:06:47 -04:00
|
|
|
|
if (!msg.tags.time) {
|
|
|
|
|
msg.tags.time = irc.formatDate(new Date());
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
var isDelivered = this.hasReceipt(bufName, ReceiptType.DELIVERED, msg);
|
|
|
|
|
var isRead = this.hasReceipt(bufName, ReceiptType.READ, msg);
|
|
|
|
|
// TODO: messages coming from infinite scroll shouldn't trigger notifications
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
2020-06-24 11:46:43 -04:00
|
|
|
|
var msgUnread = Unread.NONE;
|
2020-07-15 12:21:09 -04:00
|
|
|
|
if ((msg.command == "PRIVMSG" || msg.command == "NOTICE") && !isRead) {
|
2020-06-29 05:08:47 -04:00
|
|
|
|
var text = msg.params[1];
|
2020-06-29 05:50:42 -04:00
|
|
|
|
|
|
|
|
|
var kind;
|
2020-08-13 05:58:36 -04:00
|
|
|
|
if (msg.isHighlight) {
|
2020-06-29 05:08:47 -04:00
|
|
|
|
msgUnread = Unread.HIGHLIGHT;
|
2020-06-29 05:50:42 -04:00
|
|
|
|
kind = "highlight";
|
2021-06-03 07:29:32 -04:00
|
|
|
|
} else if (client.isMyNick(bufName)) {
|
2020-06-29 05:50:42 -04:00
|
|
|
|
msgUnread = Unread.HIGHLIGHT;
|
|
|
|
|
kind = "private message";
|
2020-06-29 05:08:47 -04:00
|
|
|
|
} else {
|
|
|
|
|
msgUnread = Unread.MESSAGE;
|
|
|
|
|
}
|
2020-06-29 05:50:42 -04:00
|
|
|
|
|
2020-08-13 10:04:39 -04:00
|
|
|
|
if (msgUnread == Unread.HIGHLIGHT && window.Notification && Notification.permission === "granted" && !isDelivered && !irc.parseCTCP(msg)) {
|
2020-06-29 05:50:42 -04:00
|
|
|
|
var title = "New " + kind + " from " + msg.prefix.name;
|
2021-06-03 07:29:32 -04:00
|
|
|
|
if (this.isChannel(bufName)) {
|
|
|
|
|
title += " in " + bufName;
|
2020-06-29 05:50:42 -04:00
|
|
|
|
}
|
|
|
|
|
var notif = new Notification(title, {
|
2020-08-13 09:38:12 -04:00
|
|
|
|
body: stripANSI(text),
|
2020-06-29 05:50:42 -04:00
|
|
|
|
requireInteraction: true,
|
|
|
|
|
});
|
|
|
|
|
notif.addEventListener("click", () => {
|
|
|
|
|
// TODO: scroll to message
|
2021-06-03 07:29:32 -04:00
|
|
|
|
this.switchBuffer({ server: serverID, name: bufName });
|
2020-06-29 05:50:42 -04:00
|
|
|
|
});
|
|
|
|
|
}
|
2020-06-24 10:56:28 -04:00
|
|
|
|
}
|
2021-06-03 05:04:32 -04:00
|
|
|
|
if (msg.command === "INVITE" && client.isMyNick(msg.params[0])) {
|
|
|
|
|
msgUnread = Unread.HIGHLIGHT;
|
|
|
|
|
|
|
|
|
|
var channel = msg.params[1];
|
|
|
|
|
if (window.Notification && Notification.permission === "granted") {
|
|
|
|
|
var notif = new Notification("Invitation to " + channel, {
|
|
|
|
|
body: msg.prefix.name + " has invited you to " + channel,
|
|
|
|
|
requireInteraction: true,
|
|
|
|
|
actions: [{
|
|
|
|
|
action: "accept",
|
|
|
|
|
title: "Accept",
|
|
|
|
|
}],
|
|
|
|
|
});
|
|
|
|
|
notif.addEventListener("click", (event) => {
|
|
|
|
|
if (event.action === "accept") {
|
|
|
|
|
this.setReceipt(bufName, ReceiptType.READ, msg);
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.open(channel, serverID);
|
2021-06-03 05:04:32 -04:00
|
|
|
|
} else {
|
|
|
|
|
// TODO: scroll to message
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.switchBuffer({ server: serverID, name: bufName });
|
2021-06-03 05:04:32 -04:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-06-24 10:56:28 -04:00
|
|
|
|
|
2021-05-27 17:40:08 -04:00
|
|
|
|
if (!client.isMyNick(msg.prefix.name) && (msg.command != "PART" && msg.comand != "QUIT")) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.createBuffer(serverID, bufName);
|
2020-06-25 12:28:54 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.setReceipt(bufName, ReceiptType.DELIVERED, msg);
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.setBufferState({ server: serverID, name: bufName }, (buf) => {
|
2020-07-15 12:21:09 -04:00
|
|
|
|
// TODO: set unread if scrolled up
|
2020-06-24 10:56:28 -04:00
|
|
|
|
var unread = buf.unread;
|
2020-09-03 05:51:52 -04:00
|
|
|
|
var lastReadReceipt = buf.lastReadReceipt;
|
2021-03-02 15:29:13 -05:00
|
|
|
|
if (this.state.activeBuffer != buf.id) {
|
2020-06-24 11:46:43 -04:00
|
|
|
|
unread = Unread.union(unread, msgUnread);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
} else {
|
|
|
|
|
this.setReceipt(bufName, ReceiptType.READ, msg);
|
2020-09-03 05:51:52 -04:00
|
|
|
|
lastReadReceipt = this.getReceipt(bufName, ReceiptType.READ);
|
2020-06-24 10:56:28 -04:00
|
|
|
|
}
|
2020-06-29 03:06:47 -04:00
|
|
|
|
var messages = insertMessage(buf.messages, msg);
|
2020-09-03 05:51:52 -04:00
|
|
|
|
return { messages, unread, lastReadReceipt };
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-22 12:44:06 -05:00
|
|
|
|
connect(params) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.lastServerID++;
|
|
|
|
|
var serverID = this.lastServerID;
|
2021-01-12 04:35:38 -05:00
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.setState((state) => {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var servers = new Map(state.servers);
|
|
|
|
|
servers.set(serverID, {
|
|
|
|
|
id: serverID,
|
|
|
|
|
status: ServerStatus.CONNECTING,
|
2021-01-22 05:34:04 -05:00
|
|
|
|
isupport: new Map(),
|
2021-01-21 13:01:50 -05:00
|
|
|
|
});
|
2021-06-03 05:46:50 -04:00
|
|
|
|
return { servers };
|
2021-01-21 13:01:50 -05:00
|
|
|
|
});
|
|
|
|
|
this.setState({ connectParams: params });
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
2021-05-31 11:57:17 -04:00
|
|
|
|
var client = new Client(fillConnectParams(params));
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.clients.set(serverID, client);
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2021-01-22 12:29:22 -05:00
|
|
|
|
client.addEventListener("status", () => {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.setServerState(serverID, { status: client.status });
|
2021-01-21 13:01:50 -05:00
|
|
|
|
});
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
2021-01-22 11:36:53 -05:00
|
|
|
|
client.addEventListener("message", (event) => {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.handleMessage(serverID, event.detail.message);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
|
|
|
|
|
2021-01-22 11:36:53 -05:00
|
|
|
|
client.addEventListener("error", (event) => {
|
2021-03-09 15:47:39 -05:00
|
|
|
|
this.setState({ error: event.detail });
|
2020-08-08 00:08:51 -04:00
|
|
|
|
});
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.createBuffer(serverID, SERVER_BUFFER);
|
2021-03-09 03:38:55 -05:00
|
|
|
|
if (!this.state.activeBuffer) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.switchBuffer({ server: serverID, name: SERVER_BUFFER });
|
2021-03-09 03:38:55 -05:00
|
|
|
|
}
|
2021-05-25 06:40:33 -04:00
|
|
|
|
|
|
|
|
|
if (params.autojoin.length > 0) {
|
|
|
|
|
this.switchToChannel = params.autojoin[0];
|
|
|
|
|
}
|
2021-05-27 12:17:41 -04:00
|
|
|
|
|
2021-06-04 11:50:49 -04:00
|
|
|
|
if (this.config.server && typeof this.config.server.ping !== "undefined") {
|
2021-05-28 03:58:06 -04:00
|
|
|
|
client.setPingInterval(this.config.server.ping);
|
2021-05-27 12:17:41 -04:00
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
disconnect(serverID) {
|
|
|
|
|
if (!serverID) {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
serverID = State.getActiveServerID(this.state);
|
2021-01-22 04:38:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(serverID);
|
2021-01-22 11:36:53 -05:00
|
|
|
|
if (client) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.clients.delete(serverID);
|
2021-01-22 12:29:22 -05:00
|
|
|
|
client.disconnect();
|
2021-01-12 04:35:38 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
reconnect(serverID) {
|
|
|
|
|
if (!serverID) {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
serverID = State.getActiveServerID(this.state);
|
2021-01-22 04:38:07 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(serverID);
|
2021-01-22 12:44:06 -05:00
|
|
|
|
if (client) {
|
|
|
|
|
client.reconnect();
|
|
|
|
|
}
|
2021-01-12 04:35:38 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
serverFromBouncerNetwork(bouncerNetworkID) {
|
2021-03-10 05:00:33 -05:00
|
|
|
|
for (var [id, client] of this.clients) {
|
|
|
|
|
if (client.params.bouncerNetwork === bouncerNetworkID) {
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
handleMessage(serverID, msg) {
|
|
|
|
|
var client = this.clients.get(serverID);
|
2021-06-04 12:27:21 -04:00
|
|
|
|
|
|
|
|
|
this.setState((state) => State.handleMessage(state, msg, serverID, client));
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
switch (msg.command) {
|
|
|
|
|
case irc.RPL_WELCOME:
|
|
|
|
|
if (this.state.connectParams.autojoin.length > 0) {
|
2021-01-22 11:36:53 -05:00
|
|
|
|
client.send({
|
2020-06-18 08:23:08 -04:00
|
|
|
|
command: "JOIN",
|
|
|
|
|
params: [this.state.connectParams.autojoin.join(",")],
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-05-18 10:53:52 -04:00
|
|
|
|
|
|
|
|
|
var lastReceipt = this.latestReceipt(ReceiptType.READ);
|
|
|
|
|
if (lastReceipt && lastReceipt.time && client.enabledCaps["draft/chathistory"] && (!client.enabledCaps["soju.im/bouncer-networks"] || client.params.bouncerNetwork)) {
|
|
|
|
|
var now = irc.formatDate(new Date());
|
|
|
|
|
client.fetchHistoryTargets(now, lastReceipt.time).then((targets) => {
|
|
|
|
|
targets.forEach((target) => {
|
|
|
|
|
var from = this.getReceipt(target, ReceiptType.READ);
|
|
|
|
|
if (!from) {
|
|
|
|
|
from = lastReceipt;
|
|
|
|
|
}
|
|
|
|
|
var to = { time: msg.tags.time || irc.formatDate(new Date()) };
|
|
|
|
|
this.fetchBacklog(client, target.name, from, to);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
2021-05-27 09:34:43 -04:00
|
|
|
|
case "MODE":
|
|
|
|
|
var target = msg.params[0];
|
|
|
|
|
if (this.isChannel(target)) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, target, msg);
|
2021-05-27 09:34:43 -04:00
|
|
|
|
}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.handleMode(serverID, msg);
|
2021-05-27 09:34:43 -04:00
|
|
|
|
break;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
case "NOTICE":
|
|
|
|
|
case "PRIVMSG":
|
|
|
|
|
var target = msg.params[0];
|
2021-05-28 04:24:40 -04:00
|
|
|
|
if (client.isMyNick(target)) {
|
|
|
|
|
if (client.cm(msg.prefix.name) === client.cm(client.serverPrefix.name)) {
|
2021-03-08 08:27:05 -05:00
|
|
|
|
target = SERVER_BUFFER;
|
|
|
|
|
} else {
|
|
|
|
|
target = msg.prefix.name;
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
2021-06-03 07:31:43 -04:00
|
|
|
|
|
|
|
|
|
var allowedPrefixes = client.isupport.get("STATUSMSG");
|
|
|
|
|
if (allowedPrefixes) {
|
|
|
|
|
var parts = irc.parseTargetPrefix(target, allowedPrefixes);
|
|
|
|
|
if (this.isChannel(parts.name)) {
|
|
|
|
|
target = parts.name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, target, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
|
|
|
|
case "JOIN":
|
|
|
|
|
var channel = msg.params[0];
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.createBuffer(serverID, channel);
|
|
|
|
|
this.setBufferState({ server: serverID, name: channel }, (buf) => {
|
2021-05-27 09:17:18 -04:00
|
|
|
|
var members = new irc.CaseMapMap(buf.members);
|
2021-05-31 10:08:33 -04:00
|
|
|
|
members.set(msg.prefix.name, "");
|
2020-06-24 10:02:56 -04:00
|
|
|
|
return { members };
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
2021-05-27 17:40:08 -04:00
|
|
|
|
if (!client.isMyNick(msg.prefix.name)) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, channel, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
2021-05-25 06:40:33 -04:00
|
|
|
|
if (channel == this.switchToChannel) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.switchBuffer({ server: serverID, name: channel });
|
2021-05-25 06:40:33 -04:00
|
|
|
|
this.switchToChannel = null;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case "PART":
|
|
|
|
|
var channel = msg.params[0];
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.setBufferState({ server: serverID, name: channel }, (buf) => {
|
2021-05-27 09:17:18 -04:00
|
|
|
|
var members = new irc.CaseMapMap(buf.members);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
members.delete(msg.prefix.name);
|
2020-06-24 10:02:56 -04:00
|
|
|
|
return { members };
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, channel, msg);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2021-05-27 17:40:08 -04:00
|
|
|
|
if (client.isMyNick(msg.prefix.name)) {
|
2021-05-26 19:28:24 -04:00
|
|
|
|
this.receipts.delete(channel);
|
|
|
|
|
this.saveReceipts();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case "KICK":
|
|
|
|
|
var channel = msg.params[0];
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, channel, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
2020-07-08 12:39:24 -04:00
|
|
|
|
case "QUIT":
|
|
|
|
|
var affectedBuffers = [];
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var buffers = new Map(state.buffers);
|
|
|
|
|
state.buffers.forEach((buf) => {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
if (buf.server != serverID) {
|
2021-05-27 09:19:28 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
2021-05-27 17:40:08 -04:00
|
|
|
|
if (!buf.members.has(msg.prefix.name) && client.cm(buf.name) !== client.cm(msg.prefix.name)) {
|
2020-07-08 12:39:24 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
2021-05-27 09:17:18 -04:00
|
|
|
|
var members = new irc.CaseMapMap(buf.members);
|
2020-07-08 12:39:24 -04:00
|
|
|
|
members.delete(msg.prefix.name);
|
2021-05-27 17:40:08 -04:00
|
|
|
|
var offline = client.cm(buf.name) === client.cm(msg.prefix.name);
|
2021-01-21 16:15:33 -05:00
|
|
|
|
buffers.set(buf.id, { ...buf, members, offline });
|
2020-07-08 12:39:24 -04:00
|
|
|
|
affectedBuffers.push(buf.name);
|
|
|
|
|
});
|
|
|
|
|
return { buffers };
|
|
|
|
|
});
|
2021-06-03 05:46:50 -04:00
|
|
|
|
affectedBuffers.forEach((name) => this.addMessage(serverID, name, msg));
|
2020-07-08 12:39:24 -04:00
|
|
|
|
break;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
case "NICK":
|
|
|
|
|
var newNick = msg.params[0];
|
|
|
|
|
|
|
|
|
|
var affectedBuffers = [];
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var buffers = new Map(state.buffers);
|
|
|
|
|
state.buffers.forEach((buf) => {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
if (buf.server != serverID) {
|
2021-05-27 09:19:28 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
if (!buf.members.has(msg.prefix.name)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-05-27 09:17:18 -04:00
|
|
|
|
var members = new irc.CaseMapMap(buf.members);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
members.set(newNick, members.get(msg.prefix.name));
|
|
|
|
|
members.delete(msg.prefix.name);
|
2021-01-21 16:15:33 -05:00
|
|
|
|
buffers.set(buf.id, { ...buf, members });
|
2020-06-18 08:23:08 -04:00
|
|
|
|
affectedBuffers.push(buf.name);
|
|
|
|
|
});
|
|
|
|
|
return { buffers };
|
|
|
|
|
});
|
2021-06-03 05:46:50 -04:00
|
|
|
|
affectedBuffers.forEach((name) => this.addMessage(serverID, name, msg));
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
|
|
|
|
case "TOPIC":
|
|
|
|
|
var channel = msg.params[0];
|
|
|
|
|
var topic = msg.params[1];
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.setBufferState({ server: serverID, name: channel }, { topic });
|
|
|
|
|
this.addMessage(serverID, channel, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
2021-06-03 05:04:32 -04:00
|
|
|
|
case "INVITE":
|
|
|
|
|
var channel = msg.params[1];
|
|
|
|
|
|
|
|
|
|
// TODO: find a more reliable way to do this
|
|
|
|
|
var bufName = channel;
|
2021-06-04 12:03:03 -04:00
|
|
|
|
if (!State.getBuffer(this.state, { server: serverID, name: channel })) {
|
2021-06-03 05:04:32 -04:00
|
|
|
|
bufName = SERVER_BUFFER;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, bufName, msg);
|
2021-06-03 05:04:32 -04:00
|
|
|
|
break;
|
2021-03-10 05:00:33 -05:00
|
|
|
|
case "BOUNCER":
|
|
|
|
|
if (msg.params[0] !== "NETWORK") {
|
|
|
|
|
break; // We're only interested in network updates
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-25 11:11:06 -04:00
|
|
|
|
if (client.isupport.has("BOUNCER_NETID")) {
|
|
|
|
|
// This cn happen if the user has specified a network to bind
|
|
|
|
|
// to via other means, e.g. "<username>/<network>".
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-10 05:00:33 -05:00
|
|
|
|
var id = msg.params[1];
|
|
|
|
|
var attrs = null;
|
|
|
|
|
if (msg.params[2] !== "*") {
|
|
|
|
|
attrs = irc.parseTags(msg.params[2]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isNew = false;
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var bouncerNetworks = new Map(state.bouncerNetworks);
|
|
|
|
|
if (!attrs) {
|
|
|
|
|
bouncerNetworks.delete(id);
|
|
|
|
|
} else {
|
|
|
|
|
var prev = bouncerNetworks.get(id);
|
|
|
|
|
isNew = prev === undefined;
|
|
|
|
|
attrs = { ...prev, ...attrs };
|
|
|
|
|
bouncerNetworks.set(id, attrs);
|
|
|
|
|
}
|
|
|
|
|
return { bouncerNetworks };
|
|
|
|
|
}, () => {
|
|
|
|
|
if (!attrs) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var serverID = this.serverFromBouncerNetwork(id);
|
|
|
|
|
if (serverID) {
|
|
|
|
|
this.close({ server: serverID, name: SERVER_BUFFER });
|
2021-03-10 05:00:33 -05:00
|
|
|
|
}
|
|
|
|
|
} else if (isNew) {
|
|
|
|
|
this.connect({
|
|
|
|
|
...client.params,
|
|
|
|
|
bouncerNetwork: id,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
break;
|
2021-06-03 04:11:48 -04:00
|
|
|
|
case irc.RPL_BANLIST:
|
|
|
|
|
case irc.RPL_ENDOFBANLIST:
|
|
|
|
|
var channel = msg.params[1];
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, channel, msg);
|
2021-06-03 04:11:48 -04:00
|
|
|
|
break;
|
2021-06-04 12:27:21 -04:00
|
|
|
|
case irc.RPL_MYINFO:
|
|
|
|
|
case irc.RPL_ISUPPORT:
|
|
|
|
|
case irc.RPL_NOTOPIC:
|
|
|
|
|
case irc.RPL_TOPIC:
|
|
|
|
|
case irc.RPL_TOPICWHOTIME:
|
|
|
|
|
case irc.RPL_NAMREPLY:
|
|
|
|
|
case irc.RPL_ENDOFNAMES:
|
|
|
|
|
case "AWAY":
|
|
|
|
|
case "SETNAME":
|
2020-06-25 06:16:42 -04:00
|
|
|
|
case "CAP":
|
|
|
|
|
case "AUTHENTICATE":
|
2020-07-01 06:12:56 -04:00
|
|
|
|
case "PING":
|
2021-05-27 12:17:41 -04:00
|
|
|
|
case "PONG":
|
2020-07-15 12:21:09 -04:00
|
|
|
|
case "BATCH":
|
2021-05-27 15:58:37 -04:00
|
|
|
|
case "TAGMSG":
|
2021-05-18 10:53:52 -04:00
|
|
|
|
case "CHATHISTORY":
|
2020-06-25 06:16:42 -04:00
|
|
|
|
// Ignore these
|
|
|
|
|
break;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
default:
|
2021-05-27 05:30:23 -04:00
|
|
|
|
if (irc.isError(msg.command) && msg.command != irc.ERR_NOMOTD) {
|
|
|
|
|
var description = msg.params[msg.params.length - 1];
|
|
|
|
|
this.setState({ error: description });
|
|
|
|
|
}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, SERVER_BUFFER, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleConnectSubmit(connectParams) {
|
2020-08-10 08:57:54 -04:00
|
|
|
|
this.setState({ error: null });
|
|
|
|
|
|
2021-05-26 12:43:11 -04:00
|
|
|
|
if (connectParams.autoconnect) {
|
|
|
|
|
store.autoconnect.put(connectParams);
|
|
|
|
|
} else {
|
|
|
|
|
store.autoconnect.put(null);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-22 12:44:06 -05:00
|
|
|
|
this.connect(connectParams);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-31 22:39:35 -04:00
|
|
|
|
handleChannelClick(channel) {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
var serverID = State.getActiveServerID(this.state);
|
|
|
|
|
var buf = State.getBuffer(this.state, { server: serverID, name: channel });
|
2021-05-31 22:39:35 -04:00
|
|
|
|
if (buf) {
|
|
|
|
|
this.switchBuffer(buf.id);
|
|
|
|
|
} else {
|
|
|
|
|
this.open(channel);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-25 12:45:41 -04:00
|
|
|
|
handleNickClick(nick) {
|
|
|
|
|
this.open(nick);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-24 09:52:33 -04:00
|
|
|
|
isChannel(name) {
|
|
|
|
|
// TODO: use the ISUPPORT token if available
|
|
|
|
|
return irc.STD_CHANNEL_TYPES.indexOf(name[0]) >= 0;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-18 10:53:52 -04:00
|
|
|
|
fetchBacklog(client, target, after, before) {
|
|
|
|
|
client.fetchHistoryBetween(target, after, before, CHATHISTORY_MAX_SIZE).catch((err) => {
|
|
|
|
|
this.setState({ error: "Failed to fetch history for '" + taregt + "': " + err });
|
|
|
|
|
this.receipts.delete(channel);
|
|
|
|
|
this.saveReceipts();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
open(target, serverID) {
|
|
|
|
|
if (!serverID) {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
serverID = State.getActiveServerID(this.state);
|
2021-06-03 05:04:32 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(serverID);
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2020-06-25 12:45:41 -04:00
|
|
|
|
if (this.isChannel(target)) {
|
2021-05-27 13:10:42 -04:00
|
|
|
|
this.switchToChannel = target;
|
2021-01-22 11:36:53 -05:00
|
|
|
|
client.send({ command: "JOIN", params: [target] });
|
2020-06-26 06:00:10 -04:00
|
|
|
|
} else {
|
2021-05-31 11:11:42 -04:00
|
|
|
|
client.who(target);
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.createBuffer(serverID, target);
|
|
|
|
|
this.switchBuffer({ server: serverID, name: target });
|
2020-06-25 12:45:41 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-22 04:26:53 -05:00
|
|
|
|
close(id) {
|
2021-06-04 12:03:03 -04:00
|
|
|
|
var buf = State.getBuffer(this.state, id);
|
2021-01-22 04:26:53 -05:00
|
|
|
|
if (!buf) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (buf.type) {
|
|
|
|
|
case BufferType.SERVER:
|
2021-03-10 04:59:39 -05:00
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var buffers = new Map(state.buffers);
|
|
|
|
|
for (var [id, b] of state.buffers) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
if (b.server === buf.server) {
|
2021-03-10 04:59:39 -05:00
|
|
|
|
buffers.delete(id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var activeBuffer = state.activeBuffer;
|
2021-06-03 05:46:50 -04:00
|
|
|
|
if (activeBuffer && state.buffers.get(activeBuffer).server === buf.server) {
|
2021-03-10 04:59:39 -05:00
|
|
|
|
if (buffers.size > 0) {
|
|
|
|
|
activeBuffer = buffers.keys().next().value;
|
|
|
|
|
} else {
|
|
|
|
|
activeBuffer = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { buffers, activeBuffer };
|
2021-01-11 12:12:28 -05:00
|
|
|
|
});
|
2021-03-10 04:59:39 -05:00
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(buf.server);
|
2021-03-10 04:59:39 -05:00
|
|
|
|
var disconnectAll = client && !client.params.bouncerNetwork && client.enabledCaps["soju.im/bouncer-networks"];
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.disconnect(buf.server);
|
2021-03-10 04:59:39 -05:00
|
|
|
|
|
2021-01-22 04:41:28 -05:00
|
|
|
|
this.setState((state) => {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var servers = new Map(state.servers);
|
|
|
|
|
servers.delete(buf.server);
|
|
|
|
|
return { servers };
|
2021-01-22 04:41:28 -05:00
|
|
|
|
});
|
2021-03-10 04:59:39 -05:00
|
|
|
|
|
|
|
|
|
if (disconnectAll) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
for (var serverID of this.clients.keys()) {
|
|
|
|
|
this.close({ server: serverID, name: SERVER_BUFFER });
|
2021-03-10 04:59:39 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
// TODO: only clear local storage if this server is stored there
|
|
|
|
|
if (buf.server == 1) {
|
2021-05-26 12:43:11 -04:00
|
|
|
|
store.autoconnect.put(null);
|
2021-05-25 06:57:40 -04:00
|
|
|
|
}
|
2021-01-22 04:26:53 -05:00
|
|
|
|
break;
|
|
|
|
|
case BufferType.CHANNEL:
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(buf.server);
|
2021-01-22 11:36:53 -05:00
|
|
|
|
client.send({ command: "PART", params: [buf.name] });
|
2021-01-22 04:26:53 -05:00
|
|
|
|
// fallthrough
|
|
|
|
|
case BufferType.NICK:
|
|
|
|
|
this.switchBuffer({ name: SERVER_BUFFER });
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var buffers = new Map(state.buffers);
|
2021-03-03 03:59:40 -05:00
|
|
|
|
buffers.delete(buf.id);
|
2021-01-22 04:26:53 -05:00
|
|
|
|
return { buffers };
|
|
|
|
|
});
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2021-01-22 11:36:53 -05:00
|
|
|
|
this.receipts.delete(buf.name);
|
2021-01-22 04:26:53 -05:00
|
|
|
|
this.saveReceipts();
|
|
|
|
|
break;
|
2020-06-25 12:28:54 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
executeCommand(s) {
|
|
|
|
|
var parts = s.split(" ");
|
2020-07-13 11:22:24 -04:00
|
|
|
|
var name = parts[0].toLowerCase().slice(1);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
var args = parts.slice(1);
|
2020-07-13 11:22:24 -04:00
|
|
|
|
|
|
|
|
|
var cmd = commands[name];
|
|
|
|
|
if (!cmd) {
|
2021-05-27 06:54:30 -04:00
|
|
|
|
this.setState({ error: `Unknown command "${name}" (run "/help" to get a command list)` });
|
2020-07-13 11:22:24 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2021-03-08 11:25:00 -05:00
|
|
|
|
cmd.execute(this, args);
|
2020-08-08 00:08:51 -04:00
|
|
|
|
} catch (error) {
|
2021-05-27 06:54:30 -04:00
|
|
|
|
console.error(`Failed to execute command "${name}":`, error);
|
2021-05-27 05:26:42 -04:00
|
|
|
|
this.setState({ error: error.message });
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-28 03:29:39 -04:00
|
|
|
|
privmsg(target, text) {
|
|
|
|
|
if (target == SERVER_BUFFER) {
|
2020-08-08 00:08:51 -04:00
|
|
|
|
this.setState({ error: "Cannot send message in server buffer" });
|
2020-06-28 03:29:39 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-04 12:03:03 -04:00
|
|
|
|
var serverID = State.getActiveServerID(this.state);
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(serverID);
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2020-06-28 03:29:39 -04:00
|
|
|
|
var msg = { command: "PRIVMSG", params: [target, text] };
|
2021-01-22 11:36:53 -05:00
|
|
|
|
client.send(msg);
|
2020-06-28 03:29:39 -04:00
|
|
|
|
|
2021-01-22 11:36:53 -05:00
|
|
|
|
if (!client.enabledCaps["echo-message"]) {
|
|
|
|
|
msg.prefix = { name: client.nick };
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.addMessage(serverID, target, msg);
|
2020-06-28 03:29:39 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
handleComposerSubmit(text) {
|
|
|
|
|
if (!text) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (text.startsWith("//")) {
|
|
|
|
|
text = text.slice(1);
|
|
|
|
|
} else if (text.startsWith("/")) {
|
|
|
|
|
this.executeCommand(text);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var buf = this.state.buffers.get(this.state.activeBuffer);
|
|
|
|
|
if (!buf) {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.privmsg(buf.name, text);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-22 15:43:58 -05:00
|
|
|
|
handleBufferListClick(id) {
|
|
|
|
|
this.switchBuffer(id);
|
2021-05-27 10:35:33 -04:00
|
|
|
|
this.closeBufferList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleBufferList() {
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var openPanels = {
|
|
|
|
|
...state.openPanels,
|
|
|
|
|
bufferList: !state.openPanels.bufferList,
|
|
|
|
|
};
|
|
|
|
|
return { openPanels };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleMemberList() {
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var openPanels = {
|
|
|
|
|
...state.openPanels,
|
|
|
|
|
memberList: !state.openPanels.memberList,
|
|
|
|
|
};
|
|
|
|
|
return { openPanels };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeBufferList() {
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var openPanels = {
|
|
|
|
|
...state.openPanels,
|
|
|
|
|
bufferList: false,
|
|
|
|
|
};
|
|
|
|
|
return { openPanels };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeMemberList() {
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var openPanels = {
|
|
|
|
|
...state.openPanels,
|
|
|
|
|
memberList: false,
|
|
|
|
|
};
|
|
|
|
|
return { openPanels };
|
|
|
|
|
});
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
handleJoinClick(serverID) {
|
|
|
|
|
this.setState({ dialog: "join", joinDialog: { server: serverID } });
|
2021-03-08 10:23:16 -05:00
|
|
|
|
}
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2021-03-08 10:23:16 -05:00
|
|
|
|
handleJoinSubmit(data) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(this.state.joinDialog.server);
|
2021-03-08 10:23:16 -05:00
|
|
|
|
|
2021-05-27 13:10:42 -04:00
|
|
|
|
this.switchToChannel = data.channel;
|
2021-03-08 10:23:16 -05:00
|
|
|
|
client.send({ command: "JOIN", params: [data.channel] });
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2021-03-08 10:23:16 -05:00
|
|
|
|
this.setState({ dialog: null, joinDialog: null });
|
2020-06-29 04:12:46 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-29 06:36:17 -04:00
|
|
|
|
autocomplete(prefix) {
|
2020-07-13 11:28:49 -04:00
|
|
|
|
function fromList(l, prefix) {
|
|
|
|
|
prefix = prefix.toLowerCase();
|
|
|
|
|
var repl = null;
|
|
|
|
|
for (var item of l) {
|
|
|
|
|
if (item.toLowerCase().startsWith(prefix)) {
|
|
|
|
|
if (repl) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
repl = item;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return repl;
|
2020-06-29 06:36:17 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-13 11:28:49 -04:00
|
|
|
|
if (prefix.startsWith("/")) {
|
|
|
|
|
var repl = fromList(Object.keys(commands), prefix.slice(1));
|
|
|
|
|
if (repl) {
|
|
|
|
|
repl = "/" + repl;
|
2020-06-29 06:36:17 -04:00
|
|
|
|
}
|
2020-07-13 11:28:49 -04:00
|
|
|
|
return repl;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var buf = this.state.buffers.get(this.state.activeBuffer);
|
|
|
|
|
if (!buf || !buf.members) {
|
2020-07-13 11:28:49 -04:00
|
|
|
|
return null;
|
2020-06-29 06:36:17 -04:00
|
|
|
|
}
|
2020-07-13 11:28:49 -04:00
|
|
|
|
return fromList(buf.members.keys(), prefix);
|
2020-06-29 06:36:17 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 11:05:48 -05:00
|
|
|
|
openHelp() {
|
|
|
|
|
this.setState({ dialog: "help" });
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-29 03:06:47 -04:00
|
|
|
|
handleBufferScrollTop() {
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var buf = this.state.buffers.get(this.state.activeBuffer);
|
|
|
|
|
if (!buf || buf.type == BufferType.SERVER) {
|
2020-06-29 03:06:47 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var client = this.clients.get(buf.server);
|
2021-01-22 11:36:53 -05:00
|
|
|
|
|
2021-05-25 08:28:48 -04:00
|
|
|
|
if (!client || !client.enabledCaps["draft/chathistory"] || !client.enabledCaps["server-time"]) {
|
2020-06-29 03:06:47 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
2021-01-22 09:49:22 -05:00
|
|
|
|
if (this.endOfHistory.get(buf.id)) {
|
2020-06-29 03:06:47 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var before;
|
|
|
|
|
if (buf.messages.length > 0) {
|
|
|
|
|
before = buf.messages[0].tags["time"];
|
|
|
|
|
} else {
|
|
|
|
|
before = irc.formatDate(new Date());
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
// Avoids sending multiple CHATHISTORY commands in parallel
|
2021-01-22 09:49:22 -05:00
|
|
|
|
this.endOfHistory.set(buf.id, true);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2021-05-11 10:10:50 -04:00
|
|
|
|
client.fetchHistoryBefore(buf.name, before, 100).then((result) => {
|
2021-01-23 06:16:57 -05:00
|
|
|
|
this.endOfHistory.set(buf.id, !result.more);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
});
|
2020-06-29 03:06:47 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 10:23:16 -05:00
|
|
|
|
handleDialogDismiss() {
|
|
|
|
|
this.setState({ dialog: null });
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 12:15:04 -05:00
|
|
|
|
handleAddNetworkClick() {
|
2021-03-09 13:10:22 -05:00
|
|
|
|
this.setState({ dialog: "network", networkDialog: null });
|
2021-03-08 12:15:04 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
handleManageNetworkClick(serverID) {
|
|
|
|
|
var server = this.state.servers.get(serverID);
|
|
|
|
|
var bouncerNetID = server.isupport.get("BOUNCER_NETID");
|
2021-03-09 13:10:22 -05:00
|
|
|
|
var bouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID);
|
|
|
|
|
this.setState({
|
|
|
|
|
dialog: "network",
|
|
|
|
|
networkDialog: {
|
|
|
|
|
id: bouncerNetID,
|
|
|
|
|
params: bouncerNetwork,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleNetworkSubmit(attrs) {
|
2021-03-08 12:15:04 -05:00
|
|
|
|
var client = this.clients.values().next().value;
|
2021-03-09 13:10:22 -05:00
|
|
|
|
|
|
|
|
|
if (this.state.networkDialog && this.state.networkDialog.id) {
|
|
|
|
|
if (Object.keys(attrs).length == 0) {
|
|
|
|
|
this.setState({ dialog: null });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client.send({
|
|
|
|
|
command: "BOUNCER",
|
|
|
|
|
params: ["CHANGENETWORK", this.state.networkDialog.id, irc.formatTags(attrs)],
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
attrs = { ...attrs, tls: "1" };
|
|
|
|
|
client.send({
|
|
|
|
|
command: "BOUNCER",
|
|
|
|
|
params: ["ADDNETWORK", irc.formatTags(attrs)],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setState({ dialog: null, networkDialog: null });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleNetworkRemove() {
|
|
|
|
|
var client = this.clients.values().next().value;
|
|
|
|
|
|
2021-03-08 12:15:04 -05:00
|
|
|
|
client.send({
|
|
|
|
|
command: "BOUNCER",
|
2021-03-09 13:10:22 -05:00
|
|
|
|
params: ["DELNETWORK", this.state.networkDialog.id],
|
2021-03-08 12:15:04 -05:00
|
|
|
|
});
|
2021-03-09 13:10:22 -05:00
|
|
|
|
|
|
|
|
|
this.setState({ dialog: null, networkDialog: null });
|
2021-03-08 12:15:04 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
handleMode(serverID, msg) {
|
|
|
|
|
var client = this.clients.get(serverID);
|
2021-05-28 11:45:27 -04:00
|
|
|
|
var chanmodes = client.isupport.get("CHANMODES") || irc.STD_CHANMODES;
|
|
|
|
|
var prefix = client.isupport.get("PREFIX") || "";
|
|
|
|
|
|
2021-06-03 07:18:28 -04:00
|
|
|
|
var prefixByMode = new Map(irc.parseMembershipModes(prefix).map((membership) => {
|
2021-05-28 11:45:27 -04:00
|
|
|
|
return [membership.mode, membership.prefix];
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
var typeByMode = new Map();
|
|
|
|
|
var [a, b, c, d] = chanmodes.split(",");
|
|
|
|
|
Array.from(a).forEach((mode) => typeByMode.set(mode, "A"));
|
|
|
|
|
Array.from(b).forEach((mode) => typeByMode.set(mode, "B"));
|
|
|
|
|
Array.from(c).forEach((mode) => typeByMode.set(mode, "C"));
|
|
|
|
|
Array.from(d).forEach((mode) => typeByMode.set(mode, "D"));
|
|
|
|
|
prefixByMode.forEach((prefix, mode) => typeByMode.set(mode, "B"));
|
|
|
|
|
|
|
|
|
|
var channel = msg.params[0];
|
|
|
|
|
var change = msg.params[1];
|
|
|
|
|
var args = msg.params.slice(2);
|
|
|
|
|
|
|
|
|
|
var plusMinus = null;
|
|
|
|
|
var j = 0;
|
|
|
|
|
for (var i = 0; i < change.length; i++) {
|
|
|
|
|
if (change[i] === "+" || change[i] === "-") {
|
|
|
|
|
plusMinus = change[i];
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!plusMinus) {
|
|
|
|
|
throw new Error("malformed mode string: missing plus/minus");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var mode = change[i];
|
|
|
|
|
var add = plusMinus === "+";
|
|
|
|
|
|
|
|
|
|
var modeType = typeByMode.get(mode);
|
|
|
|
|
if (!modeType) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var arg = null;
|
|
|
|
|
if (modeType === "A" || modeType === "B" || (modeType === "C" && add)) {
|
|
|
|
|
arg = args[j];
|
|
|
|
|
j++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (prefixByMode.has(mode)) {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.handlePrefixChange(serverID, channel, arg, prefixByMode.get(mode), add);
|
2021-05-28 11:45:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// XXX: If we eventually want to handle any mode changes with
|
|
|
|
|
// some special logic, this would be the place to. Not sure
|
|
|
|
|
// what we'd want to do in that regard, though.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
handlePrefixChange(serverID, channel, nick, letter, add) {
|
|
|
|
|
var client = this.clients.get(serverID);
|
2021-05-28 11:45:27 -04:00
|
|
|
|
var prefix = client.isupport.get("PREFIX") || "";
|
|
|
|
|
|
2021-06-03 07:18:28 -04:00
|
|
|
|
var prefixPrivs = new Map(irc.parseMembershipModes(prefix).map((membership, i) => {
|
2021-05-28 11:45:27 -04:00
|
|
|
|
return [membership.prefix, i];
|
|
|
|
|
}));
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
this.setBufferState({ server: serverID, name: channel }, (buf) => {
|
2021-05-28 11:45:27 -04:00
|
|
|
|
var members = new irc.CaseMapMap(buf.members);
|
|
|
|
|
var membership = members.get(nick);
|
|
|
|
|
if (add) {
|
|
|
|
|
var i = membership.indexOf(letter);
|
|
|
|
|
if (i < 0) {
|
|
|
|
|
membership += letter;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
membership = membership.replace(letter, "");
|
|
|
|
|
}
|
|
|
|
|
membership = Array.from(membership).sort((a, b) => {
|
|
|
|
|
return prefixPrivs.get(a) - prefixPrivs.get(b);
|
|
|
|
|
}).join("");
|
|
|
|
|
members.set(nick, membership);
|
|
|
|
|
return { members };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
componentDidMount() {
|
2020-07-23 03:58:05 -04:00
|
|
|
|
setupKeybindings(this);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var activeBuffer = null, activeServer = null, activeBouncerNetwork = null;
|
2021-03-08 12:15:04 -05:00
|
|
|
|
var isBouncer = false;
|
2021-05-10 09:09:49 -04:00
|
|
|
|
if (this.state.buffers.get(this.state.activeBuffer)) {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
activeBuffer = this.state.buffers.get(this.state.activeBuffer);
|
2021-06-03 05:46:50 -04:00
|
|
|
|
activeServer = this.state.servers.get(activeBuffer.server);
|
2021-03-08 12:15:04 -05:00
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var activeClient = this.clients.get(activeBuffer.server);
|
2021-03-08 12:15:04 -05:00
|
|
|
|
isBouncer = activeClient && activeClient.enabledCaps["soju.im/bouncer-networks"];
|
2021-03-10 05:48:58 -05:00
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
var bouncerNetID = activeServer.isupport.get("BOUNCER_NETID");
|
2021-03-10 05:48:58 -05:00
|
|
|
|
if (bouncerNetID) {
|
|
|
|
|
activeBouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID);
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 05:46:50 -04:00
|
|
|
|
if (!activeServer || (activeServer.status !== ServerStatus.REGISTERED && !activeBuffer)) {
|
2021-05-25 06:33:22 -04:00
|
|
|
|
// TODO: using key=connectParams trashes the ConnectForm state on update
|
2021-01-21 13:01:50 -05:00
|
|
|
|
return html`
|
|
|
|
|
<section id="connect">
|
2021-03-09 12:11:59 -05:00
|
|
|
|
<${ConnectForm}
|
|
|
|
|
error=${this.state.error}
|
|
|
|
|
params=${this.state.connectParams}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
disabled=${activeServer}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
onSubmit=${this.handleConnectSubmit}
|
2021-05-25 06:33:22 -04:00
|
|
|
|
key=${this.state.connectParams}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
/>
|
2021-01-21 13:01:50 -05:00
|
|
|
|
</section>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-26 09:16:07 -04:00
|
|
|
|
var bufferHeader = null;
|
2020-06-26 06:08:14 -04:00
|
|
|
|
if (activeBuffer) {
|
2020-06-26 09:16:07 -04:00
|
|
|
|
bufferHeader = html`
|
|
|
|
|
<section id="buffer-header">
|
2021-03-09 12:11:59 -05:00
|
|
|
|
<${BufferHeader}
|
|
|
|
|
buffer=${activeBuffer}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
server=${activeServer}
|
2021-03-08 12:15:04 -05:00
|
|
|
|
isBouncer=${isBouncer}
|
2021-03-10 05:48:58 -05:00
|
|
|
|
bouncerNetwork=${activeBouncerNetwork}
|
2021-05-31 22:39:35 -04:00
|
|
|
|
onChannelClick=${this.handleChannelClick}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
onClose=${() => this.close(activeBuffer)}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
onJoin=${() => this.handleJoinClick(activeBuffer.server)}
|
2021-03-08 12:15:04 -05:00
|
|
|
|
onAddNetwork=${this.handleAddNetworkClick}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
onManageNetwork=${() => this.handleManageNetworkClick(activeBuffer.server)}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
/>
|
2020-06-25 12:28:54 -04:00
|
|
|
|
</section>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-26 08:32:56 -04:00
|
|
|
|
var memberList = null;
|
|
|
|
|
if (activeBuffer && activeBuffer.type == BufferType.CHANNEL) {
|
|
|
|
|
memberList = html`
|
2021-05-27 10:35:33 -04:00
|
|
|
|
<section
|
|
|
|
|
id="member-list"
|
|
|
|
|
class=${this.state.openPanels.memberList ? "expand" : ""}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
class="expander"
|
|
|
|
|
onClick=${this.toggleMemberList}
|
|
|
|
|
>
|
|
|
|
|
<span></span>
|
|
|
|
|
<span></span>
|
|
|
|
|
</button>
|
|
|
|
|
<section>
|
|
|
|
|
<section id="member-list-header">
|
|
|
|
|
${activeBuffer.members.size} users
|
|
|
|
|
</section>
|
|
|
|
|
<${MemberList}
|
|
|
|
|
members=${activeBuffer.members}
|
|
|
|
|
onNickClick=${this.handleNickClick}
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
2020-06-26 08:32:56 -04:00
|
|
|
|
</section>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 10:23:16 -05:00
|
|
|
|
var dialog = null;
|
|
|
|
|
switch (this.state.dialog) {
|
2021-03-09 13:10:22 -05:00
|
|
|
|
case "network":
|
|
|
|
|
var title = this.state.networkDialog ? "Edit network" : "Add network";
|
2021-03-08 12:15:04 -05:00
|
|
|
|
dialog = html`
|
2021-03-09 13:10:22 -05:00
|
|
|
|
<${Dialog} title=${title} onDismiss=${this.handleDialogDismiss}>
|
|
|
|
|
<${NetworkForm}
|
|
|
|
|
onSubmit=${this.handleNetworkSubmit}
|
|
|
|
|
onRemove=${this.handleNetworkRemove}
|
|
|
|
|
params=${this.state.networkDialog ? this.state.networkDialog.params : null}
|
|
|
|
|
/>
|
2021-03-08 12:15:04 -05:00
|
|
|
|
</>
|
|
|
|
|
`;
|
|
|
|
|
break;
|
2021-03-08 11:05:48 -05:00
|
|
|
|
case "help":
|
|
|
|
|
dialog = html`
|
|
|
|
|
<${Dialog} title="Help" onDismiss=${this.handleDialogDismiss}>
|
|
|
|
|
<${Help}/>
|
|
|
|
|
</>
|
|
|
|
|
`;
|
|
|
|
|
break;
|
2021-03-08 10:23:16 -05:00
|
|
|
|
case "join":
|
|
|
|
|
dialog = html`
|
|
|
|
|
<${Dialog} title="Join channel" onDismiss=${this.handleDialogDismiss}>
|
2021-03-09 07:25:31 -05:00
|
|
|
|
<${JoinForm} onSubmit=${this.handleJoinSubmit}/>
|
2021-03-08 10:23:16 -05:00
|
|
|
|
</>
|
|
|
|
|
`;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 09:05:43 -05:00
|
|
|
|
var error = null;
|
|
|
|
|
if (this.state.error) {
|
|
|
|
|
error = html`
|
2021-03-09 12:11:59 -05:00
|
|
|
|
<p id="error-msg">
|
|
|
|
|
${this.state.error}
|
2021-03-09 15:47:39 -05:00
|
|
|
|
${" "}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
<a href="#" onClick=${this.dismissError}>×</a>
|
|
|
|
|
</p>
|
2021-03-08 09:05:43 -05:00
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-31 06:04:54 -04:00
|
|
|
|
var composerReadOnly = false;
|
|
|
|
|
if (activeBuffer && activeBuffer.type === BufferType.SERVER) {
|
|
|
|
|
composerReadOnly = true;
|
|
|
|
|
}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
if (activeServer && activeServer.status !== ServerStatus.REGISTERED) {
|
2021-05-31 06:04:54 -04:00
|
|
|
|
composerReadOnly = true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
return html`
|
2021-05-27 10:35:33 -04:00
|
|
|
|
<section
|
|
|
|
|
id="buffer-list"
|
|
|
|
|
class=${this.state.openPanels.bufferList ? "expand" : ""}
|
|
|
|
|
>
|
2021-03-09 12:11:59 -05:00
|
|
|
|
<${BufferList}
|
|
|
|
|
buffers=${this.state.buffers}
|
2021-06-03 05:46:50 -04:00
|
|
|
|
servers=${this.state.servers}
|
2021-01-22 15:01:03 -05:00
|
|
|
|
bouncerNetworks=${this.state.bouncerNetworks}
|
2021-03-08 12:15:04 -05:00
|
|
|
|
isBouncer=${isBouncer}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
activeBuffer=${this.state.activeBuffer}
|
|
|
|
|
onBufferClick=${this.handleBufferListClick}
|
|
|
|
|
/>
|
2021-05-27 10:35:33 -04:00
|
|
|
|
<button
|
|
|
|
|
class="expander"
|
|
|
|
|
onClick=${this.toggleBufferList}
|
|
|
|
|
>
|
|
|
|
|
<span></span>
|
|
|
|
|
<span></span>
|
|
|
|
|
</button>
|
2020-06-18 08:23:08 -04:00
|
|
|
|
</section>
|
2020-06-26 09:16:07 -04:00
|
|
|
|
${bufferHeader}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
<${ScrollManager}
|
|
|
|
|
target=${this.buffer}
|
|
|
|
|
stickTo=".logline"
|
|
|
|
|
scrollKey=${this.state.activeBuffer}
|
|
|
|
|
onScrollTop=${this.handleBufferScrollTop}
|
|
|
|
|
>
|
2020-06-25 06:03:05 -04:00
|
|
|
|
<section id="buffer" ref=${this.buffer}>
|
2021-05-31 22:39:35 -04:00
|
|
|
|
<${Buffer}
|
|
|
|
|
buffer=${activeBuffer}
|
|
|
|
|
onChannelClick=${this.handleChannelClick}
|
|
|
|
|
onNickClick=${this.handleNickClick}/>
|
2020-06-25 06:03:05 -04:00
|
|
|
|
</section>
|
|
|
|
|
</>
|
2020-06-26 08:32:56 -04:00
|
|
|
|
${memberList}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
<${Composer}
|
|
|
|
|
ref=${this.composer}
|
2021-05-31 06:04:54 -04:00
|
|
|
|
readOnly=${composerReadOnly}
|
2021-03-09 12:11:59 -05:00
|
|
|
|
onSubmit=${this.handleComposerSubmit}
|
|
|
|
|
autocomplete=${this.autocomplete}
|
|
|
|
|
/>
|
2021-03-08 10:23:16 -05:00
|
|
|
|
${dialog}
|
2021-03-08 09:05:43 -05:00
|
|
|
|
${error}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|