diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index a6b6dcd13..721e6bad1 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -8,7 +8,7 @@ import { search as emojiSearch } from 'soapbox/features/emoji/emoji_mart_search_ import { tagHistory } from 'soapbox/settings'; import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures, parseVersion } from 'soapbox/utils/features'; -import { formatBytes } from 'soapbox/utils/media'; +import { formatBytes, getVideoDuration } from 'soapbox/utils/media'; import resizeImage from 'soapbox/utils/resize_image'; import { showAlert, showAlertForError } from './alerts'; @@ -89,6 +89,7 @@ const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; const messages = defineMessages({ exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' }, + exceededVideoDurationLimit: { id: 'upload_error.video_duration_limit', defaultMessage: 'Video exceeds the current duration limit ({limit} seconds)' }, scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' }, success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, @@ -313,6 +314,7 @@ const uploadCompose = (files: FileList, intl: IntlShape) => const attachmentLimit = getState().instance.configuration.getIn(['statuses', 'max_media_attachments']) as number; const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined; const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined; + const maxVideoDuration = getState().instance.configuration.getIn(['media_attachments', 'video_duration_limit']) as number | undefined; const media = getState().compose.media_attachments; const progress = new Array(files.length).fill(0); @@ -325,11 +327,13 @@ const uploadCompose = (files: FileList, intl: IntlShape) => dispatch(uploadComposeRequest()); - Array.from(files).forEach((f, i) => { + Array.from(files).forEach(async(f, i) => { if (media.size + i > attachmentLimit - 1) return; const isImage = f.type.match(/image.*/); const isVideo = f.type.match(/video.*/); + const videoDurationInSeconds = (isVideo && maxVideoDuration) ? await getVideoDuration(f) : 0; + if (isImage && maxImageSize && (f.size > maxImageSize)) { const limit = formatBytes(maxImageSize); const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit }); @@ -342,6 +346,11 @@ const uploadCompose = (files: FileList, intl: IntlShape) => dispatch(snackbar.error(message)); dispatch(uploadComposeFail(true)); return; + } else if (isVideo && maxVideoDuration && (videoDurationInSeconds > maxVideoDuration)) { + const message = intl.formatMessage(messages.exceededVideoDurationLimit, { limit: maxVideoDuration }); + dispatch(snackbar.error(message)); + dispatch(uploadComposeFail(true)); + return; } // FIXME: Don't define const in loop diff --git a/app/soapbox/utils/media.ts b/app/soapbox/utils/media.ts index 1c08611bf..7947ef91f 100644 --- a/app/soapbox/utils/media.ts +++ b/app/soapbox/utils/media.ts @@ -25,4 +25,30 @@ const formatBytes = (bytes: number, decimals: number = 2) => { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }; -export { formatBytes, truncateFilename }; +const getVideoDuration = (file: File): Promise => { + const video = document.createElement('video'); + + const promise = new Promise((resolve, reject) => { + video.addEventListener('loadedmetadata', () => { + // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=642012 + if (video.duration === Infinity) { + video.currentTime = Number.MAX_SAFE_INTEGER; + video.ontimeupdate = () => { + video.ontimeupdate = null; + resolve(video.duration); + video.currentTime = 0; + }; + } else { + resolve(video.duration); + } + }); + + video.onerror = (event: any) => reject(event.target.error); + }); + + video.src = window.URL.createObjectURL(file); + + return promise; +}; + +export { getVideoDuration, formatBytes, truncateFilename };