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 { &__link-preview-input-popper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 256px; min-height: 256px;
padding: 16px; padding: 16px;
width: 360px; width: 360px;
} }
@ -259,6 +259,10 @@
justify-content: center; justify-content: center;
} }
&__link-preview-wrapper {
transform: scale(0.5);
}
&__link-preview-button { &__link-preview-button {
margin-top: 18px; margin-top: 18px;
margin-bottom: 8px; 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 { &__preview-container {
position: relative;
margin-top: 36px;
margin-left: 56px; margin-left: 56px;
margin-right: 56px; margin-right: 56px;
margin-top: 36px;
position: relative;
} }
&__preview { &__preview {
@ -101,10 +101,13 @@
} }
&__remove { &__remove {
align-items: center;
backdrop-filter: blur(26px); backdrop-filter: blur(26px);
background: $color-black-alpha-40; background: $color-black-alpha-40;
border-radius: 100%; border-radius: 100%;
display: flex;
height: 48px; height: 48px;
justify-content: center;
position: absolute; position: absolute;
right: -16px; right: -16px;
top: -16px; top: -16px;
@ -114,9 +117,6 @@
button { button {
@include button-reset; @include button-reset;
height: 24px; height: 24px;
position: absolute;
right: 12px;
top: 12px;
width: 24px; width: 24px;
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-15); @include color-svg('../images/icons/v2/x-24.svg', $color-gray-15);
} }

View file

@ -114,6 +114,7 @@
@import './components/StoryCreator.scss'; @import './components/StoryCreator.scss';
@import './components/StoryDetailsModal.scss'; @import './components/StoryDetailsModal.scss';
@import './components/StoryImage.scss'; @import './components/StoryImage.scss';
@import './components/StoryLinkPreview.scss';
@import './components/StoryListItem.scss'; @import './components/StoryListItem.scss';
@import './components/StoryReplyQuote.scss'; @import './components/StoryReplyQuote.scss';
@import './components/StoriesSettingsModal.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 type { TextAttachmentType } from '../types/Attachment';
import { AddNewLines } from './conversation/AddNewLines'; import { AddNewLines } from './conversation/AddNewLines';
import { Emojify } from './conversation/Emojify'; import { Emojify } from './conversation/Emojify';
import { StagedLinkPreview } from './conversation/StagedLinkPreview'; import { StoryLinkPreview } from './StoryLinkPreview';
import { TextAttachmentStyleType } from '../types/Attachment'; import { TextAttachmentStyleType } from '../types/Attachment';
import { count } from '../util/grapheme'; import { count } from '../util/grapheme';
import { getDomain } from '../types/LinkPreview'; import { getDomain } from '../types/LinkPreview';
@ -160,8 +160,8 @@ export const TextAttachment = ({
ref={measureRef} ref={measureRef}
style={isThumbnail ? storyBackgroundColor : undefined} 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 the story, but it must be positioned using the scaled offset
*/} */}
{textAttachment.preview && {textAttachment.preview &&
@ -276,12 +276,13 @@ export const TextAttachment = ({
/> />
</div> </div>
)} )}
<StagedLinkPreview <StoryLinkPreview
{...textAttachment.preview}
domain={getDomain(String(textAttachment.preview.url))} domain={getDomain(String(textAttachment.preview.url))}
forceCompactMode={
getTextSize(textContent) !== TextSize.Large
}
i18n={i18n} i18n={i18n}
image={textAttachment.preview.image}
imageSize={textAttachment.preview.title ? 144 : 72}
moduleClassName="TextAttachment__preview"
title={textAttachment.preview.title || undefined} title={textAttachment.preview.title || undefined}
url={textAttachment.preview.url} url={textAttachment.preview.url}
/> />

View file

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