Link preview design for stories
This commit is contained in:
parent
64fa3aac59
commit
3a6ab6a5aa
8 changed files with 317 additions and 19 deletions
|
@ -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;
|
||||||
|
|
81
stylesheets/components/StoryLinkPreview.scss
Normal file
81
stylesheets/components/StoryLinkPreview.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
116
ts/components/StoryLinkPreview.stories.tsx
Normal file
116
ts/components/StoryLinkPreview.stories.tsx
Normal 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';
|
93
ts/components/StoryLinkPreview.tsx
Normal file
93
ts/components/StoryLinkPreview.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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';
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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">
|
||||||
|
<StoryLinkPreview
|
||||||
{...linkPreview}
|
{...linkPreview}
|
||||||
|
forceCompactMode
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
moduleClassName="StoryCreator__link-preview"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className="StoryCreator__link-preview-button"
|
className="StoryCreator__link-preview-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
Loading…
Reference in a new issue