fix(desktop): discoverable note moderation — right-click menu + ⋮ overflow

Manual testing showed the note moderation actions were hard to find:
- Provide LocalDesktopIAccount on MainContent's own (non-null) provider so every
  deck/feed/profile/settings surface reliably resolves the account.
- Add a right-click ContextMenuArea on feed notes: Mute user / Report… / Copy text
  (moderation items for other authors on a writeable account).
- Swap the note action-row Share icon for a MoreVert (⋮) overflow — the menu it
  opens already carries copy/broadcast + mute/report, matching the profile ⋮.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-07-24 11:51:51 +03:00
co-authored by Claude Opus 4.8
parent 2bad8f6a77
commit df442bf7b5
3 changed files with 62 additions and 20 deletions
@@ -2051,6 +2051,7 @@ fun MainContent(
CompositionLocalProvider(
LocalRelayCategories provides relayCategories,
LocalBlossomServers provides iAccount.blossomServerList.flow,
com.vitorpamplona.amethyst.desktop.model.LocalDesktopIAccount provides iAccount,
com.vitorpamplona.amethyst.desktop.ui.relay.LocalAccountRelays provides accountRelays,
com.vitorpamplona.amethyst.desktop.ui.deck.LocalDesktopCache provides localCache,
com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager provides relayManager,
@@ -28,6 +28,8 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipArea
import androidx.compose.foundation.background
@@ -202,32 +204,71 @@ fun FeedNoteCard(
forceReveal: Boolean = false,
) {
val event = note.event ?: return
val moderationAccount = com.vitorpamplona.amethyst.desktop.model.LocalDesktopIAccount.current
val ctxScope = rememberCoroutineScope()
var showReportDialog by remember(note.idHex) { mutableStateOf(false) }
val canModerate = moderationAccount != null && moderationAccount.isWriteable() && event.pubKey != moderationAccount.pubKey
SpamCheckedNoteRender(
note = note,
localCache = localCache,
forceReveal = forceReveal,
) {
FeedNoteCardBody(
note = note,
event = event,
relayManager = relayManager,
localCache = localCache,
account = account,
nwcConnection = nwcConnection,
onReply = onReply,
onZapFeedback = onZapFeedback,
onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onImageClick = onImageClick,
onMediaClick = onMediaClick,
onHashtagClick = onHashtagClick,
followedUsers = followedUsers,
myPubKeyHex = myPubKeyHex,
onFollow = onFollow,
// Right-click context menu (Compose Desktop). Mute/Report are only added
// for other authors on a writeable account; Copy is always available.
ContextMenuArea(items = {
buildList {
add(ContextMenuItem("Copy text") { copyTextToClipboard(event.content) })
if (canModerate && moderationAccount != null) {
add(ContextMenuItem("Mute user") { ctxScope.launch { moderationAccount.hideUser(event.pubKey) } })
add(ContextMenuItem("Report…") { showReportDialog = true })
}
}
}) {
FeedNoteCardBody(
note = note,
event = event,
relayManager = relayManager,
localCache = localCache,
account = account,
nwcConnection = nwcConnection,
onReply = onReply,
onZapFeedback = onZapFeedback,
onNavigateToProfile = onNavigateToProfile,
onNavigateToThread = onNavigateToThread,
onImageClick = onImageClick,
onMediaClick = onMediaClick,
onHashtagClick = onHashtagClick,
followedUsers = followedUsers,
myPubKeyHex = myPubKeyHex,
onFollow = onFollow,
)
}
}
if (showReportDialog && moderationAccount != null) {
com.vitorpamplona.amethyst.desktop.ui.note.ReportNoteDialog(
onDismiss = { showReportDialog = false },
onReport = { type, comment ->
ctxScope.launch { moderationAccount.reportEvent(event, type, comment) }
},
onBlockAndReport = { type, comment ->
ctxScope.launch {
moderationAccount.reportEvent(event, type, comment)
moderationAccount.hideUser(event.pubKey)
}
},
)
}
}
private fun copyTextToClipboard(text: String) {
java.awt.Toolkit
.getDefaultToolkit()
.systemClipboard
.setContents(java.awt.datatransfer.StringSelection(text), null)
}
@Composable
private fun FeedNoteCardBody(
note: Note,
@@ -1311,7 +1311,7 @@ fun NoteActionsRow(
)
}
// Share menu
// Overflow menu: copy / broadcast / share links + mute / report.
val shareMenuState = rememberShareMenuState()
Box {
IconButton(
@@ -1319,8 +1319,8 @@ fun NoteActionsRow(
modifier = Modifier.size(32.dp),
) {
Icon(
MaterialSymbols.Share,
contentDescription = "Share",
MaterialSymbols.MoreVert,
contentDescription = "More actions",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp),
)