From abc2cc588cf6f0d3370ebf8816baa71ce8133827 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 29 Nov 2024 13:52:20 -0800 Subject: [PATCH 1/7] show loading overlay if client side not initialized --- src/components/LoadingOverlay.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/LoadingOverlay.tsx b/src/components/LoadingOverlay.tsx index 9c3a422e..bd3e6807 100644 --- a/src/components/LoadingOverlay.tsx +++ b/src/components/LoadingOverlay.tsx @@ -21,7 +21,9 @@ export const LoadingOverlay = observer(function LoadingOverlay() { const { context, role } = store; return ( {}} closeOnEsc={false} closeOnOverlayClick={false} From a8bfce91bcc56ec325bd2e9259916b8338b4ffa3 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 29 Nov 2024 13:54:44 -0800 Subject: [PATCH 2/7] fix dom validation error --- src/components/Menu/MenuBar.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Menu/MenuBar.tsx b/src/components/Menu/MenuBar.tsx index 5c9807d7..2f07a5fe 100644 --- a/src/components/Menu/MenuBar.tsx +++ b/src/components/Menu/MenuBar.tsx @@ -373,17 +373,16 @@ export const MenuBar = observer(function MenuBar() { py={0} variant="ghost" size="xs" + fontSize="xs" transition="all 0.2s" borderRadius="md" _hover={{ bg: "gray.500" }} _focus={{ boxShadow: "outline" }} > - - - + From de09338bcff2dd61c6f1499d30293aecca5ef2a9 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 29 Nov 2024 13:56:35 -0800 Subject: [PATCH 3/7] captureButton->showCaptureButton --- src/components/ExperienceThumbnail.tsx | 8 ++++---- src/components/Menu/MenuBar.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ExperienceThumbnail.tsx b/src/components/ExperienceThumbnail.tsx index 0a1d5deb..9e01ce6b 100644 --- a/src/components/ExperienceThumbnail.tsx +++ b/src/components/ExperienceThumbnail.tsx @@ -6,13 +6,13 @@ import { FaCamera } from "react-icons/fa"; export const ExperienceThumbnail = memo(function ExperienceThumbnail({ thumbnailURL, onClick, - captureButton = false, + showCaptureButton = false, }: { thumbnailURL: string; onClick?: () => void; - captureButton?: boolean; + showCaptureButton?: boolean; }) { - if (!thumbnailURL && !captureButton) return null; + if (!thumbnailURL && !showCaptureButton) return null; return ( - {!thumbnailURL && captureButton && ( + {!thumbnailURL && showCaptureButton && ( diff --git a/src/components/Menu/MenuBar.tsx b/src/components/Menu/MenuBar.tsx index 2f07a5fe..8359f58a 100644 --- a/src/components/Menu/MenuBar.tsx +++ b/src/components/Menu/MenuBar.tsx @@ -91,7 +91,7 @@ export const MenuBar = observer(function MenuBar() { (uiStore.capturingThumbnail = true))} - captureButton + showCaptureButton /> ) : ( From 7c2df594e9bf1772ad2ab2c3be13c6647d263985 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 29 Nov 2024 14:12:02 -0800 Subject: [PATCH 4/7] only allows thumbnail edits if owning experience --- src/components/Menu/MenuBar.tsx | 3 +-- src/types/ExperienceStore.ts | 2 +- src/types/Store.ts | 32 ++++++++++++++++++++++---------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/components/Menu/MenuBar.tsx b/src/components/Menu/MenuBar.tsx index 8359f58a..9a4ee24a 100644 --- a/src/components/Menu/MenuBar.tsx +++ b/src/components/Menu/MenuBar.tsx @@ -86,8 +86,7 @@ export const MenuBar = observer(function MenuBar() { - {/* TODO: disallow editing if you don't own this experience */} - {store.context === "experienceEditor" ? ( + {store.context === "experienceEditor" && store.canEditExperience ? ( (uiStore.capturingThumbnail = true))} diff --git a/src/types/ExperienceStore.ts b/src/types/ExperienceStore.ts index 99e3161c..26dfd974 100644 --- a/src/types/ExperienceStore.ts +++ b/src/types/ExperienceStore.ts @@ -38,7 +38,7 @@ export class ExperienceStore { loadEmptyExperience = () => { this.store.deserialize({ id: undefined, - user: this.store.userStore.me ?? { id: 0, username: "" }, + user: this.store.userStore.me ?? { id: -1, username: "" }, name: "untitled", song: NO_SONG, status: "inprogress", diff --git a/src/types/Store.ts b/src/types/Store.ts index 5860568c..a980debf 100644 --- a/src/types/Store.ts +++ b/src/types/Store.ts @@ -19,6 +19,7 @@ import { NO_SONG } from "@/src/types/Song"; import { Context, Role } from "@/src/types/context"; import "@/src/utils/mobx"; import { UserStore } from "@/src/types/UserStore"; +import { User } from "@/src/types/User"; export type BlockSelection = { type: "block"; block: Block }; @@ -132,6 +133,13 @@ export class Store { experienceStatus: ExperienceStatus = "inprogress"; experienceId: number | undefined = undefined; experienceThumbnailURL = ""; + experienceUser: User | undefined = undefined; + get canEditExperience() { + return ( + this.userStore.isAuthenticated && + this.experienceUser?.id === this.userStore.me?.id + ); + } get playing() { return this.audioStore.audioState !== "paused"; @@ -477,16 +485,19 @@ export class Store { } }; - serialize = (): Experience => ({ - id: this.experienceId, - name: this.experienceName, - user: this.userStore.me!, - song: this.audioStore.selectedSong, - status: this.experienceStatus, - version: this.experienceVersion, - data: { layers: this.layers.map((l) => l.serialize()) }, - thumbnailURL: this.experienceThumbnailURL, - }); + serialize = (): Experience => { + if (!this.userStore.me) throw new Error("User not authenticated"); + return { + id: this.experienceId, + name: this.experienceName, + user: this.userStore.me, + song: this.audioStore.selectedSong, + status: this.experienceStatus, + version: this.experienceVersion, + data: { layers: this.layers.map((l) => l.serialize()) }, + thumbnailURL: this.experienceThumbnailURL, + }; + }; deserialize = (experience: Experience) => { this.experienceId = experience.id; @@ -495,6 +506,7 @@ export class Store { this.experienceStatus = experience.status; this.experienceVersion = experience.version; this.experienceThumbnailURL = experience.thumbnailURL; + this.experienceUser = experience.user; this.layers = experience.data.layers.map((l: any) => Layer.deserialize(this, l), ); From 9baa0f6570bda8a7e77db8c54e7cf9cc979c4435 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 29 Nov 2024 17:38:19 -0800 Subject: [PATCH 5/7] better navigation work in progress --- .../ExperienceEditor/ExperienceEditorPage.tsx | 30 +++++++++++++++++-- src/components/LoadingOverlay.tsx | 6 ++-- src/components/Menu/OpenExperienceModal.tsx | 17 +++++------ src/types/ExperienceStore.ts | 10 +++++++ src/types/Store.ts | 30 +++++++++++++------ 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/components/ExperienceEditor/ExperienceEditorPage.tsx b/src/components/ExperienceEditor/ExperienceEditorPage.tsx index 6052042c..ca0ac425 100644 --- a/src/components/ExperienceEditor/ExperienceEditorPage.tsx +++ b/src/components/ExperienceEditor/ExperienceEditorPage.tsx @@ -1,7 +1,7 @@ import { Arrangement } from "@/src/components/Arrangement"; import { Box } from "@chakra-ui/react"; import { Display } from "@/src/components/Display"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useStore } from "@/src/types/StoreContext"; import { observer } from "mobx-react-lite"; import { KeyboardControls } from "@/src/components/KeyboardControls"; @@ -13,15 +13,39 @@ import { LoadingOverlay } from "@/src/components/LoadingOverlay"; export const ExperienceEditorPage = observer(function ExperienceEditorPage() { const store = useStore(); - const { uiStore, experienceStore } = store; + const { uiStore, experienceStore, initializationState } = store; + const { loadingExperienceName } = experienceStore; const router = useRouter(); useEffect(() => { - if (store.initializedClientSide || !router.query.experienceName) return; + if (initializationState !== "uninitialized" || !router.isReady) return; + + console.log("initial load", router.query.experienceName); store.initializeClientSide(router.query.experienceName as string); + }, [ + store, + initializationState, + experienceStore, + store.experienceName, + router.query.experienceName, + ]); + + useEffect(() => { + if ( + initializationState !== "initialized" || + !router.query.experienceName || + store.experienceName === router.query.experienceName || + loadingExperienceName === router.query.experienceName + ) + return; + + console.log("subsequent load", router.query.experienceName); + experienceStore.load(router.query.experienceName as string); }, [ store, experienceStore, + initializationState, + loadingExperienceName, store.experienceName, router.query.experienceName, ]); diff --git a/src/components/LoadingOverlay.tsx b/src/components/LoadingOverlay.tsx index bd3e6807..f078de35 100644 --- a/src/components/LoadingOverlay.tsx +++ b/src/components/LoadingOverlay.tsx @@ -18,11 +18,13 @@ const contextMatchesRole = (context: Context, role: Role) => { export const LoadingOverlay = observer(function LoadingOverlay() { const store = useStore(); - const { context, role } = store; + const { context, role, experienceStore } = store; return ( {}} closeOnEsc={false} diff --git a/src/components/Menu/OpenExperienceModal.tsx b/src/components/Menu/OpenExperienceModal.tsx index 038569dd..eef77cb3 100644 --- a/src/components/Menu/OpenExperienceModal.tsx +++ b/src/components/Menu/OpenExperienceModal.tsx @@ -17,20 +17,22 @@ import { action } from "mobx"; import { useState } from "react"; import { ExperiencesTable } from "@/src/components/ExperiencesTable/ExperiencesTable"; import { useExperiences } from "@/src/hooks/experiencesAndUsers"; +import { useRouter } from "next/router"; export const OpenExperienceModal = observer(function OpenExperienceModal() { const store = useStore(); - const { experienceStore, uiStore, userStore } = store; + const { uiStore, userStore } = store; const { username } = userStore; const [viewingAllExperiences, setViewingAllExperiences] = useState(false); - const [isLoadingNewExperience, setIsLoadingNewExperience] = useState(false); const { isPending, isError, isRefetching, experiences } = useExperiences({ username: viewingAllExperiences ? undefined : username, enabled: uiStore.showingOpenExperienceModal, }); + const router = useRouter(); + const onClose = action(() => (uiStore.showingOpenExperienceModal = false)); if (isError) return null; @@ -45,10 +47,7 @@ export const OpenExperienceModal = observer(function OpenExperienceModal() { - Open experience{" "} - {(isPending || isRefetching || isLoadingNewExperience) && ( - - )} + Open experience {(isPending || isRefetching) && } @@ -63,9 +62,9 @@ export const OpenExperienceModal = observer(function OpenExperienceModal() { { - setIsLoadingNewExperience(true); - await experienceStore.load(experience.name); - setIsLoadingNewExperience(false); + router.push(`/experience/${experience.name}`, undefined, { + shallow: true, + }); onClose(); })} /> diff --git a/src/types/ExperienceStore.ts b/src/types/ExperienceStore.ts index 26dfd974..158112d1 100644 --- a/src/types/ExperienceStore.ts +++ b/src/types/ExperienceStore.ts @@ -5,6 +5,14 @@ import { NO_SONG } from "@/src/types/Song"; import type { Store } from "@/src/types/Store"; export class ExperienceStore { + private _loadingExperienceName: string | null = null; + get loadingExperienceName() { + return this._loadingExperienceName; + } + set loadingExperienceName(value: string | null) { + this._loadingExperienceName = value; + } + constructor(readonly store: Store) { makeAutoObservable(this); } @@ -18,12 +26,14 @@ export class ExperienceStore { }; load = async (experienceName: string) => { + this.loadingExperienceName = experienceName; const experience = await trpcClient.experience.getExperience.query({ experienceName, usingLocalData: this.store.usingLocalData, }); if (!experience) this.loadEmptyExperience(); else this.loadExperience(experience); + this.loadingExperienceName = null; }; loadById = async (experienceId: number) => { diff --git a/src/types/Store.ts b/src/types/Store.ts index a980debf..7947d257 100644 --- a/src/types/Store.ts +++ b/src/types/Store.ts @@ -1,6 +1,6 @@ import { Block } from "@/src/types/Block"; import { UIStore } from "@/src/types/UIStore"; -import { makeAutoObservable } from "mobx"; +import { makeAutoObservable, runInAction } from "mobx"; import { AudioStore } from "@/src/types/AudioStore"; import { Variation } from "@/src/types/Variations/Variation"; import { ExperienceStore } from "@/src/types/ExperienceStore"; @@ -32,8 +32,11 @@ export type VariationSelection = { export type BlockOrVariation = BlockSelection | VariationSelection; +type InitializationState = "uninitialized" | "initializing" | "initialized"; + export class Store { initializedClientSide = false; + initializationState: InitializationState = "uninitialized"; audioStore = new AudioStore(this); beatMapStore = new BeatMapStore(this); @@ -122,7 +125,11 @@ export class Store { this._experienceName = value; if (this.context === "experienceEditor") { localStorage.setItem("experienceName", value); - window.history.pushState({}, "", `/experience/${value}`); + window.history.replaceState( + window.history.state, + "", + `/experience/${value}`, + ); } } @@ -149,9 +156,11 @@ export class Store { makeAutoObservable(this); } - initializeClientSide = (initialExperienceName?: string) => { - if (this.initializedClientSide) return; - this.initializedClientSide = true; + initializeClientSide = async (initialExperienceName?: string) => { + if (this.initializationState !== "uninitialized") return; + runInAction(() => { + this.initializationState = "initializing"; + }); if (process.env.NEXT_PUBLIC_ENABLE_VOICE === "true") setupVoiceCommandWebsocket(this); @@ -186,14 +195,17 @@ export class Store { if (usingLocalData && process.env.NEXT_PUBLIC_NODE_ENV !== "production") this._usingLocalData = usingLocalData === "true"; + if (this.context === "playground") this.playgroundStore.initialize(); + this.uiStore.initialize(); + this.audioStore.initialize(); + // load experience from path parameter if provided (e.g. /experience/my-experience) - if (initialExperienceName) this.experienceStore.load(initialExperienceName); + if (initialExperienceName) + await this.experienceStore.load(initialExperienceName); else if (this.context !== "playlistEditor") this.experienceStore.loadEmptyExperience(); - if (this.context === "playground") this.playgroundStore.initialize(); - this.uiStore.initialize(); - this.audioStore.initialize(); + runInAction(() => (this.initializationState = "initialized")); }; toggleSendingData = () => { From 7e7c11f78b87def282e983f9159d44bfed126cea Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 29 Nov 2024 18:24:00 -0800 Subject: [PATCH 6/7] better navigation mostly implemented --- .../ExperienceEditor/ExperienceEditorPage.tsx | 2 +- src/components/KeyboardControls.tsx | 4 +- src/components/LoginButton.tsx | 6 ++- src/components/Menu/MenuBar.tsx | 6 ++- src/components/Menu/OpenExperienceModal.tsx | 6 +-- .../PlaylistEditor/PlaylistEditor.tsx | 2 +- .../PlaylistEditor/PlaylistItem.tsx | 2 +- src/components/RoleSelector.tsx | 3 +- src/types/ExperienceStore.ts | 52 +++++++++++-------- src/types/Store.ts | 5 -- 10 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/components/ExperienceEditor/ExperienceEditorPage.tsx b/src/components/ExperienceEditor/ExperienceEditorPage.tsx index ca0ac425..bb1d72b8 100644 --- a/src/components/ExperienceEditor/ExperienceEditorPage.tsx +++ b/src/components/ExperienceEditor/ExperienceEditorPage.tsx @@ -1,7 +1,7 @@ import { Arrangement } from "@/src/components/Arrangement"; import { Box } from "@chakra-ui/react"; import { Display } from "@/src/components/Display"; -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; import { useStore } from "@/src/types/StoreContext"; import { observer } from "mobx-react-lite"; import { KeyboardControls } from "@/src/components/KeyboardControls"; diff --git a/src/components/KeyboardControls.tsx b/src/components/KeyboardControls.tsx index 08a43eae..c80e0483 100644 --- a/src/components/KeyboardControls.tsx +++ b/src/components/KeyboardControls.tsx @@ -2,6 +2,7 @@ import { useSaveExperience } from "@/src/hooks/experience"; import { useStore } from "@/src/types/StoreContext"; import { action } from "mobx"; import { observer } from "mobx-react-lite"; +import { useRouter } from "next/router"; import { useEffect } from "react"; type Props = { @@ -14,6 +15,7 @@ export const KeyboardControls = observer(function KeyboardControls({ const store = useStore(); const { uiStore, experienceStore, audioStore } = store; + const router = useRouter(); const { saveExperience } = useSaveExperience(); useEffect(() => { @@ -25,7 +27,7 @@ export const KeyboardControls = observer(function KeyboardControls({ saveExperience(); e.preventDefault(); } else if (editMode && e.key === "n" && (e.ctrlKey || e.metaKey)) { - experienceStore.loadEmptyExperience(); + experienceStore.openEmptyExperience(router); e.preventDefault(); } diff --git a/src/components/LoginButton.tsx b/src/components/LoginButton.tsx index c192fb46..081bec20 100644 --- a/src/components/LoginButton.tsx +++ b/src/components/LoginButton.tsx @@ -21,11 +21,13 @@ import { observer } from "mobx-react-lite"; import { trpc } from "@/src/utils/trpc"; import { sanitize } from "@/src/utils/sanitize"; import { CONJURER_USER } from "@/src/types/User"; +import { useRouter } from "next/router"; export const LoginButton = observer(function LoginButton() { const store = useStore(); const { experienceStore, uiStore, userStore, usingLocalData } = store; + const router = useRouter(); const [newUsername, setNewUsername] = useState(""); const { @@ -80,7 +82,7 @@ export const LoginButton = observer(function LoginButton() { width="100%" onClick={action(() => { userStore.me = user; - experienceStore.loadEmptyExperience(); + experienceStore.openEmptyExperience(router); if (store.context === "experienceEditor") { uiStore.showingOpenExperienceModal = true; } @@ -110,7 +112,7 @@ export const LoginButton = observer(function LoginButton() { username: newUsername, }); userStore.me = newUser; - experienceStore.loadEmptyExperience(); + experienceStore.openEmptyExperience(router); onClose(); })} > diff --git a/src/components/Menu/MenuBar.tsx b/src/components/Menu/MenuBar.tsx index 9a4ee24a..45bc00a5 100644 --- a/src/components/Menu/MenuBar.tsx +++ b/src/components/Menu/MenuBar.tsx @@ -33,11 +33,13 @@ import { action } from "mobx"; import { LatencyModal } from "@/src/components/LatencyModal/LatencyModal"; import { ExperienceThumbnail } from "@/src/components/ExperienceThumbnail"; import { ExperienceStatusIndicator } from "../ExperienceStatusIndicator"; +import { useRouter } from "next/router"; export const MenuBar = observer(function MenuBar() { const store = useStore(); const { audioStore, experienceStore, uiStore } = store; + const router = useRouter(); const { saveExperience } = useSaveExperience(); const { @@ -168,7 +170,9 @@ export const MenuBar = observer(function MenuBar() { } command="⌘N" - onClick={experienceStore.loadEmptyExperience} + onClick={() => + experienceStore.openEmptyExperience(router) + } > New experience diff --git a/src/components/Menu/OpenExperienceModal.tsx b/src/components/Menu/OpenExperienceModal.tsx index eef77cb3..55951497 100644 --- a/src/components/Menu/OpenExperienceModal.tsx +++ b/src/components/Menu/OpenExperienceModal.tsx @@ -21,7 +21,7 @@ import { useRouter } from "next/router"; export const OpenExperienceModal = observer(function OpenExperienceModal() { const store = useStore(); - const { uiStore, userStore } = store; + const { uiStore, userStore, experienceStore } = store; const { username } = userStore; const [viewingAllExperiences, setViewingAllExperiences] = useState(false); @@ -62,9 +62,7 @@ export const OpenExperienceModal = observer(function OpenExperienceModal() { { - router.push(`/experience/${experience.name}`, undefined, { - shallow: true, - }); + experienceStore.openExperience(router, experience.name); onClose(); })} /> diff --git a/src/components/PlaylistEditor/PlaylistEditor.tsx b/src/components/PlaylistEditor/PlaylistEditor.tsx index ffabbb9b..124cccf1 100644 --- a/src/components/PlaylistEditor/PlaylistEditor.tsx +++ b/src/components/PlaylistEditor/PlaylistEditor.tsx @@ -121,7 +121,7 @@ export const PlaylistEditor = observer(function PlaylistEditor() { leftIcon={} onClick={() => { store.role = "experienceCreator"; - router.push("/experience/untitled"); + experienceStore.openEmptyExperience(router); }} > New experience diff --git a/src/components/PlaylistEditor/PlaylistItem.tsx b/src/components/PlaylistEditor/PlaylistItem.tsx index 3b161c6d..16ef81dc 100644 --- a/src/components/PlaylistEditor/PlaylistItem.tsx +++ b/src/components/PlaylistEditor/PlaylistItem.tsx @@ -166,7 +166,7 @@ export const PlaylistItem = observer(function PlaylistItem({ icon={} onClick={action(() => { store.role = "experienceCreator"; - router.push(`/experience/${experience.name}`); + experienceStore.openExperience(router, experience.name); })} /> {editable && ( diff --git a/src/components/RoleSelector.tsx b/src/components/RoleSelector.tsx index 499eee61..49a58f19 100644 --- a/src/components/RoleSelector.tsx +++ b/src/components/RoleSelector.tsx @@ -7,6 +7,7 @@ import { useRouter } from "next/router"; export const RoleSelector = observer(function RoleSelector() { const store = useStore(); + const { experienceStore } = store; const router = useRouter(); return ( @@ -36,7 +37,7 @@ export const RoleSelector = observer(function RoleSelector() { { store.role = "experienceCreator"; - router.push(`/experience/${store.experienceName || "untitled"}`); + experienceStore.openExperience(router, store.experienceName); })} > Experience Creator diff --git a/src/types/ExperienceStore.ts b/src/types/ExperienceStore.ts index 158112d1..e9b0b904 100644 --- a/src/types/ExperienceStore.ts +++ b/src/types/ExperienceStore.ts @@ -3,6 +3,7 @@ import { trpcClient } from "@/src/utils/trpc"; import { Experience, EXPERIENCE_VERSION } from "@/src/types/Experience"; import { NO_SONG } from "@/src/types/Song"; import type { Store } from "@/src/types/Store"; +import { NextRouter } from "next/router"; export class ExperienceStore { private _loadingExperienceName: string | null = null; @@ -17,6 +18,18 @@ export class ExperienceStore { makeAutoObservable(this); } + // Open an experience by experience name + openExperience = (router: NextRouter, experienceName: string) => { + router.push(`/experience/${experienceName}`); + }; + + // Open an empty experience + openEmptyExperience = (router: NextRouter) => { + router.push("/experience/untitled"); + }; + + // This "load" method and subsequent load* methods are used internally to change experiences, and + // are not meant to be called directly. Instead use openExperience/openEmptyExperience. loadExperience = (experience: Experience) => { this.store.deserialize(experience); runInAction(() => { @@ -25,6 +38,22 @@ export class ExperienceStore { }); }; + loadEmptyExperience = () => { + this.store.deserialize({ + id: undefined, + user: this.store.userStore.me ?? { id: -1, username: "" }, + name: "untitled", + song: NO_SONG, + status: "inprogress", + version: EXPERIENCE_VERSION, + data: { layers: [{ patternBlocks: [] }, { patternBlocks: [] }] }, + thumbnailURL: "", + }); + + this.store.hasSaved = false; + this.store.experienceLastSavedAt = 0; + }; + load = async (experienceName: string) => { this.loadingExperienceName = experienceName; const experience = await trpcClient.experience.getExperience.query({ @@ -45,29 +74,6 @@ export class ExperienceStore { else this.loadExperience(experience); }; - loadEmptyExperience = () => { - this.store.deserialize({ - id: undefined, - user: this.store.userStore.me ?? { id: -1, username: "" }, - name: "untitled", - song: NO_SONG, - status: "inprogress", - version: EXPERIENCE_VERSION, - data: { layers: [{ patternBlocks: [] }, { patternBlocks: [] }] }, - thumbnailURL: "", - }); - - this.store.hasSaved = false; - this.store.experienceLastSavedAt = 0; - }; - - loadFromParams = () => { - const urlParams = new URLSearchParams(window.location.search); - const experience = urlParams.get("experience"); - if (experience) void this.load(experience); - return !!experience; - }; - stringifyExperience = (pretty: boolean = false): string => JSON.stringify( this.store.serialize(), diff --git a/src/types/Store.ts b/src/types/Store.ts index 7947d257..fcc54779 100644 --- a/src/types/Store.ts +++ b/src/types/Store.ts @@ -125,11 +125,6 @@ export class Store { this._experienceName = value; if (this.context === "experienceEditor") { localStorage.setItem("experienceName", value); - window.history.replaceState( - window.history.state, - "", - `/experience/${value}`, - ); } } From a7bad8201e6de911a0da02702ac1a8de830f3f50 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 29 Nov 2024 18:35:50 -0800 Subject: [PATCH 7/7] various navigation cleanup --- .../ExperienceEditor/ExperienceEditorPage.tsx | 8 ++++---- src/components/KeyboardControls.tsx | 10 +++++++++- src/components/PlaygroundPage.tsx | 10 ++++------ .../PlaylistEditor/PlaylistEditorPage.tsx | 4 ++-- src/types/Store.ts | 19 +++++++++++-------- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/components/ExperienceEditor/ExperienceEditorPage.tsx b/src/components/ExperienceEditor/ExperienceEditorPage.tsx index bb1d72b8..18e33e5c 100644 --- a/src/components/ExperienceEditor/ExperienceEditorPage.tsx +++ b/src/components/ExperienceEditor/ExperienceEditorPage.tsx @@ -17,19 +17,21 @@ export const ExperienceEditorPage = observer(function ExperienceEditorPage() { const { loadingExperienceName } = experienceStore; const router = useRouter(); + + // Initialize the store with the experience name from the URL useEffect(() => { if (initializationState !== "uninitialized" || !router.isReady) return; - - console.log("initial load", router.query.experienceName); store.initializeClientSide(router.query.experienceName as string); }, [ store, initializationState, experienceStore, store.experienceName, + router.isReady, router.query.experienceName, ]); + // Listen for url changes and load any new experience by name useEffect(() => { if ( initializationState !== "initialized" || @@ -38,8 +40,6 @@ export const ExperienceEditorPage = observer(function ExperienceEditorPage() { loadingExperienceName === router.query.experienceName ) return; - - console.log("subsequent load", router.query.experienceName); experienceStore.load(router.query.experienceName as string); }, [ store, diff --git a/src/components/KeyboardControls.tsx b/src/components/KeyboardControls.tsx index c80e0483..f06ca9ed 100644 --- a/src/components/KeyboardControls.tsx +++ b/src/components/KeyboardControls.tsx @@ -97,7 +97,15 @@ export const KeyboardControls = observer(function KeyboardControls({ window.removeEventListener("copy", handleCopy); window.removeEventListener("paste", handlePaste); }; - }, [store, uiStore, experienceStore, audioStore, editMode, saveExperience]); + }, [ + store, + uiStore, + experienceStore, + audioStore, + editMode, + saveExperience, + router, + ]); return null; }); diff --git a/src/components/PlaygroundPage.tsx b/src/components/PlaygroundPage.tsx index 95b5cd14..2dc44b04 100644 --- a/src/components/PlaygroundPage.tsx +++ b/src/components/PlaygroundPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; import { PatternPlayground } from "@/src/components/PatternPlayground/PatternPlayground"; import { useStore } from "@/src/types/StoreContext"; import { observer } from "mobx-react-lite"; @@ -10,15 +10,13 @@ import { LoginButton } from "@/src/components/LoginButton"; export const PlaygroundPage = observer(function PlaygroundPage() { const store = useStore(); - const initialized = useRef(false); useEffect(() => { - if (initialized.current) return; - initialized.current = true; + if (store.initializationState !== "uninitialized") return; store.initializeClientSide(); - }, [store]); + }, [store, store.initializationState]); return ( - store.initializedClientSide && ( + store.initializationState === "initialized" && ( <> { - if (store.initializedClientSide) return; + if (store.initializationState !== "uninitialized") return; store.initializeClientSide(); - }, [store]); + }, [store, store.initializationState]); return ( diff --git a/src/types/Store.ts b/src/types/Store.ts index fcc54779..291560f9 100644 --- a/src/types/Store.ts +++ b/src/types/Store.ts @@ -1,6 +1,6 @@ import { Block } from "@/src/types/Block"; import { UIStore } from "@/src/types/UIStore"; -import { makeAutoObservable, runInAction } from "mobx"; +import { makeAutoObservable } from "mobx"; import { AudioStore } from "@/src/types/AudioStore"; import { Variation } from "@/src/types/Variations/Variation"; import { ExperienceStore } from "@/src/types/ExperienceStore"; @@ -35,9 +35,6 @@ export type BlockOrVariation = BlockSelection | VariationSelection; type InitializationState = "uninitialized" | "initializing" | "initialized"; export class Store { - initializedClientSide = false; - initializationState: InitializationState = "uninitialized"; - audioStore = new AudioStore(this); beatMapStore = new BeatMapStore(this); uiStore = new UIStore(this); @@ -51,6 +48,14 @@ export class Store { sendingData = false; viewerMode = false; + private _initializationState: InitializationState = "uninitialized"; + get initializationState() { + return this._initializationState; + } + set initializationState(value: InitializationState) { + this._initializationState = value; + } + _globalIntensity = 1; get globalIntensity(): number { return this._globalIntensity; @@ -153,9 +158,7 @@ export class Store { initializeClientSide = async (initialExperienceName?: string) => { if (this.initializationState !== "uninitialized") return; - runInAction(() => { - this.initializationState = "initializing"; - }); + this.initializationState = "initializing"; if (process.env.NEXT_PUBLIC_ENABLE_VOICE === "true") setupVoiceCommandWebsocket(this); @@ -200,7 +203,7 @@ export class Store { else if (this.context !== "playlistEditor") this.experienceStore.loadEmptyExperience(); - runInAction(() => (this.initializationState = "initialized")); + this.initializationState = "initialized"; }; toggleSendingData = () => {