mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
feat: render channel admin events as centered system messages
NIP-28 channel create (kind 40) and metadata update (kind 41) events narrate the room rather than talk in it. They now render as a centered, muted system pill - 'X created the channel Y' / 'X updated the channel profile' - that taps through to the channel, instead of a full channel profile card inside a regular user bubble (chat design best practice: system messages should be visually distinct from user messages). The old card renderers (RenderCreateChannelNote / RenderChannelData) stay in the tree but are no longer referenced by the chat feed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
This commit is contained in:
+4
-9
@@ -46,7 +46,6 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.LocalInlineQuoteRenderer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayDraftChat
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
@@ -62,11 +61,10 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW
|
||||
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.RenderChangeChannelMetadataNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChannelAdminSystemMessage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatClip
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatRaid
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderCreateChannelNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEncryptedFile
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderMarmotEncryptedMedia
|
||||
@@ -138,6 +136,8 @@ fun ChatroomMessageCompose(
|
||||
RenderChatRaid(baseNote, accountViewModel, nav)
|
||||
} else if (event is LiveActivitiesClipEvent) {
|
||||
RenderChatClip(baseNote, accountViewModel, nav)
|
||||
} else if (event is ChannelCreateEvent || event is ChannelMetadataEvent) {
|
||||
RenderChannelAdminSystemMessage(baseNote, accountViewModel, nav)
|
||||
} else {
|
||||
NormalChatNote(
|
||||
baseNote,
|
||||
@@ -227,10 +227,7 @@ fun NormalChatNote(
|
||||
shouldHighlight = shouldHighlight,
|
||||
onHighlightFinished = onHighlightFinished,
|
||||
onClick = {
|
||||
if (note.event is ChannelCreateEvent) {
|
||||
nav.nav(Route.PublicChatChannel(note.idHex))
|
||||
true
|
||||
} else if (innerQuote && onScrollToNote != null) {
|
||||
if (innerQuote && onScrollToNote != null) {
|
||||
onScrollToNote(note)
|
||||
true
|
||||
} else {
|
||||
@@ -516,8 +513,6 @@ fun NoteRow(
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
when {
|
||||
note.event is ChannelCreateEvent -> RenderCreateChannelNote(note, bgColor, accountViewModel, nav)
|
||||
note.event is ChannelMetadataEvent -> RenderChangeChannelMetadataNote(note, bgColor, accountViewModel, nav)
|
||||
note.event is DraftWrapEvent -> RenderDraftEvent(note, canPreview, innerQuote, onWantsToReply, onWantsToEditDraft, bgColor, accountViewModel, nav)
|
||||
note.event is ChatMessageEncryptedFileHeaderEvent -> RenderEncryptedFile(note, bgColor, accountViewModel, nav)
|
||||
hasMip04Media(note.event) -> RenderMarmotEncryptedMedia(note, bgColor, accountViewModel, nav)
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.chats.feed.layouts
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
|
||||
/**
|
||||
* A centered, muted system line for events that narrate the room rather than talk
|
||||
* in it (channel created, profile updated, ...). Visually distinct from user
|
||||
* bubbles: no author row, no tail, one small pill in the middle of the feed.
|
||||
*/
|
||||
@Composable
|
||||
fun ChatSystemMessage(
|
||||
text: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
if (onClick != null) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
shape = ButtonBorder,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
SystemMessageText(text)
|
||||
}
|
||||
} else {
|
||||
Surface(
|
||||
shape = ButtonBorder,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
SystemMessageText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SystemMessageText(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = Font12SP,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 5.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ChatSystemMessagePreview() {
|
||||
ThemeComparisonColumn {
|
||||
ChatSystemMessage("Alice created the channel Amethyst Users", onClick = {})
|
||||
ChatSystemMessage("Alice updated the channel profile")
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.chats.feed.types
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
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.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
|
||||
/**
|
||||
* Channel admin events (NIP-28 kind 40/41) narrate the room instead of talking in
|
||||
* it, so they render as a centered system line — "X created the channel", "X
|
||||
* updated the channel profile" — that taps through to the channel, instead of a
|
||||
* full profile card inside a user bubble.
|
||||
*/
|
||||
@Composable
|
||||
fun RenderChannelAdminSystemMessage(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
when (val noteEvent = note.event) {
|
||||
is ChannelCreateEvent -> {
|
||||
val authorName = watchAuthorName(note, accountViewModel)
|
||||
val channelName = remember(noteEvent) { noteEvent.channelInfo().name }
|
||||
|
||||
ChatSystemMessage(
|
||||
text =
|
||||
if (channelName.isNullOrBlank()) {
|
||||
stringRes(R.string.chat_system_created_channel_unnamed, authorName)
|
||||
} else {
|
||||
stringRes(R.string.chat_system_created_channel, authorName, channelName)
|
||||
},
|
||||
onClick = { nav.nav(Route.PublicChatChannel(note.idHex)) },
|
||||
)
|
||||
}
|
||||
|
||||
is ChannelMetadataEvent -> {
|
||||
val authorName = watchAuthorName(note, accountViewModel)
|
||||
val channelId = remember(noteEvent) { noteEvent.channelId() }
|
||||
|
||||
ChatSystemMessage(
|
||||
text = stringRes(R.string.chat_system_updated_channel, authorName),
|
||||
onClick = channelId?.let { { nav.nav(Route.PublicChatChannel(it)) } },
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun watchAuthorName(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
): String {
|
||||
val author = note.author ?: return note.event?.pubKey?.take(8) ?: ""
|
||||
val userState by observeUserInfo(author, accountViewModel)
|
||||
return userState?.info?.bestName() ?: author.pubkeyDisplayHex()
|
||||
}
|
||||
@@ -2818,6 +2818,10 @@
|
||||
<string name="add_content_warning">Add content warning</string>
|
||||
<string name="remove_content_warning">Remove content warning</string>
|
||||
|
||||
<string name="chat_system_created_channel">%1$s created the channel %2$s</string>
|
||||
<string name="chat_system_created_channel_unnamed">%1$s created the channel</string>
|
||||
<string name="chat_system_updated_channel">%1$s updated the channel profile</string>
|
||||
|
||||
<string name="chat_delivery_pending">Waiting for a relay to accept this message</string>
|
||||
<string name="chat_delivery_accepted">Accepted by at least one relay</string>
|
||||
<string name="chat_delivery_delivered_all">Delivered to all recipients\' relays</string>
|
||||
|
||||
Reference in New Issue
Block a user