Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PE-69] fix: image restoration fixed for new images in private bucket #5839

Open
wants to merge 15 commits into
base: preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,20 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
src: remoteImageSrc,
setEditorContainer,
} = props;
const { width, height, aspectRatio } = node.attrs;
const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio } = node.attrs as ImageAttributes;
// states
const [size, setSize] = useState<Size>({
width: ensurePixelString(width, "35%"),
height: ensurePixelString(height, "auto"),
aspectRatio: aspectRatio || 1,
width: ensurePixelString(nodeWidth, "35%"),
height: ensurePixelString(nodeHeight, "auto"),
aspectRatio: nodeAspectRatio || null,
});
const [isResizing, setIsResizing] = useState(false);
const [initialResizeComplete, setInitialResizeComplete] = useState(false);
// refs
const containerRef = useRef<HTMLDivElement>(null);
const containerRect = useRef<DOMRect | null>(null);
const imageRef = useRef<HTMLImageElement>(null);
const [onFirstLoadError, setOnFirstLoadError] = useState(false);
Palanikannan1437 marked this conversation as resolved.
Show resolved Hide resolved

const updateAttributesSafely = useCallback(
(attributes: Partial<ImageAttributes>, errorMessage: string) => {
Expand Down Expand Up @@ -104,17 +105,17 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
}

setEditorContainer(closestEditorContainer);
const aspectRatio = img.naturalWidth / img.naturalHeight;
const aspectRatioCalculated = img.naturalWidth / img.naturalHeight;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent division by zero when calculating aspectRatioCalculated

When calculating aspectRatioCalculated, ensure that img.naturalHeight is not zero to avoid a potential division by zero error.

Apply this diff to add the check:

+ if (img.naturalHeight === 0) {
+   console.error("Image natural height is zero, cannot calculate aspect ratio.");
+   return;
+ }
  const aspectRatioCalculated = img.naturalWidth / img.naturalHeight;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const aspectRatioCalculated = img.naturalWidth / img.naturalHeight;
if (img.naturalHeight === 0) {
console.error("Image natural height is zero, cannot calculate aspect ratio.");
return;
}
const aspectRatioCalculated = img.naturalWidth / img.naturalHeight;


if (width === "35%") {
if (nodeWidth === "35%") {
const editorWidth = closestEditorContainer.clientWidth;
const initialWidth = Math.max(editorWidth * 0.35, MIN_SIZE);
const initialHeight = initialWidth / aspectRatio;
const initialHeight = initialWidth / aspectRatioCalculated;

const initialComputedSize = {
width: `${Math.round(initialWidth)}px` satisfies Pixel,
height: `${Math.round(initialHeight)}px` satisfies Pixel,
aspectRatio: aspectRatio,
aspectRatio: aspectRatioCalculated,
};

setSize(initialComputedSize);
Expand All @@ -124,9 +125,10 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
);
} else {
// as the aspect ratio in not stored for old images, we need to update the attrs
if (!aspectRatio) {
// or if aspectRatioCalculated from the image's width and height doesn't match stored aspectRatio then also we'll update the attrs
if (!nodeAspectRatio || nodeAspectRatio !== aspectRatioCalculated) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use tolerance when comparing floating-point numbers

Directly comparing floating-point numbers can lead to precision issues. Use an epsilon value to compare nodeAspectRatio and aspectRatioCalculated to account for minor differences.

Apply this diff:

+ const EPSILON = 0.00001;
  if (
    !nodeAspectRatio ||
-   nodeAspectRatio !== aspectRatioCalculated
+   Math.abs(nodeAspectRatio - aspectRatioCalculated) > EPSILON
  ) {
    // existing code
  }

Committable suggestion was skipped due to low confidence.

setSize((prevSize) => {
const newSize = { ...prevSize, aspectRatio };
const newSize = { ...prevSize, aspectRatio: aspectRatioCalculated };
updateAttributesSafely(
newSize,
"Failed to update attributes while initializing images with width but no aspect ratio:"
Expand All @@ -136,16 +138,17 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
}
}
setInitialResizeComplete(true);
}, [width, updateAttributes, editorContainer, aspectRatio]);
}, [nodeWidth, updateAttributes, editorContainer, nodeAspectRatio, size.aspectRatio]);

// for real time resizing
useLayoutEffect(() => {
setSize((prevSize) => ({
...prevSize,
width: ensurePixelString(width),
height: ensurePixelString(height),
width: ensurePixelString(nodeWidth),
height: ensurePixelString(nodeHeight),
aspectRatio: nodeAspectRatio,
}));
}, [width, height]);
}, [nodeWidth, nodeHeight, nodeAspectRatio]);

const handleResize = useCallback(
(e: MouseEvent | TouchEvent) => {
Expand All @@ -158,7 +161,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {

setSize((prevSize) => ({ ...prevSize, width: `${newWidth}px`, height: `${newHeight}px` }));
},
[size]
[size.aspectRatio]
);

const handleResizeEnd = useCallback(() => {
Expand All @@ -181,11 +184,15 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
window.addEventListener("mousemove", handleResize);
window.addEventListener("mouseup", handleResizeEnd);
window.addEventListener("mouseleave", handleResizeEnd);
window.addEventListener("touchmove", handleResize, { passive: false });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Re-evaluate the necessity of { passive: false } in touch event listener

The handleResize function does not call e.preventDefault(). If you are not preventing the default touch behavior, you can remove { passive: false } to improve scrolling performance.

Apply this diff:

- window.addEventListener("touchmove", handleResize, { passive: false });
+ window.addEventListener("touchmove", handleResize);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.addEventListener("touchmove", handleResize, { passive: false });
window.addEventListener("touchmove", handleResize);

window.addEventListener("touchend", handleResizeEnd);

return () => {
window.removeEventListener("mousemove", handleResize);
window.removeEventListener("mouseup", handleResizeEnd);
window.removeEventListener("mouseleave", handleResizeEnd);
window.removeEventListener("touchmove", handleResize);
window.removeEventListener("touchend", handleResizeEnd);
};
}
}, [isResizing, handleResize, handleResizeEnd]);
Expand All @@ -202,7 +209,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {

// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete;
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete || onFirstLoadError;
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
const showImageUtils = remoteImageSrc && initialResizeComplete;
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
Expand All @@ -217,7 +224,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
onMouseDown={handleImageMouseDown}
style={{
width: size.width,
aspectRatio: size.aspectRatio,
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
}}
>
{showImageLoader && (
Expand All @@ -230,9 +237,23 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
ref={imageRef}
src={displayedImageSrc}
onLoad={handleImageLoad}
onError={(e) => {
console.error("Error loading image", e);
setFailedToLoadImage(true);
onError={async (e) => {
// for old image extension this command doesn't exist
if (!editor?.commands.restoreImage) {
setFailedToLoadImage(true);
}

try {
setOnFirstLoadError(true);
// this is a type error from tiptap, don't remove await until it's fixed
await editor?.commands.restoreImage?.(remoteImageSrc);
imageRef.current.src = remoteImageSrc;
} catch {
setFailedToLoadImage(true);
console.log("Error while loading image", e);
} finally {
setOnFirstLoadError(false);
}
Palanikannan1437 marked this conversation as resolved.
Show resolved Hide resolved
}}
width={size.width}
className={cn("image-component block rounded-md", {
Expand All @@ -243,7 +264,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
})}
style={{
width: size.width,
aspectRatio: size.aspectRatio,
...(size.aspectRatio && { aspectRatio: size.aspectRatio }),
}}
/>
{showImageUtils && (
Expand Down Expand Up @@ -283,6 +304,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
}
)}
onMouseDown={handleResizeStart}
onTouchStart={handleResizeStart}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { useEffect, useRef, useState } from "react";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { Editor, NodeViewWrapper } from "@tiptap/react";
import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
// extensions
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";

export type CustomImageNodeViewProps = {
export type CustomImageComponentProps = {
getPos: () => number;
editor: Editor;
node: ProsemirrorNode & {
node: NodeViewProps["node"] & {
attrs: ImageAttributes;
};
updateAttributes: (attrs: Record<string, any>) => void;
selected: boolean;
};

export type CustomImageNodeViewProps = NodeViewProps & CustomImageComponentProps;

export const CustomImageNode = (props: CustomImageNodeViewProps) => {
const { getPos, editor, node, updateAttributes, selected } = props;
const { src: remoteImageSrc } = node.attrs;

const [isUploaded, setIsUploaded] = useState(false);
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
Expand All @@ -37,14 +39,13 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
// the image is already uploaded if the image-component node has src attribute
// and we need to remove the blob from our file system
useEffect(() => {
const remoteImageSrc = node.attrs.src;
if (remoteImageSrc) {
setIsUploaded(true);
setImageFromFileSystem(undefined);
} else {
setIsUploaded(false);
}
}, [node.attrs.src]);
}, [remoteImageSrc]);

return (
<NodeViewWrapper>
Expand All @@ -55,7 +56,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
editorContainer={editorContainer}
editor={editor}
// @ts-expect-error function not expected here, but will still work
src={editor?.commands?.getImageSource?.(node.attrs.src)}
src={editor?.commands?.getImageSource?.(remoteImageSrc)}
getPos={getPos}
node={node}
setEditorContainer={setEditorContainer}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import { ChangeEvent, useCallback, useEffect, useMemo, useRef } from "react";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { Editor } from "@tiptap/core";
import { ImageIcon } from "lucide-react";
// helpers
import { cn } from "@/helpers/common";
// hooks
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
// extensions
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
import { type CustomImageComponentProps, getImageComponentImageFileMap } from "@/extensions/custom-image";

export const CustomImageUploader = (props: {
editor: Editor;
failedToLoadImage: boolean;
getPos: () => number;
loadImageFromFileSystem: (file: string) => void;
type CustomImageUploaderProps = CustomImageComponentProps & {
maxFileSize: number;
node: ProsemirrorNode & {
attrs: ImageAttributes;
};
selected: boolean;
loadImageFromFileSystem: (file: string) => void;
failedToLoadImage: boolean;
setIsUploaded: (isUploaded: boolean) => void;
updateAttributes: (attrs: Record<string, any>) => void;
}) => {
};

export const CustomImageUploader = (props: CustomImageUploaderProps) => {
const {
editor,
failedToLoadImage,
Expand All @@ -36,8 +29,8 @@ export const CustomImageUploader = (props: {
// refs
const fileInputRef = useRef<HTMLInputElement>(null);
const hasTriggeredFilePickerRef = useRef(false);
const { id: imageEntityId } = node.attrs;
// derived values
const imageEntityId = node.attrs.id;
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);

const onUpload = useCallback(
Expand Down
41 changes: 24 additions & 17 deletions packages/editor/src/core/extensions/custom-image/custom-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ declare module "@tiptap/core" {
imageComponent: {
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
uploadImage: (file: File) => () => Promise<string> | undefined;
restoreImage: (src: string) => () => Promise<void>;
SatishGandham marked this conversation as resolved.
Show resolved Hide resolved
getImageSource?: (path: string) => () => string;
};
}
Expand All @@ -40,8 +41,8 @@ export const CustomImageExtension = (props: TFileHandler) => {
const {
getAssetSrc,
upload,
delete: deleteImage,
restore: restoreImage,
delete: deleteImageFn,
restore: restoreImageFn,
validation: { maxFileSize },
} = props;

Expand Down Expand Up @@ -85,36 +86,39 @@ export const CustomImageExtension = (props: TFileHandler) => {
return ["image-component", mergeAttributes(HTMLAttributes)];
},

addKeyboardShortcuts() {
return {
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
};
},

addProseMirrorPlugins() {
return [
TrackImageDeletionPlugin(this.editor, deleteImageFn, this.name),
TrackImageRestorationPlugin(this.editor, restoreImageFn, this.name),
];
},

onCreate(this) {
const imageSources = new Set<string>();
this.editor.state.doc.descendants((node) => {
if (node.type.name === this.name) {
if (!node.attrs.src.startsWith("http")) {
return;
}
imageSources.add(node.attrs.src);
}
});
imageSources.forEach(async (src) => {
try {
await restoreImage(src);
await restoreImageFn(src);
} catch (error) {
console.error("Error restoring image: ", error);
}
});
},

addKeyboardShortcuts() {
return {
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name),
};
},

addProseMirrorPlugins() {
return [
TrackImageDeletionPlugin(this.editor, deleteImage, this.name),
TrackImageRestorationPlugin(this.editor, restoreImage, this.name),
];
},

addStorage() {
return {
fileMap: new Map(),
Expand Down Expand Up @@ -179,6 +183,9 @@ export const CustomImageExtension = (props: TFileHandler) => {
const fileUrl = await upload(file);
return fileUrl;
},
restoreImage: (src: string) => async () => {
await restoreImageFn(src);
},
getImageSource: (path: string) => () => getAssetSrc(path),
};
},
Expand Down
Loading
Loading