Voice-note animation fixes
This commit is contained in:
parent
56f8842ed2
commit
458eb2ea81
12 changed files with 147 additions and 104 deletions
|
@ -162,6 +162,7 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
|
|||
if (!playing) {
|
||||
audio.play();
|
||||
setPlaying(true);
|
||||
setPlayed(true);
|
||||
}
|
||||
|
||||
if (!Number.isNaN(audio.duration)) {
|
||||
|
@ -195,10 +196,6 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
|
|||
? { playing, playbackRate, currentTime, duration: audio.duration }
|
||||
: undefined;
|
||||
|
||||
const setPlayedAction = () => {
|
||||
setPlayed(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<MessageAudio
|
||||
{...props}
|
||||
|
@ -208,7 +205,6 @@ const MessageAudioContainer: React.FC<AudioAttachmentProps> = ({
|
|||
active={active}
|
||||
played={_played}
|
||||
loadAndPlayMessageAudio={loadAndPlayMessageAudio}
|
||||
onFirstPlayed={setPlayedAction}
|
||||
setIsPlaying={setIsPlayingAction}
|
||||
setPlaybackRate={setPlaybackRateAction}
|
||||
setCurrentTime={setCurrentTimeAction}
|
||||
|
|
|
@ -18,7 +18,7 @@ import type {
|
|||
} from '../../state/ducks/conversations';
|
||||
import type { ViewStoryActionCreatorType } from '../../state/ducks/stories';
|
||||
import type { TimelineItemType } from './TimelineItem';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import type { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { AvatarSpacer } from '../AvatarSpacer';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
@ -61,6 +61,7 @@ import {
|
|||
isImageAttachment,
|
||||
isVideo,
|
||||
isGIF,
|
||||
isPlayed,
|
||||
} from '../../types/Attachment';
|
||||
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
|
||||
|
||||
|
@ -179,7 +180,6 @@ export type AudioAttachmentProps = {
|
|||
|
||||
kickOffAttachmentDownload(): void;
|
||||
onCorrupted(): void;
|
||||
onFirstPlayed(): void;
|
||||
};
|
||||
|
||||
export enum GiftBadgeStates {
|
||||
|
@ -902,7 +902,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
isSticker,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
markViewed,
|
||||
quote,
|
||||
readStatus,
|
||||
reducedMotion,
|
||||
|
@ -1017,19 +1016,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
if (isAudio(attachments)) {
|
||||
let played: boolean;
|
||||
switch (direction) {
|
||||
case 'outgoing':
|
||||
played = status === 'viewed';
|
||||
break;
|
||||
case 'incoming':
|
||||
played = readStatus === ReadStatus.Viewed;
|
||||
break;
|
||||
default:
|
||||
log.error(missingCaseError(direction));
|
||||
played = false;
|
||||
break;
|
||||
}
|
||||
const played = isPlayed(direction, status, readStatus);
|
||||
|
||||
return renderAudioAttachment({
|
||||
i18n,
|
||||
|
@ -1064,9 +1051,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
messageId: id,
|
||||
});
|
||||
},
|
||||
onFirstPlayed() {
|
||||
markViewed(id);
|
||||
},
|
||||
});
|
||||
}
|
||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useRef, useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
|
@ -38,7 +38,6 @@ export type OwnProps = Readonly<{
|
|||
timestamp: number;
|
||||
kickOffAttachmentDownload(): void;
|
||||
onCorrupted(): void;
|
||||
onFirstPlayed(): void;
|
||||
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
||||
}>;
|
||||
|
||||
|
@ -63,6 +62,7 @@ type ButtonProps = {
|
|||
mod?: string;
|
||||
label: string;
|
||||
visible?: boolean;
|
||||
animateClick?: boolean;
|
||||
onClick: () => void;
|
||||
onMouseDown?: () => void;
|
||||
onMouseUp?: () => void;
|
||||
|
@ -91,7 +91,7 @@ const BIG_INCREMENT = 5;
|
|||
|
||||
const PLAYBACK_RATES = [1, 1.5, 2, 0.5];
|
||||
|
||||
const SPRING_DEFAULTS = {
|
||||
const SPRING_CONFIG = {
|
||||
mass: 0.5,
|
||||
tension: 350,
|
||||
friction: 20,
|
||||
|
@ -131,33 +131,42 @@ const Button: React.FC<ButtonProps> = props => {
|
|||
children,
|
||||
onClick,
|
||||
visible = true,
|
||||
animateClick = true,
|
||||
} = props;
|
||||
const [isDown, setIsDown] = useState(false);
|
||||
|
||||
const animProps = useSpring({
|
||||
...SPRING_DEFAULTS,
|
||||
from: isDown ? { scale: 1 } : { scale: 0 },
|
||||
to: isDown ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
|
||||
});
|
||||
const [animProps] = useSpring(
|
||||
{
|
||||
config: SPRING_CONFIG,
|
||||
to: isDown && animateClick ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
|
||||
},
|
||||
[visible, isDown, animateClick]
|
||||
);
|
||||
|
||||
// Clicking button toggle playback
|
||||
const onButtonClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const onButtonClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClick();
|
||||
};
|
||||
onClick();
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
// Keyboard playback toggle
|
||||
const onButtonKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const onButtonKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClick();
|
||||
};
|
||||
onClick();
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<animated.div style={animProps}>
|
||||
|
@ -193,7 +202,7 @@ const PlayedDot = ({
|
|||
|
||||
const [animProps] = useSpring(
|
||||
{
|
||||
...SPRING_DEFAULTS,
|
||||
config: SPRING_CONFIG,
|
||||
from: { scale: start, opacity: start, width: start },
|
||||
to: { scale: end, opacity: end, width: end * DOT_DIV_WIDTH },
|
||||
onRest: () => {
|
||||
|
@ -253,7 +262,6 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
|
||||
kickOffAttachmentDownload,
|
||||
onCorrupted,
|
||||
onFirstPlayed,
|
||||
computePeaks,
|
||||
setPlaybackRate,
|
||||
loadAndPlayMessageAudio,
|
||||
|
@ -366,12 +374,6 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!played && isPlaying) {
|
||||
onFirstPlayed();
|
||||
}
|
||||
}, [played, isPlaying, onFirstPlayed]);
|
||||
|
||||
// Clicking waveform moves playback head position and starts playback.
|
||||
const onWaveformClick = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
|
@ -508,6 +510,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
variant="play"
|
||||
mod="download"
|
||||
label="MessageAudio--download"
|
||||
animateClick={false}
|
||||
onClick={kickOffAttachmentDownload}
|
||||
/>
|
||||
);
|
||||
|
@ -519,6 +522,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
variant="play"
|
||||
mod={isPlaying ? 'pause' : 'play'}
|
||||
label={isPlaying ? 'MessageAudio--pause' : 'MessageAudio--play'}
|
||||
animateClick={false}
|
||||
onClick={toggleIsPlaying}
|
||||
/>
|
||||
);
|
||||
|
@ -561,7 +565,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
|||
variant="playback-rate"
|
||||
i18n={i18n}
|
||||
label={(active && playbackRateLabels[active.playbackRate]) ?? ''}
|
||||
visible={isPlaying && (!played || (played && !isPlayedDotVisible))}
|
||||
visible={isPlaying && (!played || !isPlayedDotVisible)}
|
||||
onClick={() => {
|
||||
if (active) {
|
||||
setPlaybackRate(
|
||||
|
|
|
@ -65,6 +65,7 @@ export type PropsData = {
|
|||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
markViewed: (messageId: string) => void;
|
||||
} & Pick<
|
||||
MessagePropsType,
|
||||
| 'getPreferredBadge'
|
||||
|
@ -78,7 +79,6 @@ export type PropsBackboneActions = Pick<
|
|||
| 'displayTapToViewMessage'
|
||||
| 'kickOffAttachmentDownload'
|
||||
| 'markAttachmentAsCorrupted'
|
||||
| 'markViewed'
|
||||
| 'openConversation'
|
||||
| 'openGiftBadge'
|
||||
| 'openLink'
|
||||
|
|
|
@ -22,11 +22,15 @@ import type {
|
|||
import {
|
||||
SELECTED_CONVERSATION_CHANGED,
|
||||
setVoiceNotePlaybackRate,
|
||||
markViewed,
|
||||
} from './conversations';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { globalMessageAudio } from '../../services/globalMessageAudio';
|
||||
import { isPlayed } from '../../types/Attachment';
|
||||
import { getMessageIdForLogging } from '../../util/idForLogging';
|
||||
import { getMessagePropStatus } from '../selectors/message';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -254,6 +258,33 @@ function loadAndPlayMessageAudio(
|
|||
},
|
||||
});
|
||||
|
||||
// mark the message as played
|
||||
const message = getState().conversations.messagesLookup[id];
|
||||
if (message) {
|
||||
const messageIdForLogging = getMessageIdForLogging(message);
|
||||
const status = getMessagePropStatus(message, message.conversationId);
|
||||
|
||||
if (message.type === 'incoming' || message.type === 'outgoing') {
|
||||
if (!isPlayed(message.type, status, message.readStatus)) {
|
||||
markViewed(id);
|
||||
} else {
|
||||
log.info(
|
||||
'audioPlayer.loadAndPlayMessageAudio: message already played',
|
||||
{ message: messageIdForLogging }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
`audioPlayer.loadAndPlayMessageAudio: message wrong type: ${message.type}`,
|
||||
{ message: messageIdForLogging }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.warn('audioPlayer.loadAndPlayMessageAudio: message not found', {
|
||||
message: id,
|
||||
});
|
||||
}
|
||||
|
||||
// set the playback rate to the stored value for the selected conversation
|
||||
const conversationId = getSelectedConversationId(getState());
|
||||
if (conversationId) {
|
||||
|
|
|
@ -76,6 +76,7 @@ import {
|
|||
OneTimeModalState,
|
||||
UsernameSaveState,
|
||||
} from './conversationsEnums';
|
||||
import { markViewed as messageUpdaterMarkViewed } from '../../services/MessageUpdater';
|
||||
import { showToast } from '../../util/showToast';
|
||||
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
|
@ -83,8 +84,15 @@ import { useBoundActions } from '../../hooks/useBoundActions';
|
|||
import type { NoopActionType } from './noop';
|
||||
import { conversationJobQueue } from '../../jobs/conversationJobQueue';
|
||||
import type { TimelineMessageLoadingState } from '../../util/timelineUtil';
|
||||
import { isGroup } from '../../util/whatTypeOfConversation';
|
||||
import {
|
||||
isDirectConversation,
|
||||
isGroup,
|
||||
} from '../../util/whatTypeOfConversation';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
|
||||
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus';
|
||||
import { isIncoming } from '../selectors/message';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -1003,6 +1011,56 @@ function generateNewGroupLink(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Not an actual redux action creator, so it doesn't produce an action (or dispatch
|
||||
* itself) because updates are managed through the backbone model, which will trigger
|
||||
* necessary updates and refresh conversation_view.
|
||||
*
|
||||
* In practice, it's similar to an already-connected thunk action. Later on we will
|
||||
* replace it with an actual action that fits in with the redux approach.
|
||||
*/
|
||||
export const markViewed = (messageId: string): void => {
|
||||
const message = window.MessageController.getById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`markViewed: Message ${messageId} missing!`);
|
||||
}
|
||||
|
||||
if (message.get('readStatus') === ReadStatus.Viewed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const senderE164 = message.get('source');
|
||||
const senderUuid = message.get('sourceUuid');
|
||||
const timestamp = message.get('sent_at');
|
||||
|
||||
message.set(messageUpdaterMarkViewed(message.attributes, Date.now()));
|
||||
|
||||
if (isIncoming(message.attributes)) {
|
||||
viewedReceiptsJobQueue.add({
|
||||
viewedReceipt: {
|
||||
messageId,
|
||||
senderE164,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
isDirectConversation: isDirectConversation(
|
||||
message.getConversation()?.attributes
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
viewSyncJobQueue.add({
|
||||
viewSyncs: [
|
||||
{
|
||||
messageId,
|
||||
senderE164,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
function setAccessControlAddFromInviteLinkSetting(
|
||||
conversationId: string,
|
||||
value: boolean
|
||||
|
|
|
@ -12,7 +12,7 @@ import { SmartMessageDetail } from '../smart/MessageDetail';
|
|||
|
||||
export const createMessageDetail = (
|
||||
store: Store,
|
||||
props: OwnProps
|
||||
props: Omit<OwnProps, 'markViewed'>
|
||||
): ReactElement => (
|
||||
<Provider store={store}>
|
||||
<SmartMessageDetail {...props} />
|
||||
|
|
|
@ -40,7 +40,6 @@ export type Props = {
|
|||
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
|
||||
kickOffAttachmentDownload(): void;
|
||||
onCorrupted(): void;
|
||||
onFirstPlayed(): void;
|
||||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
|
|
|
@ -14,6 +14,7 @@ import { renderAudioAttachment } from './renderAudioAttachment';
|
|||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
import { renderReactionPicker } from './renderReactionPicker';
|
||||
import { getContactNameColorSelector } from '../selectors/conversations';
|
||||
import { markViewed } from '../ducks/conversations';
|
||||
|
||||
export { Contact } from '../../components/conversation/MessageDetail';
|
||||
export type OwnProps = Omit<
|
||||
|
@ -25,6 +26,7 @@ export type OwnProps = Omit<
|
|||
| 'renderEmojiPicker'
|
||||
| 'renderReactionPicker'
|
||||
| 'theme'
|
||||
| 'markViewed'
|
||||
>;
|
||||
|
||||
const mapStateToProps = (
|
||||
|
@ -43,7 +45,6 @@ const mapStateToProps = (
|
|||
displayTapToViewMessage,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
markViewed,
|
||||
openConversation,
|
||||
openGiftBadge,
|
||||
openLink,
|
||||
|
|
|
@ -50,6 +50,7 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
|
|||
import type { UnreadIndicatorPlacement } from '../../util/timelineUtil';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { markViewed } from '../ducks/conversations';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
|
@ -76,7 +77,6 @@ export type TimelinePropsType = ExternalProps &
|
|||
| 'loadOlderMessages'
|
||||
| 'markAttachmentAsCorrupted'
|
||||
| 'markMessageRead'
|
||||
| 'markViewed'
|
||||
| 'onBlock'
|
||||
| 'onBlockAndReportSpam'
|
||||
| 'onDelete'
|
||||
|
@ -317,6 +317,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
renderContactSpoofingReviewDialog,
|
||||
renderHeroRow,
|
||||
renderTypingBubble,
|
||||
markViewed,
|
||||
...actions,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -29,6 +29,8 @@ import * as GoogleChrome from '../util/GoogleChrome';
|
|||
import { parseIntOrThrow } from '../util/parseIntOrThrow';
|
||||
import { getValue } from '../RemoteConfig';
|
||||
import { isRecord } from '../util/isRecord';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import type { MessageStatusType } from '../components/conversation/Message';
|
||||
|
||||
const MAX_WIDTH = 300;
|
||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||
|
@ -652,6 +654,17 @@ export function isAudio(attachments?: ReadonlyArray<AttachmentType>): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function isPlayed(
|
||||
direction: 'outgoing' | 'incoming',
|
||||
status: MessageStatusType | undefined,
|
||||
readStatus: ReadStatus | undefined
|
||||
): boolean {
|
||||
if (direction === 'outgoing') {
|
||||
return status === 'viewed';
|
||||
}
|
||||
return readStatus === ReadStatus.Viewed;
|
||||
}
|
||||
|
||||
export function canDisplayImage(
|
||||
attachments?: ReadonlyArray<AttachmentType>
|
||||
): boolean {
|
||||
|
|
|
@ -56,7 +56,6 @@ import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
|||
import { createConversationView } from '../state/roots/createConversationView';
|
||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||
import type { CompositionAPIType } from '../components/CompositionArea';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import { ToastBlocked } from '../components/ToastBlocked';
|
||||
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||
|
@ -85,12 +84,9 @@ import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge
|
|||
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
|
||||
import { retryMessageSend } from '../util/retryMessageSend';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import { markViewed } from '../services/MessageUpdater';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
|
||||
import { showToast } from '../util/showToast';
|
||||
import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
|
||||
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||
import { UUIDKind } from '../types/UUID';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
|
@ -152,7 +148,6 @@ type MessageActionsType = {
|
|||
options: Readonly<{ messageId: string }>
|
||||
) => unknown;
|
||||
markAttachmentAsCorrupted: (options: AttachmentOptions) => unknown;
|
||||
markViewed: (messageId: string) => unknown;
|
||||
openConversation: (conversationId: string, messageId?: string) => unknown;
|
||||
openGiftBadge: (messageId: string) => unknown;
|
||||
openLink: (url: string) => unknown;
|
||||
|
@ -793,45 +788,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
}
|
||||
message.markAttachmentAsCorrupted(options.attachment);
|
||||
};
|
||||
const onMarkViewed = (messageId: string): void => {
|
||||
const message = window.MessageController.getById(messageId);
|
||||
if (!message) {
|
||||
throw new Error(`onMarkViewed: Message ${messageId} missing!`);
|
||||
}
|
||||
|
||||
if (message.get('readStatus') === ReadStatus.Viewed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const senderE164 = message.get('source');
|
||||
const senderUuid = message.get('sourceUuid');
|
||||
const timestamp = message.get('sent_at');
|
||||
|
||||
message.set(markViewed(message.attributes, Date.now()));
|
||||
|
||||
if (isIncoming(message.attributes)) {
|
||||
viewedReceiptsJobQueue.add({
|
||||
viewedReceipt: {
|
||||
messageId,
|
||||
senderE164,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
isDirectConversation: isDirectConversation(this.model.attributes),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
viewSyncJobQueue.add({
|
||||
viewSyncs: [
|
||||
{
|
||||
messageId,
|
||||
senderE164,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
const showVisualAttachment = (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
|
@ -889,7 +846,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
downloadNewVersion,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
markViewed: onMarkViewed,
|
||||
openConversation,
|
||||
openGiftBadge,
|
||||
openLink,
|
||||
|
|
Loading…
Add table
Reference in a new issue