Accept multiple images and videos in attachment picker
This commit is contained in:
parent
6cfe2a09df
commit
01587b0f39
13 changed files with 87 additions and 54 deletions
|
@ -557,12 +557,8 @@
|
|||
"message": "Submit log",
|
||||
"description": "Label for the decryption error toast button"
|
||||
},
|
||||
"oneNonImageAtATimeToast": {
|
||||
"message": "When including a non-image attachment, the limit is one attachment per message.",
|
||||
"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.",
|
||||
"cannotSelectPhotosAndVideosAlongWithFiles": {
|
||||
"message": "You can't select photos and videos along with files.",
|
||||
"description": "An error popup when the user has attempted to add an attachment"
|
||||
},
|
||||
"maximumAttachments": {
|
||||
|
@ -2623,8 +2619,8 @@
|
|||
"message": "Close Popup",
|
||||
"description": "Used as alt text for any button closing a popup"
|
||||
},
|
||||
"add-image-attachment": {
|
||||
"message": "Add image attachment",
|
||||
"addImageOrVideoattachment": {
|
||||
"message": "Add image or video attachment",
|
||||
"description": "Used in draft attachment list for the big 'add new attachment' button"
|
||||
},
|
||||
"remove-attachment": {
|
||||
|
|
|
@ -8,16 +8,21 @@ import type {
|
|||
InMemoryAttachmentDraftType,
|
||||
AttachmentDraftType,
|
||||
} from '../types/Attachment';
|
||||
import { isVideoAttachment, isImageAttachment } from '../types/Attachment';
|
||||
import { AttachmentToastType } from '../types/AttachmentToastType';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
import { ToastCannotMixImageAndNonImageAttachments } from './ToastCannotMixImageAndNonImageAttachments';
|
||||
import { ToastCannotMixMultiAndNonMultiAttachments } from './ToastCannotMixMultiAndNonMultiAttachments';
|
||||
import { ToastDangerousFileType } from './ToastDangerousFileType';
|
||||
import { ToastFileSize } from './ToastFileSize';
|
||||
import { ToastMaxAttachments } from './ToastMaxAttachments';
|
||||
import { ToastOneNonImageAtATime } from './ToastOneNonImageAtATime';
|
||||
import { ToastUnsupportedMultiAttachment } from './ToastUnsupportedMultiAttachment';
|
||||
import { ToastUnableToLoadAttachment } from './ToastUnableToLoadAttachment';
|
||||
import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachmentsProcessing';
|
||||
import {
|
||||
getSupportedImageTypes,
|
||||
getSupportedVideoTypes,
|
||||
} from '../util/GoogleChrome';
|
||||
|
||||
export type PropsType = {
|
||||
addAttachment: (
|
||||
|
@ -87,14 +92,18 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
|||
toast = <ToastDangerousFileType i18n={i18n} onClose={closeToast} />;
|
||||
} else if (toastType === AttachmentToastType.ToastMaxAttachments) {
|
||||
toast = <ToastMaxAttachments i18n={i18n} onClose={closeToast} />;
|
||||
} else if (toastType === AttachmentToastType.ToastOneNonImageAtATime) {
|
||||
toast = <ToastOneNonImageAtATime i18n={i18n} onClose={closeToast} />;
|
||||
} else if (
|
||||
toastType ===
|
||||
AttachmentToastType.ToastCannotMixImageAndNonImageAttachments
|
||||
toastType === AttachmentToastType.ToastUnsupportedMultiAttachment
|
||||
) {
|
||||
toast = (
|
||||
<ToastCannotMixImageAndNonImageAttachments
|
||||
<ToastUnsupportedMultiAttachment i18n={i18n} onClose={closeToast} />
|
||||
);
|
||||
} else if (
|
||||
toastType ===
|
||||
AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments
|
||||
) {
|
||||
toast = (
|
||||
<ToastCannotMixMultiAndNonMultiAttachments
|
||||
i18n={i18n}
|
||||
onClose={closeToast}
|
||||
/>
|
||||
|
@ -103,6 +112,14 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
|||
toast = <ToastUnableToLoadAttachment i18n={i18n} onClose={closeToast} />;
|
||||
}
|
||||
|
||||
const anyVideoOrImageAttachments = draftAttachments.some(attachment => {
|
||||
return isImageAttachment(attachment) || isVideoAttachment(attachment);
|
||||
});
|
||||
|
||||
const acceptContentTypes = anyVideoOrImageAttachments
|
||||
? [...getSupportedImageTypes(), ...getSupportedVideoTypes()]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{toast}
|
||||
|
@ -112,6 +129,7 @@ export const CompositionUpload = forwardRef<HTMLInputElement, PropsType>(
|
|||
onChange={onFileInputChange}
|
||||
ref={ref}
|
||||
type="file"
|
||||
accept={acceptContentTypes?.join(',')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ToastCannotMixImageAndNonImageAttachments } from './ToastCannotMixImageAndNonImageAttachments';
|
||||
import { ToastCannotMixMultiAndNonMultiAttachments } from './ToastCannotMixMultiAndNonMultiAttachments';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
@ -16,13 +16,13 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastCannotMixImageAndNonImageAttachments',
|
||||
title: 'Components/ToastCannotMixMultiAndNonMultiAttachments',
|
||||
};
|
||||
|
||||
export const _ToastCannotMixImageAndNonImageAttachments = (): JSX.Element => (
|
||||
<ToastCannotMixImageAndNonImageAttachments {...defaultProps} />
|
||||
export const _ToastCannotMixMultiAndNonMultiAttachments = (): JSX.Element => (
|
||||
<ToastCannotMixMultiAndNonMultiAttachments {...defaultProps} />
|
||||
);
|
||||
|
||||
_ToastCannotMixImageAndNonImageAttachments.story = {
|
||||
name: 'ToastCannotMixImageAndNonImageAttachments',
|
||||
_ToastCannotMixMultiAndNonMultiAttachments.story = {
|
||||
name: 'ToastCannotMixMultiAndNonMultiAttachments',
|
||||
};
|
|
@ -10,9 +10,13 @@ type PropsType = {
|
|||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export const ToastOneNonImageAtATime = ({
|
||||
export const ToastCannotMixMultiAndNonMultiAttachments = ({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element => {
|
||||
return <Toast onClose={onClose}>{i18n('oneNonImageAtATimeToast')}</Toast>;
|
||||
return (
|
||||
<Toast onClose={onClose}>
|
||||
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
|
||||
</Toast>
|
||||
);
|
||||
};
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ToastOneNonImageAtATime } from './ToastOneNonImageAtATime';
|
||||
import { ToastUnsupportedMultiAttachment } from './ToastUnsupportedMultiAttachment';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
@ -16,13 +16,13 @@ const defaultProps = {
|
|||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastOneNonImageAtATime',
|
||||
title: 'Components/ToastUnsupportedMultiAttachment',
|
||||
};
|
||||
|
||||
export const _ToastOneNonImageAtATime = (): JSX.Element => (
|
||||
<ToastOneNonImageAtATime {...defaultProps} />
|
||||
export const _ToastUnsupportedMultiAttachment = (): JSX.Element => (
|
||||
<ToastUnsupportedMultiAttachment {...defaultProps} />
|
||||
);
|
||||
|
||||
_ToastOneNonImageAtATime.story = {
|
||||
name: 'ToastOneNonImageAtATime',
|
||||
_ToastUnsupportedMultiAttachment.story = {
|
||||
name: 'ToastUnsupportedMultiAttachment',
|
||||
};
|
|
@ -10,13 +10,13 @@ type PropsType = {
|
|||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export const ToastCannotMixImageAndNonImageAttachments = ({
|
||||
export const ToastUnsupportedMultiAttachment = ({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element => {
|
||||
return (
|
||||
<Toast onClose={onClose}>
|
||||
{i18n('cannotMixImageAndNonImageAttachments')}
|
||||
{i18n('cannotSelectPhotosAndVideosAlongWithFiles')}
|
||||
</Toast>
|
||||
);
|
||||
};
|
|
@ -17,7 +17,7 @@ export const StagedPlaceholderAttachment = ({
|
|||
type="button"
|
||||
className="module-staged-placeholder-attachment"
|
||||
onClick={onClick}
|
||||
title={i18n('add-image-attachment')}
|
||||
title={i18n('addImageOrVideoattachment')}
|
||||
>
|
||||
<div className="module-staged-placeholder-attachment__plus-icon" />
|
||||
</button>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum AttachmentToastType {
|
||||
ToastCannotMixImageAndNonImageAttachments,
|
||||
ToastCannotMixMultiAndNonMultiAttachments,
|
||||
ToastDangerousFileType,
|
||||
ToastFileSize,
|
||||
ToastMaxAttachments,
|
||||
ToastOneNonImageAtATime,
|
||||
ToastUnsupportedMultiAttachment,
|
||||
ToastUnableToLoadAttachment,
|
||||
}
|
||||
|
|
|
@ -36,3 +36,13 @@ const SUPPORTED_VIDEO_MIME_TYPES: MIMETypeSupportMap = {
|
|||
// See: https://www.chromium.org/audio-video
|
||||
export const isVideoTypeSupported = (mimeType: MIME.MIMEType): boolean =>
|
||||
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]);
|
||||
};
|
||||
|
|
|
@ -54,7 +54,7 @@ export async function handleAttachmentsProcessing({
|
|||
for (let i = 0; i < files.length; i += 1) {
|
||||
const file = files[i];
|
||||
const processingResult = preProcessAttachment(file, nextDraftAttachments);
|
||||
if (processingResult) {
|
||||
if (processingResult != null) {
|
||||
onShowToast(processingResult);
|
||||
} else {
|
||||
const pendingAttachment = getPendingAttachment(file);
|
||||
|
|
|
@ -15,7 +15,7 @@ import { handleImageAttachment } from './handleImageAttachment';
|
|||
import { handleVideoAttachment } from './handleVideoAttachment';
|
||||
import { isAttachmentSizeOkay } from './isAttachmentSizeOkay';
|
||||
import { isFileDangerous } from './isFileDangerous';
|
||||
import { isHeic, isImage, stringToMIMEType } from '../types/MIME';
|
||||
import { isHeic, isImage, isVideo, stringToMIMEType } from '../types/MIME';
|
||||
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
|
||||
|
||||
export function getPendingAttachment(
|
||||
|
@ -57,19 +57,24 @@ export function preProcessAttachment(
|
|||
return AttachmentToastType.ToastMaxAttachments;
|
||||
}
|
||||
|
||||
const haveNonImage = draftAttachments.some(
|
||||
(attachment: AttachmentDraftType) => !isImage(attachment.contentType)
|
||||
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 (haveNonImage) {
|
||||
return AttachmentToastType.ToastOneNonImageAtATime;
|
||||
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 (!isImage(fileType) && draftAttachments.length > 0) {
|
||||
return AttachmentToastType.ToastCannotMixImageAndNonImageAttachments;
|
||||
if (!imageOrVideo && draftAttachments.length > 0) {
|
||||
return AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
|
@ -8,7 +8,7 @@ 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 { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments';
|
||||
import type { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
|
||||
import type {
|
||||
ToastCannotOpenGiftBadge,
|
||||
ToastPropsType as ToastCannotOpenGiftBadgePropsType,
|
||||
|
@ -44,7 +44,7 @@ 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 { ToastOneNonImageAtATime } from '../components/ToastOneNonImageAtATime';
|
||||
|
||||
import type { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||
import type { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull';
|
||||
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 ToastBlocked): void;
|
||||
export function showToast(Toast: typeof ToastBlockedGroup): void;
|
||||
export function showToast(
|
||||
Toast: typeof ToastCannotMixImageAndNonImageAttachments
|
||||
): void;
|
||||
export function showToast(Toast: typeof ToastUnsupportedMultiAttachment): void;
|
||||
export function showToast(Toast: typeof ToastCannotStartGroupCall): void;
|
||||
export function showToast(
|
||||
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 ToastMaxAttachments): 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 ToastPinnedConversationsFull): void;
|
||||
export function showToast(Toast: typeof ToastReactionFailed): void;
|
||||
|
|
|
@ -60,7 +60,7 @@ import { ReadStatus } from '../messages/MessageReadStatus';
|
|||
import { SignalService as Proto } from '../protobuf';
|
||||
import { ToastBlocked } from '../components/ToastBlocked';
|
||||
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
|
||||
import { ToastCannotMixImageAndNonImageAttachments } from '../components/ToastCannotMixImageAndNonImageAttachments';
|
||||
import { ToastCannotMixMultiAndNonMultiAttachments } from '../components/ToastCannotMixMultiAndNonMultiAttachments';
|
||||
import { ToastCannotStartGroupCall } from '../components/ToastCannotStartGroupCall';
|
||||
import { ToastConversationArchived } from '../components/ToastConversationArchived';
|
||||
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||
|
@ -73,7 +73,7 @@ import { ToastInvalidConversation } from '../components/ToastInvalidConversation
|
|||
import { ToastLeftGroup } from '../components/ToastLeftGroup';
|
||||
import { ToastMaxAttachments } from '../components/ToastMaxAttachments';
|
||||
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
||||
import { ToastOneNonImageAtATime } from '../components/ToastOneNonImageAtATime';
|
||||
import { ToastUnsupportedMultiAttachment } from '../components/ToastUnsupportedMultiAttachment';
|
||||
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||
import { ToastPinnedConversationsFull } from '../components/ToastPinnedConversationsFull';
|
||||
import { ToastReactionFailed } from '../components/ToastReactionFailed';
|
||||
|
@ -1028,13 +1028,15 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
showToast(ToastDangerousFileType);
|
||||
} else if (toastType === AttachmentToastType.ToastMaxAttachments) {
|
||||
showToast(ToastMaxAttachments);
|
||||
} else if (toastType === AttachmentToastType.ToastOneNonImageAtATime) {
|
||||
showToast(ToastOneNonImageAtATime);
|
||||
} else if (
|
||||
toastType === AttachmentToastType.ToastUnsupportedMultiAttachment
|
||||
) {
|
||||
showToast(ToastUnsupportedMultiAttachment);
|
||||
} else if (
|
||||
toastType ===
|
||||
AttachmentToastType.ToastCannotMixImageAndNonImageAttachments
|
||||
AttachmentToastType.ToastCannotMixMultiAndNonMultiAttachments
|
||||
) {
|
||||
showToast(ToastCannotMixImageAndNonImageAttachments);
|
||||
showToast(ToastCannotMixMultiAndNonMultiAttachments);
|
||||
} else if (
|
||||
toastType === AttachmentToastType.ToastUnableToLoadAttachment
|
||||
) {
|
||||
|
|
Loading…
Reference in a new issue