Skip to content

Commit

Permalink
feat: save images (wip)
Browse files Browse the repository at this point in the history
work in progress
  • Loading branch information
swouf committed Mar 29, 2023
1 parent b95038a commit ed9def3
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 10 deletions.
123 changes: 117 additions & 6 deletions src/components/ExcalidrawView.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { List } from 'immutable';
import debounce from 'lodash.debounce';
import isEqual from 'lodash.isequal';

import { FC, useEffect, useRef, useState } from 'react';
import { FC, useCallback, useEffect, useRef, useState } from 'react';

import Box from '@mui/material/Box';

import { Excalidraw } from '@excalidraw/excalidraw';
import type { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types';
import type {
ExcalidrawElement,
FileId,
} from '@excalidraw/excalidraw/types/element/types';
import {
AppState,
BinaryFileData,
BinaryFiles,
DataURL,
ExcalidrawImperativeAPI,
} from '@excalidraw/excalidraw/types/types';

import {
APP_DATA_TYPES,
ExcalidrawElementsAppData,
ExcalidrawStateAppData,
FileAppData,
} from '../config/appDataTypes';
import {
DEFAULT_EXCALIDRAW_ELEMENTS_APP_DATA,
Expand All @@ -32,6 +40,7 @@ import {
EXCALIDRAW_THEME,
} from '../config/settings';
import { reconcileElements } from '../utils/reconciliation';
import Loader from './common/Loader';
import RefreshButton from './common/RefreshButton';
import { useAppDataContext } from './context/AppDataContext';

Expand All @@ -46,8 +55,57 @@ const ExcalidrawView: FC = () => {
const [idElements, setIdElements] = useState<string>('');
const [idState, setIdState] = useState<string>('');
const [appState, setAppState] = useState<AppState>();
const [filesAppData, setFilesAppData] = useState<List<FileAppData>>(List());

const { appDataArray, patchAppData, postAppData } = useAppDataContext();
const {
appDataArray,
patchAppData,
postAppData,
deleteAppData,
uploadFile,
getFileContent,
status,
} = useAppDataContext();

const { isLoading } = status;

const getFile = useCallback(
async (fileAppData: FileAppData): Promise<BinaryFileData> => {
const { id, data } = fileAppData;
const { name: fileName, mimetype } = data.extra.s3File;
const fileData = await getFileContent(id || '').then((result) => {
console.log('fileData', result);
return result;
});
const file: BinaryFileData = {
mimeType: mimetype as BinaryFileData['mimeType'],
id: fileName as FileId,
dataURL: URL.createObjectURL(fileData) as DataURL,
created: data?.created as number,
};

return file;
},
[getFileContent],
);

const addAllFilesToExcalidraw = useCallback(
async (filesAppDataToAdd: List<FileAppData>): Promise<void> => {
const promisedFiles = filesAppDataToAdd.map(getFile);
const files: BinaryFileData[] = [];
promisedFiles.map((promisedFile) =>
promisedFile.then((d) => files.push(d)),
);
console.log('Files to be added: ', files);
excalidrawRef.current?.addFiles(files);
},
[getFile],
);

useEffect(() => {
// Add all the files to excalidraw
addAllFilesToExcalidraw(filesAppData);
}, [addAllFilesToExcalidraw, filesAppData]);

useEffect(() => {
const elementsAppData: ExcalidrawElementsAppData = (appDataArray.find(
Expand All @@ -65,6 +123,13 @@ const ExcalidrawView: FC = () => {
id: '',
data: { appState: {} },
};

setFilesAppData(
appDataArray.filter(
({ type }) => type === APP_DATA_TYPES.FILE,
) as List<FileAppData>,
);

const { id, data: newData } = elementsAppData;
setIdElements(id);
const { elements: newElements } = newData;
Expand Down Expand Up @@ -143,6 +208,35 @@ const ExcalidrawView: FC = () => {
}
};

const compareAndSaveFiles = useCallback(
async (
files: BinaryFiles,
currentFileData: List<FileAppData>,
): Promise<void> => {
const listOfFiles = List(Object.values(files));
console.log('Handling these files: ', listOfFiles);
console.log('With already these files: ', currentFileData);

const listOfIds = listOfFiles.map(({ id }) => String(id));
currentFileData
.filterNot(({ data: fileData }) => listOfIds.includes(fileData.name))
.forEach(({ id }) => deleteAppData({ id }));
if (!listOfFiles.isEmpty()) {
const listOfNames = currentFileData.map(
({ data: fileData }) => fileData.name,
);
listOfFiles
.filterNot(({ id }) => listOfNames.includes(id))
.forEach((file) => {
if (file.id) {
uploadFile(file);
}
});
}
},
[deleteAppData, uploadFile],
);

const debouncedCompareElements = useRef(
debounce(compareAndSaveElements, DEBOUNCE_COMPARE_ELEMENTS),
).current;
Expand All @@ -151,16 +245,26 @@ const ExcalidrawView: FC = () => {
debounce(saveState, DEBOUNCE_SAVE_STATE),
).current;

const debouncedCompareAndSaveFiles = useRef(
debounce(compareAndSaveFiles, DEBOUNCE_COMPARE_ELEMENTS),
).current;

const handleChange = (
elements: readonly ExcalidrawElement[],
newAppState: AppState,
files: BinaryFiles,
): void => {
const { isResizing, isRotating, isLoading } = newAppState;
const {
isResizing,
isRotating,
isLoading: isExcalidrawLoading,
} = newAppState;
if (
!(isLoading || isResizing || isRotating) &&
!(isExcalidrawLoading || isResizing || isRotating) &&
typeof localElements !== 'undefined'
) {
debouncedCompareElements(elements, localElements, idElements);
debouncedCompareAndSaveFiles(files, filesAppData);
debouncedSaveState(newAppState, idState);
} else {
debouncedCompareElements.cancel();
Expand All @@ -177,10 +281,17 @@ const ExcalidrawView: FC = () => {
debouncedCompareElements.cancel();
});

if (isLoading) {
return <Loader />;
}
return (
<Box height="100vh" width="100%">
<Excalidraw
initialData={{ elements: localElements, appState }}
initialData={{
elements: localElements,
appState,
files: filesAppData.map(getFile).toArray(),
}}
ref={excalidrawRef}
onChange={handleChange}
viewModeEnabled={EXCALIDRAW_ENABLE_VIEW_MODE}
Expand Down
95 changes: 92 additions & 3 deletions src/components/context/AppDataContext.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { List } from 'immutable';

import React, { FC, PropsWithChildren, createContext, useMemo } from 'react';
import React, {
FC,
PropsWithChildren,
createContext,
useCallback,
useMemo,
} from 'react';

import { AppData } from '@graasp/apps-query-client';
import {
Api,
AppData,
AppDataData,
useLocalContext,
} from '@graasp/apps-query-client';
import { AppDataVisibility } from '@graasp/apps-query-client/dist/config/constants';

import { MUTATION_KEYS, hooks, useMutation } from '../../config/queryClient';
import { REACT_APP_API_HOST } from '../../config/env';
import {
API_ROUTES,
MUTATION_KEYS,
hooks,
useMutation,
} from '../../config/queryClient';
import Loader from '../common/Loader';

type PostAppDataType = {
Expand All @@ -23,10 +40,25 @@ type DeleteAppDataType = {
id: string;
};

type FileUploadCompleteType = {
id: string;
data: AppDataData;
};

type FileToUploadType = {
mimeType: string;
id: string;
dataURL: string;
created?: number;
lastRetrieved?: number;
};

export type AppDataContextType = {
postAppData: (payload: PostAppDataType) => void;
patchAppData: (payload: PatchAppDataType) => void;
deleteAppData: (payload: DeleteAppDataType) => void;
uploadFile: (fileToUpload: FileToUploadType) => Promise<void>;
getFileContent: (fileId: string) => Promise<any>;
appDataArray: List<AppData>;
status: { isLoading: boolean; isFetching: boolean; isPreviousData: boolean };
};
Expand All @@ -35,6 +67,10 @@ const defaultContextValue = {
postAppData: () => null,
patchAppData: () => null,
deleteAppData: () => null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
uploadFile: () => new Promise<void>(() => {}), // Maybe there is a better way to write this?
// eslint-disable-next-line @typescript-eslint/no-empty-function
getFileContent: () => new Promise<any>(() => {}),
appDataArray: List<AppData>(),
status: {
isFetching: false,
Expand All @@ -47,6 +83,8 @@ const AppDataContext = createContext<AppDataContextType>(defaultContextValue);

export const AppDataProvider: FC<PropsWithChildren> = ({ children }) => {
const { data, isLoading, isFetching, isPreviousData } = hooks.useAppData();
const { itemId } = useLocalContext();
const { data: token } = hooks.useAuthToken(itemId);

const { mutate: postAppData } = useMutation<
unknown,
Expand All @@ -63,6 +101,53 @@ export const AppDataProvider: FC<PropsWithChildren> = ({ children }) => {
unknown,
DeleteAppDataType
>(MUTATION_KEYS.DELETE_APP_DATA);
const { mutate: onFileUploadComplete } = useMutation<
unknown,
unknown,
FileUploadCompleteType
>(MUTATION_KEYS.FILE_UPLOAD);

const uploadFile = useCallback(
async (fileToUpload: FileToUploadType): Promise<void> => {
const { mimeType, id, dataURL } = fileToUpload;
const xhr = new XMLHttpRequest();
xhr.open(
'POST',
`${REACT_APP_API_HOST}/${API_ROUTES.buildUploadFilesRoute(itemId)}`,
true,
);
xhr.setRequestHeader('authorization', `Bearer ${token}`);
const blob = await fetch(dataURL).then((res) => res.blob());
const file = new File([blob], id, { type: mimeType });
const formData = new FormData();
formData.append(id, file, id);
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(`File ${id} of type ${mimeType} successfully sent!`);
onFileUploadComplete({
id: itemId,
data: xhr.response?.body?.[0].filter(Boolean),
});
}
};
console.log(`Sending ${id} of type ${mimeType}...`);
xhr.send(formData);
},
[itemId, onFileUploadComplete, token],
);

const getFileContent = useCallback(
(fileId: string): Promise<any> => {
const content = Api.getFileContent({
id: fileId,
apiHost: REACT_APP_API_HOST,
token: token || '',
}).then((contentData) => contentData);
console.log('File content: ', content);
return content;
},
[token],
);

const contextValue: AppDataContextType = useMemo(
() => ({
Expand All @@ -72,6 +157,8 @@ export const AppDataProvider: FC<PropsWithChildren> = ({ children }) => {
patchAppData,
deleteAppData,
appDataArray: data || List<AppData>(),
uploadFile,
getFileContent,
status: {
isFetching,
isLoading,
Expand All @@ -86,6 +173,8 @@ export const AppDataProvider: FC<PropsWithChildren> = ({ children }) => {
isPreviousData,
patchAppData,
postAppData,
uploadFile,
getFileContent,
],
);

Expand Down
23 changes: 22 additions & 1 deletion src/config/appDataTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum APP_DATA_TYPES {
SESSION_TYPE = 'session',
EXCALIDRAW_ELEMENTS = 'excalidraw_elements',
EXCALIDRAW_STATE = 'excalidraw_state',
FILE = 'file',
}

type ExcalidrawElementsAppDataExtension = {
Expand All @@ -31,5 +32,25 @@ type ExcalidrawStateAppDataExtension = {
type ExcalidrawElementsAppData = AppData & ExcalidrawElementsAppDataExtension;
type ExcalidrawStateAppData = AppData & ExcalidrawStateAppDataExtension;

type s3FileData = {
name: string;
type: string;
extra: {
s3File: {
name: string;
path: string;
size: number;
mimetype: string;
};
};
};

type FileAppData = AppData & { data: s3FileData };

export { APP_DATA_TYPES };
export type { ExcalidrawElementsAppData, ExcalidrawStateAppData };
export type {
ExcalidrawElementsAppData,
ExcalidrawStateAppData,
s3FileData,
FileAppData,
};
11 changes: 11 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,14 @@ export const DEFAULT_EXCALIDRAW_STATE_APP_DATA = {
appState: {},
},
};

export const ALLOWED_MIMETYPES = [
'image/png',
'image/jpeg',
'image/svg+xml',
'image/gif',
'image/webp',
'image/bmp',
'image/x-icon',
'application/octet-stream',
];

0 comments on commit ed9def3

Please sign in to comment.