mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
feat(buzz): render forum posts as threads, not chat bubbles
Buzz forum roots (kind 45001) and comments (45003) were consumed onto the chat timeline (consumeBuzzTimelineEvent → addNote), so a forum post showed up as a kind-9 chat bubble and replies leaked into the chat feed. Route them like the content they are: - 45001 → the group's Threads collection (addThread) via a new consumeBuzzForumPost, so it surfaces in the forum/Threads view; 45003/45002 are now store-only. - ThreadRow renders a ForumPostEvent (body-as-heading, no title) alongside kind-11. - The Threads REQ (RELAY_GROUP_THREAD_KINDS) now also fetches 45001/45003. - A forum-type channel (channel_type=forum) opens the Threads view directly from the community Forums section instead of the chat view. - New BuzzForumThreadScreen: the root post + its 45003 replies + a reply composer (ForumCommentEvent.build). Own replies aren't re-delivered on the open sub, so a send restarts the REQ to re-fetch the thread. Verified on device: forum posts list as threads, open to root+replies, replies post and appear; the forum channel's chat no longer shows the posts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
681bd1b389
commit
dbf9b29e25
@@ -2184,6 +2184,22 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
markBuzzIfVerified(event, relay)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Buzz forum ROOT post (kind 45001): a thread, not a chat message. Route it to the group's
|
||||
* Threads collection ([RelayGroupChannel.addThread]) — the same place kind-11 threads go — so it
|
||||
* surfaces in the forum/Threads view instead of leaking into the kind-9 chat feed as a bubble.
|
||||
* Its kind-45003 comments are stored (store-only) and loaded on demand by the thread detail.
|
||||
*/
|
||||
private fun consumeBuzzForumPost(
|
||||
event: Event,
|
||||
relay: NormalizedRelayUrl?,
|
||||
wasVerified: Boolean,
|
||||
): Boolean =
|
||||
consumeRegularEvent(event, relay, wasVerified).also {
|
||||
markBuzzIfVerified(event, relay)
|
||||
attachThreadToRelayGroupIfScoped(event, relay)
|
||||
}
|
||||
|
||||
private fun consume(
|
||||
event: StreamMessageEditEvent,
|
||||
relay: NormalizedRelayUrl?,
|
||||
@@ -4673,9 +4689,11 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
is StreamMessageDiffEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is SystemMessageEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is CanvasEvent -> consume(event, relay, wasVerified)
|
||||
is ForumPostEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is ForumCommentEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is ForumVoteEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
// Forum root (45001) is a thread, not a chat row → Threads collection. Comments (45003)
|
||||
// and votes (45002) are store-only: the forum-thread detail loads them on demand by root.
|
||||
is ForumPostEvent -> consumeBuzzForumPost(event, relay, wasVerified)
|
||||
is ForumCommentEvent -> consumeBuzzRegularEvent(event, relay, wasVerified)
|
||||
is ForumVoteEvent -> consumeBuzzRegularEvent(event, relay, wasVerified)
|
||||
is JobRequestEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is JobAcceptedEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
is JobProgressEvent -> consumeBuzzTimelineEvent(event, relay, wasVerified)
|
||||
|
||||
@@ -105,6 +105,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.AgentPersonaEditScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzCanvasScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzDmListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzForumPostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzForumThreadScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzInviteScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzNewDmScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen
|
||||
@@ -769,6 +770,7 @@ fun BuildNavigation(
|
||||
}
|
||||
composableFromEndArgs<Route.BuzzCanvas> { BuzzCanvasScreen(it.channelId, accountViewModel, nav) }
|
||||
composableFromBottomArgs<Route.BuzzForumPost> { BuzzForumPostScreen(it.channelId, it.relayUrl, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.BuzzForumThread> { BuzzForumThreadScreen(it.channelId, it.relayUrl, it.rootId, accountViewModel, nav) }
|
||||
|
||||
composableFromEndArgs<Route.RelayGroupCreate> {
|
||||
RelayGroupCreateScreen(
|
||||
|
||||
@@ -711,6 +711,12 @@ sealed class Route {
|
||||
val relayUrl: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class BuzzForumThread(
|
||||
val channelId: String,
|
||||
val relayUrl: String,
|
||||
val rootId: String,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class RelayGroupCreate(
|
||||
val relayUrl: String,
|
||||
) : Route()
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent
|
||||
import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.subscribeAsFlow
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* The detail view of a Buzz **forum thread**: the root post (kind 45001) and its comment
|
||||
* replies (kind 45003), with an inline composer. Kept apart from the generic note thread
|
||||
* screen because Buzz forum comments use NIP-10 `e` tags (not the kind-1111 NIP-22 shape),
|
||||
* so they need their own root-scoped fetch + reply build ([ForumCommentEvent.build]).
|
||||
*/
|
||||
@Composable
|
||||
fun BuzzForumThreadScreen(
|
||||
channelId: String,
|
||||
relayUrl: String,
|
||||
rootId: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val relay = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } ?: return
|
||||
val rootNote = remember(rootId) { LocalCache.getOrCreateNote(rootId) }
|
||||
|
||||
// The replies to this root. Seed from cache (the forum-list REQ already pulls 45003 for the
|
||||
// channel), then live-subscribe by the root `e` tag so replies arrive while the thread is open.
|
||||
val replies = remember(rootId) { mutableStateListOf<ForumCommentEvent>() }
|
||||
val seen = remember(rootId) { HashSet<HexKey>() }
|
||||
val replyFilter =
|
||||
remember(rootId) {
|
||||
Filter(kinds = listOf(ForumCommentEvent.KIND), tags = mapOf("e" to listOf(rootId)))
|
||||
}
|
||||
|
||||
fun accept(event: ForumCommentEvent) {
|
||||
if (event.threadRoot() != rootId && event.replyTo() != rootId) return
|
||||
if (!seen.add(event.id)) return
|
||||
val at = replies.indexOfFirst { it.createdAt > event.createdAt }
|
||||
if (at < 0) replies.add(event) else replies.add(at, event)
|
||||
}
|
||||
|
||||
fun seedFromCache() = LocalCache.filter(replyFilter).forEach { (it.event as? ForumCommentEvent)?.let(::accept) }
|
||||
|
||||
// Bumped after we post a reply. The relay stores our reply but doesn't re-deliver it on our already-open
|
||||
// subscription, so a bump restarts the REQ — a fresh fetch returns every reply, including the new one.
|
||||
var refreshKey by remember(rootId) { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(rootId, refreshKey) {
|
||||
if (refreshKey > 0) delay(600) // let our just-sent reply persist on the relay before re-fetching
|
||||
seedFromCache()
|
||||
accountViewModel.account.client.subscribeAsFlow(relay, replyFilter).collect { events ->
|
||||
events.filterIsInstance<ForumCommentEvent>().forEach(::accept)
|
||||
}
|
||||
}
|
||||
|
||||
val canPost =
|
||||
remember(rootId) {
|
||||
LocalCache.getOrCreateRelayGroupChannel(GroupId(channelId, relay))
|
||||
}.canPost(accountViewModel.userProfile().pubkeyHex)
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopBarWithBackButton(stringRes(R.string.buzz_forum_thread_title), nav) },
|
||||
bottomBar = {
|
||||
if (canPost) {
|
||||
// The relay doesn't re-deliver our reply on the open subscription, so restart the REQ after
|
||||
// a send (see [refreshKey]) to re-fetch the thread including it.
|
||||
ForumReplyComposer(channelId, rootId, relay.url, accountViewModel, onSent = { refreshKey++ })
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
item(key = "root") {
|
||||
ForumPostBody(rootNote.event as? ForumPostEvent, rootNote.author?.pubkeyHex ?: rootId, accountViewModel, nav)
|
||||
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
items(replies, key = { it.id }) { reply ->
|
||||
ForumReplyRow(reply, accountViewModel, nav)
|
||||
HorizontalDivider(thickness = 0.25.dp, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ForumPostBody(
|
||||
event: ForumPostEvent?,
|
||||
authorPubkey: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val author = remember(authorPubkey) { LocalCache.getOrCreateUser(authorPubkey) }
|
||||
Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
UserPicture(author, Size35dp, accountViewModel = accountViewModel, nav = nav)
|
||||
UsernameDisplay(author, accountViewModel = accountViewModel, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
Text(text = event?.body().orEmpty(), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ForumReplyRow(
|
||||
reply: ForumCommentEvent,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val author = remember(reply.pubKey) { LocalCache.getOrCreateUser(reply.pubKey) }
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
UserPicture(author, Size35dp, accountViewModel = accountViewModel, nav = nav)
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
UsernameDisplay(author, accountViewModel = accountViewModel)
|
||||
Text(text = reply.body(), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ForumReplyComposer(
|
||||
channelId: String,
|
||||
rootId: HexKey,
|
||||
relayUrl: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onSent: () -> Unit,
|
||||
) {
|
||||
var body by remember { mutableStateOf("") }
|
||||
var isPosting by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Surface(tonalElevation = 2.dp) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = body,
|
||||
onValueChange = { body = it },
|
||||
placeholder = { Text(stringRes(R.string.buzz_forum_reply_hint)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = !isPosting,
|
||||
maxLines = 4,
|
||||
)
|
||||
IconButton(
|
||||
enabled = body.isNotBlank() && !isPosting,
|
||||
onClick = {
|
||||
val relay = RelayUrlNormalizer.normalizeOrNull(relayUrl) ?: return@IconButton
|
||||
val text = body.trim()
|
||||
isPosting = true
|
||||
scope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
accountViewModel.account.signAndSendPrivatelyOrBroadcast(
|
||||
// Direct reply to the root: rootEventId == parentEventId.
|
||||
ForumCommentEvent.build(channelId, text, rootEventId = rootId, parentEventId = rootId),
|
||||
) { listOf(relay) }
|
||||
}
|
||||
body = ""
|
||||
onSent()
|
||||
} finally {
|
||||
isPosting = false
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.Send,
|
||||
contentDescription = stringRes(R.string.buzz_forum_reply_action),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -344,7 +344,9 @@ fun RelayGroupChannelListScreen(
|
||||
isAdded = groupId.id in buzzAdded,
|
||||
onAdd = { buzzVm.add(groupId) },
|
||||
accountViewModel = accountViewModel,
|
||||
onOpen = { nav.nav(Route.RelayGroup(groupId.id, relay.url)) },
|
||||
// A forum channel's primary content is its threads (kind-45001 posts), not a
|
||||
// kind-9 chat, so open the forum/threads view directly instead of the chat.
|
||||
onOpen = { nav.nav(Route.RelayGroupThreads(groupId.id, relay.url)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+35
-11
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayG
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenThreadsSubscription
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
@@ -189,7 +190,16 @@ private fun RelayGroupThreads(
|
||||
if (index > 0) {
|
||||
HorizontalDivider(thickness = 0.25.dp, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
ThreadRow(thread, accountViewModel, nav) { nav.nav(Route.Note(thread.idHex)) }
|
||||
// A Buzz forum root (45001) opens the forum-thread detail (root + kind-45003 replies);
|
||||
// a NIP-29 kind-11 thread opens the generic note view with its kind-1111 comment tree.
|
||||
val open: () -> Unit = {
|
||||
if (thread.event is ForumPostEvent) {
|
||||
nav.nav(Route.BuzzForumThread(channel.groupId.id, channel.groupId.relayUrl.url, thread.idHex))
|
||||
} else {
|
||||
nav.nav(Route.Note(thread.idHex))
|
||||
}
|
||||
}
|
||||
ThreadRow(thread, accountViewModel, nav, open)
|
||||
}
|
||||
item(key = "threads-history-footer") {
|
||||
RelayGroupThreadsHistoryFooter(loadingOlder, status.exhausted)
|
||||
@@ -265,17 +275,31 @@ private fun ThreadRow(
|
||||
nav: INav,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
// Observe the reply count so a kind-1111 comment arriving on an already-listed thread
|
||||
// bumps it live (channel.threads only re-emits on add/remove of a thread).
|
||||
// Observe the reply count so a comment arriving on an already-listed thread bumps it live
|
||||
// (channel.threads only re-emits on add/remove of a thread).
|
||||
val replyCount by observeNoteReplyCount(thread, accountViewModel)
|
||||
val event = thread.event as? ThreadEvent
|
||||
val title = event?.title()?.takeIf { it.isNotBlank() } ?: stringRes(R.string.relay_group_thread_untitled)
|
||||
val preview =
|
||||
event
|
||||
?.content
|
||||
?.replace('\n', ' ')
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
val untitled = stringRes(R.string.relay_group_thread_untitled)
|
||||
// Two thread shapes share this list: NIP-29 kind-11 threads (title + body) and Buzz forum
|
||||
// roots (kind 45001, body only — no title). For a forum post, surface the first line as the
|
||||
// heading and the rest as the preview so it reads like a titled thread.
|
||||
val title: String
|
||||
val preview: String
|
||||
when (val event = thread.event) {
|
||||
is ThreadEvent -> {
|
||||
title = event.title()?.takeIf { it.isNotBlank() } ?: untitled
|
||||
preview = event.content.replace('\n', ' ').trim()
|
||||
}
|
||||
is ForumPostEvent -> {
|
||||
val body = event.body().trim()
|
||||
val firstLine = body.substringBefore('\n').trim()
|
||||
title = firstLine.ifEmpty { untitled }
|
||||
preview = body.removePrefix(firstLine).replace('\n', ' ').trim()
|
||||
}
|
||||
else -> {
|
||||
title = untitled
|
||||
preview = ""
|
||||
}
|
||||
}
|
||||
val author = thread.author
|
||||
|
||||
Row(
|
||||
|
||||
+6
-2
@@ -156,8 +156,12 @@ val RELAY_GROUP_ALL_TIMELINE_KINDS = RELAY_GROUP_TIMELINE_KINDS + BUZZ_RELAY_GRO
|
||||
*/
|
||||
val RELAY_GROUP_OPEN_TAIL_KINDS = RELAY_GROUP_ALL_TIMELINE_KINDS + TypingIndicatorEvent.KIND
|
||||
|
||||
/** Forum-thread kinds shown in a group's Threads tab. */
|
||||
val RELAY_GROUP_THREAD_KINDS = listOf(ThreadEvent.KIND, CommentEvent.KIND)
|
||||
/**
|
||||
* Forum-thread kinds shown in a group's Threads tab: NIP-29 kind-11 roots + kind-1111 comments, PLUS
|
||||
* Buzz forum roots (45001) + comments (45003). Requested together (a vanilla relay matches nothing on
|
||||
* the Buzz kinds, a Buzz relay nothing on kind-11), so the same Threads REQ surfaces either dialect.
|
||||
*/
|
||||
val RELAY_GROUP_THREAD_KINDS = listOf(ThreadEvent.KIND, CommentEvent.KIND, ForumPostEvent.KIND, ForumCommentEvent.KIND)
|
||||
|
||||
/** Content kinds a card warms ahead of a tap (chat + polls + threads + comments). */
|
||||
val RELAY_GROUP_CARD_WARMUP_KINDS = listOf(ChatEvent.KIND, PollEvent.KIND, ThreadEvent.KIND, CommentEvent.KIND)
|
||||
|
||||
@@ -3322,6 +3322,9 @@
|
||||
<string name="buzz_forum_new_title">New forum topic</string>
|
||||
<string name="buzz_forum_body_label">What do you want to discuss?</string>
|
||||
<string name="buzz_forum_post_action">Post topic</string>
|
||||
<string name="buzz_forum_thread_title">Forum thread</string>
|
||||
<string name="buzz_forum_reply_hint">Write a reply…</string>
|
||||
<string name="buzz_forum_reply_action">Send reply</string>
|
||||
<string name="buzz_dm_title">Direct Messages</string>
|
||||
<string name="buzz_dm_nav_label">Buzz DMs</string>
|
||||
<string name="buzz_dm_card_subtitle">Private conversations on your workspaces</string>
|
||||
|
||||
+6
-1
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
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.nip01Core.relay.client.pool.FiltersChanged
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
@@ -60,7 +62,10 @@ class RelayGroupFilterBuildersTest {
|
||||
private val g1OnA = GroupId("g1", relayA)
|
||||
|
||||
private val timelineKinds = listOf(ChatEvent.KIND, PollEvent.KIND)
|
||||
private val threadKinds = listOf(ThreadEvent.KIND, CommentEvent.KIND)
|
||||
|
||||
// NIP-29 kind-11/1111 threads + Buzz forum roots (45001) & comments (45003) — the Threads REQ
|
||||
// fetches both dialects (a vanilla relay matches nothing on the Buzz kinds and vice versa).
|
||||
private val threadKinds = listOf(ThreadEvent.KIND, CommentEvent.KIND, ForumPostEvent.KIND, ForumCommentEvent.KIND)
|
||||
|
||||
// --- State (always-on): one #d filter per host relay, batching that relay's group ids ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user