diff --git a/app/soapbox/actions/__tests__/compose.test.js b/app/soapbox/actions/__tests__/compose.test.js new file mode 100644 index 000000000..ec20acf0f --- /dev/null +++ b/app/soapbox/actions/__tests__/compose.test.js @@ -0,0 +1,103 @@ +import { InstanceRecord } from 'soapbox/normalizers'; +import rootReducer from 'soapbox/reducers'; +import { mockStore } from 'soapbox/test_helpers'; + +import { uploadCompose } from '../compose'; + +describe('uploadCompose()', () => { + describe('with images', () => { + let files, store; + + beforeEach(() => { + const instance = InstanceRecord({ + configuration: { + statuses: { + max_media_attachments: 4, + }, + media_attachments: { + image_size_limit: 10, + }, + }, + }); + + const state = rootReducer(undefined, {}) + .set('me', '1234') + .set('instance', instance); + + store = mockStore(state); + files = [{ + uri: 'image.png', + name: 'Image', + size: 15, + type: 'image/png', + }]; + }); + + it('creates an alert if exceeds max size', async() => { + const expectedActions = [ + { type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true }, + { + type: 'ALERT_SHOW', + message: 'Image exceeds the current file size limit (10 Bytes)', + actionLabel: undefined, + actionLink: undefined, + severity: 'error', + }, + { type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true }, + ]; + + await store.dispatch(uploadCompose(files)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('with videos', () => { + let files, store; + + beforeEach(() => { + const instance = InstanceRecord({ + configuration: { + statuses: { + max_media_attachments: 4, + }, + media_attachments: { + video_size_limit: 10, + }, + }, + }); + + const state = rootReducer(undefined, {}) + .set('me', '1234') + .set('instance', instance); + + store = mockStore(state); + files = [{ + uri: 'video.mp4', + name: 'Video', + size: 15, + type: 'video/mp4', + }]; + }); + + it('creates an alert if exceeds max size', async() => { + const expectedActions = [ + { type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true }, + { + type: 'ALERT_SHOW', + message: 'Video exceeds the current file size limit (10 Bytes)', + actionLabel: undefined, + actionLink: undefined, + severity: 'error', + }, + { type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true }, + ]; + + await store.dispatch(uploadCompose(files)); + const actions = store.getActions(); + + expect(actions).toEqual(expectedActions); + }); + }); +}); diff --git a/app/soapbox/actions/compose.js b/app/soapbox/actions/compose.js index 25e5fa4f1..3cebf8459 100644 --- a/app/soapbox/actions/compose.js +++ b/app/soapbox/actions/compose.js @@ -6,6 +6,7 @@ import { defineMessages } from 'react-intl'; import snackbar from 'soapbox/actions/snackbar'; import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures } from 'soapbox/utils/features'; +import { formatBytes } from 'soapbox/utils/media'; import api from '../api'; import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light'; @@ -299,6 +300,8 @@ export function uploadCompose(files) { return function(dispatch, getState) { if (!isLoggedIn(getState)) return; const attachmentLimit = getState().getIn(['instance', 'configuration', 'statuses', 'max_media_attachments']); + const maxImageSize = getState().getIn(['instance', 'configuration', 'media_attachments', 'image_size_limit']); + const maxVideoSize = getState().getIn(['instance', 'configuration', 'media_attachments', 'video_size_limit']); const media = getState().getIn(['compose', 'media_attachments']); const progress = new Array(files.length).fill(0); @@ -314,6 +317,20 @@ export function uploadCompose(files) { Array.from(files).forEach((f, i) => { if (media.size + i > attachmentLimit - 1) return; + const isImage = f.type.match(/image.*/); + const isVideo = f.type.match(/video.*/); + if (isImage && maxImageSize && (f.size > maxImageSize)) { + const message = `Image exceeds the current file size limit (${formatBytes(maxImageSize)})`; + dispatch(snackbar.error(message)); + dispatch(uploadComposeFail(true)); + return; + } else if (isVideo && maxVideoSize && (f.size > maxVideoSize)) { + const message = `Video exceeds the current file size limit (${formatBytes(maxVideoSize)})`; + dispatch(snackbar.error(message)); + dispatch(uploadComposeFail(true)); + return; + } + // FIXME: Don't define function in loop /* eslint-disable no-loop-func */ resizeImage(f).then(file => { diff --git a/app/soapbox/utils/media.ts b/app/soapbox/utils/media.ts index c8266c646..74fc4d3f8 100644 --- a/app/soapbox/utils/media.ts +++ b/app/soapbox/utils/media.ts @@ -1,6 +1,10 @@ -export const truncateFilename = (url, maxLength) => { +const truncateFilename = (url: string, maxLength: number) => { const filename = url.split('/').pop(); + if (!filename) { + return filename; + } + if (filename.length <= maxLength) return filename; return [ @@ -8,3 +12,17 @@ export const truncateFilename = (url, maxLength) => { filename.substr(filename.length - maxLength/2), ].join('…'); }; + +const formatBytes = (bytes: number, decimals: number = 2) => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +}; + +export { formatBytes, truncateFilename };