mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
feat(desktop): mute + NIP-56 report actions in the note menu
Surface the account-layer moderation write path in the UI: - LocalDesktopIAccount CompositionLocal (provided at the app content root) so deeply-nested note surfaces can reach mute/block/report without prop-drilling. - ShareMenu gains 'Mute user' + 'Report…' items (writeable accounts only). - ReportNoteDialog: NIP-56 reason picker + optional comment, with 'Report' and 'Block & report' actions. - DesktopIAccount.reportEvent(Event, …) overload for the menu (report(Note) delegates). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ac2875025a
commit
d571906dd0
@@ -1422,6 +1422,7 @@ private fun AppInner(
|
||||
LocalNamecoinPreferences provides namecoinPreferences,
|
||||
LocalNamecoinService provides namecoinService,
|
||||
LocalSpamExemptKeys provides spamExemptKeys,
|
||||
com.vitorpamplona.amethyst.desktop.model.LocalDesktopIAccount provides iAccount,
|
||||
) {
|
||||
val pendingAuthApprovals by authCoordinator.pendingApprovals.collectAsState()
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
+12
-2
@@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.ui.chats.DmSendTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
@@ -421,9 +422,18 @@ class DesktopIAccount(
|
||||
type: ReportType,
|
||||
comment: String = "",
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
val reported = note.event ?: return
|
||||
val signed = signer.sign(ReportEvent.build(reported, type, comment))
|
||||
reportEvent(reported, type, comment)
|
||||
}
|
||||
|
||||
/** Publish a NIP-56 (kind 1984) report about a raw event. */
|
||||
suspend fun reportEvent(
|
||||
reportedEvent: Event,
|
||||
type: ReportType,
|
||||
comment: String = "",
|
||||
) {
|
||||
if (!isWriteable()) return
|
||||
val signed = signer.sign(ReportEvent.build(reportedEvent, type, comment))
|
||||
relayManager.broadcastToAll(signed)
|
||||
}
|
||||
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.desktop.model
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
/**
|
||||
* The logged-in [DesktopIAccount], provided at the app content root so deeply
|
||||
* nested note surfaces (context menus, profile actions) can reach account-level
|
||||
* moderation actions (mute/block/report) without threading it through every
|
||||
* composable. Null when logged out or in a preview.
|
||||
*/
|
||||
val LocalDesktopIAccount = staticCompositionLocalOf<DesktopIAccount?> { null }
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.desktop.ui.note
|
||||
|
||||
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.foundation.selection.selectable
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
|
||||
private val REPORT_OPTIONS =
|
||||
listOf(
|
||||
ReportType.SPAM to "Spam",
|
||||
ReportType.PROFANITY to "Profanity / Hateful speech",
|
||||
ReportType.IMPERSONATION to "Impersonation",
|
||||
ReportType.NUDITY to "Nudity / Sexual content",
|
||||
ReportType.ILLEGAL to "Illegal content",
|
||||
ReportType.MALWARE to "Malware / Phishing",
|
||||
)
|
||||
|
||||
/**
|
||||
* NIP-56 report dialog. Lets the user pick a report reason and optionally add a
|
||||
* comment, then either just report or report-and-block the author. Mirrors the
|
||||
* Android `ReportNoteDialog`, adapted to Compose Desktop.
|
||||
*/
|
||||
@Composable
|
||||
fun ReportNoteDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onReport: (ReportType, String) -> Unit,
|
||||
onBlockAndReport: (ReportType, String) -> Unit,
|
||||
) {
|
||||
var selected by remember { mutableStateOf(REPORT_OPTIONS.first().first) }
|
||||
var comment by remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Report note") },
|
||||
text = {
|
||||
Column {
|
||||
REPORT_OPTIONS.forEach { (type, label) ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(selected = selected == type, onClick = { selected = type })
|
||||
.padding(vertical = 2.dp),
|
||||
) {
|
||||
RadioButton(selected = selected == type, onClick = { selected = type })
|
||||
Text(label, modifier = Modifier.padding(start = 4.dp))
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = comment,
|
||||
onValueChange = { comment = it },
|
||||
label = { Text("Comment (optional)") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onReport(selected, comment)
|
||||
onDismiss()
|
||||
}) { Text("Report") }
|
||||
},
|
||||
dismissButton = {
|
||||
Row {
|
||||
TextButton(onClick = {
|
||||
onBlockAndReport(selected, comment)
|
||||
onDismiss()
|
||||
}) { Text("Block & report") }
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -28,11 +28,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.desktop.model.LocalDesktopIAccount
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import kotlinx.coroutines.launch
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
|
||||
@@ -58,6 +61,10 @@ fun ShareMenu(
|
||||
event: Event,
|
||||
relayManager: DesktopRelayConnectionManager,
|
||||
) {
|
||||
val account = LocalDesktopIAccount.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var showReportDialog by remember { mutableStateOf(false) }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = state.expanded,
|
||||
onDismissRequest = { state.dismiss() },
|
||||
@@ -107,6 +114,40 @@ fun ShareMenu(
|
||||
state.dismiss()
|
||||
},
|
||||
)
|
||||
|
||||
// Moderation actions require a writeable account (a local signer).
|
||||
if (account != null && account.isWriteable()) {
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Mute user") },
|
||||
onClick = {
|
||||
scope.launch { account.hideUser(event.pubKey) }
|
||||
state.dismiss()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Report…") },
|
||||
onClick = {
|
||||
showReportDialog = true
|
||||
state.dismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showReportDialog && account != null) {
|
||||
ReportNoteDialog(
|
||||
onDismiss = { showReportDialog = false },
|
||||
onReport = { type, comment ->
|
||||
scope.launch { account.reportEvent(event, type, comment) }
|
||||
},
|
||||
onBlockAndReport = { type, comment ->
|
||||
scope.launch {
|
||||
account.reportEvent(event, type, comment)
|
||||
account.hideUser(event.pubKey)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user