Refactor receipts

They are now saved in the buffer store to allow for proper server
separation.
This commit is contained in:
Simon Ser 2022-02-11 16:20:42 +01:00
parent d2bcea8c86
commit 065b3f21fc
2 changed files with 61 additions and 100 deletions

View file

@ -118,17 +118,6 @@ function fillConnectParams(params) {
return params; return params;
} }
function debounce(f, delay) {
let timeout = null;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
f(...args);
}, delay);
};
}
function showNotification(title, options) { function showNotification(title, options) {
if (!window.Notification || Notification.permission !== "granted") { if (!window.Notification || Notification.permission !== "granted") {
return new EventTarget(); return new EventTarget();
@ -156,6 +145,24 @@ function receiptFromMessage(msg) {
let lastErrorID = 0; let lastErrorID = 0;
function getLatestReceipt(bufferStore, server, type) {
let buffers = bufferStore.list(server);
let last = null;
for (let buf of buffers) {
if (!buf.receipts || buf.name === "*") {
continue;
}
let receipt = buf.receipts[type];
if (!receipt) {
continue;
}
if (!last || receipt.time > last.time) {
last = receipt;
}
}
return last;
}
export default class App extends Component { export default class App extends Component {
state = { state = {
...State.create(), ...State.create(),
@ -219,9 +226,6 @@ export default class App extends Component {
this.handleVerifyClick = this.handleVerifyClick.bind(this); this.handleVerifyClick = this.handleVerifyClick.bind(this);
this.handleVerifySubmit = this.handleVerifySubmit.bind(this); this.handleVerifySubmit = this.handleVerifySubmit.bind(this);
this.saveReceipts = debounce(this.saveReceipts.bind(this), 500);
this.receipts = store.receipts.load();
this.bufferStore = new store.Buffer(); this.bufferStore = new store.Buffer();
configPromise.then((config) => { configPromise.then((config) => {
@ -413,7 +417,9 @@ export default class App extends Component {
return; return;
} }
let prevReadReceipt = this.getReceipt(buf.name, ReceiptType.READ); let client = this.clients.get(buf.server);
let stored = this.bufferStore.get({ name: buf.name, server: client.params });
let prevReadReceipt = stored && stored.receipts ? stored.receipts[ReceiptType.READ] : null;
// TODO: only mark as read if user scrolled at the bottom // TODO: only mark as read if user scrolled at the bottom
let update = State.updateBuffer(state, buf.id, { let update = State.updateBuffer(state, buf.id, {
unread: Unread.NONE, unread: Unread.NONE,
@ -431,14 +437,13 @@ export default class App extends Component {
} }
if (buf.messages.length > 0) { if (buf.messages.length > 0) {
let lastMsg = buf.messages[buf.messages.length - 1];
this.setReceipt(buf.name, ReceiptType.READ, lastMsg);
let client = this.clients.get(buf.server); let client = this.clients.get(buf.server);
let lastMsg = buf.messages[buf.messages.length - 1];
this.bufferStore.put({ this.bufferStore.put({
name: buf.name, name: buf.name,
server: client.params, server: client.params,
unread: Unread.NONE, unread: Unread.NONE,
receipts: { [ReceiptType.READ]: receiptFromMessage(lastMsg) },
}); });
} }
@ -449,53 +454,6 @@ export default class App extends Component {
}); });
} }
saveReceipts() {
store.receipts.put(this.receipts);
}
getReceipt(target, type) {
let receipts = this.receipts.get(target);
if (!receipts) {
return undefined;
}
return receipts[type];
}
hasReceipt(target, type, msg) {
let receipt = this.getReceipt(target, type);
return isMessageBeforeReceipt(msg, receipt);
}
setReceipt(target, type, msg) {
let receipt = this.getReceipt(target, type);
if (this.hasReceipt(target, type, msg)) {
return;
}
// TODO: this doesn't trigger a redraw
this.receipts.set(target, {
...this.receipts.get(target),
[type]: receiptFromMessage(msg),
});
this.saveReceipts();
}
latestReceipt(type) {
let last = null;
this.receipts.forEach((receipts, target) => {
if (target === "*") {
return;
}
let delivery = receipts[type];
if (!delivery || !delivery.time) {
return;
}
if (!last || delivery.time > last.time) {
last = delivery;
}
});
return last;
}
addMessage(serverID, bufName, msg) { addMessage(serverID, bufName, msg) {
let client = this.clients.get(serverID); let client = this.clients.get(serverID);
@ -510,8 +468,13 @@ export default class App extends Component {
msg.tags.time = irc.formatDate(new Date()); msg.tags.time = irc.formatDate(new Date());
} }
let isDelivered = this.hasReceipt(bufName, ReceiptType.DELIVERED, msg); let isDelivered = false, isRead = false;
let isRead = this.hasReceipt(bufName, ReceiptType.READ, msg); let stored = this.bufferStore.get({ name: bufName, server: client.params });
if (stored) {
isDelivered = isMessageBeforeReceipt(msg, stored.receipts[ReceiptType.DELIVERED]);
isRead = isMessageBeforeReceipt(msg, stored.receipts[ReceiptType.READ]);
}
// TODO: messages coming from infinite scroll shouldn't trigger notifications // TODO: messages coming from infinite scroll shouldn't trigger notifications
if (client.isMyNick(msg.prefix.name)) { if (client.isMyNick(msg.prefix.name)) {
@ -565,7 +528,11 @@ export default class App extends Component {
}); });
notif.addEventListener("click", (event) => { notif.addEventListener("click", (event) => {
if (event.action === "accept") { if (event.action === "accept") {
this.setReceipt(bufName, ReceiptType.READ, msg); this.bufferStore.put({
name: bufName,
server: client.params,
receipts: { [ReceiptType.READ]: receiptFromMessage(msg) },
});
this.open(channel, serverID); this.open(channel, serverID);
} else { } else {
// TODO: scroll to message // TODO: scroll to message
@ -580,19 +547,18 @@ export default class App extends Component {
this.createBuffer(serverID, bufName); this.createBuffer(serverID, bufName);
} }
this.setReceipt(bufName, ReceiptType.DELIVERED, msg);
let bufID = { server: serverID, name: bufName }; let bufID = { server: serverID, name: bufName };
this.setState((state) => State.addMessage(state, msg, bufID)); this.setState((state) => State.addMessage(state, msg, bufID));
this.setBufferState(bufID, (buf) => { this.setBufferState(bufID, (buf) => {
// TODO: set unread if scrolled up // TODO: set unread if scrolled up
let unread = buf.unread; let unread = buf.unread;
let prevReadReceipt = buf.prevReadReceipt; let prevReadReceipt = buf.prevReadReceipt;
let receipts = { [ReceiptType.DELIVERED]: receiptFromMessage(msg) };
if (this.state.activeBuffer !== buf.id) { if (this.state.activeBuffer !== buf.id) {
unread = Unread.union(unread, msgUnread); unread = Unread.union(unread, msgUnread);
} else { } else {
this.setReceipt(bufName, ReceiptType.READ, msg); receipts[ReceiptType.READ] = receiptFromMessage(msg);
} }
// Don't show unread marker for my own messages // Don't show unread marker for my own messages
@ -604,6 +570,7 @@ export default class App extends Component {
name: buf.name, name: buf.name,
server: client.params, server: client.params,
unread, unread,
receipts,
}); });
return { unread, prevReadReceipt }; return { unread, prevReadReceipt };
}); });
@ -864,7 +831,7 @@ export default class App extends Component {
let target, channel; let target, channel;
switch (msg.command) { switch (msg.command) {
case irc.RPL_WELCOME: case irc.RPL_WELCOME:
let lastReceipt = this.latestReceipt(ReceiptType.DELIVERED); let lastReceipt = getLatestReceipt(this.bufferStore, client.params, ReceiptType.DELIVERED);
if (lastReceipt && lastReceipt.time && client.caps.enabled.has("draft/chathistory") && (!client.caps.enabled.has("soju.im/bouncer-networks") || client.params.bouncerNetwork)) { if (lastReceipt && lastReceipt.time && client.caps.enabled.has("draft/chathistory") && (!client.caps.enabled.has("soju.im/bouncer-networks") || client.params.bouncerNetwork)) {
let now = irc.formatDate(new Date()); let now = irc.formatDate(new Date());
client.fetchHistoryTargets(now, lastReceipt.time).then((targets) => { client.fetchHistoryTargets(now, lastReceipt.time).then((targets) => {
@ -933,14 +900,6 @@ export default class App extends Component {
this.switchToChannel = null; this.switchToChannel = null;
} }
break; break;
case "PART":
channel = msg.params[0];
if (client.isMyNick(msg.prefix.name)) {
this.receipts.delete(channel);
this.saveReceipts();
}
break;
case "BOUNCER": case "BOUNCER":
if (msg.params[0] !== "NETWORK") { if (msg.params[0] !== "NETWORK") {
break; // We're only interested in network updates break; // We're only interested in network updates
@ -1114,8 +1073,6 @@ export default class App extends Component {
client.fetchHistoryBetween(target, after, before, CHATHISTORY_MAX_SIZE).catch((err) => { client.fetchHistoryBetween(target, after, before, CHATHISTORY_MAX_SIZE).catch((err) => {
console.error("Failed to fetch backlog for '" + target + "': ", err); console.error("Failed to fetch backlog for '" + target + "': ", err);
this.showError("Failed to fetch backlog for '" + target + "'"); this.showError("Failed to fetch backlog for '" + target + "'");
this.receipts.delete(target);
this.saveReceipts();
}); });
} }
@ -1222,9 +1179,6 @@ export default class App extends Component {
client.unmonitor(buf.name); client.unmonitor(buf.name);
this.receipts.delete(buf.name);
this.saveReceipts();
this.bufferStore.delete({ name: buf.name, server: client.params }); this.bufferStore.delete({ name: buf.name, server: client.params });
break; break;
} }

View file

@ -25,18 +25,6 @@ class Item {
export const autoconnect = new Item("autoconnect"); export const autoconnect = new Item("autoconnect");
export const naggedProtocolHandler = new Item("naggedProtocolHandler"); export const naggedProtocolHandler = new Item("naggedProtocolHandler");
const rawReceipts = new Item("receipts");
export const receipts = {
load() {
let v = rawReceipts.load();
return new Map(Object.entries(v || {}));
},
put(m) {
rawReceipts.put(Object.fromEntries(m));
},
};
function debounce(f, delay) { function debounce(f, delay) {
let timeout = null; let timeout = null;
return (...args) => { return (...args) => {
@ -85,14 +73,33 @@ export class Buffer {
put(buf) { put(buf) {
let key = this.key(buf); let key = this.key(buf);
let prev = this.m.get(key); let updated = !this.m.has(key);
if (prev && prev.unread === buf.unread) { let prev = this.m.get(key) || {};
let unread = prev.unread;
if (buf.unread !== undefined && buf.unread !== prev.unread) {
unread = buf.unread;
updated = true;
}
let receipts = { ...prev.receipts };
if (buf.receipts) {
Object.keys(buf.receipts).forEach((k) => {
if (!receipts[k] || receipts[k].time <= buf.receipts[k].time) {
receipts[k] = buf.receipts[k];
updated = true;
}
});
}
if (!updated) {
return; return;
} }
this.m.set(this.key(buf), { this.m.set(this.key(buf), {
name: buf.name, name: buf.name,
unread: buf.unread, unread,
receipts,
server: { server: {
url: buf.server.url, url: buf.server.url,
nick: buf.server.nick, nick: buf.server.nick,