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 {
|
||||
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;
|
||||
|
|
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 {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
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 { 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}
|
||||
/>
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
Loading…
Reference in a new issue