mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-25 19:56:47 +02:00
NIP-09 Event Deletion support: Works with TextNotes, Likes, Boosts and Reports.
This commit is contained in:
@@ -30,6 +30,7 @@ import nostr.postr.Contact
|
|||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.Utils
|
import nostr.postr.Utils
|
||||||
import nostr.postr.events.ContactListEvent
|
import nostr.postr.events.ContactListEvent
|
||||||
|
import nostr.postr.events.DeletionEvent
|
||||||
import nostr.postr.events.Event
|
import nostr.postr.events.Event
|
||||||
import nostr.postr.events.MetadataEvent
|
import nostr.postr.events.MetadataEvent
|
||||||
import nostr.postr.events.PrivateDmEvent
|
import nostr.postr.events.PrivateDmEvent
|
||||||
@@ -123,10 +124,26 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reactionTo(note: Note): List<Note> {
|
||||||
|
return note.reactedBy(userProfile(), "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasBoosted(note: Note): Boolean {
|
||||||
|
return boostsTo(note).isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun boostsTo(note: Note): List<Note> {
|
||||||
|
return note.boostedBy(userProfile())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasReacted(note: Note): Boolean {
|
||||||
|
return note.hasReacted(userProfile(), "+")
|
||||||
|
}
|
||||||
|
|
||||||
fun reactTo(note: Note) {
|
fun reactTo(note: Note) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
if (note.hasReacted(userProfile(), "+")) {
|
if (hasReacted(note)) {
|
||||||
// has already liked this note
|
// has already liked this note
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -151,6 +168,7 @@ class Account(
|
|||||||
fun createZapRequestFor(user: User): LnZapRequestEvent? {
|
fun createZapRequestFor(user: User): LnZapRequestEvent? {
|
||||||
return createZapRequestFor(user.pubkeyHex)
|
return createZapRequestFor(user.pubkeyHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? {
|
fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? {
|
||||||
if (!isWriteable()) return null
|
if (!isWriteable()) return null
|
||||||
|
|
||||||
@@ -174,7 +192,7 @@ class Account(
|
|||||||
note.event?.let {
|
note.event?.let {
|
||||||
val event = ReportEvent.create(it, type, loggedIn.privKey!!)
|
val event = ReportEvent.create(it, type, loggedIn.privKey!!)
|
||||||
Client.send(event)
|
Client.send(event)
|
||||||
LocalCache.consume(event)
|
LocalCache.consume(event, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,13 +206,29 @@ class Account(
|
|||||||
|
|
||||||
val event = ReportEvent.create(user.pubkeyHex, type, loggedIn.privKey!!)
|
val event = ReportEvent.create(user.pubkeyHex, type, loggedIn.privKey!!)
|
||||||
Client.send(event)
|
Client.send(event)
|
||||||
LocalCache.consume(event)
|
LocalCache.consume(event, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(note: Note) {
|
||||||
|
delete(listOf(note))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(notes: List<Note>) {
|
||||||
|
if (!isWriteable()) return
|
||||||
|
|
||||||
|
val myNotes = notes.filter { it.author == userProfile() }.map { it.idHex }
|
||||||
|
|
||||||
|
if (myNotes.isNotEmpty()) {
|
||||||
|
val event = DeletionEvent.create(myNotes, loggedIn.privKey!!)
|
||||||
|
Client.send(event)
|
||||||
|
LocalCache.consume(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun boost(note: Note) {
|
fun boost(note: Note) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
if (note.hasBoosted(userProfile())) {
|
if (note.hasBoostedInTheLast5Minutes(userProfile())) {
|
||||||
// has already bosted in the past 5mins
|
// has already bosted in the past 5mins
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -532,8 +566,6 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
backupContactList?.let {
|
backupContactList?.let {
|
||||||
println("Loading saved contacts ${it.toJson()}")
|
println("Loading saved contacts ${it.toJson()}")
|
||||||
|
@@ -281,7 +281,41 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: DeletionEvent) {
|
fun consume(event: DeletionEvent) {
|
||||||
//Log.d("DEL", event.toJson())
|
var deletedAtLeastOne = false
|
||||||
|
|
||||||
|
event.deleteEvents.mapNotNull { notes[it] }.forEach { deleteNote ->
|
||||||
|
// must be the same author
|
||||||
|
if (deleteNote.author?.pubkeyHex == event.pubKey.toHexKey()) {
|
||||||
|
deleteNote.author?.removeNote(deleteNote)
|
||||||
|
|
||||||
|
// reverts the add
|
||||||
|
deleteNote.mentions?.forEach { user ->
|
||||||
|
user.removeTaggedPost(deleteNote)
|
||||||
|
user.removeReport(deleteNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNote.replyTo?.forEach { replyingNote ->
|
||||||
|
replyingNote.author?.removeTaggedPost(deleteNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counts the replies
|
||||||
|
deleteNote.replyTo?.forEach { masterNote ->
|
||||||
|
masterNote.removeReply(deleteNote)
|
||||||
|
masterNote.removeBoost(deleteNote)
|
||||||
|
masterNote.removeReaction(deleteNote)
|
||||||
|
masterNote.removeZap(deleteNote)
|
||||||
|
masterNote.removeReport(deleteNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
notes.remove(deleteNote.idHex)
|
||||||
|
|
||||||
|
deletedAtLeastOne = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedAtLeastOne) {
|
||||||
|
live.invalidateData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: RepostEvent) {
|
fun consume(event: RepostEvent) {
|
||||||
@@ -362,13 +396,18 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: ReportEvent) {
|
fun consume(event: ReportEvent, relay: Relay?) {
|
||||||
val note = getOrCreateNote(event.id.toHex())
|
val note = getOrCreateNote(event.id.toHex())
|
||||||
|
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
|
||||||
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
||||||
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it) }
|
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
@@ -658,6 +697,8 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toBeRemoved.forEach {
|
toBeRemoved.forEach {
|
||||||
|
it.author?.removeNote(it)
|
||||||
|
|
||||||
// reverts the add
|
// reverts the add
|
||||||
it.mentions?.forEach { user ->
|
it.mentions?.forEach { user ->
|
||||||
user.removeTaggedPost(it)
|
user.removeTaggedPost(it)
|
||||||
@@ -672,6 +713,7 @@ object LocalCache {
|
|||||||
masterNote.removeBoost(it)
|
masterNote.removeBoost(it)
|
||||||
masterNote.removeReaction(it)
|
masterNote.removeReaction(it)
|
||||||
masterNote.removeZap(it)
|
masterNote.removeZap(it)
|
||||||
|
masterNote.removeReport(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
notes.remove(it.idHex)
|
notes.remove(it.idHex)
|
||||||
|
@@ -21,7 +21,6 @@ import java.util.Date
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import nostr.postr.events.Event
|
import nostr.postr.events.Event
|
||||||
import nostr.postr.toNpub
|
|
||||||
|
|
||||||
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
||||||
|
|
||||||
@@ -116,6 +115,18 @@ class Note(val idHex: String) {
|
|||||||
reactions = reactions - note
|
reactions = reactions - note
|
||||||
liveSet?.reactions?.invalidateData()
|
liveSet?.reactions?.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeReport(deleteNote: Note) {
|
||||||
|
val author = deleteNote.author ?: return
|
||||||
|
|
||||||
|
if (author in reports.keys && reports[author]?.contains(deleteNote) == true ) {
|
||||||
|
reports[author]?.let {
|
||||||
|
reports = reports + Pair(author, it.minus(deleteNote))
|
||||||
|
liveSet?.reports?.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun removeZap(note: Note) {
|
fun removeZap(note: Note) {
|
||||||
if (zaps[note] != null) {
|
if (zaps[note] != null) {
|
||||||
zaps = zaps.minus(note)
|
zaps = zaps.minus(note)
|
||||||
@@ -248,14 +259,22 @@ class Note(val idHex: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun hasReacted(loggedIn: User, content: String): Boolean {
|
fun hasReacted(loggedIn: User, content: String): Boolean {
|
||||||
return reactions.firstOrNull { it.author == loggedIn && it.event?.content == content } != null
|
return reactedBy(loggedIn, content).isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasBoosted(loggedIn: User): Boolean {
|
fun reactedBy(loggedIn: User, content: String): List<Note> {
|
||||||
|
return reactions.filter { it.author == loggedIn && it.event?.content == content }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||||
val currentTime = Date().time / 1000
|
val currentTime = Date().time / 1000
|
||||||
return boosts.firstOrNull { it.author == loggedIn && (it.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection
|
return boosts.firstOrNull { it.author == loggedIn && (it.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun boostedBy(loggedIn: User): List<Note> {
|
||||||
|
return boosts.filter { it.author == loggedIn }
|
||||||
|
}
|
||||||
|
|
||||||
var liveSet: NoteLiveSet? = null
|
var liveSet: NoteLiveSet? = null
|
||||||
|
|
||||||
fun live(): NoteLiveSet {
|
fun live(): NoteLiveSet {
|
||||||
|
@@ -134,6 +134,10 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeNote(note: Note) {
|
||||||
|
notes = notes - note
|
||||||
|
}
|
||||||
|
|
||||||
fun clearNotes() {
|
fun clearNotes() {
|
||||||
notes = setOf<Note>()
|
notes = setOf<Note>()
|
||||||
}
|
}
|
||||||
@@ -155,6 +159,17 @@ class User(val pubkeyHex: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeReport(deleteNote: Note) {
|
||||||
|
val author = deleteNote.author ?: return
|
||||||
|
|
||||||
|
if (author in reports.keys && reports[author]?.contains(deleteNote) == true ) {
|
||||||
|
reports[author]?.let {
|
||||||
|
reports = reports + Pair(author, it.minus(deleteNote))
|
||||||
|
liveSet?.reports?.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addZap(zapRequest: Note, zap: Note?) {
|
fun addZap(zapRequest: Note, zap: Note?) {
|
||||||
if (zapRequest !in zaps.keys) {
|
if (zapRequest !in zaps.keys) {
|
||||||
zaps = zaps + Pair(zapRequest, zap)
|
zaps = zaps + Pair(zapRequest, zap)
|
||||||
|
@@ -74,7 +74,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
LocalCache.consume(repostEvent)
|
LocalCache.consume(repostEvent)
|
||||||
}
|
}
|
||||||
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))
|
ReportEvent.kind -> LocalCache.consume(ReportEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
||||||
|
|
||||||
LnZapEvent.kind -> {
|
LnZapEvent.kind -> {
|
||||||
val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
|
||||||
|
@@ -42,7 +42,7 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind),
|
kinds = listOf(TextNoteEvent.kind),
|
||||||
authors = listOf(it.pubkeyHex),
|
authors = listOf(it.pubkeyHex),
|
||||||
limit = 100
|
limit = 200
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -536,6 +536,12 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
|||||||
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
||||||
Text("Broadcast")
|
Text("Broadcast")
|
||||||
}
|
}
|
||||||
|
if (note.author == accountViewModel.accountLiveData.value?.account?.userProfile()) {
|
||||||
|
Divider()
|
||||||
|
DropdownMenuItem(onClick = { accountViewModel.delete(note); onDismiss() }) {
|
||||||
|
Text("Request Deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) {
|
if (note.author != accountViewModel.accountLiveData.value?.account?.userProfile()) {
|
||||||
Divider()
|
Divider()
|
||||||
DropdownMenuItem(onClick = {
|
DropdownMenuItem(onClick = {
|
||||||
@@ -546,7 +552,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
|||||||
)
|
)
|
||||||
}; onDismiss()
|
}; onDismiss()
|
||||||
}) {
|
}) {
|
||||||
Text("Block & Hide User")
|
Text("Block & Hide Author")
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
DropdownMenuItem(onClick = {
|
DropdownMenuItem(onClick = {
|
||||||
|
@@ -192,9 +192,13 @@ private fun BoostReaction(
|
|||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.then(Modifier.size(20.dp)),
|
modifier = Modifier.then(Modifier.size(20.dp)),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (accountViewModel.isWriteable())
|
if (accountViewModel.isWriteable()) {
|
||||||
wantsToBoost = true
|
if (accountViewModel.hasBoosted(baseNote)) {
|
||||||
else
|
accountViewModel.deleteBoostsTo(baseNote)
|
||||||
|
} else {
|
||||||
|
wantsToBoost = true
|
||||||
|
}
|
||||||
|
} else
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
@@ -250,7 +254,7 @@ fun LikeReaction(
|
|||||||
textModifier: Modifier = Modifier
|
textModifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val reactionsState by baseNote.live().reactions.observeAsState()
|
val reactionsState by baseNote.live().reactions.observeAsState()
|
||||||
val reactedNote = reactionsState?.note
|
val reactedNote = reactionsState?.note ?: return
|
||||||
|
|
||||||
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -259,9 +263,13 @@ fun LikeReaction(
|
|||||||
IconButton(
|
IconButton(
|
||||||
modifier = Modifier.then(Modifier.size(20.dp)),
|
modifier = Modifier.then(Modifier.size(20.dp)),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (accountViewModel.isWriteable())
|
if (accountViewModel.isWriteable()) {
|
||||||
accountViewModel.reactTo(baseNote)
|
if (accountViewModel.hasReactedTo(baseNote)) {
|
||||||
else
|
accountViewModel.deleteReactionTo(baseNote)
|
||||||
|
} else {
|
||||||
|
accountViewModel.reactTo(baseNote)
|
||||||
|
}
|
||||||
|
} else
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
|
@@ -34,6 +34,22 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
account.reactTo(note)
|
account.reactTo(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasReactedTo(baseNote: Note): Boolean {
|
||||||
|
return account.hasReacted(baseNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteReactionTo(note: Note) {
|
||||||
|
account.delete(account.reactionTo(note))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasBoosted(baseNote: Note): Boolean {
|
||||||
|
return account.hasBoosted(baseNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteBoostsTo(note: Note) {
|
||||||
|
account.delete(account.boostsTo(note))
|
||||||
|
}
|
||||||
|
|
||||||
fun zap(note: Note, amount: Long, message: String, context: Context, onError: (String) -> Unit) {
|
fun zap(note: Note, amount: Long, message: String, context: Context, onError: (String) -> Unit) {
|
||||||
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||||
|
|
||||||
@@ -71,6 +87,10 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
account.broadcast(note)
|
account.broadcast(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun delete(note: Note) {
|
||||||
|
account.delete(note)
|
||||||
|
}
|
||||||
|
|
||||||
fun decrypt(note: Note): String? {
|
fun decrypt(note: Note): String? {
|
||||||
return account.decryptContent(note)
|
return account.decryptContent(note)
|
||||||
}
|
}
|
||||||
@@ -94,4 +114,8 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
fun prefer(source: String, target: String, preference: String) {
|
fun prefer(source: String, target: String, preference: String) {
|
||||||
account.prefer(source, target, preference)
|
account.prefer(source, target, preference)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user