NIP-09 Event Deletion support: Works with TextNotes, Likes, Boosts and Reports.

This commit is contained in:
Vitor Pamplona
2023-02-26 12:02:07 -05:00
parent 2eff0626ec
commit c087c5017c
9 changed files with 168 additions and 22 deletions

View File

@@ -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()}")

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
) )
) )
} }

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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)
} }
} }