Fix collapsed corners for link previews and image attachments

This commit is contained in:
Scott Nonnenberg 2022-04-27 16:03:50 -07:00 committed by GitHub
parent 65dc9d6afb
commit 9d3498d938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 260 additions and 181 deletions

View File

@ -779,7 +779,7 @@
width: calc(100% + 24px);
outline: none;
margin-top: -10px;
margin-top: -8px;
margin-bottom: 5px;
overflow: hidden;
@ -2492,30 +2492,10 @@ button.ConversationDetails__action-button {
left: 6px;
}
.module-image--soft-corners {
border-radius: 4px;
}
.module-image--cropped {
overflow: hidden;
}
.module-image--curved-top-left {
border-top-left-radius: 18px;
}
.module-image--curved-top-right {
border-top-right-radius: 18px;
}
.module-image--curved-bottom-left {
border-bottom-left-radius: 18px;
}
.module-image--curved-bottom-right {
border-bottom-right-radius: 18px;
}
.module-image--small-curved-top-left {
border-top-left-radius: 10px;
}
.module-image__border-overlay {
@include button-reset;

View File

@ -3,7 +3,7 @@
import React from 'react';
import { Image } from './Image';
import { CurveType, Image } from './Image';
import { StagedGenericAttachment } from './StagedGenericAttachment';
import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
import type { LocalizerType } from '../../types/Util';
@ -109,7 +109,10 @@ export const AttachmentList = <T extends AttachmentType | AttachmentDraftType>({
i18n={i18n}
attachment={attachment}
isDownloaded={isDownloaded}
softCorners
curveBottomLeft={CurveType.Tiny}
curveBottomRight={CurveType.Tiny}
curveTopLeft={CurveType.Tiny}
curveTopRight={CurveType.Tiny}
playIconOverlay={isVideo}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}

View File

@ -9,7 +9,7 @@ import { storiesOf } from '@storybook/react';
import { pngUrl } from '../../storybook/Fixtures';
import type { Props } from './Image';
import { Image } from './Image';
import { CurveType, Image } from './Image';
import { IMAGE_PNG } from '../../types/MIME';
import type { ThemeType } from '../../types/Util';
import { setupI18n } from '../../util/setupI18n';
@ -34,16 +34,22 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
blurHash: text('blurHash', overrideProps.blurHash || ''),
bottomOverlay: boolean('bottomOverlay', overrideProps.bottomOverlay || false),
closeButton: boolean('closeButton', overrideProps.closeButton || false),
curveBottomLeft: boolean(
curveBottomLeft: number(
'curveBottomLeft',
overrideProps.curveBottomLeft || false
overrideProps.curveBottomLeft || CurveType.None
),
curveBottomRight: boolean(
curveBottomRight: number(
'curveBottomRight',
overrideProps.curveBottomRight || false
overrideProps.curveBottomRight || CurveType.None
),
curveTopLeft: number(
'curveTopLeft',
overrideProps.curveTopLeft || CurveType.None
),
curveTopRight: number(
'curveTopRight',
overrideProps.curveTopRight || CurveType.None
),
curveTopLeft: boolean('curveTopLeft', overrideProps.curveTopLeft || false),
curveTopRight: boolean('curveTopRight', overrideProps.curveTopRight || false),
darkOverlay: boolean('darkOverlay', overrideProps.darkOverlay || false),
height: number('height', overrideProps.height || 100),
i18n,
@ -57,11 +63,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
'playIconOverlay',
overrideProps.playIconOverlay || false
),
smallCurveTopLeft: boolean(
'smallCurveTopLeft',
overrideProps.smallCurveTopLeft || false
),
softCorners: boolean('softCorners', overrideProps.softCorners || false),
tabIndex: number('tabIndex', overrideProps.tabIndex || 0),
theme: text('theme', overrideProps.theme || 'light') as ThemeType,
url: text('url', 'url' in overrideProps ? overrideProps.url || null : pngUrl),
@ -145,10 +146,10 @@ story.add('Pending w/blurhash', () => {
story.add('Curved Corners', () => {
const props = createProps({
curveBottomLeft: true,
curveBottomRight: true,
curveTopLeft: true,
curveTopRight: true,
curveBottomLeft: CurveType.Normal,
curveBottomRight: CurveType.Normal,
curveTopLeft: CurveType.Normal,
curveTopRight: CurveType.Normal,
});
return <Image {...props} />;
@ -156,7 +157,7 @@ story.add('Curved Corners', () => {
story.add('Small Curve Top Left', () => {
const props = createProps({
smallCurveTopLeft: true,
curveTopLeft: CurveType.Small,
});
return <Image {...props} />;
@ -164,7 +165,10 @@ story.add('Small Curve Top Left', () => {
story.add('Soft Corners', () => {
const props = createProps({
softCorners: true,
curveBottomLeft: CurveType.Tiny,
curveBottomRight: CurveType.Tiny,
curveTopLeft: CurveType.Tiny,
curveTopRight: CurveType.Tiny,
});
return <Image {...props} />;

View File

@ -13,6 +13,13 @@ import {
defaultBlurHash,
} from '../../types/Attachment';
export enum CurveType {
None = 0,
Tiny = 4,
Small = 10,
Normal = 18,
}
export type Props = {
alt: string;
attachment: AttachmentType;
@ -32,16 +39,13 @@ export type Props = {
noBackground?: boolean;
bottomOverlay?: boolean;
closeButton?: boolean;
curveBottomLeft?: boolean;
curveBottomRight?: boolean;
curveTopLeft?: boolean;
curveTopRight?: boolean;
smallCurveTopLeft?: boolean;
curveBottomLeft?: CurveType;
curveBottomRight?: CurveType;
curveTopLeft?: CurveType;
curveTopRight?: CurveType;
darkOverlay?: boolean;
playIconOverlay?: boolean;
softCorners?: boolean;
blurHash?: string;
i18n: LocalizerType;
@ -158,8 +162,6 @@ export class Image extends React.Component<Props> {
onError,
overlayText,
playIconOverlay,
smallCurveTopLeft,
softCorners,
tabIndex,
theme,
url,
@ -176,25 +178,25 @@ export class Image extends React.Component<Props> {
const resolvedBlurHash = blurHash || defaultBlurHash(theme);
const overlayClassName = classNames('module-image__border-overlay', {
'module-image__border-overlay--with-border': !noBorder,
'module-image__border-overlay--with-click-handler': canClick,
'module-image--curved-top-left': curveTopLeft,
'module-image--curved-top-right': curveTopRight,
'module-image--curved-bottom-left': curveBottomLeft,
'module-image--curved-bottom-right': curveBottomRight,
'module-image--small-curved-top-left': smallCurveTopLeft,
'module-image--soft-corners': softCorners,
'module-image__border-overlay--dark': darkOverlay,
'module-image--not-downloaded': imgNotDownloaded,
});
const curveStyles = {
borderTopLeftRadius: curveTopLeft || CurveType.None,
borderTopRightRadius: curveTopRight || CurveType.None,
borderBottomLeftRadius: curveBottomLeft || CurveType.None,
borderBottomRightRadius: curveBottomRight || CurveType.None,
};
const overlay = canClick ? (
// Not sure what this button does.
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<button
type="button"
className={overlayClassName}
className={classNames('module-image__border-overlay', {
'module-image__border-overlay--with-border': !noBorder,
'module-image__border-overlay--with-click-handler': canClick,
'module-image__border-overlay--dark': darkOverlay,
'module-image--not-downloaded': imgNotDownloaded,
})}
style={curveStyles}
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
tabIndex={tabIndex}
@ -210,15 +212,13 @@ export class Image extends React.Component<Props> {
'module-image',
className,
!noBackground ? 'module-image--with-background' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
curveTopLeft ? 'module-image--curved-top-left' : null,
curveTopRight ? 'module-image--curved-top-right' : null,
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
softCorners ? 'module-image--soft-corners' : null,
cropWidth || cropHeight ? 'module-image--cropped' : null
)}
style={{ width: width - cropWidth, height: height - cropHeight }}
style={{
width: width - cropWidth,
height: height - cropHeight,
...curveStyles,
}}
>
{pending ? (
this.renderPending()
@ -248,11 +248,11 @@ export class Image extends React.Component<Props> {
) : null}
{bottomOverlay ? (
<div
className={classNames(
'module-image__bottom-overlay',
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null
)}
className="module-image__bottom-overlay"
style={{
borderBottomLeftRadius: curveBottomLeft || CurveType.None,
borderBottomRightRadius: curveBottomRight || CurveType.None,
}}
/>
) : null}
{!pending && !imgNotDownloaded && playIconOverlay ? (

View File

@ -37,6 +37,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
}),
],
bottomOverlay: boolean('bottomOverlay', overrideProps.bottomOverlay || false),
direction: overrideProps.direction || 'incoming',
i18n,
isSticker: boolean('isSticker', overrideProps.isSticker || false),
onClick: action('onClick'),

View File

@ -14,18 +14,23 @@ import {
isVideoAttachment,
} from '../../types/Attachment';
import { Image } from './Image';
import { Image, CurveType } from './Image';
import type { LocalizerType, ThemeType } from '../../types/Util';
export type DirectionType = 'incoming' | 'outgoing';
export type Props = {
attachments: Array<AttachmentType>;
withContentAbove?: boolean;
withContentBelow?: boolean;
bottomOverlay?: boolean;
direction: DirectionType;
isSticker?: boolean;
shouldCollapseAbove?: boolean;
shouldCollapseBelow?: boolean;
stickerSize?: number;
tabIndex?: number;
withContentAbove?: boolean;
withContentBelow?: boolean;
i18n: LocalizerType;
theme?: ThemeType;
@ -36,27 +41,85 @@ export type Props = {
const GAP = 1;
function getCurves({
direction,
shouldCollapseAbove,
shouldCollapseBelow,
withContentAbove,
withContentBelow,
}: {
direction: DirectionType;
shouldCollapseAbove?: boolean;
shouldCollapseBelow?: boolean;
withContentAbove?: boolean;
withContentBelow?: boolean;
}): {
curveTopLeft: CurveType;
curveTopRight: CurveType;
curveBottomLeft: CurveType;
curveBottomRight: CurveType;
} {
let curveTopLeft = CurveType.None;
let curveTopRight = CurveType.None;
let curveBottomLeft = CurveType.None;
let curveBottomRight = CurveType.None;
if (shouldCollapseAbove && direction === 'incoming') {
curveTopLeft = CurveType.Tiny;
curveTopRight = CurveType.Normal;
} else if (shouldCollapseAbove && direction === 'outgoing') {
curveTopLeft = CurveType.Normal;
curveTopRight = CurveType.Tiny;
} else if (!withContentAbove) {
curveTopLeft = CurveType.Normal;
curveTopRight = CurveType.Normal;
}
if (shouldCollapseBelow && direction === 'incoming') {
curveBottomLeft = CurveType.Tiny;
curveBottomRight = CurveType.None;
} else if (shouldCollapseBelow && direction === 'outgoing') {
curveBottomLeft = CurveType.None;
curveBottomRight = CurveType.Tiny;
} else if (!withContentBelow) {
curveBottomLeft = CurveType.Normal;
curveBottomRight = CurveType.Normal;
}
return {
curveTopLeft,
curveTopRight,
curveBottomLeft,
curveBottomRight,
};
}
export const ImageGrid = ({
attachments,
bottomOverlay,
direction,
i18n,
isSticker,
stickerSize,
onError,
onClick,
shouldCollapseAbove,
shouldCollapseBelow,
tabIndex,
theme,
withContentAbove,
withContentBelow,
}: Props): JSX.Element | null => {
const curveTopLeft = !withContentAbove;
const curveTopRight = curveTopLeft;
const { curveTopLeft, curveTopRight, curveBottomLeft, curveBottomRight } =
getCurves({
direction,
shouldCollapseAbove,
shouldCollapseBelow,
withContentAbove,
withContentBelow,
});
const curveBottom = !withContentBelow;
const curveBottomLeft = curveBottom;
const curveBottomRight = curveBottom;
const withBottomOverlay = Boolean(bottomOverlay && curveBottom);
const withBottomOverlay = Boolean(bottomOverlay && !withContentBelow);
if (!attachments || !attachments.length) {
return null;

View File

@ -222,15 +222,30 @@ const renderMany = (propsArray: ReadonlyArray<Props>) =>
/>
));
const renderBothDirections = (props: Props) =>
renderMany([
props,
{
const renderThree = (props: Props) => renderMany([props, props, props]);
const renderBothDirections = (props: Props) => (
<>
{renderThree(props)}
{renderThree({
...props,
author: { ...props.author, id: getDefaultConversation().id },
direction: 'outgoing',
},
]);
})}
</>
);
const renderSingleBothDirections = (props: Props) => (
<>
<Message {...props} />
<Message
{...{
...props,
author: { ...props.author, id: getDefaultConversation().id },
direction: 'outgoing',
}}
/>
</>
);
story.add('Plain Message', () => {
const props = createProps({
@ -353,7 +368,7 @@ story.add('Delivered', () => {
text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
});
return <Message {...props} />;
return renderThree(props);
});
story.add('Read', () => {
@ -363,7 +378,7 @@ story.add('Read', () => {
text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
});
return <Message {...props} />;
return renderThree(props);
});
story.add('Sending', () => {
@ -373,7 +388,7 @@ story.add('Sending', () => {
text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.',
});
return <Message {...props} />;
return renderThree(props);
});
story.add('Expiring', () => {
@ -502,7 +517,7 @@ story.add('Reactions (wider message)', () => {
],
});
return renderBothDirections(props);
return renderSingleBothDirections(props);
});
const joyReactions = Array.from({ length: 52 }, () => getJoyReaction());
@ -577,7 +592,7 @@ story.add('Reactions (short message)', () => {
],
});
return renderBothDirections(props);
return renderSingleBothDirections(props);
});
story.add('Avatar in Group', () => {
@ -588,7 +603,7 @@ story.add('Avatar in Group', () => {
text: 'Hello it is me, the saxophone.',
});
return <Message {...props} />;
return renderThree(props);
});
story.add('Badge in Group', () => {
@ -599,7 +614,7 @@ story.add('Badge in Group', () => {
text: 'Hello it is me, the saxophone.',
});
return <Message {...props} />;
return renderThree(props);
});
story.add('Sticker', () => {
@ -673,8 +688,8 @@ story.add('Deleted with error', () => {
return (
<>
<Message {...propsPartialError} />
<Message {...propsError} />
{renderThree(propsPartialError)}
{renderThree(propsError)}
</>
);
});
@ -684,9 +699,10 @@ story.add('Can delete for everyone', () => {
status: 'read',
text: 'I hope you get this.',
canDeleteForEveryone: true,
direction: 'outgoing',
});
return <Message {...props} direction="outgoing" />;
return renderThree(props);
});
story.add('Error', () => {
@ -916,7 +932,7 @@ story.add('Link Preview with too new a date', () => {
});
story.add('Image', () => {
const props = createProps({
const darkImageProps = createProps({
attachments: [
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
@ -928,8 +944,25 @@ story.add('Image', () => {
],
status: 'sent',
});
const lightImageProps = createProps({
attachments: [
fakeAttachment({
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
],
status: 'sent',
});
return renderBothDirections(props);
return (
<>
{renderBothDirections(darkImageProps)}
{renderBothDirections(lightImageProps)}
</>
);
});
for (let i = 2; i <= 5; i += 1) {
@ -937,39 +970,39 @@ for (let i = 2; i <= 5; i += 1) {
const props = createProps({
attachments: [
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
url: pngUrl,
fileName: 'the-sax.png',
contentType: IMAGE_PNG,
height: 240,
width: 320,
}),
].slice(0, i),
status: 'sent',
@ -1316,7 +1349,7 @@ story.add('TapToView Error', () => {
status: 'sent',
});
return <Message {...props} />;
return renderThree(props);
});
story.add('Dangerous File Type', () => {
@ -1419,23 +1452,23 @@ story.add('Not approved, with link preview', () => {
story.add('Custom Color', () => (
<>
<Message
{...createProps({ text: 'Solid.' })}
direction="outgoing"
customColor={{
{renderThree({
...createProps({ text: 'Solid.' }),
direction: 'outgoing',
customColor: {
start: { hue: 82, saturation: 35 },
}}
/>
},
})}
<br style={{ clear: 'both' }} />
<Message
{...createProps({ text: 'Gradient.' })}
direction="outgoing"
customColor={{
{renderThree({
...createProps({ text: 'Gradient.' }),
direction: 'outgoing',
customColor: {
deg: 192,
start: { hue: 304, saturation: 85 },
end: { hue: 231, saturation: 76 },
}}
/>
},
})}
</>
));
@ -1506,20 +1539,18 @@ story.add('Collapsing text-only group messages', () => {
story.add('Story reply', () => {
const conversation = getDefaultConversation();
return (
<Message
{...createProps({ text: 'Wow!' })}
storyReplyContext={{
authorTitle: conversation.title,
conversationColor: ConversationColors[0],
isFromMe: false,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
}}
/>
);
return renderThree({
...createProps({ text: 'Wow!' }),
storyReplyContext: {
authorTitle: conversation.title,
conversationColor: ConversationColors[0],
isFromMe: false,
rawAttachment: fakeAttachment({
url: '/fixtures/snow.jpg',
thumbnail: fakeThumbnail('/fixtures/snow.jpg'),
}),
},
});
});
const fullContact = {
@ -1559,7 +1590,7 @@ story.add('EmbeddedContact: Full Contact', () => {
return renderBothDirections(props);
});
story.add('EmbeddedContact: 2x Incoming, with Send Message', () => {
story.add('EmbeddedContact: with Send Message', () => {
const props = createProps({
contact: {
...fullContact,
@ -1568,19 +1599,7 @@ story.add('EmbeddedContact: 2x Incoming, with Send Message', () => {
},
direction: 'incoming',
});
return renderMany([props, props]);
});
story.add('EmbeddedContact: 2x Outgoing, with Send Message', () => {
const props = createProps({
contact: {
...fullContact,
firstNumber: fullContact.number[0].value,
uuid: UUID.generate().toString(),
},
direction: 'outgoing',
});
return renderMany([props, props]);
return renderBothDirections(props);
});
story.add('EmbeddedContact: Only Email', () => {

View File

@ -28,7 +28,7 @@ import { MessageMetadata } from './MessageMetadata';
import { MessageTextMetadataSpacer } from './MessageTextMetadataSpacer';
import { ImageGrid } from './ImageGrid';
import { GIF } from './GIF';
import { Image } from './Image';
import { CurveType, Image } from './Image';
import { ContactName } from './ContactName';
import type { QuotedAttachmentType } from './Quote';
import { Quote } from './Quote';
@ -908,18 +908,17 @@ export class Message extends React.PureComponent<Props, State> {
<div className={containerClassName}>
<ImageGrid
attachments={attachments}
withContentAbove={
isSticker || withContentAbove || shouldCollapseAbove
}
withContentBelow={
isSticker || withContentBelow || shouldCollapseBelow
}
direction={direction}
withContentAbove={isSticker || withContentAbove}
withContentBelow={isSticker || withContentBelow}
isSticker={isSticker}
stickerSize={STICKER_SIZE}
bottomOverlay={bottomOverlay}
i18n={i18n}
theme={theme}
onError={this.handleImageError}
theme={theme}
shouldCollapseAbove={shouldCollapseAbove}
shouldCollapseBelow={shouldCollapseBelow}
tabIndex={tabIndex}
onClick={attachment => {
if (!isDownloaded(attachment)) {
@ -1060,6 +1059,7 @@ export class Message extends React.PureComponent<Props, State> {
openLink,
previews,
quote,
shouldCollapseAbove,
theme,
kickOffAttachmentDownload,
} = this.props;
@ -1113,6 +1113,8 @@ export class Message extends React.PureComponent<Props, State> {
<ImageGrid
attachments={[first.image]}
withContentAbove={withContentAbove}
direction={direction}
shouldCollapseAbove={shouldCollapseAbove}
withContentBelow
onError={this.handleImageError}
i18n={i18n}
@ -1124,10 +1126,14 @@ export class Message extends React.PureComponent<Props, State> {
{first.image && previewHasImage && !isFullSizeImage ? (
<div className="module-message__link-preview__icon_container">
<Image
smallCurveTopLeft={!withContentAbove}
noBorder
noBackground
softCorners
curveBottomLeft={
withContentAbove ? CurveType.Tiny : CurveType.Small
}
curveBottomRight={CurveType.Tiny}
curveTopRight={CurveType.Tiny}
curveTopLeft={CurveType.Tiny}
alt={i18n('previewThumbnail', [first.domain])}
height={72}
width={72}

View File

@ -5,7 +5,7 @@ import React from 'react';
import classNames from 'classnames';
import { unescape } from 'lodash';
import { Image } from './Image';
import { CurveType, Image } from './Image';
import { LinkPreviewDate } from './LinkPreviewDate';
import type { AttachmentType } from '../../types/Attachment';
@ -51,7 +51,10 @@ export const StagedLinkPreview: React.FC<Props> = ({
<div className="module-staged-link-preview__icon-container">
<Image
alt={i18n('stagedPreviewThumbnail', [domain])}
softCorners
curveBottomLeft={CurveType.Tiny}
curveBottomRight={CurveType.Tiny}
curveTopRight={CurveType.Tiny}
curveTopLeft={CurveType.Tiny}
height={72}
width={72}
url={image.url}