From 8a5a109cdf14b6503b6bd07aa667788924a12fe6 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem 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 --- apps/mobile/app/dashboard/(tabs)/_layout.tsx | 15 ++- apps/mobile/app/dashboard/(tabs)/highlights.tsx | 56 ++++++++ .../mobile/components/highlights/HighlightCard.tsx | 141 +++++++++++++++++++++ .../mobile/components/highlights/HighlightList.tsx | 65 ++++++++++ 4 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/app/dashboard/(tabs)/highlights.tsx create mode 100644 apps/mobile/components/highlights/HighlightCard.tsx create mode 100644 apps/mobile/components/highlights/HighlightList.tsx (limited to 'apps') diff --git a/apps/mobile/app/dashboard/(tabs)/_layout.tsx b/apps/mobile/app/dashboard/(tabs)/_layout.tsx index 316eddcf..5cc6aa92 100644 --- a/apps/mobile/app/dashboard/(tabs)/_layout.tsx +++ b/apps/mobile/app/dashboard/(tabs)/_layout.tsx @@ -2,7 +2,13 @@ import React, { useLayoutEffect } from "react"; import { Tabs, useNavigation } from "expo-router"; import { StyledTabs } from "@/components/navigation/tabs"; import { useColorScheme } from "@/lib/useColorScheme"; -import { ClipboardList, Home, Settings, Tag } from "lucide-react-native"; +import { + ClipboardList, + Highlighter, + Home, + Settings, + Tag, +} from "lucide-react-native"; export default function TabLayout() { const { colors } = useColorScheme(); @@ -44,6 +50,13 @@ export default function TabLayout() { tabBarIcon: ({ color }) => , }} /> + , + }} + /> lastPage.nextCursor, + }, + ); + + if (error) { + return refetch()} />; + } + + if (isPending || !data) { + return ; + } + + const onRefresh = () => { + apiUtils.highlights.getAll.invalidate(); + }; + + return ( + + p.highlights)} + header={ + + + + } + onRefresh={onRefresh} + fetchNextPage={fetchNextPage} + isFetchingNextPage={isFetchingNextPage} + isRefreshing={isPending || isPlaceholderData} + /> + + ); +} 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 ( + + + {/* Highlight text with colored border */} + + + {highlight.text || "No text available"} + + + + {/* Note if present */} + {highlight.note && ( + + + Note: {highlight.note} + + + )} + + {/* Footer with timestamp and actions */} + + + + {dayjs(highlight.createdAt).fromNow()} + + {bookmark && ( + <> + + + + Source + + + )} + + + + {isDeleting ? ( + + ) : ( + { + Haptics.selectionAsync(); + deleteHighlightAlert(); + }} + > + + + )} + + + + + ); +} diff --git a/apps/mobile/components/highlights/HighlightList.tsx b/apps/mobile/components/highlights/HighlightList.tsx new file mode 100644 index 00000000..865add2a --- /dev/null +++ b/apps/mobile/components/highlights/HighlightList.tsx @@ -0,0 +1,65 @@ +import { useRef } from "react"; +import { ActivityIndicator, Keyboard, View } from "react-native"; +import Animated, { LinearTransition } from "react-native-reanimated"; +import { Text } from "@/components/ui/Text"; +import { useScrollToTop } from "@react-navigation/native"; + +import type { ZHighlight } from "@karakeep/shared/types/highlights"; + +import HighlightCard from "./HighlightCard"; + +export default function HighlightList({ + highlights, + header, + onRefresh, + fetchNextPage, + isFetchingNextPage, + isRefreshing, +}: { + highlights: ZHighlight[]; + onRefresh: () => void; + isRefreshing: boolean; + fetchNextPage?: () => void; + header?: React.ReactElement; + isFetchingNextPage?: boolean; +}) { + const flatListRef = useRef(null); + useScrollToTop(flatListRef); + + return ( + } + ListEmptyComponent={ + + No Highlights + + Highlights you create will appear here + + + } + data={highlights} + refreshing={isRefreshing} + onRefresh={onRefresh} + onScrollBeginDrag={Keyboard.dismiss} + keyExtractor={(h) => h.id} + onEndReached={fetchNextPage} + ListFooterComponent={ + isFetchingNextPage ? ( + + + + ) : ( + + ) + } + /> + ); +} -- cgit v1.2.3-70-g09d2