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 = (
{shares.map((share) => (
))}
)
}
return (
)
}
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"}
}
to={ }
/>
Copy share link
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 = (
Share link options
)
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}
{
setActivePopover(null)
}}
>
Cancel
Save{" "}
⏎
)
}
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}
{
setExpirationType(value as ExpirationType)
}}
>
Link expires on a date
Link never expires
{expirationType === EXPIRATION_TYPE.date ? (
) : null}
{
setActivePopover(null)
}}
>
Cancel
Save{" "}
⏎
)
}
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 (
{
createShare({ items: [item.id] })
}}
>
Create share link
)
}