ConversationView: Move attachments processing into redux

This commit is contained in:
Scott Nonnenberg 2022-12-07 17:26:59 -08:00 committed by GitHub
parent ff6750e4fd
commit 452e0b7b31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 544 additions and 763 deletions

View file

@ -33,7 +33,6 @@ export default {
const useProps = (overrideProps: Partial<Props> = {}): Props => ({
addAttachment: action('addAttachment'),
addPendingAttachment: action('addPendingAttachment'),
conversationId: '123',
i18n,
onSendMessage: action('onSendMessage'),

View file

@ -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>

View file

@ -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(',')}
/>
);
}
);

View file

@ -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',
};

View file

@ -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>
);
}

View file

@ -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 = {

View file

@ -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>
);
}

View file

@ -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: {

View file

@ -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}>

View file

@ -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',
};

View file

@ -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>;
}

View file

@ -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',
};

View file

@ -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',
};

View file

@ -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>
);
}

View file

@ -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>