diff --git a/app/editor/components/PasteMenu.tsx b/app/editor/components/PasteMenu.tsx index 6ce9004ac8..fd1a9eeda6 100644 --- a/app/editor/components/PasteMenu.tsx +++ b/app/editor/components/PasteMenu.tsx @@ -81,7 +81,7 @@ function useItems({ mentionType = integration ? determineMentionType({ url, integration }) - : undefined; + : MentionType.URL; } return [ diff --git a/plugins/iframely/server/iframely.ts b/plugins/iframely/server/iframely.ts index 6ca473c6e7..5a2af3f994 100644 --- a/plugins/iframely/server/iframely.ts +++ b/plugins/iframely/server/iframely.ts @@ -9,7 +9,7 @@ class Iframely { public static async requestResource( url: string, - type = "oembed" + type = "iframely" ): Promise { 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 }; }; } diff --git a/server/presenters/unfurl.ts b/server/presenters/unfurl.ts index b6cc199b02..d39f1a3eab 100644 --- a/server/presenters/unfurl.ts +++ b/server/presenters/unfurl.ts @@ -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 -): 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 ( diff --git a/server/routes/api/urls/urls.test.ts b/server/routes/api/urls/urls.test.ts index 17a6a94d2e..cf4b074e32 100644 --- a/server/routes/api/urls/urls.test.ts +++ b/server/routes/api/urls/urls.test.ts @@ -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" ); }); diff --git a/shared/editor/components/Mentions.tsx b/shared/editor/components/Mentions.tsx index 1f9898ee4c..79ada79619 100644 --- a/shared/editor/components/Mentions.tsx +++ b/shared/editor/components/Mentions.tsx @@ -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 ? ( + + ) : ( + + ); + } + + return ( + + + {unfurl.faviconUrl ? ( + + ) : null} + + + + + + ); +}; + 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; +`; diff --git a/shared/editor/nodes/Mention.tsx b/shared/editor/nodes/Mention.tsx index ce0b2518dc..ae69768fc8 100644 --- a/shared/editor/nodes/Mention.tsx +++ b/shared/editor/nodes/Mention.tsx @@ -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 ; default: return null; } diff --git a/shared/types.ts b/shared/types.ts index 8b0de99203..72a1294dfc 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -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 = }; 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 */