From 908d0408f58a4240da794e6d9215d231a90c791c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 15 Oct 2025 02:37:44 +0200 Subject: [PATCH] fix: Display fallback instead of error if cannot unfurl URL (#10370) * fix: Display fallback instead of error if cannot unfurl URL * Optimised images with calibre/image-actions * fix: Write loaded to props to attrs * Optimised images with calibre/image-actions * white background * Optimised images with calibre/image-actions --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/images/link.png | Bin 0 -> 377 bytes shared/editor/components/Mentions.tsx | 47 ++++++++++++++++++++------ shared/editor/nodes/Mention.tsx | 10 ++++-- shared/utils/urls.ts | 15 ++++++++ 4 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 public/images/link.png diff --git a/public/images/link.png b/public/images/link.png new file mode 100644 index 0000000000000000000000000000000000000000..679c89f9c5ff2e1cec3775f7292a371fddbeaf02 GIT binary patch literal 377 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaA1qS$pxcX!k{)d6TfB(XmSZHm1 z(@&p1ZP>UuAvwd&(f#SuXXnmem^ytHP-$aJ`{E_bgTta%uU-Fly4@9^;d~`Qe!&c- z4hfIXzZclAFnhg0z*&(ayMZ#C1s;*b3=G`DAk4@xYYxa^TRdGHLn>}`;{5ic@UN8EwEe~f8@O83 z`lmg8`smP!x(6GIwWcoW%w5m7Zz)d)_eER9+b<4SH_M+5ymoSaT=)BVdH-9Z%b$1u zdKdlUXVk9m7Vl4nBSEh2E&(PiZ1?YYTPgg&ebxsLQ00#lUga7~l literal 0 HcmV?d00001 diff --git a/shared/editor/components/Mentions.tsx b/shared/editor/components/Mentions.tsx index 79ada79619..bced78a7ac 100644 --- a/shared/editor/components/Mentions.tsx +++ b/shared/editor/components/Mentions.tsx @@ -22,13 +22,13 @@ import useStores from "../../hooks/useStores"; import theme from "../../styles/theme"; import { IntegrationService, + UnfurlResourceType, type JSONValue, - type UnfurlResourceType, type UnfurlResponse, } from "../../types"; import { cn } from "../styles/utils"; import { ComponentProps } from "../types"; -import { sanitizeUrl } from "@shared/utils/urls"; +import { toDisplayUrl, cdnPath } from "../../utils/urls"; type Attrs = { className: string; @@ -144,10 +144,15 @@ type IssuePrProps = ComponentProps & { ) => void; }; -export const MentionURL = (props: ComponentProps) => { +type IssueUrlProps = ComponentProps & { + onChangeUnfurl: (unfurl: UnfurlResponse[UnfurlResourceType.URL]) => void; +}; + +export const MentionURL = (props: IssueUrlProps) => { const { unfurls } = useStores(); const isMounted = useIsMounted(); const [loaded, setLoaded] = React.useState(false); + const onChangeUnfurl = React.useRef(props.onChangeUnfurl).current; // stable reference to callback function. const { isSelected, node } = props; const { @@ -156,17 +161,39 @@ export const MentionURL = (props: ComponentProps) => { ...attrs } = getAttributesFromNode(node); + const url = String(attrs.href); const unfurl = unfurls.get(attrs.href)?.data ?? unfurlAttr; React.useEffect(() => { const fetchUnfurl = async () => { - await unfurls.fetchUnfurl({ url: attrs.href }); + try { + const unfurlModel = await unfurls.fetchUnfurl({ url }); - if (!isMounted()) { - return; + if (!isMounted()) { + return; + } + + if (unfurlModel) { + onChangeUnfurl( + unfurlModel.data satisfies UnfurlResponse[UnfurlResourceType.URL] + ); + } else { + // If we didn't get a result back, we still want to add a basic unfurl + // to avoid refetching again in future. This will just show the URL + // with a generic link icon. + unfurls.add({ + id: url, + type: UnfurlResourceType.URL, + fetchedAt: new Date().toISOString(), + data: { + title: toDisplayUrl(url), + faviconUrl: cdnPath("/images/link.png"), + }, + }); + } + } finally { + setLoaded(true); } - - setLoaded(true); }; void fetchUnfurl(); @@ -191,9 +218,7 @@ export const MentionURL = (props: ComponentProps) => { rel="noopener noreferrer nofollow" > - {unfurl.faviconUrl ? ( - - ) : null} + {unfurl.faviconUrl ? : null} diff --git a/shared/editor/nodes/Mention.tsx b/shared/editor/nodes/Mention.tsx index bc75d66884..4028ad9a73 100644 --- a/shared/editor/nodes/Mention.tsx +++ b/shared/editor/nodes/Mention.tsx @@ -145,7 +145,12 @@ export default class Mention extends Node { /> ); case MentionType.URL: - return ; + return ( + + ); default: return null; } @@ -323,7 +328,8 @@ export default class Mention extends Node { const label = unfurl.type === UnfurlResourceType.Issue || - unfurl.type === UnfurlResourceType.PR + unfurl.type === UnfurlResourceType.PR || + unfurl.type === UnfurlResourceType.URL ? unfurl.title : undefined; diff --git a/shared/utils/urls.ts b/shared/utils/urls.ts index 9e384c42cd..a822db396a 100644 --- a/shared/utils/urls.ts +++ b/shared/utils/urls.ts @@ -230,3 +230,18 @@ export function urlRegex(url: string | null | undefined): RegExp | undefined { export function getUrls(text: string) { return Array.from(text.match(/(?:https?):\/\/[^\s]+/gi) || []); } + +/** + * Converts a url to a display friendly format, removing the protocol and trailing slash. + * + * @param url The url to convert. + * @returns The display friendly url. + */ +export function toDisplayUrl(url: string) { + try { + const parsed = new URL(url); + return parsed.host + (parsed.pathname === "/" ? "" : parsed.pathname); + } catch { + return url; + } +}