From 34078d5da751c609f9e0c12871bd91f36909f43f Mon Sep 17 00:00:00 2001
From: Simon Ser <contact@emersion.fr>
Date: Fri, 4 Jun 2021 19:45:51 +0200
Subject: [PATCH] Add support for draft/event-playback

---
 components/app.js | 81 +++++++++++++++++++++++++++--------------------
 lib/client.js     |  2 ++
 lib/irc.js        | 11 +++++++
 state.js          |  5 +++
 4 files changed, 65 insertions(+), 34 deletions(-)

diff --git a/components/app.js b/components/app.js
index ca27c17..c4f0507 100644
--- a/components/app.js
+++ b/components/app.js
@@ -488,6 +488,7 @@ export default class App extends Component {
 
 	handleMessage(serverID, msg) {
 		var client = this.clients.get(serverID);
+		var chatHistoryBatch = irc.findBatchByType(msg, "chathistory");
 
 		this.setState((state) => State.handleMessage(state, msg, serverID, client));
 
@@ -520,7 +521,9 @@ export default class App extends Component {
 			if (client.isChannel(target)) {
 				this.addMessage(serverID, target, msg);
 			}
-			this.handleMode(serverID, msg);
+			if (!chatHistoryBatch) {
+				this.handleMode(serverID, msg);
+			}
 			break;
 		case "NOTICE":
 		case "PRIVMSG":
@@ -559,7 +562,7 @@ export default class App extends Component {
 
 			this.addMessage(serverID, channel, msg);
 
-			if (client.isMyNick(msg.prefix.name)) {
+			if (!chatHistoryBatch && client.isMyNick(msg.prefix.name)) {
 				this.receipts.delete(channel);
 				this.saveReceipts();
 			}
@@ -570,46 +573,56 @@ export default class App extends Component {
 			break;
 		case "QUIT":
 			var affectedBuffers = [];
-			this.setState((state) => {
-				var buffers = new Map(state.buffers);
-				state.buffers.forEach((buf) => {
-					if (buf.server != serverID) {
-						return;
-					}
-					if (!buf.members.has(msg.prefix.name) && client.cm(buf.name) !== client.cm(msg.prefix.name)) {
-						return;
-					}
-					var members = new irc.CaseMapMap(buf.members);
-					members.delete(msg.prefix.name);
-					var offline = client.cm(buf.name) === client.cm(msg.prefix.name);
-					buffers.set(buf.id, { ...buf, members, offline });
-					affectedBuffers.push(buf.name);
+			if (chatHistoryBatch) {
+				affectedBuffers.push(chatHistoryBatch.params[0]);
+			} else {
+				this.setState((state) => {
+					var buffers = new Map(state.buffers);
+					state.buffers.forEach((buf) => {
+						if (buf.server != serverID) {
+							return;
+						}
+						if (!buf.members.has(msg.prefix.name) && client.cm(buf.name) !== client.cm(msg.prefix.name)) {
+							return;
+						}
+						var members = new irc.CaseMapMap(buf.members);
+						members.delete(msg.prefix.name);
+						var offline = client.cm(buf.name) === client.cm(msg.prefix.name);
+						buffers.set(buf.id, { ...buf, members, offline });
+						affectedBuffers.push(buf.name);
+					});
+					return { buffers };
 				});
-				return { buffers };
-			});
+			}
+
 			affectedBuffers.forEach((name) => this.addMessage(serverID, name, msg));
 			break;
 		case "NICK":
 			var newNick = msg.params[0];
 
 			var affectedBuffers = [];
-			this.setState((state) => {
-				var buffers = new Map(state.buffers);
-				state.buffers.forEach((buf) => {
-					if (buf.server != serverID) {
-						return;
-					}
-					if (!buf.members.has(msg.prefix.name)) {
-						return;
-					}
-					var members = new irc.CaseMapMap(buf.members);
-					members.set(newNick, members.get(msg.prefix.name));
-					members.delete(msg.prefix.name);
-					buffers.set(buf.id, { ...buf, members });
-					affectedBuffers.push(buf.name);
+			if (chatHistoryBatch) {
+				affectedBuffers.push(chatHistoryBatch.params[0]);
+			} else {
+				this.setState((state) => {
+					var buffers = new Map(state.buffers);
+					state.buffers.forEach((buf) => {
+						if (buf.server != serverID) {
+							return;
+						}
+						if (!buf.members.has(msg.prefix.name)) {
+							return;
+						}
+						var members = new irc.CaseMapMap(buf.members);
+						members.set(newNick, members.get(msg.prefix.name));
+						members.delete(msg.prefix.name);
+						buffers.set(buf.id, { ...buf, members });
+						affectedBuffers.push(buf.name);
+					});
+					return { buffers };
 				});
-				return { buffers };
-			});
+			}
+
 			affectedBuffers.forEach((name) => this.addMessage(serverID, name, msg));
 			break;
 		case "TOPIC":
diff --git a/lib/client.js b/lib/client.js
index a2057d6..a46aa82 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -13,6 +13,7 @@ const permanentCaps = [
 	"setname",
 
 	"draft/chathistory",
+	"draft/event-playback",
 
 	"soju.im/bouncer-networks",
 ];
@@ -160,6 +161,7 @@ export default class Client extends EventTarget {
 			msgBatch = this.batches.get(msg.tags["batch"]);
 			if (msgBatch) {
 				msgBatch.messages.push(msg);
+				msg.batch = msgBatch;
 			}
 		}
 
diff --git a/lib/irc.js b/lib/irc.js
index 6c0508c..1669d87 100644
--- a/lib/irc.js
+++ b/lib/irc.js
@@ -533,3 +533,14 @@ export function parseMembershipModes(str) {
 	}
 	return memberships;
 }
+
+export function findBatchByType(msg, type) {
+	var batch = msg.batch;
+	while (batch) {
+		if (batch.type === type) {
+			return batch;
+		}
+		batch = batch.parent;
+	}
+	return null;
+}
diff --git a/state.js b/state.js
index 925409a..4ac1777 100644
--- a/state.js
+++ b/state.js
@@ -249,6 +249,11 @@ export const State = {
 			return State.updateBuffer(state, { server: serverID, name }, updater);
 		}
 
+		// Don't update our internal state if it's a chat history message
+		if (irc.findBatchByType(msg, "chathistory")) {
+			return;
+		}
+
 		switch (msg.command) {
 		case irc.RPL_MYINFO:
 			// TODO: parse available modes