Skip to content

Commit

Permalink
Link preview design for stories
Browse files Browse the repository at this point in the history
  • Loading branch information
josh-signal committed Nov 2, 2022
1 parent 64fa3aa commit 3a6ab6a
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 19 deletions.
6 changes: 5 additions & 1 deletion stylesheets/components/StoryCreator.scss
Expand Up @@ -242,7 +242,7 @@
&__link-preview-input-popper {
display: flex;
flex-direction: column;
height: 256px;
min-height: 256px;
padding: 16px;
width: 360px;
}
Expand All @@ -259,6 +259,10 @@
justify-content: center;
}

&__link-preview-wrapper {
transform: scale(0.5);
}

&__link-preview-button {
margin-top: 18px;
margin-bottom: 8px;
Expand Down
81 changes: 81 additions & 0 deletions stylesheets/components/StoryLinkPreview.scss
@@ -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;
}
}
}
10 changes: 5 additions & 5 deletions stylesheets/components/TextAttachment.scss
Expand Up @@ -61,10 +61,10 @@
}

&__preview-container {
position: relative;
margin-top: 36px;
margin-left: 56px;
margin-right: 56px;
margin-top: 36px;
position: relative;
}

&__preview {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions stylesheets/manifest.scss
Expand Up @@ -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';
Expand Down
116 changes: 116 additions & 0 deletions ts/components/StoryLinkPreview.stories.tsx
@@ -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 changes: 93 additions & 0 deletions ts/components/StoryLinkPreview.tsx
@@ -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>
);
};
15 changes: 8 additions & 7 deletions ts/components/TextAttachment.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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}
/>
Expand Down

0 comments on commit 3a6ab6a

Please sign in to comment.