mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-03-07 06:07:01 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9edb450b6a | ||
|
|
4fa1f57351 | ||
|
|
f3d30085de | ||
|
|
da8761387f | ||
|
|
c9fd573b31 | ||
|
|
c99f9edd9a | ||
|
|
389a96dadc | ||
|
|
c8b1129e4f | ||
|
|
b9fd802288 |
@@ -120,8 +120,8 @@ const RootComponent = ({
|
||||
auth: MobileAuth;
|
||||
}) => {
|
||||
const { colorScheme } = useColorScheme();
|
||||
const updateLink = useUpdateLink(auth);
|
||||
const deleteLink = useDeleteLink(auth);
|
||||
const updateLink = useUpdateLink({ auth, Alert });
|
||||
const deleteLink = useDeleteLink({ auth, Alert });
|
||||
|
||||
const { tmp } = useTmpStore();
|
||||
|
||||
@@ -229,12 +229,12 @@ const RootComponent = ({
|
||||
{tmp.link && tmp.user && (
|
||||
<DropdownMenu.Item
|
||||
key="pin-link"
|
||||
onSelect={async () => {
|
||||
onSelect={() => {
|
||||
const isAlreadyPinned =
|
||||
tmp.link?.pinnedBy && tmp.link.pinnedBy[0]
|
||||
? true
|
||||
: false;
|
||||
await updateLink.mutateAsync({
|
||||
updateLink.mutateAsync({
|
||||
...(tmp.link as LinkIncludingShortenedCollectionAndTags),
|
||||
pinnedBy: (isAlreadyPinned
|
||||
? [{ id: undefined }]
|
||||
@@ -282,18 +282,15 @@ const RootComponent = ({
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: () => {
|
||||
onPress: async () => {
|
||||
deleteLink.mutate(
|
||||
tmp.link?.id as number,
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await deleteLinkCache(
|
||||
tmp.link?.id as number
|
||||
);
|
||||
},
|
||||
}
|
||||
tmp.link?.id as number
|
||||
);
|
||||
// go back
|
||||
|
||||
await deleteLinkCache(
|
||||
tmp.link?.id as number
|
||||
);
|
||||
|
||||
router.back();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function IncomingScreen() {
|
||||
const { auth } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const { data, updateData } = useDataStore();
|
||||
const addLink = useAddLink(auth);
|
||||
const addLink = useAddLink({ auth });
|
||||
const { colorScheme } = useColorScheme();
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
export default function AddLinkSheet() {
|
||||
const actionSheetRef = useRef<ActionSheetRef>(null);
|
||||
const { auth } = useAuthStore();
|
||||
const addLink = useAddLink(auth);
|
||||
const addLink = useAddLink({ auth, Alert });
|
||||
const [link, setLink] = useState("");
|
||||
const { colorScheme } = useColorScheme();
|
||||
|
||||
@@ -43,21 +43,12 @@ export default function AddLinkSheet() {
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={() =>
|
||||
addLink.mutate(
|
||||
{ url: link },
|
||||
{
|
||||
onSuccess: () => {
|
||||
actionSheetRef.current?.hide();
|
||||
setLink("");
|
||||
},
|
||||
onError: (error) => {
|
||||
Alert.alert("Error", "There was an error adding the link.");
|
||||
console.error("Error adding link:", error);
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
onPress={() => {
|
||||
addLink.mutate({ url: link });
|
||||
|
||||
actionSheetRef.current?.hide();
|
||||
setLink("");
|
||||
}}
|
||||
isLoading={addLink.isPending}
|
||||
variant="accent"
|
||||
className="mb-2"
|
||||
|
||||
@@ -33,7 +33,7 @@ const Main = (props: SheetProps<"edit-link-sheet">) => {
|
||||
const [link, setLink] = useState<
|
||||
LinkIncludingShortenedCollectionAndTags | undefined
|
||||
>(props.payload?.link);
|
||||
const editLink = useUpdateLink(auth);
|
||||
const updateLink = useUpdateLink({ auth, Alert });
|
||||
const router = useSheetRouter("edit-link-sheet");
|
||||
const { colorScheme } = useColorScheme();
|
||||
|
||||
@@ -124,23 +124,15 @@ const Main = (props: SheetProps<"edit-link-sheet">) => {
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={() =>
|
||||
editLink.mutate(link as LinkIncludingShortenedCollectionAndTags, {
|
||||
onSuccess: () => {
|
||||
if (link && tmp.link)
|
||||
updateTmp({
|
||||
link,
|
||||
});
|
||||
|
||||
SheetManager.hide("edit-link-sheet");
|
||||
},
|
||||
onError: (error) => {
|
||||
Alert.alert("Error", "There was an error editing the link.");
|
||||
console.error("Error editing link:", error);
|
||||
},
|
||||
})
|
||||
}
|
||||
isLoading={editLink.isPending}
|
||||
onPress={() => {
|
||||
updateLink.mutate(link as LinkIncludingShortenedCollectionAndTags);
|
||||
if (link && tmp.link)
|
||||
updateTmp({
|
||||
link,
|
||||
});
|
||||
SheetManager.hide("edit-link-sheet");
|
||||
}}
|
||||
isLoading={updateLink.isPending}
|
||||
variant="accent"
|
||||
className="mb-2"
|
||||
>
|
||||
@@ -162,7 +154,7 @@ const Main = (props: SheetProps<"edit-link-sheet">) => {
|
||||
|
||||
const Collections = () => {
|
||||
const { auth } = useAuthStore();
|
||||
const addLink = useAddLink(auth);
|
||||
const addLink = useAddLink({ auth });
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const router = useSheetRouter("edit-link-sheet");
|
||||
const { link: currentLink } = useSheetRouteParams<
|
||||
@@ -270,7 +262,7 @@ const Collections = () => {
|
||||
|
||||
const Tags = () => {
|
||||
const { auth } = useAuthStore();
|
||||
const addLink = useAddLink(auth);
|
||||
const addLink = useAddLink({ auth });
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const router = useSheetRouter("edit-link-sheet");
|
||||
const params = useSheetRouteParams("edit-link-sheet", "tags");
|
||||
|
||||
@@ -19,7 +19,7 @@ const CollectionListing = ({ collection }: Props) => {
|
||||
const router = useRouter();
|
||||
const { colorScheme } = useColorScheme();
|
||||
|
||||
const deleteCollection = useDeleteCollection(auth);
|
||||
const deleteCollection = useDeleteCollection({ auth, Alert });
|
||||
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
|
||||
@@ -40,12 +40,12 @@ type Props = {
|
||||
const LinkListing = ({ link, dashboard }: Props) => {
|
||||
const { auth } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const updateLink = useUpdateLink(auth);
|
||||
const updateLink = useUpdateLink({ auth, Alert });
|
||||
const { data: user } = useUser(auth);
|
||||
const { colorScheme } = useColorScheme();
|
||||
const { data } = useDataStore();
|
||||
|
||||
const deleteLink = useDeleteLink(auth);
|
||||
const deleteLink = useDeleteLink({ auth, Alert });
|
||||
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
@@ -57,7 +57,7 @@ const LinkListing = ({ link, dashboard }: Props) => {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [link]);
|
||||
}, [link.url]);
|
||||
|
||||
return (
|
||||
<ContextMenu.Root>
|
||||
@@ -122,8 +122,8 @@ const LinkListing = ({ link, dashboard }: Props) => {
|
||||
<View className="flex flex-row gap-1 items-center mt-1.5 pr-1.5 self-start rounded-md">
|
||||
<Folder
|
||||
size={16}
|
||||
fill={link.collection.color || ""}
|
||||
color={link.collection.color || ""}
|
||||
fill={link.collection.color || "#0ea5e9"}
|
||||
color={link.collection.color || "#0ea5e9"}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
@@ -215,11 +215,11 @@ const LinkListing = ({ link, dashboard }: Props) => {
|
||||
|
||||
<ContextMenu.Item
|
||||
key="pin-link"
|
||||
onSelect={async () => {
|
||||
onSelect={() => {
|
||||
const isAlreadyPinned =
|
||||
link?.pinnedBy && link.pinnedBy[0] ? true : false;
|
||||
|
||||
await updateLink.mutateAsync({
|
||||
updateLink.mutateAsync({
|
||||
...link,
|
||||
pinnedBy: (isAlreadyPinned
|
||||
? [{ id: undefined }]
|
||||
@@ -319,12 +319,10 @@ const LinkListing = ({ link, dashboard }: Props) => {
|
||||
{
|
||||
text: "Delete",
|
||||
style: "destructive",
|
||||
onPress: () => {
|
||||
deleteLink.mutate(link.id as number, {
|
||||
onSuccess: async () => {
|
||||
await deleteLinkCache(link.id as number);
|
||||
},
|
||||
});
|
||||
onPress: async () => {
|
||||
deleteLink.mutate(link.id as number);
|
||||
|
||||
await deleteLinkCache(link.id as number);
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import useOnScreen from "@/hooks/useOnScreen";
|
||||
import { useCollections } from "@linkwarden/router/collections";
|
||||
import { useUser } from "@linkwarden/router/user";
|
||||
import { useGetLink, useLinks } from "@linkwarden/router/links";
|
||||
import { useGetLink } from "@linkwarden/router/links";
|
||||
import { useRouter } from "next/router";
|
||||
import openLink from "@/lib/client/openLink";
|
||||
import LinkIcon from "./LinkViews/LinkComponents/LinkIcon";
|
||||
@@ -82,8 +82,6 @@ export function Card({ link, editMode, dashboardType }: Props) {
|
||||
settings: { show },
|
||||
} = useLocalSettingsStore();
|
||||
|
||||
const { links } = useLinks();
|
||||
|
||||
const router = useRouter();
|
||||
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
|
||||
|
||||
@@ -102,7 +100,7 @@ export function Card({ link, editMode, dashboardType }: Props) {
|
||||
(e) => e.id === link.collection.id
|
||||
) as CollectionIncludingMembersAndLinkCount
|
||||
);
|
||||
}, [collections, links]);
|
||||
}, [collections, link]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isVisible = useOnScreen(ref);
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function DragNDrop({
|
||||
onDragEnd: onDragEndProp,
|
||||
}: DragNDropProps) {
|
||||
const { t } = useTranslation();
|
||||
const updateLink = useUpdateLink();
|
||||
const updateLink = useUpdateLink({ toast, t });
|
||||
const pinLink = usePinLink();
|
||||
const { data: user } = useUser();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -104,25 +104,7 @@ export default function DragNDrop({
|
||||
updatedLink: LinkIncludingShortenedCollectionAndTags,
|
||||
opts?: { invalidateDashboardOnError?: boolean }
|
||||
) => {
|
||||
const load = toast.loading(t("updating"));
|
||||
await updateLink.mutateAsync(updatedLink, {
|
||||
onSettled: async (_, error) => {
|
||||
toast.dismiss(load);
|
||||
if (error) {
|
||||
if (
|
||||
opts?.invalidateDashboardOnError &&
|
||||
typeof queryClient !== "undefined"
|
||||
) {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["dashboardData"],
|
||||
});
|
||||
}
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.success(t("updated"));
|
||||
}
|
||||
},
|
||||
});
|
||||
updateLink.mutateAsync(updatedLink);
|
||||
};
|
||||
|
||||
// DROP ON TAG
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function LinkDetails({
|
||||
);
|
||||
};
|
||||
|
||||
const updateLink = useUpdateLink();
|
||||
const updateLink = useUpdateLink({ toast, t });
|
||||
const updateFile = useUpdateFile();
|
||||
|
||||
const submit = async (e?: any) => {
|
||||
@@ -126,21 +126,9 @@ export default function LinkDetails({
|
||||
return;
|
||||
}
|
||||
|
||||
const load = toast.loading(t("updating"));
|
||||
updateLink.mutateAsync(link);
|
||||
|
||||
await updateLink.mutateAsync(link, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.success(t("updated"));
|
||||
setMode && setMode("view");
|
||||
setLink(data);
|
||||
}
|
||||
},
|
||||
});
|
||||
setMode && setMode("view");
|
||||
};
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function LinkActions({
|
||||
const [refreshPreservationsModal, setRefreshPreservationsModal] =
|
||||
useState(false);
|
||||
|
||||
const deleteLink = useDeleteLink();
|
||||
const deleteLink = useDeleteLink({ toast, t });
|
||||
|
||||
const updateArchive = async () => {
|
||||
const load = toast.loading(t("sending_request"));
|
||||
@@ -131,13 +131,7 @@ export default function LinkActions({
|
||||
onClick={async (e) => {
|
||||
if (e.shiftKey) {
|
||||
const load = toast.loading(t("deleting"));
|
||||
await deleteLink.mutateAsync(link.id as number, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
if (error) toast.error(error.message);
|
||||
else toast.success(t("deleted"));
|
||||
},
|
||||
});
|
||||
await deleteLink.mutateAsync(link.id as number);
|
||||
} else {
|
||||
setDeleteLinkModal(true);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types/global";
|
||||
import Image from "next/image";
|
||||
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Icon from "@/components/Icon";
|
||||
import { IconWeight } from "@phosphor-icons/react";
|
||||
import clsx from "clsx";
|
||||
@@ -30,6 +30,10 @@ function LinkIcon({
|
||||
|
||||
const [faviconLoaded, setFaviconLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFaviconLoaded(false);
|
||||
}, [link.url]);
|
||||
|
||||
return (
|
||||
<div onClick={() => onClick && onClick()}>
|
||||
{link.icon ? (
|
||||
|
||||
@@ -66,7 +66,13 @@ export default function MobileNavigation({}: Props) {
|
||||
<MobileNavigationButton href={`/collections`} icon={"bi-folder"} />
|
||||
</div>
|
||||
</div>
|
||||
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||
{newLinkModal && (
|
||||
<NewLinkModal
|
||||
onClose={() => {
|
||||
setNewLinkModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{newCollectionModal && (
|
||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@linkwarden/types/global";
|
||||
import { useRouter } from "next/router";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
@@ -22,7 +21,6 @@ export default function DeleteCollectionModal({
|
||||
const { t } = useTranslation();
|
||||
const [collection, setCollection] =
|
||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const router = useRouter();
|
||||
const permissions = usePermissions(collection.id as number);
|
||||
|
||||
@@ -30,32 +28,15 @@ export default function DeleteCollectionModal({
|
||||
setCollection(activeCollection);
|
||||
}, []);
|
||||
|
||||
const deleteCollection = useDeleteCollection();
|
||||
const deleteCollection = useDeleteCollection({ toast, t });
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
if (!collection) return null;
|
||||
if (!collection) return null;
|
||||
|
||||
setSubmitLoader(true);
|
||||
deleteCollection.mutateAsync(collection.id as number);
|
||||
|
||||
const load = toast.loading(t("deleting_collection"));
|
||||
|
||||
deleteCollection.mutateAsync(collection.id as number, {
|
||||
onSettled: (data, error) => {
|
||||
setSubmitLoader(false);
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("deleted"));
|
||||
router.push("/collections");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
router.push("/collections");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
|
||||
const deleteLink = useDeleteLink();
|
||||
const deleteLink = useDeleteLink({ toast, t });
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -26,26 +26,15 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
||||
}, []);
|
||||
|
||||
const submit = async () => {
|
||||
const load = toast.loading(t("deleting"));
|
||||
deleteLink.mutateAsync(link.id as number);
|
||||
|
||||
await deleteLink.mutateAsync(link.id as number, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
if (
|
||||
router.pathname.startsWith("/links/[id]") ||
|
||||
router.pathname.startsWith("/preserved/[id]")
|
||||
) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
toast.success(t("deleted"));
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
});
|
||||
if (
|
||||
router.pathname.startsWith("/links/[id]") ||
|
||||
router.pathname.startsWith("/preserved/[id]")
|
||||
) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function LinkModal({
|
||||
|
||||
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
|
||||
|
||||
const deleteLink = useDeleteLink();
|
||||
const deleteLink = useDeleteLink({ toast, t });
|
||||
|
||||
const [mode, setMode] = useState<"view" | "edit">(activeMode || "view");
|
||||
|
||||
@@ -51,13 +51,8 @@ export default function LinkModal({
|
||||
setTimeout(() => (document.body.style.pointerEvents = ""), 0);
|
||||
|
||||
if (e.shiftKey && link.id) {
|
||||
const loading = toast.loading(t("deleting"));
|
||||
await deleteLink.mutateAsync(link.id, {
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(loading);
|
||||
error ? toast.error(error.message) : toast.success(t("deleted"));
|
||||
},
|
||||
});
|
||||
deleteLink.mutateAsync(link.id);
|
||||
|
||||
onClose();
|
||||
} else {
|
||||
onDelete();
|
||||
|
||||
@@ -7,14 +7,17 @@ import { useRouter } from "next/router";
|
||||
import Modal from "../Modal";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCollections } from "@linkwarden/router/collections";
|
||||
import { useAddLink } from "@linkwarden/router/links";
|
||||
import toast from "react-hot-toast";
|
||||
import { PostLinkSchemaType } from "@linkwarden/lib/schemaValidation";
|
||||
import {
|
||||
PostLinkSchema,
|
||||
PostLinkSchemaType,
|
||||
} from "@linkwarden/lib/schemaValidation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { useAddLink } from "@linkwarden/router/links";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function NewLinkModal({ onClose }: Props) {
|
||||
@@ -31,10 +34,13 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||
},
|
||||
} as PostLinkSchemaType;
|
||||
|
||||
const addLink = useAddLink({
|
||||
toast,
|
||||
t,
|
||||
});
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [link, setLink] = useState<PostLinkSchemaType>(initial);
|
||||
const addLink = useAddLink();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||
const router = useRouter();
|
||||
const { data: collections = [] } = useCollections();
|
||||
@@ -80,22 +86,17 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||
}, []);
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
const load = toast.loading(t("creating_link"));
|
||||
await addLink.mutateAsync(link, {
|
||||
onSettled: (data, error) => {
|
||||
setSubmitLoader(false);
|
||||
toast.dismiss(load);
|
||||
if (error) {
|
||||
toast.error(t(error.message));
|
||||
} else {
|
||||
onClose();
|
||||
toast.success(t("link_created"));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
const dataValidation = PostLinkSchema.safeParse(link);
|
||||
|
||||
if (!dataValidation.success)
|
||||
return toast.error(
|
||||
`Error: ${
|
||||
dataValidation.error.issues[0].message
|
||||
} [${dataValidation.error.issues[0].path.join(", ")}]`
|
||||
);
|
||||
|
||||
addLink.mutateAsync(link);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -182,7 +182,11 @@ export default function Navbar({
|
||||
</div>
|
||||
)}
|
||||
{newLinkModal && (
|
||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
||||
<NewLinkModal
|
||||
onClose={() => {
|
||||
setNewLinkModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{newCollectionModal && (
|
||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||
|
||||
@@ -40,7 +40,13 @@ export default function NoLinksFound({ text }: Props) {
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||
{newLinkModal && (
|
||||
<NewLinkModal
|
||||
onClose={() => {
|
||||
setNewLinkModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function SearchBar({ placeholder }: Props) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{user?.hasUnIndexedLinks && !dismissSearchNote ? (
|
||||
{/* {user?.hasUnIndexedLinks && !dismissSearchNote ? (
|
||||
<div
|
||||
role="alert"
|
||||
className="border border-neutral p-2 my-1 rounded flex flex-col gap-2"
|
||||
@@ -204,7 +204,7 @@ export default function SearchBar({ placeholder }: Props) {
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined}
|
||||
) : undefined} */}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,39 +6,21 @@ import { useUser } from "@linkwarden/router/user";
|
||||
|
||||
const usePinLink = () => {
|
||||
const { t } = useTranslation();
|
||||
const updateLink = useUpdateLink();
|
||||
const updateLink = useUpdateLink({ toast, t });
|
||||
const { data: user } = useUser();
|
||||
|
||||
// Return a function that can be used to pin/unpin the link
|
||||
const pinLink = async (link: LinkIncludingShortenedCollectionAndTags) => {
|
||||
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0] ? true : false;
|
||||
|
||||
const load = toast.loading(t("updating"));
|
||||
|
||||
try {
|
||||
await updateLink.mutateAsync(
|
||||
{
|
||||
...link,
|
||||
pinnedBy: (isAlreadyPinned
|
||||
? [{ id: undefined }]
|
||||
: [{ id: user?.id }]) as any,
|
||||
},
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.success(
|
||||
isAlreadyPinned ? t("link_unpinned") : t("link_pinned")
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
updateLink.mutateAsync({
|
||||
...link,
|
||||
pinnedBy: (isAlreadyPinned
|
||||
? [{ id: undefined }]
|
||||
: [{ id: user?.id }]) as any,
|
||||
});
|
||||
} catch (e) {
|
||||
toast.dismiss(load);
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -158,7 +158,7 @@ const Page: NextPageWithLayout = () => {
|
||||
<ul className="px-5 list-disc">
|
||||
<Trans
|
||||
i18nKey="regenerate_broken_preserved_content_desc"
|
||||
components={[<li />]}
|
||||
components={[<li key={0} />]}
|
||||
values={{
|
||||
count: workerStats.data?.link.failed,
|
||||
}}
|
||||
@@ -181,7 +181,7 @@ const Page: NextPageWithLayout = () => {
|
||||
<ul className="px-5 list-disc">
|
||||
<Trans
|
||||
i18nKey="delete_all_preserved_content_and_regenerate_desc"
|
||||
components={[<li />]}
|
||||
components={[<li key={0} />]}
|
||||
/>
|
||||
</ul>
|
||||
<div role="alert" className="alert alert-warning mt-3">
|
||||
|
||||
@@ -26,7 +26,6 @@ import ViewDropdown from "@/components/ViewDropdown";
|
||||
import clsx from "clsx";
|
||||
import Icon from "@/components/Icon";
|
||||
import Droppable from "@/components/Droppable";
|
||||
import { useUpdateLink } from "@linkwarden/router/links";
|
||||
import { NextPageWithLayout } from "./_app";
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
@@ -95,7 +94,6 @@ const Page: NextPageWithLayout = () => {
|
||||
const [showSurveyModal, setShowsSurveyModal] = useState(false);
|
||||
|
||||
const updateUser = useUpdateUser();
|
||||
const updateLink = useUpdateLink();
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
@@ -192,7 +190,13 @@ const Page: NextPageWithLayout = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||
{newLinkModal && (
|
||||
<NewLinkModal
|
||||
onClose={() => {
|
||||
setNewLinkModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,14 +50,17 @@ export default async function archiveHandler(
|
||||
process.env.PERPLEXITY_API_KEY)
|
||||
? true
|
||||
: undefined,
|
||||
indexVersion: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
abortController.abort();
|
||||
reject(
|
||||
new Error(
|
||||
@@ -208,6 +211,10 @@ export default async function archiveHandler(
|
||||
console.log("Reason:", err);
|
||||
throw err;
|
||||
} finally {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
const finalLink = await prisma.link.findUnique({
|
||||
where: { id: link.id },
|
||||
});
|
||||
@@ -227,6 +234,7 @@ export default async function archiveHandler(
|
||||
!finalLink.aiTagged
|
||||
? true
|
||||
: undefined,
|
||||
indexVersion: null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -25,17 +25,10 @@ export default async function handleMonolith(
|
||||
|
||||
const child = spawn("monolith", args, {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
detached: true,
|
||||
signal,
|
||||
killSignal: "SIGKILL",
|
||||
});
|
||||
|
||||
const abortListener = () => {
|
||||
try {
|
||||
if (child.pid) killProcess(child.pid);
|
||||
} catch {}
|
||||
reject(new Error("Monolith aborted"));
|
||||
};
|
||||
signal?.addEventListener("abort", abortListener);
|
||||
|
||||
child.stdin.write(htmlFromPage);
|
||||
child.stdin.end();
|
||||
|
||||
@@ -43,14 +36,11 @@ export default async function handleMonolith(
|
||||
child.stdout.on("data", (c) => chunks.push(c));
|
||||
|
||||
child.on("error", (err) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
child.on("close", async (code) => {
|
||||
cleanup();
|
||||
|
||||
if (code !== 0) {
|
||||
if (code !== 0 && code !== null) {
|
||||
return reject(new Error(`Monolith exited with code ${code}`));
|
||||
}
|
||||
|
||||
@@ -64,29 +54,21 @@ export default async function handleMonolith(
|
||||
return reject(new Error("Monolith output exceeded buffer limit"));
|
||||
}
|
||||
|
||||
await createFile({
|
||||
data: html,
|
||||
filePath: `archives/${link.collectionId}/${link.id}.html`,
|
||||
});
|
||||
try {
|
||||
await createFile({
|
||||
data: html,
|
||||
filePath: `archives/${link.collectionId}/${link.id}.html`,
|
||||
});
|
||||
|
||||
await prisma.link.update({
|
||||
where: { id: link.id },
|
||||
data: { monolith: `archives/${link.collectionId}/${link.id}.html` },
|
||||
});
|
||||
await prisma.link.update({
|
||||
where: { id: link.id },
|
||||
data: { monolith: `archives/${link.collectionId}/${link.id}.html` },
|
||||
});
|
||||
|
||||
resolve();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
function cleanup() {
|
||||
signal?.removeEventListener("abort", abortListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const killProcess = async (PID: number) => {
|
||||
if (process.platform === "win32") {
|
||||
process.kill(PID, "SIGKILL");
|
||||
} else {
|
||||
process.kill(-PID, "SIGKILL");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,14 +80,6 @@ export async function startIndexing(interval = 10) {
|
||||
{ indexVersion: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{ lastPreserved: { not: null } },
|
||||
{
|
||||
url: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...(process.env.STRIPE_SECRET_KEY
|
||||
? {
|
||||
@@ -172,7 +164,6 @@ export async function startIndexing(interval = 10) {
|
||||
{ indexVersion: { not: MEILI_INDEX_VERSION } },
|
||||
{ indexVersion: null },
|
||||
],
|
||||
lastPreserved: { not: null },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
MobileAuth,
|
||||
} from "@linkwarden/types/global";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type toaster from "react-hot-toast";
|
||||
import { TFunction } from "next-i18next";
|
||||
import type { Alert as Alert_ } from "react-native";
|
||||
|
||||
const useCollections = (auth?: MobileAuth) => {
|
||||
let status: "loading" | "authenticated" | "unauthenticated";
|
||||
@@ -109,7 +112,17 @@ const useUpdateCollection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const useDeleteCollection = (auth?: MobileAuth) => {
|
||||
const useDeleteCollection = ({
|
||||
auth,
|
||||
Alert,
|
||||
toast,
|
||||
t,
|
||||
}: {
|
||||
auth?: MobileAuth;
|
||||
Alert?: typeof Alert_;
|
||||
toast?: typeof toaster;
|
||||
t?: TFunction;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
@@ -133,6 +146,44 @@ const useDeleteCollection = (auth?: MobileAuth) => {
|
||||
|
||||
return data.response;
|
||||
},
|
||||
onMutate: async (id) => {
|
||||
await queryClient.cancelQueries({ queryKey: ["collections"] });
|
||||
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
|
||||
|
||||
const previousCollections = queryClient.getQueryData(["collections"]);
|
||||
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
|
||||
|
||||
queryClient.setQueryData(["collections"], (oldData: any) => {
|
||||
return oldData?.filter((collection: any) => collection.id !== id);
|
||||
});
|
||||
|
||||
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
|
||||
if (!oldData) return oldData;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
links:
|
||||
oldData.links?.filter((link: any) => link.collectionId !== id) ||
|
||||
[],
|
||||
numberOfPinnedLinks:
|
||||
oldData.links?.filter(
|
||||
(link: any) => link.collectionId !== id && link.isPinned
|
||||
).length || 0,
|
||||
};
|
||||
});
|
||||
|
||||
return { previousCollections, previousDashboard };
|
||||
},
|
||||
onError: (error, _variables, context) => {
|
||||
if (toast && t) toast.error(t(error.message));
|
||||
else if (Alert)
|
||||
Alert.alert("Error", "There was an error deleting the collection.");
|
||||
|
||||
if (!context) return;
|
||||
|
||||
queryClient.setQueryData(["collections"], context.previousCollections);
|
||||
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
|
||||
},
|
||||
onSuccess: (data, id) => {
|
||||
queryClient.setQueryData(["collections"], (oldData: any) => {
|
||||
return oldData.filter((collection: any) => collection.id !== id);
|
||||
@@ -153,6 +204,9 @@ const useDeleteCollection = (auth?: MobileAuth) => {
|
||||
).length || 0,
|
||||
};
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["tags"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["links"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
useQueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
QueryKey,
|
||||
} from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
@@ -18,6 +19,9 @@ import {
|
||||
} from "@linkwarden/lib/schemaValidation";
|
||||
import getFormatFromContentType from "@linkwarden/lib/getFormatFromContentType";
|
||||
import getLinkTypeFromFormat from "@linkwarden/lib/getLinkTypeFromFormat";
|
||||
import type toaster from "react-hot-toast";
|
||||
import { TFunction } from "next-i18next";
|
||||
import type { Alert as Alert_ } from "react-native";
|
||||
|
||||
const useLinks = (params: LinkRequestQuery = {}, auth?: MobileAuth) => {
|
||||
const sort =
|
||||
@@ -111,7 +115,254 @@ const buildQueryString = (params: LinkRequestQuery) => {
|
||||
.join("&");
|
||||
};
|
||||
|
||||
const useAddLink = (auth?: MobileAuth) => {
|
||||
const upsertLinkInList = (
|
||||
links: LinkIncludingShortenedCollectionAndTags[] = [],
|
||||
link: LinkIncludingShortenedCollectionAndTags,
|
||||
optimisticId?: number
|
||||
) => {
|
||||
const existingIndex = links.findIndex(
|
||||
(item) => item.id === optimisticId || item.id === link.id
|
||||
);
|
||||
|
||||
if (existingIndex === -1) return [link, ...links];
|
||||
|
||||
const nextLinks = [...links];
|
||||
nextLinks[existingIndex] = link;
|
||||
return nextLinks;
|
||||
};
|
||||
|
||||
const upsertLinkInInfiniteData = (
|
||||
oldData: any,
|
||||
link: LinkIncludingShortenedCollectionAndTags,
|
||||
optimisticId?: number
|
||||
) => {
|
||||
if (!oldData?.pages?.length) return oldData;
|
||||
|
||||
let replaced = false;
|
||||
const pages = oldData.pages.map((page: any) => {
|
||||
const links = page?.links?.map((item: any) => {
|
||||
if (item.id === optimisticId || item.id === link.id) {
|
||||
replaced = true;
|
||||
return link;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
return { ...page, links };
|
||||
});
|
||||
|
||||
if (!replaced) {
|
||||
const firstPage = pages[0];
|
||||
pages[0] = {
|
||||
...firstPage,
|
||||
links: upsertLinkInList(firstPage?.links ?? [], link, optimisticId),
|
||||
};
|
||||
}
|
||||
|
||||
return { ...oldData, pages };
|
||||
};
|
||||
|
||||
const upsertLinkInDashboardData = (
|
||||
oldData: any,
|
||||
link: LinkIncludingShortenedCollectionAndTags,
|
||||
optimisticId?: number
|
||||
) => {
|
||||
if (!oldData) return oldData;
|
||||
|
||||
const updatedLinks = upsertLinkInList(
|
||||
oldData.links ?? [],
|
||||
link,
|
||||
optimisticId
|
||||
).slice(0, 16);
|
||||
|
||||
const collectionLinks = { ...(oldData.collectionLinks ?? {}) };
|
||||
const collectionId = link.collection?.id;
|
||||
|
||||
if (collectionId != null && collectionLinks[collectionId]) {
|
||||
collectionLinks[collectionId] = upsertLinkInList(
|
||||
collectionLinks[collectionId],
|
||||
link,
|
||||
optimisticId
|
||||
).slice(0, 16);
|
||||
}
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
links: updatedLinks,
|
||||
collectionLinks,
|
||||
};
|
||||
};
|
||||
|
||||
const removeLinkFromInfiniteData = (oldData: any, linkId: number) => {
|
||||
if (!oldData?.pages?.length) return oldData;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
pages: oldData.pages.map((page: any) => ({
|
||||
...page,
|
||||
links: (page.links ?? []).filter((item: any) => item.id !== linkId),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const removeLinkFromDashboardData = (oldData: any, linkId: number) => {
|
||||
if (!oldData) return oldData;
|
||||
|
||||
const removedLink = (oldData.links ?? []).find(
|
||||
(item: any) => item.id === linkId
|
||||
);
|
||||
const numberOfPinnedLinks = removedLink?.pinnedBy?.length
|
||||
? Math.max(0, (oldData.numberOfPinnedLinks ?? 0) - 1)
|
||||
: oldData.numberOfPinnedLinks;
|
||||
|
||||
const hasCollectionLinks = oldData.collectionLinks != null;
|
||||
const collectionLinks = hasCollectionLinks
|
||||
? { ...oldData.collectionLinks }
|
||||
: oldData.collectionLinks;
|
||||
if (hasCollectionLinks) {
|
||||
for (const [collectionId, links] of Object.entries(collectionLinks)) {
|
||||
collectionLinks[Number(collectionId)] = (links as any[]).filter(
|
||||
(item: any) => item.id !== linkId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
links: (oldData.links ?? []).filter((item: any) => item.id !== linkId),
|
||||
collectionLinks,
|
||||
numberOfPinnedLinks,
|
||||
};
|
||||
};
|
||||
|
||||
const isLinkPinned = (link?: LinkIncludingShortenedCollectionAndTags) => {
|
||||
return Boolean(link?.pinnedBy && link.pinnedBy.length > 0);
|
||||
};
|
||||
|
||||
const findLinkInInfiniteData = (data: any, linkId: number) => {
|
||||
if (!data?.pages?.length) return undefined;
|
||||
for (const page of data.pages) {
|
||||
const match = page?.links?.find((item: any) => item.id === linkId);
|
||||
if (match) return match;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const findLinkInQueriesData = (
|
||||
queries: [QueryKey, unknown][],
|
||||
linkId: number
|
||||
) => {
|
||||
for (const [, data] of queries ?? []) {
|
||||
const match = findLinkInInfiniteData(data as any, linkId);
|
||||
if (match) return match;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const findLinkInDashboardData = (data: any, linkId: number) => {
|
||||
const match = data?.links?.find((item: any) => item.id === linkId);
|
||||
if (match) return match;
|
||||
|
||||
const collectionLinks = data?.collectionLinks;
|
||||
if (!collectionLinks) return undefined;
|
||||
|
||||
for (const links of Object.values(collectionLinks)) {
|
||||
const collectionMatch = (links as any[])?.find(
|
||||
(item: any) => item.id === linkId
|
||||
);
|
||||
if (collectionMatch) return collectionMatch;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const replaceLinkInInfiniteData = (
|
||||
oldData: any,
|
||||
link: LinkIncludingShortenedCollectionAndTags
|
||||
) => {
|
||||
if (!oldData?.pages?.length) return oldData;
|
||||
|
||||
let updated = false;
|
||||
const pages = oldData.pages.map((page: any) => {
|
||||
const links = (page.links ?? []).map((item: any) => {
|
||||
if (item.id === link.id) {
|
||||
updated = true;
|
||||
return link;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return { ...page, links };
|
||||
});
|
||||
|
||||
if (!updated) return oldData;
|
||||
|
||||
return { ...oldData, pages };
|
||||
};
|
||||
|
||||
const replaceLinkInDashboardData = (
|
||||
oldData: any,
|
||||
link: LinkIncludingShortenedCollectionAndTags
|
||||
) => {
|
||||
if (!oldData) return oldData;
|
||||
|
||||
let updated = false;
|
||||
const links = (oldData.links ?? []).map((item: any) => {
|
||||
if (item.id === link.id) {
|
||||
updated = true;
|
||||
return link;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
let collectionLinks = oldData.collectionLinks;
|
||||
if (oldData.collectionLinks != null) {
|
||||
const nextCollectionLinks = { ...oldData.collectionLinks };
|
||||
for (const [collectionId, linksForCollection] of Object.entries(
|
||||
nextCollectionLinks
|
||||
)) {
|
||||
const linkList = linksForCollection as any[];
|
||||
if (!Array.isArray(linkList)) continue;
|
||||
if (!linkList.some((item) => item.id === link.id)) continue;
|
||||
updated = true;
|
||||
nextCollectionLinks[Number(collectionId)] = linkList.map((item) =>
|
||||
item.id === link.id ? link : item
|
||||
);
|
||||
}
|
||||
collectionLinks = nextCollectionLinks;
|
||||
}
|
||||
|
||||
if (!updated) return oldData;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
links,
|
||||
collectionLinks,
|
||||
};
|
||||
};
|
||||
|
||||
const applyPinnedDelta = (oldData: any, delta: number) => {
|
||||
if (!oldData || !delta) return oldData;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
numberOfPinnedLinks: Math.max(
|
||||
0,
|
||||
(oldData.numberOfPinnedLinks ?? 0) + delta
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const useAddLink = ({
|
||||
auth,
|
||||
Alert,
|
||||
toast,
|
||||
t,
|
||||
}: {
|
||||
auth?: MobileAuth;
|
||||
Alert?: typeof Alert_;
|
||||
toast?: typeof toaster;
|
||||
t?: TFunction;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
@@ -144,20 +395,118 @@ const useAddLink = (auth?: MobileAuth) => {
|
||||
|
||||
return data.response;
|
||||
},
|
||||
onSuccess: (data: LinkIncludingShortenedCollectionAndTags[]) => {
|
||||
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => {
|
||||
if (!oldData) return undefined;
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
links: [data, ...oldData?.pages?.[0]?.links],
|
||||
nextCursor: oldData?.pages?.[0]?.nextCursor,
|
||||
},
|
||||
...oldData?.pages?.slice(1),
|
||||
],
|
||||
pageParams: oldData?.pageParams,
|
||||
};
|
||||
onMutate: async (link) => {
|
||||
await queryClient.cancelQueries({ queryKey: ["links"] });
|
||||
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
|
||||
|
||||
const previousLinks = queryClient.getQueriesData({
|
||||
queryKey: ["links"],
|
||||
});
|
||||
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
|
||||
|
||||
const collections =
|
||||
(queryClient.getQueryData(["collections"]) as any[]) ?? [];
|
||||
const tags = (queryClient.getQueryData(["tags"]) as any[]) ?? [];
|
||||
const user = queryClient.getQueryData(["user"]) as any;
|
||||
|
||||
const collectionFromId =
|
||||
link.collection?.id != null
|
||||
? collections.find(
|
||||
(collection) => collection.id === link.collection?.id
|
||||
)
|
||||
: undefined;
|
||||
const collectionFromName =
|
||||
!collectionFromId && link.collection?.name
|
||||
? collections.find(
|
||||
(collection) => collection.name === link.collection?.name
|
||||
)
|
||||
: undefined;
|
||||
const resolvedCollection = collectionFromId ?? collectionFromName;
|
||||
|
||||
const tempId = -Date.now();
|
||||
const tempCollectionId = tempId - 1;
|
||||
const collectionId =
|
||||
resolvedCollection?.id ?? link.collection?.id ?? tempCollectionId;
|
||||
const collectionName =
|
||||
resolvedCollection?.name ?? link.collection?.name ?? "Unorganized";
|
||||
|
||||
const resolvedTags =
|
||||
link.tags?.map((tag, index) => {
|
||||
if (tag.id != null) {
|
||||
return (
|
||||
tags.find((existing) => existing.id === tag.id) ?? {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const existingTag = tags.find(
|
||||
(existing) => existing.name === tag.name
|
||||
);
|
||||
return (
|
||||
existingTag ?? {
|
||||
id: tempId - 2 - index,
|
||||
name: tag.name,
|
||||
}
|
||||
);
|
||||
}) ?? [];
|
||||
|
||||
const optimisticLink = {
|
||||
id: tempId,
|
||||
name: link.name?.trim() || link.url || "",
|
||||
url: link.url || "",
|
||||
description: link.description || "",
|
||||
type: link.type || "url",
|
||||
preview: "",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
collectionId,
|
||||
collection:
|
||||
resolvedCollection ??
|
||||
({
|
||||
id: collectionId,
|
||||
name: collectionName,
|
||||
ownerId: user?.id ?? 0,
|
||||
} as any),
|
||||
tags: resolvedTags,
|
||||
} as LinkIncludingShortenedCollectionAndTags;
|
||||
|
||||
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
|
||||
upsertLinkInInfiniteData(oldData, optimisticLink, tempId)
|
||||
);
|
||||
|
||||
queryClient.setQueryData(["dashboardData"], (oldData: any) =>
|
||||
upsertLinkInDashboardData(oldData, optimisticLink, tempId)
|
||||
);
|
||||
|
||||
return {
|
||||
previousLinks,
|
||||
previousDashboard,
|
||||
optimisticId: tempId,
|
||||
};
|
||||
},
|
||||
onError: (error, _variables, context) => {
|
||||
if (toast && t) toast.error(t(error.message));
|
||||
else if (Alert)
|
||||
Alert.alert("Error", "There was an error adding the link.");
|
||||
|
||||
if (!context) return;
|
||||
|
||||
context.previousLinks?.forEach(([queryKey, data]: [unknown, unknown]) => {
|
||||
queryClient.setQueryData(queryKey as QueryKey, data);
|
||||
});
|
||||
|
||||
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
|
||||
},
|
||||
onSuccess: (
|
||||
data: LinkIncludingShortenedCollectionAndTags,
|
||||
_link,
|
||||
context
|
||||
) => {
|
||||
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
|
||||
upsertLinkInInfiniteData(oldData, data, context?.optimisticId)
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["dashboardData"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["collections"] });
|
||||
@@ -167,7 +516,17 @@ const useAddLink = (auth?: MobileAuth) => {
|
||||
});
|
||||
};
|
||||
|
||||
const useUpdateLink = (auth?: MobileAuth) => {
|
||||
const useUpdateLink = ({
|
||||
auth,
|
||||
Alert,
|
||||
toast,
|
||||
t,
|
||||
}: {
|
||||
auth?: MobileAuth;
|
||||
Alert?: typeof Alert_;
|
||||
toast?: typeof toaster;
|
||||
t?: TFunction;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
@@ -192,6 +551,117 @@ const useUpdateLink = (auth?: MobileAuth) => {
|
||||
|
||||
return data.response;
|
||||
},
|
||||
onMutate: async (link) => {
|
||||
const linkId = link.id;
|
||||
if (linkId == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
await queryClient.cancelQueries({ queryKey: ["links"] });
|
||||
await queryClient.cancelQueries({ queryKey: ["publicLinks"] });
|
||||
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
|
||||
|
||||
const previousLinks = queryClient.getQueriesData({
|
||||
queryKey: ["links"],
|
||||
});
|
||||
const previousPublicLinks = queryClient.getQueriesData({
|
||||
queryKey: ["publicLinks"],
|
||||
});
|
||||
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
|
||||
const previousLinkQueries = queryClient.getQueriesData({
|
||||
queryKey: ["link", linkId],
|
||||
});
|
||||
|
||||
const cachedLink =
|
||||
findLinkInQueriesData(previousLinks, linkId) ??
|
||||
findLinkInDashboardData(previousDashboard, linkId);
|
||||
const collections =
|
||||
(queryClient.getQueryData(["collections"]) as any[]) ?? [];
|
||||
const nextCollectionId =
|
||||
link.collection?.id ?? cachedLink?.collection?.id;
|
||||
const resolvedCollection =
|
||||
nextCollectionId != null
|
||||
? collections.find((collection) => collection.id === nextCollectionId)
|
||||
: undefined;
|
||||
const optimisticCollection =
|
||||
resolvedCollection ??
|
||||
(link.collection?.id &&
|
||||
cachedLink?.collection?.id === link.collection.id
|
||||
? { ...cachedLink.collection, ...link.collection }
|
||||
: link.collection ?? cachedLink?.collection);
|
||||
|
||||
const previousPinned = isLinkPinned(cachedLink);
|
||||
const nextPinned =
|
||||
typeof link.pinnedBy === "undefined"
|
||||
? previousPinned
|
||||
: isLinkPinned(link);
|
||||
const pinnedDelta =
|
||||
nextPinned === previousPinned ? 0 : nextPinned ? 1 : -1;
|
||||
|
||||
const optimisticLink = {
|
||||
...(cachedLink ?? {}),
|
||||
...link,
|
||||
collection: optimisticCollection,
|
||||
collectionId:
|
||||
optimisticCollection?.id ??
|
||||
link.collection?.id ??
|
||||
cachedLink?.collectionId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as LinkIncludingShortenedCollectionAndTags;
|
||||
|
||||
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
|
||||
replaceLinkInInfiniteData(oldData, optimisticLink)
|
||||
);
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ["publicLinks"] },
|
||||
(oldData: any) => replaceLinkInInfiniteData(oldData, optimisticLink)
|
||||
);
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ["link", linkId] },
|
||||
() => optimisticLink
|
||||
);
|
||||
|
||||
queryClient.setQueryData(["dashboardData"], (oldData: any) =>
|
||||
applyPinnedDelta(
|
||||
replaceLinkInDashboardData(oldData, optimisticLink),
|
||||
pinnedDelta
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
previousLinks,
|
||||
previousPublicLinks,
|
||||
previousDashboard,
|
||||
previousLinkQueries,
|
||||
};
|
||||
},
|
||||
onError: (error, _variables, context) => {
|
||||
if (toast && t) toast.error(t(error.message));
|
||||
else if (Alert)
|
||||
Alert.alert("Error", "There was an error updating the link.");
|
||||
|
||||
if (!context) return;
|
||||
|
||||
context.previousLinks?.forEach(([queryKey, data]: [unknown, unknown]) => {
|
||||
queryClient.setQueryData(queryKey as QueryKey, data);
|
||||
});
|
||||
|
||||
context.previousPublicLinks?.forEach(
|
||||
([queryKey, data]: [unknown, unknown]) => {
|
||||
queryClient.setQueryData(queryKey as QueryKey, data);
|
||||
}
|
||||
);
|
||||
|
||||
context.previousLinkQueries?.forEach(
|
||||
([queryKey, data]: [unknown, unknown]) => {
|
||||
queryClient.setQueryData(queryKey as QueryKey, data);
|
||||
}
|
||||
);
|
||||
|
||||
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["links"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["link", data.id] });
|
||||
@@ -203,7 +673,17 @@ const useUpdateLink = (auth?: MobileAuth) => {
|
||||
});
|
||||
};
|
||||
|
||||
const useDeleteLink = (auth?: MobileAuth) => {
|
||||
const useDeleteLink = ({
|
||||
auth,
|
||||
Alert,
|
||||
toast,
|
||||
t,
|
||||
}: {
|
||||
auth?: MobileAuth;
|
||||
Alert?: typeof Alert_;
|
||||
toast?: typeof toaster;
|
||||
t?: TFunction;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
@@ -226,6 +706,57 @@ const useDeleteLink = (auth?: MobileAuth) => {
|
||||
|
||||
return data.response;
|
||||
},
|
||||
onMutate: async (id) => {
|
||||
await queryClient.cancelQueries({ queryKey: ["links"] });
|
||||
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
|
||||
await queryClient.cancelQueries({ queryKey: ["publicLinks"] });
|
||||
|
||||
const previousLinks = queryClient.getQueriesData({
|
||||
queryKey: ["links"],
|
||||
});
|
||||
const previousPublicLinks = queryClient.getQueriesData({
|
||||
queryKey: ["publicLinks"],
|
||||
});
|
||||
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
|
||||
|
||||
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
|
||||
removeLinkFromInfiniteData(oldData, id)
|
||||
);
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ["publicLinks"] },
|
||||
(oldData: any) => removeLinkFromInfiniteData(oldData, id)
|
||||
);
|
||||
|
||||
queryClient.setQueryData(["dashboardData"], (oldData: any) =>
|
||||
removeLinkFromDashboardData(oldData, id)
|
||||
);
|
||||
|
||||
return {
|
||||
previousLinks,
|
||||
previousPublicLinks,
|
||||
previousDashboard,
|
||||
};
|
||||
},
|
||||
onError: (error, _variables, context) => {
|
||||
if (toast && t) toast.error(t(error.message));
|
||||
else if (Alert)
|
||||
Alert.alert("Error", "There was an error deleting the link.");
|
||||
|
||||
if (!context) return;
|
||||
|
||||
context.previousLinks?.forEach(([queryKey, data]: [unknown, unknown]) => {
|
||||
queryClient.setQueryData(queryKey as QueryKey, data);
|
||||
});
|
||||
|
||||
context.previousPublicLinks?.forEach(
|
||||
([queryKey, data]: [unknown, unknown]) => {
|
||||
queryClient.setQueryData(queryKey as QueryKey, data);
|
||||
}
|
||||
);
|
||||
|
||||
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => {
|
||||
if (!oldData?.pages?.[0]) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user