From 91208a6d47d4ca5d60cad21a41018763585a2209 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Tue, 18 May 2021 16:53:52 +0200 Subject: [PATCH] Add support for CHATHISTORY TARGETS The main motivation is to avoid missing direct messages coming from other users. A nice side-effect is that we no longer need to issue CHATHISTORY queries for each channel we JOIN: instead, we can only fetch history for targets known to have new messages available (as indicated by CHATHISTORY TARGETS). We use read receipts instead of delivery receipts, so that reloading the webapp restores the exact same state (ie, unread messages are re-fetched). References: https://github.com/ircv3/ircv3-specifications/pull/450 --- components/app.js | 49 ++++++++++++++++++++++++++++++++++++----------- lib/client.js | 18 +++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/components/app.js b/components/app.js index 5fc2034..53ae60c 100644 --- a/components/app.js +++ b/components/app.js @@ -450,6 +450,20 @@ export default class App extends Component { this.saveReceipts(); } + 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; + } + addMessage(netID, bufName, msg) { var client = this.clients.get(netID); @@ -608,6 +622,21 @@ export default class App extends Component { params: [this.state.connectParams.autojoin.join(",")], }); } + + 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); + }); + }); + } break; case irc.RPL_MYINFO: // TODO: parse available modes @@ -729,17 +758,6 @@ export default class App extends Component { this.switchBuffer({ network: netID, name: channel }); this.switchToChannel = null; } - - var receipt = this.getReceipt(channel, ReceiptType.READ); - if (client.isMyNick(msg.prefix.name) && receipt && client.enabledCaps["draft/chathistory"] && client.enabledCaps["server-time"]) { - var after = receipt; - var before = { time: msg.tags.time || irc.formatDate(new Date()) }; - client.fetchHistoryBetween(channel, after, before, CHATHISTORY_MAX_SIZE).catch((err) => { - this.setState({ error: "Failed to fetch history: " + err }); - this.receipts.delete(channel); - this.saveReceipts(); - }); - } break; case "PART": var channel = msg.params[0]; @@ -886,6 +904,7 @@ export default class App extends Component { case "PONG": case "BATCH": case "TAGMSG": + case "CHATHISTORY": // Ignore these break; default: @@ -928,6 +947,14 @@ export default class App extends Component { return irc.STD_CHANNEL_TYPES.indexOf(name[0]) >= 0; } + 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(); + }); + } + open(target) { var netID = getActiveNetworkID(this.state); var client = this.clients.get(netID); diff --git a/lib/client.js b/lib/client.js index 39ca4d3..fb95cc5 100644 --- a/lib/client.js +++ b/lib/client.js @@ -575,6 +575,24 @@ export default class Client extends EventTarget { }); } + fetchHistoryTargets(t1, t2) { + var msg = { + command: "CHATHISTORY", + params: ["TARGETS", "timestamp=" + t1, "timestamp=" + t2, 1000], + }; + return this.fetchBatch(msg, "draft/chathistory-targets").then((batch) => { + return batch.messages.map((msg) => { + if (msg.command != "CHATHISTORY" || msg.params[0] != "TARGETS") { + throw new Error("Cannot fetch chat history targets: unexpected message " + msg); + } + return { + name: msg.params[1], + latestMessage: msg.params[2], + }; + }); + }); + } + listBouncerNetworks() { if (!this.enabledCaps["soju.im/bouncer-networks"]) { return Promise.reject(new Error("Server doesn't support the BOUNCER extension"));