Moves DraftAttachments into redux

This commit is contained in:
Josh Perez 2021-09-24 16:02:30 -04:00 committed by Josh Perez
parent f81f61af4e
commit 1c3c971cf4
20 changed files with 818 additions and 444 deletions

View file

@ -0,0 +1,15 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { AttachmentType } from '../types/Attachment';
export async function deleteDraftAttachment(
attachment: Pick<AttachmentType, 'screenshotPath' | 'path'>
): Promise<void> {
if (attachment.screenshotPath) {
await window.Signal.Migrations.deleteDraftFile(attachment.screenshotPath);
}
if (attachment.path) {
await window.Signal.Migrations.deleteDraftFile(attachment.path);
}
}

18
ts/util/fileToBytes.ts Normal file
View file

@ -0,0 +1,18 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function fileToBytes(file: Blob): Promise<Uint8Array> {
return new Promise((resolve, rejectPromise) => {
const FR = new FileReader();
FR.onload = () => {
if (!FR.result || typeof FR.result === 'string') {
rejectPromise(new Error('bytesFromFile: No result!'));
return;
}
resolve(new Uint8Array(FR.result));
};
FR.onerror = rejectPromise;
FR.onabort = rejectPromise;
FR.readAsArrayBuffer(file);
});
}

View file

@ -0,0 +1,82 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
getPendingAttachment,
preProcessAttachment,
processAttachment,
} from './processAttachment';
import { AttachmentType } from '../types/Attachment';
import { AttachmentToastType } from '../types/AttachmentToastType';
export type AddAttachmentActionType = (
conversationId: string,
attachment: AttachmentType
) => unknown;
export type AddPendingAttachmentActionType = (
conversationId: string,
pendingAttachment: AttachmentType
) => unknown;
export type RemoveAttachmentActionType = (
conversationId: string,
filePath: string
) => unknown;
export type HandleAttachmentsProcessingArgsType = {
addAttachment: AddAttachmentActionType;
addPendingAttachment: AddPendingAttachmentActionType;
conversationId: string;
draftAttachments: ReadonlyArray<AttachmentType>;
files: ReadonlyArray<File>;
onShowToast: (toastType: AttachmentToastType) => unknown;
removeAttachment: RemoveAttachmentActionType;
};
export async function handleAttachmentsProcessing({
addAttachment,
addPendingAttachment,
conversationId,
draftAttachments,
files,
onShowToast,
removeAttachment,
}: HandleAttachmentsProcessingArgsType): Promise<void> {
if (!files.length) {
return;
}
const nextDraftAttachments = [...draftAttachments];
const filesToProcess: Array<File> = [];
for (let i = 0; i < files.length; i += 1) {
const file = files[i];
const processingResult = preProcessAttachment(file, nextDraftAttachments);
if (processingResult) {
onShowToast(processingResult);
} else {
const pendingAttachment = getPendingAttachment(file);
if (pendingAttachment) {
addPendingAttachment(conversationId, pendingAttachment);
filesToProcess.push(file);
// we keep a running count of the draft attachments so we can show a
// toast in case we add too many attachments at once
nextDraftAttachments.push(pendingAttachment);
}
}
}
await Promise.all(
filesToProcess.map(async file => {
try {
const attachment = await processAttachment(file);
if (!attachment) {
removeAttachment(conversationId, file.path);
return;
}
addAttachment(conversationId, attachment);
} catch (err) {
removeAttachment(conversationId, file.path);
onShowToast(AttachmentToastType.ToastUnableToLoadAttachment);
}
})
);
}

View file

@ -0,0 +1,43 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { blobToArrayBuffer } from 'blob-util';
import * as log from '../logging/log';
import { makeVideoScreenshot } from '../types/VisualAttachment';
import { IMAGE_PNG, stringToMIMEType } from '../types/MIME';
import { InMemoryAttachmentDraftType } from '../types/Attachment';
import { fileToBytes } from './fileToBytes';
export async function handleVideoAttachment(
file: Readonly<File>
): Promise<InMemoryAttachmentDraftType> {
const objectUrl = URL.createObjectURL(file);
if (!objectUrl) {
throw new Error('Failed to create object url for video!');
}
try {
const screenshotContentType = IMAGE_PNG;
const screenshotBlob = await makeVideoScreenshot({
objectUrl,
contentType: screenshotContentType,
logger: log,
});
const screenshotData = await blobToArrayBuffer(screenshotBlob);
const data = await fileToBytes(file);
return {
contentType: stringToMIMEType(file.type),
data,
fileName: file.name,
path: file.name,
pending: false,
screenshotContentType,
screenshotData: new Uint8Array(screenshotData),
screenshotSize: screenshotData.byteLength,
size: data.byteLength,
};
} finally {
URL.revokeObjectURL(objectUrl);
}
}

View file

@ -12289,22 +12289,6 @@
"updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Our code, no user input, only clearing out the dom"
},
{
"rule": "React-useRef",
"path": "ts/components/CompositionArea.js",
"line": " const inputApiRef = React.useRef();",
"reasonCategory": "falseMatch",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element."
},
{
"rule": "React-useRef",
"path": "ts/components/CompositionArea.js",
"line": " const micCellRef = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Needed for the composition area."
},
{
"rule": "DOM-innerHTML",
"path": "ts/components/CompositionArea.tsx",
@ -12316,16 +12300,23 @@
{
"rule": "React-useRef",
"path": "ts/components/CompositionArea.tsx",
"line": " const inputApiRef = React.useRef<InputApi | undefined>();",
"line": " const micCellRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z"
},
{
"rule": "React-useRef",
"path": "ts/components/CompositionArea.tsx",
"line": " const micCellRef = React.useRef<HTMLDivElement>(null);",
"line": " const inputApiRef = useRef<InputApi | undefined>();",
"reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z"
"updated": "2021-09-23T00:07:11.885Z"
},
{
"rule": "React-useRef",
"path": "ts/components/CompositionArea.tsx",
"line": " const fileInputRef = useRef<null | HTMLInputElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-23T00:07:11.885Z"
},
{
"rule": "React-useRef",
@ -14283,4 +14274,4 @@
"reasonCategory": "usageTrusted",
"updated": "2021-09-17T21:02:59.414Z"
}
]
]

View file

@ -0,0 +1,122 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import path from 'path';
import * as log from '../logging/log';
import { AttachmentType } from '../types/Attachment';
import { AttachmentToastType } from '../types/AttachmentToastType';
import { fileToBytes } from './fileToBytes';
import { handleImageAttachment } from './handleImageAttachment';
import { handleVideoAttachment } from './handleVideoAttachment';
import { isAttachmentSizeOkay } from './isAttachmentSizeOkay';
import { isFileDangerous } from './isFileDangerous';
import { isHeic, isImage, stringToMIMEType } from '../types/MIME';
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
export function getPendingAttachment(file: File): AttachmentType | undefined {
if (!file) {
return;
}
const fileType = stringToMIMEType(file.type);
const { name: fileName } = path.parse(file.name);
return {
contentType: fileType,
fileName,
path: file.name,
pending: true,
};
}
export function preProcessAttachment(
file: File,
draftAttachments: Array<AttachmentType>
): AttachmentToastType | undefined {
if (!file) {
return;
}
const MB = 1000 * 1024;
if (file.size > 100 * MB) {
return AttachmentToastType.ToastFileSize;
}
if (isFileDangerous(file.name)) {
return AttachmentToastType.ToastDangerousFileType;
}
if (draftAttachments.length >= 32) {
return AttachmentToastType.ToastMaxAttachments;
}
const haveNonImage = draftAttachments.some(
(attachment: AttachmentType) => !isImage(attachment.contentType)
);
// You can't add another attachment if you already have a non-image staged
if (haveNonImage) {
return AttachmentToastType.ToastOneNonImageAtATime;
}
const fileType = stringToMIMEType(file.type);
// You can't add a non-image attachment if you already have attachments staged
if (!isImage(fileType) && draftAttachments.length > 0) {
return AttachmentToastType.ToastCannotMixImageAndNonImageAttachments;
}
return undefined;
}
export async function processAttachment(
file: File
): Promise<AttachmentType | void> {
const fileType = stringToMIMEType(file.type);
let attachment: AttachmentType;
try {
if (isImageTypeSupported(fileType) || isHeic(fileType)) {
attachment = await handleImageAttachment(file);
} else if (isVideoTypeSupported(fileType)) {
attachment = await handleVideoAttachment(file);
} else {
const data = await fileToBytes(file);
attachment = {
contentType: fileType,
data,
fileName: file.name,
path: file.name,
pending: false,
size: data.byteLength,
};
}
} catch (e) {
log.error(
`Was unable to generate thumbnail for fileType ${fileType}`,
e && e.stack ? e.stack : e
);
const data = await fileToBytes(file);
attachment = {
contentType: fileType,
data,
fileName: file.name,
path: file.name,
pending: false,
size: data.byteLength,
};
}
try {
if (isAttachmentSizeOkay(attachment)) {
return attachment;
}
} catch (error) {
log.error(
'Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
throw error;
}
}

View file

@ -0,0 +1,40 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { pick } from 'lodash';
import * as log from '../logging/log';
import { AttachmentType } from '../types/Attachment';
export function resolveAttachmentOnDisk(
attachment: AttachmentType
): AttachmentType {
let url = '';
if (attachment.pending) {
return attachment;
}
if (attachment.screenshotPath) {
url = window.Signal.Migrations.getAbsoluteDraftPath(
attachment.screenshotPath
);
} else if (attachment.path) {
url = window.Signal.Migrations.getAbsoluteDraftPath(attachment.path);
} else {
log.warn(
'resolveOnDiskAttachment: Attachment was missing both screenshotPath and path fields'
);
}
return {
...pick(attachment, [
'blurHash',
'caption',
'contentType',
'fileName',
'path',
'size',
]),
pending: false,
url,
};
}

View file

@ -0,0 +1,29 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { omit } from 'lodash';
import { AttachmentType } from '../types/Attachment';
export async function writeDraftAttachment(
attachment: AttachmentType
): Promise<AttachmentType> {
if (attachment.pending) {
throw new Error('writeDraftAttachment: Cannot write pending attachment');
}
const result: AttachmentType = {
...omit(attachment, ['data', 'screenshotData']),
pending: false,
};
if (attachment.data) {
result.path = await window.Signal.Migrations.writeNewDraftData(
attachment.data
);
}
if (attachment.screenshotData) {
result.screenshotPath = await window.Signal.Migrations.writeNewDraftData(
attachment.screenshotData
);
}
return result;
}