mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-27 20:46:32 +02:00
Activating NIP 56 (Report Users and Posts with Event Kind 1984)
This commit is contained in:
@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
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.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
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 (!isWriteable()) return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -117,6 +118,27 @@ class Account(
|
|||||||
Client.send(event)
|
Client.send(event)
|
||||||
LocalCache.consume(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) {
|
fun boost(note: Note) {
|
||||||
@@ -347,9 +369,12 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isHidden(user: User) = user !in hiddenUsers()
|
||||||
|
|
||||||
fun isAcceptable(user: User): Boolean {
|
fun isAcceptable(user: User): Boolean {
|
||||||
return user !in hiddenUsers() // if user hasn't hided this author
|
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 {
|
fun isAcceptableDirect(note: Note): Boolean {
|
||||||
@@ -364,6 +389,7 @@ class Account(
|
|||||||
|| (note.event is RepostEvent && note.replyTo?.firstOrNull { isAcceptableDirect(note) } != null)
|
|| (note.event is RepostEvent && note.replyTo?.firstOrNull { isAcceptableDirect(note) } != null)
|
||||||
) // is not a reaction about a blocked post
|
) // is not a reaction about a blocked post
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
|
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
|
||||||
|
@@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
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.model.RepostEvent
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.time.Instant
|
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) {
|
fun consume(event: ChannelCreateEvent) {
|
||||||
//Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
|
//Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
|
||||||
// new event
|
// new event
|
||||||
|
@@ -35,7 +35,6 @@ class Note(val idHex: String) {
|
|||||||
val replies = Collections.synchronizedSet(mutableSetOf<Note>())
|
val replies = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
val reactions = Collections.synchronizedSet(mutableSetOf<Note>())
|
val reactions = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
val boosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
val boosts = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
|
|
||||||
val reports = Collections.synchronizedSet(mutableSetOf<Note>())
|
val reports = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
|
|
||||||
var channel: Channel? = null
|
var channel: Channel? = null
|
||||||
|
@@ -38,6 +38,8 @@ class User(val pubkey: ByteArray) {
|
|||||||
val followers = Collections.synchronizedSet(mutableSetOf<User>())
|
val followers = Collections.synchronizedSet(mutableSetOf<User>())
|
||||||
val messages = ConcurrentHashMap<User, MutableSet<Note>>()
|
val messages = ConcurrentHashMap<User, MutableSet<Note>>()
|
||||||
|
|
||||||
|
val reports = Collections.synchronizedSet(mutableSetOf<Note>())
|
||||||
|
|
||||||
fun toBestDisplayName(): String {
|
fun toBestDisplayName(): String {
|
||||||
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex
|
return bestDisplayName() ?: bestUsername() ?: pubkeyDisplayHex
|
||||||
}
|
}
|
||||||
@@ -83,6 +85,13 @@ class User(val pubkey: ByteArray) {
|
|||||||
updateSubscribers { it.onNewPosts() }
|
updateSubscribers { it.onNewPosts() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addReport(note: Note) {
|
||||||
|
if (reports.add(note)) {
|
||||||
|
updateSubscribers { it.onNewReports() }
|
||||||
|
invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getOrCreateChannel(user: User): MutableSet<Note> {
|
fun getOrCreateChannel(user: User): MutableSet<Note> {
|
||||||
return messages[user] ?: run {
|
return messages[user] ?: run {
|
||||||
@@ -169,6 +178,7 @@ class User(val pubkey: ByteArray) {
|
|||||||
open fun onFollowsChange() = Unit
|
open fun onFollowsChange() = Unit
|
||||||
open fun onNewPosts() = Unit
|
open fun onNewPosts() = Unit
|
||||||
open fun onNewMessage() = Unit
|
open fun onNewMessage() = Unit
|
||||||
|
open fun onNewReports() = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
|
@@ -11,6 +11,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
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.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
@@ -66,6 +67,7 @@ abstract class NostrDataSource<T>(val debugName: String) {
|
|||||||
else -> when (event.kind) {
|
else -> when (event.kind) {
|
||||||
RepostEvent.kind -> LocalCache.consume(RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
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))
|
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))
|
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))
|
ChannelMetadataEvent.kind -> LocalCache.consume(ChannelMetadataEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
||||||
|
@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
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.model.RepostEvent
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
@@ -24,7 +25,7 @@ object NostrSingleEventDataSource: NostrDataSource<Note>("SingleEventFeed") {
|
|||||||
// downloads all the reactions to a given event.
|
// downloads all the reactions to a given event.
|
||||||
return JsonFilter(
|
return JsonFilter(
|
||||||
kinds = listOf(
|
kinds = listOf(
|
||||||
TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind
|
TextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind
|
||||||
),
|
),
|
||||||
tags = mapOf("e" to reactionsToWatch)
|
tags = mapOf("e" to reactionsToWatch)
|
||||||
)
|
)
|
||||||
|
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import nostr.postr.JsonFilter
|
import nostr.postr.JsonFilter
|
||||||
import nostr.postr.events.MetadataEvent
|
import nostr.postr.events.MetadataEvent
|
||||||
@@ -22,7 +23,19 @@ object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createUserReportFilter(): List<JsonFilter>? {
|
||||||
|
if (usersToWatch.isEmpty()) return null
|
||||||
|
|
||||||
|
return usersToWatch.map {
|
||||||
|
JsonFilter(
|
||||||
|
kinds = listOf(ReportEvent.kind),
|
||||||
|
tags = mapOf("p" to listOf(it))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val userChannel = requestNewChannel()
|
val userChannel = requestNewChannel()
|
||||||
|
val userReportChannel = requestNewChannel()
|
||||||
|
|
||||||
override fun feed(): List<User> {
|
override fun feed(): List<User> {
|
||||||
return synchronized(usersToWatch) {
|
return synchronized(usersToWatch) {
|
||||||
@@ -34,6 +47,7 @@ object NostrSingleUserDataSource: NostrDataSource<User>("SingleUserFeed") {
|
|||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
userChannel.filter = createUserFilter()
|
userChannel.filter = createUserFilter()
|
||||||
|
userReportChannel.filter = createUserReportFilter()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(userId: String) {
|
fun add(userId: String) {
|
||||||
|
@@ -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<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: ByteArray
|
||||||
|
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
@Transient val reportType: List<ReportType>
|
||||||
|
@Transient val reportedPost: List<String>
|
||||||
|
@Transient val reportedAuthor: List<String>
|
||||||
|
|
||||||
|
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<List<String>> = 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<List<String>> = 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
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components
|
|||||||
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.LocalTextStyle
|
import androidx.compose.material.LocalTextStyle
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
@@ -46,6 +47,7 @@ fun isValidURL(url: String?): Boolean {
|
|||||||
@Composable
|
@Composable
|
||||||
fun RichTextViewer(content: String, tags: List<List<String>>?, navController: NavController) {
|
fun RichTextViewer(content: String, tags: List<List<String>>?, navController: NavController) {
|
||||||
Column(modifier = Modifier.padding(top = 5.dp)) {
|
Column(modifier = Modifier.padding(top = 5.dp)) {
|
||||||
|
|
||||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||||
content.split('\n').forEach { paragraph ->
|
content.split('\n').forEach { paragraph ->
|
||||||
|
|
||||||
|
@@ -20,6 +20,7 @@ import androidx.compose.material.DropdownMenu
|
|||||||
import androidx.compose.material.DropdownMenuItem
|
import androidx.compose.material.DropdownMenuItem
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.LocalContentColor
|
import androidx.compose.material.LocalContentColor
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -51,6 +53,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.model.toNote
|
import com.vitorpamplona.amethyst.model.toNote
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
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.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -74,6 +77,9 @@ fun NoteCompose(
|
|||||||
val noteState by baseNote.live.observeAsState()
|
val noteState by baseNote.live.observeAsState()
|
||||||
val note = noteState?.note
|
val note = noteState?.note
|
||||||
|
|
||||||
|
val noteReportsState by baseNote.liveReports.observeAsState()
|
||||||
|
val noteForReports = noteReportsState?.note ?: return
|
||||||
|
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val context = LocalContext.current.applicationContext
|
val context = LocalContext.current.applicationContext
|
||||||
@@ -83,7 +89,7 @@ fun NoteCompose(
|
|||||||
onClick = { },
|
onClick = { },
|
||||||
onLongClick = { popupExpanded = true },
|
onLongClick = { popupExpanded = true },
|
||||||
), isInnerNote)
|
), isInnerNote)
|
||||||
} else if (account?.isAcceptable(note) == false) {
|
} else if (!account.isAcceptable(noteForReports)) {
|
||||||
HiddenNote(modifier.combinedClickable(
|
HiddenNote(modifier.combinedClickable(
|
||||||
onClick = { },
|
onClick = { },
|
||||||
onLongClick = { popupExpanded = true },
|
onLongClick = { popupExpanded = true },
|
||||||
@@ -239,8 +245,20 @@ fun NoteCompose(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val eventContent = note.event?.content
|
val eventContent = note.event?.content
|
||||||
if (eventContent != null)
|
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)
|
RichTextViewer(eventContent, note.event?.tags, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//if (note.event !is ChannelMessageEvent) {
|
//if (note.event !is ChannelMessageEvent) {
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
@@ -364,7 +382,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
|||||||
Text("Copy Text")
|
Text("Copy Text")
|
||||||
}
|
}
|
||||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.author?.pubkey?.toNpub() ?: "")); onDismiss() }) {
|
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() }) {
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.id.toNote())); onDismiss() }) {
|
||||||
Text("Copy Note ID")
|
Text("Copy Note ID")
|
||||||
@@ -374,11 +392,37 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
|||||||
Text("Broadcast")
|
Text("Broadcast")
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
DropdownMenuItem(onClick = { accountViewModel.report(note); onDismiss() }) {
|
|
||||||
Text("Report Post")
|
|
||||||
}
|
|
||||||
DropdownMenuItem(onClick = { note.author?.let { accountViewModel.hide(it, context) }; onDismiss() }) {
|
DropdownMenuItem(onClick = { note.author?.let { accountViewModel.hide(it, context) }; onDismiss() }) {
|
||||||
Text("Hide User")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -68,7 +68,7 @@ fun UserCompose(baseUser: User, accountViewModel: AccountViewModel, navControlle
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
if (account?.isAcceptable(user) == false) {
|
if (account?.isHidden(user) == false) {
|
||||||
ShowUserButton {
|
ShowUserButton {
|
||||||
account.showUser(user.pubkeyHex)
|
account.showUser(user.pubkeyHex)
|
||||||
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
|
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
|||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
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.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
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.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
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 noteState by baseNote.live.observeAsState()
|
||||||
val note = noteState?.note
|
val note = noteState?.note
|
||||||
|
|
||||||
|
val noteReportsState by baseNote.liveReports.observeAsState()
|
||||||
|
val noteForReports = noteReportsState?.note ?: return
|
||||||
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
val account = accountState?.account ?: return
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
if (note?.event == null) {
|
if (note?.event == null) {
|
||||||
BlankNote()
|
BlankNote()
|
||||||
|
} else if (!account.isAcceptable(noteForReports)) {
|
||||||
|
HiddenNote()
|
||||||
} else {
|
} else {
|
||||||
val authorState by note.author!!.live.observeAsState()
|
val authorState by note.author!!.live.observeAsState()
|
||||||
val author = authorState?.user
|
val author = authorState?.user
|
||||||
|
@@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.AccountState
|
import com.vitorpamplona.amethyst.model.AccountState
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
|
|
||||||
class AccountViewModel(private val account: Account): ViewModel() {
|
class AccountViewModel(private val account: Account): ViewModel() {
|
||||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||||
@@ -17,8 +18,12 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
account.reactTo(note)
|
account.reactTo(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun report(note: Note) {
|
fun report(note: Note, type: ReportEvent.ReportType) {
|
||||||
account.report(note)
|
account.report(note, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun report(user: User, type: ReportEvent.ReportType) {
|
||||||
|
account.report(user, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun boost(note: Note) {
|
fun boost(note: Note) {
|
||||||
|
@@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.model.toNote
|
|||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowersDataSource
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileFollowsDataSource
|
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.NewChannelView
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
@@ -266,7 +267,7 @@ private fun ProfileHeader(
|
|||||||
if (accountUser == user) {
|
if (accountUser == user) {
|
||||||
EditButton(account)
|
EditButton(account)
|
||||||
} else {
|
} else {
|
||||||
if (!account.isAcceptable(user)) {
|
if (!account.isHidden(user)) {
|
||||||
ShowUserButton {
|
ShowUserButton {
|
||||||
account.showUser(user.pubkeyHex)
|
account.showUser(user.pubkeyHex)
|
||||||
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
@@ -507,8 +508,10 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
|||||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub() ?: "")); onDismiss() }) {
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkey.toNpub() ?: "")); onDismiss() }) {
|
||||||
Text("Copy User ID")
|
Text("Copy User ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( account.userProfile() != user) {
|
||||||
Divider()
|
Divider()
|
||||||
if (!account.isAcceptable(user)) {
|
if (!account.isHidden(user)) {
|
||||||
DropdownMenuItem(onClick = {
|
DropdownMenuItem(onClick = {
|
||||||
user.let {
|
user.let {
|
||||||
accountViewModel.show(
|
accountViewModel.show(
|
||||||
@@ -521,7 +524,37 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(onClick = { user.let { accountViewModel.hide(it, context) }; onDismiss() }) {
|
DropdownMenuItem(onClick = { user.let { accountViewModel.hide(it, context) }; onDismiss() }) {
|
||||||
Text("Block User")
|
Text("Hide 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user