import { useMutation, useQuery } from "@tanstack/react-query" import { atom, type PrimitiveAtom, useAtomValue, useSetAtom, useStore, } from "jotai" import { CheckIcon, CopyIcon, EllipsisIcon, LinkIcon, LockKeyholeIcon, } from "lucide-react" import type React from "react" import { createContext, useContext, useMemo, useRef, useState } 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 { Input } from "@/components/ui/input" import { copyToClipboardMutation } from "@/lib/clipboard" import { createShareMutationAtom, deleteShareMutationAtom, directorySharesQueryAtom, fileSharesQueryAtom, updateShareMutationAtom, } from "@/sharing/api" import type { DirectoryItem } from "@/vfs/vfs" import { DateInput, type DateInputHandle } from "../components/date-input" import { Checkbox } from "../components/ui/checkbox" import { Kbd } from "../components/ui/kbd" import { Label } from "../components/ui/label" import { Popover, PopoverContent, PopoverTrigger, } from "../components/ui/popover" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../components/ui/select" import { Tooltip, TooltipContent, TooltipTrigger, } from "../components/ui/tooltip" import { cn } from "../lib/utils" 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) } } const formattedExpirationDate = share.expiresAt ? share.expiresAt.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric", }) : "" 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

    {formattedExpirationDate ? `Expires at ${formattedExpirationDate}` : "Never expires"}

    Copy share link
  • ) } const ACTIVE_POPOVER_KIND = { rename: "rename", setExpiration: "setExpiration", } as const type ActivePopoverKind = (typeof ACTIVE_POPOVER_KIND)[keyof typeof ACTIVE_POPOVER_KIND] function ShareLinkOptionsMenuButton({ share }: { share: Share }) { const activePopoverAtom = useMemo( () => atom(null), [], ) const activePopoverKind = useAtomValue(activePopoverAtom) const button = ( ) switch (activePopoverKind) { case "rename": return ( {button} ) case "setExpiration": return ( {button} ) default: return ( {button} ) } } function RenameShareLinkPopover({ share, children, activePopoverAtom, }: React.PropsWithChildren<{ share: Share activePopoverAtom: PrimitiveAtom }>) { const setActivePopover = useSetAtom(activePopoverAtom) const inputId = `rename-share-link-${share.id}` return ( { if (!nextOpen) { setActivePopover(null) } }} > {children}

    Set a label for this link

    Only you can see the label.

    ) } function ConfigureShareLinkExpirationPopover({ share, children, activePopoverAtom, }: React.PropsWithChildren<{ share: Share activePopoverAtom: PrimitiveAtom }>) { const EXPIRATION_TYPE = { date: "date", never: "never", } as const type ExpirationType = (typeof EXPIRATION_TYPE)[keyof typeof EXPIRATION_TYPE] const [expirationType, setExpirationType] = useState( share.expiresAt === null ? EXPIRATION_TYPE.never : EXPIRATION_TYPE.date, ) const { item } = useContext(ItemShareDialogContext) const store = useStore() const setActivePopover = useSetAtom(activePopoverAtom) const dateInputRef = useRef(null) const { mutate: updateShare, isPending: isUpdatingShare } = useMutation({ ...useAtomValue(updateShareMutationAtom), onSuccess: (_updatedShare, _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, }) } setActivePopover(null) }, }) const updateExpirationDate = () => { switch (expirationType) { case EXPIRATION_TYPE.date: if (dateInputRef.current?.date) { updateShare({ shareId: share.id, expiresAt: dateInputRef.current.date, }) } break case EXPIRATION_TYPE.never: updateShare({ shareId: share.id, expiresAt: null, }) break } } return ( { if (!nextOpen) { setActivePopover(null) } }} > {children}

    Configure expiration

    The share link will be accessible until the selected date.

    {expirationType === EXPIRATION_TYPE.date ? ( ) : null}
    ) } function ShareLinkOptionsMenu({ share, children, activePopoverAtom, }: React.PropsWithChildren<{ share: Share activePopoverAtom: PrimitiveAtom }>) { const { item } = useContext(ItemShareDialogContext) const setActivePopover = useSetAtom(activePopoverAtom) 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 ( {children} { setActivePopover(ACTIVE_POPOVER_KIND.rename) }} > Rename link { setActivePopover(ACTIVE_POPOVER_KIND.setExpiration) }} > Configure 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 ( ) }