diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 95abe0a83..fb8e12e60 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent +import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Relay @@ -102,7 +103,7 @@ class Account( } } - fun report(note: Note) { + fun report(note: Note, type: ReportEvent.ReportType) { if (!isWriteable()) return if ( @@ -117,6 +118,27 @@ class Account( Client.send(event) LocalCache.consume(event) } + + note.event?.let { + val event = ReportEvent.create(it, type, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event) + } + } + + fun report(user: User, type: ReportEvent.ReportType) { + if (!isWriteable()) return + + if ( + user.reports.firstOrNull { it.author == userProfile() && it.event is ReportEvent && (it.event as ReportEvent).reportType.contains(type) } != null + ) { + // has already reported this note + return + } + + val event = ReportEvent.create(user.pubkeyHex, type, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event) } fun boost(note: Note) { @@ -347,9 +369,12 @@ class Account( } } + fun isHidden(user: User) = user !in hiddenUsers() fun isAcceptable(user: User): Boolean { return user !in hiddenUsers() // if user hasn't hided this author + && user.reports.firstOrNull { it.author == userProfile() } == null // if user has not reported this post + && user.reports.filter { it.author in userProfile().follows }.size < 5 } fun isAcceptableDirect(note: Note): Boolean { @@ -364,6 +389,7 @@ class Account( || (note.event is RepostEvent && note.replyTo?.firstOrNull { isAcceptableDirect(note) } != null) ) // is not a reaction about a blocked post } + } class AccountLiveData(private val account: Account): LiveData(AccountState(account)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5572dbfeb..1edd45787 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent +import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import java.io.ByteArrayInputStream import java.time.Instant @@ -273,6 +274,28 @@ object LocalCache { } } + fun consume(event: ReportEvent) { + val note = getOrCreateNote(event.id.toHex()) + + // Already processed this event. + if (note.event != null) return + + val author = getOrCreateUser(event.pubKey) + val mentions = event.reportedAuthor.map { getOrCreateUser(decodePublicKey(it)) } + val repliesTo = event.reportedPost.map { getOrCreateNote(it) }.toMutableList() + + note.loadEvent(event, author, mentions, repliesTo) + + //Log.d("RP", "New Report ${event.content} by ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") + // Adds notifications to users. + mentions.forEach { + it.addReport(note) + } + repliesTo.forEach { + it.addReport(note) + } + } + fun consume(event: ChannelCreateEvent) { //Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") // new event diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index fd67f2795..10c3d07d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -35,7 +35,6 @@ class Note(val idHex: String) { val replies = Collections.synchronizedSet(mutableSetOf()) val reactions = Collections.synchronizedSet(mutableSetOf()) val boosts = Collections.synchronizedSet(mutableSetOf()) - val reports = Collections.synchronizedSet(mutableSetOf()) var channel: Channel? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 9a5e75e2f..8c4069bfb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -38,6 +38,8 @@ class User(val pubkey: ByteArray) { val followers = Collections.synchronizedSet(mutableSetOf()) val messages = ConcurrentHashMap>() + val reports = Collections.synchronizedSet(mutableSetOf()) + fun toBestDisplayName(): String { return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex } @@ -83,6 +85,13 @@ class User(val pubkey: ByteArray) { updateSubscribers { it.onNewPosts() } } + fun addReport(note: Note) { + if (reports.add(note)) { + updateSubscribers { it.onNewReports() } + invalidateData() + } + } + @Synchronized fun getOrCreateChannel(user: User): MutableSet { return messages[user] ?: run { @@ -169,6 +178,7 @@ class User(val pubkey: ByteArray) { open fun onFollowsChange() = Unit open fun onNewPosts() = Unit open fun onNewMessage() = Unit + open fun onNewReports() = Unit } // Refreshes observers in batches. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 6b13a6ae1..f353a9e05 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent +import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Relay @@ -66,6 +67,7 @@ abstract class NostrDataSource(val debugName: String) { else -> when (event.kind) { RepostEvent.kind -> LocalCache.consume(RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) + ReportEvent.kind -> LocalCache.consume(ReportEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) ChannelCreateEvent.kind -> LocalCache.consume(ChannelCreateEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) ChannelMetadataEvent.kind -> LocalCache.consume(ChannelMetadataEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index d618109fd..6b82fe8ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent +import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import java.util.Collections import nostr.postr.JsonFilter @@ -24,7 +25,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") { // downloads all the reactions to a given event. return JsonFilter( kinds = listOf( - TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind + TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind ), tags = mapOf("e" to reactionsToWatch) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 83ad6438f..5aa09df91 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.ReportEvent import java.util.Collections import nostr.postr.JsonFilter import nostr.postr.events.MetadataEvent @@ -22,7 +23,19 @@ object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { } } + fun createUserReportFilter(): List? { + if (usersToWatch.isEmpty()) return null + + return usersToWatch.map { + JsonFilter( + kinds = listOf(ReportEvent.kind), + tags = mapOf("p" to listOf(it)) + ) + } + } + val userChannel = requestNewChannel() + val userReportChannel = requestNewChannel() override fun feed(): List { return synchronized(usersToWatch) { @@ -34,6 +47,7 @@ object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { override fun updateChannelFilters() { userChannel.filter = createUserFilter() + userReportChannel.filter = createUserReportFilter() } fun add(userId: String) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt new file mode 100644 index 000000000..66f2385e3 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -0,0 +1,66 @@ +package com.vitorpamplona.amethyst.service.model + +import androidx.compose.ui.text.toUpperCase +import java.util.Date +import nostr.postr.Utils +import nostr.postr.events.Event +import nostr.postr.toHex + +// NIP 56 event. +class ReportEvent ( + id: ByteArray, + pubKey: ByteArray, + createdAt: Long, + tags: List>, + content: String, + sig: ByteArray +): Event(id, pubKey, createdAt, kind, tags, content, sig) { + + @Transient val reportType: List + @Transient val reportedPost: List + @Transient val reportedAuthor: List + + init { + reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) } + reportedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + reportedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + } + + companion object { + const val kind = 1984 + + fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { + val content = "" + + val reportTypeTag = listOf("report", type.name.toLowerCase()) + val reportPostTag = listOf("e", reportedPost.id.toHex()) + val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex()) + + val pubKey = Utils.pubkeyCreate(privateKey) + val tags:List> = listOf(reportTypeTag, reportPostTag, reportAuthorTag) + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return ReportEvent(id, pubKey, createdAt, tags, content, sig) + } + + fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { + val content = "" + + val reportTypeTag = listOf("report", type.name.toLowerCase()) + val reportAuthorTag = listOf("p", reportedUser) + + val pubKey = Utils.pubkeyCreate(privateKey) + val tags:List> = listOf(reportTypeTag, reportAuthorTag) + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return ReportEvent(id, pubKey, createdAt, tags, content, sig) + } + } + + enum class ReportType() { + EXPLICIT, + ILLEGAL, + SPAM, + IMPERSONATION + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 584bd72f3..3fbec9426 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components import android.util.Patterns import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text @@ -46,6 +47,7 @@ fun isValidURL(url: String?): Boolean { @Composable fun RichTextViewer(content: String, tags: List>?, navController: NavController) { Column(modifier = Modifier.padding(top = 5.dp)) { + // FlowRow doesn't work well with paragraphs. So we need to split them content.split('\n').forEach { paragraph -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index d42ba2df2..5bb452767 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -20,6 +20,7 @@ import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.LocalContentColor +import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -38,6 +39,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -51,6 +53,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.toNote import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent +import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.ui.components.RichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -74,6 +77,9 @@ fun NoteCompose( val noteState by baseNote.live.observeAsState() val note = noteState?.note + val noteReportsState by baseNote.liveReports.observeAsState() + val noteForReports = noteReportsState?.note ?: return + var popupExpanded by remember { mutableStateOf(false) } val context = LocalContext.current.applicationContext @@ -83,7 +89,7 @@ fun NoteCompose( onClick = { }, onLongClick = { popupExpanded = true }, ), isInnerNote) - } else if (account?.isAcceptable(note) == false) { + } else if (!account.isAcceptable(noteForReports)) { HiddenNote(modifier.combinedClickable( onClick = { }, onLongClick = { popupExpanded = true }, @@ -239,8 +245,20 @@ fun NoteCompose( } } else { val eventContent = note.event?.content - if (eventContent != null) - RichTextViewer(eventContent, note.event?.tags, navController) + if (eventContent != null) { + if (note.reports.size > 0) { + // Doesn't load images + Row() { + Text( + text = eventContent, + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + ) + } + } else { + RichTextViewer(eventContent, note.event?.tags, navController) + } + } + //if (note.event !is ChannelMessageEvent) { ReactionsRow(note, accountViewModel) @@ -364,7 +382,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, Text("Copy Text") } DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.author?.pubkey?.toNpub() ?: "")); onDismiss() }) { - Text("Copy User ID") + Text("Copy User PubKey") } DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.id.toNote())); onDismiss() }) { Text("Copy Note ID") @@ -374,11 +392,37 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, Text("Broadcast") } Divider() - DropdownMenuItem(onClick = { accountViewModel.report(note); onDismiss() }) { - Text("Report Post") - } DropdownMenuItem(onClick = { note.author?.let { accountViewModel.hide(it, context) }; onDismiss() }) { Text("Hide User") } + Divider() + DropdownMenuItem(onClick = { + accountViewModel.report(note, ReportEvent.ReportType.SPAM); + note.author?.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Spam / Scam") + } + DropdownMenuItem(onClick = { + accountViewModel.report(note, ReportEvent.ReportType.IMPERSONATION); + note.author?.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Impersonation") + } + DropdownMenuItem(onClick = { + accountViewModel.report(note, ReportEvent.ReportType.EXPLICIT); + note.author?.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Explicit Content") + } + DropdownMenuItem(onClick = { + accountViewModel.report(note, ReportEvent.ReportType.ILLEGAL); + note.author?.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Illegal Behaviour") + } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 571da8b4b..bfbf85194 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -68,7 +68,7 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle } Column(modifier = Modifier.padding(start = 10.dp)) { - if (account?.isAcceptable(user) == false) { + if (account?.isHidden(user) == false) { ShowUserButton { account.showUser(user.pubkeyHex) LocalPreferences(ctx).saveToEncryptedStorage(account) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 4134e730c..4210b5545 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.animation.Crossfade import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -39,6 +40,7 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.RichTextViewer import com.vitorpamplona.amethyst.ui.note.BlankNote +import com.vitorpamplona.amethyst.ui.note.HiddenNote import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.UserPicture @@ -157,11 +159,16 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController val noteState by baseNote.live.observeAsState() val note = noteState?.note + val noteReportsState by baseNote.liveReports.observeAsState() + val noteForReports = noteReportsState?.note ?: return + val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return if (note?.event == null) { BlankNote() + } else if (!account.isAcceptable(noteForReports)) { + HiddenNote() } else { val authorState by note.author!!.live.observeAsState() val author = authorState?.user diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 84e77f735..d1b6094e6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.model.ReportEvent class AccountViewModel(private val account: Account): ViewModel() { val accountLiveData: LiveData = account.live.map { it } @@ -17,8 +18,12 @@ class AccountViewModel(private val account: Account): ViewModel() { account.reactTo(note) } - fun report(note: Note) { - account.report(note) + fun report(note: Note, type: ReportEvent.ReportType) { + account.report(note, type) + } + + fun report(user: User, type: ReportEvent.ReportType) { + account.report(user, type) } fun boost(note: Note) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 445951e15..ecd41cf3b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.model.toNote import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource +import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.note.UserPicture @@ -266,7 +267,7 @@ private fun ProfileHeader( if (accountUser == user) { EditButton(account) } else { - if (!account.isAcceptable(user)) { + if (!account.isHidden(user)) { ShowUserButton { account.showUser(user.pubkeyHex) LocalPreferences(ctx).saveToEncryptedStorage(account) @@ -507,21 +508,53 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () -> DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub() ?: "")); onDismiss() }) { Text("Copy User ID") } - Divider() - if (!account.isAcceptable(user)) { - DropdownMenuItem(onClick = { - user.let { - accountViewModel.show( - it, - context - ) - }; onDismiss() - }) { - Text("Unblock User") + + if ( account.userProfile() != user) { + Divider() + if (!account.isHidden(user)) { + DropdownMenuItem(onClick = { + user.let { + accountViewModel.show( + it, + context + ) + }; onDismiss() + }) { + Text("Unblock User") + } + } else { + DropdownMenuItem(onClick = { user.let { accountViewModel.hide(it, context) }; onDismiss() }) { + Text("Hide User") + } } - } else { - DropdownMenuItem(onClick = { user.let { accountViewModel.hide(it, context) }; onDismiss() }) { - Text("Block User") + Divider() + DropdownMenuItem(onClick = { + accountViewModel.report(user, ReportEvent.ReportType.SPAM); + user.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Spam / Scam") + } + DropdownMenuItem(onClick = { + accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION); + user.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Impersonation") + } + DropdownMenuItem(onClick = { + accountViewModel.report(user, ReportEvent.ReportType.EXPLICIT); + user.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Explicit Content") + } + DropdownMenuItem(onClick = { + accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL); + user.let { accountViewModel.hide(it, context) } + onDismiss() + }) { + Text("Report Illegal Behaviour") } } }