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 2ae14f5087..f7df0334d4 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
@@ -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)
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatSystemMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatSystemMessage.kt
new file mode 100644
index 0000000000..79fbfd5f12
--- /dev/null
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatSystemMessage.kt
@@ -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")
+ }
+}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt
new file mode 100644
index 0000000000..b483f436aa
--- /dev/null
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChannelAdminNote.kt
@@ -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()
+}
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index fe644627bb..c10d2fc861 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -2818,6 +2818,10 @@
Add content warning
Remove content warning
+ %1$s created the channel %2$s
+ %1$s created the channel
+ %1$s updated the channel profile
+
Waiting for a relay to accept this message
Accepted by at least one relay
Delivered to all recipients\' relays