feat: markdown link supports open share feed link directly (#4811)

* feat: markdown link supports open share feed link directly

* docs: update changelog

* fix: revert stopPropagation

* fix: revert stopPropagation

* fix: revert stopPropagation
This commit is contained in:
XLor 2026-01-16 21:35:54 +08:00 committed by GitHub
parent 420c6e9683
commit f03e81b8fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 71 additions and 2 deletions

View File

@ -2,6 +2,8 @@
## Shiny new things
- markdown link supports open share feed link directly in the app
## Improvements
## No longer broken

View File

@ -7,11 +7,15 @@ import {
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.jsx"
import { useCorrectZIndex } from "@follow/components/ui/z-index/ctx.js"
import { cn, stopPropagation } from "@follow/utils"
import { env } from "@follow/shared/env.desktop"
import { feedSyncServices } from "@follow/store/feed/store"
import { cn, parseSafeUrl, stopPropagation } from "@follow/utils"
import type { MouseEvent } from "react"
import { use, useCallback } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
import { copyToClipboard } from "~/lib/clipboard"
import { MarkdownRenderActionContext } from "../context"
@ -21,6 +25,7 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
const { t } = useTranslation()
const populatedFullHref = transformUrl(props.href)
const shareFeedInfo = parseShareFeedInfo(populatedFullHref)
const handleCopyLink = useCallback(async () => {
try {
@ -34,6 +39,25 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
}
}, [populatedFullHref, t])
const handleClickLink = useCallback(
async (event: MouseEvent<HTMLAnchorElement>) => {
stopPropagation(event)
if (!shareFeedInfo) {
return
}
event.preventDefault()
const view = await resolveShareFeedView(shareFeedInfo)
navigateEntry({
feedId: shareFeedInfo.id,
entryId: null,
view,
})
},
[shareFeedInfo],
)
const parseTimeStamp = isAudio(populatedFullHref)
const zIndex = useCorrectZIndex(0)
if (parseTimeStamp) {
@ -58,7 +82,7 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
title={props.title}
target="_blank"
rel="noreferrer"
onClick={stopPropagation}
onClick={handleClickLink}
>
{props.children}
@ -93,3 +117,46 @@ export const MarkdownLink: Component<LinkProps> = (props) => {
</Tooltip>
)
}
const parseShareFeedInfo = (href?: string) => {
if (!href) return null
const baseUrl = parseSafeUrl(env.VITE_WEB_URL)
if (!baseUrl) return null
let parsedUrl: URL
try {
parsedUrl = new URL(href, baseUrl)
} catch {
return null
}
if (parsedUrl.host !== baseUrl.host) return null
const pathParts = parsedUrl.pathname.split("/").filter(Boolean)
if (pathParts.length !== 3 || pathParts[0] !== "share" || pathParts[1] !== "feeds") {
return null
}
const viewParam = parsedUrl.searchParams.get("view")
const view = viewParam ? Number.parseInt(viewParam, 10) : undefined
return {
id: pathParts[2]!,
view: Number.isNaN(view) ? undefined : view,
}
}
const resolveShareFeedView = async (info: { id: string; view?: number }) => {
if (typeof info.view === "number") {
return info.view
}
const data = await feedSyncServices.fetchFeedById({ id: info.id }).catch(() => {})
const analyticsView = data?.analytics?.view
if (typeof analyticsView === "number") {
return analyticsView
}
return 0
}