Audio messages: move countdown under waveform
This commit is contained in:
parent
e4efa01073
commit
831ec98418
7 changed files with 338 additions and 215 deletions
|
@ -330,7 +330,7 @@
|
|||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 7px;
|
||||
min-width: 0px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
@ -1046,15 +1046,10 @@
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
margin-bottom: -3px;
|
||||
|
||||
&--outgoing {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&--with-reactions {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
// With an image and no caption, this section needs to be on top of the image overlay
|
||||
|
@ -10607,7 +10602,7 @@ $contact-modal-padding: 18px;
|
|||
}
|
||||
|
||||
&--with-reactions {
|
||||
margin-bottom: -10px;
|
||||
margin-bottom: -9px;
|
||||
}
|
||||
|
||||
&--deleted-for-everyone {
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
$audio-attachment-button-size: 36px;
|
||||
$audio-attachment-button-margin-big: 12px;
|
||||
$audio-attachment-button-margin-small: 4px;
|
||||
|
||||
.module-message__audio-attachment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.module-message__audio-attachment__button-and-waveform {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
margin-top: 2px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* The separator between audio and text */
|
||||
|
@ -16,7 +24,7 @@
|
|||
padding-bottom: 12px;
|
||||
margin-bottom: 7px;
|
||||
|
||||
.module-message__audio-attachment--incoming & {
|
||||
&.module-message__audio-attachment--incoming {
|
||||
@include light-theme {
|
||||
border-color: $color-black-alpha-20;
|
||||
}
|
||||
|
@ -36,15 +44,20 @@
|
|||
|
||||
.module-message__audio-attachment__button,
|
||||
.module-message__audio-attachment__spinner {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
@include button-reset;
|
||||
|
||||
flex-shrink: 0;
|
||||
width: $audio-attachment-button-size;
|
||||
height: $audio-attachment-button-size;
|
||||
margin-right: $audio-attachment-button-margin-big;
|
||||
|
||||
outline: none;
|
||||
border-radius: 18px;
|
||||
|
||||
@media (min-width: 0px) and (max-width: 799px) {
|
||||
margin-right: $audio-attachment-button-margin-small;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
@ -93,7 +106,6 @@
|
|||
|
||||
.module-message__audio-attachment__waveform {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -154,13 +166,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
.module-message__audio-attachment__metadata {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.module-message__audio-attachment--outgoing & {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.module-message__audio-attachment--outgoing &,
|
||||
.module-message__audio-attachment--with-content-below & {
|
||||
margin-left: $audio-attachment-button-size +
|
||||
$audio-attachment-button-margin-big;
|
||||
@media (min-width: 0px) and (max-width: 799px) {
|
||||
margin-left: $audio-attachment-button-size +
|
||||
$audio-attachment-button-margin-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__audio-attachment__countdown {
|
||||
flex-shrink: 1;
|
||||
|
||||
/* Prevent text from jumping when countdown changes */
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
|
||||
user-select: none;
|
||||
|
||||
@include font-caption;
|
||||
|
@ -178,19 +207,3 @@
|
|||
color: $color-white-alpha-80;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 0px) and (max-width: 799px) {
|
||||
.module-message__audio-attachment__waveform {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* Clip the countdown text when it is too long on small screens */
|
||||
.module-message__audio-attachment__countdown {
|
||||
margin-left: 4px;
|
||||
|
||||
max-width: 46px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,10 @@ import {
|
|||
import { Avatar } from '../Avatar';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { MessageBody } from './MessageBody';
|
||||
import { ExpireTimer } from './ExpireTimer';
|
||||
import { MessageMetadata } from './MessageMetadata';
|
||||
import { ImageGrid } from './ImageGrid';
|
||||
import { GIF } from './GIF';
|
||||
import { Image } from './Image';
|
||||
import { Timestamp } from './Timestamp';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Quote, QuotedAttachmentType } from './Quote';
|
||||
import { EmbeddedContact } from './EmbeddedContact';
|
||||
|
@ -88,16 +87,23 @@ export const Directions = ['incoming', 'outgoing'] as const;
|
|||
export type DirectionType = typeof Directions[number];
|
||||
|
||||
export type AudioAttachmentProps = {
|
||||
id: string;
|
||||
renderingContext: string;
|
||||
i18n: LocalizerType;
|
||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||
direction: DirectionType;
|
||||
theme: ThemeType | undefined;
|
||||
attachment: AttachmentType;
|
||||
withContentAbove: boolean;
|
||||
withContentBelow: boolean;
|
||||
|
||||
direction: DirectionType;
|
||||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
id: string;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
textPending?: boolean;
|
||||
timestamp: number;
|
||||
|
||||
kickOffAttachmentDownload(): void;
|
||||
onCorrupted(): void;
|
||||
};
|
||||
|
@ -549,82 +555,9 @@ export class Message extends React.Component<Props, State> {
|
|||
return isMessageRequestAccepted && !isBlocked;
|
||||
}
|
||||
|
||||
public renderTimestamp(): JSX.Element {
|
||||
const {
|
||||
direction,
|
||||
i18n,
|
||||
id,
|
||||
isSticker,
|
||||
isTapToViewExpired,
|
||||
showMessageDetail,
|
||||
status,
|
||||
text,
|
||||
timestamp,
|
||||
} = this.props;
|
||||
|
||||
const isShowingImage = this.isShowingImage();
|
||||
const withImageNoCaption = Boolean(!isSticker && !text && isShowingImage);
|
||||
|
||||
const isError = status === 'error' && direction === 'outgoing';
|
||||
const isPartiallySent =
|
||||
status === 'partial-sent' && direction === 'outgoing';
|
||||
const isPaused = status === 'paused';
|
||||
|
||||
if (isError || isPartiallySent || isPaused) {
|
||||
let statusInfo: React.ReactChild;
|
||||
if (isError) {
|
||||
statusInfo = i18n('sendFailed');
|
||||
} else if (isPaused) {
|
||||
statusInfo = i18n('sendPaused');
|
||||
} else {
|
||||
statusInfo = (
|
||||
<button
|
||||
type="button"
|
||||
className="module-message__metadata__tapable"
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
showMessageDetail(id);
|
||||
}}
|
||||
>
|
||||
{i18n('partiallySent')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames({
|
||||
'module-message__metadata__date': true,
|
||||
'module-message__metadata__date--with-sticker': isSticker,
|
||||
[`module-message__metadata__date--${direction}`]: !isSticker,
|
||||
'module-message__metadata__date--with-image-no-caption': withImageNoCaption,
|
||||
})}
|
||||
>
|
||||
{statusInfo}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const metadataDirection = isSticker ? undefined : direction;
|
||||
|
||||
return (
|
||||
<Timestamp
|
||||
i18n={i18n}
|
||||
timestamp={timestamp}
|
||||
extended
|
||||
direction={metadataDirection}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
module="module-message__metadata__date"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public renderMetadata(): JSX.Element | null {
|
||||
const {
|
||||
attachments,
|
||||
collapseMetadata,
|
||||
direction,
|
||||
expirationLength,
|
||||
|
@ -632,68 +565,40 @@ export class Message extends React.Component<Props, State> {
|
|||
isSticker,
|
||||
isTapToViewExpired,
|
||||
status,
|
||||
i18n,
|
||||
text,
|
||||
textPending,
|
||||
timestamp,
|
||||
id,
|
||||
showMessageDetail,
|
||||
} = this.props;
|
||||
|
||||
if (collapseMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isShowingImage = this.isShowingImage();
|
||||
const withImageNoCaption = Boolean(!isSticker && !text && isShowingImage);
|
||||
const metadataDirection = isSticker ? undefined : direction;
|
||||
// The message audio component renders its own metadata because it positions the
|
||||
// metadata in line with some of its own.
|
||||
if (isAudio(attachments) && !text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
`module-message__metadata--${direction}`,
|
||||
this.hasReactions()
|
||||
? 'module-message__metadata--with-reactions'
|
||||
: null,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata--with-image-no-caption'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{this.renderTimestamp()}
|
||||
{expirationLength && expirationTimestamp ? (
|
||||
<ExpireTimer
|
||||
direction={metadataDirection}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
/>
|
||||
) : null}
|
||||
{textPending ? (
|
||||
<div className="module-message__metadata__spinner-container">
|
||||
<Spinner svgSize="small" size="14px" direction={direction} />
|
||||
</div>
|
||||
) : null}
|
||||
{!textPending &&
|
||||
direction === 'outgoing' &&
|
||||
status !== 'error' &&
|
||||
status !== 'partial-sent' ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata__status-icon',
|
||||
`module-message__metadata__status-icon--${status}`,
|
||||
isSticker
|
||||
? 'module-message__metadata__status-icon--with-sticker'
|
||||
: null,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata__status-icon--with-image-no-caption'
|
||||
: null,
|
||||
isTapToViewExpired
|
||||
? 'module-message__metadata__status-icon--with-tap-to-view-expired'
|
||||
: null
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<MessageMetadata
|
||||
direction={direction}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
hasText={Boolean(text)}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
isShowingImage={this.isShowingImage()}
|
||||
isSticker={isSticker}
|
||||
isTapToViewExpired={isTapToViewExpired}
|
||||
showMessageDetail={showMessageDetail}
|
||||
status={status}
|
||||
textPending={textPending}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -751,19 +656,24 @@ export class Message extends React.Component<Props, State> {
|
|||
collapseMetadata,
|
||||
conversationType,
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
i18n,
|
||||
id,
|
||||
renderingContext,
|
||||
isSticker,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
quote,
|
||||
showVisualAttachment,
|
||||
isSticker,
|
||||
text,
|
||||
theme,
|
||||
reducedMotion,
|
||||
|
||||
renderAudioAttachment,
|
||||
renderingContext,
|
||||
showMessageDetail,
|
||||
showVisualAttachment,
|
||||
status,
|
||||
text,
|
||||
textPending,
|
||||
theme,
|
||||
timestamp,
|
||||
} = this.props;
|
||||
|
||||
const { imageBroken } = this.state;
|
||||
|
@ -851,14 +761,21 @@ export class Message extends React.Component<Props, State> {
|
|||
return renderAudioAttachment({
|
||||
i18n,
|
||||
buttonRef: this.audioButtonRef,
|
||||
id,
|
||||
renderingContext,
|
||||
direction,
|
||||
theme,
|
||||
attachment: firstAttachment,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
id,
|
||||
showMessageDetail,
|
||||
status,
|
||||
textPending,
|
||||
timestamp,
|
||||
|
||||
kickOffAttachmentDownload() {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
|
@ -1698,9 +1615,7 @@ export class Message extends React.Component<Props, State> {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// Messy return here.
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
public isShowingImage() {
|
||||
public isShowingImage(): boolean {
|
||||
const { isTapToView, attachments, previews } = this.props;
|
||||
const { imageBroken } = this.state;
|
||||
|
||||
|
|
|
@ -8,18 +8,28 @@ import { noop } from 'lodash';
|
|||
import { assert } from '../../util/assert';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { hasNotDownloaded, AttachmentType } from '../../types/Attachment';
|
||||
import type { DirectionType, MessageStatusType } from './Message';
|
||||
|
||||
import { ComputePeaksResult } from '../GlobalAudioContext';
|
||||
import { MessageMetadata } from './MessageMetadata';
|
||||
|
||||
export type Props = {
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
id: string;
|
||||
renderingContext: string;
|
||||
i18n: LocalizerType;
|
||||
attachment: AttachmentType;
|
||||
withContentAbove: boolean;
|
||||
withContentBelow: boolean;
|
||||
|
||||
// Message properties. Many are needed for rendering metadata
|
||||
direction: DirectionType;
|
||||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
id: string;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
textPending?: boolean;
|
||||
timestamp: number;
|
||||
|
||||
// See: GlobalAudioContext.tsx
|
||||
audio: HTMLAudioElement;
|
||||
|
||||
|
@ -134,13 +144,20 @@ const Button: React.FC<ButtonProps> = props => {
|
|||
export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
i18n,
|
||||
id,
|
||||
renderingContext,
|
||||
direction,
|
||||
attachment,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
id,
|
||||
showMessageDetail,
|
||||
status,
|
||||
textPending,
|
||||
timestamp,
|
||||
|
||||
buttonRef,
|
||||
kickOffAttachmentDownload,
|
||||
onCorrupted,
|
||||
|
@ -492,6 +509,29 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
|
||||
const countDown = duration - currentTime;
|
||||
|
||||
const metadata = (
|
||||
<div className={`${CSS_BASE}__metadata`}>
|
||||
{!withContentBelow && (
|
||||
<MessageMetadata
|
||||
direction={direction}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
hasText={withContentBelow}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
isShowingImage={false}
|
||||
isSticker={false}
|
||||
isTapToViewExpired={false}
|
||||
showMessageDetail={showMessageDetail}
|
||||
status={status}
|
||||
textPending={textPending}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)}
|
||||
<div className={`${CSS_BASE}__countdown`}>{timeToText(countDown)}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -501,9 +541,11 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
withContentAbove ? `${CSS_BASE}--with-content-above` : null
|
||||
)}
|
||||
>
|
||||
{button}
|
||||
{waveform}
|
||||
<div className={`${CSS_BASE}__countdown`}>{timeToText(countDown)}</div>
|
||||
<div className={`${CSS_BASE}__button-and-waveform`}>
|
||||
{button}
|
||||
{waveform}
|
||||
</div>
|
||||
{metadata}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
155
ts/components/conversation/MessageMetadata.tsx
Normal file
155
ts/components/conversation/MessageMetadata.tsx
Normal file
|
@ -0,0 +1,155 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { FunctionComponent, ReactChild } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import type { DirectionType, MessageStatusType } from './Message';
|
||||
import { ExpireTimer } from './ExpireTimer';
|
||||
import { Timestamp } from './Timestamp';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
||||
type PropsType = {
|
||||
direction: DirectionType;
|
||||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
hasText: boolean;
|
||||
i18n: LocalizerType;
|
||||
id: string;
|
||||
isShowingImage: boolean;
|
||||
isSticker?: boolean;
|
||||
isTapToViewExpired?: boolean;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
textPending?: boolean;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export const MessageMetadata: FunctionComponent<PropsType> = props => {
|
||||
const {
|
||||
direction,
|
||||
expirationLength,
|
||||
expirationTimestamp,
|
||||
hasText,
|
||||
i18n,
|
||||
id,
|
||||
isShowingImage,
|
||||
isSticker,
|
||||
isTapToViewExpired,
|
||||
showMessageDetail,
|
||||
status,
|
||||
textPending,
|
||||
timestamp,
|
||||
} = props;
|
||||
|
||||
const withImageNoCaption = Boolean(!isSticker && !hasText && isShowingImage);
|
||||
const metadataDirection = isSticker ? undefined : direction;
|
||||
|
||||
let timestampNode: ReactChild;
|
||||
{
|
||||
const isError = status === 'error' && direction === 'outgoing';
|
||||
const isPartiallySent =
|
||||
status === 'partial-sent' && direction === 'outgoing';
|
||||
const isPaused = status === 'paused';
|
||||
|
||||
if (isError || isPartiallySent || isPaused) {
|
||||
let statusInfo: React.ReactChild;
|
||||
if (isError) {
|
||||
statusInfo = i18n('sendFailed');
|
||||
} else if (isPaused) {
|
||||
statusInfo = i18n('sendPaused');
|
||||
} else {
|
||||
statusInfo = (
|
||||
<button
|
||||
type="button"
|
||||
className="module-message__metadata__tapable"
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
showMessageDetail(id);
|
||||
}}
|
||||
>
|
||||
{i18n('partiallySent')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
timestampNode = (
|
||||
<span
|
||||
className={classNames({
|
||||
'module-message__metadata__date': true,
|
||||
'module-message__metadata__date--with-sticker': isSticker,
|
||||
[`module-message__metadata__date--${direction}`]: !isSticker,
|
||||
'module-message__metadata__date--with-image-no-caption': withImageNoCaption,
|
||||
})}
|
||||
>
|
||||
{statusInfo}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
timestampNode = (
|
||||
<Timestamp
|
||||
i18n={i18n}
|
||||
timestamp={timestamp}
|
||||
extended
|
||||
direction={metadataDirection}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
module="module-message__metadata__date"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata',
|
||||
`module-message__metadata--${direction}`,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata--with-image-no-caption'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{timestampNode}
|
||||
{expirationLength && expirationTimestamp ? (
|
||||
<ExpireTimer
|
||||
direction={metadataDirection}
|
||||
expirationLength={expirationLength}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
withImageNoCaption={withImageNoCaption}
|
||||
withSticker={isSticker}
|
||||
withTapToViewExpired={isTapToViewExpired}
|
||||
/>
|
||||
) : null}
|
||||
{textPending ? (
|
||||
<div className="module-message__metadata__spinner-container">
|
||||
<Spinner svgSize="small" size="14px" direction={direction} />
|
||||
</div>
|
||||
) : null}
|
||||
{!textPending &&
|
||||
direction === 'outgoing' &&
|
||||
status !== 'error' &&
|
||||
status !== 'partial-sent' ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__metadata__status-icon',
|
||||
`module-message__metadata__status-icon--${status}`,
|
||||
isSticker
|
||||
? 'module-message__metadata__status-icon--with-sticker'
|
||||
: null,
|
||||
withImageNoCaption
|
||||
? 'module-message__metadata__status-icon--with-image-no-caption'
|
||||
: null,
|
||||
isTapToViewExpired
|
||||
? 'module-message__metadata__status-icon--with-tap-to-view-expired'
|
||||
: null
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -10,18 +10,29 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { StateType } from '../reducer';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
import type {
|
||||
DirectionType,
|
||||
MessageStatusType,
|
||||
} from '../../components/conversation/Message';
|
||||
|
||||
export type Props = {
|
||||
audio: HTMLAudioElement;
|
||||
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
id: string;
|
||||
renderingContext: string;
|
||||
i18n: LocalizerType;
|
||||
attachment: AttachmentType;
|
||||
withContentAbove: boolean;
|
||||
withContentBelow: boolean;
|
||||
|
||||
direction: DirectionType;
|
||||
expirationLength?: number;
|
||||
expirationTimestamp?: number;
|
||||
id: string;
|
||||
showMessageDetail: (id: string) => void;
|
||||
status?: MessageStatusType;
|
||||
textPending?: boolean;
|
||||
timestamp: number;
|
||||
|
||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||
|
||||
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
||||
|
|
|
@ -118,31 +118,27 @@ export function getExtensionForDisplay({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export function isAudio(
|
||||
attachments?: Array<AttachmentType>
|
||||
): boolean | undefined {
|
||||
return (
|
||||
export function isAudio(attachments?: Array<AttachmentType>): boolean {
|
||||
return Boolean(
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
!attachments[0].isCorrupted &&
|
||||
MIME.isAudio(attachments[0].contentType)
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
!attachments[0].isCorrupted &&
|
||||
MIME.isAudio(attachments[0].contentType)
|
||||
);
|
||||
}
|
||||
|
||||
export function canDisplayImage(
|
||||
attachments?: Array<AttachmentType>
|
||||
): boolean | 0 | undefined {
|
||||
export function canDisplayImage(attachments?: Array<AttachmentType>): boolean {
|
||||
const { height, width } =
|
||||
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
|
||||
|
||||
return (
|
||||
return Boolean(
|
||||
height &&
|
||||
height > 0 &&
|
||||
height <= 4096 &&
|
||||
width &&
|
||||
width > 0 &&
|
||||
width <= 4096
|
||||
height > 0 &&
|
||||
height <= 4096 &&
|
||||
width &&
|
||||
width > 0 &&
|
||||
width <= 4096
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -164,14 +160,12 @@ export function getUrl(attachment: AttachmentType): string | undefined {
|
|||
return attachment.url;
|
||||
}
|
||||
|
||||
export function isImage(
|
||||
attachments?: Array<AttachmentType>
|
||||
): boolean | undefined {
|
||||
return (
|
||||
export function isImage(attachments?: Array<AttachmentType>): boolean {
|
||||
return Boolean(
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
isImageTypeSupported(attachments[0].contentType)
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
isImageTypeSupported(attachments[0].contentType)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -193,13 +187,11 @@ export function canBeTranscoded(
|
|||
);
|
||||
}
|
||||
|
||||
export function hasImage(
|
||||
attachments?: Array<AttachmentType>
|
||||
): string | boolean | undefined {
|
||||
return (
|
||||
export function hasImage(attachments?: Array<AttachmentType>): boolean {
|
||||
return Boolean(
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
(attachments[0].url || attachments[0].pending || attachments[0].blurHash)
|
||||
attachments[0] &&
|
||||
(attachments[0].url || attachments[0].pending || attachments[0].blurHash)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue