Skip to content

Commit

Permalink
Sync image elements with cloud.
Browse files Browse the repository at this point in the history
  • Loading branch information
Yyassin committed Mar 11, 2024
1 parent dc6ddf1 commit 7dfb354
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 28 deletions.
91 changes: 65 additions & 26 deletions client/src/components/lib/BoardScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { useToast } from '@/components/ui/use-toast';
import { ToastAction } from '@/components/ui/toast';
import { useAuthStore } from '@/stores/AuthStore';
import { fetchImageFromFirebaseStorage } from '@/views/SignInPage';
import { commitImageToCache, getImageDataUrl } from '@/lib/image';
import { BinaryFileData } from '@/types';

export const createStateWithRoughElement = (state: CanvasElementState) => {
const roughElements: Record<string, CanvasElement['roughElement']> = {};
Expand Down Expand Up @@ -49,7 +51,6 @@ export const createStateWithRoughElement = (state: CanvasElementState) => {
).roughElement;
}
state.roughElements = roughElements;
state.fileIds = {};

return state;
};
Expand All @@ -75,9 +76,12 @@ export const BoardScroll = ({
'folder',
'setBoardMeta',
]);
const { setCanvasElementState } = useCanvasElementStore([
'setCanvasElementState',
]);
const { setCanvasElementState, editCanvasElement, fileIds } =
useCanvasElementStore([
'setCanvasElementState',
'editCanvasElement',
'fileIds',
]);
const { userID } = useAuthStore(['userID']);

const setCanvasState = () => {
Expand All @@ -99,6 +103,32 @@ export const BoardScroll = ({
return dateB.getTime() - dateA.getTime();
});

useEffect(() => {
if (state === undefined) return;

const canvas = renderElementsOnOffscreenCanvas(state.allIds, {
p1: state.p1,
p2: state.p2,
angles: state.angles,
types: state.types,
freehandPoints: state.freehandPoints,
freehandBounds: state.freehandBounds,
textStrings: state.textStrings,
fontFamilies: state.fontFamilies,
fontSizes: state.fontSizes,
fillColors: state.fillColors,
isImagePlaceds: state.isImagePlaceds,
fileIds: fileIds,
roughElements: state.roughElements,
opacities: state.opacities,
strokeColors: state.strokeColors,
strokeWidths: state.strokeWidths,
});

setThumbnailUrl(canvas?.toDataURL('image/png') ?? '');
// Rerender on isImagePlaceds change to update the thumbnail
}, [state, fileIds]);

return (
<div className="relative flex flex-col mx-2 h-full">
<div className="w-full h-[250px] overflow-x-scroll scroll whitespace-nowrap scroll-smooth">
Expand All @@ -123,36 +153,45 @@ export const BoardScroll = ({
params: { id: board.id, userID },
});

// Fetch images from firebase storage
Object.entries(
boardState.data.board.serialized.fileIds,
).forEach(async ([elemId, fileId]) => {
const imageUrl = await fetchImageFromFirebaseStorage(
`boardImages/${fileId}.jpg`,
);
const dataUrl =
imageUrl && (await getImageDataUrl(imageUrl));
if (!dataUrl)
throw new Error('Failed to resolve saved image dataurls');

const binary = {
dataURL: dataUrl,
id: fileId,
mimeType: 'image/jpeg',
} as BinaryFileData;

const imageElement = { id: elemId };
commitImageToCache(
{
...binary,
lastRetrieved: Date.now(),
},
imageElement,
// Will set fileIds, triggering a rerender. A placeholder
// will be shown in the mean time.
editCanvasElement,
);
});

collabID = boardState.data.collabID;
users = boardState.data.users;
permission = boardState.data.permissionLevel;

const state = createStateWithRoughElement(
boardState.data.board.serialized,
);

setState(state);

const canvas = renderElementsOnOffscreenCanvas(state.allIds, {
p1: state.p1,
p2: state.p2,
angles: state.angles,
types: state.types,
freehandPoints: state.freehandPoints,
freehandBounds: state.freehandBounds,
textStrings: state.textStrings,
fontFamilies: state.fontFamilies,
fontSizes: state.fontSizes,
fillColors: state.fillColors,
isImagePlaceds: state.isImagePlaceds,
fileIds: state.fileIds,
roughElements: state.roughElements,
opacities: state.opacities,
strokeColors: state.strokeColors,
strokeWidths: state.strokeWidths,
});

setThumbnailUrl(canvas?.toDataURL('image/png') ?? '');
}
const collaboratorAvatarMeta = (
await axios.put(REST.collaborators.getAvatar, {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/lib/SaveOpenDropDownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export const SaveOpenDropDownMenu = () => {
className="group text-[13px] indent-[10px] leading-none text-violet11 rounded-[3px] flex items-center h-[25px] px-[5px] relative pl-[25px] select-none outline-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:bg-violet9 hover:bg-indigo-200"
onClick={handleSave}
>
<DownloadIcon /> Save Localy
<DownloadIcon /> Save Locally
</DropdownMenu.Item>
</>
);
Expand Down
48 changes: 47 additions & 1 deletion client/src/lib/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
DRAGGING_THRESHOLD,
MAX_ALLOWED_FILE_BYTES,
} from '@/constants';
import { firebaseApp } from '@/firebaseDB/firebase';
import { getStorage, ref, uploadBytes } from 'firebase/storage';

/**
* Defines helpers for interacting with images in state, and rendering them on the canvas.
Expand Down Expand Up @@ -303,7 +305,7 @@ export const commitImageToCache = <T extends Pick<CanvasElement, 'id'>>(

// Update the image element in the application state with the file ID
// once loaded, this will trigger a rerender to show the image, provided
// the element has been placed.
// the element has been placed.d
editImageInState(imageElement.id, { fileId: file.id });
// Resolve the Promise with the inserted image element
resolve(imageElement);
Expand Down Expand Up @@ -359,6 +361,13 @@ export const injectImageElement = async (
throw new Error('Failed to insert image');
}

// Try to upload the image to firebase, in case it doesn't already exist.
const storage = getStorage(firebaseApp);
const storageRef = ref(storage, `boardImages/${fileId}.jpg`); // give the image a random id
uploadBytes(storageRef, imageFile).catch(() => {
alert('Error uploading');
});

// Check if file data already exists in the cache
const existingFileData = fileCache.cache[fileId];
// If an image doesn't exist in cache, create it and add it.
Expand Down Expand Up @@ -411,3 +420,40 @@ export const injectImageElement = async (
console.error('Failed to insert image');
}
};

/**
* Given an image URL, returns a data URL for the image.
* @param url The URL of the image.
* @returns The data URL for the image.
*/
export const getImageDataUrl = async (url: string) => {
return new Promise((resolve, reject) => {
// Create an image element and load the image from the URL
const img = new Image();
img.crossOrigin = 'Anonymous';

// Set up event listeners to resolve or reject the promise
img.onload = function () {
// Create a canvas and draw the image on it
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');

if (ctx === null) {
reject(new Error('Failed to get canvas context'));
return;
}

// Draw the image on the canvas and resolve the promise with the data URL
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL('image/jpeg');
resolve(dataURL);
};
img.onerror = function (error) {
reject(new Error('Failed to load image: ' + error));
};
// Set the image source to the URL, this will trigger the load event
img.src = url;
});
};
16 changes: 16 additions & 0 deletions client/src/stores/CanvasElementsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@ const removeCanvasElements =
const types = { ...state.types };
const strokeColors = { ...state.strokeColors };
const fillColors = { ...state.fillColors };
const fileIds = { ...state.fileIds };
const isImagePlaceds = { ...state.isImagePlaceds };
const attachedFileUrls = { ...state.attachedFileUrls };
const attachedUrls = { ...state.attachedUrls };
const fontFamilies = { ...state.fontFamilies };
Expand All @@ -565,6 +567,16 @@ const removeCanvasElements =
const textStrings = { ...state.textStrings };

ids.forEach((id) => {
// Delete images from storage, if applicable
// const fileId = fileIds[id];
// if (fileId !== undefined) {
// const storage = getStorage(firebaseApp);
// const storageRef = ref(storage, `boardImages/${fileId}.jpg`);
// deleteObject(storageRef)
// .then(() => console.log('Deleted image', fileId))
// .catch((error) => console.error('Error deleting image', error));
// }

allIds.splice(allIds.indexOf(id), 1);
delete types[id];
delete strokeColors[id];
Expand All @@ -584,6 +596,8 @@ const removeCanvasElements =
delete p2s[id];
delete angles[id];
delete textStrings[id];
delete fileIds[id];
delete isImagePlaceds[id];
});

return {
Expand All @@ -607,6 +621,8 @@ const removeCanvasElements =
p2: p2s,
textStrings,
angles,
fileIds,
isImagePlaceds,
};
});

Expand Down

0 comments on commit 7dfb354

Please sign in to comment.