diff --git a/apps/frontend/src/components/layout/drop.files.tsx b/apps/frontend/src/components/layout/drop.files.tsx
index 746c1585..e84b9c7a 100644
--- a/apps/frontend/src/components/layout/drop.files.tsx
+++ b/apps/frontend/src/components/layout/drop.files.tsx
@@ -2,15 +2,24 @@ import { useDropzone } from 'react-dropzone';
import { FC, ReactNode } from 'react';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import clsx from 'clsx';
+import { useToaster } from '@gitroom/react/toaster/toaster';
export const DropFiles: FC<{
children: ReactNode;
className?: string;
onDrop: (files: File[]) => void;
+ disabled?: boolean;
}> = (props) => {
const t = useT();
+ const toaster = useToaster();
const { getRootProps, isDragActive } = useDropzone({
- onDrop: props.onDrop,
+ onDrop: (files) => {
+ if (props.disabled) {
+ toaster.show('Upload current in progress, please wait and then try again.', 'warning');
+ return ;
+ }
+ props.onDrop(files);
+ },
});
return (
diff --git a/apps/frontend/src/components/media/media.component.tsx b/apps/frontend/src/components/media/media.component.tsx
index 9a289958..97a10637 100644
--- a/apps/frontend/src/components/media/media.component.tsx
+++ b/apps/frontend/src/components/media/media.component.tsx
@@ -18,6 +18,7 @@ import { Media } from '@prisma/client';
import { useMediaDirectory } from '@gitroom/react/helpers/use.media.directory';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import EventEmitter from 'events';
+import { useToaster } from '@gitroom/react/toaster/toaster';
import clsx from 'clsx';
import { VideoFrame } from '@gitroom/react/helpers/video.frame';
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
@@ -48,6 +49,7 @@ import {
} from '@gitroom/frontend/components/ui/icons';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useShallow } from 'zustand/react/shallow';
+import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
const Polonto = dynamic(
() => import('@gitroom/frontend/components/launches/polonto')
);
@@ -194,6 +196,7 @@ export const showMediaBox = (
showModalEmitter.emit('show-modal', callback);
};
const CHUNK_SIZE = 1024 * 1024;
+const MAX_UPLOAD_SIZE = 1024 * 1024 * 1024; // 1 GB
export const MediaBox: FC<{
setMedia: (params: { id: string; path: string }[]) => void;
standalone?: boolean;
@@ -203,6 +206,7 @@ export const MediaBox: FC<{
const [page, setPage] = useState(0);
const fetch = useFetch();
const modals = useModals();
+ const toaster = useToaster();
const loadMedia = useCallback(async () => {
return (await fetch(`/media?page=${page + 1}`)).json();
}, [page]);
@@ -211,6 +215,7 @@ export const MediaBox: FC<{
const t = useT();
const uploaderRef = useRef
(null);
const mediaDirectory = useMediaDirectory();
+ const [loading, setLoading] = useState(false);
const uppy = useUppyUploader({
allowedFileTypes:
@@ -220,7 +225,6 @@ export const MediaBox: FC<{
? 'video/mp4'
: 'image/*,video/mp4',
onUploadSuccess: async (arr) => {
- uppy.clear();
await mutate();
if (standalone) {
return;
@@ -229,6 +233,8 @@ export const MediaBox: FC<{
return [...prevSelected, ...arr];
});
},
+ onStart: () => setLoading(true),
+ onEnd: () => setLoading(false),
});
const addRemoveSelected = useCallback(
@@ -255,11 +261,29 @@ export const MediaBox: FC<{
modals.closeCurrent();
}, [selected]);
- const addToUpload = useCallback(async (e: ChangeEvent) => {
- const files = Array.from(e.target.files).slice(0, 5);
- // @ts-ignore
- uppy.addFiles(files);
- }, []);
+ const addToUpload = useCallback(
+ async (e: ChangeEvent) => {
+ const files = Array.from(e.target.files || []);
+ const totalSize = files.reduce((acc, file) => acc + file.size, 0);
+
+ if (totalSize > MAX_UPLOAD_SIZE) {
+ toaster.show(
+ t(
+ 'upload_size_limit_exceeded',
+ 'Upload size limit exceeded. Maximum 1 GB per upload session.'
+ ),
+ 'warning'
+ );
+ return;
+ }
+
+ setLoading(true);
+
+ // @ts-ignore
+ uppy.addFiles(files);
+ },
+ [toaster, t]
+ );
const dragAndDrop = useCallback(
async (event: ClipboardEvent | File[]) => {
@@ -272,7 +296,7 @@ export const MediaBox: FC<{
return;
}
- const files = [];
+ const files: File[] = [];
// @ts-ignore
for (const item of clipboardItems) {
if (item.kind === 'file') {
@@ -283,11 +307,26 @@ export const MediaBox: FC<{
}
}
- for (const file of files.slice(0, 5)) {
+ const totalSize = files.reduce((acc, file) => acc + file.size, 0);
+
+ if (totalSize > MAX_UPLOAD_SIZE) {
+ toaster.show(
+ t(
+ 'upload_size_limit_exceeded',
+ 'Upload size limit exceeded. Maximum 1 GB per upload session.'
+ ),
+ 'warning'
+ );
+ return;
+ }
+
+ setLoading(true);
+
+ for (const file of files) {
uppy.addFile(file);
}
},
- []
+ [toaster, t]
);
const maximize = useCallback(
@@ -299,7 +338,10 @@ export const MediaBox: FC<{
children: (
{media.path.indexOf('mp4') > -1 ? (
-
+
) : (
![]()
{
return (
);
- }, [t]);
+ }, [t, loading]);
return (
-
+
{t(
- 'select_or_upload_pictures_max_5',
- 'Select or upload pictures (maximum 5 at a time).'
+ 'select_or_upload_pictures_max_1gb',
+ 'Select or upload pictures (maximum 1 GB per upload).'
)}
{'\n'}
{t(
@@ -423,8 +472,8 @@ export const MediaBox: FC<{
{t(
- 'select_or_upload_pictures_max_5',
- 'Select or upload pictures (maximum 5 at a time).'
+ 'select_or_upload_pictures_max_1gb',
+ 'Select or upload pictures (maximum 1 GB per upload).'
)}{' '}
{'\n'}
{t(
@@ -476,7 +525,7 @@ export const MediaBox: FC<{
onClick={addRemoveSelected(media)}
>
{!!selected.find((p: any) => p.id === media.id) ? (
-
+
{selected.findIndex((z: any) => z.id === media.id) + 1}
) : (
diff --git a/apps/frontend/src/components/media/new.uploader.tsx b/apps/frontend/src/components/media/new.uploader.tsx
index 540b9e10..fe7172e1 100644
--- a/apps/frontend/src/components/media/new.uploader.tsx
+++ b/apps/frontend/src/components/media/new.uploader.tsx
@@ -12,7 +12,7 @@ import Compressor from '@uppy/compressor';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
-import { uniq } from 'lodash';
+import { uniqBy } from 'lodash';
export class CompressionWrapper
extends Compressor {
override async prepareUpload(fileIDs: string[]) {
@@ -35,50 +35,11 @@ export class CompressionWrapper extends Compressor {
}
}
-export function MultipartFileUploader({
- onUploadSuccess,
- allowedFileTypes,
- uppRef,
-}: {
- // @ts-ignore
- onUploadSuccess: (result: UploadResult) => void;
- allowedFileTypes: string;
- uppRef?: any;
-}) {
- const [loaded, setLoaded] = useState(false);
- const [reload, setReload] = useState(false);
- const onUploadSuccessExtended = useCallback(
- (result: UploadResult) => {
- setReload(true);
- onUploadSuccess(result);
- },
- [onUploadSuccess]
- );
- useEffect(() => {
- if (reload) {
- setTimeout(() => {
- setReload(false);
- }, 1);
- }
- }, [reload]);
- useEffect(() => {
- setLoaded(true);
- }, []);
- if (!loaded || reload) {
- return null;
- }
- return (
-
- );
-}
-
export function useUppyUploader(props: {
// @ts-ignore
onUploadSuccess: (result: UploadResult) => void;
+ onStart: () => void;
+ onEnd: () => void;
allowedFileTypes: string;
}) {
const setLocked = useLaunchStore((state) => state.setLocked);
@@ -88,6 +49,9 @@ export function useUppyUploader(props: {
const { onUploadSuccess, allowedFileTypes } = props;
const fetch = useFetch();
return useMemo(() => {
+ // Track file order to maintain original sequence after upload
+ let fileOrderIndex = 0;
+
const uppy2 = new Uppy({
autoProceed: true,
restrictions: {
@@ -221,49 +185,79 @@ export function useUppyUploader(props: {
setLocked(true);
uppy2.setFileMeta(file.id, {
useCloudflare: storageProvider === 'cloudflare' ? 'true' : 'false', // Example of adding a custom field
+ addedOrder: fileOrderIndex++, // Track original order for sorting after upload
// Add more fields as needed
});
});
uppy2.on('error', (result) => {
uppy2.clear();
setLocked(false);
+ props.onEnd();
+ fileOrderIndex = 0;
+ });
+ uppy2.on('upload-start', () => {
+ props.onStart();
});
uppy2.on('complete', async (result) => {
+ for (const file of [...result.successful]) {
+ uppy2.removeFile(file.id);
+ }
+
+ props.onEnd();
+ // Sort results by original add order to maintain file sequence
+ const sortedSuccessful = [...result.successful].sort((a, b) => {
+ const orderA = +((a.meta as any)?.addedOrder ?? 0);
+ const orderB = +((b.meta as any)?.addedOrder ?? 0);
+ return orderA - orderB;
+ });
+
if (storageProvider === 'local') {
setLocked(false);
- onUploadSuccess(result.successful.map((p) => p.response.body));
+ fileOrderIndex = 0;
+ onUploadSuccess(sortedSuccessful.map((p) => p.response.body));
return;
}
if (transloadit.length > 0) {
// @ts-ignore
const allRes = result.transloadit[0].results;
- const toSave = uniq(
- (allRes[Object.keys(allRes)[0]] || []).flatMap((item: any) =>
- item.url.split('/').pop()
- )
+ const toSave = uniqBy<{ name: string; order: number }>(
+ (allRes[Object.keys(allRes)[0]] || []).flatMap((item: any) => ({
+ name: item.url.split('/').pop(),
+ order: +item.user_meta.addedOrder,
+ })),
+ (item) => item.name
);
- const loadAllMedia = await Promise.all(
- toSave.map(async (name) => {
- return (
- await fetch('/media/save-media', {
- method: 'POST',
- body: JSON.stringify({
- name,
- }),
- })
- ).json();
+ const loadAllMedia = (
+ await Promise.all(
+ toSave.map(async ({ name, order }) => ({
+ file: await (
+ await fetch('/media/save-media', {
+ method: 'POST',
+ body: JSON.stringify({
+ name,
+ }),
+ })
+ ).json(),
+ order,
+ }))
+ )
+ )
+ .sort((a, b) => {
+ return a.order - b.order;
})
- );
+ .map((p) => p.file);
setLocked(false);
+ fileOrderIndex = 0;
onUploadSuccess(loadAllMedia);
return;
}
setLocked(false);
- onUploadSuccess(result.successful.map((p) => p.response.body.saved));
+ fileOrderIndex = 0;
+ onUploadSuccess(sortedSuccessful.map((p) => p.response.body.saved));
});
uppy2.on('upload-success', (file, response) => {
// @ts-ignore
@@ -279,53 +273,3 @@ export function useUppyUploader(props: {
return uppy2;
}, []);
}
-export function MultipartFileUploaderAfter({
- onUploadSuccess,
- allowedFileTypes,
- uppRef,
-}: {
- // @ts-ignore
- onUploadSuccess: (result: UploadResult) => void;
- allowedFileTypes: string;
- uppRef: any;
-}) {
- const t = useT();
- const uppy = useUppyUploader({
- onUploadSuccess,
- allowedFileTypes,
- });
- const uppyInstance = useMemo(() => {
- uppRef.current = uppy;
- return uppy;
- }, []);
- return (
- <>
- {/* */}
-
-
-
- n,
- }}
- />
- >
- );
-}
diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx
index 0c56ed0e..87ff52d2 100644
--- a/apps/frontend/src/components/new-launch/editor.tsx
+++ b/apps/frontend/src/components/new-launch/editor.tsx
@@ -56,6 +56,7 @@ import { suggestion } from '@gitroom/frontend/components/new-launch/mention.comp
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { AComponent } from '@gitroom/frontend/components/new-launch/a.component';
import { Placeholder } from '@tiptap/extensions';
+import { useToaster } from '@gitroom/react/toaster/toaster';
import { InformationComponent } from '@gitroom/frontend/components/launches/information.component';
import {
LockIcon,
@@ -67,6 +68,8 @@ import {
} from '@gitroom/frontend/components/ui/icons';
import { DelayComponent } from '@gitroom/frontend/components/new-launch/delay.component';
+const MAX_UPLOAD_SIZE = 1024 * 1024 * 1024; // 1 GB
+
const InterceptBoldShortcut = Extension.create({
name: 'preventBoldWithUnderline',
@@ -558,7 +561,9 @@ export const Editor: FC<{
const [id] = useState(makeId(10));
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
const t = useT();
+ const toaster = useToaster();
const editorRef = useRef();
+ const [loading, setLoading] = useState(false);
const uppy = useUppyUploader({
onUploadSuccess: (result: any) => {
@@ -566,15 +571,32 @@ export const Editor: FC<{
uppy.clear();
},
allowedFileTypes: 'image/*,video/mp4',
+ onStart: () => setLoading(true),
+ onEnd: () => setLoading(false),
});
const onDrop = useCallback(
(acceptedFiles: File[]) => {
+ const totalSize = acceptedFiles.reduce((acc, file) => acc + file.size, 0);
+
+ if (totalSize > MAX_UPLOAD_SIZE) {
+ toaster.show(
+ t(
+ 'upload_size_limit_exceeded',
+ 'Upload size limit exceeded. Maximum 1 GB per upload session.'
+ ),
+ 'warning'
+ );
+ return;
+ }
+
+ setLoading(true);
+
for (const file of acceptedFiles) {
uppy.addFile(file);
}
},
- [uppy]
+ [uppy, toaster, t]
);
const paste = useCallback(
@@ -588,21 +610,47 @@ export const Editor: FC<{
return;
}
+ const files: File[] = [];
// @ts-ignore
for (const item of clipboardItems) {
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
- uppy.addFile(file);
+ files.push(file);
}
}
}
+
+ const totalSize = files.reduce((acc, file) => acc + file.size, 0);
+
+ if (totalSize > MAX_UPLOAD_SIZE) {
+ toaster.show(
+ t(
+ 'upload_size_limit_exceeded',
+ 'Upload size limit exceeded. Maximum 1 GB per upload session.'
+ ),
+ 'warning'
+ );
+ return;
+ }
+
+ setLoading(true);
+
+ for (const file of files) {
+ uppy.addFile(file);
+ }
},
- [uppy, num, comments]
+ [uppy, num, comments, toaster, t]
);
const { getRootProps, isDragActive } = useDropzone({
- onDrop,
+ onDrop: (files) => {
+ if (loading) {
+ toaster.show('Upload current in progress, please wait and then try again.', 'warning');
+ return ;
+ }
+ onDrop(files);
+ },
noDrag: num > 0 && comments === 'no-media',
});