From 09652176f97f11bc06f4c9b57a448e14744eac12 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 24 May 2025 12:59:43 +0000 Subject: feat: Allow defaulting to reader mode when clicking on bookmarks. Fixes #662 --- apps/web/app/dashboard/layout.tsx | 42 +++++---- apps/web/app/settings/layout.tsx | 17 ++-- .../components/dashboard/bookmarks/LinkCard.tsx | 26 +++++- apps/web/components/settings/UserOptions.tsx | 102 ++++++++++++++++++++- apps/web/lib/i18n/locales/en/translation.json | 8 +- apps/web/lib/userSettings.tsx | 33 +++++++ 6 files changed, 195 insertions(+), 33 deletions(-) create mode 100644 apps/web/lib/userSettings.tsx (limited to 'apps/web') 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 ( - - - - - } - /> - } - mobileSidebar={} - modal={modal} - > - {children} - + + + + + + } + /> + } + mobileSidebar={} + modal={modal} + > + {children} + + ); } 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 ( - } - mobileSidebar={} - > - {children} - + + } + mobileSidebar={} + > + {children} + + ); } 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 ( - + {getBookmarkTitle(bookmark) ?? parsedUrl.host} ); @@ -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 ( 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>({ + 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 ( +
+ ( +
+ + +
+ )} + /> + + ); +} + export function UserOptions() { const { t } = useTranslation(); @@ -46,9 +139,12 @@ export function UserOptions() {
{t("settings.info.options")}
-
- - +
+
+ + +
+
); 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({ + bookmarkClickAction: "open_original_link", +}); + +export function UserSettingsContextProvider({ + userSettings, + children, +}: { + userSettings: ZUserSettings; + children: React.ReactNode; +}) { + const { data } = api.users.settings.useQuery(undefined, { + initialData: userSettings, + }); + + return ( + + {children} + + ); +} + +export function useUserSettings() { + return useContext(UserSettingsContext); +} -- cgit v1.2.3-70-g09d2