Download attachments in separate queue from message processing

This commit is contained in:
Scott Nonnenberg 2019-01-30 12:15:07 -08:00
parent a43a78731a
commit 1d2c3ae23c
34 changed files with 2062 additions and 214 deletions

15
ts/components/Spinner.md Normal file
View file

@ -0,0 +1,15 @@
#### Large
```jsx
<util.ConversationContext theme={util.theme}>
<Spinner />
</util.ConversationContext>
```
#### Small
```jsx
<util.ConversationContext theme={util.theme}>
<Spinner small />
</util.ConversationContext>
```

47
ts/components/Spinner.tsx Normal file
View file

@ -0,0 +1,47 @@
import React from 'react';
import classNames from 'classnames';
interface Props {
small?: boolean;
direction?: string;
}
export class Spinner extends React.Component<Props> {
public render() {
const { small, direction } = this.props;
return (
<div
className={classNames(
'module-spinner__container',
direction ? `module-spinner__container--${direction}` : null,
small ? 'module-spinner__container--small' : null,
small && direction
? `module-spinner__container--small-${direction}`
: null
)}
>
<div
className={classNames(
'module-spinner__circle',
direction ? `module-spinner__circle--${direction}` : null,
small ? 'module-spinner__circle--small' : null,
small && direction
? `module-spinner__circle--small-${direction}`
: null
)}
/>
<div
className={classNames(
'module-spinner__arc',
direction ? `module-spinner__arc--${direction}` : null,
small ? 'module-spinner__arc--small' : null,
small && direction
? `module-spinner__arc--small-${direction}`
: null
)}
/>
</div>
);
}
}

View file

@ -66,6 +66,51 @@ const contact = {
</util.ConversationContext>;
```
#### Image download pending
```jsx
const contact = {
name: {
displayName: 'Someone Somewhere',
},
number: [
{
value: '(202) 555-0000',
type: 1,
},
],
avatar: {
avatar: {
pending: true,
},
},
onClick: () => console.log('onClick'),
onSendMessage: () => console.log('onSendMessage'),
hasSignalAccount: true,
};
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
<li>
<Message
authorColor="green"
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
</util.ConversationContext>;
```
#### Really long data
```

View file

@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { Avatar } from '../Avatar';
import { Spinner } from '../Spinner';
import { Contact, getName } from '../../types/Contact';
import { Localizer } from '../../types/Util';
@ -27,6 +28,7 @@ export class EmbeddedContact extends React.Component<Props> {
withContentBelow,
} = this.props;
const module = 'embedded-contact';
const direction = isIncoming ? 'incoming' : 'outgoing';
return (
<div
@ -42,7 +44,7 @@ export class EmbeddedContact extends React.Component<Props> {
role="button"
onClick={onClick}
>
{renderAvatar({ contact, i18n, size: 48 })}
{renderAvatar({ contact, i18n, size: 48, direction })}
<div className="module-embedded-contact__text-container">
{renderName({ contact, isIncoming, module })}
{renderContactShorthand({ contact, isIncoming, module })}
@ -58,16 +60,27 @@ export function renderAvatar({
contact,
i18n,
size,
direction,
}: {
contact: Contact;
i18n: Localizer;
size: number;
direction?: string;
}) {
const { avatar } = contact;
const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
const pending = avatar && avatar.avatar && avatar.avatar.pending;
const name = getName(contact) || '';
if (pending) {
return (
<div className="module-embedded-contact__spinner-container">
<Spinner small={size < 50} direction={direction} />
</div>
);
}
return (
<Avatar
avatarPath={avatarPath}

View file

@ -1,44 +1,203 @@
### Various sizes
```jsx
<Image height='200' width='199' url={util.pngObjectUrl} />
<Image height='149' width='149' url={util.pngObjectUrl} />
<Image height='99' width='99' url={util.pngObjectUrl} />
<util.ConversationContext theme={util.theme}>
<Image
height="200"
width="199"
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
url={util.pngObjectUrl}
attachment={{ pending: true }}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### Various curved corners
```jsx
<Image height='149' width='149' curveTopLeft url={util.pngObjectUrl} />
<Image height='149' width='149' curveTopRight url={util.pngObjectUrl} />
<Image height='149' width='149' curveBottomLeft url={util.pngObjectUrl} />
<Image height='149' width='149' curveBottomRight url={util.pngObjectUrl} />
<util.ConversationContext theme={util.theme}>
<Image
height="149"
width="149"
curveTopLeft
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
curveTopRight
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
curveBottomLeft
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
curveBottomRight
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
curveBottomRight
url={util.pngObjectUrl}
attachment={{ pending: true }}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### With bottom overlay
```jsx
<Image height='149' width='149' bottomOverlay url={util.pngObjectUrl} />
<Image height='149' width='149' bottomOverlay curveBottomRight url={util.pngObjectUrl} />
<Image height='149' width='149' bottomOverlay curveBottomLeft url={util.pngObjectUrl} />
<util.ConversationContext theme={util.theme}>
<Image
height="149"
width="149"
bottomOverlay
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
bottomOverlay
curveBottomRight
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
bottomOverlay
curveBottomLeft
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
bottomOverlay
curveBottomLeft
url={util.pngObjectUrl}
attachment={{ pending: true }}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### With play icon
```jsx
<Image height='200' width='199' playIconOverlay url={util.pngObjectUrl} />
<Image height='149' width='149' playIconOverlay url={util.pngObjectUrl} />
<Image height='99' width='99' playIconOverlay url={util.pngObjectUrl} />
<util.ConversationContext theme={util.theme}>
<Image
height="200"
width="199"
playIconOverlay
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
playIconOverlay
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
playIconOverlay
url={util.pngObjectUrl}
attachment={{}}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
playIconOverlay
url={util.pngObjectUrl}
attachment={{ pending: true }}
i18n={util.i18n}
/>
</util.ConversationContext>
```
### With dark overlay and text
```jsx
<div>
<util.ConversationContext theme={util.theme}>
<div>
<Image height="200" width="199" darkOverlay url={util.pngObjectUrl} />
<Image height="149" width="149" darkOverlay url={util.pngObjectUrl} />
<Image height="99" width="99" darkOverlay url={util.pngObjectUrl} />
<Image
height="200"
width="199"
darkOverlay
attachment={{}}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
darkOverlay
attachment={{}}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
darkOverlay
attachment={{}}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
darkOverlay
attachment={{ pending: true }}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
</div>
<hr />
<div>
@ -46,31 +205,46 @@
height="200"
width="199"
darkOverlay
attachment={{}}
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="149"
width="149"
darkOverlay
attachment={{}}
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
darkOverlay
attachment={{}}
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
darkOverlay
attachment={{ pending: true }}
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
</div>
</div>
</util.ConversationContext>
```
### With caption
```jsx
<div>
<util.ConversationContext theme={util.theme}>
<div>
<Image
height="200"
@ -93,6 +267,13 @@
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
attachment={{ caption: 'dogs playing', pending: true }}
url={util.pngObjectUrl}
i18n={util.i18n}
/>
</div>
<hr />
<div>
@ -123,18 +304,28 @@
url={util.pngObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
attachment={{ caption: 'dogs playing', pending: true }}
darkOverlay
overlayText="+3"
url={util.pngObjectUrl}
i18n={util.i18n}
/>
</div>
</div>
</util.ConversationContext>
```
### With top-right X and soft corners
```jsx
<div>
<util.ConversationContext theme={util.theme}>
<div>
<Image
height="200"
width="199"
attachment={{}}
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
@ -145,6 +336,7 @@
<Image
height="149"
width="149"
attachment={{}}
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
@ -155,6 +347,18 @@
<Image
height="99"
width="99"
attachment={{}}
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
attachment={{ pending: true }}
closeButton={true}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
@ -168,6 +372,7 @@
<Image
height="200"
width="199"
attachment={{}}
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
@ -179,6 +384,7 @@
<Image
height="149"
width="149"
attachment={{}}
closeButton={true}
attachment={{ caption: 'dogs playing' }}
onClick={() => console.log('onClick')}
@ -198,6 +404,17 @@
url={util.gifObjectUrl}
i18n={util.i18n}
/>
<Image
height="99"
width="99"
closeButton={true}
attachment={{ caption: 'dogs playing', pending: true }}
onClick={() => console.log('onClick')}
onClickClose={attachment => console.log('onClickClose', attachment)}
softCorners={true}
url={util.gifObjectUrl}
i18n={util.i18n}
/>
</div>
</div>
</util.ConversationContext>
```

View file

@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import { Spinner } from '../Spinner';
import { Localizer } from '../../types/Util';
import { AttachmentType } from './types';
@ -59,19 +60,20 @@ export class Image extends React.Component<Props> {
width,
} = this.props;
const { caption } = attachment || { caption: null };
const { caption, pending } = attachment || { caption: null, pending: true };
const canClick = onClick && !pending;
return (
<div
role={onClick ? 'button' : undefined}
role={canClick ? 'button' : undefined}
onClick={() => {
if (onClick) {
if (canClick) {
onClick(attachment);
}
}}
className={classNames(
'module-image',
onClick ? 'module-image__with-click-handler' : null,
canClick ? 'module-image__with-click-handler' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
curveTopLeft ? 'module-image--curved-top-left' : null,
@ -80,14 +82,29 @@ export class Image extends React.Component<Props> {
softCorners ? 'module-image--soft-corners' : null
)}
>
<img
onError={onError}
className="module-image__image"
alt={alt}
height={height}
width={width}
src={url}
/>
{pending ? (
<div
className="module-image__loading-placeholder"
style={{
height: `${height}px`,
width: `${width}px`,
lineHeight: `${height}px`,
textAlign: 'center',
}}
// alt={i18n('loading')}
>
<Spinner />
</div>
) : (
<img
onError={onError}
className="module-image__image"
alt={alt}
height={height}
width={width}
src={url}
/>
)}
{caption ? (
<img
className="module-image__caption-icon"
@ -128,7 +145,7 @@ export class Image extends React.Component<Props> {
)}
/>
) : null}
{playIconOverlay ? (
{!pending && playIconOverlay ? (
<div className="module-image__play-overlay__circle">
<div className="module-image__play-overlay__icon" />
</div>

View file

@ -340,7 +340,11 @@ export function isImageAttachment(attachment: AttachmentType) {
);
}
export function hasImage(attachments?: Array<AttachmentType>) {
return attachments && attachments[0] && attachments[0].url;
return (
attachments &&
attachments[0] &&
(attachments[0].url || attachments[0].pending)
);
}
export function isVideo(attachments?: Array<AttachmentType>) {

View file

@ -1166,6 +1166,111 @@ Note that the delivered indicator is always Signal Blue, not the conversation co
</util.ConversationContext>
```
#### Pending images
```
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="green"
direction="incoming"
timestamp={Date.now()}
text="Hey there!"
i18n={util.i18n}
attachments={[
{
pending: true,
contentType: 'image/gif',
width: 320,
height: 240,
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
<li>
<Message
authorColor="green"
direction="outgoing"
status="sent"
timestamp={Date.now()}
text="Hey there!"
i18n={util.i18n}
attachments={[
{
pending: true,
contentType: 'image/gif',
width: 320,
height: 240,
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
<li>
<Message
authorColor="green"
direction="incoming"
timestamp={Date.now()}
i18n={util.i18n}
text="Three images"
attachments={[
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
{
pending: true,
contentType: 'image/gif',
width: 320,
height: 240,
},
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
<li>
<Message
authorColor="green"
direction="outgoing"
status="delivered"
timestamp={Date.now()}
i18n={util.i18n}
text="Three images"
attachments={[
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
{
pending: true,
contentType: 'image/gif',
width: 320,
height: 240,
},
{
url: util.gifObjectUrl,
contentType: 'image/gif',
width: 320,
height: 240,
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
</util.ConversationContext>
```
#### Image with portrait aspect ratio
```jsx
@ -2533,6 +2638,84 @@ Voice notes are not shown any differently from audio attachments.
</util.ConversationContext>
```
#### Other file type pending
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="green"
direction="incoming"
text="My manifesto is now complete!"
i18n={util.i18n}
timestamp={Date.now()}
attachments={[
{
pending: true,
contentType: 'text/plain',
fileName: 'my_manifesto.txt',
fileSize: '3.05 KB',
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
<li>
<Message
authorColor="green"
direction="outgoing"
text="My manifesto is now complete!"
status="sent"
i18n={util.i18n}
timestamp={Date.now()}
attachments={[
{
pending: true,
contentType: 'text/plain',
fileName: 'my_manifesto.txt',
fileSize: '3.05 KB',
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
<li>
<Message
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
attachments={[
{
pending: true,
contentType: 'text/plain',
fileName: 'my_manifesto.txt',
fileSize: '3.05 KB',
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
<li>
<Message
authorColor="green"
direction="outgoing"
i18n={util.i18n}
timestamp={Date.now()}
attachments={[
{
pending: true,
contentType: 'text/plain',
fileName: 'my_manifesto.txt',
fileSize: '3.05 KB',
},
]}
onClickAttachment={() => console.log('onClickAttachment')}
/>
</li>
</util.ConversationContext>
```
#### Dangerous file type
```jsx
@ -2799,6 +2982,103 @@ Voice notes are not shown any differently from audio attachments.
</util.ConversationContext>
```
#### Link previews with pending image
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
text="Pretty sweet link: https://instagram.com/something"
previews={[
{
title: 'This is a really sweet post',
domain: 'instagram.com',
image: {
pending: true,
contentType: 'image/png',
width: 800,
height: 1200,
},
},
]}
onClickLinkPreview={url => console.log('onClickLinkPreview', url)}
/>
</li>
<li>
<Message
authorColor="green"
direction="outgoing"
i18n={util.i18n}
timestamp={Date.now()}
status="sent"
text="Pretty sweet link: https://instagram.com/something"
previews={[
{
title: 'This is a really sweet post',
domain: 'instagram.com',
image: {
pending: true,
contentType: 'image/png',
width: 800,
height: 1200,
},
},
]}
onClickLinkPreview={url => console.log('onClickLinkPreview', url)}
/>
</li>
<li>
<Message
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
text="Pretty sweet link: https://instagram.com/something"
previews={[
{
title: 'This is a really sweet post',
domain: 'instagram.com',
image: {
pending: true,
contentType: 'image/png',
width: 160,
height: 120,
},
},
]}
onClickLinkPreview={url => console.log('onClickLinkPreview', url)}
/>
</li>
<li>
<Message
authorColor="green"
direction="outgoing"
i18n={util.i18n}
timestamp={Date.now()}
status="sent"
text="Pretty sweet link: https://instagram.com/something"
previews={[
{
title: 'This is a really sweet post',
domain: 'instagram.com',
image: {
pending: true,
contentType: 'image/png',
width: 160,
height: 120,
},
},
]}
onClickLinkPreview={url => console.log('onClickLinkPreview', url)}
/>
</li>
</util.ConversationContext>
```
#### Link previews, no image
```jsx

View file

@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { Avatar } from '../Avatar';
import { Spinner } from '../Spinner';
import { MessageBody } from './MessageBody';
import { ExpireTimer, getIncrement } from './ExpireTimer';
import {
@ -345,7 +346,7 @@ export class Message extends React.Component<Props, State> {
/>
</div>
);
} else if (isAudio(attachments)) {
} else if (!firstAttachment.pending && isAudio(attachments)) {
return (
<audio
controls={true}
@ -358,12 +359,13 @@ export class Message extends React.Component<Props, State> {
? 'module-message__audio-attachment--with-content-above'
: null
)}
key={firstAttachment.url}
>
<source src={firstAttachment.url} />
</audio>
);
} else {
const { fileName, fileSize, contentType } = firstAttachment;
const { pending, fileName, fileSize, contentType } = firstAttachment;
const extension = getExtension({ contentType, fileName });
const isDangerous = isFileDangerous(fileName || '');
@ -379,20 +381,26 @@ export class Message extends React.Component<Props, State> {
: null
)}
>
<div className="module-message__generic-attachment__icon-container">
<div className="module-message__generic-attachment__icon">
{extension ? (
<div className="module-message__generic-attachment__icon__extension">
{extension}
{pending ? (
<div className="module-message__generic-attachment__spinner-container">
<Spinner small={true} direction={direction} />
</div>
) : (
<div className="module-message__generic-attachment__icon-container">
<div className="module-message__generic-attachment__icon">
{extension ? (
<div className="module-message__generic-attachment__icon__extension">
{extension}
</div>
) : null}
</div>
{isDangerous ? (
<div className="module-message__generic-attachment__icon-dangerous-container">
<div className="module-message__generic-attachment__icon-dangerous" />
</div>
) : null}
</div>
{isDangerous ? (
<div className="module-message__generic-attachment__icon-dangerous-container">
<div className="module-message__generic-attachment__icon-dangerous" />
</div>
) : null}
</div>
)}
<div className="module-message__generic-attachment__text">
<div
className={classNames(
@ -711,9 +719,10 @@ export class Message extends React.Component<Props, State> {
attachments && attachments[0] ? attachments[0].fileName : null;
const isDangerous = isFileDangerous(fileName || '');
const multipleAttachments = attachments && attachments.length > 1;
const firstAttachment = attachments && attachments[0];
const downloadButton =
!multipleAttachments && attachments && attachments[0] ? (
!multipleAttachments && firstAttachment && !firstAttachment.pending ? (
<div
onClick={() => {
if (onDownload) {
@ -983,6 +992,10 @@ export function getExtension({
}
}
if (!contentType) {
return null;
}
const slash = contentType.indexOf('/');
if (slash >= 0) {
return contentType.slice(slash + 1);

View file

@ -1016,6 +1016,50 @@ messages the color is taken from the contact who wrote the quoted message.
quote={{
authorColor: 'purple',
attachment: {
pending: true,
contentType: 'image/gif',
fileName: 'pi.gif',
},
authorPhoneNumber: '(202) 555-0011',
}}
/>
</li>
</util.ConversationContext>
```
#### Pending image download
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
direction="incoming"
timestamp={Date.now()}
authorColor="green"
text="Yeah, pi. Tough to wrap your head around."
i18n={util.i18n}
quote={{
authorColor: 'purple',
attachment: {
contentType: 'image/gif',
fileName: 'pi.gif',
},
authorPhoneNumber: '(202) 555-0011',
}}
/>
</li>
<li>
<Message
direction="outgoing"
timestamp={Date.now()}
status="sending"
authorColor="green"
text="Yeah, pi. Tough to wrap your head around."
i18n={util.i18n}
quote={{
authorColor: 'purple',
attachment: {
pending: true,
contentType: 'image/gif',
fileName: 'pi.gif',
},

View file

@ -26,6 +26,10 @@ interface Props {
referencedMessageNotFound: boolean;
}
interface State {
imageBroken: boolean;
}
export interface QuotedAttachmentType {
contentType: MIME.MIMEType;
fileName: string;
@ -85,7 +89,27 @@ function getTypeLabel({
return null;
}
export class Quote extends React.Component<Props> {
export class Quote extends React.Component<Props, State> {
public handleImageErrorBound: () => void;
public constructor(props: Props) {
super(props);
this.handleImageErrorBound = this.handleImageError.bind(this);
this.state = {
imageBroken: false,
};
}
public handleImageError() {
// tslint:disable-next-line no-console
console.log('Message: Image failed to load; failing over to placeholder');
this.setState({
imageBroken: true,
});
}
public renderImage(url: string, i18n: Localizer, icon?: string) {
const iconElement = icon ? (
<div className="module-quote__icon-container__inner">
@ -102,7 +126,11 @@ export class Quote extends React.Component<Props> {
return (
<div className="module-quote__icon-container">
<img src={url} alt={i18n('quoteThumbnailAlt')} />
<img
src={url}
alt={i18n('quoteThumbnailAlt')}
onError={this.handleImageErrorBound}
/>
{iconElement}
</div>
);
@ -159,6 +187,8 @@ export class Quote extends React.Component<Props> {
public renderIconContainer() {
const { attachment, i18n } = this.props;
const { imageBroken } = this.state;
if (!attachment) {
return null;
}
@ -167,12 +197,12 @@ export class Quote extends React.Component<Props> {
const objectUrl = getObjectUrl(thumbnail);
if (GoogleChrome.isVideoTypeSupported(contentType)) {
return objectUrl
return objectUrl && !imageBroken
? this.renderImage(objectUrl, i18n, 'play')
: this.renderIcon('movie');
}
if (GoogleChrome.isImageTypeSupported(contentType)) {
return objectUrl
return objectUrl && !imageBroken
? this.renderImage(objectUrl, i18n)
: this.renderIcon('image');
}

View file

@ -10,6 +10,7 @@ export interface AttachmentType {
url: string;
size?: number;
fileSize?: string;
pending?: boolean;
width?: number;
height?: number;
screenshot?: {