aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx110
-rw-r--r--apps/mobile/app/dashboard/lists/[slug].tsx49
-rw-r--r--apps/mobile/components/bookmarks/BookmarkCard.tsx202
-rw-r--r--apps/mobile/components/bookmarks/BottomActions.tsx11
-rw-r--r--apps/mobile/components/bookmarks/NotePreview.tsx33
-rw-r--r--apps/mobile/components/bookmarks/TagPill.tsx20
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>
);
}