aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-11-23 23:21:03 +0000
committerMohamedBassem <me@mbassem.com>2024-11-23 23:21:03 +0000
commit27453300782a1bade030277c021d4961061e9c69 (patch)
treec7f805c909a103ee5895ab13d788a562eb7d1338 /apps/mobile
parentfbb264457d0d2737db33510303c3b3950e1e021e (diff)
downloadkarakeep-27453300782a1bade030277c021d4961061e9c69.tar.zst
feat(mobile): Add support for managing tags from mobile
Diffstat (limited to 'apps/mobile')
-rw-r--r--apps/mobile/app/dashboard/_layout.tsx9
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx15
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx20
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx139
-rw-r--r--apps/mobile/app/sharing.tsx35
-rw-r--r--apps/mobile/components/bookmarks/BookmarkCard.tsx9
6 files changed, 211 insertions, 16 deletions
diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx
index bc743c7a..5717f711 100644
--- a/apps/mobile/app/dashboard/_layout.tsx
+++ b/apps/mobile/app/dashboard/_layout.tsx
@@ -67,6 +67,15 @@ export default function Dashboard() {
}}
/>
<Stack.Screen
+ name="bookmarks/[slug]/manage_tags"
+ options={{
+ headerTitle: "Manage Tags",
+ headerBackTitle: "Back",
+ headerTransparent: true,
+ presentation: "modal",
+ }}
+ />
+ <Stack.Screen
name="bookmarks/[slug]/manage_lists"
options={{
headerTitle: "Manage Lists",
diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx
index 87330a88..98158ab1 100644
--- a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx
+++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/Input";
import { useToast } from "@/components/ui/Toast";
import { useAssetUrl } from "@/lib/hooks";
import { api } from "@/lib/trpc";
-import { ClipboardList, Globe, Info, Trash2 } from "lucide-react-native";
+import { ClipboardList, Globe, Info, Tag, Trash2 } from "lucide-react-native";
import {
useDeleteBookmark,
@@ -79,6 +79,19 @@ function BottomActions({ bookmark }: { bookmark: ZBookmark }) {
disabled: false,
},
{
+ id: "tags",
+ icon: (
+ <TailwindResolver
+ className="text-foreground"
+ comp={(styles) => <Tag color={styles?.color?.toString()} />}
+ />
+ ),
+ shouldRender: true,
+ onClick: () =>
+ router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`),
+ disabled: false,
+ },
+ {
id: "open",
icon: (
<TailwindResolver
diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx
index e0d87a09..e1b1bdbc 100644
--- a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx
+++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx
@@ -31,10 +31,22 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) {
<Skeleton className="h-4 w-full" />
</>
) : bookmark.tags.length > 0 ? (
- <View className="flex flex-row flex-wrap gap-2 rounded-lg bg-background p-4">
- {bookmark.tags.map((t) => (
- <TagPill key={t.id} tag={t} />
- ))}
+ <View className="flex flex-col gap-2">
+ <View className="flex flex-row flex-wrap gap-2 rounded-lg bg-background p-4">
+ {bookmark.tags.map((t) => (
+ <TagPill key={t.id} tag={t} />
+ ))}
+ </View>
+
+ <Pressable
+ onPress={() =>
+ router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`)
+ }
+ className="flex w-full flex-row justify-between gap-3 rounded-lg bg-white px-4 py-2 dark:bg-accent"
+ >
+ <Text className="text-lg text-accent-foreground">Manage Tags</Text>
+ <ChevronRight color="rgb(0, 122, 255)" />
+ </Pressable>
</View>
) : (
<Text className="text-foreground">No tags</Text>
diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx
new file mode 100644
index 00000000..1712fdfe
--- /dev/null
+++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx
@@ -0,0 +1,139 @@
+import React, { useMemo } from "react";
+import { Pressable, SectionList, Text, View } from "react-native";
+import { useLocalSearchParams } from "expo-router";
+import { TailwindResolver } from "@/components/TailwindResolver";
+import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView";
+import FullPageSpinner from "@/components/ui/FullPageSpinner";
+import { Input } from "@/components/ui/Input";
+import { useToast } from "@/components/ui/Toast";
+import { Check } from "lucide-react-native";
+
+import { useUpdateBookmarkTags } from "@hoarder/shared-react/hooks/bookmarks";
+import { api } from "@hoarder/shared-react/trpc";
+
+const ListPickerPage = () => {
+ const { slug: bookmarkId } = useLocalSearchParams();
+
+ const [search, setSearch] = React.useState("");
+
+ if (typeof bookmarkId !== "string") {
+ throw new Error("Unexpected param type");
+ }
+ const { toast } = useToast();
+ const onError = () => {
+ toast({
+ message: "Something went wrong",
+ variant: "destructive",
+ showProgress: false,
+ });
+ };
+ const { data: allTags, isPending: isAllTagsPending } =
+ api.tags.list.useQuery();
+ const { data: existingTags } = api.bookmarks.getBookmark.useQuery(
+ {
+ bookmarkId,
+ },
+ {
+ select: (data) => data.tags.map((t) => ({ id: t.id, name: t.name })),
+ },
+ );
+
+ const [optimisticTags, setOptimisticTags] = React.useState(
+ existingTags ?? [],
+ );
+
+ const { mutate: updateTags } = useUpdateBookmarkTags({
+ onMutate: (req) => {
+ req.attach.forEach((t) =>
+ setOptimisticTags((prev) => [
+ ...prev,
+ { id: t.tagId!, name: t.tagName! },
+ ]),
+ );
+ req.detach.forEach((t) =>
+ setOptimisticTags((prev) => prev.filter((p) => p.id != t.tagId!)),
+ );
+ },
+ onError,
+ });
+
+ const optimisticExistingTagIds = useMemo(() => {
+ return new Set(optimisticTags?.map((t) => t.id) ?? []);
+ }, [optimisticTags]);
+
+ const filteredTags = useMemo(() => {
+ return allTags?.tags.filter(
+ (t) =>
+ t.name.toLowerCase().startsWith(search.toLowerCase()) &&
+ !optimisticExistingTagIds.has(t.id),
+ );
+ }, [search, allTags, optimisticExistingTagIds]);
+
+ if (isAllTagsPending) {
+ return <FullPageSpinner />;
+ }
+
+ return (
+ <CustomSafeAreaView>
+ <View className="px-3">
+ <SectionList
+ className="h-full"
+ ListHeaderComponent={
+ <Input
+ placeholder="Search Tags ..."
+ autoCapitalize="none"
+ onChangeText={setSearch}
+ />
+ }
+ keyExtractor={(t) => t.id}
+ contentContainerStyle={{
+ gap: 5,
+ }}
+ SectionSeparatorComponent={() => <View className="h-1" />}
+ sections={[
+ {
+ title: "Existing Tags",
+ data: optimisticTags ?? [],
+ },
+ {
+ title: "All Tags",
+ data: filteredTags ?? [],
+ },
+ ]}
+ renderItem={(t) => (
+ <Pressable
+ key={t.item.id}
+ onPress={() =>
+ updateTags({
+ bookmarkId,
+ detach:
+ t.section.title == "Existing Tags"
+ ? [{ tagId: t.item.id, tagName: t.item.name }]
+ : [],
+ attach:
+ t.section.title == "All Tags"
+ ? [{ tagId: t.item.id, tagName: t.item.name }]
+ : [],
+ })
+ }
+ >
+ <View className="mx-2 flex flex-row items-center gap-2 rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent">
+ {t.section.title == "Existing Tags" && (
+ <TailwindResolver
+ className="text-accent-foreground"
+ comp={(s) => <Check color={s?.color} />}
+ />
+ )}
+ <Text className="text-center text-lg text-accent-foreground">
+ {t.item.name}
+ </Text>
+ </View>
+ </Pressable>
+ )}
+ />
+ </View>
+ </CustomSafeAreaView>
+ );
+};
+
+export default ListPickerPage;
diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx
index e41535b7..3551aea9 100644
--- a/apps/mobile/app/sharing.tsx
+++ b/apps/mobile/app/sharing.tsx
@@ -96,17 +96,30 @@ export default function Sharing() {
<Text className="text-4xl text-foreground">
{mode.type === "alreadyExists" ? "Already Hoarded!" : "Hoarded!"}
</Text>
- <Button
- label="Add to List"
- onPress={() => {
- router.push(
- `/dashboard/bookmarks/${mode.bookmarkId}/manage_lists`,
- );
- if (autoCloseTimeoutId) {
- clearTimeout(autoCloseTimeoutId);
- }
- }}
- />
+ <View className="flex flex-row gap-2">
+ <Button
+ label="Add to List"
+ onPress={() => {
+ router.push(
+ `/dashboard/bookmarks/${mode.bookmarkId}/manage_lists`,
+ );
+ if (autoCloseTimeoutId) {
+ clearTimeout(autoCloseTimeoutId);
+ }
+ }}
+ />
+ <Button
+ label="Manage Tags"
+ onPress={() => {
+ router.push(
+ `/dashboard/bookmarks/${mode.bookmarkId}/manage_tags`,
+ );
+ if (autoCloseTimeoutId) {
+ clearTimeout(autoCloseTimeoutId);
+ }
+ }}
+ />
+ </View>
<Pressable onPress={() => router.replace("dashboard")}>
<Text className="text-muted-foreground">Dismiss</Text>
</Pressable>
diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx
index df5aa666..13d639c9 100644
--- a/apps/mobile/components/bookmarks/BookmarkCard.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx
@@ -103,6 +103,8 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
});
} else if (nativeEvent.event === "manage_list") {
router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`);
+ } else if (nativeEvent.event === "manage_tags") {
+ router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`);
}
}}
actions={[
@@ -130,6 +132,13 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
ios: "list",
}),
},
+ {
+ id: "manage_tags",
+ title: "Manage Tags",
+ image: Platform.select({
+ ios: "tag",
+ }),
+ },
]}
shouldOpenOnLongPress={false}
>