diff --git a/src/renderer/src/atoms/settings/general.ts b/src/renderer/src/atoms/settings/general.ts index 011846ee5..e41737bc5 100644 --- a/src/renderer/src/atoms/settings/general.ts +++ b/src/renderer/src/atoms/settings/general.ts @@ -19,6 +19,8 @@ const createDefaultSettings = (): GeneralSettings => ({ // UX // autoHideFeedColumn: true, groupByDate: true, + // Secure + jumpOutLinkWarn: true, }) export const { diff --git a/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx b/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx index 3bb3f2aad..021bc1f6c 100644 --- a/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx +++ b/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx @@ -1,4 +1,5 @@ import { FeedViewType } from "@renderer/lib/enum" +import { WarnGoToExternalLink } from "@renderer/modules/entry-content/components/WarnGoToExternalLink" import { useEntryContentContext } from "@renderer/modules/entry-content/hooks" import { useFeedByIdSelector } from "@renderer/store/feed" import { useMemo } from "react" @@ -46,18 +47,19 @@ export const MarkdownLink = (props: LinkProps) => { return ( - {props.children} {typeof props.children === "string" && ( )} - + {!!props.href && ( diff --git a/src/renderer/src/modules/entry-content/entry-placeholder.tsx b/src/renderer/src/modules/entry-content/components/EntryPlaceholderLogo.tsx similarity index 92% rename from src/renderer/src/modules/entry-content/entry-placeholder.tsx rename to src/renderer/src/modules/entry-content/components/EntryPlaceholderLogo.tsx index e23012574..998eb5dd6 100644 --- a/src/renderer/src/modules/entry-content/entry-placeholder.tsx +++ b/src/renderer/src/modules/entry-content/components/EntryPlaceholderLogo.tsx @@ -3,7 +3,7 @@ import { stopPropagation } from "@renderer/lib/dom" import { cn } from "@renderer/lib/utils" import { useFeedHeaderTitle } from "@renderer/store/feed" -import { useEntryContentPlaceholderLogoShow } from "./atoms" +import { useEntryContentPlaceholderLogoShow } from "../atoms" export const EntryPlaceholderLogo = () => { const title = useFeedHeaderTitle() diff --git a/src/renderer/src/modules/entry-content/read-history.tsx b/src/renderer/src/modules/entry-content/components/EntryReadHistory.tsx similarity index 97% rename from src/renderer/src/modules/entry-content/read-history.tsx rename to src/renderer/src/modules/entry-content/components/EntryReadHistory.tsx index 67703d8e1..8bf0ff57c 100644 --- a/src/renderer/src/modules/entry-content/read-history.tsx +++ b/src/renderer/src/modules/entry-content/components/EntryReadHistory.tsx @@ -15,7 +15,7 @@ import { useEntryReadHistory } from "@renderer/store/entry" import { useUserById } from "@renderer/store/user" import { Fragment } from "react" -import { usePresentUserProfileModal } from "../profile/hooks" +import { usePresentUserProfileModal } from "../../profile/hooks" export const EntryReadHistory: Component<{ entryId: string }> = ({ entryId, diff --git a/src/renderer/src/modules/entry-content/components/WarnGoToExternalLink.tsx b/src/renderer/src/modules/entry-content/components/WarnGoToExternalLink.tsx new file mode 100644 index 000000000..f1b483cfa --- /dev/null +++ b/src/renderer/src/modules/entry-content/components/WarnGoToExternalLink.tsx @@ -0,0 +1,115 @@ +import { Label } from "@radix-ui/react-label" +import { PopoverPortal } from "@radix-ui/react-popover" +import { useGeneralSettingKey } from "@renderer/atoms/settings/general" +import { Button } from "@renderer/components/ui/button" +import { Checkbox } from "@renderer/components/ui/checkbox" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@renderer/components/ui/popover" +import { jotaiStore } from "@renderer/lib/jotai" +import { getStorageNS } from "@renderer/lib/ns" +import { m } from "framer-motion" +import { atomWithStorage } from "jotai/utils" +import { forwardRef, Fragment, useState } from "react" + +const TrustedKey = getStorageNS("trusted-external-link") +const trustedAtom = atomWithStorage(TrustedKey, [] as string[], undefined, { + getOnInit: true, +}) + +const trustedDefaultLinks = new Set([ + "github.com", + "gitlab.com", + "google.com", + "sspai.com", +]) + +const getURLDomain = (url: string) => { + if (URL.canParse(url)) { + const urlObj = new URL(url) + return urlObj.hostname + } + return null +} +export const WarnGoToExternalLink = forwardRef< + HTMLAnchorElement, + React.DetailedHTMLProps< + React.AnchorHTMLAttributes, + HTMLAnchorElement + > +>(({ ...rest }, ref) => { + const [open, setOpen] = useState(false) + const [checked, setChecked] = useState(false) + + const shouldWarn = useGeneralSettingKey("jumpOutLinkWarn") + const handleOpen: React.MouseEventHandler = (e) => { + rest.onClick?.(e) + if (!shouldWarn) return + const { href } = rest + if (!href) return + const domain = getURLDomain(href) + + if (domain && + !trustedDefaultLinks.has(domain) && + !jotaiStore.get(trustedAtom).includes(domain)) { + setOpen(true) + e.preventDefault() + } + } + const handleGo = () => { + open() + if (!checked) { + return + } + + const { href } = rest + if (!href) return + + const domain = getURLDomain(href) + if (domain && !jotaiStore.get(trustedAtom).includes(domain)) { + jotaiStore.set(trustedAtom, (prev) => [...prev, domain]) + } + + function open() { + if (!rest.href) return + window.open(rest.href, "_blank", "noopener,noreferrer") + } + } + return ( + + !v && setOpen(false)}> + + + + + +

+ You are about to leave this site to go to an external page, do you + trust this URL and go to it? +

+

{rest.href}

+ +
+ + + +
+
+
+
+
+ ) +}) diff --git a/src/renderer/src/modules/entry-content/header.tsx b/src/renderer/src/modules/entry-content/header.tsx index a41bca714..6dc6c0508 100644 --- a/src/renderer/src/modules/entry-content/header.tsx +++ b/src/renderer/src/modules/entry-content/header.tsx @@ -7,7 +7,7 @@ import { useEntry } from "@renderer/store/entry/hooks" import { AnimatePresence, m } from "framer-motion" import { useEntryContentScrollToTop, useEntryTitleMeta } from "./atoms" -import { EntryReadHistory } from "./read-history" +import { EntryReadHistory } from "./components/EntryReadHistory" export function EntryHeader({ view, diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx index fb8ff366d..affe89a8d 100644 --- a/src/renderer/src/modules/entry-content/index.tsx +++ b/src/renderer/src/modules/entry-content/index.tsx @@ -36,7 +36,8 @@ import { LoadingCircle } from "../../components/ui/loading" import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily" import { EntryTranslation } from "../entry-column/translation" import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms" -import { EntryPlaceholderLogo } from "./entry-placeholder" +import { EntryPlaceholderLogo } from "./components/EntryPlaceholderLogo" +import { WarnGoToExternalLink } from "./components/WarnGoToExternalLink" import { EntryHeader } from "./header" import { EntryContentProvider } from "./provider" @@ -147,6 +148,7 @@ export const EntryContentRender: Component<{ entryId: string }> = ({ "h-0 min-w-0 grow overflow-y-auto @container", className, )} + scrollbarClassName="mr-1" viewportClassName="p-5" ref={scrollerRef} > @@ -167,7 +169,7 @@ export const EntryContentRender: Component<{ entryId: string }> = ({ onContextMenu={stopPropagation} className="relative m-auto min-w-0 max-w-[550px] @3xl:max-w-[70ch]" > - = ({ - + diff --git a/src/renderer/src/modules/settings/tabs/general.tsx b/src/renderer/src/modules/settings/tabs/general.tsx index 437985bde..f33f6ae3c 100644 --- a/src/renderer/src/modules/settings/tabs/general.tsx +++ b/src/renderer/src/modules/settings/tabs/general.tsx @@ -2,9 +2,7 @@ import { setGeneralSetting, useGeneralSettingValue, } from "@renderer/atoms/settings/general" -import { - createSetting, -} from "@renderer/atoms/settings/helper" +import { createSetting } from "@renderer/atoms/settings/helper" import { initPostHog } from "@renderer/initialize/posthog" import { tipcClient } from "@renderer/lib/client" import { clearLocalPersistStoreData } from "@renderer/store/utils/clear" @@ -78,7 +76,11 @@ export const SettingGeneral = () => { description: "Automatically mark single-level entries (e.g., social media posts, pictures, video views) as read when they enter the view.", }), - + { type: "title", value: "Secure" }, + defineSettingItem("jumpOutLinkWarn", { + label: "Warn when opening external links", + description: "When you open an untrusted external link, you need to make sure that you open the link.", + }), { type: "title", value: "Privacy & Data", diff --git a/src/renderer/src/styles/tailwind-extend.css b/src/renderer/src/styles/tailwind-extend.css index c1d7d7f9a..960953646 100644 --- a/src/renderer/src/styles/tailwind-extend.css +++ b/src/renderer/src/styles/tailwind-extend.css @@ -159,7 +159,7 @@ appearance: none; border-radius: 5px; border-width: 1px; - border-color: theme(colors.theme.inactive); + border-color: theme(colors.border); --tw-border-opacity: 0.2; } diff --git a/src/shared/src/interface/settings.ts b/src/shared/src/interface/settings.ts index 273f78ed2..2903405f7 100644 --- a/src/shared/src/interface/settings.ts +++ b/src/shared/src/interface/settings.ts @@ -7,6 +7,7 @@ export interface GeneralSettings { hoverMarkUnread: boolean renderMarkUnread: boolean groupByDate: boolean + jumpOutLinkWarn: boolean } export interface UISettings {