Link preview design for stories

This commit is contained in:
Josh Perez 2022-11-02 17:04:50 -04:00 committed by GitHub
parent 64fa3aac59
commit 3a6ab6a5aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 317 additions and 19 deletions

View file

@ -242,7 +242,7 @@
&__link-preview-input-popper {
display: flex;
flex-direction: column;
height: 256px;
min-height: 256px;
padding: 16px;
width: 360px;
}
@ -259,6 +259,10 @@
justify-content: center;
}
&__link-preview-wrapper {
transform: scale(0.5);
}
&__link-preview-button {
margin-top: 18px;
margin-bottom: 8px;

View file

@ -0,0 +1,81 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.StoryLinkPreview {
align-items: center;
background-color: $color-white;
border-radius: 36px;
color: $color-gray-90;
display: inline-flex;
max-width: 560px;
min-width: 560px;
overflow: hidden;
&__content {
margin-left: 24px;
margin-right: 24px;
padding: 16px 0;
}
&__title {
@include font-body-1-bold;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
display: -webkit-box;
overflow: hidden;
font-size: 28px;
line-height: 40px;
letter-spacing: -0.16px;
}
&__description {
@include font-body-2;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
display: -webkit-box;
overflow: hidden;
font-size: 26px;
line-height: 36px;
letter-spacing: -0.06px;
}
&__location {
@include font-caption;
color: $color-gray-45;
font-size: 22px;
line-height: 28px;
letter-spacing: 0.12px;
}
&__no-image {
align-items: center;
display: flex;
height: 176px;
justify-content: center;
margin-left: 52px;
margin-right: 52px;
&::before {
@include color-svg('../images/icons/v2/link-24.svg', $color-gray-90);
content: '';
display: block;
height: 48px;
width: 48px;
}
}
&--tall {
flex-direction: column;
}
&--tiny {
min-width: inherit;
.StoryLinkPreview__no-image {
height: 100px;
margin-left: 24px;
margin-right: 0;
width: auto;
}
}
}

View file

@ -61,10 +61,10 @@
}
&__preview-container {
position: relative;
margin-top: 36px;
margin-left: 56px;
margin-right: 56px;
margin-top: 36px;
position: relative;
}
&__preview {
@ -101,10 +101,13 @@
}
&__remove {
align-items: center;
backdrop-filter: blur(26px);
background: $color-black-alpha-40;
border-radius: 100%;
display: flex;
height: 48px;
justify-content: center;
position: absolute;
right: -16px;
top: -16px;
@ -114,9 +117,6 @@
button {
@include button-reset;
height: 24px;
position: absolute;
right: 12px;
top: 12px;
width: 24px;
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-15);
}

View file

@ -114,6 +114,7 @@
@import './components/StoryCreator.scss';
@import './components/StoryDetailsModal.scss';
@import './components/StoryImage.scss';
@import './components/StoryLinkPreview.scss';
@import './components/StoryListItem.scss';
@import './components/StoryReplyQuote.scss';
@import './components/StoriesSettingsModal.scss';

View file

@ -0,0 +1,116 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta, Story } from '@storybook/react';
import React from 'react';
import type { Props } from './StoryLinkPreview';
import enMessages from '../../_locales/en/messages.json';
import { StoryLinkPreview } from './StoryLinkPreview';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
import { setupI18n } from '../util/setupI18n';
import { IMAGE_JPEG } from '../types/MIME';
const LONG_TITLE =
"This is a super-sweet site. And it's got some really amazing content in store for you if you just click that link. Can you click that link for me?";
const LONG_DESCRIPTION =
"You're gonna love this description. Not only does it have a lot of characters, but it will also be truncated in the UI. How cool is that??";
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/StoryLinkPreview',
component: StoryLinkPreview,
argTypes: {
description: {
defaultValue:
'Introducing Mac Studio. Stunningly compact. Endless connectivity. And astonishing performance with M1 Max or the new M1 Ultra chip.',
},
forceCompactMode: {
defaultValue: false,
},
i18n: {
defaultValue: i18n,
},
image: {
defaultValue: fakeAttachment({
// url: 'https://www.apple.com/v/mac-studio/c/images/meta/mac-studio_overview__eedzbosm1t26_og.png',
url: '/fixtures/kitten-4-112-112.jpg',
contentType: IMAGE_JPEG,
}),
},
title: {
defaultValue: 'Mac Studio',
},
url: {
defaultValue: 'https://www.apple.com/mac-studio/',
},
},
} as Meta;
const Template: Story<Props> = args => <StoryLinkPreview {...args} />;
export const Default = Template.bind({});
export const CompactMode = Template.bind({});
CompactMode.args = {
forceCompactMode: true,
};
export const NoImage = Template.bind({});
NoImage.args = {
image: undefined,
};
export const ImageNoDescription = Template.bind({});
ImageNoDescription.args = {
description: '',
};
ImageNoDescription.storyName = 'Image, No Description';
export const ImageNoTitleOrDescription = Template.bind({});
ImageNoTitleOrDescription.args = {
title: '',
description: '',
};
ImageNoTitleOrDescription.storyName = 'Image, No Title Or Description';
export const NoImageNoTitleOrDescription = Template.bind({});
NoImageNoTitleOrDescription.args = {
image: undefined,
title: '',
description: '',
};
NoImageNoTitleOrDescription.storyName = 'Just URL';
export const NoImageLongTitleWithDescription = Template.bind({});
NoImageLongTitleWithDescription.args = {
image: undefined,
title: LONG_TITLE,
};
NoImageLongTitleWithDescription.storyName =
'No Image, Long Title With Description';
export const NoImageLongTitleWithoutDescription = Template.bind({});
NoImageLongTitleWithoutDescription.args = {
image: undefined,
title: LONG_TITLE,
description: '',
};
NoImageLongTitleWithoutDescription.storyName =
'No Image, Long Title Without Description';
export const ImageLongTitleWithoutDescription = Template.bind({});
ImageLongTitleWithoutDescription.args = {
description: '',
title: LONG_TITLE,
};
ImageLongTitleWithoutDescription.storyName =
'Image, Long Title Without Description';
export const ImageLongTitleAndDescription = Template.bind({});
ImageLongTitleAndDescription.args = {
title: LONG_TITLE,
description: LONG_DESCRIPTION,
};
ImageLongTitleAndDescription.storyName = 'Image, Long Title And Description';

View file

@ -0,0 +1,93 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import classNames from 'classnames';
import { unescape } from 'lodash';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import type { LocalizerType } from '../types/Util';
import { CurveType, Image } from './conversation/Image';
import { isImageAttachment } from '../types/Attachment';
import { getDomain } from '../types/LinkPreview';
export type Props = LinkPreviewType & {
forceCompactMode?: boolean;
i18n: LocalizerType;
};
export const StoryLinkPreview = ({
description,
domain,
forceCompactMode,
i18n,
image,
title,
url,
}: Props): JSX.Element => {
const isImage = isImageAttachment(image);
const location = domain || getDomain(String(url));
const isCompact = forceCompactMode || !image;
let content: JSX.Element | undefined;
if (!title && !description) {
content = (
<div
className={classNames(
'StoryLinkPreview__content',
'StoryLinkPreview__content--only-url'
)}
>
<div className="StoryLinkPreview__title">{location}</div>
</div>
);
} else {
content = (
<div className="StoryLinkPreview__content">
<div className="StoryLinkPreview__title">{title}</div>
{description && (
<div className="StoryLinkPreview__description">
{unescape(description)}
</div>
)}
<div className="StoryLinkPreview__footer">
<div className="StoryLinkPreview__location">{location}</div>
</div>
</div>
);
}
const imageWidth = isCompact ? 176 : 560;
const imageHeight =
!isCompact && image
? imageWidth / ((image.width || 1) / (image.height || 1))
: 176;
return (
<div
className={classNames('StoryLinkPreview', {
'StoryLinkPreview--tall': !isCompact,
'StoryLinkPreview--tiny': !title && !description && !image,
})}
>
{isImage && image ? (
<div className="StoryLinkPreview__icon-container">
<Image
alt={i18n('stagedPreviewThumbnail', [location])}
attachment={image}
curveBottomLeft={CurveType.Tiny}
curveBottomRight={CurveType.Tiny}
curveTopLeft={CurveType.Tiny}
curveTopRight={CurveType.Tiny}
height={imageHeight}
i18n={i18n}
url={image.url}
width={imageWidth}
/>
</div>
) : null}
{!isImage && <div className="StoryLinkPreview__no-image" />}
{content}
</div>
);
};

View file

@ -10,7 +10,7 @@ import type { LocalizerType, RenderTextCallbackType } from '../types/Util';
import type { TextAttachmentType } from '../types/Attachment';
import { AddNewLines } from './conversation/AddNewLines';
import { Emojify } from './conversation/Emojify';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { StoryLinkPreview } from './StoryLinkPreview';
import { TextAttachmentStyleType } from '../types/Attachment';
import { count } from '../util/grapheme';
import { getDomain } from '../types/LinkPreview';
@ -160,8 +160,8 @@ export const TextAttachment = ({
ref={measureRef}
style={isThumbnail ? storyBackgroundColor : undefined}
>
{/*
The tooltip must be outside of the scaled area, as it should not scale with
{/*
The tooltip must be outside of the scaled area, as it should not scale with
the story, but it must be positioned using the scaled offset
*/}
{textAttachment.preview &&
@ -276,12 +276,13 @@ export const TextAttachment = ({
/>
</div>
)}
<StagedLinkPreview
<StoryLinkPreview
{...textAttachment.preview}
domain={getDomain(String(textAttachment.preview.url))}
forceCompactMode={
getTextSize(textContent) !== TextSize.Large
}
i18n={i18n}
image={textAttachment.preview.image}
imageSize={textAttachment.preview.title ? 144 : 72}
moduleClassName="TextAttachment__preview"
title={textAttachment.preview.title || undefined}
url={textAttachment.preview.url}
/>

View file

@ -17,7 +17,7 @@ import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview';
import type { MaybeGrabLinkPreviewOptionsType } from '../types/LinkPreview';
import { Input } from './Input';
import { Slider } from './Slider';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { StoryLinkPreview } from './StoryLinkPreview';
import { TextAttachment } from './TextAttachment';
import { Theme, themeClassName } from '../util/theme';
import { getRGBA, getRGBANumber } from '../mediaEditor/util/color';
@ -541,11 +541,13 @@ export const TextStoryCreator = ({
<div className="StoryCreator__link-preview-container">
{linkPreview ? (
<>
<StagedLinkPreview
{...linkPreview}
i18n={i18n}
moduleClassName="StoryCreator__link-preview"
/>
<div className="StoryCreator__link-preview-wrapper">
<StoryLinkPreview
{...linkPreview}
forceCompactMode
i18n={i18n}
/>
</div>
<Button
className="StoryCreator__link-preview-button"
onClick={() => {