From 8a5a109cdf14b6503b6bd07aa667788924a12fe6 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sun, 23 Nov 2025 12:22:34 +0000
Subject: feat(mobile): Add highlights page to mobile app (#2156)

* feat: Add highlights page to mobile app

This commit adds a new highlights page to the mobile app where users can
view all their highlights with the following features:

- HighlightCard component: Displays individual highlights with colored borders,
  text, optional notes, timestamps, and a link to the source bookmark
- HighlightList component: Renders a scrollable list of highlights with
  pull-to-refresh and infinite scroll pagination
- UpdatingHighlightList component: Handles data fetching using tRPC infinite
  queries with automatic cache invalidation
- New /dashboard/highlights route with large header title
- Added navigation link in Settings tab under "App Settings"

All components follow the existing mobile app patterns and integrate with
the existing highlights API.

* make it a tab

---------

Co-authored-by: Claude <noreply@anthropic.com>
---
 .../mobile/components/highlights/HighlightCard.tsx | 141 +++++++++++++++++++++
 1 file changed, 141 insertions(+)
 create mode 100644 apps/mobile/components/highlights/HighlightCard.tsx

(limited to 'apps/mobile/components/highlights/HighlightCard.tsx')

diff --git a/apps/mobile/components/highlights/HighlightCard.tsx b/apps/mobile/components/highlights/HighlightCard.tsx
new file mode 100644
index 00000000..7e0b4a2b
--- /dev/null
+++ b/apps/mobile/components/highlights/HighlightCard.tsx
@@ -0,0 +1,141 @@
+import { ActivityIndicator, Alert, Pressable, View } from "react-native";
+import * as Haptics from "expo-haptics";
+import { useRouter } from "expo-router";
+import { Text } from "@/components/ui/Text";
+import { api } from "@/lib/trpc";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { ExternalLink, Trash2 } from "lucide-react-native";
+
+import type { ZHighlight } from "@karakeep/shared/types/highlights";
+import { useDeleteHighlight } from "@karakeep/shared-react/hooks/highlights";
+
+import { useToast } from "../ui/Toast";
+
+dayjs.extend(relativeTime);
+
+// Color map for highlights (mapped to Tailwind CSS classes used in NativeWind)
+const HIGHLIGHT_COLOR_MAP = {
+  red: "#fecaca", // bg-red-200
+  green: "#bbf7d0", // bg-green-200
+  blue: "#bfdbfe", // bg-blue-200
+  yellow: "#fef08a", // bg-yellow-200
+} as const;
+
+export default function HighlightCard({
+  highlight,
+}: {
+  highlight: ZHighlight;
+}) {
+  const { toast } = useToast();
+  const router = useRouter();
+
+  const onError = () => {
+    toast({
+      message: "Something went wrong",
+      variant: "destructive",
+      showProgress: false,
+    });
+  };
+
+  const { mutate: deleteHighlight, isPending: isDeleting } = useDeleteHighlight(
+    {
+      onSuccess: () => {
+        toast({
+          message: "Highlight has been deleted!",
+          showProgress: false,
+        });
+      },
+      onError,
+    },
+  );
+
+  const deleteHighlightAlert = () =>
+    Alert.alert(
+      "Delete highlight?",
+      "Are you sure you want to delete this highlight?",
+      [
+        { text: "Cancel", style: "cancel" },
+        {
+          text: "Delete",
+          onPress: () => deleteHighlight({ highlightId: highlight.id }),
+          style: "destructive",
+        },
+      ],
+    );
+
+  const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
+    {
+      bookmarkId: highlight.bookmarkId,
+    },
+    {
+      retry: false,
+    },
+  );
+
+  const handleBookmarkPress = () => {
+    Haptics.selectionAsync();
+    router.push(`/dashboard/bookmarks/${highlight.bookmarkId}`);
+  };
+
+  return (
+    <View className="overflow-hidden rounded-xl bg-card p-4">
+      <View className="flex gap-3">
+        {/* Highlight text with colored border */}
+        <View
+          className="rounded-r-lg border-l-4 bg-muted/30 p-3"
+          style={{ borderLeftColor: HIGHLIGHT_COLOR_MAP[highlight.color] }}
+        >
+          <Text className="italic text-foreground">
+            {highlight.text || "No text available"}
+          </Text>
+        </View>
+
+        {/* Note if present */}
+        {highlight.note && (
+          <View className="rounded-lg bg-muted/50 p-2">
+            <Text className="text-sm text-muted-foreground">
+              Note: {highlight.note}
+            </Text>
+          </View>
+        )}
+
+        {/* Footer with timestamp and actions */}
+        <View className="flex flex-row items-center justify-between">
+          <View className="flex flex-row items-center gap-2">
+            <Text className="text-xs text-muted-foreground">
+              {dayjs(highlight.createdAt).fromNow()}
+            </Text>
+            {bookmark && (
+              <>
+                <Text className="text-xs text-muted-foreground">•</Text>
+                <Pressable
+                  onPress={handleBookmarkPress}
+                  className="flex flex-row items-center gap-1"
+                >
+                  <ExternalLink size={12} color="gray" />
+                  <Text className="text-xs text-muted-foreground">Source</Text>
+                </Pressable>
+              </>
+            )}
+          </View>
+
+          <View className="flex flex-row gap-2">
+            {isDeleting ? (
+              <ActivityIndicator size="small" />
+            ) : (
+              <Pressable
+                onPress={() => {
+                  Haptics.selectionAsync();
+                  deleteHighlightAlert();
+                }}
+              >
+                <Trash2 size={18} color="#ef4444" />
+              </Pressable>
+            )}
+          </View>
+        </View>
+      </View>
+    </View>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

