feat(desktop): resolve url with site url to make sure url is valid (#3511)
* feat(desktop): enhance entry actions and URL resolution with siteUrl support * update * update
This commit is contained in:
parent
fd74a0ed7e
commit
2bbb00c9fa
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
@ -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: <i className="i-mgc-web-cute-re" />,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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<{
|
|||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{item.entries
|
||||
.filter((e) => !!e)
|
||||
.map((entry) => {
|
||||
const assertEntry = entry
|
||||
return (
|
||||
<a
|
||||
key={assertEntry.id}
|
||||
href={assertEntry.url || void 0}
|
||||
target="_blank"
|
||||
className="group relative flex flex-col overflow-hidden rounded-lg bg-zinc-50/50 shadow-zinc-100 transition-all duration-200 hover:-translate-y-px hover:shadow-md dark:bg-zinc-800/50 dark:shadow-neutral-700/50"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="aspect-[3/2] w-full overflow-hidden">
|
||||
<FeedCardMediaThumbnail entry={assertEntry} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-between p-3">
|
||||
{assertEntry.title ? (
|
||||
<div className="line-clamp-2 text-xs font-medium leading-4 text-zinc-900 group-hover:text-black dark:text-zinc-200 dark:group-hover:text-white">
|
||||
{assertEntry.title}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<i className="i-mgc-link-cute-re shrink-0 translate-y-px self-start text-[14px]" />
|
||||
<span className="line-clamp-2 break-all">
|
||||
{assertEntry.url || "Untitled"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<RelativeTime
|
||||
date={assertEntry.publishedAt}
|
||||
displayAbsoluteTimeAfterDay={Infinity}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
.map((entry) => (
|
||||
<SearchResultContent key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
@ -516,3 +484,37 @@ const FeedCardMediaThumbnail: FC<{
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchResultContent: FC<{
|
||||
entry: NonUndefined<DiscoverSearchData[number]["entries"]>[number]
|
||||
}> = memo(({ entry }) => {
|
||||
const safeUrl = useFeedSafeUrl(entry.id)
|
||||
return (
|
||||
<a
|
||||
key={entry.id}
|
||||
href={safeUrl ?? "#"}
|
||||
target="_blank"
|
||||
className="group relative flex flex-col overflow-hidden rounded-lg bg-zinc-50/50 shadow-zinc-100 transition-all duration-200 hover:-translate-y-px hover:shadow-md dark:bg-zinc-800/50 dark:shadow-neutral-700/50"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="aspect-[3/2] w-full overflow-hidden">
|
||||
<FeedCardMediaThumbnail entry={entry} />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-between p-3">
|
||||
{entry.title ? (
|
||||
<div className="line-clamp-2 text-xs font-medium leading-4 text-zinc-900 group-hover:text-black dark:text-zinc-200 dark:group-hover:text-white">
|
||||
{entry.title}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<i className="i-mgc-link-cute-re shrink-0 translate-y-px self-start text-[14px]" />
|
||||
<span className="line-clamp-2 break-all">{entry.url || "Untitled"}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<RelativeTime date={entry.publishedAt} displayAbsoluteTimeAfterDay={Infinity} />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<EntryContentProps> = ({
|
|||
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<EntryContentProps> = ({
|
|||
</article>
|
||||
</div>
|
||||
</ScrollArea.ScrollArea>
|
||||
<SourceContentPanel src={entry.entries.url} />
|
||||
<SourceContentPanel src={safeUrl ?? "#"} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
Loading…
Reference in New Issue