Files
lumina/components/CardOptionsDropdown.tsx
Silberengel 525a0850b1 Upload from url PART 2 (#139)
* add URL option to image upload

* Full NIP-68 and NIP-71 implementation

* changed deprecated note ids to nevent ids, with backward-compatability

* interim state reels implementation

* Fixed uploading from URL and added a Cancel button to the upload modal. Couldn't get rid of the errors.

* Added ability to upload kinds 20, 21, 22, along with source tags (e, a, or u). Includes validation check.

* added thumbnail support

* included kind 21 and kind 22 in the feeds and searches

* Implement inboxes/outboxes

* implemented thumbnails in the profile feed

* enhanced reels feed with #reels

* interim implementation of pins

* added pins

* fixed the pins

* tidied up the reels

* fixed the uploader

* Fixed build

* update reels feed with the one from Lumina main

* fixed the reels interactions

* Added audio controls

* Interim reelfeed state

* feed working again

* full fead

---------

Co-authored-by: Silberengel <silberengel7@proton.com>
2025-09-01 23:05:54 +02:00

288 lines
11 KiB
TypeScript

import React, { useMemo } from 'react';
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Textarea } from "./ui/textarea";
import { DotsVerticalIcon, CodeIcon, Share1Icon } from "@radix-ui/react-icons";
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 { publishToOutbox } from "@/utils/publishUtils";
import { useCurrentUserPubkey } from "@/utils/relayHooks";
import { signEvent } from "@/utils/utils";
interface CardOptionsDropdownProps {
event: NostrEvent;
}
export default function CardOptionsDropdown({ event }: CardOptionsDropdownProps) {
const jsonEvent = useMemo(() => JSON.stringify(event, null, 2), [event]);
const inputRef = useRef(null);
const inputRefID = useRef(null);
const { toast } = useToast();
const currentUserPubkey = useCurrentUserPubkey();
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 {
await navigator.clipboard.writeText(window.location.href);
toast({
description: 'URL copied to clipboard',
title: 'Copied'
});
} catch (err) {
toast({
description: 'Error copying URL to clipboard',
title: 'Error',
variant: 'destructive'
});
}
};
const handleCopyNoteId = async () => {
try {
// Create nevent with relay hints
const nevent = nip19.neventEncode({
id: event.id,
relays: []
});
await navigator.clipboard.writeText(nevent);
toast({
description: 'Event ID copied to clipboard',
title: 'Copied'
});
} catch (err) {
toast({
description: 'Error copying Event ID to clipboard',
title: 'Error',
variant: 'destructive'
});
}
};
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
await publishToOutbox(signedEvent, currentUserPubkey || undefined);
toast({
description: 'Deletion request has been published',
title: 'Success'
});
setDeleteDrawerOpen(false);
} else {
toast({
description: 'Failed to sign deletion request',
title: 'Error',
variant: 'destructive'
});
}
};
return (
<>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="ml-auto mt-0 flex-shrink-0" aria-label="Options">
<DotsVerticalIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Share option */}
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false);
setTimeout(() => setShareDrawerOpen(true), 100);
}}
>
<Share1Icon className="mr-2 h-4 w-4" />
Share
</DropdownMenuItem>
{/* View Raw option */}
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false);
setTimeout(() => setRawDrawerOpen(true), 100);
}}
>
<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>
{/* Share Drawer */}
<Drawer open={shareDrawerOpen} onOpenChange={setShareDrawerOpen}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Share this Note</DrawerTitle>
<DrawerDescription>Share this Note with others.</DrawerDescription>
</DrawerHeader>
<div className="px-4">
<div className="flex items-center mb-4">
<Input ref={inputRef} value={window.location.href} readOnly className="mr-2" />
<Button variant="outline" onClick={handleCopyLink}>Copy Link</Button>
</div>
<div className="flex items-center mb-4">
<Input ref={inputRefID} value={nip19.neventEncode({
id: event.id,
relays: []
})} readOnly className="mr-2" />
<Button variant="outline" onClick={handleCopyNoteId}>Copy Event ID</Button>
</div>
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
{/* Raw Event Drawer */}
<Drawer open={rawDrawerOpen} onOpenChange={setRawDrawerOpen}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Raw Event</DrawerTitle>
<DrawerDescription>This shows the raw event data.</DrawerDescription>
</DrawerHeader>
<div className="px-4 pb-4">
<Textarea rows={20} readOnly value={jsonEvent} />
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</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>
</>
);
}