2018-04-03 22:56:12 +00:00
|
|
|
import React from 'react';
|
2018-07-07 00:48:14 +00:00
|
|
|
import classNames from 'classnames';
|
2018-06-27 20:53:49 +00:00
|
|
|
|
2018-09-27 00:23:17 +00:00
|
|
|
import { Avatar } from '../Avatar';
|
2019-01-30 20:15:07 +00:00
|
|
|
import { Spinner } from '../Spinner';
|
2018-06-27 20:53:49 +00:00
|
|
|
import { MessageBody } from './MessageBody';
|
2018-07-09 21:29:13 +00:00
|
|
|
import { ExpireTimer, getIncrement } from './ExpireTimer';
|
2018-11-14 18:47:19 +00:00
|
|
|
import {
|
|
|
|
getGridDimensions,
|
2019-01-16 03:03:56 +00:00
|
|
|
getImageDimensions,
|
2018-11-14 18:47:19 +00:00
|
|
|
hasImage,
|
|
|
|
hasVideoScreenshot,
|
|
|
|
ImageGrid,
|
|
|
|
isImage,
|
2019-01-16 03:03:56 +00:00
|
|
|
isImageAttachment,
|
2018-11-14 18:47:19 +00:00
|
|
|
isVideo,
|
|
|
|
} from './ImageGrid';
|
2019-01-16 03:03:56 +00:00
|
|
|
import { Image } from './Image';
|
2018-07-09 21:29:13 +00:00
|
|
|
import { Timestamp } from './Timestamp';
|
|
|
|
import { ContactName } from './ContactName';
|
2018-11-14 18:47:19 +00:00
|
|
|
import { Quote, QuotedAttachmentType } from './Quote';
|
2018-06-27 20:53:49 +00:00
|
|
|
import { EmbeddedContact } from './EmbeddedContact';
|
2018-11-14 18:47:19 +00:00
|
|
|
import * as MIME from '../../../ts/types/MIME';
|
2018-06-27 20:53:49 +00:00
|
|
|
|
2018-11-14 18:47:19 +00:00
|
|
|
import { AttachmentType } from './types';
|
2018-10-04 01:12:42 +00:00
|
|
|
import { isFileDangerous } from '../../util/isFileDangerous';
|
2018-06-27 20:53:49 +00:00
|
|
|
import { Contact } from '../../types/Contact';
|
2018-07-09 21:29:13 +00:00
|
|
|
import { Color, Localizer } from '../../types/Util';
|
|
|
|
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
|
|
|
|
|
|
|
interface Trigger {
|
|
|
|
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
|
|
}
|
|
|
|
|
2019-01-16 03:03:56 +00:00
|
|
|
// Same as MIN_WIDTH in ImageGrid.tsx
|
|
|
|
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
|
|
|
|
|
|
|
interface LinkPreviewType {
|
|
|
|
title: string;
|
|
|
|
domain: string;
|
|
|
|
url: string;
|
|
|
|
image?: AttachmentType;
|
|
|
|
}
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
export interface Props {
|
|
|
|
disableMenu?: boolean;
|
2018-06-27 20:53:49 +00:00
|
|
|
text?: string;
|
|
|
|
id?: string;
|
|
|
|
collapseMetadata?: boolean;
|
|
|
|
direction: 'incoming' | 'outgoing';
|
|
|
|
timestamp: number;
|
2018-07-09 21:29:13 +00:00
|
|
|
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
|
|
|
// What if changed this over to a single contact like quote, and put the events on it?
|
|
|
|
contact?: Contact & {
|
|
|
|
hasSignalAccount: boolean;
|
|
|
|
onSendMessage?: () => void;
|
|
|
|
onClick?: () => void;
|
|
|
|
};
|
2018-06-27 20:53:49 +00:00
|
|
|
i18n: Localizer;
|
|
|
|
authorName?: string;
|
|
|
|
authorProfileName?: string;
|
|
|
|
/** Note: this should be formatted for display */
|
2018-07-09 21:29:13 +00:00
|
|
|
authorPhoneNumber: string;
|
2018-10-09 22:56:14 +00:00
|
|
|
authorColor?: Color;
|
2018-06-27 20:53:49 +00:00
|
|
|
conversationType: 'group' | 'direct';
|
2018-11-14 18:47:19 +00:00
|
|
|
attachments?: Array<AttachmentType>;
|
2018-06-27 20:53:49 +00:00
|
|
|
quote?: {
|
|
|
|
text: string;
|
2018-11-14 18:47:19 +00:00
|
|
|
attachment?: QuotedAttachmentType;
|
2018-06-27 20:53:49 +00:00
|
|
|
isFromMe: boolean;
|
2018-07-09 21:29:13 +00:00
|
|
|
authorPhoneNumber: string;
|
2018-06-27 20:53:49 +00:00
|
|
|
authorProfileName?: string;
|
2018-07-09 21:29:13 +00:00
|
|
|
authorName?: string;
|
2018-10-09 22:56:14 +00:00
|
|
|
authorColor?: Color;
|
2018-07-09 21:29:13 +00:00
|
|
|
onClick?: () => void;
|
2018-08-15 19:31:29 +00:00
|
|
|
referencedMessageNotFound: boolean;
|
2018-06-27 20:53:49 +00:00
|
|
|
};
|
2019-01-16 03:03:56 +00:00
|
|
|
previews: Array<LinkPreviewType>;
|
2018-06-27 20:53:49 +00:00
|
|
|
authorAvatarPath?: string;
|
2018-08-09 23:18:10 +00:00
|
|
|
isExpired: boolean;
|
2018-06-27 20:53:49 +00:00
|
|
|
expirationLength?: number;
|
|
|
|
expirationTimestamp?: number;
|
2018-11-14 18:47:19 +00:00
|
|
|
onClickAttachment?: (attachment: AttachmentType) => void;
|
2019-01-16 03:03:56 +00:00
|
|
|
onClickLinkPreview?: (url: string) => void;
|
2018-07-09 21:29:13 +00:00
|
|
|
onReply?: () => void;
|
|
|
|
onRetrySend?: () => void;
|
2018-10-04 01:12:42 +00:00
|
|
|
onDownload?: (isDangerous: boolean) => void;
|
2018-07-09 21:29:13 +00:00
|
|
|
onDelete?: () => void;
|
|
|
|
onShowDetail: () => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
expiring: boolean;
|
|
|
|
expired: boolean;
|
|
|
|
imageBroken: boolean;
|
2018-06-27 20:53:49 +00:00
|
|
|
}
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
const EXPIRATION_CHECK_MINIMUM = 2000;
|
|
|
|
const EXPIRED_DELAY = 600;
|
2018-06-27 20:53:49 +00:00
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
export class Message extends React.Component<Props, State> {
|
|
|
|
public captureMenuTriggerBound: (trigger: any) => void;
|
|
|
|
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
|
|
public handleImageErrorBound: () => void;
|
|
|
|
|
|
|
|
public menuTriggerRef: Trigger | null;
|
|
|
|
public expirationCheckInterval: any;
|
|
|
|
public expiredTimeout: any;
|
|
|
|
|
|
|
|
public constructor(props: Props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this);
|
|
|
|
this.showMenuBound = this.showMenu.bind(this);
|
|
|
|
this.handleImageErrorBound = this.handleImageError.bind(this);
|
|
|
|
|
|
|
|
this.menuTriggerRef = null;
|
|
|
|
this.expirationCheckInterval = null;
|
|
|
|
this.expiredTimeout = null;
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
expiring: false,
|
|
|
|
expired: false,
|
|
|
|
imageBroken: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public componentDidMount() {
|
|
|
|
const { expirationLength } = this.props;
|
|
|
|
if (!expirationLength) {
|
|
|
|
return;
|
2018-06-27 20:53:49 +00:00
|
|
|
}
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
const increment = getIncrement(expirationLength);
|
|
|
|
const checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment);
|
2018-04-03 22:56:12 +00:00
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
this.checkExpired();
|
|
|
|
|
|
|
|
this.expirationCheckInterval = setInterval(() => {
|
|
|
|
this.checkExpired();
|
|
|
|
}, checkFrequency);
|
|
|
|
}
|
|
|
|
|
|
|
|
public componentWillUnmount() {
|
|
|
|
if (this.expirationCheckInterval) {
|
|
|
|
clearInterval(this.expirationCheckInterval);
|
|
|
|
}
|
|
|
|
if (this.expiredTimeout) {
|
|
|
|
clearTimeout(this.expiredTimeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-09 23:18:10 +00:00
|
|
|
public componentDidUpdate() {
|
|
|
|
this.checkExpired();
|
|
|
|
}
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
public checkExpired() {
|
|
|
|
const now = Date.now();
|
2018-08-09 23:18:10 +00:00
|
|
|
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
|
|
|
if (!expirationTimestamp || !expirationLength) {
|
|
|
|
return;
|
|
|
|
}
|
2018-08-09 23:18:10 +00:00
|
|
|
if (this.expiredTimeout) {
|
|
|
|
return;
|
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2018-08-09 23:18:10 +00:00
|
|
|
if (isExpired || now >= expirationTimestamp) {
|
2018-07-09 21:29:13 +00:00
|
|
|
this.setState({
|
|
|
|
expiring: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const setExpired = () => {
|
|
|
|
this.setState({
|
|
|
|
expired: true,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
this.expiredTimeout = setTimeout(setExpired, EXPIRED_DELAY);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public handleImageError() {
|
|
|
|
// tslint:disable-next-line no-console
|
|
|
|
console.log('Message: Image failed to load; failing over to placeholder');
|
|
|
|
this.setState({
|
|
|
|
imageBroken: true,
|
|
|
|
});
|
2018-06-27 20:53:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public renderMetadata() {
|
|
|
|
const {
|
|
|
|
collapseMetadata,
|
|
|
|
direction,
|
2018-07-09 21:29:13 +00:00
|
|
|
expirationLength,
|
|
|
|
expirationTimestamp,
|
2018-06-27 20:53:49 +00:00
|
|
|
i18n,
|
|
|
|
status,
|
|
|
|
text,
|
2018-07-09 21:29:13 +00:00
|
|
|
timestamp,
|
2018-06-27 20:53:49 +00:00
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
if (collapseMetadata) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-01-16 03:03:56 +00:00
|
|
|
const isShowingImage = this.isShowingImage();
|
|
|
|
const withImageNoCaption = Boolean(!text && isShowingImage);
|
2018-07-09 21:29:13 +00:00
|
|
|
const showError = status === 'error' && direction === 'outgoing';
|
2018-06-27 20:53:49 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-06-27 20:53:49 +00:00
|
|
|
'module-message__metadata',
|
|
|
|
withImageNoCaption
|
|
|
|
? 'module-message__metadata--with-image-no-caption'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
>
|
2018-07-09 21:29:13 +00:00
|
|
|
{showError ? (
|
|
|
|
<span
|
|
|
|
className={classNames(
|
|
|
|
'module-message__metadata__date',
|
|
|
|
`module-message__metadata__date--${direction}`,
|
|
|
|
withImageNoCaption
|
|
|
|
? 'module-message__metadata__date--with-image-no-caption'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{i18n('sendFailed')}
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
<Timestamp
|
|
|
|
i18n={i18n}
|
|
|
|
timestamp={timestamp}
|
2018-07-18 03:25:55 +00:00
|
|
|
extended={true}
|
2018-07-09 21:29:13 +00:00
|
|
|
direction={direction}
|
|
|
|
withImageNoCaption={withImageNoCaption}
|
|
|
|
module="module-message__metadata__date"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
{expirationLength && expirationTimestamp ? (
|
|
|
|
<ExpireTimer
|
|
|
|
direction={direction}
|
|
|
|
expirationLength={expirationLength}
|
|
|
|
expirationTimestamp={expirationTimestamp}
|
|
|
|
withImageNoCaption={withImageNoCaption}
|
|
|
|
/>
|
|
|
|
) : null}
|
2018-06-27 20:53:49 +00:00
|
|
|
<span className="module-message__metadata__spacer" />
|
2018-07-09 21:29:13 +00:00
|
|
|
{direction === 'outgoing' && status !== 'error' ? (
|
2018-06-27 20:53:49 +00:00
|
|
|
<div
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-06-27 20:53:49 +00:00
|
|
|
'module-message__metadata__status-icon',
|
2018-07-09 21:29:13 +00:00
|
|
|
`module-message__metadata__status-icon--${status}`,
|
2018-06-27 20:53:49 +00:00
|
|
|
withImageNoCaption
|
|
|
|
? 'module-message__metadata__status-icon--with-image-no-caption'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public renderAuthor() {
|
|
|
|
const {
|
|
|
|
authorName,
|
2018-07-09 21:29:13 +00:00
|
|
|
authorPhoneNumber,
|
|
|
|
authorProfileName,
|
2018-06-27 20:53:49 +00:00
|
|
|
conversationType,
|
|
|
|
direction,
|
|
|
|
i18n,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const title = authorName ? authorName : authorPhoneNumber;
|
|
|
|
|
|
|
|
if (direction !== 'incoming' || conversationType !== 'group' || !title) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="module-message__author">
|
2018-07-09 21:29:13 +00:00
|
|
|
<ContactName
|
|
|
|
phoneNumber={authorPhoneNumber}
|
|
|
|
name={authorName}
|
|
|
|
profileName={authorProfileName}
|
|
|
|
module="module-message__author"
|
|
|
|
i18n={i18n}
|
|
|
|
/>
|
2018-06-27 20:53:49 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-10-11 19:24:58 +00:00
|
|
|
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
2018-06-27 20:53:49 +00:00
|
|
|
public renderAttachment() {
|
|
|
|
const {
|
2018-11-14 18:47:19 +00:00
|
|
|
attachments,
|
2018-06-27 20:53:49 +00:00
|
|
|
text,
|
|
|
|
collapseMetadata,
|
|
|
|
conversationType,
|
|
|
|
direction,
|
2018-11-14 18:47:19 +00:00
|
|
|
i18n,
|
2018-06-27 20:53:49 +00:00
|
|
|
quote,
|
|
|
|
onClickAttachment,
|
|
|
|
} = this.props;
|
2018-07-09 21:29:13 +00:00
|
|
|
const { imageBroken } = this.state;
|
2018-06-27 20:53:49 +00:00
|
|
|
|
2018-11-14 18:47:19 +00:00
|
|
|
if (!attachments || !attachments[0]) {
|
2018-06-27 20:53:49 +00:00
|
|
|
return null;
|
|
|
|
}
|
2018-11-14 18:47:19 +00:00
|
|
|
const firstAttachment = attachments[0];
|
2018-06-27 20:53:49 +00:00
|
|
|
|
|
|
|
// For attachments which aren't full-frame
|
2018-11-14 18:47:19 +00:00
|
|
|
const withContentBelow = Boolean(text);
|
2018-06-27 20:53:49 +00:00
|
|
|
const withContentAbove =
|
2018-11-14 18:47:19 +00:00
|
|
|
Boolean(quote) ||
|
|
|
|
(conversationType === 'group' && direction === 'incoming');
|
|
|
|
const displayImage = canDisplayImage(attachments);
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2018-11-14 18:47:19 +00:00
|
|
|
if (
|
2018-09-05 19:07:53 +00:00
|
|
|
displayImage &&
|
|
|
|
!imageBroken &&
|
2018-11-14 18:47:19 +00:00
|
|
|
((isImage(attachments) && hasImage(attachments)) ||
|
|
|
|
(isVideo(attachments) && hasVideoScreenshot(attachments)))
|
2018-09-05 19:07:53 +00:00
|
|
|
) {
|
2018-06-27 20:53:49 +00:00
|
|
|
return (
|
2018-07-09 21:29:13 +00:00
|
|
|
<div
|
2018-08-01 22:40:13 +00:00
|
|
|
className={classNames(
|
|
|
|
'module-message__attachment-container',
|
|
|
|
withContentAbove
|
|
|
|
? 'module-message__attachment-container--with-content-above'
|
2018-11-14 18:47:19 +00:00
|
|
|
: null,
|
|
|
|
withContentBelow
|
|
|
|
? 'module-message__attachment-container--with-content-below'
|
2018-08-01 22:40:13 +00:00
|
|
|
: null
|
|
|
|
)}
|
2018-06-27 20:53:49 +00:00
|
|
|
>
|
2018-11-14 18:47:19 +00:00
|
|
|
<ImageGrid
|
|
|
|
attachments={attachments}
|
|
|
|
withContentAbove={withContentAbove}
|
|
|
|
withContentBelow={withContentBelow}
|
|
|
|
bottomOverlay={!collapseMetadata}
|
|
|
|
i18n={i18n}
|
2018-07-09 21:29:13 +00:00
|
|
|
onError={this.handleImageErrorBound}
|
2018-11-14 18:47:19 +00:00
|
|
|
onClickAttachment={onClickAttachment}
|
2018-07-09 21:29:13 +00:00
|
|
|
/>
|
|
|
|
</div>
|
2018-06-27 20:53:49 +00:00
|
|
|
);
|
2019-01-30 20:15:07 +00:00
|
|
|
} else if (!firstAttachment.pending && isAudio(attachments)) {
|
2018-06-27 20:53:49 +00:00
|
|
|
return (
|
|
|
|
<audio
|
|
|
|
controls={true}
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-06-27 20:53:49 +00:00
|
|
|
'module-message__audio-attachment',
|
|
|
|
withContentBelow
|
|
|
|
? 'module-message__audio-attachment--with-content-below'
|
|
|
|
: null,
|
|
|
|
withContentAbove
|
|
|
|
? 'module-message__audio-attachment--with-content-above'
|
|
|
|
: null
|
|
|
|
)}
|
2019-01-30 20:15:07 +00:00
|
|
|
key={firstAttachment.url}
|
2018-06-27 20:53:49 +00:00
|
|
|
>
|
2018-11-14 18:47:19 +00:00
|
|
|
<source src={firstAttachment.url} />
|
2018-06-27 20:53:49 +00:00
|
|
|
</audio>
|
|
|
|
);
|
|
|
|
} else {
|
2019-01-30 20:15:07 +00:00
|
|
|
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
2018-06-27 20:53:49 +00:00
|
|
|
const extension = getExtension({ contentType, fileName });
|
2018-10-15 18:54:50 +00:00
|
|
|
const isDangerous = isFileDangerous(fileName || '');
|
2018-06-27 20:53:49 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-06-27 20:53:49 +00:00
|
|
|
'module-message__generic-attachment',
|
|
|
|
withContentBelow
|
|
|
|
? 'module-message__generic-attachment--with-content-below'
|
|
|
|
: null,
|
|
|
|
withContentAbove
|
|
|
|
? 'module-message__generic-attachment--with-content-above'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
>
|
2019-01-30 20:15:07 +00:00
|
|
|
{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" />
|
2018-10-04 01:12:42 +00:00
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</div>
|
2019-01-30 20:15:07 +00:00
|
|
|
)}
|
2018-06-27 20:53:49 +00:00
|
|
|
<div className="module-message__generic-attachment__text">
|
|
|
|
<div
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-06-27 20:53:49 +00:00
|
|
|
'module-message__generic-attachment__file-name',
|
|
|
|
`module-message__generic-attachment__file-name--${direction}`
|
|
|
|
)}
|
2018-04-03 22:56:12 +00:00
|
|
|
>
|
2018-06-27 20:53:49 +00:00
|
|
|
{fileName}
|
|
|
|
</div>
|
|
|
|
<div
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-06-27 20:53:49 +00:00
|
|
|
'module-message__generic-attachment__file-size',
|
|
|
|
`module-message__generic-attachment__file-size--${direction}`
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{fileSize}
|
|
|
|
</div>
|
2018-04-03 22:56:12 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2018-06-27 20:53:49 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-16 03:03:56 +00:00
|
|
|
// tslint:disable-next-line cyclomatic-complexity
|
|
|
|
public renderPreview() {
|
|
|
|
const {
|
|
|
|
attachments,
|
|
|
|
conversationType,
|
|
|
|
direction,
|
|
|
|
i18n,
|
|
|
|
onClickLinkPreview,
|
|
|
|
previews,
|
|
|
|
quote,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
// Attachments take precedence over Link Previews
|
|
|
|
if (attachments && attachments.length) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!previews || previews.length < 1) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const first = previews[0];
|
|
|
|
if (!first) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const withContentAbove =
|
|
|
|
Boolean(quote) ||
|
|
|
|
(conversationType === 'group' && direction === 'incoming');
|
|
|
|
|
|
|
|
const previewHasImage = first.image && isImageAttachment(first.image);
|
|
|
|
const width = first.image && first.image.width;
|
|
|
|
const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
role="button"
|
|
|
|
className={classNames(
|
|
|
|
'module-message__link-preview',
|
|
|
|
withContentAbove
|
|
|
|
? 'module-message__link-preview--with-content-above'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
onClick={() => {
|
|
|
|
if (onClickLinkPreview) {
|
|
|
|
onClickLinkPreview(first.url);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{first.image && previewHasImage && isFullSizeImage ? (
|
|
|
|
<ImageGrid
|
|
|
|
attachments={[first.image]}
|
|
|
|
withContentAbove={withContentAbove}
|
|
|
|
withContentBelow={true}
|
|
|
|
onError={this.handleImageErrorBound}
|
|
|
|
i18n={i18n}
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'module-message__link-preview__content',
|
|
|
|
withContentAbove || isFullSizeImage
|
|
|
|
? 'module-message__link-preview__content--with-content-above'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{first.image && previewHasImage && !isFullSizeImage ? (
|
|
|
|
<div className="module-message__link-preview__icon_container">
|
|
|
|
<Image
|
|
|
|
smallCurveTopLeft={!withContentAbove}
|
|
|
|
softCorners={true}
|
|
|
|
alt={i18n('previewThumbnail', [first.domain])}
|
|
|
|
height={72}
|
|
|
|
width={72}
|
|
|
|
url={first.image.url}
|
|
|
|
attachment={first.image}
|
|
|
|
onError={this.handleImageErrorBound}
|
|
|
|
i18n={i18n}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'module-message__link-preview__text',
|
|
|
|
previewHasImage && !isFullSizeImage
|
|
|
|
? 'module-message__link-preview__text--with-icon'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
<div className="module-message__link-preview__title">
|
|
|
|
{first.title}
|
|
|
|
</div>
|
|
|
|
<div className="module-message__link-preview__location">
|
|
|
|
{first.domain}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-06-27 20:53:49 +00:00
|
|
|
public renderQuote() {
|
2018-10-18 18:57:10 +00:00
|
|
|
const {
|
|
|
|
conversationType,
|
|
|
|
authorColor,
|
|
|
|
direction,
|
|
|
|
i18n,
|
|
|
|
quote,
|
|
|
|
} = this.props;
|
2018-06-27 20:53:49 +00:00
|
|
|
|
|
|
|
if (!quote) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const withContentAbove =
|
|
|
|
conversationType === 'group' && direction === 'incoming';
|
2018-10-18 18:57:10 +00:00
|
|
|
const quoteColor =
|
|
|
|
direction === 'incoming' ? authorColor : quote.authorColor;
|
2018-06-27 20:53:49 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Quote
|
|
|
|
i18n={i18n}
|
2018-07-09 21:29:13 +00:00
|
|
|
onClick={quote.onClick}
|
2018-06-27 20:53:49 +00:00
|
|
|
text={quote.text}
|
2018-07-09 21:29:13 +00:00
|
|
|
attachment={quote.attachment}
|
2018-06-27 20:53:49 +00:00
|
|
|
isIncoming={direction === 'incoming'}
|
2018-07-09 21:29:13 +00:00
|
|
|
authorPhoneNumber={quote.authorPhoneNumber}
|
|
|
|
authorProfileName={quote.authorProfileName}
|
|
|
|
authorName={quote.authorName}
|
2018-10-18 18:57:10 +00:00
|
|
|
authorColor={quoteColor}
|
2018-08-15 19:31:29 +00:00
|
|
|
referencedMessageNotFound={quote.referencedMessageNotFound}
|
2018-06-27 20:53:49 +00:00
|
|
|
isFromMe={quote.isFromMe}
|
|
|
|
withContentAbove={withContentAbove}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public renderEmbeddedContact() {
|
|
|
|
const {
|
|
|
|
collapseMetadata,
|
2018-07-09 21:29:13 +00:00
|
|
|
contact,
|
2018-06-27 20:53:49 +00:00
|
|
|
conversationType,
|
|
|
|
direction,
|
|
|
|
i18n,
|
|
|
|
text,
|
|
|
|
} = this.props;
|
2018-07-09 21:29:13 +00:00
|
|
|
if (!contact) {
|
2018-06-27 20:53:49 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const withCaption = Boolean(text);
|
|
|
|
const withContentAbove =
|
|
|
|
conversationType === 'group' && direction === 'incoming';
|
|
|
|
const withContentBelow = withCaption || !collapseMetadata;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<EmbeddedContact
|
2018-07-09 21:29:13 +00:00
|
|
|
contact={contact}
|
|
|
|
hasSignalAccount={contact.hasSignalAccount}
|
2018-06-27 20:53:49 +00:00
|
|
|
isIncoming={direction === 'incoming'}
|
|
|
|
i18n={i18n}
|
2018-07-09 21:29:13 +00:00
|
|
|
onClick={contact.onClick}
|
2018-06-27 20:53:49 +00:00
|
|
|
withContentAbove={withContentAbove}
|
|
|
|
withContentBelow={withContentBelow}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public renderSendMessageButton() {
|
2018-07-09 21:29:13 +00:00
|
|
|
const { contact, i18n } = this.props;
|
|
|
|
if (!contact || !contact.hasSignalAccount) {
|
2018-06-27 20:53:49 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
role="button"
|
2018-07-09 21:29:13 +00:00
|
|
|
onClick={contact.onSendMessage}
|
2018-06-27 20:53:49 +00:00
|
|
|
className="module-message__send-message-button"
|
|
|
|
>
|
|
|
|
{i18n('sendMessageToContact')}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public renderAvatar() {
|
|
|
|
const {
|
2018-09-27 00:23:17 +00:00
|
|
|
authorAvatarPath,
|
2018-06-27 20:53:49 +00:00
|
|
|
authorName,
|
|
|
|
authorPhoneNumber,
|
|
|
|
authorProfileName,
|
|
|
|
collapseMetadata,
|
2018-10-09 22:56:14 +00:00
|
|
|
authorColor,
|
2018-06-27 20:53:49 +00:00
|
|
|
conversationType,
|
|
|
|
direction,
|
|
|
|
i18n,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
if (
|
|
|
|
collapseMetadata ||
|
|
|
|
conversationType !== 'group' ||
|
|
|
|
direction === 'outgoing'
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="module-message__author-avatar">
|
2018-09-27 00:23:17 +00:00
|
|
|
<Avatar
|
|
|
|
avatarPath={authorAvatarPath}
|
2018-10-09 22:56:14 +00:00
|
|
|
color={authorColor}
|
2018-09-27 00:23:17 +00:00
|
|
|
conversationType="direct"
|
|
|
|
i18n={i18n}
|
|
|
|
name={authorName}
|
|
|
|
phoneNumber={authorPhoneNumber}
|
|
|
|
profileName={authorProfileName}
|
|
|
|
size={36}
|
|
|
|
/>
|
2018-06-27 20:53:49 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public renderText() {
|
2018-07-09 21:29:13 +00:00
|
|
|
const { text, i18n, direction, status } = this.props;
|
|
|
|
|
|
|
|
const contents =
|
|
|
|
direction === 'incoming' && status === 'error'
|
|
|
|
? i18n('incomingError')
|
|
|
|
: text;
|
2018-06-27 20:53:49 +00:00
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
if (!contents) {
|
2018-06-27 20:53:49 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
2018-07-27 17:42:26 +00:00
|
|
|
dir="auto"
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-06-27 20:53:49 +00:00
|
|
|
'module-message__text',
|
2018-07-09 21:29:13 +00:00
|
|
|
`module-message__text--${direction}`,
|
|
|
|
status === 'error' && direction === 'incoming'
|
|
|
|
? 'module-message__text--error'
|
|
|
|
: null
|
2018-06-27 20:53:49 +00:00
|
|
|
)}
|
|
|
|
>
|
2018-07-09 21:29:13 +00:00
|
|
|
<MessageBody text={contents || ''} i18n={i18n} />
|
2018-06-27 20:53:49 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
public renderError(isCorrectSide: boolean) {
|
|
|
|
const { status, direction } = this.props;
|
|
|
|
|
|
|
|
if (!isCorrectSide || status !== 'error') {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="module-message__error-container">
|
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'module-message__error',
|
|
|
|
`module-message__error--${direction}`
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public captureMenuTrigger(triggerRef: Trigger) {
|
|
|
|
this.menuTriggerRef = triggerRef;
|
|
|
|
}
|
|
|
|
public showMenu(event: React.MouseEvent<HTMLDivElement>) {
|
|
|
|
if (this.menuTriggerRef) {
|
|
|
|
this.menuTriggerRef.handleContextClick(event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public renderMenu(isCorrectSide: boolean, triggerId: string) {
|
2018-06-27 20:53:49 +00:00
|
|
|
const {
|
2018-11-14 18:47:19 +00:00
|
|
|
attachments,
|
2018-07-09 21:29:13 +00:00
|
|
|
direction,
|
|
|
|
disableMenu,
|
|
|
|
onDownload,
|
|
|
|
onReply,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
if (!isCorrectSide || disableMenu) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-11-14 18:47:19 +00:00
|
|
|
const fileName =
|
|
|
|
attachments && attachments[0] ? attachments[0].fileName : null;
|
2018-10-04 01:12:42 +00:00
|
|
|
const isDangerous = isFileDangerous(fileName || '');
|
2018-11-14 18:47:19 +00:00
|
|
|
const multipleAttachments = attachments && attachments.length > 1;
|
2019-01-30 20:15:07 +00:00
|
|
|
const firstAttachment = attachments && attachments[0];
|
2018-10-04 01:12:42 +00:00
|
|
|
|
2018-11-14 18:47:19 +00:00
|
|
|
const downloadButton =
|
2019-01-30 20:15:07 +00:00
|
|
|
!multipleAttachments && firstAttachment && !firstAttachment.pending ? (
|
2018-11-14 18:47:19 +00:00
|
|
|
<div
|
|
|
|
onClick={() => {
|
|
|
|
if (onDownload) {
|
|
|
|
onDownload(isDangerous);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
role="button"
|
|
|
|
className={classNames(
|
|
|
|
'module-message__buttons__download',
|
|
|
|
`module-message__buttons__download--${direction}`
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
) : null;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
|
|
|
const replyButton = (
|
|
|
|
<div
|
|
|
|
onClick={onReply}
|
|
|
|
role="button"
|
|
|
|
className={classNames(
|
|
|
|
'module-message__buttons__reply',
|
|
|
|
`module-message__buttons__download--${direction}`
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
const menuButton = (
|
|
|
|
<ContextMenuTrigger id={triggerId} ref={this.captureMenuTriggerBound}>
|
|
|
|
<div
|
|
|
|
role="button"
|
|
|
|
onClick={this.showMenuBound}
|
|
|
|
className={classNames(
|
|
|
|
'module-message__buttons__menu',
|
|
|
|
`module-message__buttons__download--${direction}`
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
</ContextMenuTrigger>
|
|
|
|
);
|
|
|
|
|
|
|
|
const first = direction === 'incoming' ? downloadButton : menuButton;
|
|
|
|
const last = direction === 'incoming' ? menuButton : downloadButton;
|
|
|
|
|
|
|
|
return (
|
2018-08-11 00:15:00 +00:00
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'module-message__buttons',
|
|
|
|
`module-message__buttons--${direction}`
|
|
|
|
)}
|
|
|
|
>
|
2018-07-09 21:29:13 +00:00
|
|
|
{first}
|
|
|
|
{replyButton}
|
|
|
|
{last}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public renderContextMenu(triggerId: string) {
|
|
|
|
const {
|
2018-11-14 18:47:19 +00:00
|
|
|
attachments,
|
2018-07-09 21:29:13 +00:00
|
|
|
direction,
|
|
|
|
status,
|
|
|
|
onDelete,
|
2018-08-11 00:15:00 +00:00
|
|
|
onDownload,
|
|
|
|
onReply,
|
2018-07-09 21:29:13 +00:00
|
|
|
onRetrySend,
|
|
|
|
onShowDetail,
|
|
|
|
i18n,
|
|
|
|
} = this.props;
|
|
|
|
|
|
|
|
const showRetry = status === 'error' && direction === 'outgoing';
|
2018-11-14 18:47:19 +00:00
|
|
|
const fileName =
|
|
|
|
attachments && attachments[0] ? attachments[0].fileName : null;
|
2018-10-23 21:11:30 +00:00
|
|
|
const isDangerous = isFileDangerous(fileName || '');
|
2018-11-14 18:47:19 +00:00
|
|
|
const multipleAttachments = attachments && attachments.length > 1;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<ContextMenu id={triggerId}>
|
2018-11-14 18:47:19 +00:00
|
|
|
{!multipleAttachments && attachments && attachments[0] ? (
|
2018-08-11 00:15:00 +00:00
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'module-message__context__download',
|
|
|
|
}}
|
2018-10-23 21:11:30 +00:00
|
|
|
onClick={() => {
|
|
|
|
if (onDownload) {
|
|
|
|
onDownload(isDangerous);
|
|
|
|
}
|
|
|
|
}}
|
2018-08-11 00:15:00 +00:00
|
|
|
>
|
|
|
|
{i18n('downloadAttachment')}
|
|
|
|
</MenuItem>
|
|
|
|
) : null}
|
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'module-message__context__reply',
|
|
|
|
}}
|
|
|
|
onClick={onReply}
|
|
|
|
>
|
|
|
|
{i18n('replyToMessage')}
|
|
|
|
</MenuItem>
|
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'module-message__context__more-info',
|
|
|
|
}}
|
|
|
|
onClick={onShowDetail}
|
|
|
|
>
|
|
|
|
{i18n('moreInfo')}
|
|
|
|
</MenuItem>
|
2018-07-09 21:29:13 +00:00
|
|
|
{showRetry ? (
|
2018-08-11 00:15:00 +00:00
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'module-message__context__retry-send',
|
|
|
|
}}
|
|
|
|
onClick={onRetrySend}
|
|
|
|
>
|
|
|
|
{i18n('retrySend')}
|
|
|
|
</MenuItem>
|
2018-07-09 21:29:13 +00:00
|
|
|
) : null}
|
2018-08-11 00:15:00 +00:00
|
|
|
<MenuItem
|
|
|
|
attributes={{
|
|
|
|
className: 'module-message__context__delete-message',
|
|
|
|
}}
|
|
|
|
onClick={onDelete}
|
|
|
|
>
|
|
|
|
{i18n('deleteMessage')}
|
|
|
|
</MenuItem>
|
2018-07-09 21:29:13 +00:00
|
|
|
</ContextMenu>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-01-16 03:03:56 +00:00
|
|
|
public getWidth(): Number | undefined {
|
|
|
|
const { attachments, previews } = this.props;
|
|
|
|
|
|
|
|
if (attachments && attachments.length) {
|
|
|
|
const dimensions = getGridDimensions(attachments);
|
|
|
|
if (dimensions) {
|
|
|
|
return dimensions.width;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (previews && previews.length) {
|
|
|
|
const first = previews[0];
|
|
|
|
|
|
|
|
if (!first || !first.image) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const { width } = first.image;
|
|
|
|
|
|
|
|
if (
|
|
|
|
isImageAttachment(first.image) &&
|
|
|
|
width &&
|
|
|
|
width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH
|
|
|
|
) {
|
|
|
|
const dimensions = getImageDimensions(first.image);
|
|
|
|
if (dimensions) {
|
|
|
|
return dimensions.width;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
public isShowingImage() {
|
|
|
|
const { attachments, previews } = this.props;
|
|
|
|
const { imageBroken } = this.state;
|
|
|
|
|
|
|
|
if (imageBroken) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attachments && attachments.length) {
|
|
|
|
const displayImage = canDisplayImage(attachments);
|
|
|
|
|
|
|
|
return (
|
|
|
|
displayImage &&
|
|
|
|
((isImage(attachments) && hasImage(attachments)) ||
|
|
|
|
(isVideo(attachments) && hasVideoScreenshot(attachments)))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (previews && previews.length) {
|
|
|
|
const first = previews[0];
|
|
|
|
const { image } = first;
|
|
|
|
|
|
|
|
if (!image) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return isImageAttachment(image);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
public render() {
|
|
|
|
const {
|
|
|
|
authorPhoneNumber,
|
2018-10-09 22:56:14 +00:00
|
|
|
authorColor,
|
2018-06-27 20:53:49 +00:00
|
|
|
direction,
|
|
|
|
id,
|
2018-07-09 21:29:13 +00:00
|
|
|
timestamp,
|
2018-06-27 20:53:49 +00:00
|
|
|
} = this.props;
|
2019-01-16 03:03:56 +00:00
|
|
|
const { expired, expiring } = this.state;
|
2018-06-27 20:53:49 +00:00
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
// This id is what connects our triple-dot click with our associated pop-up menu.
|
|
|
|
// It needs to be unique.
|
|
|
|
const triggerId = String(id || `${authorPhoneNumber}-${timestamp}`);
|
|
|
|
|
|
|
|
if (expired) {
|
|
|
|
return null;
|
|
|
|
}
|
2018-06-27 20:53:49 +00:00
|
|
|
|
2019-01-16 03:03:56 +00:00
|
|
|
const width = this.getWidth();
|
|
|
|
const isShowingImage = this.isShowingImage();
|
2018-11-14 18:47:19 +00:00
|
|
|
|
2018-06-27 20:53:49 +00:00
|
|
|
return (
|
2018-07-09 21:29:13 +00:00
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'module-message',
|
|
|
|
`module-message--${direction}`,
|
|
|
|
expiring ? 'module-message--expired' : null
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{this.renderError(direction === 'incoming')}
|
|
|
|
{this.renderMenu(direction === 'outgoing', triggerId)}
|
2018-06-27 20:53:49 +00:00
|
|
|
<div
|
2018-07-07 00:48:14 +00:00
|
|
|
className={classNames(
|
2018-07-09 21:29:13 +00:00
|
|
|
'module-message__container',
|
|
|
|
`module-message__container--${direction}`,
|
2018-10-09 22:56:14 +00:00
|
|
|
direction === 'incoming'
|
|
|
|
? `module-message__container--incoming-${authorColor}`
|
2018-06-27 20:53:49 +00:00
|
|
|
: null
|
|
|
|
)}
|
2019-01-16 03:03:56 +00:00
|
|
|
style={{
|
|
|
|
width: isShowingImage ? width : undefined,
|
|
|
|
}}
|
2018-06-27 20:53:49 +00:00
|
|
|
>
|
|
|
|
{this.renderAuthor()}
|
|
|
|
{this.renderQuote()}
|
|
|
|
{this.renderAttachment()}
|
2019-01-16 03:03:56 +00:00
|
|
|
{this.renderPreview()}
|
2018-06-27 20:53:49 +00:00
|
|
|
{this.renderEmbeddedContact()}
|
|
|
|
{this.renderText()}
|
|
|
|
{this.renderMetadata()}
|
|
|
|
{this.renderSendMessageButton()}
|
|
|
|
{this.renderAvatar()}
|
|
|
|
</div>
|
2018-07-09 21:29:13 +00:00
|
|
|
{this.renderError(direction === 'outgoing')}
|
|
|
|
{this.renderMenu(direction === 'incoming', triggerId)}
|
|
|
|
{this.renderContextMenu(triggerId)}
|
|
|
|
</div>
|
2018-04-03 22:56:12 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2018-12-02 01:48:53 +00:00
|
|
|
|
|
|
|
export function getExtension({
|
|
|
|
fileName,
|
|
|
|
contentType,
|
|
|
|
}: {
|
|
|
|
fileName: string;
|
|
|
|
contentType: MIME.MIMEType;
|
|
|
|
}): string | null {
|
|
|
|
if (fileName && fileName.indexOf('.') >= 0) {
|
|
|
|
const lastPeriod = fileName.lastIndexOf('.');
|
|
|
|
const extension = fileName.slice(lastPeriod + 1);
|
|
|
|
if (extension.length) {
|
|
|
|
return extension;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-30 20:15:07 +00:00
|
|
|
if (!contentType) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-12-02 01:48:53 +00:00
|
|
|
const slash = contentType.indexOf('/');
|
|
|
|
if (slash >= 0) {
|
|
|
|
return contentType.slice(slash + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isAudio(attachments?: Array<AttachmentType>) {
|
|
|
|
return (
|
|
|
|
attachments &&
|
|
|
|
attachments[0] &&
|
|
|
|
attachments[0].contentType &&
|
|
|
|
MIME.isAudio(attachments[0].contentType)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function canDisplayImage(attachments?: Array<AttachmentType>) {
|
|
|
|
const { height, width } =
|
|
|
|
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
|
|
|
|
|
|
|
|
return (
|
|
|
|
height &&
|
|
|
|
height > 0 &&
|
|
|
|
height <= 4096 &&
|
|
|
|
width &&
|
|
|
|
width > 0 &&
|
|
|
|
width <= 4096
|
|
|
|
);
|
|
|
|
}
|