diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt
index a01bb8f1bc..f73ef2018e 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt
@@ -56,7 +56,10 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatGroupPosition
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzEditedNote
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzForumVote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzSystemMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChannelAdminSystemMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatClip
@@ -67,9 +70,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEncr
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderMarmotEncryptedMedia
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderRegularTextNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasMip04Media
+import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.isBuzzActivityRow
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeBuzzEdit
import com.vitorpamplona.amethyst.ui.theme.ReactionRowZapraiser
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
+import com.vitorpamplona.quartz.buzz.forum.ForumVoteEvent
+import com.vitorpamplona.quartz.buzz.stream.StreamMessageDiffEvent
import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
@@ -136,6 +142,16 @@ fun ChatroomMessageCompose(
} else if (event is SystemMessageEvent) {
// Buzz kind-40099: relay-signed room narration (join/leave/topic).
RenderBuzzSystemMessage(baseNote)
+ } else if (event is StreamMessageDiffEvent) {
+ // Buzz kind-40008: a code/text diff pushed into the channel.
+ RenderBuzzDiff(baseNote)
+ } else if (event is ForumVoteEvent) {
+ // Buzz kind-45002: a forum up/down vote.
+ RenderBuzzForumVote(baseNote)
+ } else if (isBuzzActivityRow(event)) {
+ // Buzz agent-job (43xxx) and huddle (48xxx) lifecycle narration. Huddles
+ // especially must be caught here — their content is JSON, not chat text.
+ RenderBuzzActivityRow(baseNote)
} else {
NormalChatNote(
baseNote,
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt
index 1037a5fddd..0a7458d050 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderBuzzNotes.kt
@@ -20,7 +20,12 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types
+import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -30,6 +35,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
@@ -41,7 +49,20 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatSystemMessage
import com.vitorpamplona.amethyst.ui.stringRes
+import com.vitorpamplona.quartz.buzz.forum.ForumVoteEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleEndedEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantJoinedEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantLeftEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleStartedEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobAcceptedEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobCancelEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobErrorEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobProgressEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobRequestEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobResultEvent
+import com.vitorpamplona.quartz.buzz.stream.StreamMessageDiffEvent
import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent
+import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip29RelayGroups.groupId
/**
@@ -131,3 +152,117 @@ fun RenderBuzzSystemMessage(note: Note) {
ChatSystemMessage(text = text)
}
+
+/**
+ * True for the Buzz agent-job and huddle lifecycle kinds that [RenderBuzzActivityRow]
+ * narrates as a centered system line rather than a chat bubble. Huddle events in
+ * particular MUST be caught here — their `content` is JSON, so rendering them as a plain
+ * chat message would show raw `{"ephemeral_channel_id":…}`.
+ */
+fun isBuzzActivityRow(event: Event?): Boolean =
+ event is JobRequestEvent ||
+ event is JobAcceptedEvent ||
+ event is JobProgressEvent ||
+ event is JobResultEvent ||
+ event is JobCancelEvent ||
+ event is JobErrorEvent ||
+ event is HuddleStartedEvent ||
+ event is HuddleParticipantJoinedEvent ||
+ event is HuddleParticipantLeftEvent ||
+ event is HuddleEndedEvent
+
+/**
+ * Narrates a Buzz agent-job or huddle lifecycle event as a centered system line. The
+ * label is derived from the kind; job progress/result/error also append a short content
+ * snippet (the human-readable status/result/error the agent wrote).
+ */
+@Composable
+fun RenderBuzzActivityRow(note: Note) {
+ val event = note.event ?: return
+ val text =
+ remember(event) {
+ when (event) {
+ is JobRequestEvent -> "⚙ job requested" + event.request().snippet()
+ is JobAcceptedEvent -> "⚙ job accepted"
+ is JobProgressEvent -> "⚙ job progress" + (event.status()?.let { ": $it" } ?: "") + event.content.snippet()
+ is JobResultEvent -> "⚙ job result" + event.result().snippet()
+ is JobCancelEvent -> "⚙ job cancelled"
+ is JobErrorEvent -> "⚠ job error" + event.error().snippet()
+ is HuddleStartedEvent -> "🔊 huddle started"
+ is HuddleParticipantJoinedEvent -> "🔊 someone joined the huddle"
+ is HuddleParticipantLeftEvent -> "🔊 someone left the huddle"
+ is HuddleEndedEvent -> "🔊 huddle ended"
+ else -> event.content.take(120)
+ }
+ }
+ ChatSystemMessage(text = text)
+}
+
+/** A short one-line snippet of free-text content appended after a label, or "" if blank. */
+private fun String.snippet(): String {
+ val oneLine = trim().replace('\n', ' ')
+ if (oneLine.isEmpty()) return ""
+ return ": " + if (oneLine.length > 80) oneLine.take(80) + "…" else oneLine
+}
+
+/**
+ * A Buzz kind-40008 stream diff: a code/text diff pushed into the channel. Renders the
+ * repo/commit/file header from the `DiffMeta` tags plus the diff body in a monospace,
+ * horizontally-scrollable block, rather than the plain-text fallback (which mangles the
+ * `+`/`-` gutters into wrapped prose).
+ */
+@Composable
+fun RenderBuzzDiff(note: Note) {
+ val event = note.event as? StreamMessageDiffEvent ?: return
+ val meta = remember(event) { event.diffMeta() }
+
+ Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
+ Column(modifier = Modifier.padding(12.dp)) {
+ val header =
+ remember(meta) {
+ meta?.let {
+ buildString {
+ it.filePath?.let(::append)
+ it.language?.let { lang -> append(if (isEmpty()) lang else " · $lang") }
+ it.prNumber?.let { pr -> append(" · PR #$pr") }
+ it.commitSha
+ ?.takeIf { sha -> sha.isNotBlank() }
+ ?.let { sha -> append(" · ${sha.take(8)}") }
+ }.ifBlank { null }
+ }
+ }
+ if (header != null) {
+ Text(
+ text = header,
+ style = MaterialTheme.typography.labelMedium,
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ Text(
+ text = event.content,
+ style = MaterialTheme.typography.bodySmall,
+ fontFamily = FontFamily.Monospace,
+ modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
+ )
+ if (meta?.truncated == true) {
+ Text(
+ text = stringRes(R.string.buzz_diff_truncated),
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ fontSize = 10.sp,
+ )
+ }
+ }
+ }
+}
+
+/** A Buzz kind-45002 forum vote: a lightweight up/down signal, shown as a system line. */
+@Composable
+fun RenderBuzzForumVote(note: Note) {
+ val event = note.event as? ForumVoteEvent ?: return
+ // Content is the vote token ("+"/"-" or similar); show a compact glyph line.
+ val text = remember(event) { if (event.content.trim().startsWith("-")) "▼ downvoted a post" else "▲ upvoted a post" }
+ ChatSystemMessage(text = text)
+}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupFilterBuilders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupFilterBuilders.kt
index 932cc54210..92b02e6ecc 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupFilterBuilders.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupFilterBuilders.kt
@@ -20,6 +20,20 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource
+import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent
+import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent
+import com.vitorpamplona.quartz.buzz.forum.ForumVoteEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleEndedEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantJoinedEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleParticipantLeftEvent
+import com.vitorpamplona.quartz.buzz.huddles.HuddleStartedEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobAcceptedEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobCancelEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobErrorEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobProgressEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobRequestEvent
+import com.vitorpamplona.quartz.buzz.jobs.JobResultEvent
+import com.vitorpamplona.quartz.buzz.stream.CanvasEvent
import com.vitorpamplona.quartz.buzz.stream.StreamMessageDiffEvent
import com.vitorpamplona.quartz.buzz.stream.StreamMessageEditEvent
import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event
@@ -91,12 +105,21 @@ val RELAY_GROUP_TIMELINE_KINDS = listOf(ChatEvent.KIND, PollEvent.KIND)
/**
* Extra timeline kinds a `block/buzz` workspace relay serves in the same `h`-scoped
- * channels: stream messages v2 (40002), edits (40003), diffs (40008) and system rows
- * (40099). Requested UNCONDITIONALLY alongside the NIP-29 set — on a vanilla relay the
+ * channels, requested UNCONDITIONALLY alongside the NIP-29 set — on a vanilla relay the
* kinds simply match nothing. A dialect-gated version was tried and reverted: gating
* creates a bootstrap hole (nothing asks for a Buzz kind until one is consumed) and a
* worse one — history pages fetched before the mark advance their cursors past ranges
* queried WITHOUT Buzz kinds, permanently skipping older workspace messages.
+ *
+ * All are `h`-scoped (`GroupIdTag`), so the same `#h` group REQ returns them:
+ * - stream messages v2 (40002), edits (40003), diffs (40008), system rows (40099), canvas (40100)
+ * - forum posts/votes/comments (45001-45003)
+ * - agent jobs (43001-43006)
+ * - huddle lifecycle (48100-48103)
+ *
+ * Consumption for every one of these already exists in `LocalCache` (see
+ * `consumeBuzzTimelineEvent`); requesting them here is what lets them actually arrive for
+ * a group feed instead of only appearing if another subscription happened to fetch them.
*/
val BUZZ_RELAY_GROUP_TIMELINE_EXTRA_KINDS =
listOf(
@@ -104,6 +127,20 @@ val BUZZ_RELAY_GROUP_TIMELINE_EXTRA_KINDS =
StreamMessageEditEvent.KIND,
StreamMessageDiffEvent.KIND,
SystemMessageEvent.KIND,
+ CanvasEvent.KIND,
+ ForumPostEvent.KIND,
+ ForumVoteEvent.KIND,
+ ForumCommentEvent.KIND,
+ JobRequestEvent.KIND,
+ JobAcceptedEvent.KIND,
+ JobProgressEvent.KIND,
+ JobResultEvent.KIND,
+ JobCancelEvent.KIND,
+ JobErrorEvent.KIND,
+ HuddleStartedEvent.KIND,
+ HuddleParticipantJoinedEvent.KIND,
+ HuddleParticipantLeftEvent.KIND,
+ HuddleEndedEvent.KIND,
)
/** The timeline kinds requested for every relay-group REQ (NIP-29 + Buzz; see above). */
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index 8ffb313fe3..e2953d2978 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -3291,6 +3291,7 @@
%1$s created the channel
%1$s updated the channel profile
(edited)
+ (diff truncated)
Message Delivery
Close