Mark attachment as corrupted if audio load failed
Sending corrupted audio should not leave user with non-functional UI. Mark attachment as corrupted and show generic attachment UI for it instead.
This commit is contained in:
parent
d6063d71e5
commit
9fa3359477
12 changed files with 98 additions and 5 deletions
|
@ -111,6 +111,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
isTapToViewError: overrideProps.isTapToViewError,
|
isTapToViewError: overrideProps.isTapToViewError,
|
||||||
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||||
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
openConversation: action('openConversation'),
|
openConversation: action('openConversation'),
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
previews: overrideProps.previews || [],
|
previews: overrideProps.previews || [],
|
||||||
|
|
|
@ -90,6 +90,7 @@ export type AudioAttachmentProps = {
|
||||||
withContentBelow: boolean;
|
withContentBelow: boolean;
|
||||||
|
|
||||||
kickOffAttachmentDownload(): void;
|
kickOffAttachmentDownload(): void;
|
||||||
|
onCorrupted(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
|
@ -185,6 +186,10 @@ export type PropsActions = {
|
||||||
attachment: AttachmentType;
|
attachment: AttachmentType;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
markAttachmentAsCorrupted: (options: {
|
||||||
|
attachment: AttachmentType;
|
||||||
|
messageId: string;
|
||||||
|
}) => void;
|
||||||
showVisualAttachment: (options: {
|
showVisualAttachment: (options: {
|
||||||
attachment: AttachmentType;
|
attachment: AttachmentType;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -686,6 +691,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
|
markAttachmentAsCorrupted,
|
||||||
quote,
|
quote,
|
||||||
showVisualAttachment,
|
showVisualAttachment,
|
||||||
isSticker,
|
isSticker,
|
||||||
|
@ -773,6 +779,12 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
messageId: id,
|
messageId: id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onCorrupted() {
|
||||||
|
markAttachmentAsCorrupted({
|
||||||
|
attachment: firstAttachment,
|
||||||
|
messageId: id,
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||||
|
|
|
@ -25,6 +25,7 @@ export type Props = {
|
||||||
|
|
||||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||||
kickOffAttachmentDownload(): void;
|
kickOffAttachmentDownload(): void;
|
||||||
|
onCorrupted(): void;
|
||||||
|
|
||||||
activeAudioID: string | undefined;
|
activeAudioID: string | undefined;
|
||||||
setActiveAudioID: (id: string | undefined) => void;
|
setActiveAudioID: (id: string | undefined) => void;
|
||||||
|
@ -208,6 +209,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
buttonRef,
|
buttonRef,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
|
onCorrupted,
|
||||||
|
|
||||||
audio,
|
audio,
|
||||||
audioContext,
|
audioContext,
|
||||||
|
@ -275,14 +277,27 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
|
||||||
setPeaks(newPeaks);
|
setPeaks(newPeaks);
|
||||||
setDuration(Math.max(newDuration, 1e-23));
|
setDuration(Math.max(newDuration, 1e-23));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.log.error('MessageAudio: loadAudio error', err);
|
window.log.error(
|
||||||
|
'MessageAudio: loadAudio error, marking as corrupted',
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
onCorrupted();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
};
|
};
|
||||||
}, [attachment, audioContext, setDuration, setPeaks, state, waveformCache]);
|
}, [
|
||||||
|
attachment,
|
||||||
|
audioContext,
|
||||||
|
setDuration,
|
||||||
|
setPeaks,
|
||||||
|
onCorrupted,
|
||||||
|
state,
|
||||||
|
waveformCache,
|
||||||
|
]);
|
||||||
|
|
||||||
// This effect attaches/detaches event listeners to the global <audio/>
|
// This effect attaches/detaches event listeners to the global <audio/>
|
||||||
// instance that we reuse from the GlobalAudioContext.
|
// instance that we reuse from the GlobalAudioContext.
|
||||||
|
|
|
@ -36,6 +36,7 @@ const defaultMessage: MessageProps = {
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||||
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
openConversation: () => null,
|
openConversation: () => null,
|
||||||
openLink: () => null,
|
openLink: () => null,
|
||||||
previews: [],
|
previews: [],
|
||||||
|
|
|
@ -39,6 +39,7 @@ const defaultMessageProps: MessagesProps = {
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
kickOffAttachmentDownload: () => null,
|
kickOffAttachmentDownload: () => null,
|
||||||
|
markAttachmentAsCorrupted: () => null,
|
||||||
openConversation: () => null,
|
openConversation: () => null,
|
||||||
openLink: () => null,
|
openLink: () => null,
|
||||||
previews: [],
|
previews: [],
|
||||||
|
|
|
@ -235,6 +235,7 @@ const actions = () => ({
|
||||||
showContactDetail: action('showContactDetail'),
|
showContactDetail: action('showContactDetail'),
|
||||||
showContactModal: action('showContactModal'),
|
showContactModal: action('showContactModal'),
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||||
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
showVisualAttachment: action('showVisualAttachment'),
|
showVisualAttachment: action('showVisualAttachment'),
|
||||||
downloadAttachment: action('downloadAttachment'),
|
downloadAttachment: action('downloadAttachment'),
|
||||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||||
|
|
|
@ -47,6 +47,7 @@ const getDefaultProps = () => ({
|
||||||
deleteMessage: action('deleteMessage'),
|
deleteMessage: action('deleteMessage'),
|
||||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||||
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
showMessageDetail: action('showMessageDetail'),
|
showMessageDetail: action('showMessageDetail'),
|
||||||
openConversation: action('openConversation'),
|
openConversation: action('openConversation'),
|
||||||
showContactDetail: action('showContactDetail'),
|
showContactDetail: action('showContactDetail'),
|
||||||
|
|
|
@ -2967,6 +2967,47 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markAttachmentAsCorrupted(attachment: AttachmentType): void {
|
||||||
|
if (!attachment.path) {
|
||||||
|
throw new Error(
|
||||||
|
"Attachment can't be marked as corrupted because it wasn't loaded"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We intentionally don't check in quotes/stickers/contacts/... here,
|
||||||
|
// because this function should be called only for something that can
|
||||||
|
// be displayed as a generic attachment.
|
||||||
|
const attachments: ReadonlyArray<AttachmentType> =
|
||||||
|
this.get('attachments') || [];
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
const newAttachments = attachments.map(existing => {
|
||||||
|
if (existing.path !== attachment.path) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...existing,
|
||||||
|
isCorrupted: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
throw new Error(
|
||||||
|
"Attachment can't be marked as corrupted because it wasn't found"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
'markAttachmentAsCorrupted: marking an attachment as corrupted'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.set({
|
||||||
|
attachments: newAttachments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
async copyFromQuotedMessage(message: WhatIsThis): Promise<boolean> {
|
async copyFromQuotedMessage(message: WhatIsThis): Promise<boolean> {
|
||||||
const { quote } = message;
|
const { quote } = message;
|
||||||
|
|
|
@ -25,6 +25,7 @@ export type Props = {
|
||||||
|
|
||||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||||
kickOffAttachmentDownload(): void;
|
kickOffAttachmentDownload(): void;
|
||||||
|
onCorrupted(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: Props) => {
|
const mapStateToProps = (state: StateType, props: Props) => {
|
||||||
|
|
|
@ -49,6 +49,7 @@ export type AttachmentType = {
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
isCorrupted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// UI-focused functions
|
// UI-focused functions
|
||||||
|
@ -87,6 +88,7 @@ export function isAudio(
|
||||||
attachments &&
|
attachments &&
|
||||||
attachments[0] &&
|
attachments[0] &&
|
||||||
attachments[0].contentType &&
|
attachments[0].contentType &&
|
||||||
|
!attachments[0].isCorrupted &&
|
||||||
MIME.isAudio(attachments[0].contentType)
|
MIME.isAudio(attachments[0].contentType)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14658,7 +14658,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 237,
|
"lineNumber": 242,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for managing focus only"
|
"reasonDetail": "Used for managing focus only"
|
||||||
|
@ -14667,7 +14667,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
||||||
"lineNumber": 239,
|
"lineNumber": 244,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||||
|
@ -14676,7 +14676,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " > = React.createRef();",
|
"line": " > = React.createRef();",
|
||||||
"lineNumber": 243,
|
"lineNumber": 248,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { MediaItemType } from '../components/LightboxGallery';
|
||||||
import { MessageType } from '../state/ducks/conversations';
|
import { MessageType } from '../state/ducks/conversations';
|
||||||
import { ConversationModel } from '../models/conversations';
|
import { ConversationModel } from '../models/conversations';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
|
import { assert } from '../util/assert';
|
||||||
|
|
||||||
type GetLinkPreviewImageResult = {
|
type GetLinkPreviewImageResult = {
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
|
@ -27,6 +28,11 @@ type GetLinkPreviewResult = {
|
||||||
date: number | null;
|
date: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AttachmentOptions = {
|
||||||
|
messageId: string;
|
||||||
|
attachment: AttachmentType;
|
||||||
|
};
|
||||||
|
|
||||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||||
const LINK_PREVIEW_TIMEOUT = 60 * 1000;
|
const LINK_PREVIEW_TIMEOUT = 60 * 1000;
|
||||||
|
|
||||||
|
@ -756,6 +762,16 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const message = this.model.messageCollection.get(options.messageId);
|
const message = this.model.messageCollection.get(options.messageId);
|
||||||
await message.queueAttachmentDownloads();
|
await message.queueAttachmentDownloads();
|
||||||
};
|
};
|
||||||
|
const markAttachmentAsCorrupted = (options: AttachmentOptions) => {
|
||||||
|
if (!this.model.messageCollection) {
|
||||||
|
throw new Error('Message collection does not exist');
|
||||||
|
}
|
||||||
|
const message: MessageModel = this.model.messageCollection.get(
|
||||||
|
options.messageId
|
||||||
|
);
|
||||||
|
assert(message, 'Message not found');
|
||||||
|
message.markAttachmentAsCorrupted(options.attachment);
|
||||||
|
};
|
||||||
const showVisualAttachment = (options: any) => {
|
const showVisualAttachment = (options: any) => {
|
||||||
this.showLightbox(options);
|
this.showLightbox(options);
|
||||||
};
|
};
|
||||||
|
@ -949,6 +965,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
downloadNewVersion,
|
downloadNewVersion,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
|
markAttachmentAsCorrupted,
|
||||||
loadNewerMessages,
|
loadNewerMessages,
|
||||||
loadNewestMessages: this.loadNewestMessages.bind(this),
|
loadNewestMessages: this.loadNewestMessages.bind(this),
|
||||||
loadAndScroll: this.loadAndScroll.bind(this),
|
loadAndScroll: this.loadAndScroll.bind(this),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue