Moves DraftAttachments into redux
This commit is contained in:
parent
f81f61af4e
commit
1c3c971cf4
20 changed files with 818 additions and 444 deletions
|
@ -30,15 +30,19 @@ const micCellEl = new DOMParser().parseFromString(
|
||||||
).body.firstElementChild as HTMLElement;
|
).body.firstElementChild as HTMLElement;
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
|
conversationId: '123',
|
||||||
i18n,
|
i18n,
|
||||||
micCellEl,
|
micCellEl,
|
||||||
onChooseAttachment: action('onChooseAttachment'),
|
|
||||||
|
addAttachment: action('addAttachment'),
|
||||||
|
addPendingAttachment: action('addPendingAttachment'),
|
||||||
|
processAttachments: action('processAttachments'),
|
||||||
|
removeAttachment: action('removeAttachment'),
|
||||||
|
|
||||||
// AttachmentList
|
// AttachmentList
|
||||||
draftAttachments: overrideProps.draftAttachments || [],
|
draftAttachments: overrideProps.draftAttachments || [],
|
||||||
onAddAttachment: action('onAddAttachment'),
|
|
||||||
onClearAttachments: action('onClearAttachments'),
|
onClearAttachments: action('onClearAttachments'),
|
||||||
onClickAttachment: action('onClickAttachment'),
|
onClickAttachment: action('onClickAttachment'),
|
||||||
onCloseAttachment: action('onCloseAttachment'),
|
|
||||||
// StagedLinkPreview
|
// StagedLinkPreview
|
||||||
linkPreviewLoading: Boolean(overrideProps.linkPreviewLoading),
|
linkPreviewLoading: Boolean(overrideProps.linkPreviewLoading),
|
||||||
linkPreviewResult: overrideProps.linkPreviewResult,
|
linkPreviewResult: overrideProps.linkPreviewResult,
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
// Copyright 2019-2021 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, {
|
||||||
|
MutableRefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { get, noop } from 'lodash';
|
import { get, noop } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Spinner } from './Spinner';
|
import { Spinner } from './Spinner';
|
||||||
|
@ -39,52 +46,61 @@ import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||||
import { LinkPreviewWithDomain } from '../types/LinkPreview';
|
import { LinkPreviewWithDomain } from '../types/LinkPreview';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
import { AnnouncementsOnlyGroupBanner } from './AnnouncementsOnlyGroupBanner';
|
import { AnnouncementsOnlyGroupBanner } from './AnnouncementsOnlyGroupBanner';
|
||||||
|
import { CompositionUpload } from './CompositionUpload';
|
||||||
|
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
|
||||||
|
|
||||||
export type CompositionAPIType = {
|
export type CompositionAPIType = {
|
||||||
focusInput: () => void;
|
focusInput: () => void;
|
||||||
isDirty: () => boolean;
|
isDirty: () => boolean;
|
||||||
setDisabled: (disabled: boolean) => void;
|
setDisabled: (disabled: boolean) => void;
|
||||||
setShowMic: (showMic: boolean) => void;
|
|
||||||
setMicActive: (micActive: boolean) => void;
|
setMicActive: (micActive: boolean) => void;
|
||||||
reset: InputApi['reset'];
|
reset: InputApi['reset'];
|
||||||
resetEmojiResults: InputApi['resetEmojiResults'];
|
resetEmojiResults: InputApi['resetEmojiResults'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OwnProps = Readonly<{
|
export type OwnProps = Readonly<{
|
||||||
i18n: LocalizerType;
|
acceptedMessageRequest?: boolean;
|
||||||
areWePending?: boolean;
|
addAttachment: (
|
||||||
areWePendingApproval?: boolean;
|
conversationId: string,
|
||||||
|
attachment: AttachmentType
|
||||||
|
) => unknown;
|
||||||
|
addPendingAttachment: (
|
||||||
|
conversationId: string,
|
||||||
|
pendingAttachment: AttachmentType
|
||||||
|
) => unknown;
|
||||||
announcementsOnly?: boolean;
|
announcementsOnly?: boolean;
|
||||||
areWeAdmin?: boolean;
|
areWeAdmin?: boolean;
|
||||||
|
areWePending?: boolean;
|
||||||
|
areWePendingApproval?: boolean;
|
||||||
|
compositionApi?: MutableRefObject<CompositionAPIType>;
|
||||||
|
conversationId: string;
|
||||||
|
draftAttachments: ReadonlyArray<AttachmentType>;
|
||||||
groupAdmins: Array<ConversationType>;
|
groupAdmins: Array<ConversationType>;
|
||||||
groupVersion?: 1 | 2;
|
groupVersion?: 1 | 2;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
isFetchingUUID?: boolean;
|
||||||
isGroupV1AndDisabled?: boolean;
|
isGroupV1AndDisabled?: boolean;
|
||||||
isMissingMandatoryProfileSharing?: boolean;
|
isMissingMandatoryProfileSharing?: boolean;
|
||||||
isSMSOnly?: boolean;
|
isSMSOnly?: boolean;
|
||||||
isFetchingUUID?: boolean;
|
|
||||||
left?: boolean;
|
left?: boolean;
|
||||||
|
linkPreviewLoading: boolean;
|
||||||
|
linkPreviewResult?: LinkPreviewWithDomain;
|
||||||
messageRequestsEnabled?: boolean;
|
messageRequestsEnabled?: boolean;
|
||||||
acceptedMessageRequest?: boolean;
|
|
||||||
compositionApi?: React.MutableRefObject<CompositionAPIType>;
|
|
||||||
micCellEl?: HTMLElement;
|
micCellEl?: HTMLElement;
|
||||||
draftAttachments: ReadonlyArray<AttachmentType>;
|
|
||||||
shouldSendHighQualityAttachments: boolean;
|
|
||||||
onChooseAttachment(): unknown;
|
|
||||||
onAddAttachment(): unknown;
|
|
||||||
onClickAttachment(): unknown;
|
|
||||||
onCloseAttachment(): unknown;
|
|
||||||
onClearAttachments(): unknown;
|
onClearAttachments(): unknown;
|
||||||
|
onClickAttachment(): unknown;
|
||||||
|
onClickQuotedMessage(): unknown;
|
||||||
|
onCloseLinkPreview(): unknown;
|
||||||
|
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
|
||||||
onSelectMediaQuality(isHQ: boolean): unknown;
|
onSelectMediaQuality(isHQ: boolean): unknown;
|
||||||
|
openConversation(conversationId: string): unknown;
|
||||||
quotedMessageProps?: Omit<
|
quotedMessageProps?: Omit<
|
||||||
QuoteProps,
|
QuoteProps,
|
||||||
'i18n' | 'onClick' | 'onClose' | 'withContentAbove'
|
'i18n' | 'onClick' | 'onClose' | 'withContentAbove'
|
||||||
>;
|
>;
|
||||||
onClickQuotedMessage(): unknown;
|
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
||||||
setQuotedMessage(message: undefined): unknown;
|
setQuotedMessage(message: undefined): unknown;
|
||||||
linkPreviewLoading: boolean;
|
shouldSendHighQualityAttachments: boolean;
|
||||||
linkPreviewResult?: LinkPreviewWithDomain;
|
|
||||||
onCloseLinkPreview(): unknown;
|
|
||||||
openConversation(conversationId: string): unknown;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type Props = Pick<
|
export type Props = Pick<
|
||||||
|
@ -129,15 +145,19 @@ const emptyElement = (el: HTMLElement) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CompositionArea = ({
|
export const CompositionArea = ({
|
||||||
|
// Base props
|
||||||
|
addAttachment,
|
||||||
|
addPendingAttachment,
|
||||||
|
conversationId,
|
||||||
i18n,
|
i18n,
|
||||||
micCellEl,
|
micCellEl,
|
||||||
onChooseAttachment,
|
processAttachments,
|
||||||
|
removeAttachment,
|
||||||
|
|
||||||
// AttachmentList
|
// AttachmentList
|
||||||
draftAttachments,
|
draftAttachments,
|
||||||
onAddAttachment,
|
|
||||||
onClearAttachments,
|
onClearAttachments,
|
||||||
onClickAttachment,
|
onClickAttachment,
|
||||||
onCloseAttachment,
|
|
||||||
// StagedLinkPreview
|
// StagedLinkPreview
|
||||||
linkPreviewLoading,
|
linkPreviewLoading,
|
||||||
linkPreviewResult,
|
linkPreviewResult,
|
||||||
|
@ -206,21 +226,21 @@ export const CompositionArea = ({
|
||||||
isSMSOnly,
|
isSMSOnly,
|
||||||
isFetchingUUID,
|
isFetchingUUID,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [disabled, setDisabled] = React.useState(false);
|
const [disabled, setDisabled] = useState(false);
|
||||||
const [showMic, setShowMic] = React.useState(!draftText);
|
const [micActive, setMicActive] = useState(false);
|
||||||
const [micActive, setMicActive] = React.useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [dirty, setDirty] = React.useState(false);
|
const [large, setLarge] = useState(false);
|
||||||
const [large, setLarge] = React.useState(false);
|
const inputApiRef = useRef<InputApi | undefined>();
|
||||||
const inputApiRef = React.useRef<InputApi | undefined>();
|
const fileInputRef = useRef<null | HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleForceSend = React.useCallback(() => {
|
const handleForceSend = useCallback(() => {
|
||||||
setLarge(false);
|
setLarge(false);
|
||||||
if (inputApiRef.current) {
|
if (inputApiRef.current) {
|
||||||
inputApiRef.current.submit();
|
inputApiRef.current.submit();
|
||||||
}
|
}
|
||||||
}, [inputApiRef, setLarge]);
|
}, [inputApiRef, setLarge]);
|
||||||
|
|
||||||
const handleSubmit = React.useCallback<typeof onSubmit>(
|
const handleSubmit = useCallback<typeof onSubmit>(
|
||||||
(...args) => {
|
(...args) => {
|
||||||
setLarge(false);
|
setLarge(false);
|
||||||
onSubmit(...args);
|
onSubmit(...args);
|
||||||
|
@ -228,7 +248,17 @@ export const CompositionArea = ({
|
||||||
[setLarge, onSubmit]
|
[setLarge, onSubmit]
|
||||||
);
|
);
|
||||||
|
|
||||||
const focusInput = React.useCallback(() => {
|
const launchAttachmentPicker = () => {
|
||||||
|
const fileInput = fileInputRef.current;
|
||||||
|
if (fileInput) {
|
||||||
|
// Setting the value to empty so that onChange always fires in case
|
||||||
|
// you add multiple photos.
|
||||||
|
fileInput.value = '';
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusInput = useCallback(() => {
|
||||||
if (inputApiRef.current) {
|
if (inputApiRef.current) {
|
||||||
inputApiRef.current.focus();
|
inputApiRef.current.focus();
|
||||||
}
|
}
|
||||||
|
@ -249,7 +279,6 @@ export const CompositionArea = ({
|
||||||
isDirty: () => dirty,
|
isDirty: () => dirty,
|
||||||
focusInput,
|
focusInput,
|
||||||
setDisabled,
|
setDisabled,
|
||||||
setShowMic,
|
|
||||||
setMicActive,
|
setMicActive,
|
||||||
reset: () => {
|
reset: () => {
|
||||||
if (inputApiRef.current) {
|
if (inputApiRef.current) {
|
||||||
|
@ -264,7 +293,7 @@ export const CompositionArea = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertEmoji = React.useCallback(
|
const insertEmoji = useCallback(
|
||||||
(e: EmojiPickDataType) => {
|
(e: EmojiPickDataType) => {
|
||||||
if (inputApiRef.current) {
|
if (inputApiRef.current) {
|
||||||
inputApiRef.current.insertEmoji(e);
|
inputApiRef.current.insertEmoji(e);
|
||||||
|
@ -274,14 +303,16 @@ export const CompositionArea = ({
|
||||||
[inputApiRef, onPickEmoji]
|
[inputApiRef, onPickEmoji]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleLarge = React.useCallback(() => {
|
const handleToggleLarge = useCallback(() => {
|
||||||
setLarge(l => !l);
|
setLarge(l => !l);
|
||||||
}, [setLarge]);
|
}, [setLarge]);
|
||||||
|
|
||||||
|
const shouldShowMicrophone = !draftAttachments.length && !draftText;
|
||||||
|
|
||||||
// The following is a work-around to allow react to lay-out backbone-managed
|
// The following is a work-around to allow react to lay-out backbone-managed
|
||||||
// dom nodes until those functions are in React
|
// dom nodes until those functions are in React
|
||||||
const micCellRef = React.useRef<HTMLDivElement>(null);
|
const micCellRef = useRef<HTMLDivElement>(null);
|
||||||
React.useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const { current: micCellContainer } = micCellRef;
|
const { current: micCellContainer } = micCellRef;
|
||||||
if (micCellContainer && micCellEl) {
|
if (micCellContainer && micCellEl) {
|
||||||
emptyElement(micCellContainer);
|
emptyElement(micCellContainer);
|
||||||
|
@ -289,7 +320,7 @@ export const CompositionArea = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return noop;
|
return noop;
|
||||||
}, [micCellRef, micCellEl, large, dirty, showMic]);
|
}, [micCellRef, micCellEl, large, dirty, shouldShowMicrophone]);
|
||||||
|
|
||||||
const showMediaQualitySelector = draftAttachments.some(isImageAttachment);
|
const showMediaQualitySelector = draftAttachments.some(isImageAttachment);
|
||||||
|
|
||||||
|
@ -318,7 +349,7 @@ export const CompositionArea = ({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const micButtonFragment = showMic ? (
|
const micButtonFragment = shouldShowMicrophone ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'CompositionArea__button-cell',
|
'CompositionArea__button-cell',
|
||||||
|
@ -338,7 +369,7 @@ export const CompositionArea = ({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="paperclip thumbnail"
|
className="paperclip thumbnail"
|
||||||
onClick={onChooseAttachment}
|
onClick={launchAttachmentPicker}
|
||||||
aria-label={i18n('CompositionArea--attach-file')}
|
aria-label={i18n('CompositionArea--attach-file')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -384,7 +415,7 @@ export const CompositionArea = ({
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
// Listen for cmd/ctrl-shift-x to toggle large composition mode
|
// Listen for cmd/ctrl-shift-x to toggle large composition mode
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
const { key, shiftKey, ctrlKey, metaKey } = e;
|
const { key, shiftKey, ctrlKey, metaKey } = e;
|
||||||
// When using the ctrl key, `key` is `'X'`. When using the cmd key, `key` is `'x'`
|
// When using the ctrl key, `key` is `'X'`. When using the cmd key, `key` is `'x'`
|
||||||
|
@ -557,10 +588,14 @@ export const CompositionArea = ({
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
attachments={draftAttachments}
|
attachments={draftAttachments}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onAddAttachment={onAddAttachment}
|
onAddAttachment={launchAttachmentPicker}
|
||||||
onClickAttachment={onClickAttachment}
|
onClickAttachment={onClickAttachment}
|
||||||
onClose={onClearAttachments}
|
onClose={onClearAttachments}
|
||||||
onCloseAttachment={onCloseAttachment}
|
onCloseAttachment={attachment => {
|
||||||
|
if (attachment.path) {
|
||||||
|
removeAttachment(conversationId, attachment.path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -610,9 +645,19 @@ export const CompositionArea = ({
|
||||||
{stickerButtonFragment}
|
{stickerButtonFragment}
|
||||||
{attButton}
|
{attButton}
|
||||||
{!dirty ? micButtonFragment : null}
|
{!dirty ? micButtonFragment : null}
|
||||||
{dirty || !showMic ? sendButtonFragment : null}
|
{dirty || !shouldShowMicrophone ? sendButtonFragment : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<CompositionUpload
|
||||||
|
addAttachment={addAttachment}
|
||||||
|
addPendingAttachment={addPendingAttachment}
|
||||||
|
conversationId={conversationId}
|
||||||
|
draftAttachments={draftAttachments}
|
||||||
|
i18n={i18n}
|
||||||
|
processAttachments={processAttachments}
|
||||||
|
removeAttachment={removeAttachment}
|
||||||
|
ref={fileInputRef}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
113
ts/components/CompositionUpload.tsx
Normal file
113
ts/components/CompositionUpload.tsx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { ChangeEventHandler, forwardRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { AttachmentType } from '../types/Attachment';
|
||||||
|
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
import { ToastCannotMixImageAndNonImageAttachments } from './ToastCannotMixImageAndNonImageAttachments';
|
||||||
|
import { ToastDangerousFileType } from './ToastDangerousFileType';
|
||||||
|
import { ToastFileSize } from './ToastFileSize';
|
||||||
|
import { ToastMaxAttachments } from './ToastMaxAttachments';
|
||||||
|
import { ToastOneNonImageAtATime } from './ToastOneNonImageAtATime';
|
||||||
|
import { ToastUnableToLoadAttachment } from './ToastUnableToLoadAttachment';
|
||||||
|
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
addAttachment: (
|
||||||
|
conversationId: string,
|
||||||
|
attachment: AttachmentType
|
||||||
|
) => unknown;
|
||||||
|
addPendingAttachment: (
|
||||||
|
conversationId: string,
|
||||||
|
pendingAttachment: AttachmentType
|
||||||
|
) => unknown;
|
||||||
|
conversationId: string;
|
||||||
|
draftAttachments: ReadonlyArray<AttachmentType>;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
|
||||||
|
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
addAttachment,
|
||||||
|
addPendingAttachment,
|
||||||
|
conversationId,
|
||||||
|
draftAttachments,
|
||||||
|
i18n,
|
||||||
|
processAttachments,
|
||||||
|
removeAttachment,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [toastType, setToastType] = useState<
|
||||||
|
AttachmentToastType | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const onFileInputChange: ChangeEventHandler<HTMLInputElement> = async event => {
|
||||||
|
const files = event.target.files || [];
|
||||||
|
|
||||||
|
await processAttachments({
|
||||||
|
addAttachment,
|
||||||
|
addPendingAttachment,
|
||||||
|
conversationId,
|
||||||
|
files: Array.from(files),
|
||||||
|
draftAttachments,
|
||||||
|
onShowToast: setToastType,
|
||||||
|
removeAttachment,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function closeToast() {
|
||||||
|
setToastType(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
let toast;
|
||||||
|
|
||||||
|
if (toastType === AttachmentToastType.ToastFileSize) {
|
||||||
|
toast = (
|
||||||
|
<ToastFileSize
|
||||||
|
i18n={i18n}
|
||||||
|
limit={100}
|
||||||
|
onClose={closeToast}
|
||||||
|
units="MB"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (toastType === AttachmentToastType.ToastDangerousFileType) {
|
||||||
|
toast = <ToastDangerousFileType i18n={i18n} onClose={closeToast} />;
|
||||||
|
} else if (toastType === AttachmentToastType.ToastMaxAttachments) {
|
||||||
|
toast = <ToastMaxAttachments i18n={i18n} onClose={closeToast} />;
|
||||||
|
} else if (toastType === AttachmentToastType.ToastOneNonImageAtATime) {
|
||||||
|
toast = <ToastOneNonImageAtATime i18n={i18n} onClose={closeToast} />;
|
||||||
|
} else if (
|
||||||
|
toastType ===
|
||||||
|
AttachmentToastType.ToastCannotMixImageAndNonImageAttachments
|
||||||
|
) {
|
||||||
|
toast = (
|
||||||
|
<ToastCannotMixImageAndNonImageAttachments
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={closeToast}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (toastType === AttachmentToastType.ToastUnableToLoadAttachment) {
|
||||||
|
toast = <ToastUnableToLoadAttachment i18n={i18n} onClose={closeToast} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{toast}
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
multiple
|
||||||
|
onChange={onFileInputChange}
|
||||||
|
ref={ref}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -23,7 +23,7 @@ export const Toast = ({
|
||||||
disableCloseOnClick = false,
|
disableCloseOnClick = false,
|
||||||
onClick,
|
onClick,
|
||||||
onClose,
|
onClose,
|
||||||
timeout = 2000,
|
timeout = 8000,
|
||||||
}: PropsType): JSX.Element | null => {
|
}: PropsType): JSX.Element | null => {
|
||||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ export const AttachmentList = ({
|
||||||
{(attachments || []).map((attachment, index) => {
|
{(attachments || []).map((attachment, index) => {
|
||||||
const url = getUrl(attachment);
|
const url = getUrl(attachment);
|
||||||
|
|
||||||
const key = url || attachment.fileName || index;
|
const key = url || attachment.path || attachment.fileName || index;
|
||||||
|
|
||||||
const isImage = isImageAttachment(attachment);
|
const isImage = isImageAttachment(attachment);
|
||||||
const isVideo = isVideoAttachment(attachment);
|
const isVideo = isVideoAttachment(attachment);
|
||||||
|
|
8
ts/model-types.d.ts
vendored
8
ts/model-types.d.ts
vendored
|
@ -22,11 +22,7 @@ import {
|
||||||
} from './messages/MessageSendState';
|
} from './messages/MessageSendState';
|
||||||
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
|
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
|
||||||
import { ConversationColorType } from './types/Colors';
|
import { ConversationColorType } from './types/Colors';
|
||||||
import {
|
import { AttachmentType, ThumbnailType } from './types/Attachment';
|
||||||
AttachmentType,
|
|
||||||
ThumbnailType,
|
|
||||||
OnDiskAttachmentDraftType,
|
|
||||||
} from './types/Attachment';
|
|
||||||
import { EmbeddedContactType } from './types/EmbeddedContact';
|
import { EmbeddedContactType } from './types/EmbeddedContact';
|
||||||
import { SignalService as Proto } from './protobuf';
|
import { SignalService as Proto } from './protobuf';
|
||||||
import { AvatarDataType } from './types/Avatar';
|
import { AvatarDataType } from './types/Avatar';
|
||||||
|
@ -214,7 +210,7 @@ export type ConversationAttributesType = {
|
||||||
customColorId?: string;
|
customColorId?: string;
|
||||||
discoveredUnregisteredAt?: number;
|
discoveredUnregisteredAt?: number;
|
||||||
draftChanged?: boolean;
|
draftChanged?: boolean;
|
||||||
draftAttachments?: Array<OnDiskAttachmentDraftType>;
|
draftAttachments?: Array<AttachmentType>;
|
||||||
draftBodyRanges?: Array<BodyRangeType>;
|
draftBodyRanges?: Array<BodyRangeType>;
|
||||||
draftTimestamp?: number | null;
|
draftTimestamp?: number | null;
|
||||||
inbox_position: number;
|
inbox_position: number;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
|
import * as log from '../../logging/log';
|
||||||
|
import { NoopActionType } from './noop';
|
||||||
import { StateType as RootStateType } from '../reducer';
|
import { StateType as RootStateType } from '../reducer';
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
import { AttachmentType } from '../../types/Attachment';
|
||||||
import { MessageAttributesType } from '../../model-types.d';
|
import { MessageAttributesType } from '../../model-types.d';
|
||||||
|
@ -12,6 +14,13 @@ import {
|
||||||
REMOVE_PREVIEW as REMOVE_LINK_PREVIEW,
|
REMOVE_PREVIEW as REMOVE_LINK_PREVIEW,
|
||||||
RemoveLinkPreviewActionType,
|
RemoveLinkPreviewActionType,
|
||||||
} from './linkPreviews';
|
} from './linkPreviews';
|
||||||
|
import { writeDraftAttachment } from '../../util/writeDraftAttachment';
|
||||||
|
import { replaceIndex } from '../../util/replaceIndex';
|
||||||
|
import { resolveAttachmentOnDisk } from '../../util/resolveAttachmentOnDisk';
|
||||||
|
import {
|
||||||
|
handleAttachmentsProcessing,
|
||||||
|
HandleAttachmentsProcessingArgsType,
|
||||||
|
} from '../../util/handleAttachmentsProcessing';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -25,12 +34,18 @@ export type ComposerStateType = {
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
|
const ADD_PENDING_ATTACHMENT = 'composer/ADD_PENDING_ATTACHMENT';
|
||||||
const REPLACE_ATTACHMENTS = 'composer/REPLACE_ATTACHMENTS';
|
const REPLACE_ATTACHMENTS = 'composer/REPLACE_ATTACHMENTS';
|
||||||
const RESET_COMPOSER = 'composer/RESET_COMPOSER';
|
const RESET_COMPOSER = 'composer/RESET_COMPOSER';
|
||||||
const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING';
|
const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING';
|
||||||
const SET_LINK_PREVIEW_RESULT = 'composer/SET_LINK_PREVIEW_RESULT';
|
const SET_LINK_PREVIEW_RESULT = 'composer/SET_LINK_PREVIEW_RESULT';
|
||||||
const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
|
const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
|
||||||
|
|
||||||
|
type AddPendingAttachmentActionType = {
|
||||||
|
type: typeof ADD_PENDING_ATTACHMENT;
|
||||||
|
payload: AttachmentType;
|
||||||
|
};
|
||||||
|
|
||||||
type ReplaceAttachmentsActionType = {
|
type ReplaceAttachmentsActionType = {
|
||||||
type: typeof REPLACE_ATTACHMENTS;
|
type: typeof REPLACE_ATTACHMENTS;
|
||||||
payload: ReadonlyArray<AttachmentType>;
|
payload: ReadonlyArray<AttachmentType>;
|
||||||
|
@ -59,16 +74,21 @@ type SetQuotedMessageActionType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type ComposerActionType =
|
type ComposerActionType =
|
||||||
|
| AddPendingAttachmentActionType
|
||||||
|
| RemoveLinkPreviewActionType
|
||||||
| ReplaceAttachmentsActionType
|
| ReplaceAttachmentsActionType
|
||||||
| ResetComposerActionType
|
| ResetComposerActionType
|
||||||
| SetHighQualitySettingActionType
|
| SetHighQualitySettingActionType
|
||||||
| SetLinkPreviewResultActionType
|
| SetLinkPreviewResultActionType
|
||||||
| RemoveLinkPreviewActionType
|
|
||||||
| SetQuotedMessageActionType;
|
| SetQuotedMessageActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
addAttachment,
|
||||||
|
addPendingAttachment,
|
||||||
|
processAttachments,
|
||||||
|
removeAttachment,
|
||||||
replaceAttachments,
|
replaceAttachments,
|
||||||
resetComposer,
|
resetComposer,
|
||||||
setLinkPreviewResult,
|
setLinkPreviewResult,
|
||||||
|
@ -76,9 +96,137 @@ export const actions = {
|
||||||
setQuotedMessage,
|
setQuotedMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Not cool that we have to pull from ConversationModel here
|
||||||
|
// but if the current selected conversation isn't the one that we're operating
|
||||||
|
// on then we won't be able to grab attachments from state so we resort to the
|
||||||
|
// next in-memory store.
|
||||||
|
function getAttachmentsFromConversationModel(
|
||||||
|
conversationId: string
|
||||||
|
): Array<AttachmentType> {
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
return conversation?.get('draftAttachments') || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAttachment(
|
||||||
|
conversationId: string,
|
||||||
|
attachment: AttachmentType
|
||||||
|
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const isSelectedConversation =
|
||||||
|
getState().conversations.selectedConversationId === conversationId;
|
||||||
|
|
||||||
|
const draftAttachments = isSelectedConversation
|
||||||
|
? getState().composer.attachments
|
||||||
|
: getAttachmentsFromConversationModel(conversationId);
|
||||||
|
|
||||||
|
const hasDraftAttachmentPending = draftAttachments.some(
|
||||||
|
draftAttachment =>
|
||||||
|
draftAttachment.pending && draftAttachment.path === attachment.path
|
||||||
|
);
|
||||||
|
|
||||||
|
// User has canceled the draft so we don't need to continue processing
|
||||||
|
if (!hasDraftAttachmentPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDisk = await writeDraftAttachment(attachment);
|
||||||
|
|
||||||
|
// Remove any pending attachments that were transcoding
|
||||||
|
const index = draftAttachments.findIndex(
|
||||||
|
draftAttachment => draftAttachment.path === attachment.path
|
||||||
|
);
|
||||||
|
let nextAttachments = draftAttachments;
|
||||||
|
if (index < 0) {
|
||||||
|
log.warn(
|
||||||
|
`addAttachment: Failed to find pending attachment with path ${attachment.path}`
|
||||||
|
);
|
||||||
|
nextAttachments = [...draftAttachments, onDisk];
|
||||||
|
} else {
|
||||||
|
nextAttachments = replaceIndex(draftAttachments, index, onDisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceAttachments(conversationId, nextAttachments)(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (conversation) {
|
||||||
|
conversation.attributes.draftAttachments = nextAttachments;
|
||||||
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPendingAttachment(
|
||||||
|
conversationId: string,
|
||||||
|
pendingAttachment: AttachmentType
|
||||||
|
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const isSelectedConversation =
|
||||||
|
getState().conversations.selectedConversationId === conversationId;
|
||||||
|
|
||||||
|
const draftAttachments = isSelectedConversation
|
||||||
|
? getState().composer.attachments
|
||||||
|
: getAttachmentsFromConversationModel(conversationId);
|
||||||
|
|
||||||
|
const nextAttachments = [...draftAttachments, pendingAttachment];
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: REPLACE_ATTACHMENTS,
|
||||||
|
payload: nextAttachments,
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (conversation) {
|
||||||
|
conversation.attributes.draftAttachments = nextAttachments;
|
||||||
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAttachments(
|
||||||
|
options: HandleAttachmentsProcessingArgsType
|
||||||
|
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
await handleAttachmentsProcessing(options);
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAttachment(
|
||||||
|
conversationId: string,
|
||||||
|
filePath: string
|
||||||
|
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { attachments } = getState().composer;
|
||||||
|
|
||||||
|
const nextAttachments = attachments.filter(
|
||||||
|
attachment => attachment.path !== filePath
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (conversation) {
|
||||||
|
conversation.attributes.draftAttachments = nextAttachments;
|
||||||
|
conversation.attributes.draftChanged = true;
|
||||||
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceAttachments(conversationId, nextAttachments)(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function replaceAttachments(
|
function replaceAttachments(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
payload: ReadonlyArray<AttachmentType>
|
attachments: ReadonlyArray<AttachmentType>
|
||||||
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
// If the call came from a conversation we are no longer in we do not
|
// If the call came from a conversation we are no longer in we do not
|
||||||
|
@ -89,7 +237,7 @@ function replaceAttachments(
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: REPLACE_ATTACHMENTS,
|
type: REPLACE_ATTACHMENTS,
|
||||||
payload,
|
payload: attachments.map(resolveAttachmentOnDisk),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -189,5 +337,12 @@ export function reducer(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === ADD_PENDING_ATTACHMENT) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
attachments: [...state.attachments, action.payload],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Base
|
// Base
|
||||||
|
conversationId: id,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
// AttachmentsList
|
// AttachmentsList
|
||||||
draftAttachments,
|
draftAttachments,
|
||||||
|
|
|
@ -39,7 +39,9 @@ describe('both/state/ducks/composer', () => {
|
||||||
const { replaceAttachments } = actions;
|
const { replaceAttachments } = actions;
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
|
|
||||||
const attachments: Array<AttachmentType> = [{ contentType: IMAGE_JPEG }];
|
const attachments: Array<AttachmentType> = [
|
||||||
|
{ contentType: IMAGE_JPEG, pending: false, url: '' },
|
||||||
|
];
|
||||||
replaceAttachments('123', attachments)(
|
replaceAttachments('123', attachments)(
|
||||||
dispatch,
|
dispatch,
|
||||||
getRootStateFunction('123'),
|
getRootStateFunction('123'),
|
||||||
|
|
|
@ -55,6 +55,7 @@ export type AttachmentType = {
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
screenshotData?: Uint8Array;
|
||||||
screenshotPath?: string;
|
screenshotPath?: string;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
thumbnail?: ThumbnailType;
|
thumbnail?: ThumbnailType;
|
||||||
|
@ -96,19 +97,6 @@ export type InMemoryAttachmentDraftType =
|
||||||
pending: true;
|
pending: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OnDiskAttachmentDraftType =
|
|
||||||
| ({
|
|
||||||
caption?: string;
|
|
||||||
pending: false;
|
|
||||||
screenshotPath?: string;
|
|
||||||
} & BaseAttachmentDraftType)
|
|
||||||
| {
|
|
||||||
contentType: MIME.MIMEType;
|
|
||||||
fileName: string;
|
|
||||||
path: string;
|
|
||||||
pending: true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AttachmentDraftType =
|
export type AttachmentDraftType =
|
||||||
| ({
|
| ({
|
||||||
url: string;
|
url: string;
|
||||||
|
|
11
ts/types/AttachmentToastType.ts
Normal file
11
ts/types/AttachmentToastType.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export enum AttachmentToastType {
|
||||||
|
ToastCannotMixImageAndNonImageAttachments,
|
||||||
|
ToastDangerousFileType,
|
||||||
|
ToastFileSize,
|
||||||
|
ToastMaxAttachments,
|
||||||
|
ToastOneNonImageAtATime,
|
||||||
|
ToastUnableToLoadAttachment,
|
||||||
|
}
|
15
ts/util/deleteDraftAttachment.ts
Normal file
15
ts/util/deleteDraftAttachment.ts
Normal 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
18
ts/util/fileToBytes.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
82
ts/util/handleAttachmentsProcessing.ts
Normal file
82
ts/util/handleAttachmentsProcessing.ts
Normal 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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
43
ts/util/handleVideoAttachment.ts
Normal file
43
ts/util/handleVideoAttachment.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12289,22 +12289,6 @@
|
||||||
"updated": "2020-05-20T20:10:43.540Z",
|
"updated": "2020-05-20T20:10:43.540Z",
|
||||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
"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",
|
"rule": "DOM-innerHTML",
|
||||||
"path": "ts/components/CompositionArea.tsx",
|
"path": "ts/components/CompositionArea.tsx",
|
||||||
|
@ -12316,16 +12300,23 @@
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/CompositionArea.tsx",
|
"path": "ts/components/CompositionArea.tsx",
|
||||||
"line": " const inputApiRef = React.useRef<InputApi | undefined>();",
|
"line": " const micCellRef = useRef<HTMLDivElement>(null);",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
"updated": "2021-07-30T16:57:33.618Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/CompositionArea.tsx",
|
"path": "ts/components/CompositionArea.tsx",
|
||||||
"line": " const micCellRef = React.useRef<HTMLDivElement>(null);",
|
"line": " const inputApiRef = useRef<InputApi | undefined>();",
|
||||||
"reasonCategory": "usageTrusted",
|
"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",
|
"rule": "React-useRef",
|
||||||
|
@ -14283,4 +14274,4 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-09-17T21:02:59.414Z"
|
"updated": "2021-09-17T21:02:59.414Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
122
ts/util/processAttachment.ts
Normal file
122
ts/util/processAttachment.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
40
ts/util/resolveAttachmentOnDisk.ts
Normal file
40
ts/util/resolveAttachmentOnDisk.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
29
ts/util/writeDraftAttachment.ts
Normal file
29
ts/util/writeDraftAttachment.ts
Normal 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;
|
||||||
|
}
|
|
@ -1,16 +1,13 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import nodePath from 'path';
|
|
||||||
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
|
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
|
||||||
import { debounce, flatten, omit, pick, reject, throttle } from 'lodash';
|
import { debounce, flatten, omit, throttle } from 'lodash';
|
||||||
import { render } from 'mustache';
|
import { render } from 'mustache';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AttachmentDraftType,
|
AttachmentDraftType,
|
||||||
AttachmentType,
|
AttachmentType,
|
||||||
InMemoryAttachmentDraftType,
|
|
||||||
OnDiskAttachmentDraftType,
|
|
||||||
isGIF,
|
isGIF,
|
||||||
} from '../types/Attachment';
|
} from '../types/Attachment';
|
||||||
import * as Attachment from '../types/Attachment';
|
import * as Attachment from '../types/Attachment';
|
||||||
|
@ -20,8 +17,6 @@ import { BodyRangeType, BodyRangesType } from '../types/Util';
|
||||||
import {
|
import {
|
||||||
IMAGE_JPEG,
|
IMAGE_JPEG,
|
||||||
IMAGE_WEBP,
|
IMAGE_WEBP,
|
||||||
IMAGE_PNG,
|
|
||||||
isHeic,
|
|
||||||
MIMEType,
|
MIMEType,
|
||||||
stringToMIMEType,
|
stringToMIMEType,
|
||||||
} from '../types/MIME';
|
} from '../types/MIME';
|
||||||
|
@ -41,7 +36,6 @@ import {
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
import { maybeParseUrl } from '../util/url';
|
import { maybeParseUrl } from '../util/url';
|
||||||
import { replaceIndex } from '../util/replaceIndex';
|
|
||||||
import { addReportSpamJob } from '../jobs/helpers/addReportSpamJob';
|
import { addReportSpamJob } from '../jobs/helpers/addReportSpamJob';
|
||||||
import { reportSpamJobQueue } from '../jobs/reportSpamJobQueue';
|
import { reportSpamJobQueue } from '../jobs/reportSpamJobQueue';
|
||||||
import { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
|
import { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
|
||||||
|
@ -72,17 +66,13 @@ import {
|
||||||
} from '../types/LinkPreview';
|
} from '../types/LinkPreview';
|
||||||
import * as LinkPreview from '../types/LinkPreview';
|
import * as LinkPreview from '../types/LinkPreview';
|
||||||
import { SignalService as Proto } from '../protobuf';
|
import { SignalService as Proto } from '../protobuf';
|
||||||
import {
|
import { autoScale } from '../util/handleImageAttachment';
|
||||||
autoScale,
|
|
||||||
handleImageAttachment,
|
|
||||||
} from '../util/handleImageAttachment';
|
|
||||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||||
import { markViewed } from '../services/MessageUpdater';
|
import { markViewed } from '../services/MessageUpdater';
|
||||||
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
||||||
import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
|
import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
|
||||||
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||||
import * as VisualAttachment from '../types/VisualAttachment';
|
import * as VisualAttachment from '../types/VisualAttachment';
|
||||||
import * as MIME from '../types/MIME';
|
|
||||||
import type { AnyViewClass, BasicReactWrapperViewClass } from '../window.d';
|
import type { AnyViewClass, BasicReactWrapperViewClass } from '../window.d';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
|
@ -93,7 +83,6 @@ import { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCa
|
||||||
import { showToast } from '../util/showToast';
|
import { showToast } from '../util/showToast';
|
||||||
import { ToastBlocked } from '../components/ToastBlocked';
|
import { ToastBlocked } from '../components/ToastBlocked';
|
||||||
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||||
import { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments';
|
|
||||||
import { ToastConversationArchived } from '../components/ToastConversationArchived';
|
import { ToastConversationArchived } from '../components/ToastConversationArchived';
|
||||||
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||||
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||||
|
@ -101,23 +90,26 @@ import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||||
import { ToastDeleteForEveryoneFailed } from '../components/ToastDeleteForEveryoneFailed';
|
import { ToastDeleteForEveryoneFailed } from '../components/ToastDeleteForEveryoneFailed';
|
||||||
import { ToastExpired } from '../components/ToastExpired';
|
import { ToastExpired } from '../components/ToastExpired';
|
||||||
import { ToastFileSaved } from '../components/ToastFileSaved';
|
import { ToastFileSaved } from '../components/ToastFileSaved';
|
||||||
import { ToastFileSize } from '../components/ToastFileSize';
|
|
||||||
import { ToastInvalidConversation } from '../components/ToastInvalidConversation';
|
import { ToastInvalidConversation } from '../components/ToastInvalidConversation';
|
||||||
import { ToastLeftGroup } from '../components/ToastLeftGroup';
|
import { ToastLeftGroup } from '../components/ToastLeftGroup';
|
||||||
import { ToastMaxAttachments } from '../components/ToastMaxAttachments';
|
|
||||||
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
||||||
import { ToastOneNonImageAtATime } from '../components/ToastOneNonImageAtATime';
|
|
||||||
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||||
import { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull';
|
import { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull';
|
||||||
import { ToastReactionFailed } from '../components/ToastReactionFailed';
|
import { ToastReactionFailed } from '../components/ToastReactionFailed';
|
||||||
import { ToastReportedSpamAndBlocked } from '../components/ToastReportedSpamAndBlocked';
|
import { ToastReportedSpamAndBlocked } from '../components/ToastReportedSpamAndBlocked';
|
||||||
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
||||||
import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
|
import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
|
||||||
import { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
|
|
||||||
import { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
|
import { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
|
||||||
import { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment';
|
import { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment';
|
||||||
import { copyGroupLink } from '../util/copyGroupLink';
|
import { copyGroupLink } from '../util/copyGroupLink';
|
||||||
import { isAttachmentSizeOkay } from '../util/isAttachmentSizeOkay';
|
import { fileToBytes } from '../util/fileToBytes';
|
||||||
|
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||||
|
import { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments';
|
||||||
|
import { ToastFileSize } from '../components/ToastFileSize';
|
||||||
|
import { ToastMaxAttachments } from '../components/ToastMaxAttachments';
|
||||||
|
import { ToastOneNonImageAtATime } from '../components/ToastOneNonImageAtATime';
|
||||||
|
import { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
|
||||||
|
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
|
||||||
|
|
||||||
type AttachmentOptions = {
|
type AttachmentOptions = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
@ -134,10 +126,8 @@ const { Message } = window.Signal.Types;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
copyIntoTempDirectory,
|
copyIntoTempDirectory,
|
||||||
deleteDraftFile,
|
|
||||||
deleteTempFile,
|
deleteTempFile,
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
getAbsoluteDraftPath,
|
|
||||||
getAbsoluteTempPath,
|
getAbsoluteTempPath,
|
||||||
loadAttachmentData,
|
loadAttachmentData,
|
||||||
loadPreviewData,
|
loadPreviewData,
|
||||||
|
@ -147,7 +137,6 @@ const {
|
||||||
readDraftData,
|
readDraftData,
|
||||||
saveAttachmentToDisk,
|
saveAttachmentToDisk,
|
||||||
upgradeMessageSchema,
|
upgradeMessageSchema,
|
||||||
writeNewDraftData,
|
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -543,7 +532,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
caretLocation?: number
|
caretLocation?: number
|
||||||
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
|
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
|
||||||
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
|
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
|
||||||
onChooseAttachment: this.onChooseAttachment.bind(this),
|
|
||||||
getQuotedMessage: () => this.model.get('quotedMessageId'),
|
getQuotedMessage: () => this.model.get('quotedMessageId'),
|
||||||
clearQuotedMessage: () => this.setQuoteMessage(null),
|
clearQuotedMessage: () => this.setQuoteMessage(null),
|
||||||
micCellEl,
|
micCellEl,
|
||||||
|
@ -595,9 +583,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onAddAttachment: this.onChooseAttachment.bind(this),
|
|
||||||
onClickAttachment: this.onClickAttachment.bind(this),
|
onClickAttachment: this.onClickAttachment.bind(this),
|
||||||
onCloseAttachment: this.removeDraftAttachment.bind(this),
|
|
||||||
onClearAttachments: this.clearAttachments.bind(this),
|
onClearAttachments: this.clearAttachments.bind(this),
|
||||||
onSelectMediaQuality: (isHQ: boolean) => {
|
onSelectMediaQuality: (isHQ: boolean) => {
|
||||||
window.reduxActions.composer.setMediaQualitySetting(isHQ);
|
window.reduxActions.composer.setMediaQualitySetting(isHQ);
|
||||||
|
@ -1320,20 +1306,59 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onChooseAttachment(): void {
|
onChooseAttachment(): void {
|
||||||
|
// TODO: DESKTOP-2425
|
||||||
this.$('input.file-input').click();
|
this.$('input.file-input').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onChoseAttachment(): Promise<void> {
|
async onChoseAttachment(): Promise<void> {
|
||||||
const fileField = this.$('input.file-input');
|
const fileField = this.$('input.file-input');
|
||||||
const files = fileField.prop('files');
|
const files: Array<File> = Array.from(fileField.prop('files'));
|
||||||
|
|
||||||
for (let i = 0, max = files.length; i < max; i += 1) {
|
|
||||||
const file = files[i];
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await this.maybeAddAttachment(file);
|
|
||||||
this.toggleMicrophone();
|
|
||||||
}
|
|
||||||
|
|
||||||
fileField.val([]);
|
fileField.val([]);
|
||||||
|
|
||||||
|
await this.processAttachments(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO DESKTOP-2426
|
||||||
|
async processAttachments(files: Array<File>): Promise<void> {
|
||||||
|
const {
|
||||||
|
addAttachment,
|
||||||
|
addPendingAttachment,
|
||||||
|
processAttachments,
|
||||||
|
removeAttachment,
|
||||||
|
} = window.reduxActions.composer;
|
||||||
|
|
||||||
|
await processAttachments({
|
||||||
|
addAttachment,
|
||||||
|
addPendingAttachment,
|
||||||
|
conversationId: this.model.id,
|
||||||
|
draftAttachments: this.model.get('draftAttachments') || [],
|
||||||
|
files,
|
||||||
|
onShowToast: (toastType: AttachmentToastType) => {
|
||||||
|
if (toastType === AttachmentToastType.ToastFileSize) {
|
||||||
|
showToast(ToastFileSize, {
|
||||||
|
limit: 100,
|
||||||
|
units: 'MB',
|
||||||
|
});
|
||||||
|
} else if (toastType === AttachmentToastType.ToastDangerousFileType) {
|
||||||
|
showToast(ToastDangerousFileType);
|
||||||
|
} else if (toastType === AttachmentToastType.ToastMaxAttachments) {
|
||||||
|
showToast(ToastMaxAttachments);
|
||||||
|
} else if (toastType === AttachmentToastType.ToastOneNonImageAtATime) {
|
||||||
|
showToast(ToastOneNonImageAtATime);
|
||||||
|
} else if (
|
||||||
|
toastType ===
|
||||||
|
AttachmentToastType.ToastCannotMixImageAndNonImageAttachments
|
||||||
|
) {
|
||||||
|
showToast(ToastCannotMixImageAndNonImageAttachments);
|
||||||
|
} else if (
|
||||||
|
toastType === AttachmentToastType.ToastUnableToLoadAttachment
|
||||||
|
) {
|
||||||
|
showToast(ToastUnableToLoadAttachment);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAttachment,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
unload(reason: string): void {
|
unload(reason: string): void {
|
||||||
|
@ -1419,10 +1444,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const { files } = event.dataTransfer;
|
const { files } = event.dataTransfer;
|
||||||
for (let i = 0, max = files.length; i < max; i += 1) {
|
this.processAttachments(Array.from(files));
|
||||||
const file = files[i];
|
|
||||||
this.maybeAddAttachment(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onPaste(e: JQuery.TriggeredEvent): void {
|
onPaste(e: JQuery.TriggeredEvent): void {
|
||||||
|
@ -1445,14 +1467,17 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const files: Array<File> = [];
|
||||||
for (let i = 0; i < items.length; i += 1) {
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
if (items[i].type.split('/')[0] === 'image') {
|
if (items[i].type.split('/')[0] === 'image') {
|
||||||
const file = items[i].getAsFile();
|
const file = items[i].getAsFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
this.maybeAddAttachment(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processAttachments(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
syncMessageRequestResponse(
|
syncMessageRequestResponse(
|
||||||
|
@ -1511,7 +1536,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const onSave = (caption?: string) => {
|
const onSave = (caption?: string) => {
|
||||||
const attachments = this.model.get('draftAttachments') || [];
|
const attachments = this.model.get('draftAttachments') || [];
|
||||||
this.model.set({
|
this.model.set({
|
||||||
draftAttachments: attachments.map((item: OnDiskAttachmentDraftType) => {
|
draftAttachments: attachments.map((item: AttachmentType) => {
|
||||||
if (item.pending || attachment.pending) {
|
if (item.pending || attachment.pending) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
@ -1551,98 +1576,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
window.Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
|
window.Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
async deleteDraftAttachment(
|
|
||||||
attachment: Pick<AttachmentType, 'screenshotPath' | 'path'>
|
|
||||||
): Promise<void> {
|
|
||||||
if (attachment.screenshotPath) {
|
|
||||||
await deleteDraftFile(attachment.screenshotPath);
|
|
||||||
}
|
|
||||||
if (attachment.path) {
|
|
||||||
await deleteDraftFile(attachment.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveModel(): Promise<void> {
|
async saveModel(): Promise<void> {
|
||||||
window.Signal.Data.updateConversation(this.model.attributes);
|
window.Signal.Data.updateConversation(this.model.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAttachment(attachment: InMemoryAttachmentDraftType): Promise<void> {
|
|
||||||
const onDisk = await this.writeDraftAttachment(attachment);
|
|
||||||
|
|
||||||
// Remove any pending attachments that were transcoding
|
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
|
||||||
const index = draftAttachments.findIndex(
|
|
||||||
draftAttachment => draftAttachment.path === attachment.path
|
|
||||||
);
|
|
||||||
if (index < 0) {
|
|
||||||
log.warn(
|
|
||||||
`addAttachment: Failed to find pending attachment with path ${attachment.path}`
|
|
||||||
);
|
|
||||||
this.model.set({
|
|
||||||
draftAttachments: [...draftAttachments, onDisk],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.model.set({
|
|
||||||
draftAttachments: replaceIndex(draftAttachments, index, onDisk),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.updateAttachmentsView();
|
|
||||||
|
|
||||||
await this.saveModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
resolveOnDiskAttachment(
|
|
||||||
attachment: OnDiskAttachmentDraftType
|
|
||||||
): AttachmentDraftType {
|
|
||||||
let url = '';
|
|
||||||
if (attachment.pending) {
|
|
||||||
return attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachment.screenshotPath) {
|
|
||||||
url = getAbsoluteDraftPath(attachment.screenshotPath);
|
|
||||||
} else if (attachment.path) {
|
|
||||||
url = 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeDraftAttachment(
|
|
||||||
attachment: Pick<AttachmentType, 'path' | 'screenshotPath'>
|
|
||||||
): Promise<void> {
|
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
|
||||||
|
|
||||||
this.model.set({
|
|
||||||
draftAttachments: reject(
|
|
||||||
draftAttachments,
|
|
||||||
item => item.path === attachment.path
|
|
||||||
),
|
|
||||||
draftChanged: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateAttachmentsView();
|
|
||||||
|
|
||||||
await this.saveModel();
|
|
||||||
await this.deleteDraftAttachment(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearAttachments(): Promise<void> {
|
async clearAttachments(): Promise<void> {
|
||||||
this.voiceNoteAttachment = undefined;
|
this.voiceNoteAttachment = undefined;
|
||||||
|
|
||||||
|
@ -1658,9 +1595,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.saveModel(),
|
this.saveModel(),
|
||||||
Promise.all(
|
Promise.all(
|
||||||
draftAttachments.map(attachment =>
|
draftAttachments.map(attachment => deleteDraftAttachment(attachment))
|
||||||
this.deleteDraftAttachment(attachment)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -1690,12 +1625,16 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
async getFile(
|
async getFile(
|
||||||
attachment?: OnDiskAttachmentDraftType
|
attachment?: AttachmentType
|
||||||
): Promise<AttachmentType | undefined> {
|
): Promise<AttachmentType | undefined> {
|
||||||
if (!attachment || attachment.pending) {
|
if (!attachment || attachment.pending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!attachment.path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await readDraftData(attachment.path);
|
const data = await readDraftData(attachment.path);
|
||||||
if (data.byteLength !== attachment.size) {
|
if (data.byteLength !== attachment.size) {
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -1710,231 +1649,17 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
bytesFromFile(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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAttachmentsView(): void {
|
updateAttachmentsView(): void {
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
const draftAttachments = this.model.get('draftAttachments') || [];
|
||||||
window.reduxActions.composer.replaceAttachments(
|
window.reduxActions.composer.replaceAttachments(
|
||||||
this.model.get('id'),
|
this.model.get('id'),
|
||||||
draftAttachments.map((att: OnDiskAttachmentDraftType) =>
|
draftAttachments
|
||||||
this.resolveOnDiskAttachment(att)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
this.toggleMicrophone();
|
|
||||||
if (this.hasFiles({ includePending: true })) {
|
if (this.hasFiles({ includePending: true })) {
|
||||||
this.removeLinkPreview();
|
this.removeLinkPreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
async writeDraftAttachment(
|
|
||||||
attachment: InMemoryAttachmentDraftType
|
|
||||||
): Promise<OnDiskAttachmentDraftType> {
|
|
||||||
if (attachment.pending) {
|
|
||||||
throw new Error('writeDraftAttachment: Cannot write pending attachment');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: OnDiskAttachmentDraftType = {
|
|
||||||
...omit(attachment, ['data', 'screenshotData']),
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
if (attachment.data) {
|
|
||||||
result.path = await writeNewDraftData(attachment.data);
|
|
||||||
}
|
|
||||||
if (attachment.screenshotData) {
|
|
||||||
result.screenshotPath = await writeNewDraftData(
|
|
||||||
attachment.screenshotData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async maybeAddAttachment(file: File): Promise<void> {
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MB = 1000 * 1024;
|
|
||||||
if (file.size > 100 * MB) {
|
|
||||||
showToast(ToastFileSize, {
|
|
||||||
limit: 100,
|
|
||||||
units: 'MB',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.Signal.Util.isFileDangerous(file.name)) {
|
|
||||||
showToast(ToastDangerousFileType);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
|
||||||
if (draftAttachments.length >= 32) {
|
|
||||||
showToast(ToastMaxAttachments);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haveNonImage = draftAttachments.some(
|
|
||||||
(attachment: OnDiskAttachmentDraftType) =>
|
|
||||||
!MIME.isImage(attachment.contentType)
|
|
||||||
);
|
|
||||||
// You can't add another attachment if you already have a non-image staged
|
|
||||||
if (haveNonImage) {
|
|
||||||
showToast(ToastOneNonImageAtATime);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileType = stringToMIMEType(file.type);
|
|
||||||
|
|
||||||
// You can't add a non-image attachment if you already have attachments staged
|
|
||||||
if (!MIME.isImage(fileType) && draftAttachments.length > 0) {
|
|
||||||
showToast(ToastCannotMixImageAndNonImageAttachments);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a pending attachment since async processing happens below
|
|
||||||
const path = file.name;
|
|
||||||
const fileName = nodePath.parse(file.name).name;
|
|
||||||
this.model.set({
|
|
||||||
draftAttachments: [
|
|
||||||
...draftAttachments,
|
|
||||||
{
|
|
||||||
contentType: fileType,
|
|
||||||
fileName,
|
|
||||||
path,
|
|
||||||
pending: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
this.updateAttachmentsView();
|
|
||||||
|
|
||||||
let attachment: InMemoryAttachmentDraftType;
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
window.Signal.Util.GoogleChrome.isImageTypeSupported(fileType) ||
|
|
||||||
isHeic(fileType)
|
|
||||||
) {
|
|
||||||
attachment = await handleImageAttachment(file);
|
|
||||||
|
|
||||||
const hasDraftAttachmentPending = (
|
|
||||||
this.model.get('draftAttachments') || []
|
|
||||||
).some(
|
|
||||||
draftAttachment =>
|
|
||||||
draftAttachment.pending && draftAttachment.path === path
|
|
||||||
);
|
|
||||||
|
|
||||||
// User has canceled the draft so we don't need to continue processing
|
|
||||||
if (!hasDraftAttachmentPending) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
window.Signal.Util.GoogleChrome.isVideoTypeSupported(fileType)
|
|
||||||
) {
|
|
||||||
attachment = await this.handleVideoAttachment(file);
|
|
||||||
} else {
|
|
||||||
const data = await this.bytesFromFile(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 this.bytesFromFile(file);
|
|
||||||
attachment = {
|
|
||||||
contentType: fileType,
|
|
||||||
data,
|
|
||||||
fileName: file.name,
|
|
||||||
path: file.name,
|
|
||||||
pending: false,
|
|
||||||
size: data.byteLength,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!isAttachmentSizeOkay(attachment)) {
|
|
||||||
this.removeDraftAttachment(attachment);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'Error ensuring that image is properly sized:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
|
|
||||||
this.removeDraftAttachment(attachment);
|
|
||||||
showToast(ToastUnableToLoadAttachment);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.addAttachment(attachment);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
'Error saving draft attachment:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
|
|
||||||
showToast(ToastUnableToLoadAttachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async 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 VisualAttachment.makeVideoScreenshot({
|
|
||||||
objectUrl,
|
|
||||||
contentType: screenshotContentType,
|
|
||||||
logger: log,
|
|
||||||
});
|
|
||||||
const screenshotData = await VisualAttachment.blobToArrayBuffer(
|
|
||||||
screenshotBlob
|
|
||||||
);
|
|
||||||
const data = await this.bytesFromFile(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
async markAllAsVerifiedDefault(
|
async markAllAsVerifiedDefault(
|
||||||
unverified: ReadonlyArray<ConversationModel>
|
unverified: ReadonlyArray<ConversationModel>
|
||||||
|
@ -1957,12 +1682,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
await Promise.all(untrusted.map(contact => contact.setApproved()));
|
await Promise.all(untrusted.map(contact => contact.setApproved()));
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMicrophone(): void {
|
|
||||||
this.compositionApi.current?.setShowMic(
|
|
||||||
!this.hasFiles({ includePending: true })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
captureAudio(e?: Event): void {
|
captureAudio(e?: Event): void {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -2019,7 +1738,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
throw new Error('A voice note cannot be sent with other attachments');
|
throw new Error('A voice note cannot be sent with other attachments');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await this.bytesFromFile(blob);
|
const data = await fileToBytes(blob);
|
||||||
|
|
||||||
// These aren't persisted to disk; they are meant to be sent immediately
|
// These aren't persisted to disk; they are meant to be sent immediately
|
||||||
this.voiceNoteAttachment = {
|
this.voiceNoteAttachment = {
|
||||||
|
@ -4097,7 +3816,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
fileName: title,
|
fileName: title,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await this.bytesFromFile(withBlob.file);
|
const data = await fileToBytes(withBlob.file);
|
||||||
objectUrl = URL.createObjectURL(withBlob.file);
|
objectUrl = URL.createObjectURL(withBlob.file);
|
||||||
|
|
||||||
const blurHash = await window.imageToBlurHash(withBlob.file);
|
const blurHash = await window.imageToBlurHash(withBlob.file);
|
||||||
|
|
Loading…
Reference in a new issue