mirror of
https://github.com/outline/outline.git
synced 2026-01-06 11:09:55 -06:00
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:
@@ -81,7 +81,7 @@ function useItems({
|
||||
|
||||
mentionType = integration
|
||||
? determineMentionType({ url, integration })
|
||||
: undefined;
|
||||
: MentionType.URL;
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user