diff --git a/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx b/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx index 44179cdfb..af72b34dd 100644 --- a/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx +++ b/apps/desktop/src/renderer/src/hooks/biz/useEntryActions.tsx @@ -141,6 +141,7 @@ export const useEntryActions = ({ type: feed.type, ownerUserId: feed.ownerUserId, id: feed.id, + siteUrl: feed.siteUrl, } }) const listId = useRouteParamsSelector((s) => s.listId) @@ -242,7 +243,9 @@ export const useEntryActions = ({ }), new EntryActionMenuItem({ id: COMMAND_ID.entry.viewSourceContent, - onClick: runCmdFn(COMMAND_ID.entry.viewSourceContent, [{ entryId }]), + onClick: runCmdFn(COMMAND_ID.entry.viewSourceContent, [ + { entryId, siteUrl: feed?.siteUrl }, + ]), hide: isMobile() || !entry?.entries.url, active: isShowSourceContent, entryId, @@ -343,6 +346,7 @@ export const useEntryActions = ({ isEntryInReadability, feed?.id, feed?.ownerUserId, + feed?.siteUrl, hasEntry, imageLength, inList, diff --git a/apps/desktop/src/renderer/src/hooks/common/useFeedSafeUrl.ts b/apps/desktop/src/renderer/src/hooks/common/useFeedSafeUrl.ts new file mode 100644 index 000000000..098ea52b5 --- /dev/null +++ b/apps/desktop/src/renderer/src/hooks/common/useFeedSafeUrl.ts @@ -0,0 +1,28 @@ +import { resolveUrlWithBase } from "@follow/utils/utils" +import { useMemo } from "react" + +import { useEntry } from "~/store/entry" +import { useFeedById } from "~/store/feed" +import { useInboxById } from "~/store/inbox" + +export const useFeedSafeUrl = (entryId: string) => { + const entry = useEntry(entryId) + const feed = useFeedById(entry?.feedId) + const inbox = useInboxById(entry?.inboxId, (inbox) => inbox !== null) + + return useMemo(() => { + if (inbox) return entry?.entries.authorUrl + const href = entry?.entries.url + if (!href) return "#" + + if (href.startsWith("http")) { + const domain = new URL(href).hostname + if (domain === "localhost") return "#" + + return href + } + const feedSiteUrl = feed?.type === "feed" ? feed.siteUrl : null + if (feedSiteUrl) return resolveUrlWithBase(href, feedSiteUrl) + return href + }, [entry?.entries.authorUrl, entry?.entries.url, feed?.type, inbox]) +} diff --git a/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx b/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx index f9e526dac..cf2e9ce03 100644 --- a/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx +++ b/apps/desktop/src/renderer/src/modules/command/commands/entry.tsx @@ -1,6 +1,6 @@ import { FeedViewType, UserRole } from "@follow/constants" import { IN_ELECTRON } from "@follow/shared/constants" -import { cn } from "@follow/utils/utils" +import { cn, resolveUrlWithBase } from "@follow/utils/utils" import { useMutation } from "@tanstack/react-query" import { useTranslation } from "react-i18next" import { toast } from "sonner" @@ -234,7 +234,7 @@ export const useRegisterEntryCommands = () => { id: COMMAND_ID.entry.viewSourceContent, label: t("entry_actions.view_source_content"), icon: , - run: ({ entryId }) => { + run: ({ entryId, siteUrl }) => { if (!getShowSourceContent()) { const entry = useEntryStore.getState().flatMapEntries[entryId] if (!entry || !entry.entries.url) { @@ -250,7 +250,7 @@ export const useRegisterEntryCommands = () => { if (viewPreviewInModal) { showSourceContentModal({ title: entry.entries.title ?? undefined, - src: entry.entries.url, + src: siteUrl ? resolveUrlWithBase(entry?.entries.url, siteUrl) : entry?.entries.url, }) return } diff --git a/apps/desktop/src/renderer/src/modules/command/commands/types.ts b/apps/desktop/src/renderer/src/modules/command/commands/types.ts index 81bc3cf8d..07e7eb88a 100644 --- a/apps/desktop/src/renderer/src/modules/command/commands/types.ts +++ b/apps/desktop/src/renderer/src/modules/command/commands/types.ts @@ -42,7 +42,7 @@ export type OpenInBrowserCommand = Command<{ export type ViewSourceContentCommand = Command<{ id: typeof COMMAND_ID.entry.viewSourceContent - fn: (data: { entryId: string }) => void + fn: (data: { entryId: string; siteUrl?: string | null | undefined }) => void }> export type ShareCommand = Command<{ diff --git a/apps/desktop/src/renderer/src/modules/discover/form.tsx b/apps/desktop/src/renderer/src/modules/discover/form.tsx index 1cb82bf51..f5131e1f3 100644 --- a/apps/desktop/src/renderer/src/modules/discover/form.tsx +++ b/apps/desktop/src/renderer/src/modules/discover/form.tsx @@ -30,6 +30,7 @@ import { Media } from "~/components/ui/media" import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { useFollow } from "~/hooks/biz/useFollow" import { getRouteParams } from "~/hooks/biz/useRouteParams" +import { useFeedSafeUrl } from "~/hooks/common/useFeedSafeUrl" import { apiClient } from "~/lib/api-fetch" import { UrlBuilder } from "~/lib/url-builder" @@ -365,42 +366,9 @@ const SearchCard: FC<{
{item.entries .filter((e) => !!e) - .map((entry) => { - const assertEntry = entry - return ( - -
- -
-
- {assertEntry.title ? ( -
- {assertEntry.title} -
- ) : ( -
- - - {assertEntry.url || "Untitled"} - -
- )} -
- -
-
-
- ) - })} + .map((entry) => ( + + ))}
)} @@ -516,3 +484,37 @@ const FeedCardMediaThumbnail: FC<{ ) } + +const SearchResultContent: FC<{ + entry: NonUndefined[number] +}> = memo(({ entry }) => { + const safeUrl = useFeedSafeUrl(entry.id) + return ( + +
+ +
+
+ {entry.title ? ( +
+ {entry.title} +
+ ) : ( +
+ + {entry.url || "Untitled"} +
+ )} +
+ +
+
+
+ ) +}) diff --git a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx index e6d10410d..b12d38c17 100644 --- a/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx +++ b/apps/desktop/src/renderer/src/modules/entry-content/components/EntryTitle.tsx @@ -5,6 +5,7 @@ import { useUISettingKey } from "~/atoms/settings/ui" import { useWhoami } from "~/atoms/user" import { RelativeTime } from "~/components/ui/datetime" import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" +import { useFeedSafeUrl } from "~/hooks/common/useFeedSafeUrl" import { FeedIcon } from "~/modules/feed/feed-icon" import { useEntryTranslation } from "~/store/ai/hook" import { useEntry, useEntryReadHistory } from "~/store/entry" @@ -18,37 +19,13 @@ interface EntryLinkProps { compact?: boolean } -const safeUrl = (url: string, baseUrl: string) => { - try { - return new URL(url, baseUrl).href - } catch { - return url - } -} - export const EntryTitle = ({ entryId, compact }: EntryLinkProps) => { const user = useWhoami() const entry = useEntry(entryId) const feed = useFeedById(entry?.feedId) const inbox = useInboxById(entry?.inboxId) const entryHistory = useEntryReadHistory(entryId) - - const populatedFullHref = useMemo(() => { - if (inbox) return entry?.entries.authorUrl - const href = entry?.entries.url - if (!href) return "#" - - if (href.startsWith("http")) { - const domain = new URL(href).hostname - if (domain === "localhost") return "#" - - return href - } - const feedSiteUrl = feed?.type === "feed" ? feed.siteUrl : null - if (href.startsWith("/") && feedSiteUrl) return safeUrl(href, feedSiteUrl) - return href - }, [entry?.entries.authorUrl, entry?.entries.url, feed?.siteUrl, feed?.type, inbox]) - + const populatedFullHref = useFeedSafeUrl(entryId) const translation = useEntryTranslation({ entry, extraFields: ["title"] }) const dateFormat = useUISettingKey("dateFormat") diff --git a/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx b/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx index b446ed226..25b6d2b35 100644 --- a/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx +++ b/apps/desktop/src/renderer/src/modules/entry-content/index.electron.tsx @@ -18,6 +18,7 @@ import { ShadowDOM } from "~/components/common/ShadowDOM" import { useInPeekModal } from "~/components/ui/modal/inspire/PeekModal" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" import { useAuthQuery } from "~/hooks/common" +import { useFeedSafeUrl } from "~/hooks/common/useFeedSafeUrl" import { WrappedElementProvider } from "~/providers/wrapped-element-provider" import { Queries } from "~/queries" import { useEntryTranslation } from "~/store/ai/hook" @@ -88,6 +89,8 @@ export const EntryContent: Component = ({ const contentLineHeight = useUISettingKey("contentLineHeight") const contentFontSize = useUISettingKey("contentFontSize") + const safeUrl = useFeedSafeUrl(entryId) + const stableRenderStyle = useMemo(() => { const css = {} as React.CSSProperties if (readerFontFamily) { @@ -236,7 +239,7 @@ export const EntryContent: Component = ({ - + ) diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index 38ac57b11..4573f822e 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -186,6 +186,18 @@ export const parseSafeUrl = (url: string) => { return null } } + +/** + * @deprecated Remove it in the future but not now + */ +export const resolveUrlWithBase = (url: string, baseUrl: string) => { + try { + return new URL(url, baseUrl).href + } catch { + return url + } +} + export const getUrlIcon = (url: string, fallback?: boolean | undefined) => { let src: string let fallbackUrl = ""