import { useMutation, useQuery } from "@tanstack/react-query" import { useAtomValue, useStore } from "jotai" import { CheckIcon, CopyIcon, EllipsisIcon, LinkIcon, LockKeyholeIcon, } from "lucide-react" import { createContext, useContext, useRef } from "react" import { CrossfadeIcon, type CrossfadeIconHandle, } from "@/components/crossfade-icon" import { Button } from "@/components/ui/button" import { ButtonGroup } from "@/components/ui/button-group" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { copyToClipboardMutation } from "@/lib/clipboard" import { createShareMutationAtom, deleteShareMutationAtom, directorySharesQueryAtom, fileSharesQueryAtom, } from "@/sharing/api" import type { DirectoryItem } from "@/vfs/vfs" import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip" import type { Share } from "./share" type ItemShareDialogProps = { item: DirectoryItem | null open: boolean onClose: () => void } const ItemShareDialogContext = createContext<{ item: DirectoryItem }>( null as unknown as { item: DirectoryItem }, ) export function ItemShareDialog({ item, open, onClose }: ItemShareDialogProps) { let description: string switch (item?.kind) { case "file": description = "Configure external access to this file." break case "directory": description = "Configure external access to this directory." break default: description = "Configure external access to this item." break } return ( Share {item?.name} {description} {item && (
)}
) } function PublicAccessSection({ item }: { item: DirectoryItem }) { const fileSharesQuery = useAtomValue(fileSharesQueryAtom(item.id)) const directorySharesQuery = useAtomValue(directorySharesQueryAtom(item.id)) const { data: fileShares, isLoading: isLoadingFileShares } = useQuery({ ...fileSharesQuery, enabled: item.kind === "file", }) const { data: directoryShares, isLoading: isLoadingDirectoryShares } = useQuery({ ...directorySharesQuery, enabled: item.kind === "directory", }) let shares: Share[] = [] if (fileShares) { shares = fileShares } else if (directoryShares) { shares = directoryShares } let content: React.ReactNode = null if (isLoadingFileShares || isLoadingDirectoryShares) { content =
Loading...
} else if (shares.length === 0) { content = (

No share link created

Only you can access this item.

) } else { content = ( ) } return (

Public Access

{content}
) } function ShareLinkListItem({ share }: { share: Share }) { const { item } = useContext(ItemShareDialogContext) const copyLinkButtonRef = useRef(null) const copyIconRef = useRef(null) const { mutate: copyToClipboard } = useMutation({ ...copyToClipboardMutation, onSuccess: () => { copyIconRef.current?.trigger() }, }) const copyItemShareLinkToClipboard = () => { let link: string switch (item.kind) { case "file": link = `${window.location.origin}/shares/${share.id}/files/${item.id}` break case "directory": link = `${window.location.origin}/shares/${share.id}/directories/${item.id}` break default: link = "" break } if (link) { copyToClipboard(link) } } return (
  • {/** biome-ignore lint/a11y/noStaticElementInteractions: this is strictly for convenience. the normal copy link button is still accessible. */} {/** biome-ignore lint/a11y/useKeyWithClickEvents: this is strictly for convenience. the normal copy link button is still accessible. */}
    { copyLinkButtonRef.current?.click() }} >
    {/** biome-ignore lint/a11y/noStaticElementInteractions: this is strictly for convenience. the normal copy link button is still accessible. */} {/** biome-ignore lint/a11y/useKeyWithClickEvents: this is strictly for convenience. the normal copy link button is still accessible. */}
    { copyLinkButtonRef.current?.click() }} >

    Share link

    {share.expiresAt ? `Expires at ${share.expiresAt}` : "Never expires"}

    Copy share link
  • ) } function ShareLinkOptionsMenu({ share }: { share: Share }) { const { item } = useContext(ItemShareDialogContext) const store = useStore() const { mutate: deleteShare } = useMutation({ ...useAtomValue(deleteShareMutationAtom), onMutate: ({ shareId }, { client }) => { let queryKey: readonly unknown[] | null switch (item.kind) { case "file": queryKey = store.get(fileSharesQueryAtom(item.id)).queryKey break case "directory": queryKey = store.get( directorySharesQueryAtom(item.id), ).queryKey break default: queryKey = null break } if (queryKey) { const prevShares = client.getQueryData(queryKey) client.setQueryData( queryKey, (old) => old?.filter((s) => s.id !== shareId) ?? old, ) return { queryKey, prevShares } } return null }, onSuccess: (_data, _vars, mutateResult, { client }) => { if (mutateResult) { client.invalidateQueries({ queryKey: mutateResult.queryKey, }) } }, onError: (error, _vars, mutateResult, { client }) => { console.error(error) if (mutateResult) { client.setQueryData( mutateResult.queryKey, mutateResult.prevShares, ) } }, }) return ( Rename link Set expiration { deleteShare({ shareId: share.id }) }} > Delete link ) } function CreateShareLinkButton() { const { item } = useContext(ItemShareDialogContext) const store = useStore() const { mutate: createShare, isPending: isCreatingShare } = useMutation({ ...useAtomValue(createShareMutationAtom), onSuccess: (_createdShare, _vars, _, { client }) => { let queryKey: readonly unknown[] | null switch (item.kind) { case "file": queryKey = store.get(fileSharesQueryAtom(item.id)).queryKey break case "directory": queryKey = store.get( directorySharesQueryAtom(item.id), ).queryKey break default: queryKey = null break } if (queryKey) { client.invalidateQueries({ queryKey, }) } }, }) return ( ) }