Support url-only link previews in stories
This commit is contained in:
parent
89e25fb7e3
commit
b950480d36
8 changed files with 92 additions and 37 deletions
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -128,7 +128,7 @@ export function SmartForwardMessageModal(): JSX.Element | null {
|
||||||
maybeGrabLinkPreview(
|
maybeGrabLinkPreview(
|
||||||
messageText,
|
messageText,
|
||||||
LinkPreviewSourceType.ForwardMessageModal,
|
LinkPreviewSourceType.ForwardMessageModal,
|
||||||
caretLocation
|
{ caretLocation }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue