mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(nip17): render subject-only renames as a system line instead of an empty bubble
A NIP-17 message that only sets a subject (no text) drew an empty bubble under the subject divider, whose only content was the time and delivery ticks. Render it as a centered system pill instead: the renamer's avatar, "Alice renamed the conversation" / "You renamed the conversation", and the tappable time with delivery ticks. Inside a reply quote, where no divider names the subject, the sentence includes the new name. ChatSystemMessage gains a trailing slot for the time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMJHLxbswamX91WDaPT2uR
This commit is contained in:
@@ -2323,6 +2323,10 @@
|
||||
<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_system_renamed_conversation">%1$s renamed the conversation</string>
|
||||
<string name="chat_system_renamed_conversation_you">You renamed the conversation</string>
|
||||
<string name="chat_system_renamed_conversation_to">%1$s renamed the conversation to %2$s</string>
|
||||
<string name="chat_system_renamed_conversation_to_you">You renamed the conversation to %1$s</string>
|
||||
<string name="buzz_message_edited">(edited)</string>
|
||||
<string name="buzz_diff_truncated">(diff truncated)</string>
|
||||
<string name="buzz_system_member_joined">%1$s joined</string>
|
||||
|
||||
+28
-5
@@ -54,12 +54,17 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText
|
||||
* [leading] is an optional slot rendered inside the pill, before the text — used to
|
||||
* put the avatar of whoever the line is about ("Alice joined") next to the sentence,
|
||||
* so a membership change is recognizable without reading the name.
|
||||
*
|
||||
* [trailing] is an optional slot rendered inside the pill, after the text — used by
|
||||
* system lines that are real messages (a NIP-17 rename) to keep their time and
|
||||
* delivery ticks without wrapping them in an otherwise empty bubble.
|
||||
*/
|
||||
@Composable
|
||||
fun ChatSystemMessage(
|
||||
text: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
leading: (@Composable () -> Unit)? = null,
|
||||
trailing: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
@@ -78,16 +83,27 @@ fun ChatSystemMessage(
|
||||
Modifier
|
||||
},
|
||||
) {
|
||||
if (leading == null) {
|
||||
if (leading == null && trailing == null) {
|
||||
SystemMessageText(text)
|
||||
} else {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
start = if (leading != null) 8.dp else 0.dp,
|
||||
end = if (trailing != null) 10.dp else 0.dp,
|
||||
),
|
||||
) {
|
||||
leading()
|
||||
SystemMessageText(text, startPadding = 0.dp)
|
||||
leading?.invoke()
|
||||
SystemMessageText(
|
||||
text = text,
|
||||
startPadding = if (leading != null) 0.dp else 12.dp,
|
||||
endPadding = if (trailing != null) 0.dp else 12.dp,
|
||||
// Lets a long sentence wrap instead of pushing the trailing slot out of the pill.
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
trailing?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,13 +114,15 @@ fun ChatSystemMessage(
|
||||
private fun SystemMessageText(
|
||||
text: String,
|
||||
startPadding: Dp = 12.dp,
|
||||
endPadding: Dp = 12.dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
fontSize = Font12SP,
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(start = startPadding, end = 12.dp, top = 5.dp, bottom = 5.dp),
|
||||
modifier = modifier.padding(start = startPadding, end = endPadding, top = 5.dp, bottom = 5.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,5 +136,10 @@ private fun ChatSystemMessagePreview() {
|
||||
"Bob was added by Alice",
|
||||
leading = { Box(Modifier.size(Size18dp).clip(CircleShape).background(MaterialTheme.colorScheme.primary)) },
|
||||
)
|
||||
ChatSystemMessage(
|
||||
"You renamed the conversation",
|
||||
leading = { Box(Modifier.size(Size18dp).clip(CircleShape).background(MaterialTheme.colorScheme.primary)) },
|
||||
trailing = { Text("10:42", fontSize = Font12SP, color = MaterialTheme.colorScheme.grayText) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.Re
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChannelAdminSystemMessage
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatClip
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatRaid
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatSubjectChange
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderChatZap
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderConcordTimerNotice
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent
|
||||
@@ -78,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.Re
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasMip04Media
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.isBuzzActivityRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.isSubjectOnlyChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowZapraiser
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
@@ -178,6 +180,9 @@ fun ChatroomMessageCompose(
|
||||
} else if (event is ConcordTimerNoticeEvent) {
|
||||
// Concord kind-1740: "Alice set disappearing messages to 30 days" (CORD-08 §4).
|
||||
RenderConcordTimerNotice(baseNote, accountViewModel, nav)
|
||||
} else if (isSubjectOnlyChatMessage(event)) {
|
||||
// NIP-17 rename with no text: a system line under the subject divider, not an empty bubble.
|
||||
RenderChatSubjectChange(baseNote, innerQuote, accountViewModel, nav)
|
||||
} 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.
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.commons.ui.screen.loggedIn.chats.feed.types
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatSystemMessage
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation_to
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation_to_you
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation_you
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatTimeWithDelivery
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size18dp
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
|
||||
/**
|
||||
* A NIP-17 message that only renames the conversation: it carries a `subject` tag and no text.
|
||||
* The room's subject divider already announces the new name above it, so drawing it as a bubble
|
||||
* left an empty balloon under the divider whose only content was the time.
|
||||
*/
|
||||
fun isSubjectOnlyChatMessage(event: Event?): Boolean = event is ChatMessageEvent && event.content.isBlank() && event.subject() != null
|
||||
|
||||
/**
|
||||
* A rename ([isSubjectOnlyChatMessage]) as a centered system line — "Alice renamed the
|
||||
* conversation" with Alice's avatar — under the subject divider that carries the new name.
|
||||
*
|
||||
* Unlike the other system lines this one is a real message with a sender and a delivery, so the
|
||||
* pill keeps the bubble's tappable time and, on our own renames, its relay-acceptance ticks.
|
||||
*
|
||||
* Inside a reply quote there is no divider to name the subject, so the sentence includes it and
|
||||
* the time is dropped, as on any quoted message.
|
||||
*/
|
||||
@Composable
|
||||
fun RenderChatSubjectChange(
|
||||
note: Note,
|
||||
innerQuote: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val event = note.event as? ChatMessageEvent ?: return
|
||||
val isLoggedInUser = remember(event.pubKey) { accountViewModel.isLoggedUser(event.pubKey) }
|
||||
|
||||
val text =
|
||||
if (innerQuote) {
|
||||
val subject = remember(event) { event.subject() ?: "" }
|
||||
if (isLoggedInUser) {
|
||||
stringRes(Res.string.chat_system_renamed_conversation_to_you, subject)
|
||||
} else {
|
||||
stringRes(Res.string.chat_system_renamed_conversation_to, observeUserNameByHex(event.pubKey, accountViewModel), subject)
|
||||
}
|
||||
} else {
|
||||
if (isLoggedInUser) {
|
||||
stringRes(Res.string.chat_system_renamed_conversation_you)
|
||||
} else {
|
||||
stringRes(Res.string.chat_system_renamed_conversation, observeUserNameByHex(event.pubKey, accountViewModel))
|
||||
}
|
||||
}
|
||||
|
||||
ChatSystemMessage(
|
||||
text = text,
|
||||
onClick = { nav.nav(Route.Profile(event.pubKey)) },
|
||||
leading = {
|
||||
UserPicture(
|
||||
userHex = event.pubKey,
|
||||
size = Size18dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
},
|
||||
trailing =
|
||||
if (innerQuote) {
|
||||
null
|
||||
} else {
|
||||
{ ChatTimeWithDelivery(note, isLoggedInUser, accountViewModel, nav) }
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user