Files
linkwarden/apps/mobile/components/LinkListing.tsx

342 lines
11 KiB
TypeScript

import {
View,
Text,
Image,
Pressable,
Platform,
Alert,
ActivityIndicator,
Linking,
} from "react-native";
import { decode } from "html-entities";
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types";
import { ArchivedFormat } from "@linkwarden/types";
import getFormatBasedOnPreference from "@linkwarden/lib/getFormatBasedOnPreference";
import getOriginalFormat from "@linkwarden/lib/getOriginalFormat";
import {
atLeastOneFormatAvailable,
formatAvailable,
} from "@linkwarden/lib/formatStats";
import useAuthStore from "@/store/auth";
import { useRouter } from "expo-router";
import * as ContextMenu from "zeego/context-menu";
import { useDeleteLink, useUpdateLink } from "@linkwarden/router/links";
import { SheetManager } from "react-native-actions-sheet";
import * as Clipboard from "expo-clipboard";
import { cn } from "@linkwarden/lib/utils";
import { useUser } from "@linkwarden/router/user";
import { rawTheme, ThemeName } from "@/lib/colors";
import { useColorScheme } from "nativewind";
import { CalendarDays, Folder } from "lucide-react-native";
import useDataStore from "@/store/data";
import { useEffect, useState } from "react";
import { deleteLinkCache } from "@/lib/cache";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
dashboard?: boolean;
};
const LinkListing = ({ link, dashboard }: Props) => {
const { auth } = useAuthStore();
const router = useRouter();
const updateLink = useUpdateLink(auth);
const { data: user } = useUser(auth);
const { colorScheme } = useColorScheme();
const { data } = useDataStore();
const deleteLink = useDeleteLink(auth);
const [url, setUrl] = useState("");
useEffect(() => {
try {
if (link.url) {
setUrl(new URL(link.url).host.toLowerCase());
}
} catch (error) {
console.log(error);
}
}, [link]);
return (
<ContextMenu.Root>
<ContextMenu.Trigger asChild>
<Pressable
className={cn(
"p-5 flex-row justify-between",
dashboard ? "bg-base-200" : "bg-base-100",
Platform.OS !== "android" && "active:bg-base-200/50",
dashboard && "rounded-xl"
)}
onLongPress={() => {}}
onPress={() => {
if (user) {
const format = getFormatBasedOnPreference({
link,
preference: user.linksRouteTo,
});
data.preferredBrowser === "app"
? router.navigate(
format !== null
? `/links/${link.id}?format=${format}`
: `/links/${link.id}`
)
: Linking.openURL(
format !== null
? auth.instance +
`/preserved/${link?.id}?format=${format}`
: (link.url as string)
);
}
}}
android_ripple={{
color: colorScheme === "dark" ? "rgba(255,255,255,0.2)" : "#ddd",
borderless: false,
}}
>
<View
className={cn(
"flex-row justify-between",
dashboard ? "w-80" : "w-full"
)}
>
<View className="w-[65%] flex-col justify-between">
<Text
numberOfLines={2}
className="font-medium text-lg text-base-content"
>
{decode(link.name || link.description || link.url)}
</Text>
{url && (
<Text
numberOfLines={1}
className="mt-1.5 font-light text-sm text-base-content"
>
{url}
</Text>
)}
<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 || ""}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{link.collection.name}
</Text>
</View>
</View>
<View className="flex-col items-end">
<View className="rounded-lg overflow-hidden relative">
{formatAvailable(link, "preview") ? (
<Image
source={{
uri: `${auth.instance}/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true&updatedAt=${link.updatedAt}`,
headers: {
Authorization: `Bearer ${auth.session}`,
},
}}
className="rounded-md h-[60px] w-[90px] object-cover scale-105"
/>
) : !link.preview ? (
<ActivityIndicator
size="small"
className="h-[60px] w-[90px]"
/>
) : (
<View className="h-[60px] w-[90px]" />
)}
</View>
<View className="flex flex-row gap-1 items-center mt-5 self-start">
<CalendarDays
size={16}
color={rawTheme[colorScheme as ThemeName]["neutral"]}
/>
<Text
numberOfLines={1}
className="font-light text-xs text-base-content"
>
{new Date(link.createdAt as string).toLocaleString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</Text>
</View>
</View>
</View>
</Pressable>
</ContextMenu.Trigger>
<ContextMenu.Content avoidCollisions>
<ContextMenu.Item
key="open-original"
onSelect={() => {
if (link) {
const format = getOriginalFormat(link);
data.preferredBrowser === "app"
? router.navigate(
format !== null
? `/links/${link.id}?format=${format}`
: `/links/${link.id}`
)
: Linking.openURL(
format !== null
? auth.instance +
`/preserved/${link?.id}?format=${format}`
: (link.url as string)
);
}
}}
>
<ContextMenu.ItemTitle>Open Original</ContextMenu.ItemTitle>
</ContextMenu.Item>
{link?.url && (
<>
<ContextMenu.Item
key="copy-url"
onSelect={async () => {
await Clipboard.setStringAsync(link.url as string);
}}
>
<ContextMenu.ItemTitle>Copy URL</ContextMenu.ItemTitle>
</ContextMenu.Item>
</>
)}
<ContextMenu.Item
key="pin-link"
onSelect={async () => {
const isAlreadyPinned =
link?.pinnedBy && link.pinnedBy[0] ? true : false;
await updateLink.mutateAsync({
...link,
pinnedBy: (isAlreadyPinned
? [{ id: undefined }]
: [{ id: user?.id }]) as any,
});
}}
>
<ContextMenu.ItemTitle>
{link.pinnedBy && link.pinnedBy[0] ? "Unpin Link" : "Pin Link"}
</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item
key="edit-link"
onSelect={() => {
SheetManager.show("edit-link-sheet", {
payload: { link: link },
});
}}
>
<ContextMenu.ItemTitle>Edit Link</ContextMenu.ItemTitle>
</ContextMenu.Item>
{link.url && atLeastOneFormatAvailable(link) && (
<ContextMenu.Sub>
<ContextMenu.SubTrigger key="preserved-formats">
<ContextMenu.ItemTitle>Preserved Formats</ContextMenu.ItemTitle>
</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
{formatAvailable(link, "monolith") && (
<ContextMenu.Item
key="preserved-formats-webpage"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${ArchivedFormat.monolith}`
)
}
>
<ContextMenu.ItemTitle>Webpage</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
{formatAvailable(link, "image") && (
<ContextMenu.Item
key="preserved-formats-screenshot"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${
link.image?.endsWith(".png")
? ArchivedFormat.png
: ArchivedFormat.jpeg
}`
)
}
>
<ContextMenu.ItemTitle>Screenshot</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
{formatAvailable(link, "pdf") && (
<ContextMenu.Item
key="preserved-formats-pdf"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${ArchivedFormat.pdf}`
)
}
>
<ContextMenu.ItemTitle>PDF</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
{formatAvailable(link, "readable") && (
<ContextMenu.Item
key="preserved-formats-readable"
onSelect={() =>
router.navigate(
`/links/${link.id}?format=${ArchivedFormat.readability}`
)
}
>
<ContextMenu.ItemTitle>Readable</ContextMenu.ItemTitle>
</ContextMenu.Item>
)}
</ContextMenu.SubContent>
</ContextMenu.Sub>
)}
<ContextMenu.Item
key="delete-link"
onSelect={() => {
return Alert.alert(
"Delete Link",
"Are you sure you want to delete this link? This action cannot be undone.",
[
{
text: "Cancel",
style: "cancel",
},
{
text: "Delete",
style: "destructive",
onPress: () => {
deleteLink.mutate(link.id as number, {
onSuccess: async () => {
await deleteLinkCache(link.id as number);
},
});
},
},
]
);
}}
>
<ContextMenu.ItemTitle>Delete</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
);
};
export default LinkListing;