Support url-only link previews in stories

This commit is contained in:
Fedor Indutny 2022-10-06 13:22:59 -07:00 committed by GitHub
parent 89e25fb7e3
commit b950480d36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 92 additions and 37 deletions

View file

@ -52,6 +52,10 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-right: 20px; margin-right: 20px;
&--only-url {
justify-content: center;
}
} }
.module-staged-link-preview__title { .module-staged-link-preview__title {
@include font-body-1-bold; @include font-body-1-bold;

View file

@ -14,6 +14,7 @@ import type { TextAttachmentType } from '../types/Attachment';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { ContextMenu } from './ContextMenu'; import { ContextMenu } from './ContextMenu';
import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview'; import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview';
import type { MaybeGrabLinkPreviewOptionsType } from '../types/LinkPreview';
import { Input } from './Input'; import { Input } from './Input';
import { Slider } from './Slider'; import { Slider } from './Slider';
import { StagedLinkPreview } from './conversation/StagedLinkPreview'; import { StagedLinkPreview } from './conversation/StagedLinkPreview';
@ -31,7 +32,8 @@ import { handleOutsideClick } from '../util/handleOutsideClick';
export type PropsType = { export type PropsType = {
debouncedMaybeGrabLinkPreview: ( debouncedMaybeGrabLinkPreview: (
message: string, message: string,
source: LinkPreviewSourceType source: LinkPreviewSourceType,
options?: MaybeGrabLinkPreviewOptionsType
) => unknown; ) => unknown;
i18n: LocalizerType; i18n: LocalizerType;
linkPreview?: LinkPreviewType; linkPreview?: LinkPreviewType;
@ -178,7 +180,10 @@ export const TextStoryCreator = ({
} }
debouncedMaybeGrabLinkPreview( debouncedMaybeGrabLinkPreview(
linkPreviewInputValue, linkPreviewInputValue,
LinkPreviewSourceType.StoryCreator LinkPreviewSourceType.StoryCreator,
{
mode: 'story',
}
); );
}, [ }, [
debouncedMaybeGrabLinkPreview, debouncedMaybeGrabLinkPreview,
@ -525,12 +530,9 @@ export const TextStoryCreator = ({
{linkPreview ? ( {linkPreview ? (
<> <>
<StagedLinkPreview <StagedLinkPreview
domain={linkPreview.domain} {...linkPreview}
i18n={i18n} i18n={i18n}
image={linkPreview.image}
moduleClassName="StoryCreator__link-preview" moduleClassName="StoryCreator__link-preview"
title={linkPreview.title}
url={linkPreview.url}
/> />
<Button <Button
className="StoryCreator__link-preview-button" className="StoryCreator__link-preview-button"

View file

@ -39,6 +39,38 @@ export const StagedLinkPreview: React.FC<Props> = ({
moduleClassName moduleClassName
); );
let maybeContent: JSX.Element | undefined;
if (isLoaded) {
// No title, no description - display only domain
if (!title && !description) {
maybeContent = (
<div
className={classNames(
getClassName('__content'),
getClassName('__content--only-url')
)}
>
<div className={getClassName('__title')}>{domain}</div>
</div>
);
} else {
maybeContent = (
<div className={getClassName('__content')}>
<div className={getClassName('__title')}>{title}</div>
{description && (
<div className={getClassName('__description')}>
{unescape(description)}
</div>
)}
<div className={getClassName('__footer')}>
<div className={getClassName('__location')}>{domain}</div>
<LinkPreviewDate date={date} className={getClassName('__date')} />
</div>
</div>
);
}
}
return ( return (
<div <div
className={classNames( className={classNames(
@ -68,20 +100,7 @@ export const StagedLinkPreview: React.FC<Props> = ({
</div> </div>
) : null} ) : null}
{isLoaded && !image && <div className={getClassName('__no-image')} />} {isLoaded && !image && <div className={getClassName('__no-image')} />}
{isLoaded ? ( {maybeContent}
<div className={getClassName('__content')}>
<div className={getClassName('__title')}>{title}</div>
{description && (
<div className={getClassName('__description')}>
{unescape(description)}
</div>
)}
<div className={getClassName('__footer')}>
<div className={getClassName('__location')}>{domain}</div>
<LinkPreviewDate date={date} className={getClassName('__date')} />
</div>
</div>
) : null}
{onClose && ( {onClose && (
<button <button
aria-label={i18n('close')} aria-label={i18n('close')}

View file

@ -8,6 +8,8 @@ import type {
LinkPreviewImage, LinkPreviewImage,
LinkPreviewResult, LinkPreviewResult,
LinkPreviewSourceType, LinkPreviewSourceType,
MaybeGrabLinkPreviewOptionsType,
AddLinkPreviewOptionsType,
} from '../types/LinkPreview'; } from '../types/LinkPreview';
import type { StickerPackType as StickerPackDBType } from '../sql/Interface'; import type { StickerPackType as StickerPackDBType } from '../sql/Interface';
import type { MIMEType } from '../types/MIME'; import type { MIMEType } from '../types/MIME';
@ -45,10 +47,11 @@ export const maybeGrabLinkPreview = debounce(_maybeGrabLinkPreview, 200);
function _maybeGrabLinkPreview( function _maybeGrabLinkPreview(
message: string, message: string,
source: LinkPreviewSourceType, source: LinkPreviewSourceType,
caretLocation?: number { caretLocation, mode = 'conversation' }: MaybeGrabLinkPreviewOptionsType = {}
): void { ): void {
// Don't generate link previews if user has turned them off // Don't generate link previews if user has turned them off. When posting a
if (!window.Events.getLinkPreviewSetting()) { // story we should return minimal (url-only) link previews.
if (!window.Events.getLinkPreviewSetting() && mode === 'conversation') {
return; return;
} }
@ -88,7 +91,9 @@ function _maybeGrabLinkPreview(
return; return;
} }
addLinkPreview(link, source); addLinkPreview(link, source, {
disableFetch: !window.Events.getLinkPreviewSetting(),
});
} }
export function resetLinkPreview(): void { export function resetLinkPreview(): void {
@ -113,7 +118,8 @@ export function removeLinkPreview(): void {
export async function addLinkPreview( export async function addLinkPreview(
url: string, url: string,
source: LinkPreviewSourceType source: LinkPreviewSourceType,
{ disableFetch }: AddLinkPreviewOptionsType = {}
): Promise<void> { ): Promise<void> {
if (currentlyMatchedLink === url) { if (currentlyMatchedLink === url) {
log.warn('addLinkPreview should not be called with the same URL like this'); log.warn('addLinkPreview should not be called with the same URL like this');
@ -153,7 +159,17 @@ export async function addLinkPreview(
); );
try { try {
const result = await getPreview(url, thisRequestAbortController.signal); let result: LinkPreviewResult | null;
if (disableFetch) {
result = {
title: null,
url,
description: null,
date: null,
};
} else {
result = await getPreview(url, thisRequestAbortController.signal);
}
if (!result) { if (!result) {
log.info( log.info(
@ -179,7 +195,7 @@ export async function addLinkPreview(
type: result.image.contentType, type: result.image.contentType,
}); });
result.image.url = URL.createObjectURL(blob); result.image.url = URL.createObjectURL(blob);
} else if (!result.title) { } else if (!result.title && !disableFetch) {
// A link preview isn't worth showing unless we have either a title or an image // A link preview isn't worth showing unless we have either a title or an image
removeLinkPreview(); removeLinkPreview();
return; return;
@ -188,6 +204,7 @@ export async function addLinkPreview(
window.reduxActions.linkPreviews.addLinkPreview( window.reduxActions.linkPreviews.addLinkPreview(
{ {
...result, ...result,
title: dropNull(result.title),
description: dropNull(result.description), description: dropNull(result.description),
date: dropNull(result.date), date: dropNull(result.date),
domain: LinkPreview.getDomain(result.url), domain: LinkPreview.getDomain(result.url),
@ -232,6 +249,7 @@ export function getLinkPreviewForSend(message: string): Array<LinkPreviewType> {
return { return {
...item, ...item,
image: omit(item.image, 'url'), image: omit(item.image, 'url'),
title: dropNull(item.title),
description: dropNull(item.description), description: dropNull(item.description),
date: dropNull(item.date), date: dropNull(item.date),
domain: LinkPreview.getDomain(item.url), domain: LinkPreview.getDomain(item.url),
@ -241,6 +259,7 @@ export function getLinkPreviewForSend(message: string): Array<LinkPreviewType> {
return { return {
...item, ...item,
title: dropNull(item.title),
description: dropNull(item.description), description: dropNull(item.description),
date: dropNull(item.date), date: dropNull(item.date),
domain: LinkPreview.getDomain(item.url), domain: LinkPreview.getDomain(item.url),

View file

@ -6,7 +6,10 @@ import type { ThunkAction } from 'redux-thunk';
import type { NoopActionType } from './noop'; import type { NoopActionType } from './noop';
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
import type { LinkPreviewType } from '../../types/message/LinkPreviews'; import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import type { LinkPreviewSourceType } from '../../types/LinkPreview'; import type {
LinkPreviewSourceType,
MaybeGrabLinkPreviewOptionsType,
} from '../../types/LinkPreview';
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
import { maybeGrabLinkPreview } from '../../services/LinkPreview'; import { maybeGrabLinkPreview } from '../../services/LinkPreview';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
@ -43,10 +46,11 @@ type LinkPreviewsActionType =
function debouncedMaybeGrabLinkPreview( function debouncedMaybeGrabLinkPreview(
message: string, message: string,
source: LinkPreviewSourceType source: LinkPreviewSourceType,
options?: MaybeGrabLinkPreviewOptionsType
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return dispatch => { return dispatch => {
maybeGrabLinkPreview(message, source); maybeGrabLinkPreview(message, source, options);
dispatch({ dispatch({
type: 'NOOP', type: 'NOOP',

View file

@ -128,7 +128,7 @@ export function SmartForwardMessageModal(): JSX.Element | null {
maybeGrabLinkPreview( maybeGrabLinkPreview(
messageText, messageText,
LinkPreviewSourceType.ForwardMessageModal, LinkPreviewSourceType.ForwardMessageModal,
caretLocation { caretLocation }
); );
} }
}} }}

View file

@ -15,7 +15,7 @@ export type LinkPreviewImage = AttachmentType & {
}; };
export type LinkPreviewResult = { export type LinkPreviewResult = {
title: string; title: string | null;
url: string; url: string;
image?: LinkPreviewImage; image?: LinkPreviewImage;
description: string | null; description: string | null;
@ -32,6 +32,15 @@ export enum LinkPreviewSourceType {
StoryCreator, StoryCreator,
} }
export type MaybeGrabLinkPreviewOptionsType = Readonly<{
caretLocation?: number;
mode?: 'conversation' | 'story';
}>;
export type AddLinkPreviewOptionsType = Readonly<{
disableFetch?: boolean;
}>;
const linkify = LinkifyIt(); const linkify = LinkifyIt();
export function shouldPreviewHref(href: string): boolean { export function shouldPreviewHref(href: string): boolean {

View file

@ -2575,11 +2575,9 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
// If we have attachments, don't add link preview // If we have attachments, don't add link preview
if (!this.hasFiles({ includePending: true })) { if (!this.hasFiles({ includePending: true })) {
maybeGrabLinkPreview( maybeGrabLinkPreview(messageText, LinkPreviewSourceType.Composer, {
messageText, caretLocation,
LinkPreviewSourceType.Composer, });
caretLocation
);
} }
} }