Skip to content

Commit 459ee50

Browse files
feat(mobile): Add highlights support for the mobile app (#2494)
1 parent fbc63b9 commit 459ee50

20 files changed

Lines changed: 506 additions & 229 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use dom";
2+
3+
import "@/globals.css";
4+
5+
import type { Highlight } from "@karakeep/shared-react/components/BookmarkHtmlHighlighter";
6+
import BookmarkHTMLHighlighter from "@karakeep/shared-react/components/BookmarkHtmlHighlighter";
7+
8+
export default function BookmarkHtmlHighlighterDom({
9+
htmlContent,
10+
contentStyle,
11+
highlights,
12+
readOnly,
13+
onHighlight,
14+
onUpdateHighlight,
15+
onDeleteHighlight,
16+
}: {
17+
htmlContent: string;
18+
contentStyle?: React.CSSProperties;
19+
highlights?: Highlight[];
20+
readOnly?: boolean;
21+
onHighlight?: (highlight: Highlight) => void;
22+
onUpdateHighlight?: (highlight: Highlight) => void;
23+
onDeleteHighlight?: (highlight: Highlight) => void;
24+
dom?: import("expo/dom").DOMProps;
25+
}) {
26+
return (
27+
<div style={{ maxWidth: "100vw", overflowX: "hidden" }}>
28+
<BookmarkHTMLHighlighter
29+
htmlContent={htmlContent}
30+
highlights={highlights}
31+
readOnly={readOnly}
32+
onHighlight={onHighlight}
33+
onUpdateHighlight={onUpdateHighlight}
34+
onDeleteHighlight={onDeleteHighlight}
35+
style={contentStyle}
36+
/>
37+
</div>
38+
);
39+
}

apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ import { useReaderSettings, WEBVIEW_FONT_FAMILIES } from "@/lib/readerSettings";
99
import { useColorScheme } from "@/lib/useColorScheme";
1010
import { useQuery } from "@tanstack/react-query";
1111

12+
import {
13+
useCreateHighlight,
14+
useDeleteHighlight,
15+
useUpdateHighlight,
16+
} from "@karakeep/shared-react/hooks/highlights";
1217
import { useTRPC } from "@karakeep/shared-react/trpc";
1318
import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
1419

1520
import FullPageError from "../FullPageError";
1621
import FullPageSpinner from "../ui/FullPageSpinner";
1722
import BookmarkAssetImage from "./BookmarkAssetImage";
23+
import BookmarkHtmlHighlighterDom from "./BookmarkHtmlHighlighterDom";
1824
import { PDFViewer } from "./PDFViewer";
1925

2026
export function BookmarkLinkBrowserPreview({
@@ -80,6 +86,16 @@ export function BookmarkLinkReaderPreview({
8086
}),
8187
);
8288

89+
const { data: highlights } = useQuery(
90+
api.highlights.getForBookmark.queryOptions({
91+
bookmarkId: bookmark.id,
92+
}),
93+
);
94+
95+
const { mutate: createHighlight } = useCreateHighlight();
96+
const { mutate: updateHighlight } = useUpdateHighlight();
97+
const { mutate: deleteHighlight } = useDeleteHighlight();
98+
8399
if (isLoading) {
84100
return <FullPageSpinner />;
85101
}
@@ -92,74 +108,44 @@ export function BookmarkLinkReaderPreview({
92108
throw new Error("Wrong content type rendered");
93109
}
94110

95-
const fontFamily = WEBVIEW_FONT_FAMILIES[readerSettings.fontFamily];
96-
const fontSize = readerSettings.fontSize;
97-
const lineHeight = readerSettings.lineHeight;
111+
const contentStyle: React.CSSProperties = {
112+
fontFamily: WEBVIEW_FONT_FAMILIES[readerSettings.fontFamily],
113+
fontSize: `${readerSettings.fontSize}px`,
114+
lineHeight: String(readerSettings.lineHeight),
115+
color: isDark ? "#e5e7eb" : "#374151",
116+
padding: "16px",
117+
background: isDark ? "#000000" : "#ffffff",
118+
};
98119

99120
return (
100121
<View className="flex-1 bg-background">
101-
<WebView
102-
originWhitelist={["*"]}
103-
source={{
104-
html: `
105-
<!DOCTYPE html>
106-
<html>
107-
<head>
108-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
109-
<style>
110-
body {
111-
font-family: ${fontFamily};
112-
font-size: ${fontSize}px;
113-
line-height: ${lineHeight};
114-
color: ${isDark ? "#e5e7eb" : "#374151"};
115-
margin: 0;
116-
padding: 16px;
117-
background: ${isDark ? "#000000" : "#ffffff"};
118-
}
119-
p { margin: 0 0 1em 0; }
120-
h1, h2, h3, h4, h5, h6 { margin: 1.5em 0 0.5em 0; line-height: 1.2; }
121-
img { max-width: 100%; height: auto; border-radius: 8px; }
122-
a { color: #3b82f6; text-decoration: none; }
123-
a:hover { text-decoration: underline; }
124-
blockquote {
125-
border-left: 4px solid ${isDark ? "#374151" : "#e5e7eb"};
126-
margin: 1em 0;
127-
padding-left: 1em;
128-
color: ${isDark ? "#9ca3af" : "#6b7280"};
129-
}
130-
pre, code {
131-
font-family: ui-monospace, Menlo, Monaco, 'Courier New', monospace;
132-
background: ${isDark ? "#1f2937" : "#f3f4f6"};
133-
}
134-
pre {
135-
padding: 1em;
136-
border-radius: 6px;
137-
overflow-x: auto;
138-
}
139-
code {
140-
padding: 0.2em 0.4em;
141-
border-radius: 3px;
142-
font-size: 0.9em;
143-
}
144-
pre code {
145-
padding: 0;
146-
background: none;
147-
}
148-
</style>
149-
</head>
150-
<body>
151-
${bookmarkWithContent.content.htmlContent}
152-
</body>
153-
</html>
154-
`,
155-
}}
156-
style={{
157-
flex: 1,
158-
backgroundColor: isDark ? "#000000" : "#ffffff",
159-
}}
160-
showsVerticalScrollIndicator={false}
161-
showsHorizontalScrollIndicator={false}
162-
decelerationRate={0.998}
122+
<BookmarkHtmlHighlighterDom
123+
htmlContent={bookmarkWithContent.content.htmlContent ?? ""}
124+
contentStyle={contentStyle}
125+
highlights={highlights?.highlights ?? []}
126+
onHighlight={(h) =>
127+
createHighlight({
128+
startOffset: h.startOffset,
129+
endOffset: h.endOffset,
130+
color: h.color,
131+
bookmarkId: bookmark.id,
132+
text: h.text,
133+
note: h.note ?? null,
134+
})
135+
}
136+
onUpdateHighlight={(h) =>
137+
updateHighlight({
138+
highlightId: h.id,
139+
color: h.color,
140+
note: h.note,
141+
})
142+
}
143+
onDeleteHighlight={(h) =>
144+
deleteHighlight({
145+
highlightId: h.id,
146+
})
147+
}
148+
dom={{ scrollEnabled: true }}
163149
/>
164150
</View>
165151
);

apps/mobile/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"lucide-react-native": "^0.513.0",
5353
"nativewind": "^4.2.1",
5454
"react": "^19.2.1",
55+
"react-dom": "^19.2.1",
5556
"react-native": "0.81.5",
5657
"react-native-awesome-slider": "^2.5.3",
5758
"react-native-blob-util": "^0.21.2",
@@ -65,6 +66,7 @@
6566
"react-native-safe-area-context": "~5.6.0",
6667
"react-native-screens": "~4.16.0",
6768
"react-native-svg": "15.12.1",
69+
"react-native-web": "^0.21.0",
6870
"react-native-webview": "13.15.0",
6971
"react-native-worklets": "0.5.1",
7072
"sonner-native": "^0.22.2",

apps/mobile/tailwind.config.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ const { hairlineWidth } = require("nativewind/theme");
33
/** @type {import('tailwindcss').Config} */
44
module.exports = {
55
// NOTE: Update this to include the paths to all of your component files.
6-
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
6+
content: [
7+
"./app/**/*.{js,jsx,ts,tsx}",
8+
"./components/**/*.{js,jsx,ts,tsx}",
9+
"../../packages/shared-react/**/*.{js,jsx,ts,tsx}",
10+
],
711
presets: [require("nativewind/preset")],
812
theme: {
913
extend: {
@@ -47,7 +51,7 @@ module.exports = {
4751
},
4852
},
4953
},
50-
plugins: [],
54+
plugins: [require("@tailwindcss/typography")],
5155
};
5256

5357
function withOpacity(variableName) {

apps/web/app/reader/[bookmarkId]/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ export default function ReaderViewPage() {
128128
<Suspense fallback={<FullPageSpinner />}>
129129
<div className="overflow-x-hidden">
130130
<ReaderView
131-
className="prose prose-neutral max-w-none break-words dark:prose-invert [&_code]:break-all [&_img]:h-auto [&_img]:max-w-full [&_pre]:overflow-x-auto [&_table]:block [&_table]:overflow-x-auto"
132131
style={{
133132
fontFamily: READER_FONT_FAMILIES[settings.fontFamily],
134133
fontSize: `${settings.fontSize}px`,

apps/web/components/dashboard/preview/LinkContentSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export default function LinkContentSection({
149149
content = (
150150
<div className="h-full w-full overflow-y-auto overflow-x-hidden px-3 sm:px-4">
151151
<ReaderView
152-
className="prose prose-neutral mx-auto max-w-none break-words dark:prose-invert [&_code]:break-all [&_img]:h-auto [&_img]:max-w-full [&_pre]:overflow-x-auto [&_table]:block [&_table]:overflow-x-auto"
152+
className="mx-auto"
153153
style={{
154154
fontFamily: READER_FONT_FAMILIES[settings.fontFamily],
155155
fontSize: `${settings.fontSize}px`,

apps/web/components/dashboard/preview/ReaderView.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useTranslation } from "@/lib/i18n/client";
44
import { useQuery } from "@tanstack/react-query";
55
import { FileX } from "lucide-react";
66

7+
import BookmarkHTMLHighlighter from "@karakeep/shared-react/components/BookmarkHtmlHighlighter";
78
import {
89
useCreateHighlight,
910
useDeleteHighlight,
@@ -12,8 +13,6 @@ import {
1213
import { useTRPC } from "@karakeep/shared-react/trpc";
1314
import { BookmarkTypes } from "@karakeep/shared/types/bookmarks";
1415

15-
import BookmarkHTMLHighlighter from "./BookmarkHtmlHighlighter";
16-
1716
export default function ReaderView({
1817
bookmarkId,
1918
className,
Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1 @@
1-
// Tailwind requires the color to be complete strings (can't be dynamic), so we have to list all the strings here manually.
2-
export const HIGHLIGHT_COLOR_MAP = {
3-
bg: {
4-
red: "bg-red-200",
5-
green: "bg-green-200",
6-
blue: "bg-blue-200",
7-
yellow: "bg-yellow-200",
8-
} as const,
9-
["border-l"]: {
10-
red: "border-l-red-200",
11-
green: "border-l-green-200",
12-
blue: "border-l-blue-200",
13-
yellow: "border-l-yellow-200",
14-
} as const,
15-
};
1+
export { HIGHLIGHT_COLOR_MAP } from "@karakeep/shared-react/components/highlights";

apps/web/components/ui/button.tsx

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import type { VariantProps } from "class-variance-authority";
21
import * as React from "react";
3-
import { cn } from "@/lib/utils";
4-
import { Slot } from "@radix-ui/react-slot";
5-
import { cva } from "class-variance-authority";
2+
3+
import type { ButtonProps } from "@karakeep/shared-react/components/ui/button";
4+
import { Button } from "@karakeep/shared-react/components/ui/button";
65

76
import {
87
Tooltip,
@@ -11,60 +10,11 @@ import {
1110
TooltipTrigger,
1211
} from "./tooltip";
1312

14-
const buttonVariants = cva(
15-
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
16-
{
17-
variants: {
18-
variant: {
19-
none: "",
20-
default: "bg-primary text-primary-foreground hover:bg-primary/90",
21-
destructive:
22-
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
23-
destructiveOutline:
24-
"border border-destructive bg-transparent text-destructive hover:bg-destructive/90 hover:text-destructive-foreground",
25-
outline:
26-
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
27-
secondary:
28-
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
29-
ghost:
30-
"hover:bg-accent hover:text-accent-foreground focus-visible:ring-0 focus-visible:ring-offset-0",
31-
border: "border border-input hover:bg-accent",
32-
link: "text-primary underline-offset-4 hover:underline",
33-
},
34-
size: {
35-
none: "",
36-
default: "h-10 px-4 py-2",
37-
sm: "h-9 rounded-md px-3",
38-
lg: "h-11 rounded-md px-8",
39-
icon: "size-10",
40-
},
41-
},
42-
defaultVariants: {
43-
variant: "default",
44-
size: "default",
45-
},
46-
},
47-
);
48-
49-
export interface ButtonProps
50-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
51-
VariantProps<typeof buttonVariants> {
52-
asChild?: boolean;
53-
}
54-
55-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
56-
({ className, variant, size, asChild = false, ...props }, ref) => {
57-
const Comp = asChild ? Slot : "button";
58-
return (
59-
<Comp
60-
className={cn(buttonVariants({ variant, size, className }))}
61-
ref={ref}
62-
{...props}
63-
/>
64-
);
65-
},
66-
);
67-
Button.displayName = "Button";
13+
export {
14+
Button,
15+
buttonVariants,
16+
type ButtonProps,
17+
} from "@karakeep/shared-react/components/ui/button";
6818

6919
const ButtonWithTooltip = React.forwardRef<
7020
HTMLButtonElement,
@@ -83,4 +33,4 @@ const ButtonWithTooltip = React.forwardRef<
8333
});
8434
ButtonWithTooltip.displayName = "ButtonWithTooltip";
8535

86-
export { Button, buttonVariants, ButtonWithTooltip };
36+
export { ButtonWithTooltip };

apps/web/components/ui/popover.tsx

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,5 @@
1-
"use client";
2-
3-
import * as React from "react";
4-
import { cn } from "@/lib/utils";
5-
import * as PopoverPrimitive from "@radix-ui/react-popover";
6-
7-
const Popover = PopoverPrimitive.Root;
8-
9-
const PopoverTrigger = PopoverPrimitive.Trigger;
10-
11-
const PopoverContent = React.forwardRef<
12-
React.ElementRef<typeof PopoverPrimitive.Content>,
13-
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
14-
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
15-
<PopoverPrimitive.Portal>
16-
<PopoverPrimitive.Content
17-
ref={ref}
18-
align={align}
19-
sideOffset={sideOffset}
20-
className={cn(
21-
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
22-
className,
23-
)}
24-
{...props}
25-
/>
26-
</PopoverPrimitive.Portal>
27-
));
28-
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
29-
30-
export { Popover, PopoverTrigger, PopoverContent };
1+
export {
2+
Popover,
3+
PopoverTrigger,
4+
PopoverContent,
5+
} from "@karakeep/shared-react/components/ui/popover";

0 commit comments

Comments
 (0)