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.Utils
import nostr.postr.events.ContactListEvent
import nostr.postr.events.DeletionEvent
import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent
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) {
if (!isWriteable()) return
if (note.hasReacted(userProfile(), "+")) {
if (hasReacted(note)) {
// has already liked this note
return
}
@ -151,6 +168,7 @@ class Account(
fun createZapRequestFor(user: User): LnZapRequestEvent? {
return createZapRequestFor(user.pubkeyHex)
}
fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? {
if (!isWriteable()) return null
@ -174,7 +192,7 @@ class Account(
note.event?.let {
val event = ReportEvent.create(it, type, loggedIn.privKey!!)
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!!)
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) {
if (!isWriteable()) return
if (note.hasBoosted(userProfile())) {
if (note.hasBoostedInTheLast5Minutes(userProfile())) {
// has already bosted in the past 5mins
return
}
@ -532,8 +566,6 @@ class Account(
saveable.invalidateData()
}
init {
backupContactList?.let {
println("Loading saved contacts ${it.toJson()}")

View File

@ -281,7 +281,41 @@ object LocalCache {
}
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) {
@ -362,13 +396,18 @@ object LocalCache {
}
}
fun consume(event: ReportEvent) {
fun consume(event: ReportEvent, relay: Relay?) {
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.
if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it) }
@ -658,6 +697,8 @@ object LocalCache {
}
toBeRemoved.forEach {
it.author?.removeNote(it)
// reverts the add
it.mentions?.forEach { user ->
user.removeTaggedPost(it)
@ -672,6 +713,7 @@ object LocalCache {
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.removeReport(it)
}
notes.remove(it.idHex)

View File

@ -21,7 +21,6 @@ import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
import nostr.postr.events.Event
import nostr.postr.toNpub
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
@ -116,6 +115,18 @@ class Note(val idHex: String) {
reactions = reactions - note
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) {
if (zaps[note] != null) {
zaps = zaps.minus(note)
@ -248,14 +259,22 @@ class Note(val idHex: String) {
}
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
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
fun live(): NoteLiveSet {

View File

@ -134,6 +134,10 @@ class User(val pubkeyHex: String) {
}
}
fun removeNote(note: Note) {
notes = notes - note
}
fun clearNotes() {
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?) {
if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap)

View File

@ -74,7 +74,7 @@ abstract class NostrDataSource(val debugName: String) {
LocalCache.consume(repostEvent)
}
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 -> {
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(
kinds = listOf(TextNoteEvent.kind),
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() }) {
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()) {
Divider()
DropdownMenuItem(onClick = {
@ -546,7 +552,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
)
}; onDismiss()
}) {
Text("Block & Hide User")
Text("Block & Hide Author")
}
Divider()
DropdownMenuItem(onClick = {

View File

@ -192,9 +192,13 @@ private fun BoostReaction(
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = {
if (accountViewModel.isWriteable())
wantsToBoost = true
else
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasBoosted(baseNote)) {
accountViewModel.deleteBoostsTo(baseNote)
} else {
wantsToBoost = true
}
} else
scope.launch {
Toast.makeText(
context,
@ -250,7 +254,7 @@ fun LikeReaction(
textModifier: Modifier = Modifier
) {
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 context = LocalContext.current
@ -259,9 +263,13 @@ fun LikeReaction(
IconButton(
modifier = Modifier.then(Modifier.size(20.dp)),
onClick = {
if (accountViewModel.isWriteable())
accountViewModel.reactTo(baseNote)
else
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasReactedTo(baseNote)) {
accountViewModel.deleteReactionTo(baseNote)
} else {
accountViewModel.reactTo(baseNote)
}
} else
scope.launch {
Toast.makeText(
context,

View File

@ -34,6 +34,22 @@ class AccountViewModel(private val account: Account): ViewModel() {
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) {
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)
}
fun delete(note: Note) {
account.delete(note)
}
fun decrypt(note: Note): String? {
return account.decryptContent(note)
}
@ -94,4 +114,8 @@ class AccountViewModel(private val account: Account): ViewModel() {
fun prefer(source: String, target: String, preference: String) {
account.prefer(source, target, preference)
}
}