Mention chip for regular URLs (#10327)

* fix: replace oembed with iframely

* feat: wip

fix: favicon

* fix: missing icon in API response
This commit is contained in:
Apoorv Mishra
2025-10-11 05:10:05 +05:30
committed by GitHub
parent ee7738c141
commit 95f0c42d56
7 changed files with 112 additions and 20 deletions

View File

@@ -81,7 +81,7 @@ function useItems({
mentionType = integration
? determineMentionType({ url, integration })
: undefined;
: MentionType.URL;
}
return [

View File

@@ -9,7 +9,7 @@ class Iframely {
public static async requestResource(
url: string,
type = "oembed"
type = "iframely"
): Promise<JSONObject | UnfurlError> {
const isDefaultHost = env.IFRAMELY_URL === this.defaultUrl;
@@ -38,7 +38,7 @@ class Iframely {
const data = await Iframely.requestResource(url);
return "error" in data // In addition to our custom UnfurlError, sometimes iframely returns error in the response body.
? ({ error: data.error } as UnfurlError)
: { ...data, type: UnfurlResourceType.OEmbed };
: { ...data, type: UnfurlResourceType.URL };
};
}

View File

@@ -19,18 +19,19 @@ async function presentUnfurl(
case UnfurlResourceType.Issue:
return presentIssue(data);
default:
return presentOEmbed(data);
return presentURL(data);
}
}
const presentOEmbed = (
const presentURL = (
data: Record<string, any>
): UnfurlResponse[UnfurlResourceType.OEmbed] => ({
type: UnfurlResourceType.OEmbed,
): UnfurlResponse[UnfurlResourceType.URL] => ({
type: UnfurlResourceType.URL,
url: data.url,
title: data.title,
description: data.description,
thumbnailUrl: data.thumbnail_url,
title: data.meta.title,
description: data.meta.description,
thumbnailUrl: (data.links.thumbnail ?? [])[0]?.href ?? "",
faviconUrl: (data.links.icon ?? [])[0]?.href ?? "",
});
const presentMention = async (

View File

@@ -162,11 +162,32 @@ describe("#urls.unfurl", () => {
Promise.resolve({
url: "https://www.flickr.com",
type: "rich",
title: "Flickr",
description:
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!",
thumbnail_url:
"https://farm4.staticflickr.com/3914/15118079089_489aa62638_b.jpg",
meta: {
title: "Flickr",
description:
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!",
},
links: {
thumbnail: [
{
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg",
type: "image/jpg",
rel: ["twitter", "thumbnail", "ssl", "og"],
content_length: 412824,
media: {
width: 1200,
height: 630,
},
},
],
icon: [
{
href: "https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/67167dd041b0982f0f230dab_flickr-webclip.png",
rel: ["apple-touch-icon", "icon", "ssl"],
type: "image/png",
},
],
},
})
);
@@ -182,13 +203,13 @@ describe("#urls.unfurl", () => {
expect(res.status).toEqual(200);
expect(body.url).toEqual("https://www.flickr.com");
expect(body.type).toEqual(UnfurlResourceType.OEmbed);
expect(body.type).toEqual(UnfurlResourceType.URL);
expect(body.title).toEqual("Flickr");
expect(body.description).toEqual(
"The safest and most inclusive global community of photography enthusiasts. The best place for inspiration, connection, and sharing!"
);
expect(body.thumbnailUrl).toEqual(
"https://farm4.staticflickr.com/3914/15118079089_489aa62638_b.jpg"
"https://combo.staticflickr.com/66a031f9fc343c5e42d965ca/671aaf5d51c929e483e8b26d_Open%20Graph%20Home.jpg"
);
});

View File

@@ -28,6 +28,7 @@ import {
} from "../../types";
import { cn } from "../styles/utils";
import { ComponentProps } from "../types";
import { sanitizeUrl } from "@shared/utils/urls";
type Attrs = {
className: string;
@@ -143,6 +144,64 @@ type IssuePrProps = ComponentProps & {
) => void;
};
export const MentionURL = (props: ComponentProps) => {
const { unfurls } = useStores();
const isMounted = useIsMounted();
const [loaded, setLoaded] = React.useState(false);
const { isSelected, node } = props;
const {
className,
unfurl: unfurlAttr,
...attrs
} = getAttributesFromNode(node);
const unfurl = unfurls.get(attrs.href)?.data ?? unfurlAttr;
React.useEffect(() => {
const fetchUnfurl = async () => {
await unfurls.fetchUnfurl({ url: attrs.href });
if (!isMounted()) {
return;
}
setLoaded(true);
};
void fetchUnfurl();
}, [unfurls, attrs.href, isMounted]);
if (!unfurl) {
return !loaded ? (
<MentionLoading className={className} />
) : (
<MentionError className={className} />
);
}
return (
<a
{...attrs}
className={cn(className, {
"ProseMirror-selectednode": isSelected,
})}
href={attrs.href as string}
target="_blank"
rel="noopener noreferrer nofollow"
>
<Flex align="center" gap={6}>
{unfurl.faviconUrl ? (
<Logo src={sanitizeUrl(unfurl.faviconUrl)} alt="" />
) : null}
<Text>
<Backticks content={unfurl.title} />
</Text>
</Flex>
</a>
);
};
export const MentionIssue = observer((props: IssuePrProps) => {
const { unfurls } = useStores();
const isMounted = useIsMounted();
@@ -316,3 +375,8 @@ const MentionError = ({ className }: { className: string }) => {
const StyledWarningIcon = styled(WarningIcon)`
margin: 0 -2px;
`;
const Logo = styled.img`
width: 16px;
height: 16px;
`;

View File

@@ -22,6 +22,7 @@ import {
MentionDocument,
MentionIssue,
MentionPullRequest,
MentionURL,
MentionUser,
} from "../components/Mentions";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
@@ -145,6 +146,8 @@ export default class Mention extends Node {
onChangeUnfurl={this.handleChangeUnfurl(props)}
/>
);
case MentionType.URL:
return <MentionURL {...props} />;
default:
return null;
}

View File

@@ -85,6 +85,7 @@ export enum MentionType {
Collection = "collection",
Issue = "issue",
PullRequest = "pull_request",
URL = "url",
}
export type PublicEnv = {
@@ -404,7 +405,7 @@ export const NotificationEventDefaults: Record<NotificationEventType, boolean> =
};
export enum UnfurlResourceType {
OEmbed = "oembed",
URL = "url",
Mention = "mention",
Document = "document",
Issue = "issue",
@@ -412,9 +413,9 @@ export enum UnfurlResourceType {
}
export type UnfurlResponse = {
[UnfurlResourceType.OEmbed]: {
[UnfurlResourceType.URL]: {
/** The resource type */
type: UnfurlResourceType.OEmbed;
type: UnfurlResourceType.URL;
/** URL pointing to the resource */
url: string;
/** A text title, describing the resource */
@@ -423,6 +424,8 @@ export type UnfurlResponse = {
description: string;
/** A URL to a thumbnail image representing the resource */
thumbnailUrl: string;
/** A URL to a favicon representing the resource */
faviconUrl: string;
};
[UnfurlResourceType.Mention]: {
/** The resource type */