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"
|
||||
},
|
||||
"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": {
|
||||
"message": "Unable to load selected attachment."
|
||||
|
|
|
@ -33,7 +33,6 @@ export default {
|
|||
|
||||
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
addAttachment: action('addAttachment'),
|
||||
addPendingAttachment: action('addPendingAttachment'),
|
||||
conversationId: '123',
|
||||
i18n,
|
||||
onSendMessage: action('onSendMessage'),
|
||||
|
|
|
@ -12,7 +12,6 @@ import type {
|
|||
} from '../types/Util';
|
||||
import type { ErrorDialogAudioRecorderType } from '../state/ducks/audioRecorder';
|
||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
|
||||
import type { imageToBlurHash } from '../util/imageToBlurHash';
|
||||
import { Spinner } from './Spinner';
|
||||
import type {
|
||||
|
@ -76,10 +75,6 @@ export type OwnProps = Readonly<{
|
|||
conversationId: string,
|
||||
attachment: InMemoryAttachmentDraftType
|
||||
) => unknown;
|
||||
addPendingAttachment: (
|
||||
conversationId: string,
|
||||
pendingAttachment: AttachmentDraftType
|
||||
) => unknown;
|
||||
announcementsOnly?: boolean;
|
||||
areWeAdmin?: boolean;
|
||||
areWePending?: boolean;
|
||||
|
@ -112,7 +107,10 @@ export type OwnProps = Readonly<{
|
|||
onClearAttachments(): unknown;
|
||||
onClickQuotedMessage(): unknown;
|
||||
onCloseLinkPreview(): unknown;
|
||||
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
|
||||
processAttachments: (options: {
|
||||
conversationId: string;
|
||||
files: ReadonlyArray<File>;
|
||||
}) => unknown;
|
||||
onSelectMediaQuality(isHQ: boolean): unknown;
|
||||
onSendMessage(options: {
|
||||
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
|
||||
|
@ -171,7 +169,6 @@ export type Props = Pick<
|
|||
export function CompositionArea({
|
||||
// Base props
|
||||
addAttachment,
|
||||
addPendingAttachment,
|
||||
conversationId,
|
||||
i18n,
|
||||
onSendMessage,
|
||||
|
@ -733,13 +730,10 @@ export function CompositionArea({
|
|||
</div>
|
||||
) : null}
|
||||
<CompositionUpload
|
||||
addAttachment={addAttachment}
|
||||
addPendingAttachment={addPendingAttachment}
|
||||
conversationId={conversationId}
|
||||
draftAttachments={draftAttachments}
|
||||
i18n={i18n}
|
||||
processAttachments={processAttachments}
|
||||
removeAttachment={removeAttachment}
|
||||
ref={fileInputRef}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,116 +2,43 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import type {
|
||||
InMemoryAttachmentDraftType,
|
||||
AttachmentDraftType,
|
||||
} from '../types/Attachment';
|
||||
import type { AttachmentDraftType } from '../types/Attachment';
|
||||
import { isVideoAttachment, isImageAttachment } from '../types/Attachment';
|
||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||
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 {
|
||||
getSupportedImageTypes,
|
||||
getSupportedVideoTypes,
|
||||
} from '../util/GoogleChrome';
|
||||
|
||||
export type PropsType = {
|
||||
addAttachment: (
|
||||
conversationId: string,
|
||||
attachment: InMemoryAttachmentDraftType
|
||||
) => unknown;
|
||||
addPendingAttachment: (
|
||||
conversationId: string,
|
||||
pendingAttachment: AttachmentDraftType
|
||||
) => unknown;
|
||||
conversationId: string;
|
||||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
||||
i18n: LocalizerType;
|
||||
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
|
||||
removeAttachment: (conversationId: string, filePath: string) => unknown;
|
||||
processAttachments: (options: {
|
||||
conversationId: string;
|
||||
files: ReadonlyArray<File>;
|
||||
}) => unknown;
|
||||
};
|
||||
|
||||
export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
||||
function CompositionUploadInner(
|
||||
{
|
||||
addAttachment,
|
||||
addPendingAttachment,
|
||||
conversationId,
|
||||
draftAttachments,
|
||||
i18n,
|
||||
processAttachments,
|
||||
removeAttachment,
|
||||
},
|
||||
{ conversationId, draftAttachments, processAttachments },
|
||||
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.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 => {
|
||||
return isImageAttachment(attachment) || isVideoAttachment(attachment);
|
||||
});
|
||||
|
@ -121,17 +48,14 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
|||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{toast}
|
||||
<input
|
||||
hidden
|
||||
multiple
|
||||
onChange={onFileInputChange}
|
||||
ref={ref}
|
||||
type="file"
|
||||
accept={acceptContentTypes?.join(',')}
|
||||
/>
|
||||
</>
|
||||
<input
|
||||
hidden
|
||||
multiple
|
||||
onChange={onFileInputChange}
|
||||
ref={ref}
|
||||
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 => (
|
||||
<ToastFileSize {...defaultProps} limit={100} units="MB" />
|
||||
<ToastFileSize {...defaultProps} limit="100" units="MB" />
|
||||
);
|
||||
|
||||
_ToastFileSize.story = {
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { LocalizerType } from '../types/Util';
|
|||
import { Toast } from './Toast';
|
||||
|
||||
export type ToastPropsType = {
|
||||
limit: number;
|
||||
limit: string;
|
||||
units: string;
|
||||
};
|
||||
|
||||
|
@ -23,8 +23,7 @@ export function ToastFileSize({
|
|||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose}>
|
||||
{i18n('fileSizeWarning')} {limit}
|
||||
{units}
|
||||
{i18n('icu:fileSizeWarning', { limit, units })}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,13 @@ AddingUserToGroup.args = {
|
|||
},
|
||||
};
|
||||
|
||||
export const CannotMixMultiAndNonMultiAttachments = Template.bind({});
|
||||
CannotMixMultiAndNonMultiAttachments.args = {
|
||||
toast: {
|
||||
toastType: ToastType.CannotMixMultiAndNonMultiAttachments,
|
||||
},
|
||||
};
|
||||
|
||||
export const CannotStartGroupCall = Template.bind({});
|
||||
CannotStartGroupCall.args = {
|
||||
toast: {
|
||||
|
@ -70,6 +77,13 @@ CopiedUsernameLink.args = {
|
|||
},
|
||||
};
|
||||
|
||||
export const DangerousFileType = Template.bind({});
|
||||
DangerousFileType.args = {
|
||||
toast: {
|
||||
toastType: ToastType.DangerousFileType,
|
||||
},
|
||||
};
|
||||
|
||||
export const DeleteForEveryoneFailed = Template.bind({});
|
||||
DeleteForEveryoneFailed.args = {
|
||||
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({});
|
||||
MessageBodyTooLong.args = {
|
||||
toast: {
|
||||
|
@ -105,13 +137,6 @@ PinnedConversationsFull.args = {
|
|||
},
|
||||
};
|
||||
|
||||
export const StoryMuted = Template.bind({});
|
||||
StoryMuted.args = {
|
||||
toast: {
|
||||
toastType: ToastType.StoryMuted,
|
||||
},
|
||||
};
|
||||
|
||||
export const ReportedSpamAndBlocked = Template.bind({});
|
||||
ReportedSpamAndBlocked.args = {
|
||||
toast: {
|
||||
|
@ -119,6 +144,13 @@ ReportedSpamAndBlocked.args = {
|
|||
},
|
||||
};
|
||||
|
||||
export const StoryMuted = Template.bind({});
|
||||
StoryMuted.args = {
|
||||
toast: {
|
||||
toastType: ToastType.StoryMuted,
|
||||
},
|
||||
};
|
||||
|
||||
export const StoryReact = Template.bind({});
|
||||
StoryReact.args = {
|
||||
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({});
|
||||
UserAddedToGroup.args = {
|
||||
toast: {
|
||||
|
|
|
@ -42,6 +42,14 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.CannotMixMultiAndNonMultiAttachments) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.CannotStartGroupCall) {
|
||||
return (
|
||||
<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) {
|
||||
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) {
|
||||
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) {
|
||||
return (
|
||||
<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';
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
processAttachments: (options: {
|
||||
conversationId: string;
|
||||
files: ReadonlyArray<File>;
|
||||
}) => void;
|
||||
renderCompositionArea: () => JSX.Element;
|
||||
renderConversationHeader: () => JSX.Element;
|
||||
renderTimeline: () => JSX.Element;
|
||||
};
|
||||
|
||||
export function ConversationView({
|
||||
conversationId,
|
||||
processAttachments,
|
||||
renderCompositionArea,
|
||||
renderConversationHeader,
|
||||
renderTimeline,
|
||||
}: 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 (
|
||||
<div className="ConversationView">
|
||||
<div className="ConversationView" onDrop={onDrop} onPaste={onPaste}>
|
||||
<div className="ConversationView__header">
|
||||
{renderConversationHeader()}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import * as log from '../../logging/log';
|
||||
|
@ -25,9 +27,19 @@ import { writeDraftAttachment } from '../../util/writeDraftAttachment';
|
|||
import { deleteDraftAttachment } from '../../util/deleteDraftAttachment';
|
||||
import { replaceIndex } from '../../util/replaceIndex';
|
||||
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
|
||||
import type { HandleAttachmentsProcessingArgsType } from '../../util/handleAttachmentsProcessing';
|
||||
import { handleAttachmentsProcessing } from '../../util/handleAttachmentsProcessing';
|
||||
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
|
||||
|
||||
|
@ -187,11 +199,112 @@ function addPendingAttachment(
|
|||
};
|
||||
}
|
||||
|
||||
function processAttachments(
|
||||
options: HandleAttachmentsProcessingArgsType
|
||||
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||
return async dispatch => {
|
||||
await handleAttachmentsProcessing(options);
|
||||
function processAttachments({
|
||||
conversationId,
|
||||
files,
|
||||
}: {
|
||||
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({
|
||||
type: 'NOOP',
|
||||
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(
|
||||
conversationId: string,
|
||||
filePath: string
|
||||
|
|
|
@ -6,12 +6,16 @@ import type { ReplacementValuesType } from '../../types/Util';
|
|||
|
||||
export enum ToastType {
|
||||
AddingUserToGroup = 'AddingUserToGroup',
|
||||
CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments',
|
||||
CannotStartGroupCall = 'CannotStartGroupCall',
|
||||
CopiedUsername = 'CopiedUsername',
|
||||
CopiedUsernameLink = 'CopiedUsernameLink',
|
||||
DangerousFileType = 'DangerousFileType',
|
||||
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
|
||||
Error = 'Error',
|
||||
FailedToDeleteUsername = 'FailedToDeleteUsername',
|
||||
FileSize = 'FileSize',
|
||||
MaxAttachments = 'MaxAttachments',
|
||||
MessageBodyTooLong = 'MessageBodyTooLong',
|
||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
||||
|
@ -21,6 +25,8 @@ export enum ToastType {
|
|||
StoryVideoError = 'StoryVideoError',
|
||||
StoryVideoTooLong = 'StoryVideoTooLong',
|
||||
StoryVideoUnsupported = 'StoryVideoUnsupported',
|
||||
UnableToLoadAttachment = 'UnableToLoadAttachment',
|
||||
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
||||
UserAddedToGroup = 'UserAddedToGroup',
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { TimelinePropsType } from './Timeline';
|
|||
import { SmartTimeline } from './Timeline';
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
compositionAreaProps: Pick<
|
||||
CompositionAreaPropsType,
|
||||
| 'clearQuotedMessage'
|
||||
|
@ -38,10 +39,15 @@ export type PropsType = {
|
|||
};
|
||||
|
||||
const mapStateToProps = (_state: StateType, props: PropsType) => {
|
||||
const { compositionAreaProps, conversationHeaderProps, timelineProps } =
|
||||
props;
|
||||
const {
|
||||
compositionAreaProps,
|
||||
conversationHeaderProps,
|
||||
conversationId,
|
||||
timelineProps,
|
||||
} = props;
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
renderCompositionArea: () => (
|
||||
<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
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import * as log from '../logging/log';
|
||||
import type {
|
||||
AttachmentDraftType,
|
||||
AttachmentType,
|
||||
InMemoryAttachmentDraftType,
|
||||
} from '../types/Attachment';
|
||||
import * as Errors from '../types/errors';
|
||||
import { getMaximumAttachmentSize } from './attachments';
|
||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||
import * as Errors from '../types/errors';
|
||||
import { fileToBytes } from './fileToBytes';
|
||||
import { handleImageAttachment } from './handleImageAttachment';
|
||||
import { handleVideoAttachment } from './handleVideoAttachment';
|
||||
import { isAttachmentSizeOkay } from './isAttachmentSizeOkay';
|
||||
import { isFileDangerous } from './isFileDangerous';
|
||||
import { isHeic, isImage, isVideo, stringToMIMEType } from '../types/MIME';
|
||||
import { isHeic, stringToMIMEType } from '../types/MIME';
|
||||
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
|
||||
|
||||
export 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,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
import { showToast } from './showToast';
|
||||
import { ToastFileSize } from '../components/ToastFileSize';
|
||||
|
||||
export async function processAttachment(
|
||||
file: File
|
||||
|
@ -132,3 +67,34 @@ export async function processAttachment(
|
|||
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 { ToastBlocked } from '../components/ToastBlocked';
|
||||
import type { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||
import type { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
|
||||
import type {
|
||||
ToastCannotOpenGiftBadge,
|
||||
ToastPropsType as ToastCannotOpenGiftBadgePropsType,
|
||||
|
@ -21,7 +20,6 @@ import type {
|
|||
} from '../components/ToastConversationArchived';
|
||||
import type { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||
import type { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||
import type { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||
import type {
|
||||
ToastInternalError,
|
||||
ToastPropsType as ToastInternalErrorPropsType,
|
||||
|
@ -40,7 +38,6 @@ import type { ToastInvalidConversation } from '../components/ToastInvalidConvers
|
|||
import type { ToastLeftGroup } from '../components/ToastLeftGroup';
|
||||
import type { ToastLinkCopied } from '../components/ToastLinkCopied';
|
||||
import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
|
||||
import type { ToastMaxAttachments } from '../components/ToastMaxAttachments';
|
||||
import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
||||
|
||||
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 ToastBlocked): void;
|
||||
export function showToast(Toast: typeof ToastBlockedGroup): void;
|
||||
export function showToast(Toast: typeof ToastUnsupportedMultiAttachment): void;
|
||||
export function showToast(
|
||||
Toast: typeof ToastCannotOpenGiftBadge,
|
||||
props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>
|
||||
|
@ -69,7 +65,6 @@ export function showToast(
|
|||
): void;
|
||||
export function showToast(Toast: typeof ToastConversationMarkedUnread): void;
|
||||
export function showToast(Toast: typeof ToastConversationUnarchived): void;
|
||||
export function showToast(Toast: typeof ToastDangerousFileType): void;
|
||||
export function showToast(
|
||||
Toast: typeof ToastInternalError,
|
||||
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 ToastLinkCopied): 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 ToastUnsupportedMultiAttachment): void;
|
||||
export function showToast(Toast: typeof ToastOriginalMessageNotFound): void;
|
||||
export function showToast(Toast: typeof ToastReactionFailed): void;
|
||||
export function showToast(Toast: typeof ToastStickerPackInstallFailed): void;
|
||||
|
|
|
@ -19,10 +19,8 @@ import type { ConversationModel } from '../models/conversations';
|
|||
import type {
|
||||
GroupV2PendingMemberType,
|
||||
MessageAttributesType,
|
||||
QuotedMessageType,
|
||||
} from '../model-types.d';
|
||||
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem';
|
||||
import type { MessageModel } from '../models/messages';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
import { getContactId } from '../messages/helpers';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
@ -53,22 +51,17 @@ import { ConversationDetailsMembershipList } from '../components/conversation/co
|
|||
import * as log from '../logging/log';
|
||||
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||
import { createConversationView } from '../state/roots/createConversationView';
|
||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||
import type { CompositionAPIType } from '../components/CompositionArea';
|
||||
import { ToastBlocked } from '../components/ToastBlocked';
|
||||
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||
import { ToastCannotMixMultiAndNonMultiAttachments } from '../components/ToastCannotMixMultiAndNonMultiAttachments';
|
||||
import { ToastConversationArchived } from '../components/ToastConversationArchived';
|
||||
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||
import { ToastExpired } from '../components/ToastExpired';
|
||||
import { ToastFileSize } from '../components/ToastFileSize';
|
||||
import { ToastInvalidConversation } from '../components/ToastInvalidConversation';
|
||||
import { ToastLeftGroup } from '../components/ToastLeftGroup';
|
||||
import { ToastMaxAttachments } from '../components/ToastMaxAttachments';
|
||||
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
||||
import { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
|
||||
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||
import { ToastReactionFailed } from '../components/ToastReactionFailed';
|
||||
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
||||
|
@ -81,7 +74,6 @@ import { isNotNil } from '../util/isNotNil';
|
|||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
|
||||
import { showToast } from '../util/showToast';
|
||||
import { RecordingState } from '../state/ducks/audioRecorder';
|
||||
import { UUIDKind } from '../types/UUID';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
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 {
|
||||
getLinkPreviewForSend,
|
||||
hasLinkPreviewLoaded,
|
||||
maybeGrabLinkPreview,
|
||||
removeLinkPreview,
|
||||
resetLinkPreview,
|
||||
|
@ -200,10 +191,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
} = { current: undefined };
|
||||
private sendStart?: number;
|
||||
|
||||
// Quotes
|
||||
private quote?: QuotedMessageType;
|
||||
private quotedMessage?: MessageModel;
|
||||
|
||||
// Sub-views
|
||||
private contactModalView?: Backbone.View;
|
||||
private conversationView?: Backbone.View;
|
||||
|
@ -241,8 +228,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
this.model,
|
||||
'toggle-reply',
|
||||
(messageId: string | undefined) => {
|
||||
const target = this.quote || !messageId ? null : messageId;
|
||||
this.setQuoteMessage(target);
|
||||
const composerState = window.reduxStore
|
||||
? window.reduxStore.getState().composer
|
||||
: undefined;
|
||||
const quote = composerState?.quotedMessage?.quote;
|
||||
|
||||
this.setQuoteMessage(quote ? undefined : messageId);
|
||||
}
|
||||
);
|
||||
this.listenTo(
|
||||
|
@ -458,7 +449,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
|
||||
onTextTooLong: () => showToast(ToastMessageBodyTooLong),
|
||||
getQuotedMessage: () => this.model.get('quotedMessageId'),
|
||||
clearQuotedMessage: () => this.setQuoteMessage(null),
|
||||
clearQuotedMessage: () => this.setQuoteMessage(undefined),
|
||||
onStartGroupMigration: () => this.startMigrationToGV2(),
|
||||
onCancelJoinRequest: async () => {
|
||||
await window.showConfirmationDialog({
|
||||
|
@ -516,6 +507,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
// createConversationView root
|
||||
|
||||
const JSX = createConversationView(window.reduxStore, {
|
||||
conversationId: this.model.id,
|
||||
compositionAreaProps,
|
||||
conversationHeaderProps,
|
||||
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 {
|
||||
log.info(
|
||||
'unloading conversation',
|
||||
|
@ -865,101 +804,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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> {
|
||||
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> {
|
||||
this.model.onOpenStart();
|
||||
|
||||
|
@ -1219,26 +1067,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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 {
|
||||
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 {
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
|
||||
|
@ -2105,9 +1865,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
).filter(isNotNil);
|
||||
}
|
||||
|
||||
const shouldSendHighQualityAttachments = window.reduxStore
|
||||
? window.reduxStore.getState().composer.shouldSendHighQualityAttachments
|
||||
const composerState = window.reduxStore
|
||||
? window.reduxStore.getState().composer
|
||||
: undefined;
|
||||
const shouldSendHighQualityAttachments =
|
||||
composerState?.shouldSendHighQualityAttachments;
|
||||
const quote = composerState?.quotedMessage?.quote;
|
||||
|
||||
const sendHQImages =
|
||||
shouldSendHighQualityAttachments !== undefined
|
||||
|
@ -2122,7 +1885,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
{
|
||||
body: message,
|
||||
attachments,
|
||||
quote: this.quote,
|
||||
quote,
|
||||
preview: getLinkPreviewForSend(message),
|
||||
mentions,
|
||||
},
|
||||
|
@ -2132,7 +1895,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
extraReduxActions: () => {
|
||||
this.compositionApi.current?.reset();
|
||||
this.model.setMarkedUnread(false);
|
||||
this.setQuoteMessage(null);
|
||||
this.setQuoteMessage(undefined);
|
||||
resetLinkPreview();
|
||||
this.clearAttachments();
|
||||
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(
|
||||
messageText: string,
|
||||
bodyRanges: DraftBodyRangesType,
|
||||
caretLocation?: number
|
||||
): void {
|
||||
this.maybeBumpTyping(messageText);
|
||||
if (messageText.length && this.model.throttledBumpTyping) {
|
||||
this.model.throttledBumpTyping();
|
||||
}
|
||||
|
||||
this.debouncedSaveDraft(messageText, bodyRanges);
|
||||
|
||||
// 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
|
||||
// fires if there's content in the message field after the change.
|
||||
maybeBumpTyping(messageText: string): void {
|
||||
if (messageText.length && this.model.throttledBumpTyping) {
|
||||
this.model.throttledBumpTyping();
|
||||
async setQuoteMessage(messageId: string | undefined): 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;
|
||||
}
|
||||
|
||||
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…
Reference in a new issue