Accept multiple images and videos in attachment picker

This commit is contained in:
Jamie Kyle 2022-09-15 14:40:48 -07:00 committed by GitHub
parent 6cfe2a09df
commit 01587b0f39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 87 additions and 54 deletions

View file

@ -557,12 +557,8 @@
"message": "Submit log", "message": "Submit log",
"description": "Label for the decryption error toast button" "description": "Label for the decryption error toast button"
}, },
"oneNonImageAtATimeToast": { "cannotSelectPhotosAndVideosAlongWithFiles": {
"message": "When including a non-image attachment, the limit is one attachment per message.", "message": "You can't select photos and videos along with files.",
"description": "An error popup when the user has attempted to add an attachment"
},
"cannotMixImageAndNonImageAttachments": {
"message": "You cannot mix non-image and image attachments in one message.",
"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"
}, },
"maximumAttachments": { "maximumAttachments": {
@ -2623,8 +2619,8 @@
"message": "Close Popup", "message": "Close Popup",
"description": "Used as alt text for any button closing a popup" "description": "Used as alt text for any button closing a popup"
}, },
"add-image-attachment": { "addImageOrVideoattachment": {
"message": "Add image attachment", "message": "Add image or video attachment",
"description": "Used in draft attachment list for the big 'add new attachment' button" "description": "Used in draft attachment list for the big 'add new attachment' button"
}, },
"remove-attachment": { "remove-attachment": {

View file

@ -8,16 +8,21 @@ import type {
InMemoryAttachmentDraftType, InMemoryAttachmentDraftType,
AttachmentDraftType, AttachmentDraftType,
} from '../types/Attachment'; } from '../types/Attachment';
import { isVideoAttachment, isImageAttachment } from '../types/Attachment';
import { AttachmentToastType } from '../types/AttachmentToastType'; import { AttachmentToastType } from '../types/AttachmentToastType';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { ToastCannotMixImageAndNonImageAttachments } from './ToastCannotMixImageAndNonImageAttachments'; import { ToastCannotMixMultiAndNonMultiAttachments } from './ToastCannotMixMultiAndNonMultiAttachments';
import { ToastDangerousFileType } from './ToastDangerousFileType'; import { ToastDangerousFileType } from './ToastDangerousFileType';
import { ToastFileSize } from './ToastFileSize'; import { ToastFileSize } from './ToastFileSize';
import { ToastMaxAttachments } from './ToastMaxAttachments'; import { ToastMaxAttachments } from './ToastMaxAttachments';
import { ToastOneNonImageAtATime } from './ToastOneNonImageAtATime'; import { ToastUnsupportedMultiAttachment } from './ToastUnsupportedMultiAttachment';
import { ToastUnableToLoadAttachment } from './ToastUnableToLoadAttachment'; import { ToastUnableToLoadAttachment } from './ToastUnableToLoadAttachment';
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing'; import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
import {
getSupportedImageTypes,
getSupportedVideoTypes,
} from '../util/GoogleChrome';
export type PropsType = { export type PropsType = {
addAttachment: ( addAttachment: (
@ -87,14 +92,18 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
toast = <ToastDangerousFileType i18n={i18n} onClose={closeToast} />; toast = <ToastDangerousFileType i18n={i18n} onClose={closeToast} />;
} else if (toastType === AttachmentToastType.ToastMaxAttachments) { } else if (toastType === AttachmentToastType.ToastMaxAttachments) {
toast = <ToastMaxAttachments i18n={i18n} onClose={closeToast} />; toast = <ToastMaxAttachments i18n={i18n} onClose={closeToast} />;
} else if (toastType === AttachmentToastType.ToastOneNonImageAtATime) {
toast = <ToastOneNonImageAtATime i18n={i18n} onClose={closeToast} />;
} else if ( } else if (
toastType === toastType === AttachmentToastType.ToastUnsupportedMultiAttachment
AttachmentToastType.ToastCannotMixImageAndNonImageAttachments
) { ) {
toast = ( toast = (
<ToastCannotMixImageAndNonImageAttachments <ToastUnsupportedMultiAttachment i18n={i18n} onClose={closeToast} />
);
} else if (
toastType ===
AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments
) {
toast = (
<ToastCannotMixMultiAndNonMultiAttachments
i18n={i18n} i18n={i18n}
onClose={closeToast} onClose={closeToast}
/> />
@ -103,6 +112,14 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
toast = <ToastUnableToLoadAttachment i18n={i18n} onClose={closeToast} />; toast = <ToastUnableToLoadAttachment i18n={i18n} onClose={closeToast} />;
} }
const anyVideoOrImageAttachments = draftAttachments.some(attachment => {
return isImageAttachment(attachment) || isVideoAttachment(attachment);
});
const acceptContentTypes = anyVideoOrImageAttachments
? [...getSupportedImageTypes(), ...getSupportedVideoTypes()]
: null;
return ( return (
<> <>
{toast} {toast}
@ -112,6 +129,7 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
onChange={onFileInputChange} onChange={onFileInputChange}
ref={ref} ref={ref}
type="file" type="file"
accept={acceptContentTypes?.join(',')}
/> />
</> </>
); );

View file

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { ToastCannotMixImageAndNonImageAttachments } from './ToastCannotMixImageAndNonImageAttachments'; import { ToastCannotMixMultiAndNonMultiAttachments } from './ToastCannotMixMultiAndNonMultiAttachments';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
@ -16,13 +16,13 @@ const defaultProps = {
}; };
export default { export default {
title: 'Components/ToastCannotMixImageAndNonImageAttachments', title: 'Components/ToastCannotMixMultiAndNonMultiAttachments',
}; };
export const _ToastCannotMixImageAndNonImageAttachments = (): JSX.Element => ( export const _ToastCannotMixMultiAndNonMultiAttachments = (): JSX.Element => (
<ToastCannotMixImageAndNonImageAttachments {...defaultProps} /> <ToastCannotMixMultiAndNonMultiAttachments {...defaultProps} />
); );
_ToastCannotMixImageAndNonImageAttachments.story = { _ToastCannotMixMultiAndNonMultiAttachments.story = {
name: 'ToastCannotMixImageAndNonImageAttachments', name: 'ToastCannotMixMultiAndNonMultiAttachments',
}; };

View file

@ -10,9 +10,13 @@ type PropsType = {
onClose: () => unknown; onClose: () => unknown;
}; };
export const ToastOneNonImageAtATime = ({ export const ToastCannotMixMultiAndNonMultiAttachments = ({
i18n, i18n,
onClose, onClose,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
return <Toast onClose={onClose}>{i18n('oneNonImageAtATimeToast')}</Toast>; return (
<Toast onClose={onClose}>
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
</Toast>
);
}; };

View file

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { ToastOneNonImageAtATime } from './ToastOneNonImageAtATime'; import { ToastUnsupportedMultiAttachment } from './ToastUnsupportedMultiAttachment';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
@ -16,13 +16,13 @@ const defaultProps = {
}; };
export default { export default {
title: 'Components/ToastOneNonImageAtATime', title: 'Components/ToastUnsupportedMultiAttachment',
}; };
export const _ToastOneNonImageAtATime = (): JSX.Element => ( export const _ToastUnsupportedMultiAttachment = (): JSX.Element => (
<ToastOneNonImageAtATime {...defaultProps} /> <ToastUnsupportedMultiAttachment {...defaultProps} />
); );
_ToastOneNonImageAtATime.story = { _ToastUnsupportedMultiAttachment.story = {
name: 'ToastOneNonImageAtATime', name: 'ToastUnsupportedMultiAttachment',
}; };

View file

@ -10,13 +10,13 @@ type PropsType = {
onClose: () => unknown; onClose: () => unknown;
}; };
export const ToastCannotMixImageAndNonImageAttachments = ({ export const ToastUnsupportedMultiAttachment = ({
i18n, i18n,
onClose, onClose,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
return ( return (
<Toast onClose={onClose}> <Toast onClose={onClose}>
{i18n('cannotMixImageAndNonImageAttachments')} {i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
</Toast> </Toast>
); );
}; };

View file

@ -17,7 +17,7 @@ export const StagedPlaceholderAttachment = ({
type="button" type="button"
className="module-staged-placeholder-attachment" className="module-staged-placeholder-attachment"
onClick={onClick} onClick={onClick}
title={i18n('add-image-attachment')} title={i18n('addImageOrVideoattachment')}
> >
<div className="module-staged-placeholder-attachment__plus-icon" /> <div className="module-staged-placeholder-attachment__plus-icon" />
</button> </button>

View file

@ -2,10 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
export enum AttachmentToastType { export enum AttachmentToastType {
ToastCannotMixImageAndNonImageAttachments, ToastCannotMixMultiAndNonMultiAttachments,
ToastDangerousFileType, ToastDangerousFileType,
ToastFileSize, ToastFileSize,
ToastMaxAttachments, ToastMaxAttachments,
ToastOneNonImageAtATime, ToastUnsupportedMultiAttachment,
ToastUnableToLoadAttachment, ToastUnableToLoadAttachment,
} }

View file

@ -36,3 +36,13 @@ const SUPPORTED_VIDEO_MIME_TYPES: MIMETypeSupportMap = {
// See: https://www.chromium.org/audio-video // See: https://www.chromium.org/audio-video
export const isVideoTypeSupported = (mimeType: MIME.MIMEType): boolean => export const isVideoTypeSupported = (mimeType: MIME.MIMEType): boolean =>
SUPPORTED_VIDEO_MIME_TYPES[mimeType] === true; SUPPORTED_VIDEO_MIME_TYPES[mimeType] === true;
export const getSupportedImageTypes = (): Array<MIME.MIMEType> => {
const keys = Object.keys(SUPPORTED_IMAGE_MIME_TYPES) as Array<MIME.MIMEType>;
return keys.filter(contentType => SUPPORTED_IMAGE_MIME_TYPES[contentType]);
};
export const getSupportedVideoTypes = (): Array<MIME.MIMEType> => {
const keys = Object.keys(SUPPORTED_VIDEO_MIME_TYPES) as Array<MIME.MIMEType>;
return keys.filter(contentType => SUPPORTED_VIDEO_MIME_TYPES[contentType]);
};

View file

@ -54,7 +54,7 @@ export async function handleAttachmentsProcessing({
for (let i = 0; i < files.length; i += 1) { for (let i = 0; i < files.length; i += 1) {
const file = files[i]; const file = files[i];
const processingResult = preProcessAttachment(file, nextDraftAttachments); const processingResult = preProcessAttachment(file, nextDraftAttachments);
if (processingResult) { if (processingResult != null) {
onShowToast(processingResult); onShowToast(processingResult);
} else { } else {
const pendingAttachment = getPendingAttachment(file); const pendingAttachment = getPendingAttachment(file);

View file

@ -15,7 +15,7 @@ import { handleImageAttachment } from './handleImageAttachment';
import { handleVideoAttachment } from './handleVideoAttachment'; import { handleVideoAttachment } from './handleVideoAttachment';
import { isAttachmentSizeOkay } from './isAttachmentSizeOkay'; import { isAttachmentSizeOkay } from './isAttachmentSizeOkay';
import { isFileDangerous } from './isFileDangerous'; import { isFileDangerous } from './isFileDangerous';
import { isHeic, isImage, stringToMIMEType } from '../types/MIME'; import { isHeic, isImage, isVideo, stringToMIMEType } from '../types/MIME';
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome'; import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
export function getPendingAttachment( export function getPendingAttachment(
@ -57,19 +57,24 @@ export function preProcessAttachment(
return AttachmentToastType.ToastMaxAttachments; return AttachmentToastType.ToastMaxAttachments;
} }
const haveNonImage = draftAttachments.some( const haveNonImageOrVideo = draftAttachments.some(
(attachment: AttachmentDraftType) => !isImage(attachment.contentType) (attachment: AttachmentDraftType) => {
return (
!isImage(attachment.contentType) && !isVideo(attachment.contentType)
);
}
); );
// You can't add another attachment if you already have a non-image staged // You can't add another attachment if you already have a non-image staged
if (haveNonImage) { if (haveNonImageOrVideo) {
return AttachmentToastType.ToastOneNonImageAtATime; return AttachmentToastType.ToastUnsupportedMultiAttachment;
} }
const fileType = stringToMIMEType(file.type); 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 // You can't add a non-image attachment if you already have attachments staged
if (!isImage(fileType) && draftAttachments.length > 0) { if (!imageOrVideo && draftAttachments.length > 0) {
return AttachmentToastType.ToastCannotMixImageAndNonImageAttachments; return AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments;
} }
return undefined; return undefined;

View file

@ -8,7 +8,7 @@ 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 { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments'; import type { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
import type { import type {
ToastCannotOpenGiftBadge, ToastCannotOpenGiftBadge,
ToastPropsType as ToastCannotOpenGiftBadgePropsType, ToastPropsType as ToastCannotOpenGiftBadgePropsType,
@ -44,7 +44,7 @@ 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 { ToastMaxAttachments } from '../components/ToastMaxAttachments';
import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import type { ToastOneNonImageAtATime } from '../components/ToastOneNonImageAtATime';
import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound'; import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import type { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull'; import type { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull';
import type { ToastReactionFailed } from '../components/ToastReactionFailed'; import type { ToastReactionFailed } from '../components/ToastReactionFailed';
@ -60,9 +60,7 @@ 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( export function showToast(Toast: typeof ToastUnsupportedMultiAttachment): void;
Toast: typeof ToastCannotMixImageAndNonImageAttachments
): void;
export function showToast(Toast: typeof ToastCannotStartGroupCall): void; export function showToast(Toast: typeof ToastCannotStartGroupCall): void;
export function showToast( export function showToast(
Toast: typeof ToastCannotOpenGiftBadge, Toast: typeof ToastCannotOpenGiftBadge,
@ -98,7 +96,7 @@ 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 ToastMaxAttachments): void;
export function showToast(Toast: typeof ToastMessageBodyTooLong): void; export function showToast(Toast: typeof ToastMessageBodyTooLong): void;
export function showToast(Toast: typeof ToastOneNonImageAtATime): 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 ToastPinnedConversationsFull): void; export function showToast(Toast: typeof ToastPinnedConversationsFull): void;
export function showToast(Toast: typeof ToastReactionFailed): void; export function showToast(Toast: typeof ToastReactionFailed): void;

View file

@ -60,7 +60,7 @@ import { ReadStatus } from '../messages/MessageReadStatus';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
import { ToastBlocked } from '../components/ToastBlocked'; import { ToastBlocked } from '../components/ToastBlocked';
import { ToastBlockedGroup } from '../components/ToastBlockedGroup'; import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
import { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments'; import { ToastCannotMixMultiAndNonMultiAttachments } from '../components/ToastCannotMixMultiAndNonMultiAttachments';
import { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCall'; import { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCall';
import { ToastConversationArchived } from '../components/ToastConversationArchived'; import { ToastConversationArchived } from '../components/ToastConversationArchived';
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread'; import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
@ -73,7 +73,7 @@ import { ToastInvalidConversation } from '../components/ToastInvalidConversation
import { ToastLeftGroup } from '../components/ToastLeftGroup'; import { ToastLeftGroup } from '../components/ToastLeftGroup';
import { ToastMaxAttachments } from '../components/ToastMaxAttachments'; import { ToastMaxAttachments } from '../components/ToastMaxAttachments';
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import { ToastOneNonImageAtATime } from '../components/ToastOneNonImageAtATime'; import { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound'; import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull'; import { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull';
import { ToastReactionFailed } from '../components/ToastReactionFailed'; import { ToastReactionFailed } from '../components/ToastReactionFailed';
@ -1028,13 +1028,15 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
showToast(ToastDangerousFileType); showToast(ToastDangerousFileType);
} else if (toastType === AttachmentToastType.ToastMaxAttachments) { } else if (toastType === AttachmentToastType.ToastMaxAttachments) {
showToast(ToastMaxAttachments); showToast(ToastMaxAttachments);
} else if (toastType === AttachmentToastType.ToastOneNonImageAtATime) { } else if (
showToast(ToastOneNonImageAtATime); toastType === AttachmentToastType.ToastUnsupportedMultiAttachment
) {
showToast(ToastUnsupportedMultiAttachment);
} else if ( } else if (
toastType === toastType ===
AttachmentToastType.ToastCannotMixImageAndNonImageAttachments AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments
) { ) {
showToast(ToastCannotMixImageAndNonImageAttachments); showToast(ToastCannotMixMultiAndNonMultiAttachments);
} else if ( } else if (
toastType === AttachmentToastType.ToastUnableToLoadAttachment toastType === AttachmentToastType.ToastUnableToLoadAttachment
) { ) {