Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions extension/src/components/dialog/LeaveRoomDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { useHtmlActions, useRoomActions } from "@cb/hooks/store";
import {
useLeetCodeProblemsHtmlActions,
useRoomActions,
} from "@cb/hooks/store";
import { Button } from "@cb/lib/components/ui/button";
import { DialogClose } from "@cb/lib/components/ui/dialog";
import { DialogOverlay } from "@radix-ui/react-dialog";
Expand All @@ -9,7 +12,7 @@ import { RoomDialog, baseButtonClassName } from "./RoomDialog";

export function LeaveRoomDialog() {
const { leave, closeSidebarTab } = useRoomActions();
const { blurHtml, unblurHtml } = useHtmlActions();
const { blurHtml, unblurHtml } = useLeetCodeProblemsHtmlActions();

const leaveRoomThrottled = React.useMemo(() => {
return throttle((event: React.MouseEvent<HTMLButtonElement>) => {
Expand Down
52 changes: 47 additions & 5 deletions extension/src/components/panel/editor/tab/CodeTab.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,56 @@
import { SkeletonWrapper } from "@cb/components/ui/SkeletonWrapper";
import { DOM } from "@cb/constants";
import { useCodeBuddyMonacoHtmlActions } from "@cb/hooks/store";
import React from "react";

// note: This needs to match the sidepanel icons
const MONACO_EDITOR_Z_INDEX = 1000;

export const CodeTab: React.FC = () => {
const { showHtml } = useCodeBuddyMonacoHtmlActions();

const onContainerRefCallback = React.useCallback(
(node: HTMLElement | null) => {
if (!node) return;

let lastRect: DOMRect;
let animationFrameId: number;
const repositionIframeOnPositionChange = () => {
const rect = node.getBoundingClientRect();
if (
!lastRect ||
rect.top !== lastRect.top ||
rect.left !== lastRect.left
) {
showHtml(node, MONACO_EDITOR_Z_INDEX);
lastRect = rect;
}
animationFrameId = requestAnimationFrame(
repositionIframeOnPositionChange
);
};

animationFrameId = requestAnimationFrame(
repositionIframeOnPositionChange
);

const repositionIframeOnWindowResize = () => {
lastRect = node.getBoundingClientRect();
showHtml(node, MONACO_EDITOR_Z_INDEX);
};

window.addEventListener("resize", repositionIframeOnPositionChange);

return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener("resize", repositionIframeOnWindowResize);
};
},
[showHtml]
);

return (
<SkeletonWrapper loading={false} className="relative">
<div
id={DOM.CODEBUDDY_EDITOR_ID}
className="h-full w-full overflow-hidden"
/>
<div ref={onContainerRefCallback} className="h-full w-full" />
</SkeletonWrapper>
);
};
8 changes: 6 additions & 2 deletions extension/src/components/panel/info/LeetCodeQuestions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { QuestionSelectorPanel } from "@cb/components/panel/problem";
import { useHtmlActions, useRoomActions, useRoomData } from "@cb/hooks/store";
import {
useLeetCodeProblemsHtmlActions,
useRoomActions,
useRoomData,
} from "@cb/hooks/store";
import { SidebarTabIdentifier } from "@cb/store";
import { DialogTitle } from "@radix-ui/react-dialog";
import React from "react";
Expand All @@ -8,7 +12,7 @@ import { SidebarTabHeader, SidebarTabLayout } from "./SidebarTabLayout";
export const LeetCodeQuestions = () => {
const { activeSidebarTab, questions } = useRoomData();
const { addQuestion, closeSidebarTab } = useRoomActions();
const { hideHtml } = useHtmlActions();
const { hideHtml } = useLeetCodeProblemsHtmlActions();

React.useEffect(() => {
if (activeSidebarTab === undefined) hideHtml();
Expand Down
23 changes: 8 additions & 15 deletions extension/src/components/panel/problem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { SkeletonWrapper } from "@cb/components/ui/SkeletonWrapper";
import { DOM, EXTENSION } from "@cb/constants";
import { useHtmlActions } from "@cb/hooks/store";
import { useLeetCodeProblemsHtmlActions } from "@cb/hooks/store";
import useResource from "@cb/hooks/useResource";
import { Question } from "@cb/types";
import React, { useEffect } from "react";
import { toast } from "sonner";

const INJECTED_ATTRIBUTE = "data-injected";

const LEETCODE_PROBLEM_Z_INDEX = 3000;

interface QuestionSelectorPanelProps {
handleQuestionSelect: (link: string) => void;
filterQuestions: Question[];
Expand All @@ -19,7 +21,7 @@ export const QuestionSelectorPanel = React.memo(
const { register: registerObserver } = useResource<MutationObserver>({
name: "observer",
});
const iframeActions = useHtmlActions();
const iframeActions = useLeetCodeProblemsHtmlActions();

const onContainerRefCallback = React.useCallback(
(node: HTMLElement | null) => {
Expand All @@ -34,7 +36,7 @@ export const QuestionSelectorPanel = React.memo(
rect.top !== lastRect.top ||
rect.left !== lastRect.left
) {
iframeActions.showHtml(node);
iframeActions.showHtml(node, LEETCODE_PROBLEM_Z_INDEX);
lastRect = rect;
}
animationFrameId = requestAnimationFrame(
Expand All @@ -48,7 +50,7 @@ export const QuestionSelectorPanel = React.memo(

const repositionIframeOnWindowResize = () => {
lastRect = node.getBoundingClientRect();
iframeActions.showHtml(node);
iframeActions.showHtml(node, LEETCODE_PROBLEM_Z_INDEX);
};

window.addEventListener("resize", repositionIframeOnPositionChange);
Expand All @@ -64,10 +66,6 @@ export const QuestionSelectorPanel = React.memo(
useEffect(() => {
setLoading(true);

if (iframeActions.isContentProcessed()) {
setLoading(false);
}

const iframe = iframeActions.getHtmlElement();
if (iframe) {
const processIframe = async () => {
Expand Down Expand Up @@ -146,16 +144,13 @@ export const QuestionSelectorPanel = React.memo(
obs.disconnect()
);
observer.observe(rowContainer, { childList: true });
processQuestionLinks();

iframeActions.setContentProcessed(true);
await processQuestionLinks();
setLoading(false);
} catch (e) {
console.error("Unable to mount Leetcode iframe", e);
toast.error(
"Unable to show question selector, please try again later."
);
setLoading(false);
}
};

Expand All @@ -171,7 +166,7 @@ export const QuestionSelectorPanel = React.memo(
}
};

if (iframeActions.isHtmlLoaded()) {
if (iframe.contentDocument?.readyState === "complete") {
await processIframeDocument();
} else {
// Set up load event listener
Expand All @@ -186,8 +181,6 @@ export const QuestionSelectorPanel = React.memo(

processIframe();
}

return () => iframeActions.setContentProcessed(false);
}, [
handleQuestionSelect,
filterQuestions,
Expand Down
13 changes: 13 additions & 0 deletions extension/src/components/portals/CodeBuddyMonacoPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DOM } from "@cb/constants";
import { useCodeBuddyMonacoHtmlActions } from "@cb/hooks/store";

export const CodeBuddyMonacoPortal = () => {
const { setHtmlElement } = useCodeBuddyMonacoHtmlActions();
return (
<div
id={DOM.CODEBUDDY_EDITOR_ID}
ref={(node) => setHtmlElement(node)}
className="hidden"
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { URLS } from "@cb/constants";
import { useHtmlActions } from "@cb/hooks/store";
import { useLeetCodeProblemsHtmlActions } from "@cb/hooks/store";
import React from "react";

export const IframeContainer: React.FC = () => {
const { setHtmlElement } = useHtmlActions();
export const IFramePortal: React.FC = () => {
const { setHtmlElement } = useLeetCodeProblemsHtmlActions();

return (
<iframe
Expand Down
6 changes: 4 additions & 2 deletions extension/src/entrypoints/content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ContentScriptContext } from "#imports";
import { IframeContainer } from "@cb/components/iframe/IframeContainer";
import { CodeBuddyMonacoPortal } from "@cb/components/portals/CodeBuddyMonacoPortal";
import { IFramePortal } from "@cb/components/portals/IFramePortal";
import { ContentScript } from "@cb/components/root/ContentScript";
import { DOM, URLS } from "@cb/constants";
import { getOrCreateControllers } from "@cb/services";
Expand Down Expand Up @@ -45,7 +46,8 @@ const createUi = (ctx: ContentScriptContext) => {

createRoot(extensionRoot).render(
<>
<IframeContainer />
<IFramePortal />
<CodeBuddyMonacoPortal />
<ContentScript leetCodeNode={leetCodeNode} />
</>
);
Expand Down
12 changes: 10 additions & 2 deletions extension/src/hooks/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { CSS } from "@cb/constants";
import { useApp, useRoom } from "@cb/store";
import { useHtml } from "@cb/store/htmlStore";
import {
useCodeBuddyMonacoHtml,
useLeetCodeProblemsHtml,
} from "@cb/store/htmlStore";
import { useLeetCode } from "@cb/store/leetCodeStore";
import { QuestionProgressStatus, User } from "@cb/types";
import React from "react";
Expand Down Expand Up @@ -101,4 +104,9 @@ export const useAppActions = ({ panelRef }: any) => {
return { collapseExtension, expandExtension, setAppWidth, handleDoubleClick };
};

export const useHtmlActions = () => useHtml((state) => state.actions);
// todo(nickbar01234): Is there such thing as a hooks factory?
export const useLeetCodeProblemsHtmlActions = () =>
useLeetCodeProblemsHtml((state) => state.actions);

export const useCodeBuddyMonacoHtmlActions = () =>
useCodeBuddyMonacoHtml((state) => state.actions);
110 changes: 50 additions & 60 deletions extension/src/store/htmlStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,63 @@ import { BoundStore } from "@cb/types";
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

interface HtmlState {
contentProcessed: boolean;
htmlElement: HTMLIFrameElement | null;
interface HtmlState<T extends HTMLElement> {
htmlElement: T | null;
}

interface HtmlActions {
showHtml: (container: HTMLElement) => void;
interface HtmlActions<T extends HTMLElement> {
showHtml: (container: HTMLElement, zIndex: number) => void;
hideHtml: () => void;
blurHtml: () => void;
unblurHtml: () => void;
setContentProcessed: (processed: boolean) => void;
setHtmlElement: (element: HTMLIFrameElement | null) => void;
getHtmlElement: () => HTMLIFrameElement | null;
isContentProcessed: () => boolean;
isHtmlLoaded: () => boolean;
setHtmlElement: (element: T | null) => void;
getHtmlElement: () => T | null;
}

export const useHtml = create<BoundStore<HtmlState, HtmlActions>>()(
immer((set, get) => ({
contentProcessed: false,
htmlElement: null,
actions: {
showHtml: (container) => {
const { htmlElement } = get();
if (!htmlElement) return;
const containerRect = container.getBoundingClientRect();
const createHtmlStore = <T extends HTMLElement>() => {
return create<BoundStore<HtmlState<T>, HtmlActions<T>>>()(
immer((set, get) => ({
htmlElement: null,
actions: {
showHtml: (container, zIndex) => {
const { htmlElement } = get();
if (!htmlElement) return;
const containerRect = container.getBoundingClientRect();

// static styles
htmlElement.className =
"block fixed z-[3000] pointer-events-auto w-full h-full transition";
// Runtime-calculated positions, doesn't work with Tailwind classes
htmlElement.style.top = `${containerRect.top}px`;
htmlElement.style.left = `${containerRect.left}px`;
htmlElement.style.width = `${containerRect.width}px`;
htmlElement.style.height = `${containerRect.height}px`;
// static styles
htmlElement.className = `block fixed z-[${zIndex}] pointer-events-auto w-full h-full transition isolate`;
// Runtime-calculated positions, doesn't work with Tailwind classes
htmlElement.style.top = `${containerRect.top}px`;
htmlElement.style.left = `${containerRect.left}px`;
htmlElement.style.width = `${containerRect.width}px`;
htmlElement.style.height = `${containerRect.height}px`;
},
blurHtml: () => {
const { htmlElement } = get();
if (!htmlElement) return;
appendClassIdempotent(htmlElement, ["blur-sm", "filter"]);
},
unblurHtml: () => {
const { htmlElement } = get();
if (!htmlElement) return;
htmlElement.classList.remove("blur-sm", "filter");
},
hideHtml: () => {
const { htmlElement } = get();
if (!htmlElement) return;
htmlElement.className = "hidden pointer-events-none fixed";
},
setHtmlElement: (element) => set({ htmlElement: element }),
getHtmlElement: () => {
return get().htmlElement;
},
},
blurHtml: () => {
const { htmlElement } = get();
if (!htmlElement) return;
appendClassIdempotent(htmlElement, ["blur-sm", "filter"]);
},
unblurHtml: () => {
const { htmlElement } = get();
if (!htmlElement) return;
htmlElement.classList.remove("blur-sm", "filter");
},
hideHtml: () => {
const { htmlElement } = get();
if (!htmlElement) return;
htmlElement.className = "hidden pointer-events-none fixed";
},
setContentProcessed: (processed: boolean) => {
set((state) => {
state.contentProcessed = processed;
});
},
setHtmlElement: (element) => set({ htmlElement: element }),
getHtmlElement: () => {
return get().htmlElement;
},
isContentProcessed: () => {
return get().contentProcessed;
},
isHtmlLoaded: () =>
get().htmlElement?.contentDocument?.readyState === "complete",
},
}))
);
}))
);
};

export const useLeetCodeProblemsHtml = createHtmlStore<HTMLIFrameElement>();

export const useCodeBuddyMonacoHtml = createHtmlStore();

export type HtmlStore = typeof useHtml;
export type HtmlStore = ReturnType<typeof createHtmlStore>;
Loading