import type { QueryClient } from "@tanstack/react-query"; import { qk } from "./queryKeys"; import { api, type NewShareLink } from "./api"; import { copyText } from "./clipboard"; import { notify } from "./notifications"; import { notifyActionError } from "./actionErrors"; // The download queue, the per-card download index, the library (its listings and folder counts) and // the storage meter all move together whenever a job is enqueued, deleted, finishes, or a file is // renamed or moved — invalidate them as one unit. export function invalidateDownloads(qc: QueryClient) { qc.invalidateQueries({ queryKey: qk.downloads() }); qc.invalidateQueries({ queryKey: qk.downloadIndex() }); qc.invalidateQueries({ queryKey: qk.files() }); qc.invalidateQueries({ queryKey: qk.fileFolders() }); qc.invalidateQueries({ queryKey: qk.storageUsage() }); } // What a public link opens: one file, or one of the user's own folders (whatever is in it when the // link is opened). export type LinkTarget = { kind: "file"; id: number } | { kind: "folder"; id: number }; export const linksKey = (target: LinkTarget) => target.kind === "file" ? qk.downloadLinks(target.id) : qk.folderLinks(target.id); export const fetchLinks = (target: LinkTarget) => target.kind === "file" ? api.downloadLinks(target.id) : api.folderLinks(target.id); export const makeLink = (target: LinkTarget, body: NewShareLink) => target.kind === "file" ? api.createDownloadLink(target.id, body) : api.createFolderLink(target.id, body); /** After a link was made, changed or revoked: its list, and — for a folder — the rail's mark of the * folders a live link opens (an expiry change can end it too). */ export function invalidateLinks(qc: QueryClient, target: LinkTarget) { void qc.invalidateQueries({ queryKey: linksKey(target) }); if (target.kind === "folder") void qc.invalidateQueries({ queryKey: qk.fileFolders() }); } // One-click share: the row's Share button turns "share this" into a single action. // Ensure the file or folder has a public link (minting one on demand — omitting `allow_download` so // the backend default makes it downloadable), copy it to the clipboard, and toast with an "open in // new tab" action. A copy failure still toasts (warning) WITH that action, so the link is never a dead // end. Advanced controls (password, expiry, revoke, share-with-a-user) live in the Share dialog. // Returns the shared URL, or null if link creation failed — so the caller/tests can react to the outcome. export async function quickShareLink( qc: QueryClient, target: LinkTarget, t: (key: string) => string, ): Promise { let path; try { const existing = await qc.fetchQuery({ queryKey: linksKey(target), queryFn: () => fetchLinks(target), }); // The newest link whose address can still be shown; one that cannot is no use to copy. path = existing.find((l) => l.url)?.url; if (!path) { const made = await makeLink(target, {}); // omit allow_download → backend default (true) qc.setQueryData(linksKey(target), [made, ...existing]); if (target.kind === "folder") void qc.invalidateQueries({ queryKey: qk.fileFolders() }); path = made.url; } if (!path) throw new Error("the new link came back without its address"); } catch (e) { notifyActionError(e, t("downloads.share.linkFailed")); return null; } const url = window.location.origin + path; const action = { label: t("downloads.share.openNewTab"), onClick: () => window.open(url, "_blank", "noopener,noreferrer"), }; notify( (await copyText(url)) ? { level: "success", message: t("downloads.share.linkCopied"), action } : { level: "warning", message: t("downloads.share.copyFailed"), action }, ); return url; }