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

Collaboration Websocket Server #468

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
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
33 changes: 33 additions & 0 deletions api/share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { VercelRequest, VercelResponse } from "@vercel/node";
import { confirmActiveSubscriptionFromToken } from "./_lib/_helpers";
import { supabase } from "./_lib/_supabase";

export default async function handler(req: VercelRequest, res: VercelResponse) {
// make sure user is logged in
const token = req.headers.authorization;
if (!token) {
return res.status(401).send("Unauthorized");
}
if (!confirmActiveSubscriptionFromToken(token)) {
return res.status(402).send("Unauthorized");
}

// get chartId and userEmail from request body
const { chartId, userEmail } = req.body;
if (!chartId || !userEmail) {
return res.status(400).send("Missing chartId or userEmail");
}

const result = await supabase.from("shared_charts").insert([
{
flowchart_id: chartId,
user_email: userEmail,
},
]);

if (result.error) {
return res.status(400).send(result.error.message);
}

return res.status(200).send("Chart shared successfully");
}
14 changes: 11 additions & 3 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@
"@sentry/tracing": "^7.38.0",
"@stripe/react-stripe-js": "^1.16.4",
"@stripe/stripe-js": "^1.46.0",
"@supabase/gotrue-js": "^2",
"@supabase/supabase-js": "^2",
"@supabase/gotrue-js": "^2.12.1",
"@supabase/realtime-js": "^2.6.0",
"@supabase/supabase-js": "^2.8.0",
"@svgr/webpack": "^6.3.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
Expand Down Expand Up @@ -107,6 +108,10 @@
"svgo": "^2.8.0",
"use-debounce": "^5.2.1",
"web-vitals": "^1.0.1",
"y-monaco": "^0.1.4",
"y-protocols": "^1.0.5",
"y-websocket": "^1.4.5",
"yjs": "^13.5.47",
"zustand": "^4.3.3"
},
"eslintConfig": {
Expand Down Expand Up @@ -189,6 +194,9 @@
],
"transformIgnorePatterns": [
"node_modules/(?!(react-use-localstorage)).*\\.js$"
]
],
"moduleNameMapper": {
"monaco-editor": "<rootDir>/node_modules/@monaco-editor/react"
}
}
}
2 changes: 1 addition & 1 deletion app/src/components/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { colors, darkTheme } from "../slang/config";
type Theme = typeof colors;

// Stored in localStorage
export type UserSettings = {
type UserSettings = {
mode: "light" | "dark";
language?: string;
};
Expand Down
5 changes: 3 additions & 2 deletions app/src/components/CloneButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Trans } from "@lingui/macro";
import { FaCopy } from "react-icons/fa";
import { useHistory } from "react-router-dom";

import { getDoc } from "../lib/docHelpers";
import { docToString } from "../lib/docToString";
import { randomChartName, titleToLocalStorageKey } from "../lib/helpers";
import { docToString, useDoc } from "../lib/useDoc";
import { Type } from "../slang";
import styles from "./EditorWrapper.module.css";

export function CloneButton() {
const { push } = useHistory();
const fullText = useDoc((s) => docToString(s));
const fullText = docToString(getDoc());
return (
<button
className={`${styles.ShareButton} ${styles.cloneBtn}`}
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/EditorError.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AnimatePresence, motion } from "framer-motion";
import { BiErrorCircle } from "react-icons/bi";

import { useParseError } from "../lib/useDoc";
import { useParseError } from "../lib/useParseError";
import { Box, Type } from "../slang";
import styles from "./EditorError.module.css";

Expand Down
15 changes: 4 additions & 11 deletions app/src/components/EditorOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Trans } from "@lingui/macro";
import produce from "immer";
import React, { ReactNode } from "react";

import { setMetaImmer } from "../lib/docHelpers";
import { parsers, useParser } from "../lib/parsers";
import { useDoc } from "../lib/useDoc";
import { Box, Type } from "../slang";
import styles from "./EditorOptions.module.css";
import { SyntaxReference } from "./SyntaxReference";
Expand All @@ -29,15 +28,9 @@ export function EditorOptions({ children }: { children: ReactNode }) {
background="color-lineNumbers"
value={parser}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
useDoc.setState(
(state) => {
return produce(state, (draft) => {
draft.meta.parser = e.target.value;
});
},
false,
"syntax"
);
setMetaImmer((draft) => {
draft.parser = e.target.value;
}, "syntax");
}}
>
{parsers.map((p) => (
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/EditorWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Export } from "phosphor-react";
import { Suspense, useContext } from "react";

import { useIsReadOnly, useIsValidCustomer } from "../lib/hooks";
import { useDocDetails } from "../lib/useDoc";
import { useDetails } from "../lib/useDetails";
import { Box, Type } from "../slang";
import { AppContext } from "./AppContext";
import { CloneButton } from "./CloneButton";
Expand All @@ -17,7 +17,7 @@ import { RenameButton } from "./RenameButton";
* Adds title and export button to the editor
*/
export function EditorWrapper({ children }: { children: React.ReactNode }) {
const title = useDocDetails("title", "flowchart.fun");
const title = useDetails("title", "flowchart.fun");
const { setShareModal } = useContext(AppContext);
const isReadOnly = useIsReadOnly();
const isValidCustomer = useIsValidCustomer();
Expand Down
75 changes: 41 additions & 34 deletions app/src/components/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import klay from "cytoscape-klay";
import cytoscapeSvg from "cytoscape-svg";
import throttle from "lodash.throttle";
import React, {
memo,
Dispatch,
MutableRefObject,
SetStateAction,
useCallback,
useEffect,
useMemo,
Expand All @@ -18,9 +19,10 @@ import { useDebouncedCallback } from "use-debounce";

import { buildStylesForGraph } from "../lib/buildStylesForGraph";
import { cytoscape } from "../lib/cytoscape";
import { getDoc, setMetaImmer, subscribeToDoc } from "../lib/docHelpers";
import { getGetSize, TGetSize } from "../lib/getGetSize";
import { getLayout } from "../lib/getLayout";
import { getUserStyle } from "../lib/getTheme";
import { getUserStyle, useUserStyle } from "../lib/getTheme";
import { DEFAULT_GRAPH_PADDING } from "../lib/graphOptions";
import {
useBackgroundColor,
Expand All @@ -30,12 +32,14 @@ import {
import { isError } from "../lib/helpers";
import { getAnimationSettings } from "../lib/hooks";
import { Parsers, universalParse, useParser } from "../lib/parsers";
import { useYDoc } from "../lib/realtime";
import { Theme } from "../lib/themes/constants";
import original from "../lib/themes/original";
import { useContextMenuState } from "../lib/useContextMenuState";
import { Doc, useDoc, useParseError } from "../lib/useDoc";
import { Doc, useDetailsStore } from "../lib/useDoc";
import { useGraphStore } from "../lib/useGraphStore";
import { useHoverLine } from "../lib/useHoverLine";
import { useParseError } from "../lib/useParseError";
import { Box } from "../slang";
import { getNodePositionsFromCy } from "./getNodePositionsFromCy";
import styles from "./Graph.module.css";
Expand All @@ -58,32 +62,24 @@ if (!cytoscape.prototype.hasInitialised) {

const shouldAnimate = getAnimationSettings();

const Graph = memo(function Graph({ shouldResize }: { shouldResize: number }) {
export default function Graph({ shouldResize }: { shouldResize: number }) {
const [initResizeNumber] = useState(shouldResize);
const cy = useRef<undefined | Core>();
const errorCatcher = useRef<undefined | Core>();
const graphInitialized = useRef(false);
const themeKey = useThemeKey();
const theme = useCurrentTheme(themeKey) as unknown as Theme;
const bg = useBackgroundColor(theme);
const userStyle = useUserStyle();
const [graphReady, setGraphReady] = useState(false);

const getSize = useRef<TGetSize>(getGetSize(theme));
const parser = useParser();
const handleDragFree = useCallback(() => {
const nodePositions = getNodePositionsFromCy();
useDoc.setState(
(state) => {
return {
...state,
meta: {
...state.meta,
nodePositions,
},
};
},
false,
"Graph/handleDragFree"
);
setMetaImmer((draft) => {
draft.nodePositions = nodePositions;
}, "Graph/handleDragFree");
}, []);

const handleResize = useCallback(() => {
Expand All @@ -103,6 +99,7 @@ const Graph = memo(function Graph({ shouldResize }: { shouldResize: number }) {

// Initialize Graph
useEffect(() => {
if (graphInitialized.current) return;
return initializeGraph({
errorCatcher,
cy,
Expand All @@ -124,21 +121,24 @@ const Graph = memo(function Graph({ shouldResize }: { shouldResize: number }) {

// Apply theme
useEffect(() => {
getStyleUpdater({ cy, errorCatcher, bg })(theme);
}, [bg, theme]);
getStyleUpdater({ cy, errorCatcher, bg, userStyle })(theme);
}, [bg, theme, userStyle]);

const throttleUpdate = useMemo(
() =>
getGraphUpdater({ cy, errorCatcher, graphInitialized, getSize, parser }),
getGraphUpdater({
cy,
errorCatcher,
graphInitialized,
getSize,
parser,
}),
[parser]
);

useEffect(() => {
throttleUpdate();
const unsubscribe = useDoc.subscribe((doc) => {
throttleUpdate(doc);
});
return unsubscribe;
return subscribeToDoc(throttleUpdate);
}, [throttleUpdate]);

// Update Graph when Sponsor Layouts Load
Expand Down Expand Up @@ -167,9 +167,7 @@ const Graph = memo(function Graph({ shouldResize }: { shouldResize: number }) {
<GraphContextMenu />
</Box>
);
});

export default Graph;
}

function initializeGraph({
errorCatcher,
Expand All @@ -180,7 +178,7 @@ function initializeGraph({
}) {
try {
errorCatcher.current = cytoscape();
const bg = (useDoc.getState().meta?.background as string) ?? original.bg;
const bg = (getDoc().meta?.background as string) ?? original.bg;
cy.current = cytoscape({
container: document.getElementById("cy"), // container to render in
elements: [],
Expand Down Expand Up @@ -272,7 +270,7 @@ function getGraphUpdater({
return throttle((_doc?: Doc) => {
if (!cy.current) return;
if (!errorCatcher.current) return;
const doc = _doc || useDoc.getState();
const doc = _doc || getDoc();
let elements: cytoscape.ElementDefinition[] = [];

try {
Expand All @@ -284,14 +282,20 @@ function getGraphUpdater({
// TODO: what happens if you add animate false and run() here?
errorCatcher.current.layout(layout);

const isHosted = useDetailsStore.getState().isHosted;
const isReady = useYDoc.getState().isReady;
const isReadyToAnimate = !isHosted || isReady;

// Update
cy.current.json({ elements });
if (layout.name !== "preset") {
cy.current
.layout({
animate: graphInitialized.current
? elements.length < 200
? shouldAnimate
animate: isReadyToAnimate
? graphInitialized.current
? elements.length < 200
? shouldAnimate
: false
: false
: false,
animationDuration: shouldAnimate ? 333 : 0,
Expand All @@ -314,6 +318,8 @@ function getGraphUpdater({

// Update Graph Store
useGraphStore.setState({ layout, elements });

//
} catch (e) {
errorCatcher.current.destroy();
errorCatcher.current = cytoscape();
Expand All @@ -330,17 +336,18 @@ function getStyleUpdater({
cy,
errorCatcher,
bg,
userStyle = [],
}: {
cy: React.MutableRefObject<cytoscape.Core | undefined>;
errorCatcher: React.MutableRefObject<cytoscape.Core | undefined>;
bg?: string;
userStyle: cytoscape.StylesheetStyle[];
}) {
return throttle((theme: Theme) => {
if (!cy.current) return;
if (!errorCatcher.current) return;
try {
// Prepare Styles
const style = buildStylesForGraph(theme, getUserStyle(), bg);
const style = buildStylesForGraph(theme, userStyle, bg);

// Test Error First
errorCatcher.current.json({ style });
Expand Down
Loading