mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-18 03:16:57 +02:00
Feature: NIP-09 Event Deletion Request (#120)
* feat: Implement deletion request functionality in CardOptionsDropdown component * Update components/CardOptionsDropdown.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: highperfocused <highperfocused@pm.me> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
53
.github/prompts/nostr-nip09.prompt.md
vendored
Normal file
53
.github/prompts/nostr-nip09.prompt.md
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
NIP-09
|
||||
======
|
||||
|
||||
Event Deletion Request
|
||||
----------------------
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
A special event with kind `5`, meaning "deletion request" is defined as having a list of one or more `e` or `a` tags, each referencing an event the author is requesting to be deleted. Deletion requests SHOULD include a `k` tag for the kind of each event being requested for deletion.
|
||||
|
||||
The event's `content` field MAY contain a text note describing the reason for the deletion request.
|
||||
|
||||
For example:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 5,
|
||||
"pubkey": <32-bytes hex-encoded public key of the event creator>,
|
||||
"tags": [
|
||||
["e", "dcd59..464a2"],
|
||||
["e", "968c5..ad7a4"],
|
||||
["a", "<kind>:<pubkey>:<d-identifier>"],
|
||||
["k", "1"],
|
||||
["k", "30023"]
|
||||
],
|
||||
"content": "these posts were published by accident",
|
||||
// other fields...
|
||||
}
|
||||
```
|
||||
|
||||
Relays SHOULD delete or stop publishing any referenced events that have an identical `pubkey` as the deletion request. Clients SHOULD hide or otherwise indicate a deletion request status for referenced events.
|
||||
|
||||
Relays SHOULD continue to publish/share the deletion request events indefinitely, as clients may already have the event that's intended to be deleted. Additionally, clients SHOULD broadcast deletion request events to other relays which don't have it.
|
||||
|
||||
When an `a` tag is used, relays SHOULD delete all versions of the replaceable event up to the `created_at` timestamp of the deletion request event.
|
||||
|
||||
## Client Usage
|
||||
|
||||
Clients MAY choose to fully hide any events that are referenced by valid deletion request events. This includes text notes, direct messages, or other yet-to-be defined event kinds. Alternatively, they MAY show the event along with an icon or other indication that the author has "disowned" the event. The `content` field MAY also be used to replace the deleted events' own content, although a user interface should clearly indicate that this is a deletion request reason, not the original content.
|
||||
|
||||
A client MUST validate that each event `pubkey` referenced in the `e` tag of the deletion request is identical to the deletion request `pubkey`, before hiding or deleting any event. Relays can not, in general, perform this validation and should not be treated as authoritative.
|
||||
|
||||
Clients display the deletion request event itself in any way they choose, e.g., not at all, or with a prominent notice.
|
||||
|
||||
Clients MAY choose to inform the user that their request for deletion does not guarantee deletion because it is impossible to delete events from all relays and clients.
|
||||
|
||||
## Relay Usage
|
||||
|
||||
Relays MAY validate that a deletion request event only references events that have the same `pubkey` as the deletion request itself, however this is not required since relays may not have knowledge of all referenced events.
|
||||
|
||||
## Deletion Request of a Deletion Request
|
||||
|
||||
Publishing a deletion request event against a deletion request has no effect. Clients and relays are not obliged to support "unrequest deletion" functionality.
|
||||
@@ -22,6 +22,9 @@ import { Input } from "./ui/input";
|
||||
import { useRef, useState } from 'react';
|
||||
import { useToast } from "./ui/use-toast";
|
||||
import { Event as NostrEvent, nip19 } from "nostr-tools";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useNostr } from "nostr-react";
|
||||
import { signEvent } from "@/utils/utils";
|
||||
|
||||
interface CardOptionsDropdownProps {
|
||||
event: NostrEvent;
|
||||
@@ -32,9 +35,12 @@ export default function CardOptionsDropdown({ event }: CardOptionsDropdownProps)
|
||||
const inputRef = useRef(null);
|
||||
const inputRefID = useRef(null);
|
||||
const { toast } = useToast();
|
||||
const { publish } = useNostr();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [shareDrawerOpen, setShareDrawerOpen] = useState(false);
|
||||
const [rawDrawerOpen, setRawDrawerOpen] = useState(false);
|
||||
const [deleteDrawerOpen, setDeleteDrawerOpen] = useState(false);
|
||||
const [deleteReason, setDeleteReason] = useState('');
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
@@ -67,6 +73,73 @@ export default function CardOptionsDropdown({ event }: CardOptionsDropdownProps)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestDeletion = async () => {
|
||||
// Check if the user is the owner of the event
|
||||
const userPubkey = window.localStorage.getItem('pubkey');
|
||||
if (!userPubkey) {
|
||||
toast({
|
||||
description: 'You need to be logged in to request deletion',
|
||||
title: 'Error',
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (userPubkey !== event.pubkey) {
|
||||
toast({
|
||||
description: 'You can only request deletion of your own posts',
|
||||
title: 'Error',
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const loginType = window.localStorage.getItem('loginType');
|
||||
if (!loginType) {
|
||||
toast({
|
||||
description: 'Login type is missing. Please log in again.',
|
||||
title: 'Error',
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a kind 5 event (deletion request) as per NIP-09
|
||||
const deletionEvent: NostrEvent = {
|
||||
kind: 5,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: deleteReason,
|
||||
tags: [
|
||||
["e", event.id],
|
||||
["k", event.kind.toString()]
|
||||
],
|
||||
pubkey: "",
|
||||
id: "",
|
||||
sig: "",
|
||||
};
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = await signEvent(loginType, deletionEvent);
|
||||
|
||||
if (signedEvent) {
|
||||
// Publish the deletion request
|
||||
publish(signedEvent);
|
||||
|
||||
toast({
|
||||
description: 'Deletion request has been published',
|
||||
title: 'Success'
|
||||
});
|
||||
|
||||
setDeleteDrawerOpen(false);
|
||||
} else {
|
||||
toast({
|
||||
description: 'Failed to sign deletion request',
|
||||
title: 'Error',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -98,6 +171,19 @@ export default function CardOptionsDropdown({ event }: CardOptionsDropdownProps)
|
||||
<CodeIcon className="mr-2 h-4 w-4" />
|
||||
View Raw
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Delete option (only visible for the owner) */}
|
||||
{window.localStorage.getItem('pubkey') === event.pubkey && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false);
|
||||
setTimeout(() => setDeleteDrawerOpen(true), 100);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Request Deletion
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -143,6 +229,50 @@ export default function CardOptionsDropdown({ event }: CardOptionsDropdownProps)
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
{/* Deletion Request Drawer */}
|
||||
<Drawer open={deleteDrawerOpen} onOpenChange={setDeleteDrawerOpen}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Request Deletion</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
This will publish a deletion request (NIP-09) for this event. Clients and relays may hide or delete this event when they see your request.
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="px-4 pb-4">
|
||||
<div className="space-y-2 mb-4">
|
||||
<label htmlFor="delete-reason" className="text-sm font-medium">
|
||||
Reason for deletion (optional)
|
||||
</label>
|
||||
<Textarea
|
||||
id="delete-reason"
|
||||
placeholder="Why do you want to delete this event?"
|
||||
value={deleteReason}
|
||||
onChange={(e) => setDeleteReason(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-muted p-3 rounded-md text-sm mb-4">
|
||||
<p className="font-medium mb-1">Note:</p>
|
||||
<p className="text-muted-foreground">
|
||||
Deletion requests cannot be guaranteed to remove content from all relays and clients.
|
||||
Some relays may choose to ignore deletion requests, and previously downloaded content may still be available in clients.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DrawerFooter className="flex-row space-x-2">
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DrawerClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleRequestDeletion}
|
||||
>
|
||||
Request Deletion
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
|
||||
src={imageUrl}
|
||||
alt={text}
|
||||
className="rounded-lg w-full h-auto object-contain"
|
||||
onError={() => handleImageError(imageUrl)}
|
||||
// onError={() => handleImageError(imageUrl)}
|
||||
loading="lazy"
|
||||
style={{
|
||||
maxHeight: "80vh",
|
||||
|
||||
@@ -44,7 +44,7 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
|
||||
alt={text}
|
||||
className='w-full h-full rounded lg:rounded-lg object-cover'
|
||||
loading="lazy"
|
||||
onError={() => setImageError(true)}
|
||||
// onError={() => setImageError(true)}
|
||||
style={{ objectPosition: 'center' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user