ConversationView: Move attachments processing into redux
This commit is contained in:
parent
ff6750e4fd
commit
452e0b7b31
25 changed files with 544 additions and 763 deletions
|
@ -690,7 +690,12 @@
|
||||||
"description": "An error popup when the user has attempted to add an attachment"
|
"description": "An error popup when the user has attempted to add an attachment"
|
||||||
},
|
},
|
||||||
"fileSizeWarning": {
|
"fileSizeWarning": {
|
||||||
"message": "Sorry, the selected file exceeds message size restrictions."
|
"message": "Sorry, the selected file exceeds message size restrictions.",
|
||||||
|
"description": "(deleted 2022/12/07) Shown in a toast if the user tries to attach too-large file"
|
||||||
|
},
|
||||||
|
"icu:fileSizeWarning": {
|
||||||
|
"messageformat": "Sorry, the selected file exceeds message size restrictions. {limit}{units}",
|
||||||
|
"description": "Shown in a toast if the user tries to attach too-large file"
|
||||||
},
|
},
|
||||||
"unableToLoadAttachment": {
|
"unableToLoadAttachment": {
|
||||||
"message": "Unable to load selected attachment."
|
"message": "Unable to load selected attachment."
|
||||||
|
|
|
@ -33,7 +33,6 @@ export default {
|
||||||
|
|
||||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
addAttachment: action('addAttachment'),
|
addAttachment: action('addAttachment'),
|
||||||
addPendingAttachment: action('addPendingAttachment'),
|
|
||||||
conversationId: '123',
|
conversationId: '123',
|
||||||
i18n,
|
i18n,
|
||||||
onSendMessage: action('onSendMessage'),
|
onSendMessage: action('onSendMessage'),
|
||||||
|
|
|
@ -12,7 +12,6 @@ import type {
|
||||||
} from '../types/Util';
|
} from '../types/Util';
|
||||||
import type { ErrorDialogAudioRecorderType } from '../state/ducks/audioRecorder';
|
import type { ErrorDialogAudioRecorderType } from '../state/ducks/audioRecorder';
|
||||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||||
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
|
|
||||||
import type { imageToBlurHash } from '../util/imageToBlurHash';
|
import type { imageToBlurHash } from '../util/imageToBlurHash';
|
||||||
import { Spinner } from './Spinner';
|
import { Spinner } from './Spinner';
|
||||||
import type {
|
import type {
|
||||||
|
@ -76,10 +75,6 @@ export type OwnProps = Readonly<{
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
attachment: InMemoryAttachmentDraftType
|
attachment: InMemoryAttachmentDraftType
|
||||||
) => unknown;
|
) => unknown;
|
||||||
addPendingAttachment: (
|
|
||||||
conversationId: string,
|
|
||||||
pendingAttachment: AttachmentDraftType
|
|
||||||
) => unknown;
|
|
||||||
announcementsOnly?: boolean;
|
announcementsOnly?: boolean;
|
||||||
areWeAdmin?: boolean;
|
areWeAdmin?: boolean;
|
||||||
areWePending?: boolean;
|
areWePending?: boolean;
|
||||||
|
@ -112,7 +107,10 @@ export type OwnProps = Readonly<{
|
||||||
onClearAttachments(): unknown;
|
onClearAttachments(): unknown;
|
||||||
onClickQuotedMessage(): unknown;
|
onClickQuotedMessage(): unknown;
|
||||||
onCloseLinkPreview(): unknown;
|
onCloseLinkPreview(): unknown;
|
||||||
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
|
processAttachments: (options: {
|
||||||
|
conversationId: string;
|
||||||
|
files: ReadonlyArray<File>;
|
||||||
|
}) => unknown;
|
||||||
onSelectMediaQuality(isHQ: boolean): unknown;
|
onSelectMediaQuality(isHQ: boolean): unknown;
|
||||||
onSendMessage(options: {
|
onSendMessage(options: {
|
||||||
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
|
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
|
||||||
|
@ -171,7 +169,6 @@ export type Props = Pick<
|
||||||
export function CompositionArea({
|
export function CompositionArea({
|
||||||
// Base props
|
// Base props
|
||||||
addAttachment,
|
addAttachment,
|
||||||
addPendingAttachment,
|
|
||||||
conversationId,
|
conversationId,
|
||||||
i18n,
|
i18n,
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
|
@ -733,13 +730,10 @@ export function CompositionArea({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<CompositionUpload
|
<CompositionUpload
|
||||||
addAttachment={addAttachment}
|
|
||||||
addPendingAttachment={addPendingAttachment}
|
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
draftAttachments={draftAttachments}
|
draftAttachments={draftAttachments}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
processAttachments={processAttachments}
|
processAttachments={processAttachments}
|
||||||
removeAttachment={removeAttachment}
|
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,116 +2,43 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import React, { forwardRef, useState } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
|
|
||||||
import type {
|
import type { AttachmentDraftType } from '../types/Attachment';
|
||||||
InMemoryAttachmentDraftType,
|
|
||||||
AttachmentDraftType,
|
|
||||||
} from '../types/Attachment';
|
|
||||||
import { isVideoAttachment, isImageAttachment } from '../types/Attachment';
|
import { isVideoAttachment, isImageAttachment } from '../types/Attachment';
|
||||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
import { ToastCannotMixMultiAndNonMultiAttachments } from './ToastCannotMixMultiAndNonMultiAttachments';
|
|
||||||
import { ToastDangerousFileType } from './ToastDangerousFileType';
|
|
||||||
import { ToastFileSize } from './ToastFileSize';
|
|
||||||
import { ToastMaxAttachments } from './ToastMaxAttachments';
|
|
||||||
import { ToastUnsupportedMultiAttachment } from './ToastUnsupportedMultiAttachment';
|
|
||||||
import { ToastUnableToLoadAttachment } from './ToastUnableToLoadAttachment';
|
|
||||||
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
|
|
||||||
import {
|
import {
|
||||||
getSupportedImageTypes,
|
getSupportedImageTypes,
|
||||||
getSupportedVideoTypes,
|
getSupportedVideoTypes,
|
||||||
} from '../util/GoogleChrome';
|
} from '../util/GoogleChrome';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
addAttachment: (
|
|
||||||
conversationId: string,
|
|
||||||
attachment: InMemoryAttachmentDraftType
|
|
||||||
) => unknown;
|
|
||||||
addPendingAttachment: (
|
|
||||||
conversationId: string,
|
|
||||||
pendingAttachment: AttachmentDraftType
|
|
||||||
) => unknown;
|
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
|
processAttachments: (options: {
|
||||||
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
conversationId: string;
|
||||||
|
files: ReadonlyArray<File>;
|
||||||
|
}) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
||||||
function CompositionUploadInner(
|
function CompositionUploadInner(
|
||||||
{
|
{ conversationId, draftAttachments, processAttachments },
|
||||||
addAttachment,
|
|
||||||
addPendingAttachment,
|
|
||||||
conversationId,
|
|
||||||
draftAttachments,
|
|
||||||
i18n,
|
|
||||||
processAttachments,
|
|
||||||
removeAttachment,
|
|
||||||
},
|
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const [toastType, setToastType] = useState<
|
|
||||||
AttachmentToastType | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const onFileInputChange: ChangeEventHandler<
|
const onFileInputChange: ChangeEventHandler<
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> = async event => {
|
> = async event => {
|
||||||
const files = event.target.files || [];
|
const files = event.target.files || [];
|
||||||
|
|
||||||
await processAttachments({
|
await processAttachments({
|
||||||
addAttachment,
|
|
||||||
addPendingAttachment,
|
|
||||||
conversationId,
|
conversationId,
|
||||||
files: Array.from(files),
|
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.ToastUnsupportedMultiAttachment
|
|
||||||
) {
|
|
||||||
toast = (
|
|
||||||
<ToastUnsupportedMultiAttachment i18n={i18n} onClose={closeToast} />
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
toastType ===
|
|
||||||
AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments
|
|
||||||
) {
|
|
||||||
toast = (
|
|
||||||
<ToastCannotMixMultiAndNonMultiAttachments
|
|
||||||
i18n={i18n}
|
|
||||||
onClose={closeToast}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (toastType === AttachmentToastType.ToastUnableToLoadAttachment) {
|
|
||||||
toast = <ToastUnableToLoadAttachment i18n={i18n} onClose={closeToast} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const anyVideoOrImageAttachments = draftAttachments.some(attachment => {
|
const anyVideoOrImageAttachments = draftAttachments.some(attachment => {
|
||||||
return isImageAttachment(attachment) || isVideoAttachment(attachment);
|
return isImageAttachment(attachment) || isVideoAttachment(attachment);
|
||||||
});
|
});
|
||||||
|
@ -121,17 +48,14 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<input
|
||||||
{toast}
|
hidden
|
||||||
<input
|
multiple
|
||||||
hidden
|
onChange={onFileInputChange}
|
||||||
multiple
|
ref={ref}
|
||||||
onChange={onFileInputChange}
|
type="file"
|
||||||
ref={ref}
|
accept={acceptContentTypes?.join(',')}
|
||||||
type="file"
|
/>
|
||||||
accept={acceptContentTypes?.join(',')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { ToastCannotMixMultiAndNonMultiAttachments } from './ToastCannotMixMultiAndNonMultiAttachments';
|
|
||||||
|
|
||||||
import { setupI18n } from '../util/setupI18n';
|
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
i18n,
|
|
||||||
onClose: action('onClose'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Components/ToastCannotMixMultiAndNonMultiAttachments',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _ToastCannotMixMultiAndNonMultiAttachments = (): JSX.Element => (
|
|
||||||
<ToastCannotMixMultiAndNonMultiAttachments {...defaultProps} />
|
|
||||||
);
|
|
||||||
|
|
||||||
_ToastCannotMixMultiAndNonMultiAttachments.story = {
|
|
||||||
name: 'ToastCannotMixMultiAndNonMultiAttachments',
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
|
||||||
import { Toast } from './Toast';
|
|
||||||
|
|
||||||
type PropsType = {
|
|
||||||
i18n: LocalizerType;
|
|
||||||
onClose: () => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ToastCannotMixMultiAndNonMultiAttachments({
|
|
||||||
i18n,
|
|
||||||
onClose,
|
|
||||||
}: PropsType): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Toast onClose={onClose}>
|
|
||||||
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
|
|
||||||
</Toast>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -20,7 +20,7 @@ export default {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const _ToastFileSize = (): JSX.Element => (
|
export const _ToastFileSize = (): JSX.Element => (
|
||||||
<ToastFileSize {...defaultProps} limit={100} units="MB" />
|
<ToastFileSize {...defaultProps} limit="100" units="MB" />
|
||||||
);
|
);
|
||||||
|
|
||||||
_ToastFileSize.story = {
|
_ToastFileSize.story = {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type { LocalizerType } from '../types/Util';
|
||||||
import { Toast } from './Toast';
|
import { Toast } from './Toast';
|
||||||
|
|
||||||
export type ToastPropsType = {
|
export type ToastPropsType = {
|
||||||
limit: number;
|
limit: string;
|
||||||
units: string;
|
units: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,8 +23,7 @@ export function ToastFileSize({
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={onClose}>
|
<Toast onClose={onClose}>
|
||||||
{i18n('fileSizeWarning')} {limit}
|
{i18n('icu:fileSizeWarning', { limit, units })}
|
||||||
{units}
|
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,13 @@ AddingUserToGroup.args = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CannotMixMultiAndNonMultiAttachments = Template.bind({});
|
||||||
|
CannotMixMultiAndNonMultiAttachments.args = {
|
||||||
|
toast: {
|
||||||
|
toastType: ToastType.CannotMixMultiAndNonMultiAttachments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const CannotStartGroupCall = Template.bind({});
|
export const CannotStartGroupCall = Template.bind({});
|
||||||
CannotStartGroupCall.args = {
|
CannotStartGroupCall.args = {
|
||||||
toast: {
|
toast: {
|
||||||
|
@ -70,6 +77,13 @@ CopiedUsernameLink.args = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DangerousFileType = Template.bind({});
|
||||||
|
DangerousFileType.args = {
|
||||||
|
toast: {
|
||||||
|
toastType: ToastType.DangerousFileType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const DeleteForEveryoneFailed = Template.bind({});
|
export const DeleteForEveryoneFailed = Template.bind({});
|
||||||
DeleteForEveryoneFailed.args = {
|
DeleteForEveryoneFailed.args = {
|
||||||
toast: {
|
toast: {
|
||||||
|
@ -91,6 +105,24 @@ FailedToDeleteUsername.args = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FileSize = Template.bind({});
|
||||||
|
FileSize.args = {
|
||||||
|
toast: {
|
||||||
|
toastType: ToastType.FileSize,
|
||||||
|
parameters: {
|
||||||
|
limit: '100',
|
||||||
|
units: 'MB',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MaxAttachments = Template.bind({});
|
||||||
|
MaxAttachments.args = {
|
||||||
|
toast: {
|
||||||
|
toastType: ToastType.MaxAttachments,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const MessageBodyTooLong = Template.bind({});
|
export const MessageBodyTooLong = Template.bind({});
|
||||||
MessageBodyTooLong.args = {
|
MessageBodyTooLong.args = {
|
||||||
toast: {
|
toast: {
|
||||||
|
@ -105,13 +137,6 @@ PinnedConversationsFull.args = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoryMuted = Template.bind({});
|
|
||||||
StoryMuted.args = {
|
|
||||||
toast: {
|
|
||||||
toastType: ToastType.StoryMuted,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ReportedSpamAndBlocked = Template.bind({});
|
export const ReportedSpamAndBlocked = Template.bind({});
|
||||||
ReportedSpamAndBlocked.args = {
|
ReportedSpamAndBlocked.args = {
|
||||||
toast: {
|
toast: {
|
||||||
|
@ -119,6 +144,13 @@ ReportedSpamAndBlocked.args = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const StoryMuted = Template.bind({});
|
||||||
|
StoryMuted.args = {
|
||||||
|
toast: {
|
||||||
|
toastType: ToastType.StoryMuted,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const StoryReact = Template.bind({});
|
export const StoryReact = Template.bind({});
|
||||||
StoryReact.args = {
|
StoryReact.args = {
|
||||||
toast: {
|
toast: {
|
||||||
|
@ -154,6 +186,20 @@ StoryVideoUnsupported.args = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UnableToLoadAttachment = Template.bind({});
|
||||||
|
UnableToLoadAttachment.args = {
|
||||||
|
toast: {
|
||||||
|
toastType: ToastType.UnableToLoadAttachment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UnsupportedMultiAttachment = Template.bind({});
|
||||||
|
UnsupportedMultiAttachment.args = {
|
||||||
|
toast: {
|
||||||
|
toastType: ToastType.UnsupportedMultiAttachment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const UserAddedToGroup = Template.bind({});
|
export const UserAddedToGroup = Template.bind({});
|
||||||
UserAddedToGroup.args = {
|
UserAddedToGroup.args = {
|
||||||
toast: {
|
toast: {
|
||||||
|
|
|
@ -42,6 +42,14 @@ export function ToastManager({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.CannotMixMultiAndNonMultiAttachments) {
|
||||||
|
return (
|
||||||
|
<Toast onClose={hideToast}>
|
||||||
|
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType.CannotStartGroupCall) {
|
if (toastType === ToastType.CannotStartGroupCall) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
|
@ -66,6 +74,10 @@ export function ToastManager({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.DangerousFileType) {
|
||||||
|
return <Toast onClose={hideToast}>{i18n('dangerousFileType')}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType.DeleteForEveryoneFailed) {
|
if (toastType === ToastType.DeleteForEveryoneFailed) {
|
||||||
return <Toast onClose={hideToast}>{i18n('deleteForEveryoneFailed')}</Toast>;
|
return <Toast onClose={hideToast}>{i18n('deleteForEveryoneFailed')}</Toast>;
|
||||||
}
|
}
|
||||||
|
@ -93,6 +105,18 @@ export function ToastManager({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.FileSize) {
|
||||||
|
return (
|
||||||
|
<Toast onClose={hideToast}>
|
||||||
|
{i18n('icu:fileSizeWarning', toast?.parameters)}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.MaxAttachments) {
|
||||||
|
return <Toast onClose={hideToast}>{i18n('maximumAttachments')}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType.MessageBodyTooLong) {
|
if (toastType === ToastType.MessageBodyTooLong) {
|
||||||
return <ToastMessageBodyTooLong i18n={i18n} onClose={hideToast} />;
|
return <ToastMessageBodyTooLong i18n={i18n} onClose={hideToast} />;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +181,18 @@ export function ToastManager({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.UnableToLoadAttachment) {
|
||||||
|
return <Toast onClose={hideToast}>{i18n('unableToLoadAttachment')}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.UnsupportedMultiAttachment) {
|
||||||
|
return (
|
||||||
|
<Toast onClose={hideToast}>
|
||||||
|
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType.UserAddedToGroup) {
|
if (toastType === ToastType.UserAddedToGroup) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { ToastMaxAttachments } from './ToastMaxAttachments';
|
|
||||||
|
|
||||||
import { setupI18n } from '../util/setupI18n';
|
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
i18n,
|
|
||||||
onClose: action('onClose'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Components/ToastMaxAttachments',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _ToastMaxAttachments = (): JSX.Element => (
|
|
||||||
<ToastMaxAttachments {...defaultProps} />
|
|
||||||
);
|
|
||||||
|
|
||||||
_ToastMaxAttachments.story = {
|
|
||||||
name: 'ToastMaxAttachments',
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
|
||||||
import { Toast } from './Toast';
|
|
||||||
|
|
||||||
type PropsType = {
|
|
||||||
i18n: LocalizerType;
|
|
||||||
onClose: () => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ToastMaxAttachments({ i18n, onClose }: PropsType): JSX.Element {
|
|
||||||
return <Toast onClose={onClose}>{i18n('maximumAttachments')}</Toast>;
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { ToastUnableToLoadAttachment } from './ToastUnableToLoadAttachment';
|
|
||||||
|
|
||||||
import { setupI18n } from '../util/setupI18n';
|
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
i18n,
|
|
||||||
onClose: action('onClose'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Components/ToastUnableToLoadAttachment',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _ToastUnableToLoadAttachment = (): JSX.Element => (
|
|
||||||
<ToastUnableToLoadAttachment {...defaultProps} />
|
|
||||||
);
|
|
||||||
|
|
||||||
_ToastUnableToLoadAttachment.story = {
|
|
||||||
name: 'ToastUnableToLoadAttachment',
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { ToastUnsupportedMultiAttachment } from './ToastUnsupportedMultiAttachment';
|
|
||||||
|
|
||||||
import { setupI18n } from '../util/setupI18n';
|
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
i18n,
|
|
||||||
onClose: action('onClose'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Components/ToastUnsupportedMultiAttachment',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _ToastUnsupportedMultiAttachment = (): JSX.Element => (
|
|
||||||
<ToastUnsupportedMultiAttachment {...defaultProps} />
|
|
||||||
);
|
|
||||||
|
|
||||||
_ToastUnsupportedMultiAttachment.story = {
|
|
||||||
name: 'ToastUnsupportedMultiAttachment',
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import type { LocalizerType } from '../types/Util';
|
|
||||||
import { Toast } from './Toast';
|
|
||||||
|
|
||||||
type PropsType = {
|
|
||||||
i18n: LocalizerType;
|
|
||||||
onClose: () => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ToastUnsupportedMultiAttachment({
|
|
||||||
i18n,
|
|
||||||
onClose,
|
|
||||||
}: PropsType): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Toast onClose={onClose}>
|
|
||||||
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
|
|
||||||
</Toast>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,18 +4,82 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
|
conversationId: string;
|
||||||
|
processAttachments: (options: {
|
||||||
|
conversationId: string;
|
||||||
|
files: ReadonlyArray<File>;
|
||||||
|
}) => void;
|
||||||
renderCompositionArea: () => JSX.Element;
|
renderCompositionArea: () => JSX.Element;
|
||||||
renderConversationHeader: () => JSX.Element;
|
renderConversationHeader: () => JSX.Element;
|
||||||
renderTimeline: () => JSX.Element;
|
renderTimeline: () => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ConversationView({
|
export function ConversationView({
|
||||||
|
conversationId,
|
||||||
|
processAttachments,
|
||||||
renderCompositionArea,
|
renderCompositionArea,
|
||||||
renderConversationHeader,
|
renderConversationHeader,
|
||||||
renderTimeline,
|
renderTimeline,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
|
const onDrop = React.useCallback(
|
||||||
|
(event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
if (!event.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.dataTransfer.types[0] !== 'Files') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { files } = event.dataTransfer;
|
||||||
|
processAttachments({
|
||||||
|
conversationId,
|
||||||
|
files: Array.from(files),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[conversationId, processAttachments]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPaste = React.useCallback(
|
||||||
|
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
||||||
|
if (!event.clipboardData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { items } = event.clipboardData;
|
||||||
|
|
||||||
|
const anyImages = [...items].some(
|
||||||
|
item => item.type.split('/')[0] === 'image'
|
||||||
|
);
|
||||||
|
if (!anyImages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const files: Array<File> = [];
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
if (items[i].type.split('/')[0] === 'image') {
|
||||||
|
const file = items[i].getAsFile();
|
||||||
|
if (file) {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processAttachments({
|
||||||
|
conversationId,
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[conversationId, processAttachments]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ConversationView">
|
<div className="ConversationView" onDrop={onDrop} onPaste={onPaste}>
|
||||||
<div className="ConversationView__header">
|
<div className="ConversationView__header">
|
||||||
{renderConversationHeader()}
|
{renderConversationHeader()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import type { ThunkAction } from 'redux-thunk';
|
import type { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
@ -25,9 +27,19 @@ import { writeDraftAttachment } from '../../util/writeDraftAttachment';
|
||||||
import { deleteDraftAttachment } from '../../util/deleteDraftAttachment';
|
import { deleteDraftAttachment } from '../../util/deleteDraftAttachment';
|
||||||
import { replaceIndex } from '../../util/replaceIndex';
|
import { replaceIndex } from '../../util/replaceIndex';
|
||||||
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
|
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
|
||||||
import type { HandleAttachmentsProcessingArgsType } from '../../util/handleAttachmentsProcessing';
|
|
||||||
import { handleAttachmentsProcessing } from '../../util/handleAttachmentsProcessing';
|
|
||||||
import { LinkPreviewSourceType } from '../../types/LinkPreview';
|
import { LinkPreviewSourceType } from '../../types/LinkPreview';
|
||||||
|
import { RecordingState } from './audioRecorder';
|
||||||
|
import { hasLinkPreviewLoaded } from '../../services/LinkPreview';
|
||||||
|
import { SHOW_TOAST, ToastType } from './toast';
|
||||||
|
import type { ShowToastActionType } from './toast';
|
||||||
|
import { getMaximumAttachmentSize } from '../../util/attachments';
|
||||||
|
import { isFileDangerous } from '../../util/isFileDangerous';
|
||||||
|
import { isImage, isVideo, stringToMIMEType } from '../../types/MIME';
|
||||||
|
import {
|
||||||
|
getRenderDetailsForLimit,
|
||||||
|
processAttachment,
|
||||||
|
} from '../../util/processAttachment';
|
||||||
|
import type { ReplacementValuesType } from '../../types/Util';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -187,11 +199,112 @@ function addPendingAttachment(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function processAttachments(
|
function processAttachments({
|
||||||
options: HandleAttachmentsProcessingArgsType
|
conversationId,
|
||||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
files,
|
||||||
return async dispatch => {
|
}: {
|
||||||
await handleAttachmentsProcessing(options);
|
conversationId: string;
|
||||||
|
files: ReadonlyArray<File>;
|
||||||
|
}): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
NoopActionType | ShowToastActionType
|
||||||
|
> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
if (!files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the call came from a conversation we are no longer in we do not
|
||||||
|
// update the state.
|
||||||
|
if (getState().conversations.selectedConversationId !== conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error('processAttachments: Unable to find conv');
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const isRecording =
|
||||||
|
state.audioRecorder.recordingState === RecordingState.Recording;
|
||||||
|
|
||||||
|
if (hasLinkPreviewLoaded() || isRecording) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let toastToShow:
|
||||||
|
| { toastType: ToastType; parameters?: ReplacementValuesType }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const nextDraftAttachments = (
|
||||||
|
conversation.get('draftAttachments') || []
|
||||||
|
).slice();
|
||||||
|
const filesToProcess: Array<File> = [];
|
||||||
|
for (let i = 0; i < files.length; i += 1) {
|
||||||
|
const file = files[i];
|
||||||
|
const processingResult = preProcessAttachment(file, nextDraftAttachments);
|
||||||
|
if (processingResult != null) {
|
||||||
|
toastToShow = processingResult;
|
||||||
|
} else {
|
||||||
|
const pendingAttachment = getPendingAttachment(file);
|
||||||
|
if (pendingAttachment) {
|
||||||
|
addPendingAttachment(conversationId, pendingAttachment)(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
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)(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addAttachment(conversationId, attachment)(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
log.error(
|
||||||
|
'handleAttachmentsProcessing: failed to process attachment:',
|
||||||
|
err.stack
|
||||||
|
);
|
||||||
|
removeAttachment(conversationId, file.path)(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
toastToShow = { toastType: ToastType.UnableToLoadAttachment };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (toastToShow) {
|
||||||
|
dispatch({
|
||||||
|
type: SHOW_TOAST,
|
||||||
|
payload: toastToShow,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'NOOP',
|
type: 'NOOP',
|
||||||
payload: null,
|
payload: null,
|
||||||
|
@ -199,6 +312,70 @@ function processAttachments(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preProcessAttachment(
|
||||||
|
file: File,
|
||||||
|
draftAttachments: Array<AttachmentDraftType>
|
||||||
|
): { toastType: ToastType; parameters?: ReplacementValuesType } | undefined {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const limitKb = getMaximumAttachmentSize();
|
||||||
|
if (file.size > limitKb) {
|
||||||
|
return {
|
||||||
|
toastType: ToastType.FileSize,
|
||||||
|
parameters: getRenderDetailsForLimit(limitKb),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFileDangerous(file.name)) {
|
||||||
|
return { toastType: ToastType.DangerousFileType };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftAttachments.length >= 32) {
|
||||||
|
return { toastType: ToastType.MaxAttachments };
|
||||||
|
}
|
||||||
|
|
||||||
|
const haveNonImageOrVideo = draftAttachments.some(
|
||||||
|
(attachment: AttachmentDraftType) => {
|
||||||
|
return (
|
||||||
|
!isImage(attachment.contentType) && !isVideo(attachment.contentType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// You can't add another attachment if you already have a non-image staged
|
||||||
|
if (haveNonImageOrVideo) {
|
||||||
|
return { toastType: ToastType.UnsupportedMultiAttachment };
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileType = stringToMIMEType(file.type);
|
||||||
|
const imageOrVideo = isImage(fileType) || isVideo(fileType);
|
||||||
|
|
||||||
|
// You can't add a non-image attachment if you already have attachments staged
|
||||||
|
if (!imageOrVideo && draftAttachments.length > 0) {
|
||||||
|
return { toastType: ToastType.CannotMixMultiAndNonMultiAttachments };
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPendingAttachment(file: File): AttachmentDraftType | undefined {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileType = stringToMIMEType(file.type);
|
||||||
|
const { name: fileName } = path.parse(file.name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentType: fileType,
|
||||||
|
fileName,
|
||||||
|
size: file.size,
|
||||||
|
path: file.name,
|
||||||
|
pending: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function removeAttachment(
|
function removeAttachment(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
filePath: string
|
filePath: string
|
||||||
|
|
|
@ -6,12 +6,16 @@ import type { ReplacementValuesType } from '../../types/Util';
|
||||||
|
|
||||||
export enum ToastType {
|
export enum ToastType {
|
||||||
AddingUserToGroup = 'AddingUserToGroup',
|
AddingUserToGroup = 'AddingUserToGroup',
|
||||||
|
CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments',
|
||||||
CannotStartGroupCall = 'CannotStartGroupCall',
|
CannotStartGroupCall = 'CannotStartGroupCall',
|
||||||
CopiedUsername = 'CopiedUsername',
|
CopiedUsername = 'CopiedUsername',
|
||||||
CopiedUsernameLink = 'CopiedUsernameLink',
|
CopiedUsernameLink = 'CopiedUsernameLink',
|
||||||
|
DangerousFileType = 'DangerousFileType',
|
||||||
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
|
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
FailedToDeleteUsername = 'FailedToDeleteUsername',
|
FailedToDeleteUsername = 'FailedToDeleteUsername',
|
||||||
|
FileSize = 'FileSize',
|
||||||
|
MaxAttachments = 'MaxAttachments',
|
||||||
MessageBodyTooLong = 'MessageBodyTooLong',
|
MessageBodyTooLong = 'MessageBodyTooLong',
|
||||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||||
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
||||||
|
@ -21,6 +25,8 @@ export enum ToastType {
|
||||||
StoryVideoError = 'StoryVideoError',
|
StoryVideoError = 'StoryVideoError',
|
||||||
StoryVideoTooLong = 'StoryVideoTooLong',
|
StoryVideoTooLong = 'StoryVideoTooLong',
|
||||||
StoryVideoUnsupported = 'StoryVideoUnsupported',
|
StoryVideoUnsupported = 'StoryVideoUnsupported',
|
||||||
|
UnableToLoadAttachment = 'UnableToLoadAttachment',
|
||||||
|
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
||||||
UserAddedToGroup = 'UserAddedToGroup',
|
UserAddedToGroup = 'UserAddedToGroup',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type { TimelinePropsType } from './Timeline';
|
||||||
import { SmartTimeline } from './Timeline';
|
import { SmartTimeline } from './Timeline';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
|
conversationId: string;
|
||||||
compositionAreaProps: Pick<
|
compositionAreaProps: Pick<
|
||||||
CompositionAreaPropsType,
|
CompositionAreaPropsType,
|
||||||
| 'clearQuotedMessage'
|
| 'clearQuotedMessage'
|
||||||
|
@ -38,10 +39,15 @@ export type PropsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (_state: StateType, props: PropsType) => {
|
const mapStateToProps = (_state: StateType, props: PropsType) => {
|
||||||
const { compositionAreaProps, conversationHeaderProps, timelineProps } =
|
const {
|
||||||
props;
|
compositionAreaProps,
|
||||||
|
conversationHeaderProps,
|
||||||
|
conversationId,
|
||||||
|
timelineProps,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
conversationId,
|
||||||
renderCompositionArea: () => (
|
renderCompositionArea: () => (
|
||||||
<SmartCompositionArea {...compositionAreaProps} />
|
<SmartCompositionArea {...compositionAreaProps} />
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export enum AttachmentToastType {
|
|
||||||
ToastCannotMixMultiAndNonMultiAttachments,
|
|
||||||
ToastDangerousFileType,
|
|
||||||
ToastFileSize,
|
|
||||||
ToastMaxAttachments,
|
|
||||||
ToastUnsupportedMultiAttachment,
|
|
||||||
ToastUnableToLoadAttachment,
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import {
|
|
||||||
getPendingAttachment,
|
|
||||||
preProcessAttachment,
|
|
||||||
processAttachment,
|
|
||||||
} from './processAttachment';
|
|
||||||
import type {
|
|
||||||
AttachmentDraftType,
|
|
||||||
InMemoryAttachmentDraftType,
|
|
||||||
} from '../types/Attachment';
|
|
||||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
|
||||||
import * as log from '../logging/log';
|
|
||||||
|
|
||||||
export type AddAttachmentActionType = (
|
|
||||||
conversationId: string,
|
|
||||||
attachment: InMemoryAttachmentDraftType
|
|
||||||
) => unknown;
|
|
||||||
export type AddPendingAttachmentActionType = (
|
|
||||||
conversationId: string,
|
|
||||||
pendingAttachment: AttachmentDraftType
|
|
||||||
) => unknown;
|
|
||||||
export type RemoveAttachmentActionType = (
|
|
||||||
conversationId: string,
|
|
||||||
filePath: string
|
|
||||||
) => unknown;
|
|
||||||
|
|
||||||
export type HandleAttachmentsProcessingArgsType = {
|
|
||||||
addAttachment: AddAttachmentActionType;
|
|
||||||
addPendingAttachment: AddPendingAttachmentActionType;
|
|
||||||
conversationId: string;
|
|
||||||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
|
||||||
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 != null) {
|
|
||||||
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) {
|
|
||||||
log.error(
|
|
||||||
'handleAttachmentsProcessing: failed to process attachment:',
|
|
||||||
err.stack
|
|
||||||
);
|
|
||||||
removeAttachment(conversationId, file.path);
|
|
||||||
onShowToast(AttachmentToastType.ToastUnableToLoadAttachment);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type { AttachmentType } from '../types/Attachment';
|
|
||||||
import { getMaximumAttachmentSize } from './attachments';
|
|
||||||
import { showToast } from './showToast';
|
|
||||||
import { ToastFileSize } from '../components/ToastFileSize';
|
|
||||||
|
|
||||||
export function isAttachmentSizeOkay(
|
|
||||||
attachment: Readonly<AttachmentType>
|
|
||||||
): boolean {
|
|
||||||
const limitKb = getMaximumAttachmentSize();
|
|
||||||
// this needs to be cast properly
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
if ((attachment.data.byteLength / 1024).toFixed(4) >= limitKb) {
|
|
||||||
const units = ['kB', 'MB', 'GB'];
|
|
||||||
let u = -1;
|
|
||||||
let limit = limitKb * 1000;
|
|
||||||
do {
|
|
||||||
limit /= 1000;
|
|
||||||
u += 1;
|
|
||||||
} while (limit >= 1000 && u < units.length - 1);
|
|
||||||
showToast(ToastFileSize, {
|
|
||||||
limit,
|
|
||||||
units: units[u],
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
|
@ -1,85 +1,20 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type {
|
import type {
|
||||||
AttachmentDraftType,
|
AttachmentType,
|
||||||
InMemoryAttachmentDraftType,
|
InMemoryAttachmentDraftType,
|
||||||
} from '../types/Attachment';
|
} from '../types/Attachment';
|
||||||
import * as Errors from '../types/errors';
|
|
||||||
import { getMaximumAttachmentSize } from './attachments';
|
import { getMaximumAttachmentSize } from './attachments';
|
||||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
import * as Errors from '../types/errors';
|
||||||
import { fileToBytes } from './fileToBytes';
|
import { fileToBytes } from './fileToBytes';
|
||||||
import { handleImageAttachment } from './handleImageAttachment';
|
import { handleImageAttachment } from './handleImageAttachment';
|
||||||
import { handleVideoAttachment } from './handleVideoAttachment';
|
import { handleVideoAttachment } from './handleVideoAttachment';
|
||||||
import { isAttachmentSizeOkay } from './isAttachmentSizeOkay';
|
import { isHeic, stringToMIMEType } from '../types/MIME';
|
||||||
import { isFileDangerous } from './isFileDangerous';
|
|
||||||
import { isHeic, isImage, isVideo, stringToMIMEType } from '../types/MIME';
|
|
||||||
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
|
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
|
||||||
|
import { showToast } from './showToast';
|
||||||
export function getPendingAttachment(
|
import { ToastFileSize } from '../components/ToastFileSize';
|
||||||
file: File
|
|
||||||
): AttachmentDraftType | undefined {
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileType = stringToMIMEType(file.type);
|
|
||||||
const { name: fileName } = path.parse(file.name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentType: fileType,
|
|
||||||
fileName,
|
|
||||||
size: file.size,
|
|
||||||
path: file.name,
|
|
||||||
pending: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preProcessAttachment(
|
|
||||||
file: File,
|
|
||||||
draftAttachments: Array<AttachmentDraftType>
|
|
||||||
): AttachmentToastType | undefined {
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > getMaximumAttachmentSize()) {
|
|
||||||
return AttachmentToastType.ToastFileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFileDangerous(file.name)) {
|
|
||||||
return AttachmentToastType.ToastDangerousFileType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draftAttachments.length >= 32) {
|
|
||||||
return AttachmentToastType.ToastMaxAttachments;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haveNonImageOrVideo = draftAttachments.some(
|
|
||||||
(attachment: AttachmentDraftType) => {
|
|
||||||
return (
|
|
||||||
!isImage(attachment.contentType) && !isVideo(attachment.contentType)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// You can't add another attachment if you already have a non-image staged
|
|
||||||
if (haveNonImageOrVideo) {
|
|
||||||
return AttachmentToastType.ToastUnsupportedMultiAttachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileType = stringToMIMEType(file.type);
|
|
||||||
const imageOrVideo = isImage(fileType) || isVideo(fileType);
|
|
||||||
|
|
||||||
// You can't add a non-image attachment if you already have attachments staged
|
|
||||||
if (!imageOrVideo && draftAttachments.length > 0) {
|
|
||||||
return AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processAttachment(
|
export async function processAttachment(
|
||||||
file: File
|
file: File
|
||||||
|
@ -132,3 +67,34 @@ export async function processAttachment(
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRenderDetailsForLimit(limitKb: number): {
|
||||||
|
limit: string;
|
||||||
|
units: string;
|
||||||
|
} {
|
||||||
|
const units = ['kB', 'MB', 'GB'];
|
||||||
|
let u = -1;
|
||||||
|
let limit = limitKb * 1000;
|
||||||
|
do {
|
||||||
|
limit /= 1000;
|
||||||
|
u += 1;
|
||||||
|
} while (limit >= 1000 && u < units.length - 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
limit: limit.toFixed(0),
|
||||||
|
units: units[u],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAttachmentSizeOkay(attachment: Readonly<AttachmentType>): boolean {
|
||||||
|
const limitKb = getMaximumAttachmentSize();
|
||||||
|
// this needs to be cast properly
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
if ((attachment.data.byteLength / 1024).toFixed(4) >= limitKb) {
|
||||||
|
showToast(ToastFileSize, getRenderDetailsForLimit(limitKb));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMem
|
||||||
import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
|
import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
|
||||||
import type { ToastBlocked } from '../components/ToastBlocked';
|
import type { ToastBlocked } from '../components/ToastBlocked';
|
||||||
import type { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
import type { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||||
import type { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
|
|
||||||
import type {
|
import type {
|
||||||
ToastCannotOpenGiftBadge,
|
ToastCannotOpenGiftBadge,
|
||||||
ToastPropsType as ToastCannotOpenGiftBadgePropsType,
|
ToastPropsType as ToastCannotOpenGiftBadgePropsType,
|
||||||
|
@ -21,7 +20,6 @@ import type {
|
||||||
} from '../components/ToastConversationArchived';
|
} from '../components/ToastConversationArchived';
|
||||||
import type { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
import type { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||||
import type { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
import type { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||||
import type { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
|
||||||
import type {
|
import type {
|
||||||
ToastInternalError,
|
ToastInternalError,
|
||||||
ToastPropsType as ToastInternalErrorPropsType,
|
ToastPropsType as ToastInternalErrorPropsType,
|
||||||
|
@ -40,7 +38,6 @@ import type { ToastInvalidConversation } from '../components/ToastInvalidConvers
|
||||||
import type { ToastLeftGroup } from '../components/ToastLeftGroup';
|
import type { ToastLeftGroup } from '../components/ToastLeftGroup';
|
||||||
import type { ToastLinkCopied } from '../components/ToastLinkCopied';
|
import type { ToastLinkCopied } from '../components/ToastLinkCopied';
|
||||||
import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
|
import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
|
||||||
import type { ToastMaxAttachments } from '../components/ToastMaxAttachments';
|
|
||||||
import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
||||||
|
|
||||||
import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||||
|
@ -56,7 +53,6 @@ export function showToast(Toast: typeof ToastAlreadyGroupMember): void;
|
||||||
export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void;
|
export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void;
|
||||||
export function showToast(Toast: typeof ToastBlocked): void;
|
export function showToast(Toast: typeof ToastBlocked): void;
|
||||||
export function showToast(Toast: typeof ToastBlockedGroup): void;
|
export function showToast(Toast: typeof ToastBlockedGroup): void;
|
||||||
export function showToast(Toast: typeof ToastUnsupportedMultiAttachment): void;
|
|
||||||
export function showToast(
|
export function showToast(
|
||||||
Toast: typeof ToastCannotOpenGiftBadge,
|
Toast: typeof ToastCannotOpenGiftBadge,
|
||||||
props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>
|
props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>
|
||||||
|
@ -69,7 +65,6 @@ export function showToast(
|
||||||
): void;
|
): void;
|
||||||
export function showToast(Toast: typeof ToastConversationMarkedUnread): void;
|
export function showToast(Toast: typeof ToastConversationMarkedUnread): void;
|
||||||
export function showToast(Toast: typeof ToastConversationUnarchived): void;
|
export function showToast(Toast: typeof ToastConversationUnarchived): void;
|
||||||
export function showToast(Toast: typeof ToastDangerousFileType): void;
|
|
||||||
export function showToast(
|
export function showToast(
|
||||||
Toast: typeof ToastInternalError,
|
Toast: typeof ToastInternalError,
|
||||||
props: ToastInternalErrorPropsType
|
props: ToastInternalErrorPropsType
|
||||||
|
@ -88,9 +83,7 @@ export function showToast(Toast: typeof ToastInvalidConversation): void;
|
||||||
export function showToast(Toast: typeof ToastLeftGroup): void;
|
export function showToast(Toast: typeof ToastLeftGroup): void;
|
||||||
export function showToast(Toast: typeof ToastLinkCopied): void;
|
export function showToast(Toast: typeof ToastLinkCopied): void;
|
||||||
export function showToast(Toast: typeof ToastLoadingFullLogs): void;
|
export function showToast(Toast: typeof ToastLoadingFullLogs): void;
|
||||||
export function showToast(Toast: typeof ToastMaxAttachments): void;
|
|
||||||
export function showToast(Toast: typeof ToastMessageBodyTooLong): void;
|
export function showToast(Toast: typeof ToastMessageBodyTooLong): void;
|
||||||
export function showToast(Toast: typeof ToastUnsupportedMultiAttachment): void;
|
|
||||||
export function showToast(Toast: typeof ToastOriginalMessageNotFound): void;
|
export function showToast(Toast: typeof ToastOriginalMessageNotFound): void;
|
||||||
export function showToast(Toast: typeof ToastReactionFailed): void;
|
export function showToast(Toast: typeof ToastReactionFailed): void;
|
||||||
export function showToast(Toast: typeof ToastStickerPackInstallFailed): void;
|
export function showToast(Toast: typeof ToastStickerPackInstallFailed): void;
|
||||||
|
|
|
@ -19,10 +19,8 @@ import type { ConversationModel } from '../models/conversations';
|
||||||
import type {
|
import type {
|
||||||
GroupV2PendingMemberType,
|
GroupV2PendingMemberType,
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
QuotedMessageType,
|
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem';
|
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem';
|
||||||
import type { MessageModel } from '../models/messages';
|
|
||||||
import { getMessageById } from '../messages/getMessageById';
|
import { getMessageById } from '../messages/getMessageById';
|
||||||
import { getContactId } from '../messages/helpers';
|
import { getContactId } from '../messages/helpers';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
@ -53,22 +51,17 @@ import { ConversationDetailsMembershipList } from '../components/conversation/co
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||||
import { createConversationView } from '../state/roots/createConversationView';
|
import { createConversationView } from '../state/roots/createConversationView';
|
||||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
|
||||||
import type { CompositionAPIType } from '../components/CompositionArea';
|
import type { CompositionAPIType } from '../components/CompositionArea';
|
||||||
import { ToastBlocked } from '../components/ToastBlocked';
|
import { ToastBlocked } from '../components/ToastBlocked';
|
||||||
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||||
import { ToastCannotMixMultiAndNonMultiAttachments } from '../components/ToastCannotMixMultiAndNonMultiAttachments';
|
|
||||||
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';
|
||||||
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||||
import { ToastExpired } from '../components/ToastExpired';
|
import { ToastExpired } from '../components/ToastExpired';
|
||||||
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 { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
|
|
||||||
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||||
import { ToastReactionFailed } from '../components/ToastReactionFailed';
|
import { ToastReactionFailed } from '../components/ToastReactionFailed';
|
||||||
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
||||||
|
@ -81,7 +74,6 @@ import { isNotNil } from '../util/isNotNil';
|
||||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||||
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
|
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
|
||||||
import { showToast } from '../util/showToast';
|
import { showToast } from '../util/showToast';
|
||||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
|
||||||
import { UUIDKind } from '../types/UUID';
|
import { UUIDKind } from '../types/UUID';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone';
|
import { retryDeleteForEveryone } from '../util/retryDeleteForEveryone';
|
||||||
|
@ -90,7 +82,6 @@ import { MediaGallery } from '../components/conversation/media-gallery/MediaGall
|
||||||
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
|
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
|
||||||
import {
|
import {
|
||||||
getLinkPreviewForSend,
|
getLinkPreviewForSend,
|
||||||
hasLinkPreviewLoaded,
|
|
||||||
maybeGrabLinkPreview,
|
maybeGrabLinkPreview,
|
||||||
removeLinkPreview,
|
removeLinkPreview,
|
||||||
resetLinkPreview,
|
resetLinkPreview,
|
||||||
|
@ -200,10 +191,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
} = { current: undefined };
|
} = { current: undefined };
|
||||||
private sendStart?: number;
|
private sendStart?: number;
|
||||||
|
|
||||||
// Quotes
|
|
||||||
private quote?: QuotedMessageType;
|
|
||||||
private quotedMessage?: MessageModel;
|
|
||||||
|
|
||||||
// Sub-views
|
// Sub-views
|
||||||
private contactModalView?: Backbone.View;
|
private contactModalView?: Backbone.View;
|
||||||
private conversationView?: Backbone.View;
|
private conversationView?: Backbone.View;
|
||||||
|
@ -241,8 +228,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
this.model,
|
this.model,
|
||||||
'toggle-reply',
|
'toggle-reply',
|
||||||
(messageId: string | undefined) => {
|
(messageId: string | undefined) => {
|
||||||
const target = this.quote || !messageId ? null : messageId;
|
const composerState = window.reduxStore
|
||||||
this.setQuoteMessage(target);
|
? window.reduxStore.getState().composer
|
||||||
|
: undefined;
|
||||||
|
const quote = composerState?.quotedMessage?.quote;
|
||||||
|
|
||||||
|
this.setQuoteMessage(quote ? undefined : messageId);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.listenTo(
|
this.listenTo(
|
||||||
|
@ -458,7 +449,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
|
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
|
||||||
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
|
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
|
||||||
getQuotedMessage: () => this.model.get('quotedMessageId'),
|
getQuotedMessage: () => this.model.get('quotedMessageId'),
|
||||||
clearQuotedMessage: () => this.setQuoteMessage(null),
|
clearQuotedMessage: () => this.setQuoteMessage(undefined),
|
||||||
onStartGroupMigration: () => this.startMigrationToGV2(),
|
onStartGroupMigration: () => this.startMigrationToGV2(),
|
||||||
onCancelJoinRequest: async () => {
|
onCancelJoinRequest: async () => {
|
||||||
await window.showConfirmationDialog({
|
await window.showConfirmationDialog({
|
||||||
|
@ -516,6 +507,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
// createConversationView root
|
// createConversationView root
|
||||||
|
|
||||||
const JSX = createConversationView(window.reduxStore, {
|
const JSX = createConversationView(window.reduxStore, {
|
||||||
|
conversationId: this.model.id,
|
||||||
compositionAreaProps,
|
compositionAreaProps,
|
||||||
conversationHeaderProps,
|
conversationHeaderProps,
|
||||||
timelineProps,
|
timelineProps,
|
||||||
|
@ -750,59 +742,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO DESKTOP-2426
|
|
||||||
async processAttachments(files: Array<File>): Promise<void> {
|
|
||||||
const state = window.reduxStore.getState();
|
|
||||||
|
|
||||||
const isRecording =
|
|
||||||
state.audioRecorder.recordingState === RecordingState.Recording;
|
|
||||||
|
|
||||||
if (hasLinkPreviewLoaded() || isRecording) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.ToastUnsupportedMultiAttachment
|
|
||||||
) {
|
|
||||||
showToast(ToastUnsupportedMultiAttachment);
|
|
||||||
} else if (
|
|
||||||
toastType ===
|
|
||||||
AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments
|
|
||||||
) {
|
|
||||||
showToast(ToastCannotMixMultiAndNonMultiAttachments);
|
|
||||||
} else if (
|
|
||||||
toastType === AttachmentToastType.ToastUnableToLoadAttachment
|
|
||||||
) {
|
|
||||||
showToast(ToastUnableToLoadAttachment);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeAttachment,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unload(reason: string): void {
|
unload(reason: string): void {
|
||||||
log.info(
|
log.info(
|
||||||
'unloading conversation',
|
'unloading conversation',
|
||||||
|
@ -865,101 +804,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
this.remove();
|
this.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onDrop(e: JQuery.TriggeredEvent): Promise<void> {
|
|
||||||
if (!e.originalEvent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const event = e.originalEvent as DragEvent;
|
|
||||||
if (!event.dataTransfer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.dataTransfer.types[0] !== 'Files') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const { files } = event.dataTransfer;
|
|
||||||
this.processAttachments(Array.from(files));
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaste(e: JQuery.TriggeredEvent): void {
|
|
||||||
if (!e.originalEvent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const event = e.originalEvent as ClipboardEvent;
|
|
||||||
if (!event.clipboardData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { items } = event.clipboardData;
|
|
||||||
|
|
||||||
const anyImages = [...items].some(
|
|
||||||
item => item.type.split('/')[0] === 'image'
|
|
||||||
);
|
|
||||||
if (!anyImages) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const files: Array<File> = [];
|
|
||||||
for (let i = 0; i < items.length; i += 1) {
|
|
||||||
if (items[i].type.split('/')[0] === 'image') {
|
|
||||||
const file = items[i].getAsFile();
|
|
||||||
if (file) {
|
|
||||||
files.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processAttachments(files);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveModel(): Promise<void> {
|
async saveModel(): Promise<void> {
|
||||||
window.Signal.Data.updateConversation(this.model.attributes);
|
window.Signal.Data.updateConversation(this.model.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAttachments(): Promise<void> {
|
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
|
||||||
this.model.set({
|
|
||||||
draftAttachments: [],
|
|
||||||
draftChanged: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateAttachmentsView();
|
|
||||||
|
|
||||||
// We're fine doing this all at once; at most it should be 32 attachments
|
|
||||||
await Promise.all([
|
|
||||||
this.saveModel(),
|
|
||||||
Promise.all(
|
|
||||||
draftAttachments.map(attachment => deleteDraftAttachment(attachment))
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFiles(options: { includePending: boolean }): boolean {
|
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
|
||||||
if (options.includePending) {
|
|
||||||
return draftAttachments.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return draftAttachments.some(item => !item.pending);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAttachmentsView(): void {
|
|
||||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
|
||||||
window.reduxActions.composer.replaceAttachments(
|
|
||||||
this.model.get('id'),
|
|
||||||
draftAttachments
|
|
||||||
);
|
|
||||||
if (this.hasFiles({ includePending: true })) {
|
|
||||||
removeLinkPreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async onOpened(messageId: string): Promise<void> {
|
async onOpened(messageId: string): Promise<void> {
|
||||||
this.model.onOpenStart();
|
this.model.onOpenStart();
|
||||||
|
|
||||||
|
@ -1219,26 +1067,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
focusMessageField(): void {
|
|
||||||
if (this.panels && this.panels.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.compositionApi.current?.focusInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
disableMessageField(): void {
|
|
||||||
this.compositionApi.current?.setDisabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
enableMessageField(): void {
|
|
||||||
this.compositionApi.current?.setDisabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetEmojiResults(): void {
|
|
||||||
this.compositionApi.current?.resetEmojiResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
showGV1Members(): void {
|
showGV1Members(): void {
|
||||||
const { contactCollection, id } = this.model;
|
const { contactCollection, id } = this.model;
|
||||||
|
|
||||||
|
@ -1922,74 +1750,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setQuoteMessage(messageId: null | string): Promise<void> {
|
|
||||||
const { model } = this;
|
|
||||||
const message = messageId ? await getMessageById(messageId) : undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
message &&
|
|
||||||
!canReply(
|
|
||||||
message.attributes,
|
|
||||||
window.ConversationController.getOurConversationIdOrThrow(),
|
|
||||||
findAndFormatContact
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message && !message.isNormalBubble()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.quote = undefined;
|
|
||||||
this.quotedMessage = undefined;
|
|
||||||
|
|
||||||
const existing = model.get('quotedMessageId');
|
|
||||||
if (existing !== messageId) {
|
|
||||||
const now = Date.now();
|
|
||||||
let active_at = this.model.get('active_at');
|
|
||||||
let timestamp = this.model.get('timestamp');
|
|
||||||
|
|
||||||
if (!active_at && messageId) {
|
|
||||||
active_at = now;
|
|
||||||
timestamp = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.model.set({
|
|
||||||
active_at,
|
|
||||||
draftChanged: true,
|
|
||||||
quotedMessageId: messageId,
|
|
||||||
timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.saveModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
this.quotedMessage = message;
|
|
||||||
this.quote = await model.makeQuote(this.quotedMessage);
|
|
||||||
|
|
||||||
this.enableMessageField();
|
|
||||||
this.focusMessageField();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderQuotedMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderQuotedMessage(): void {
|
|
||||||
const { model }: { model: ConversationModel } = this;
|
|
||||||
|
|
||||||
if (!this.quotedMessage) {
|
|
||||||
window.reduxActions.composer.setQuotedMessage(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.reduxActions.composer.setQuotedMessage({
|
|
||||||
conversationId: model.id,
|
|
||||||
quote: this.quote,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showInvalidMessageToast(messageText?: string): boolean {
|
showInvalidMessageToast(messageText?: string): boolean {
|
||||||
const { model }: { model: ConversationModel } = this;
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
|
||||||
|
@ -2105,9 +1865,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
).filter(isNotNil);
|
).filter(isNotNil);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldSendHighQualityAttachments = window.reduxStore
|
const composerState = window.reduxStore
|
||||||
? window.reduxStore.getState().composer.shouldSendHighQualityAttachments
|
? window.reduxStore.getState().composer
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const shouldSendHighQualityAttachments =
|
||||||
|
composerState?.shouldSendHighQualityAttachments;
|
||||||
|
const quote = composerState?.quotedMessage?.quote;
|
||||||
|
|
||||||
const sendHQImages =
|
const sendHQImages =
|
||||||
shouldSendHighQualityAttachments !== undefined
|
shouldSendHighQualityAttachments !== undefined
|
||||||
|
@ -2122,7 +1885,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
{
|
{
|
||||||
body: message,
|
body: message,
|
||||||
attachments,
|
attachments,
|
||||||
quote: this.quote,
|
quote,
|
||||||
preview: getLinkPreviewForSend(message),
|
preview: getLinkPreviewForSend(message),
|
||||||
mentions,
|
mentions,
|
||||||
},
|
},
|
||||||
|
@ -2132,7 +1895,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
extraReduxActions: () => {
|
extraReduxActions: () => {
|
||||||
this.compositionApi.current?.reset();
|
this.compositionApi.current?.reset();
|
||||||
this.model.setMarkedUnread(false);
|
this.model.setMarkedUnread(false);
|
||||||
this.setQuoteMessage(null);
|
this.setQuoteMessage(undefined);
|
||||||
resetLinkPreview();
|
resetLinkPreview();
|
||||||
this.clearAttachments();
|
this.clearAttachments();
|
||||||
window.reduxActions.composer.resetComposer();
|
window.reduxActions.composer.resetComposer();
|
||||||
|
@ -2149,12 +1912,35 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focusMessageField(): void {
|
||||||
|
if (this.panels && this.panels.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.compositionApi.current?.focusInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
disableMessageField(): void {
|
||||||
|
this.compositionApi.current?.setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
enableMessageField(): void {
|
||||||
|
this.compositionApi.current?.setDisabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetEmojiResults(): void {
|
||||||
|
this.compositionApi.current?.resetEmojiResults();
|
||||||
|
}
|
||||||
|
|
||||||
onEditorStateChange(
|
onEditorStateChange(
|
||||||
messageText: string,
|
messageText: string,
|
||||||
bodyRanges: DraftBodyRangesType,
|
bodyRanges: DraftBodyRangesType,
|
||||||
caretLocation?: number
|
caretLocation?: number
|
||||||
): void {
|
): void {
|
||||||
this.maybeBumpTyping(messageText);
|
if (messageText.length && this.model.throttledBumpTyping) {
|
||||||
|
this.model.throttledBumpTyping();
|
||||||
|
}
|
||||||
|
|
||||||
this.debouncedSaveDraft(messageText, bodyRanges);
|
this.debouncedSaveDraft(messageText, bodyRanges);
|
||||||
|
|
||||||
// If we have attachments, don't add link preview
|
// If we have attachments, don't add link preview
|
||||||
|
@ -2206,11 +1992,95 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called whenever the user changes the message composition field. But only
|
async setQuoteMessage(messageId: string | undefined): Promise<void> {
|
||||||
// fires if there's content in the message field after the change.
|
const { model } = this;
|
||||||
maybeBumpTyping(messageText: string): void {
|
const message = messageId ? await getMessageById(messageId) : undefined;
|
||||||
if (messageText.length && this.model.throttledBumpTyping) {
|
|
||||||
this.model.throttledBumpTyping();
|
if (
|
||||||
|
message &&
|
||||||
|
!canReply(
|
||||||
|
message.attributes,
|
||||||
|
window.ConversationController.getOurConversationIdOrThrow(),
|
||||||
|
findAndFormatContact
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message && !message.isNormalBubble()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = model.get('quotedMessageId');
|
||||||
|
if (existing !== messageId) {
|
||||||
|
const now = Date.now();
|
||||||
|
let active_at = this.model.get('active_at');
|
||||||
|
let timestamp = this.model.get('timestamp');
|
||||||
|
|
||||||
|
if (!active_at && messageId) {
|
||||||
|
active_at = now;
|
||||||
|
timestamp = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model.set({
|
||||||
|
active_at,
|
||||||
|
draftChanged: true,
|
||||||
|
quotedMessageId: messageId,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.saveModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
const quote = await model.makeQuote(message);
|
||||||
|
window.reduxActions.composer.setQuotedMessage({
|
||||||
|
conversationId: model.id,
|
||||||
|
quote,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.enableMessageField();
|
||||||
|
this.focusMessageField();
|
||||||
|
} else {
|
||||||
|
window.reduxActions.composer.setQuotedMessage(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAttachments(): Promise<void> {
|
||||||
|
const draftAttachments = this.model.get('draftAttachments') || [];
|
||||||
|
this.model.set({
|
||||||
|
draftAttachments: [],
|
||||||
|
draftChanged: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateAttachmentsView();
|
||||||
|
|
||||||
|
// We're fine doing this all at once; at most it should be 32 attachments
|
||||||
|
await Promise.all([
|
||||||
|
this.saveModel(),
|
||||||
|
Promise.all(
|
||||||
|
draftAttachments.map(attachment => deleteDraftAttachment(attachment))
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFiles(options: { includePending: boolean }): boolean {
|
||||||
|
const draftAttachments = this.model.get('draftAttachments') || [];
|
||||||
|
if (options.includePending) {
|
||||||
|
return draftAttachments.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return draftAttachments.some(item => !item.pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAttachmentsView(): void {
|
||||||
|
const draftAttachments = this.model.get('draftAttachments') || [];
|
||||||
|
window.reduxActions.composer.replaceAttachments(
|
||||||
|
this.model.get('id'),
|
||||||
|
draftAttachments
|
||||||
|
);
|
||||||
|
if (this.hasFiles({ includePending: true })) {
|
||||||
|
removeLinkPreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue