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
This commit is contained in:
Simon Ser 2021-05-18 16:53:52 +02:00
parent 74d9dea5bb
commit 91208a6d47
2 changed files with 56 additions and 11 deletions

View file

@ -450,6 +450,20 @@ export default class App extends Component {
this.saveReceipts(); 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) { addMessage(netID, bufName, msg) {
var client = this.clients.get(netID); var client = this.clients.get(netID);
@ -608,6 +622,21 @@ export default class App extends Component {
params: [this.state.connectParams.autojoin.join(",")], 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; break;
case irc.RPL_MYINFO: case irc.RPL_MYINFO:
// TODO: parse available modes // TODO: parse available modes
@ -729,17 +758,6 @@ export default class App extends Component {
this.switchBuffer({ network: netID, name: channel }); this.switchBuffer({ network: netID, name: channel });
this.switchToChannel = null; 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; break;
case "PART": case "PART":
var channel = msg.params[0]; var channel = msg.params[0];
@ -886,6 +904,7 @@ export default class App extends Component {
case "PONG": case "PONG":
case "BATCH": case "BATCH":
case "TAGMSG": case "TAGMSG":
case "CHATHISTORY":
// Ignore these // Ignore these
break; break;
default: default:
@ -928,6 +947,14 @@ export default class App extends Component {
return irc.STD_CHANNEL_TYPES.indexOf(name[0]) >= 0; 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) { open(target) {
var netID = getActiveNetworkID(this.state); var netID = getActiveNetworkID(this.state);
var client = this.clients.get(netID); var client = this.clients.get(netID);

View file

@ -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() { listBouncerNetworks() {
if (!this.enabledCaps["soju.im/bouncer-networks"]) { if (!this.enabledCaps["soju.im/bouncer-networks"]) {
return Promise.reject(new Error("Server doesn't support the BOUNCER extension")); return Promise.reject(new Error("Server doesn't support the BOUNCER extension"));