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