From 08b3cf266b4491e7d8c2529476b56a8accd6fab3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 22:23:13 +0000 Subject: [PATCH] fix: draw the "added you to a channel" prompt as a note row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The invite prompt was a floating Material card with a surfaceVariant fill, sitting above the Notifications feed and Messages > New Requests as its own visual language — nothing else on either surface looks like that. Render it through NoteComposeLayout instead, the same layout every note and notification card uses: the actor becomes the row's author (picture, name and time in the usual header), "Added to " plus the host relay become the row's content, and the three choices take the reactions slot. So the prompt now reads like the reply and mention notifications it sits next to, and a divider closes each row the way the feed does. The channel name is now read through observeChannel, the metadata-only observer the channel rows use, so a stranger's add resolves to a name instead of a raw group id. It does not open the channel's message subscription — holding that back until the viewer answers is the whole point of the prompt. Also relabel "Show" to "Add to Messages". acceptChannelInvite is defined as `= addRelayGroupToMessages(channel)`, literally the call behind the channel top bar's "Add to Messages" item, so one action had two words for it. Reusing the existing string drops channel_invite_accept and channel_invite_body (the actor and the question both live in the row now). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LGB1z4VcNfDw5cMt9V5NCG --- .../chats/rooms/feed/ChatroomListTabs.kt | 2 +- .../notifications/ChannelInvitesSection.kt | 128 +++++++++++++----- .../notifications/NotificationScreen.kt | 2 +- amethyst/src/main/res/values-cs/strings.xml | 2 - .../src/main/res/values-de-rDE/strings.xml | 2 - .../src/main/res/values-hi-rIN/strings.xml | 2 - .../src/main/res/values-nl-rNL/strings.xml | 2 - .../src/main/res/values-pl-rPL/strings.xml | 2 - .../src/main/res/values-pt-rBR/strings.xml | 2 - .../src/main/res/values-sl-rSI/strings.xml | 2 - .../src/main/res/values-sv-rSE/strings.xml | 2 - amethyst/src/main/res/values/strings.xml | 2 - 12 files changed, 99 insertions(+), 51 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index fcd6310c2b..1555ab0bc6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -139,7 +139,7 @@ fun MessagesPager( // same state holder, so the two surfaces cannot disagree. headerContent = if (tabs[page].resource == R.string.new_requests) { - { ChannelInvitesSection(accountViewModel) } + { ChannelInvitesSection(accountViewModel, nav) } } else { null }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/ChannelInvitesSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/ChannelInvitesSection.kt index caf6d6c1bb..68c1ab9b9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/ChannelInvitesSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/ChannelInvitesSection.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -35,15 +35,32 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelInvite +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel +import com.vitorpamplona.amethyst.ui.layouts.NoteComposeLayout +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.DisplayBlankAuthor +import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size55Modifier +import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.UserNameRowHeight +import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor +import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @@ -60,6 +77,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @Composable fun ChannelInvitesSection( accountViewModel: AccountViewModel, + nav: INav, modifier: Modifier = Modifier, ) { val invites by accountViewModel.feedStates.channelInvites.flow @@ -69,48 +87,94 @@ fun ChannelInvitesSection( Column(modifier) { invites.forEach { invite -> - ChannelInviteCard(invite, accountViewModel) + ChannelInviteCard(invite, accountViewModel, nav) + HorizontalDivider(thickness = DividerThickness) } } } +/** + * One pending add, drawn as a feed row instead of a floating Material card: the actor is the row's + * author — picture, name and time in the usual note header — and "added you to X" is the row's content, + * so the prompt reads like the reply/mention notifications it sits next to. The three choices take the + * reactions slot, which spans the full width and therefore fits "Add to Messages" without wrapping. + */ @Composable fun ChannelInviteCard( invite: BuzzChannelInvite, accountViewModel: AccountViewModel, + nav: INav, ) { - val channel = remember(invite.channelId, invite.relay) { LocalCache.getOrCreateRelayGroupChannel(GroupId(invite.channelId, invite.relay)) } + val baseChannel = + remember(invite.channelId, invite.relay) { + LocalCache.getOrCreateRelayGroupChannel(GroupId(invite.channelId, invite.relay)) + } + + // Metadata only — the same observer the channel rows use. It resolves the name a stranger's add + // arrives without, and never opens the channel's message subscription (which is the whole point of + // holding the invite back until the viewer answers). + val channelState by observeChannel(baseChannel, accountViewModel) + val channel = channelState?.channel as? RelayGroupChannel ?: baseChannel val actorUser = remember(invite.actor) { invite.actor?.let { LocalCache.getOrCreateUser(it) } } - val actorName = actorUser?.let { observeUserName(it, accountViewModel).value } - Card( - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), - modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 5.dp), - ) { - Column(Modifier.padding(12.dp)) { - Text( - text = stringRes(R.string.channel_invite_title, channel.toBestDisplayName()), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold, - ) - Text( + // A pending invite is by definition unanswered, so it always carries the new-item wash rather than + // fading with a last-read marker: it is a standing question, not a dated event. + val backgroundColor = + MaterialTheme.colorScheme.newItemBackgroundColor + .compositeOver(MaterialTheme.colorScheme.background) + + NoteComposeLayout( + modifier = Modifier.drawBehind { drawRect(backgroundColor) }.fillMaxWidth(), + authorPicture = { + Box(Size55Modifier, contentAlignment = Alignment.BottomEnd) { + if (actorUser != null) { + UserPicture(actorUser, Size55dp, accountViewModel = accountViewModel, nav = nav) + } else { + DisplayBlankAuthor(Size55dp, accountViewModel = accountViewModel) + } + } + }, + firstRow = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Size5dp), + modifier = UserNameRowHeight, + ) { // Who did it matters: the relay reports a self-join with the same event, so naming the // actor is what tells "I joined this" apart from "a stranger put me here". - text = - stringRes( - R.string.channel_invite_body, - actorName ?: stringRes(R.string.channel_invite_unknown_actor), - invite.relay.displayUrl(), - ), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 4.dp), - ) + if (actorUser != null) { + UsernameDisplay(actorUser, Modifier.weight(1f), accountViewModel = accountViewModel) + } else { + Text( + text = stringRes(R.string.channel_invite_unknown_actor), + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + TimeAgo(invite.createdAt) + } + }, + secondRow = {}, + noteContent = { + Text(text = stringRes(R.string.channel_invite_title, channel.toBestDisplayName())) + + Text( + text = invite.relay.displayUrl(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + reactionsRow = { Row( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(top = 6.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = Size10dp), ) { // Leave is separate from Ignore on purpose: Ignore is a local display choice that leaves // you in the roster, Leave is the kind-9022 that actually removes you from the channel. @@ -120,10 +184,12 @@ fun ChannelInviteCard( TextButton(onClick = { accountViewModel.dismissChannelInvite(invite.channelId) }) { Text(stringRes(R.string.channel_invite_ignore)) } + // Accepting *is* `addRelayGroupToMessages`, the same call behind the channel top bar's + // "Add to Messages", so it carries that label rather than a second word for one action. TextButton(onClick = { accountViewModel.acceptChannelInvite(channel) }) { - Text(stringRes(R.string.channel_invite_accept), fontWeight = FontWeight.Bold) + Text(stringRes(R.string.add_to_messages), fontWeight = FontWeight.Bold) } } - } - } + }, + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index 627984e45c..d2882fe543 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -246,7 +246,7 @@ internal fun SingleNotificationsBody( ObserveInboxRelayListAndDisplayIfNotFound(accountViewModel, nav) // "X added you to #channel" prompts sit above the feed rather than inside it: they are a // standing decision, not a dated event, so they must not scroll away into history. - ChannelInvitesSection(accountViewModel) + ChannelInvitesSection(accountViewModel, nav) }, ) } diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 9f2709970e..21594a1f84 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -2563,9 +2563,7 @@ Oblíbené relaye Zatím žádné zprávy Přidáno do %1$s - %1$s vás přidal do tohoto kanálu na %2$s. Zobrazit ho ve Zprávách? Někdo - Zobrazit Ignorovat Opustit Připojte se k této skupině pro odesílání zpráv. diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 7f06367211..4435dd9629 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2472,9 +2472,7 @@ Beliebte Relays Noch keine Nachrichten Zu %1$s hinzugefügt - %1$s hat dich auf %2$s zu diesem Kanal hinzugefügt. In Nachrichten anzeigen? Jemand - Anzeigen Ignorieren Verlassen Tritt dieser Gruppe bei, um Nachrichten zu senden. diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 43264dc3d3..d6a4b6334f 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -2473,9 +2473,7 @@ लोकप्रिय पुनःप्रसारक कोई सन्देश नहीं अब तक %1$s में जोडा गया - %1$s ने आपको इस प्रणाली में जोडा %2$s पर। क्या इसे सन्देशों में दिखाया जाए। कोई व्यक्ति - दिखाएँ उपेक्षा करें छोडें इस समूह से जुडें सन्देशों को भेजने के लिए। diff --git a/amethyst/src/main/res/values-nl-rNL/strings.xml b/amethyst/src/main/res/values-nl-rNL/strings.xml index 389eae2ccd..22a28884e1 100644 --- a/amethyst/src/main/res/values-nl-rNL/strings.xml +++ b/amethyst/src/main/res/values-nl-rNL/strings.xml @@ -2467,9 +2467,7 @@ Populaire relays Nog geen berichten Toegevoegd aan %1$s - %1$s heeft je toegevoegd aan dit kanaal op %2$s. In Berichten tonen? Iemand - Tonen Negeren Verlaten Neem deel aan deze groep om berichten te sturen. diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 50ee2469df..98950f5e02 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -2565,9 +2565,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Popularne transmitery Brak wiadomości Dodano do %1$s - %1$s dodał Cię do tego kanału na %2$s. Pokazać go w wiadomościach? Ktoś - Pokaż Ignoruj Wyjdź Dołącz do tej grupy, aby wysyłać wiadomości. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index c2c6fa8353..26efddbc9d 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2473,9 +2473,7 @@ Relays populares Nenhuma mensagem ainda Adicionado a %1$s - %1$s adicionou você a este canal em %2$s. Mostrar em Mensagens? Alguém - Mostrar Ignorar Sair Entre neste grupo para enviar mensagens. diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index 5265d4ba5a..23c0485bed 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -2544,9 +2544,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Popularni releji Ni sporočil Dodano v: %1$s - %1$s vas je dodal v ta kanal na %2$s. Želite to prikazati med sporočili? Nekdo - Prikaži Prezri Zapusti Pridružite se tej skupini, da boste lahko pošiljali sporočila. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 6f5ee3cbd0..0adb501ce0 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2475,9 +2475,7 @@ Populära reläer Inga meddelanden ännu Tillagd i %1$s - %1$s lade till dig i den här kanalen på %2$s. Visa den i Meddelanden? Någon - Visa Ignorera Lämna Gå med i den här gruppen för att skicka meddelanden. diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 53b93fb4a1..d5cf87e6a4 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2702,9 +2702,7 @@ Popular relays No messages yet Added to %1$s - %1$s added you to this channel on %2$s. Show it in Messages? Someone - Show Ignore Leave Join this group to send messages.