Download attachments in separate queue from message processing
This commit is contained in:
parent
a43a78731a
commit
1d2c3ae23c
34 changed files with 2062 additions and 214 deletions
15
ts/components/Spinner.md
Normal file
15
ts/components/Spinner.md
Normal 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
47
ts/components/Spinner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
```
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ export interface AttachmentType {
|
|||
url: string;
|
||||
size?: number;
|
||||
fileSize?: string;
|
||||
pending?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
screenshot?: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue