From 4d3e42812d944ed2ce28c9e5edf1e8c7ad1e68e4 Mon Sep 17 00:00:00 2001
From: Cory McWilliams <cory@unprompted.com>
Date: Sat, 31 May 2025 15:17:07 -0400
Subject: [PATCH] ssb: Condense follows/blocks more, and support replies to
 them. #122

---
 apps/ssb.json          |   2 +-
 apps/ssb/tf-message.js | 189 ++++++++++++++++++++++++++++++++---------
 apps/ssb/tf-news.js    |  10 ++-
 3 files changed, 159 insertions(+), 42 deletions(-)

diff --git a/apps/ssb.json b/apps/ssb.json
index 2b3b67b6..952be215 100644
--- a/apps/ssb.json
+++ b/apps/ssb.json
@@ -1,5 +1,5 @@
 {
 	"type": "tildefriends-app",
 	"emoji": "🦀",
-	"previous": "&n2E4F4hnQe0dz+NvcMlKl5pcAZ3a1NM7/iNyWng9fRQ=.sha256"
+	"previous": "&Ky/Q/lCC3DIcqbsO9KAnfKzeBE/e9CB/8C5jACZ3UDI=.sha256"
 }
diff --git a/apps/ssb/tf-message.js b/apps/ssb/tf-message.js
index 45af0003..b54d82ff 100644
--- a/apps/ssb/tf-message.js
+++ b/apps/ssb/tf-message.js
@@ -301,31 +301,35 @@ class TfMessageElement extends LitElement {
 		return total;
 	}
 
+	expanded_key() {
+		return this.message?.id || this.messages?.map((x) => x.id).join(':');
+	}
+
 	set_expanded(expanded, tag) {
+		let key = this.expanded_key();
 		this.dispatchEvent(
 			new CustomEvent('tf-expand', {
 				bubbles: true,
 				composed: true,
-				detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
+				detail: {id: key + (tag || ''), expanded: expanded},
 			})
 		);
 	}
 
 	toggle_expanded(tag) {
-		this.set_expanded(
-			!this.expanded[(this.message.id || '') + (tag || '')],
-			tag
-		);
+		let key = this.expanded_key();
+		this.set_expanded(!this.expanded[key + (tag || '')], tag);
 	}
 
 	is_expanded(tag) {
-		return this.expanded[(this.message.id || '') + (tag || '')];
+		let key = this.expanded_key();
+		return this.expanded[key + (tag || '')];
 	}
 
 	render_children() {
 		let self = this;
 		if (this.message.child_messages?.length) {
-			if (!this.expanded[this.message.id]) {
+			if (!this.expanded[this.expanded_key()]) {
 				return html`
 					<button
 						class="w3-button w3-theme-d1 w3-block w3-bar"
@@ -578,6 +582,44 @@ class TfMessageElement extends LitElement {
 		`;
 	}
 
+	content_group_by_author() {
+		let sorted = this.message.messages
+			.map((x) => [
+				x.author,
+				x.content.blocking !== undefined
+					? x.content.blocking
+						? 'is blocking'
+						: 'is no longer blocking'
+					: x.content.following !== undefined
+						? x.content.following
+							? 'is following'
+							: 'is no longer following'
+						: '',
+				x.content.contact,
+				x,
+			])
+			.sort();
+		let result = [];
+		let last;
+		let group;
+		for (let row of sorted) {
+			if (last && last[0] == row[0] && last[1] == row[1]) {
+				group.push(row[2]);
+			} else {
+				if (group) {
+					result.push({author: last[0], action: last[1], users: group});
+				}
+				last = row;
+				group = [row[2]];
+			}
+		}
+		if (group) {
+			result.push({author: last[0], action: last[1], users: group});
+		}
+		console.log(this.message.messages, result);
+		return result;
+	}
+
 	render() {
 		let content = this.message?.content;
 		if (this.message?.decrypted?.type == 'post') {
@@ -586,20 +628,54 @@ class TfMessageElement extends LitElement {
 		let class_background = this.class_background();
 		let self = this;
 		if (this.message?.type === 'contact_group') {
-			return this.render_frame(
-				html` ${this.message.messages.map(
-					(x) =>
-						html`<tf-message
-							.message=${x}
-							whoami=${this.whoami}
-							.users=${this.users}
-							.drafts=${this.drafts}
-							.expanded=${this.expanded}
-							channel=${this.channel}
-							channel_unread=${this.channel_unread}
-						></tf-message>`
-				)}`
-			);
+			if (this.expanded[this.expanded_key()]) {
+				return this.render_frame(html`
+					<div class="w3-padding">
+						${this.message.messages.map(
+							(x) =>
+								html`<tf-message
+									.message=${x}
+									whoami=${this.whoami}
+									.users=${this.users}
+									.drafts=${this.drafts}
+									.expanded=${this.expanded}
+									channel=${this.channel}
+									channel_unread=${this.channel_unread}
+								></tf-message>`
+						)}
+					</div>
+					<button
+						class="w3-button w3-theme-d1 w3-block w3-bar"
+						style="box-sizing: border-box"
+						@click=${() => self.set_expanded(false)}
+					>
+						Collapse
+					</button>
+				`);
+			} else {
+				return this.render_frame(html`
+					<div class="w3-padding">
+						${this.content_group_by_author().map(
+							(x) => html`
+								<tf-user id=${x.author} .users=${this.users}></tf-user>
+								${x.action}
+								${x.users.map(
+									(y) => html`
+										<tf-user id=${y} .users=${this.users}></tf-user>
+									`
+								)}
+							`
+						)}
+					</div>
+					<button
+						class="w3-button w3-theme-d1 w3-block w3-bar"
+						style="box-sizing: border-box"
+						@click=${() => self.set_expanded(true)}
+					>
+						Expand
+					</button>
+				`);
+			}
 		} else if (this.message.placeholder) {
 			return this.render_frame(
 				html`<div class="w3-padding">
@@ -679,25 +755,60 @@ class TfMessageElement extends LitElement {
 					</div>
 				`);
 			} else if (content.type == 'contact') {
-				return html`
-					<div class="w3-padding">
-						<tf-user id=${this.message.author} .users=${this.users}></tf-user>
-						is
-						${content.blocking === true
-							? 'blocking'
-							: content.blocking === false
-								? 'no longer blocking'
-								: content.following === true
-									? 'following'
-									: content.following === false
-										? 'no longer following'
-										: '?'}
-						<tf-user
-							id=${this.message.content.contact}
-							.users=${this.users}
-						></tf-user>
+				return this.render_frame(html`
+					<div class="w3-bar">
+						<div class="w3-bar-item">
+							<tf-user id=${this.message.author} .users=${this.users}></tf-user>
+							is
+							${content.blocking === true
+								? 'blocking'
+								: content.blocking === false
+									? 'no longer blocking'
+									: content.following === true
+										? 'following'
+										: content.following === false
+											? 'no longer following'
+											: '?'}
+							<tf-user
+								id=${this.message.content.contact}
+								.users=${this.users}
+							></tf-user>
+						</div>
+						<div class="w3-bar-item w3-right">
+							<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
+								%
+							</button>
+							<div
+								class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1"
+								style="right: 48px"
+							>
+								<a
+									target="_top"
+									class="w3-button w3-bar-item"
+									href=${'#' + encodeURIComponent(this.message?.id)}
+									>View Message</a
+								>
+								<button
+									class="w3-button w3-bar-item w3-border-bottom"
+									@click=${this.copy_id}
+								>
+									Copy ID
+								</button>
+								${this.drafts[this.message?.id] === undefined
+									? html`
+											<button
+												class="w3-button w3-bar-item"
+												@click=${this.show_reply}
+											>
+												↩️ Reply
+											</button>
+										`
+									: undefined}
+							</div>
+						</div>
+						${this.render_votes()} ${this.render_actions()}
 					</div>
-				`;
+				`);
 			} else if (content.type == 'post') {
 				let self = this;
 				let body;
diff --git a/apps/ssb/tf-news.js b/apps/ssb/tf-news.js
index 3e21a1da..aea87e6f 100644
--- a/apps/ssb/tf-news.js
+++ b/apps/ssb/tf-news.js
@@ -166,7 +166,10 @@ class TfNewsElement extends LitElement {
 			if (message?.content?.type === 'contact') {
 				group.push(message);
 			} else {
-				if (group.length > 0) {
+				if (group.length == 1) {
+					result.push(group[0]);
+					group = [];
+				} else if (group.length > 1) {
 					result.push({
 						rowid: Math.max(...group.map((x) => x.rowid)),
 						type: 'contact_group',
@@ -177,7 +180,10 @@ class TfNewsElement extends LitElement {
 				result.push(message);
 			}
 		}
-		if (group.length > 0) {
+		if (group.length == 1) {
+			result.push(group[0]);
+			group = [];
+		} else if (group.length > 1) {
 			result.push({
 				rowid: Math.max(...group.map((x) => x.rowid)),
 				type: 'contact_group',