diff options
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx | 110 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/lists/[slug].tsx | 49 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/BookmarkCard.tsx | 202 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/BottomActions.tsx | 11 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/NotePreview.tsx | 33 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/TagPill.tsx | 20 |
6 files changed, 279 insertions, 146 deletions
diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx index 15e2a082..c4b76aef 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx @@ -26,6 +26,7 @@ import { useSummarizeBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; +import { useWhoAmI } from "@karakeep/shared-react/hooks/users"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils"; @@ -41,7 +42,13 @@ function InfoSection({ ); } -function TagList({ bookmark }: { bookmark: ZBookmark }) { +function TagList({ + bookmark, + readOnly, +}: { + bookmark: ZBookmark; + readOnly: boolean; +}) { return ( <InfoSection> {isBookmarkStillTagging(bookmark) ? ( @@ -54,24 +61,26 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) { <> <View className="flex flex-row flex-wrap gap-2 rounded-lg p-2"> {bookmark.tags.map((t) => ( - <TagPill key={t.id} tag={t} /> + <TagPill key={t.id} tag={t} clickable={!readOnly} /> ))} </View> <Divider orientation="horizontal" /> </> ) )} - <View> - <Pressable - onPress={() => - router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`) - } - className="flex w-full flex-row justify-between gap-3" - > - <Text>Manage Tags</Text> - <ChevronRight /> - </Pressable> - </View> + {!readOnly && ( + <View> + <Pressable + onPress={() => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`) + } + className="flex w-full flex-row justify-between gap-3" + > + <Text>Manage Tags</Text> + <ChevronRight /> + </Pressable> + </View> + )} </InfoSection> ); } @@ -98,15 +107,17 @@ function TitleEditor({ title, setTitle, isPending, + disabled, }: { title: string | null | undefined; setTitle: (title: string | null) => void; isPending: boolean; + disabled?: boolean; }) { return ( <InfoSection> <Input - editable={!isPending} + editable={!isPending && !disabled} multiline={false} numberOfLines={1} placeholder="Title" @@ -121,15 +132,17 @@ function NotesEditor({ notes, setNotes, isPending, + disabled, }: { notes: string | null | undefined; setNotes: (title: string | null) => void; isPending: boolean; + disabled?: boolean; }) { return ( <InfoSection> <Input - editable={!isPending} + editable={!isPending && !disabled} multiline={true} placeholder="Notes" inputClasses="h-24" @@ -141,7 +154,13 @@ function NotesEditor({ ); } -function AISummarySection({ bookmark }: { bookmark: ZBookmark }) { +function AISummarySection({ + bookmark, + readOnly, +}: { + bookmark: ZBookmark; + readOnly: boolean; +}) { const { toast } = useToast(); const [isExpanded, setIsExpanded] = React.useState(false); @@ -214,7 +233,7 @@ function AISummarySection({ bookmark }: { bookmark: ZBookmark }) { </Text> </Pressable> )} - {isExpanded && ( + {isExpanded && !readOnly && ( <View className="mt-2 flex flex-row justify-end gap-2"> <Pressable onPress={() => resummarize({ bookmarkId: bookmark.id })} @@ -262,6 +281,9 @@ function AISummarySection({ bookmark }: { bookmark: ZBookmark }) { } // If no summary, show button to generate one + if (readOnly) { + return null; + } return ( <InfoSection> <Pressable @@ -293,6 +315,7 @@ const ViewBookmarkPage = () => { const insets = useSafeAreaInsets(); const { slug } = useLocalSearchParams(); const { toast } = useToast(); + const { data: currentUser } = useWhoAmI(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } @@ -326,6 +349,9 @@ const ViewBookmarkPage = () => { bookmarkId: slug, }); + // Check if the current user owns this bookmark + const isOwner = currentUser?.id === bookmark?.userId; + const [editedBookmark, setEditedBookmark] = React.useState<{ title?: string | null; note?: string; @@ -416,37 +442,41 @@ const ViewBookmarkPage = () => { setEditedBookmark((prev) => ({ ...prev, title })) } isPending={isEditPending} + disabled={!isOwner} /> - <AISummarySection bookmark={bookmark} /> - <TagList bookmark={bookmark} /> - <ManageLists bookmark={bookmark} /> + <AISummarySection bookmark={bookmark} readOnly={!isOwner} /> + <TagList bookmark={bookmark} readOnly={!isOwner} /> + {isOwner && <ManageLists bookmark={bookmark} />} <NotesEditor notes={bookmark.note} setNotes={(note) => setEditedBookmark((prev) => ({ ...prev, note: note ?? "" })) } isPending={isEditPending} + disabled={!isOwner} /> - <View className="flex justify-between gap-3"> - <Button - onPress={() => - editBookmark({ - bookmarkId: bookmark.id, - ...editedBookmark, - }) - } - disabled={isEditPending} - > - <Text>Save</Text> - </Button> - <Button - variant="destructive" - onPress={handleDeleteBookmark} - disabled={isDeletionPending} - > - <Text>Delete</Text> - </Button> - </View> + {isOwner && ( + <View className="flex justify-between gap-3"> + <Button + onPress={() => + editBookmark({ + bookmarkId: bookmark.id, + ...editedBookmark, + }) + } + disabled={isEditPending} + > + <Text>Save</Text> + </Button> + <Button + variant="destructive" + onPress={handleDeleteBookmark} + disabled={isDeletionPending} + > + <Text>Delete</Text> + </Button> + </View> + )} <View className="gap-2"> <Text className="items-center text-center"> Created {bookmark.createdAt.toLocaleString()} diff --git a/apps/mobile/app/dashboard/lists/[slug].tsx b/apps/mobile/app/dashboard/lists/[slug].tsx index f98dd6d3..e7aab443 100644 --- a/apps/mobile/app/dashboard/lists/[slug].tsx +++ b/apps/mobile/app/dashboard/lists/[slug].tsx @@ -9,6 +9,8 @@ import { api } from "@/lib/trpc"; import { MenuView } from "@react-native-menu/menu"; import { Ellipsis } from "lucide-react-native"; +import { ZBookmarkList } from "@karakeep/shared/types/lists"; + export default function ListView() { const { slug } = useLocalSearchParams(); if (typeof slug !== "string") { @@ -27,7 +29,9 @@ export default function ListView() { headerTitle: list ? `${list.icon} ${list.name}` : "", headerBackTitle: "Back", headerLargeTitle: true, - headerRight: () => <ListActionsMenu listId={slug} />, + headerRight: () => ( + <ListActionsMenu listId={slug} role={list?.userRole ?? "viewer"} /> + ), }} /> {error ? ( @@ -47,8 +51,20 @@ export default function ListView() { ); } -function ListActionsMenu({ listId }: { listId: string }) { - const { mutate } = api.lists.delete.useMutation({ +function ListActionsMenu({ + listId, + role, +}: { + listId: string; + role: ZBookmarkList["userRole"]; +}) { + const { mutate: deleteList } = api.lists.delete.useMutation({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }); + + const { mutate: leaveList } = api.lists.leaveList.useMutation({ onSuccess: () => { router.replace("/dashboard/lists"); }, @@ -60,7 +76,20 @@ function ListActionsMenu({ listId }: { listId: string }) { { text: "Delete", onPress: () => { - mutate({ listId }); + deleteList({ listId }); + }, + style: "destructive", + }, + ]); + }; + + const handleLeave = () => { + Alert.alert("Leave List", "Are you sure you want to leave this list?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Leave", + onPress: () => { + leaveList({ listId }); }, style: "destructive", }, @@ -75,16 +104,28 @@ function ListActionsMenu({ listId }: { listId: string }) { title: "Delete List", attributes: { destructive: true, + hidden: role !== "owner", }, image: Platform.select({ ios: "trash", }), }, + { + id: "leave", + title: "Leave List", + attributes: { + destructive: true, + hidden: role === "owner", + }, + }, ]} onPressAction={({ nativeEvent }) => { if (nativeEvent.event === "delete") { handleDelete(); } + if (nativeEvent.event === "leave") { + handleLeave(); + } }} shouldOpenOnLongPress={false} > diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 98d8d3e2..922951e5 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -24,6 +24,7 @@ import { useDeleteBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; +import { useWhoAmI } from "@karakeep/shared-react/hooks/users"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { getBookmarkLinkImageUrl, @@ -42,6 +43,10 @@ import TagPill from "./TagPill"; function ActionBar({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); const { settings } = useAppSettings(); + const { data: currentUser } = useWhoAmI(); + + // Check if the current user owns this bookmark + const isOwner = currentUser?.id === bookmark.userId; const onError = () => { toast({ @@ -156,24 +161,71 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) { } }; + // Build actions array based on ownership + const menuActions = []; + if (isOwner) { + menuActions.push( + { + id: "edit", + title: "Edit", + image: Platform.select({ + ios: "pencil", + }), + }, + { + id: "manage_list", + title: "Manage Lists", + image: Platform.select({ + ios: "list.bullet", + }), + }, + { + id: "manage_tags", + title: "Manage Tags", + image: Platform.select({ + ios: "tag", + }), + }, + { + id: "archive", + title: bookmark.archived ? "Un-archive" : "Archive", + image: Platform.select({ + ios: "folder", + }), + }, + { + id: "delete", + title: "Delete", + attributes: { + destructive: true, + }, + image: Platform.select({ + ios: "trash", + }), + }, + ); + } + return ( <View className="flex flex-row gap-4"> {(isArchivePending || isDeletionPending) && <ActivityIndicator />} - <Pressable - onPress={() => { - Haptics.selectionAsync(); - favouriteBookmark({ - bookmarkId: bookmark.id, - favourited: !bookmark.favourited, - }); - }} - > - {(variables ? variables.favourited : bookmark.favourited) ? ( - <Star fill="#ebb434" color="#ebb434" /> - ) : ( - <Star color="gray" /> - )} - </Pressable> + {isOwner && ( + <Pressable + onPress={() => { + Haptics.selectionAsync(); + favouriteBookmark({ + bookmarkId: bookmark.id, + favourited: !bookmark.favourited, + }); + }} + > + {(variables ? variables.favourited : bookmark.favourited) ? ( + <Star fill="#ebb434" color="#ebb434" /> + ) : ( + <Star color="gray" /> + )} + </Pressable> + )} <Pressable onPress={() => { @@ -184,74 +236,39 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) { <ShareIcon color="gray" /> </Pressable> - <MenuView - onPressAction={({ nativeEvent }) => { - Haptics.selectionAsync(); - if (nativeEvent.event === "delete") { - deleteBookmarkAlert(); - } else if (nativeEvent.event === "archive") { - archiveBookmark({ - bookmarkId: bookmark.id, - archived: !bookmark.archived, - }); - } 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`); - } else if (nativeEvent.event === "edit") { - router.push(`/dashboard/bookmarks/${bookmark.id}/info`); - } - }} - actions={[ - { - id: "edit", - title: "Edit", - image: Platform.select({ - ios: "pencil", - }), - }, - { - id: "manage_list", - title: "Manage Lists", - image: Platform.select({ - ios: "list.bullet", - }), - }, - { - id: "manage_tags", - title: "Manage Tags", - image: Platform.select({ - ios: "tag", - }), - }, - { - id: "archive", - title: bookmark.archived ? "Un-archive" : "Archive", - image: Platform.select({ - ios: "folder", - }), - }, - { - id: "delete", - title: "Delete", - attributes: { - destructive: true, - }, - image: Platform.select({ - ios: "trash", - }), - }, - ]} - shouldOpenOnLongPress={false} - > - <Ellipsis onPress={() => Haptics.selectionAsync()} color="gray" /> - </MenuView> + {isOwner && menuActions.length > 0 && ( + <MenuView + onPressAction={({ nativeEvent }) => { + Haptics.selectionAsync(); + if (nativeEvent.event === "delete") { + deleteBookmarkAlert(); + } else if (nativeEvent.event === "archive") { + archiveBookmark({ + bookmarkId: bookmark.id, + archived: !bookmark.archived, + }); + } 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`); + } else if (nativeEvent.event === "edit") { + router.push(`/dashboard/bookmarks/${bookmark.id}/info`); + } + }} + actions={menuActions} + shouldOpenOnLongPress={false} + > + <Ellipsis onPress={() => Haptics.selectionAsync()} color="gray" /> + </MenuView> + )} </View> ); } function TagList({ bookmark }: { bookmark: ZBookmark }) { const tags = bookmark.tags; + const { data: currentUser } = useWhoAmI(); + const isOwner = currentUser?.id === bookmark.userId; if (isBookmarkStillTagging(bookmark)) { return ( @@ -266,7 +283,7 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) { <ScrollView horizontal showsHorizontalScrollIndicator={false}> <View className="flex flex-row gap-2"> {tags.map((t) => ( - <TagPill key={t.id} tag={t} /> + <TagPill key={t.id} tag={t} clickable={isOwner} /> ))} </View> </ScrollView> @@ -281,6 +298,9 @@ function LinkCard({ onOpenBookmark: () => void; }) { const { settings } = useAppSettings(); + const { data: currentUser } = useWhoAmI(); + const isOwner = currentUser?.id === bookmark.userId; + if (bookmark.content.type !== BookmarkTypes.LINK) { throw new Error("Wrong content type rendered"); } @@ -330,7 +350,13 @@ function LinkCard({ > {bookmark.title ?? bookmark.content.title ?? parsedUrl.host} </Text> - {note && <NotePreview note={note} bookmarkId={bookmark.id} />} + {note && ( + <NotePreview + note={note} + bookmarkId={bookmark.id} + readOnly={!isOwner} + /> + )} <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="mt-2 flex flex-row justify-between px-2 pb-2"> @@ -350,6 +376,9 @@ function TextCard({ onOpenBookmark: () => void; }) { const { settings } = useAppSettings(); + const { data: currentUser } = useWhoAmI(); + const isOwner = currentUser?.id === bookmark.userId; + if (bookmark.content.type !== BookmarkTypes.TEXT) { throw new Error("Wrong content type rendered"); } @@ -369,7 +398,9 @@ function TextCard({ <BookmarkTextMarkdown text={content} /> </Pressable> </View> - {note && <NotePreview note={note} bookmarkId={bookmark.id} />} + {note && ( + <NotePreview note={note} bookmarkId={bookmark.id} readOnly={!isOwner} /> + )} <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="flex flex-row justify-between p-2"> @@ -388,6 +419,9 @@ function AssetCard({ onOpenBookmark: () => void; }) { const { settings } = useAppSettings(); + const { data: currentUser } = useWhoAmI(); + const isOwner = currentUser?.id === bookmark.userId; + if (bookmark.content.type !== BookmarkTypes.ASSET) { throw new Error("Wrong content type rendered"); } @@ -412,7 +446,13 @@ function AssetCard({ <Text className="line-clamp-2 text-xl font-bold">{title}</Text> )} </Pressable> - {note && <NotePreview note={note} bookmarkId={bookmark.id} />} + {note && ( + <NotePreview + note={note} + bookmarkId={bookmark.id} + readOnly={!isOwner} + /> + )} <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="mt-2 flex flex-row justify-between px-2 pb-2"> diff --git a/apps/mobile/components/bookmarks/BottomActions.tsx b/apps/mobile/components/bookmarks/BottomActions.tsx index 8cfa27c9..64653779 100644 --- a/apps/mobile/components/bookmarks/BottomActions.tsx +++ b/apps/mobile/components/bookmarks/BottomActions.tsx @@ -5,6 +5,7 @@ import { useToast } from "@/components/ui/Toast"; import { ClipboardList, Globe, Info, Tag, Trash2 } from "lucide-react-native"; import { useDeleteBookmark } from "@karakeep/shared-react/hooks/bookmarks"; +import { useWhoAmI } from "@karakeep/shared-react/hooks/users"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; interface BottomActionsProps { @@ -14,6 +15,10 @@ interface BottomActionsProps { export default function BottomActions({ bookmark }: BottomActionsProps) { const { toast } = useToast(); const router = useRouter(); + const { data: currentUser } = useWhoAmI(); + + // Check if the current user owns this bookmark + const isOwner = currentUser?.id === bookmark.userId; const { mutate: deleteBookmark, isPending: isDeletionPending } = useDeleteBookmark({ @@ -56,7 +61,7 @@ export default function BottomActions({ bookmark }: BottomActionsProps) { comp={(styles) => <ClipboardList color={styles?.color?.toString()} />} /> ), - shouldRender: true, + shouldRender: isOwner, onClick: () => router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`), disabled: false, @@ -69,7 +74,7 @@ export default function BottomActions({ bookmark }: BottomActionsProps) { comp={(styles) => <Tag color={styles?.color?.toString()} />} /> ), - shouldRender: true, + shouldRender: isOwner, onClick: () => router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`), disabled: false, @@ -94,7 +99,7 @@ export default function BottomActions({ bookmark }: BottomActionsProps) { comp={(styles) => <Trash2 color={styles?.color?.toString()} />} /> ), - shouldRender: true, + shouldRender: isOwner, onClick: deleteBookmarkAlert, disabled: isDeletionPending, }, diff --git a/apps/mobile/components/bookmarks/NotePreview.tsx b/apps/mobile/components/bookmarks/NotePreview.tsx index d529d56e..0283d179 100644 --- a/apps/mobile/components/bookmarks/NotePreview.tsx +++ b/apps/mobile/components/bookmarks/NotePreview.tsx @@ -10,9 +10,14 @@ import { Text } from "../ui/Text"; interface NotePreviewProps { note: string; bookmarkId: string; + readOnly?: boolean; } -export function NotePreview({ note, bookmarkId }: NotePreviewProps) { +export function NotePreview({ + note, + bookmarkId, + readOnly = false, +}: NotePreviewProps) { const [isModalVisible, setIsModalVisible] = useState(false); const { colorScheme } = useColorScheme(); const iconColor = colorScheme === "dark" ? "#9ca3af" : "#6b7280"; @@ -63,18 +68,20 @@ export function NotePreview({ note, bookmarkId }: NotePreviewProps) { </ScrollView> {/* Action Button */} - <View className="flex flex-row justify-end border-t border-border pt-4"> - <Button - variant="secondary" - onPress={() => { - setIsModalVisible(false); - router.push(`/dashboard/bookmarks/${bookmarkId}/info`); - }} - > - <Text className="text-sm">Edit Notes</Text> - <ExternalLink size={14} color={modalIconColor} /> - </Button> - </View> + {!readOnly && ( + <View className="flex flex-row justify-end border-t border-border pt-4"> + <Button + variant="secondary" + onPress={() => { + setIsModalVisible(false); + router.push(`/dashboard/bookmarks/${bookmarkId}/info`); + }} + > + <Text className="text-sm">Edit Notes</Text> + <ExternalLink size={14} color={modalIconColor} /> + </Button> + </View> + )} </View> </View> </Modal> diff --git a/apps/mobile/components/bookmarks/TagPill.tsx b/apps/mobile/components/bookmarks/TagPill.tsx index caf0f636..2097daab 100644 --- a/apps/mobile/components/bookmarks/TagPill.tsx +++ b/apps/mobile/components/bookmarks/TagPill.tsx @@ -1,17 +1,27 @@ -import { View } from "react-native"; +import { Text, View } from "react-native"; import { Link } from "expo-router"; import { ZBookmarkTags } from "@karakeep/shared/types/tags"; -export default function TagPill({ tag }: { tag: ZBookmarkTags }) { +export default function TagPill({ + tag, + clickable = true, +}: { + tag: ZBookmarkTags; + clickable?: boolean; +}) { return ( <View key={tag.id} className="rounded-full border border-input px-2.5 py-0.5 text-xs font-semibold" > - <Link className="text-foreground" href={`dashboard/tags/${tag.id}`}> - {tag.name} - </Link> + {clickable ? ( + <Link className="text-foreground" href={`dashboard/tags/${tag.id}`}> + {tag.name} + </Link> + ) : ( + <Text className="text-foreground">{tag.name}</Text> + )} </View> ); } |
