aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-05-24 12:59:43 +0000
committerMohamed Bassem <me@mbassem.com>2025-05-24 12:59:43 +0000
commit09652176f97f11bc06f4c9b57a448e14744eac12 (patch)
tree5205f65bdef233328a7b4af010667c5b8c25f285 /apps/web
parent5f3fe5d1a1ad0abd2890283cbff45086cbfa442e (diff)
downloadkarakeep-09652176f97f11bc06f4c9b57a448e14744eac12.tar.zst
feat: Allow defaulting to reader mode when clicking on bookmarks. Fixes #662
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/dashboard/layout.tsx42
-rw-r--r--apps/web/app/settings/layout.tsx17
-rw-r--r--apps/web/components/dashboard/bookmarks/LinkCard.tsx26
-rw-r--r--apps/web/components/settings/UserOptions.tsx102
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json8
-rw-r--r--apps/web/lib/userSettings.tsx33
6 files changed, 195 insertions, 33 deletions
diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx
index 45b97653..c4a53e4b 100644
--- a/apps/web/app/dashboard/layout.tsx
+++ b/apps/web/app/dashboard/layout.tsx
@@ -4,6 +4,7 @@ import MobileSidebar from "@/components/shared/sidebar/MobileSidebar";
import Sidebar from "@/components/shared/sidebar/Sidebar";
import SidebarLayout from "@/components/shared/sidebar/SidebarLayout";
import { Separator } from "@/components/ui/separator";
+import { UserSettingsContextProvider } from "@/lib/userSettings";
import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth";
import { TFunction } from "i18next";
@@ -30,7 +31,10 @@ export default async function Dashboard({
redirect("/");
}
- const lists = await api.lists.list();
+ const [lists, userSettings] = await Promise.all([
+ api.lists.list(),
+ api.users.settings(),
+ ]);
const items = (t: TFunction) =>
[
@@ -75,22 +79,24 @@ export default async function Dashboard({
];
return (
- <SidebarLayout
- sidebar={
- <Sidebar
- items={items}
- extraSections={
- <>
- <Separator />
- <AllLists initialData={lists} />
- </>
- }
- />
- }
- mobileSidebar={<MobileSidebar items={mobileSidebar} />}
- modal={modal}
- >
- {children}
- </SidebarLayout>
+ <UserSettingsContextProvider userSettings={userSettings}>
+ <SidebarLayout
+ sidebar={
+ <Sidebar
+ items={items}
+ extraSections={
+ <>
+ <Separator />
+ <AllLists initialData={lists} />
+ </>
+ }
+ />
+ }
+ mobileSidebar={<MobileSidebar items={mobileSidebar} />}
+ modal={modal}
+ >
+ {children}
+ </SidebarLayout>
+ </UserSettingsContextProvider>
);
}
diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx
index 9bac783c..1f7c5c12 100644
--- a/apps/web/app/settings/layout.tsx
+++ b/apps/web/app/settings/layout.tsx
@@ -1,6 +1,8 @@
import MobileSidebar from "@/components/shared/sidebar/MobileSidebar";
import Sidebar from "@/components/shared/sidebar/Sidebar";
import SidebarLayout from "@/components/shared/sidebar/SidebarLayout";
+import { UserSettingsContextProvider } from "@/lib/userSettings";
+import { api } from "@/server/api/client";
import { TFunction } from "i18next";
import {
ArrowLeft,
@@ -79,12 +81,15 @@ export default async function SettingsLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
+ const userSettings = await api.users.settings();
return (
- <SidebarLayout
- sidebar={<Sidebar items={settingsSidebarItems} />}
- mobileSidebar={<MobileSidebar items={settingsSidebarItems} />}
- >
- {children}
- </SidebarLayout>
+ <UserSettingsContextProvider userSettings={userSettings}>
+ <SidebarLayout
+ sidebar={<Sidebar items={settingsSidebarItems} />}
+ mobileSidebar={<MobileSidebar items={settingsSidebarItems} />}
+ >
+ {children}
+ </SidebarLayout>
+ </UserSettingsContextProvider>
);
}
diff --git a/apps/web/components/dashboard/bookmarks/LinkCard.tsx b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
index ec224ca6..2c91bd08 100644
--- a/apps/web/components/dashboard/bookmarks/LinkCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
@@ -2,6 +2,7 @@
import Image from "next/image";
import Link from "next/link";
+import { useUserSettings } from "@/lib/userSettings";
import type { ZBookmarkTypeLink } from "@karakeep/shared/types/bookmarks";
import {
@@ -14,11 +15,25 @@ import {
import { BookmarkLayoutAdaptingCard } from "./BookmarkLayoutAdaptingCard";
import FooterLinkURL from "./FooterLinkURL";
+const useOnClickUrl = (bookmark: ZBookmarkTypeLink) => {
+ const userSettings = useUserSettings();
+ return {
+ urlTarget:
+ userSettings.bookmarkClickAction === "open_original_link"
+ ? ("_blank" as const)
+ : ("_self" as const),
+ onClickUrl:
+ userSettings.bookmarkClickAction === "expand_bookmark_preview"
+ ? `/dashboard/preview/${bookmark.id}`
+ : bookmark.content.url,
+ };
+};
+
function LinkTitle({ bookmark }: { bookmark: ZBookmarkTypeLink }) {
- const link = bookmark.content;
- const parsedUrl = new URL(link.url);
+ const { onClickUrl, urlTarget } = useOnClickUrl(bookmark);
+ const parsedUrl = new URL(bookmark.content.url);
return (
- <Link href={link.url} target="_blank" rel="noreferrer">
+ <Link href={onClickUrl} target={urlTarget} rel="noreferrer">
{getBookmarkTitle(bookmark) ?? parsedUrl.host}
</Link>
);
@@ -31,6 +46,7 @@ function LinkImage({
bookmark: ZBookmarkTypeLink;
className?: string;
}) {
+ const { onClickUrl, urlTarget } = useOnClickUrl(bookmark);
const link = bookmark.content;
const imgComponent = (url: string, unoptimized: boolean) => (
@@ -61,8 +77,8 @@ function LinkImage({
return (
<Link
- href={link.url}
- target="_blank"
+ href={onClickUrl}
+ target={urlTarget}
rel="noreferrer"
className={className}
>
diff --git a/apps/web/components/settings/UserOptions.tsx b/apps/web/components/settings/UserOptions.tsx
index 33ffc46a..c8aa5e86 100644
--- a/apps/web/components/settings/UserOptions.tsx
+++ b/apps/web/components/settings/UserOptions.tsx
@@ -1,11 +1,23 @@
"use client";
+import { useEffect } from "react";
+import { useClientConfig } from "@/lib/clientConfig";
import { useTranslation } from "@/lib/i18n/client";
import { useInterfaceLang } from "@/lib/userLocalSettings/bookmarksLayout";
import { updateInterfaceLang } from "@/lib/userLocalSettings/userLocalSettings";
+import { useUserSettings } from "@/lib/userSettings";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import { useUpdateUserSettings } from "@karakeep/shared-react/hooks/users";
import { langNameMappings } from "@karakeep/shared/langs";
+import {
+ ZUserSettings,
+ zUserSettingsSchema,
+} from "@karakeep/shared/types/users";
+import { Form, FormField } from "../ui/form";
import { Label } from "../ui/label";
import {
Select,
@@ -14,6 +26,7 @@ import {
SelectTrigger,
SelectValue,
} from "../ui/select";
+import { toast } from "../ui/use-toast";
const LanguageSelect = () => {
const lang = useInterfaceLang();
@@ -38,6 +51,86 @@ const LanguageSelect = () => {
);
};
+export default function UserSettings() {
+ const { t } = useTranslation();
+ const clientConfig = useClientConfig();
+ const data = useUserSettings();
+ const { mutate } = useUpdateUserSettings({
+ onSuccess: () => {
+ toast({
+ description: t("settings.info.user_settings.user_settings_updated"),
+ });
+ },
+ onError: () => {
+ toast({
+ description: t("common.something_went_wrong"),
+ variant: "destructive",
+ });
+ },
+ });
+
+ const bookmarkClickActionTranslation: Record<
+ ZUserSettings["bookmarkClickAction"],
+ string
+ > = {
+ open_original_link: t("settings.info.user_settings.open_external_url"),
+ expand_bookmark_preview: t(
+ "settings.info.user_settings.open_bookmark_details",
+ ),
+ };
+
+ const form = useForm<z.infer<typeof zUserSettingsSchema>>({
+ resolver: zodResolver(zUserSettingsSchema),
+ defaultValues: data,
+ });
+
+ // When the actual user setting is loaded, reset the form to the current value
+ useEffect(() => {
+ form.reset(data);
+ }, [data]);
+
+ return (
+ <Form {...form}>
+ <FormField
+ control={form.control}
+ name="bookmarkClickAction"
+ render={({ field }) => (
+ <div className="flex w-full flex-col gap-2">
+ <Label>
+ {t("settings.info.user_settings.boomark_click_action")}
+ </Label>
+ <Select
+ disabled={!!clientConfig.demoMode}
+ value={field.value}
+ onValueChange={(value) => {
+ mutate({
+ bookmarkClickAction:
+ value as ZUserSettings["bookmarkClickAction"],
+ });
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue>
+ {bookmarkClickActionTranslation[field.value]}
+ </SelectValue>
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(bookmarkClickActionTranslation).map(
+ ([value, label]) => (
+ <SelectItem key={value} value={value}>
+ {label}
+ </SelectItem>
+ ),
+ )}
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+ />
+ </Form>
+ );
+}
+
export function UserOptions() {
const { t } = useTranslation();
@@ -46,9 +139,12 @@ export function UserOptions() {
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
{t("settings.info.options")}
</div>
- <div className="flex w-full flex-col gap-2">
- <Label>{t("settings.info.interface_lang")}</Label>
- <LanguageSelect />
+ <div className="flex w-full flex-col gap-3">
+ <div className="flex w-full flex-col gap-2">
+ <Label>{t("settings.info.interface_lang")}</Label>
+ <LanguageSelect />
+ </div>
+ <UserSettings />
</div>
</div>
);
diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json
index 1eef3ac4..48d32f37 100644
--- a/apps/web/lib/i18n/locales/en/translation.json
+++ b/apps/web/lib/i18n/locales/en/translation.json
@@ -98,7 +98,13 @@
"new_password": "New Password",
"confirm_new_password": "Confirm New Password",
"options": "Options",
- "interface_lang": "Interface Language"
+ "interface_lang": "Interface Language",
+ "user_settings": {
+ "user_settings_updated": "User settings have been updated!",
+ "boomark_click_action": "Bookmark Click Action",
+ "open_external_url": "Open Original URL",
+ "open_bookmark_details": "Open Bookmark Details"
+ }
},
"ai": {
"ai_settings": "AI Settings",
diff --git a/apps/web/lib/userSettings.tsx b/apps/web/lib/userSettings.tsx
new file mode 100644
index 00000000..727c823e
--- /dev/null
+++ b/apps/web/lib/userSettings.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { createContext, useContext } from "react";
+
+import { ZUserSettings } from "@karakeep/shared/types/users";
+
+import { api } from "./trpc";
+
+export const UserSettingsContext = createContext<ZUserSettings>({
+ bookmarkClickAction: "open_original_link",
+});
+
+export function UserSettingsContextProvider({
+ userSettings,
+ children,
+}: {
+ userSettings: ZUserSettings;
+ children: React.ReactNode;
+}) {
+ const { data } = api.users.settings.useQuery(undefined, {
+ initialData: userSettings,
+ });
+
+ return (
+ <UserSettingsContext.Provider value={data}>
+ {children}
+ </UserSettingsContext.Provider>
+ );
+}
+
+export function useUserSettings() {
+ return useContext(UserSettingsContext);
+}