2020-06-18 08:23:08 -04:00
|
|
|
|
import * as irc from "/lib/irc.js";
|
|
|
|
|
import Client from "/lib/client.js";
|
|
|
|
|
import Buffer from "/components/buffer.js";
|
|
|
|
|
import BufferList from "/components/buffer-list.js";
|
2020-06-25 12:28:54 -04:00
|
|
|
|
import BufferHeader from "/components/buffer-header.js";
|
2020-06-26 08:32:56 -04:00
|
|
|
|
import MemberList from "/components/member-list.js";
|
2020-06-18 08:23:08 -04:00
|
|
|
|
import Connect from "/components/connect.js";
|
|
|
|
|
import Composer from "/components/composer.js";
|
2020-06-25 06:03:05 -04:00
|
|
|
|
import ScrollManager from "/components/scroll-manager.js";
|
2020-06-18 08:23:08 -04:00
|
|
|
|
import { html, Component, createRef } from "/lib/index.js";
|
2020-08-13 09:38:12 -04:00
|
|
|
|
import { strip as stripANSI } from "/lib/ansi.js";
|
2020-07-23 03:58:05 -04:00
|
|
|
|
import { SERVER_BUFFER, BufferType, ReceiptType, Status, Unread } from "/state.js";
|
2020-07-13 11:22:24 -04:00
|
|
|
|
import commands from "/commands.js";
|
2020-07-23 03:58:05 -04:00
|
|
|
|
import { setup as setupKeybindings } from "/keybindings.js";
|
2020-06-26 04:35:38 -04:00
|
|
|
|
|
2020-06-29 03:06:47 -04:00
|
|
|
|
const CHATHISTORY_PAGE_SIZE = 100;
|
2020-07-15 12:21:09 -04:00
|
|
|
|
const CHATHISTORY_MAX_SIZE = 4000;
|
2021-01-11 12:12:28 -05:00
|
|
|
|
const RECONNECT_DELAY_SEC = 10;
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
const DEFAULT_NETWORK = "network"; // TODO: remove this global
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-03 09:43:20 -04:00
|
|
|
|
function compareBuffers(a, b) {
|
|
|
|
|
if (a.type == BufferType.SERVER) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
if (b.type == BufferType.SERVER) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-04 07:47:27 -04:00
|
|
|
|
if (a.name < b.name) {
|
2020-08-03 09:43:20 -04:00
|
|
|
|
return -1;
|
|
|
|
|
}
|
2020-08-04 07:47:27 -04:00
|
|
|
|
if (a.name > b.name) {
|
2020-08-03 09:43:20 -04:00
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
function updateState(state, updater) {
|
|
|
|
|
var updated;
|
|
|
|
|
if (typeof updater === "function") {
|
|
|
|
|
updated = updater(state, state);
|
|
|
|
|
} else {
|
|
|
|
|
updated = updater;
|
|
|
|
|
}
|
|
|
|
|
if (state === updated || !updated) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
return { ...state, ...updated };
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 16:02:53 -05:00
|
|
|
|
function getBuffer(state, id) {
|
|
|
|
|
switch (typeof id) {
|
|
|
|
|
case "number":
|
|
|
|
|
return state.buffers.get(id);
|
|
|
|
|
case "object":
|
|
|
|
|
var network = id.network, name = id.name;
|
|
|
|
|
if (!network) {
|
|
|
|
|
network = state.activeNetwork;
|
|
|
|
|
}
|
|
|
|
|
for (var buf of state.buffers.values()) {
|
|
|
|
|
if (buf.network === network && buf.name === name) {
|
|
|
|
|
return buf;
|
|
|
|
|
}
|
2021-01-21 14:41:44 -05:00
|
|
|
|
}
|
2021-01-21 16:02:53 -05:00
|
|
|
|
return null;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("Invalid buffer ID type: " + (typeof id));
|
2021-01-21 14:41:44 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
export default class App extends Component {
|
|
|
|
|
client = null;
|
|
|
|
|
state = {
|
|
|
|
|
connectParams: {
|
|
|
|
|
serverURL: null,
|
|
|
|
|
serverPass: null,
|
|
|
|
|
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: [],
|
|
|
|
|
},
|
2020-06-24 10:56:28 -04:00
|
|
|
|
status: Status.DISCONNECTED,
|
2021-01-21 13:01:50 -05:00
|
|
|
|
networks: new Map(),
|
2020-06-18 08:23:08 -04:00
|
|
|
|
buffers: new Map(),
|
2021-01-21 13:01:50 -05:00
|
|
|
|
activeNetwork: DEFAULT_NETWORK,
|
2020-06-18 08:23:08 -04:00
|
|
|
|
activeBuffer: null,
|
2020-08-08 00:08:51 -04:00
|
|
|
|
error: null,
|
2020-06-18 08:23:08 -04:00
|
|
|
|
};
|
2020-07-15 12:21:09 -04:00
|
|
|
|
pendingHistory = Promise.resolve(null);
|
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-01-12 04:35:38 -05:00
|
|
|
|
reconnectTimeoutID = null;
|
2021-01-21 14:41:44 -05:00
|
|
|
|
lastBufferID = 0;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
|
|
this.handleConnectSubmit = this.handleConnectSubmit.bind(this);
|
|
|
|
|
this.handleBufferListClick = this.handleBufferListClick.bind(this);
|
|
|
|
|
this.handleComposerSubmit = this.handleComposerSubmit.bind(this);
|
2020-06-25 12:45:41 -04:00
|
|
|
|
this.handleNickClick = this.handleNickClick.bind(this);
|
2020-06-29 04:12:46 -04:00
|
|
|
|
this.handleJoinClick = this.handleJoinClick.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);
|
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);
|
|
|
|
|
|
2020-06-24 12:14:46 -04:00
|
|
|
|
if (window.localStorage && localStorage.getItem("autoconnect")) {
|
|
|
|
|
var connectParams = JSON.parse(localStorage.getItem("autoconnect"));
|
|
|
|
|
this.state.connectParams = {
|
|
|
|
|
...this.state.connectParams,
|
|
|
|
|
...connectParams,
|
|
|
|
|
autoconnect: true,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
var params = parseQueryString();
|
|
|
|
|
|
2020-07-01 06:25:57 -04:00
|
|
|
|
var host = window.location.host || "localhost:8080";
|
|
|
|
|
var proto = "wss:";
|
|
|
|
|
if (window.location.protocol != "https:") {
|
|
|
|
|
proto = "ws:";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var serverURL;
|
2020-06-24 12:14:46 -04:00
|
|
|
|
if (params.server) {
|
2020-07-01 06:25:57 -04:00
|
|
|
|
if (params.server.startsWith("/")) {
|
|
|
|
|
serverURL = proto + "//" + host + params.server;
|
|
|
|
|
} else {
|
|
|
|
|
serverURL = params.server;
|
2020-06-24 12:14:46 -04:00
|
|
|
|
}
|
2020-07-01 06:25:57 -04:00
|
|
|
|
} else {
|
|
|
|
|
serverURL = proto + "//" + host + "/socket";
|
2020-06-24 12:14:46 -04:00
|
|
|
|
}
|
2020-07-01 06:25:57 -04:00
|
|
|
|
this.state.connectParams.serverURL = serverURL;
|
2020-06-24 12:14:46 -04:00
|
|
|
|
|
|
|
|
|
if (params.channels) {
|
|
|
|
|
this.state.connectParams.autojoin = params.channels.split(",");
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
|
|
|
|
if (window.localStorage && localStorage.getItem("receipts")) {
|
|
|
|
|
var obj = JSON.parse(localStorage.getItem("receipts"));
|
|
|
|
|
this.receipts = new Map(Object.entries(obj));
|
|
|
|
|
}
|
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-01-21 13:01:50 -05:00
|
|
|
|
setNetworkState(id, updater, callback) {
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var net = state.networks.get(id);
|
|
|
|
|
if (!net) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var updated = updateState(net, updater);
|
2021-01-21 14:41:44 -05:00
|
|
|
|
if (!updated) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-01-21 13:01:50 -05:00
|
|
|
|
|
|
|
|
|
var networks = new Map(state.networks);
|
|
|
|
|
networks.set(id, updated);
|
|
|
|
|
return { networks };
|
|
|
|
|
}, 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-01-21 16:02:53 -05:00
|
|
|
|
var buf = getBuffer(state, id);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
if (!buf) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
var updated = updateState(buf, updater);
|
2021-01-21 14:41:44 -05:00
|
|
|
|
if (!updated) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
|
|
|
|
var buffers = new Map(state.buffers);
|
2021-01-21 14:41:44 -05:00
|
|
|
|
buffers.set(buf.id, updated);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
return { buffers };
|
|
|
|
|
}, callback);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
createBuffer(netID, name, callback) {
|
|
|
|
|
var id = null;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
this.setState((state) => {
|
2021-01-21 16:02:53 -05:00
|
|
|
|
if (getBuffer(state, { network: netID, 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;
|
|
|
|
|
}
|
|
|
|
|
|
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-01-21 13:27:04 -05:00
|
|
|
|
network: netID,
|
2020-06-26 06:08:14 -04:00
|
|
|
|
serverInfo: null, // if server
|
2020-06-26 06:00:10 -04:00
|
|
|
|
topic: null, // if channel
|
2020-07-13 06:51:09 -04:00
|
|
|
|
members: new Map(), // 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
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switchBuffer(name) {
|
2020-09-03 05:51:52 -04:00
|
|
|
|
var lastReadReceipt = this.getReceipt(name, ReceiptType.READ);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
// TODO: only mark as read if user scrolled at the bottom
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ name }, {
|
2020-09-03 05:51:52 -04:00
|
|
|
|
unread: Unread.NONE,
|
|
|
|
|
lastReadReceipt,
|
|
|
|
|
});
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var buf;
|
|
|
|
|
this.setState((state) => {
|
2021-01-21 16:02:53 -05:00
|
|
|
|
buf = getBuffer(state, { name });
|
2021-01-21 14:41:44 -05:00
|
|
|
|
if (!buf) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
return { activeBuffer: buf.id };
|
|
|
|
|
}, () => {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
if (this.composer.current) {
|
|
|
|
|
this.composer.current.focus();
|
|
|
|
|
}
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
|
|
|
|
if (!buf || buf.messages.length == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var lastMsg = buf.messages[buf.messages.length - 1];
|
|
|
|
|
this.setReceipt(name, ReceiptType.READ, lastMsg);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveReceipts() {
|
|
|
|
|
if (window.localStorage) {
|
|
|
|
|
var obj = Object.fromEntries(this.receipts);
|
|
|
|
|
localStorage.setItem("receipts", JSON.stringify(obj));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
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-01-21 13:27:04 -05:00
|
|
|
|
addMessage(netID, bufName, msg) {
|
2020-06-28 09:13:06 -04:00
|
|
|
|
msg.key = messagesCount;
|
|
|
|
|
messagesCount++;
|
|
|
|
|
|
2020-08-13 05:58:36 -04:00
|
|
|
|
msg.isHighlight = irc.isHighlight(msg, this.client.nick);
|
|
|
|
|
|
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:50:42 -04:00
|
|
|
|
var target = msg.params[0];
|
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";
|
|
|
|
|
} else if (target == this.client.nick) {
|
|
|
|
|
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;
|
|
|
|
|
if (this.isChannel(target)) {
|
|
|
|
|
title += " in " + target;
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
this.switchBuffer(target);
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-06-24 10:56:28 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-08 12:39:24 -04:00
|
|
|
|
if (msg.prefix.name != this.client.nick && (msg.command != "PART" && msg.comand != "QUIT")) {
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.createBuffer(netID, bufName);
|
2020-06-25 12:28:54 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.setReceipt(bufName, ReceiptType.DELIVERED, msg);
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: bufName}, (buf, state) => {
|
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-01-21 14:41:44 -05:00
|
|
|
|
if (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-21 13:01:50 -05:00
|
|
|
|
connect(netID, params) {
|
|
|
|
|
this.disconnect(netID);
|
2021-01-12 04:35:38 -05:00
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var networks = new Map(state.networks);
|
|
|
|
|
networks.set(netID, {
|
|
|
|
|
id: netID,
|
|
|
|
|
status: Status.CONNECTING,
|
|
|
|
|
});
|
|
|
|
|
return { networks };
|
|
|
|
|
});
|
|
|
|
|
this.setState({ connectParams: params });
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
|
|
|
|
this.client = new Client({
|
|
|
|
|
url: params.serverURL,
|
|
|
|
|
pass: params.serverPass,
|
|
|
|
|
nick: params.nick,
|
|
|
|
|
username: params.username,
|
|
|
|
|
realname: params.realname,
|
|
|
|
|
saslPlain: params.saslPlain,
|
|
|
|
|
});
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.client.addEventListener("close", () => {
|
|
|
|
|
this.handleClose(netID);
|
|
|
|
|
});
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
|
|
|
|
this.client.addEventListener("message", (event) => {
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.handleMessage(netID, event.detail.message);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
|
|
|
|
|
2020-08-08 00:08:51 -04:00
|
|
|
|
this.client.addEventListener("error", (event) => {
|
|
|
|
|
this.setState({
|
|
|
|
|
error: event.detail,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.createBuffer(netID, SERVER_BUFFER);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
this.switchBuffer(SERVER_BUFFER);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
handleClose(netID) {
|
|
|
|
|
this.setNetworkState(netID, (state) => {
|
2021-01-12 04:35:38 -05:00
|
|
|
|
if (state.status == Status.DISCONNECTED) {
|
|
|
|
|
// User decided to logout
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
console.log("Reconnecting to server in " + RECONNECT_DELAY_SEC + " seconds");
|
|
|
|
|
clearTimeout(this.reconnectTimeoutID);
|
|
|
|
|
this.reconnectTimeoutID = setTimeout(() => {
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.connect(netID, this.state.connectParams);
|
2021-01-12 04:35:38 -05:00
|
|
|
|
}, RECONNECT_DELAY_SEC * 1000);
|
|
|
|
|
return { status: Status.DISCONNECTED };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
disconnect(netID) {
|
2021-01-12 04:35:38 -05:00
|
|
|
|
clearTimeout(this.reconnectTimeoutID);
|
|
|
|
|
this.reconnectTimeoutID = null;
|
|
|
|
|
|
|
|
|
|
if (this.client) {
|
|
|
|
|
// Prevent auto-reconnect from kicking in
|
|
|
|
|
this.client.removeEventListener("close", this.handleClose);
|
|
|
|
|
this.client.close();
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.setNetworkState(netID, { status: Status.DISCONNECTED });
|
2021-01-12 04:35:38 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
reconnect(netID) {
|
|
|
|
|
this.connect(netID, this.state.connectParams);
|
2021-01-12 04:35:38 -05:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
handleMessage(netID, msg) {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
switch (msg.command) {
|
|
|
|
|
case irc.RPL_WELCOME:
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.setNetworkState(netID, { status: Status.REGISTERED });
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
|
|
|
|
if (this.state.connectParams.autojoin.length > 0) {
|
|
|
|
|
this.client.send({
|
|
|
|
|
command: "JOIN",
|
|
|
|
|
params: [this.state.connectParams.autojoin.join(",")],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
break;
|
2020-06-26 06:08:14 -04:00
|
|
|
|
case irc.RPL_MYINFO:
|
|
|
|
|
// TODO: parse available modes
|
|
|
|
|
var serverInfo = {
|
|
|
|
|
name: msg.params[1],
|
|
|
|
|
version: msg.params[2],
|
|
|
|
|
};
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: SERVER_BUFFER}, { serverInfo });
|
2020-06-26 06:08:14 -04:00
|
|
|
|
break;
|
2020-08-03 12:59:54 -04:00
|
|
|
|
case irc.RPL_NOTOPIC:
|
|
|
|
|
var channel = msg.params[1];
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: channel}, { topic: null });
|
2020-08-03 12:59:54 -04:00
|
|
|
|
break;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
case irc.RPL_TOPIC:
|
|
|
|
|
var channel = msg.params[1];
|
|
|
|
|
var topic = msg.params[2];
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: channel}, { topic });
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
2020-09-03 05:36:08 -04:00
|
|
|
|
case irc.RPL_TOPICWHOTIME:
|
|
|
|
|
// Ignore
|
|
|
|
|
break;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
case irc.RPL_NAMREPLY:
|
|
|
|
|
var channel = msg.params[2];
|
2020-06-30 04:27:24 -04:00
|
|
|
|
var membersList = msg.params[3].split(" ");
|
2020-06-18 08:23:08 -04:00
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: channel}, (buf) => {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
var members = new Map(buf.members);
|
|
|
|
|
membersList.forEach((s) => {
|
|
|
|
|
var member = irc.parseMembership(s);
|
|
|
|
|
members.set(member.nick, member.prefix);
|
|
|
|
|
});
|
|
|
|
|
|
2020-06-24 10:02:56 -04:00
|
|
|
|
return { members };
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case irc.RPL_ENDOFNAMES:
|
|
|
|
|
break;
|
2020-06-26 06:00:10 -04:00
|
|
|
|
case irc.RPL_WHOREPLY:
|
|
|
|
|
var last = msg.params[msg.params.length - 1];
|
|
|
|
|
var who = {
|
|
|
|
|
username: msg.params[2],
|
|
|
|
|
hostname: msg.params[3],
|
|
|
|
|
server: msg.params[4],
|
|
|
|
|
nick: msg.params[5],
|
|
|
|
|
away: msg.params[6] == 'G', // H for here, G for gone
|
|
|
|
|
realname: last.slice(last.indexOf(" ") + 1),
|
|
|
|
|
};
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: who.nick}, { who, offline: false });
|
2020-06-26 06:00:10 -04:00
|
|
|
|
break;
|
|
|
|
|
case irc.RPL_ENDOFWHO:
|
2020-07-13 06:51:09 -04:00
|
|
|
|
var target = msg.params[1];
|
|
|
|
|
if (!this.isChannel(target) && target.indexOf("*") < 0) {
|
|
|
|
|
// Not a channel nor a mask, likely a nick
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: target}, (buf) => {
|
2020-07-13 06:51:09 -04:00
|
|
|
|
// TODO: mark user offline if we have old WHO info but this
|
|
|
|
|
// WHO reply is empty
|
|
|
|
|
if (buf.who) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
return { offline: true };
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-06-26 06:00:10 -04:00
|
|
|
|
break;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
case "NOTICE":
|
|
|
|
|
case "PRIVMSG":
|
|
|
|
|
var target = msg.params[0];
|
|
|
|
|
if (target == this.client.nick) {
|
|
|
|
|
target = msg.prefix.name;
|
|
|
|
|
}
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.addMessage(netID, target, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
|
|
|
|
case "JOIN":
|
|
|
|
|
var channel = msg.params[0];
|
|
|
|
|
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.createBuffer(netID, channel);
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: channel}, (buf) => {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
var members = new Map(buf.members);
|
|
|
|
|
members.set(msg.prefix.name, null);
|
2020-06-24 10:02:56 -04:00
|
|
|
|
return { members };
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
|
|
|
|
if (msg.prefix.name != this.client.nick) {
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.addMessage(netID, channel, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
if (channel == this.state.connectParams.autojoin[0]) {
|
|
|
|
|
// TODO: only switch once right after connect
|
|
|
|
|
this.switchBuffer(channel);
|
|
|
|
|
}
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
|
|
|
|
var receipt = this.getReceipt(channel, ReceiptType.READ);
|
2020-08-13 06:18:52 -04:00
|
|
|
|
if (msg.prefix.name == this.client.nick && receipt && this.client.enabledCaps["draft/chathistory"] && this.client.enabledCaps["server-time"]) {
|
2020-07-15 12:21:09 -04:00
|
|
|
|
var after = receipt;
|
|
|
|
|
var before = { time: msg.tags.time || irc.formatDate(new Date()) };
|
|
|
|
|
this.fetchHistoryBetween(channel, after, before, CHATHISTORY_MAX_SIZE).catch((err) => {
|
2020-08-08 00:08:51 -04:00
|
|
|
|
this.setState({ error: "Failed to fetch history: " + err });
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.receipts.delete(channel);
|
|
|
|
|
this.saveReceipts();
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
|
|
|
|
case "PART":
|
|
|
|
|
var channel = msg.params[0];
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: channel}, (buf) => {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
var members = new Map(buf.members);
|
|
|
|
|
members.delete(msg.prefix.name);
|
2020-06-24 10:02:56 -04:00
|
|
|
|
return { members };
|
2020-06-18 08:23:08 -04:00
|
|
|
|
});
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.addMessage(netID, channel, msg);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
|
|
|
|
if (msg.prefix.name == this.client.nick) {
|
|
|
|
|
this.receipts.delete(channel);
|
|
|
|
|
this.saveReceipts();
|
|
|
|
|
}
|
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) => {
|
|
|
|
|
if (!buf.members.has(msg.prefix.name) && buf.name != msg.prefix.name) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var members = new Map(buf.members);
|
|
|
|
|
members.delete(msg.prefix.name);
|
2020-07-13 06:51:09 -04:00
|
|
|
|
var offline = buf.name == msg.prefix.name;
|
|
|
|
|
buffers.set(buf.name, { ...buf, members, offline });
|
2020-07-08 12:39:24 -04:00
|
|
|
|
affectedBuffers.push(buf.name);
|
|
|
|
|
});
|
|
|
|
|
return { buffers };
|
|
|
|
|
});
|
2021-01-21 13:27:04 -05:00
|
|
|
|
affectedBuffers.forEach((name) => this.addMessage(netID, 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) => {
|
|
|
|
|
if (!buf.members.has(msg.prefix.name)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var members = new Map(buf.members);
|
|
|
|
|
members.set(newNick, members.get(msg.prefix.name));
|
|
|
|
|
members.delete(msg.prefix.name);
|
|
|
|
|
buffers.set(buf.name, { ...buf, members });
|
|
|
|
|
affectedBuffers.push(buf.name);
|
|
|
|
|
});
|
|
|
|
|
return { buffers };
|
|
|
|
|
});
|
2021-01-21 13:27:04 -05:00
|
|
|
|
affectedBuffers.forEach((name) => this.addMessage(netID, name, msg));
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
|
|
|
|
case "TOPIC":
|
|
|
|
|
var channel = msg.params[0];
|
|
|
|
|
var topic = msg.params[1];
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: channel}, { topic });
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.addMessage(netID, channel, msg);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
break;
|
2020-06-26 06:45:27 -04:00
|
|
|
|
case "AWAY":
|
|
|
|
|
var awayMessage = msg.params[0];
|
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.setBufferState({ network: netID, name: msg.prefix.name}, (buf) => {
|
2020-06-26 06:45:27 -04:00
|
|
|
|
var who = { ...buf.who, away: !!awayMessage };
|
|
|
|
|
return { who };
|
|
|
|
|
});
|
|
|
|
|
break;
|
2020-06-25 06:16:42 -04:00
|
|
|
|
case "CAP":
|
|
|
|
|
case "AUTHENTICATE":
|
2020-07-01 06:12:56 -04:00
|
|
|
|
case "PING":
|
2020-07-15 12:21:09 -04:00
|
|
|
|
case "BATCH":
|
2020-06-25 06:16:42 -04:00
|
|
|
|
// Ignore these
|
|
|
|
|
break;
|
2020-06-18 08:23:08 -04:00
|
|
|
|
default:
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.addMessage(netID, 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 });
|
|
|
|
|
|
2020-06-24 12:14:46 -04:00
|
|
|
|
if (window.localStorage) {
|
|
|
|
|
if (connectParams.autoconnect) {
|
2020-06-18 08:23:08 -04:00
|
|
|
|
localStorage.setItem("autoconnect", JSON.stringify(connectParams));
|
|
|
|
|
} else {
|
|
|
|
|
localStorage.removeItem("autoconnect");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.connect(DEFAULT_NETWORK, connectParams);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-25 12:45:41 -04:00
|
|
|
|
open(target) {
|
|
|
|
|
if (this.isChannel(target)) {
|
|
|
|
|
this.client.send({ command: "JOIN", params: [target] });
|
2020-06-26 06:00:10 -04:00
|
|
|
|
} else {
|
|
|
|
|
this.client.send({ command: "WHO", params: [target] });
|
2020-06-25 12:45:41 -04:00
|
|
|
|
}
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.createBuffer(this.state.activeNetwork, target);
|
2020-06-25 12:45:41 -04:00
|
|
|
|
this.switchBuffer(target);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-25 12:28:54 -04:00
|
|
|
|
close(target) {
|
|
|
|
|
if (target == SERVER_BUFFER) {
|
2021-01-11 12:12:28 -05:00
|
|
|
|
this.setState({
|
|
|
|
|
buffers: new Map(),
|
|
|
|
|
activeBuffer: null,
|
|
|
|
|
});
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.disconnect(DEFAULT_NETWORK);
|
2020-06-25 12:28:54 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2020-06-25 12:28:54 -04:00
|
|
|
|
if (this.isChannel(target)) {
|
|
|
|
|
this.client.send({ command: "PART", params: [target] });
|
|
|
|
|
}
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2020-06-25 12:28:54 -04:00
|
|
|
|
this.switchBuffer(SERVER_BUFFER);
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
var buffers = new Map(state.buffers);
|
|
|
|
|
buffers.delete(target);
|
|
|
|
|
return { buffers };
|
|
|
|
|
});
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2020-08-03 09:29:21 -04:00
|
|
|
|
this.receipts.delete(target);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.saveReceipts();
|
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) {
|
2020-08-08 00:08:51 -04:00
|
|
|
|
this.setState({ error: "Unknown command '" + name + "'" });
|
2020-07-13 11:22:24 -04:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
cmd(this, args);
|
2020-08-08 00:08:51 -04:00
|
|
|
|
} catch (error) {
|
|
|
|
|
this.setState({ error });
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var msg = { command: "PRIVMSG", params: [target, text] };
|
|
|
|
|
this.client.send(msg);
|
|
|
|
|
|
|
|
|
|
if (!this.client.enabledCaps["echo-message"]) {
|
|
|
|
|
msg.prefix = { name: this.client.nick };
|
2021-01-21 13:27:04 -05:00
|
|
|
|
this.addMessage(this.state.activeNetwork, 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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleBufferListClick(name) {
|
|
|
|
|
this.switchBuffer(name);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-29 04:12:46 -04:00
|
|
|
|
handleJoinClick(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
var channel = prompt("Join channel:");
|
|
|
|
|
if (!channel) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.client.send({ command: "JOIN", params: [channel] });
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
2020-07-15 12:21:09 -04:00
|
|
|
|
roundtripChatHistory(params) {
|
|
|
|
|
// Don't send multiple CHATHISTORY commands in parallel, we can't
|
|
|
|
|
// properly handle batches and errors.
|
|
|
|
|
this.pendingHistory = this.pendingHistory.catch(() => {}).then(() => {
|
|
|
|
|
var msg = {
|
|
|
|
|
command: "CHATHISTORY",
|
|
|
|
|
params,
|
|
|
|
|
};
|
|
|
|
|
return this.client.roundtrip(msg, (event) => {
|
|
|
|
|
var msg = event.detail.message;
|
|
|
|
|
|
|
|
|
|
switch (msg.command) {
|
|
|
|
|
case "BATCH":
|
|
|
|
|
var enter = msg.params[0].startsWith("+");
|
|
|
|
|
var name = msg.params[0].slice(1);
|
|
|
|
|
if (enter) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
var batch = this.client.batches.get(name);
|
|
|
|
|
if (batch.type == "chathistory") {
|
|
|
|
|
return batch;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case "FAIL":
|
|
|
|
|
if (msg.params[0] == "CHATHISTORY") {
|
|
|
|
|
throw msg;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return this.pendingHistory;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Fetch history in ascending order */
|
|
|
|
|
fetchHistoryBetween(target, after, before, limit) {
|
|
|
|
|
var max = Math.min(limit, CHATHISTORY_PAGE_SIZE);
|
|
|
|
|
var params = ["AFTER", target, "timestamp=" + after.time, max];
|
|
|
|
|
return this.roundtripChatHistory(params).then((batch) => {
|
|
|
|
|
limit -= batch.messages.length;
|
|
|
|
|
if (limit <= 0) {
|
|
|
|
|
throw new Error("Cannot fetch all chat history: too many messages");
|
|
|
|
|
}
|
|
|
|
|
if (batch.messages.length == max) {
|
|
|
|
|
// There are still more messages to fetch
|
|
|
|
|
after.time = batch.messages[batch.messages.length - 1].tags.time;
|
|
|
|
|
return this.fetchHistoryBetween(target, after, before, limit);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
if (!this.client.enabledCaps["draft/chathistory"] || !this.client.enabledCaps["server-time"]) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-01-21 14:41:44 -05:00
|
|
|
|
if (this.endOfHistory.get(buf.name)) {
|
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-21 14:41:44 -05:00
|
|
|
|
this.endOfHistory.set(buf.name, true);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
|
2021-01-21 14:41:44 -05:00
|
|
|
|
var params = ["BEFORE", buf.name, "timestamp=" + before, CHATHISTORY_PAGE_SIZE];
|
2020-07-15 12:21:09 -04:00
|
|
|
|
this.roundtripChatHistory(params).then((batch) => {
|
2021-01-21 14:41:44 -05:00
|
|
|
|
this.endOfHistory.set(buf.name, batch.messages.length < CHATHISTORY_PAGE_SIZE);
|
2020-07-15 12:21:09 -04:00
|
|
|
|
});
|
2020-06-29 03:06:47 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
componentDidMount() {
|
2020-06-24 12:14:46 -04:00
|
|
|
|
if (this.state.connectParams.autoconnect) {
|
2021-01-21 13:01:50 -05:00
|
|
|
|
this.connect(DEFAULT_NETWORK, this.state.connectParams);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
2020-07-23 03:58:05 -04:00
|
|
|
|
|
|
|
|
|
setupKeybindings(this);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
2021-01-21 13:01:50 -05:00
|
|
|
|
var activeNetwork = null;
|
|
|
|
|
if (this.state.activeNetwork) {
|
|
|
|
|
activeNetwork = this.state.networks.get(this.state.activeNetwork);
|
2020-06-18 08:23:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var activeBuffer = null;
|
|
|
|
|
if (this.state.activeBuffer) {
|
|
|
|
|
activeBuffer = this.state.buffers.get(this.state.activeBuffer);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 13:01:50 -05:00
|
|
|
|
if (!activeNetwork || (activeNetwork.status != Status.REGISTERED && !activeBuffer)) {
|
|
|
|
|
return html`
|
|
|
|
|
<section id="connect">
|
|
|
|
|
<${Connect} error=${this.state.error} params=${this.state.connectParams} disabled=${this.state.status != Status.DISCONNECTED} onSubmit=${this.handleConnectSubmit}/>
|
|
|
|
|
</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">
|
2020-06-25 12:28:54 -04:00
|
|
|
|
<${BufferHeader} buffer=${activeBuffer} onClose=${() => this.close(activeBuffer.name)}/>
|
|
|
|
|
</section>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-26 08:32:56 -04:00
|
|
|
|
var memberList = null;
|
|
|
|
|
if (activeBuffer && activeBuffer.type == BufferType.CHANNEL) {
|
|
|
|
|
memberList = html`
|
|
|
|
|
<section id="member-list-header">
|
|
|
|
|
${activeBuffer.members.size} users
|
|
|
|
|
</section>
|
|
|
|
|
<section id="member-list">
|
|
|
|
|
<${MemberList} members=${activeBuffer.members} onNickClick=${this.handleNickClick}/>
|
|
|
|
|
</section>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-18 08:23:08 -04:00
|
|
|
|
return html`
|
2020-06-26 09:16:07 -04:00
|
|
|
|
<section id="buffer-list">
|
2020-06-18 08:23:08 -04:00
|
|
|
|
<${BufferList} buffers=${this.state.buffers} activeBuffer=${this.state.activeBuffer} onBufferClick=${this.handleBufferListClick}/>
|
2020-06-29 04:12:46 -04:00
|
|
|
|
<div class="actions">
|
|
|
|
|
<a href="#" onClick=${this.handleJoinClick}>Join channel</a>
|
|
|
|
|
</div>
|
2020-06-18 08:23:08 -04:00
|
|
|
|
</section>
|
2020-06-26 09:16:07 -04:00
|
|
|
|
${bufferHeader}
|
2020-06-29 03:06:47 -04:00
|
|
|
|
<${ScrollManager} target=${this.buffer} scrollKey=${this.state.activeBuffer} onScrollTop=${this.handleBufferScrollTop}>
|
2020-06-25 06:03:05 -04:00
|
|
|
|
<section id="buffer" ref=${this.buffer}>
|
2020-06-25 12:45:41 -04:00
|
|
|
|
<${Buffer} buffer=${activeBuffer} onNickClick=${this.handleNickClick}/>
|
2020-06-25 06:03:05 -04:00
|
|
|
|
</section>
|
|
|
|
|
</>
|
2020-06-26 08:32:56 -04:00
|
|
|
|
${memberList}
|
2020-06-29 06:36:17 -04:00
|
|
|
|
<${Composer} ref=${this.composer} readOnly=${this.state.activeBuffer == SERVER_BUFFER} onSubmit=${this.handleComposerSubmit} autocomplete=${this.autocomplete}/>
|
2020-08-08 00:08:51 -04:00
|
|
|
|
${this.state.error ? html`
|
|
|
|
|
<p id="error-msg">${this.state.error} <a href="#" onClick=${this.dismissError}>×</a></p>
|
|
|
|
|
` : null}
|
2020-06-18 08:23:08 -04:00
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|