import React, { createContext } from "react" import flatted, { parse, stringify } from "flatted" import { sanitise } from "~/functions/sanitise" import { smarts } from "~/smarts" export interface ThingtimeContextInterface { thingtime: any setThingtime: any getThingtime: any thingtimeRef: any loading: boolean } export const ThingtimeContext = createContext( null ) try { window.smarts = smarts window.flatted = { parse, stringify, } } catch (err) { // nothing } const force = { settings: { undoLimit: 999, // commander: { // nav: { // commanderActive: false, // clearCommanderOnToggle: true, // clearCommanderContextOnToggle: true, // hideSuggestionsOnToggle: true, // }, // }, }, // Content: { // hidden1: "Edit this to your heart's desire.", // "How?": "Just search for Content and edit the value to whatever you want.", // "Example:": `Content = New Content! // Content.Nested Content = New Nested Content! // `, // }, version: 24, } const newVersionData = { Content: { hidden1: "Edit this to your heart's desire.", "How?": "Just search for Content and edit the value to whatever you want.", "Example:": `Content = New Content! Content.Nested Content = New Nested Content! `, }, } const initialValues = { settings: { commander: { nav: { commanderActive: false, clearCommanderOnToggle: true, clearCommanderContextOnToggle: true, hideSuggestionsOnToggle: true, }, }, }, Content: { hidden1: "Edit this to your heart's desire.", "How?": "Just search for Content and edit the value to whatever you want.", "Example:": ` Content = New Content! Content.Nested Content = New Nested Content! `, }, } // const initialThingtime = createInitialThingtime() const initialThingtime = smarts.merge(initialValues, force) // TODO: Make localStorage be loaded first before initialValues if local version exists // and is valid // Issue seems to be server id is different to client hydration // let thingtimeToUse = initialThingtime // try { // const thingtimeFromLocalStorage = window.localStorage.getItem("thingtime") // if (thingtimeFromLocalStorage) { // const parsed = parse(thingtimeFromLocalStorage) // if (parsed) { // const localIsValid = !parsed.version || parsed.version >= force.version // if (localIsValid) { // const newThingtime = smarts.merge(force, parsed) // thingtimeToUse = newThingtime // } else { // const withVersionUpdates = smarts.merge(newVersionData, parsed) // const newThingtime = smarts.merge(force, withVersionUpdates) // thingtimeToUse = newThingtime // } // } // } // } catch (err) { // console.error("Caught error restoring thingtime from localstorage", err) // } // initialise thingtime initialThingtime.thingtime = initialThingtime initialThingtime.tt = initialThingtime export const ThingtimeProvider = (props: any): JSX.Element => { const [thingtimeReference, rawSet] = React.useState() const thingtimeRef = React.useRef(thingtimeReference) const stateRef = React.useRef({ c: 1, }) const [loading, setLoading] = React.useState(true) const set = React.useCallback((newThingtime, ignoreUndoRedo?: any) => { const newThingtimeReference = { ...newThingtime, } newThingtimeReference.tt = newThingtimeReference newThingtimeReference.thingtime = newThingtimeReference // store undo/redo history if (!ignoreUndoRedo) { try { console.log( "ThingtimeProvider setting thingtime to localStorage", newThingtimeReference ) // setTimeout(() => { const stringified = stringify(newThingtimeReference) let undoHistory = [] try { const undoHistoryString = window.localStorage.getItem("undoHistory") const parsedUndoHistory = JSON.parse(undoHistoryString) if (parsedUndoHistory instanceof Array) { undoHistory = parsedUndoHistory } } catch { // nothing } // if last undoHistory does not equal new undo history // console.log( // "ThingtimeProvider saving to undo history undoHistory[undoHistory.length - 1]?.value", // undoHistory[undoHistory.length - 1]?.value // ) // console.log( // "ThingtimeProvider saving to undo history stringified", // stringified // ) if (undoHistory[undoHistory.length - 1]?.value !== stringified) { try { // console.log( // "ThingtimeProvider saving to undo history undoHistory", // undoHistory // ) const limit = newThingtimeReference?.settings?.undoLimit || 999 if (undoHistory?.length > limit) { undoHistory = undoHistory.slice(undoHistory.length - limit) } undoHistory.push({ timestamp: Date.now(), value: stringify(newThingtimeReference), }) const undoHistoryNewString = JSON.stringify(undoHistory) window.localStorage.setItem("undoHistory", undoHistoryNewString) } catch { // nothing } } // window.localStorage.setItem("thingtime", stringified) // }, 600) } catch (err) { console.error("There was an error saving thingtime to localStorage") } const saveRedo = false if (saveRedo) { try { console.log( "ThingtimeProvider setting thingtime to localStorage", newThingtimeReference ) // setTimeout(() => { const stringified = stringify(newThingtimeReference) let redoHistory = [] try { const redoHistoryString = window.localStorage.getItem("redoHistory") const parsedRedoHistory = JSON.parse(redoHistoryString) if (parsedRedoHistory instanceof Array) { redoHistory = parsedRedoHistory } } catch { // nothing } // if last redoHistory does not equal new redo history // console.log( // "ThingtimeProvider saving to redo history redoHistory[redoHistory.length - 1]?.value", // redoHistory[redoHistory.length - 1]?.value // ) // console.log( // "ThingtimeProvider saving to redo history stringified", // stringified // ) if (redoHistory[redoHistory.length - 1]?.value !== stringified) { try { // console.log( // "ThingtimeProvider saving to redo history redoHistory", // redoHistory // ) const limit = newThingtimeReference?.settings?.redoLimit || 999 if (redoHistory?.length > limit) { redoHistory = redoHistory.slice(redoHistory.length - limit) } redoHistory.push({ timestamp: Date.now(), value: stringify(newThingtimeReference), }) const redoHistoryNewString = JSON.stringify(redoHistory) window.localStorage.setItem("redoHistory", redoHistoryNewString) } catch { // nothing } } // window.localStorage.setItem("thingtime", stringified) // }, 600) } catch (err) { console.error("There was an error saving thingtime to localStorage") } } } rawSet(newThingtimeReference) }, []) const setThingtime = React.useCallback( (path, value) => { // TODO: make this a lot safer if (["thingtime", "tt"]?.includes(path)) { if (value) { set(value) return } } const newThingtime = thingtimeReference const paths = smarts.parsePropertyPath(path) // find first parent where a path is undefined // paths is array of path parts such as ["path1", "path2", "path3"] // we want to create a new reference at the first object which has an undefined part of the path // and is an object itself // so that react will detect the change and re-render // "path1" = { ...thingtime["path1"] } if path1.path2 undefined // "path1.path2" = { ...thingtime["path1"]["path2"] } if path1.path2.path3 undefined // "path1.path2.path3" = { ...thingtime["path1"]["path2"]["path3"] } // etc let done = false paths.forEach((pathPart, index) => { if (!done) { const pathParts = paths.slice(0, index + 1) const tmpPath = pathParts.join(".") const parentPath = pathParts.slice(0, -1).join(".") const valAtPath = smarts.getsmart(newThingtime, tmpPath) if (parentPath) { if (typeof valAtPath !== "object" || valAtPath === null) { const parentVal = smarts.getsmart(newThingtime, parentPath) if (typeof parentVal === "object") { const newParent = Array.isArray(parentVal) ? [...parentVal] : { ...parentVal } smarts.setsmart(newThingtime, parentPath, newParent) } done = true } } } }) // TODO: make thingtime settable newThingtime.thingtime = newThingtime newThingtime.tt = newThingtime console.log( "ThingtimeProvider setting newThingtime value at path", '"' + path + '"', "value: ", value ) smarts.setsmart(newThingtime, path, value) set(newThingtime) }, [thingtimeReference, set] ) const getThingtime = React.useCallback( (...args) => { const rawPath = args[0] const path = rawPath if (!path) { return thingtimeReference } // do we need to sanitise? // const path = sanitise(rawPath) console.log("ThingtimeProvider getting thingtime at path", path) // console.trace("Getting thingtime at path", path) return smarts.getsmart(thingtimeReference, path) }, [thingtimeReference] ) const populatePaths = React.useCallback((obj, path, paths, seen = []) => { try { Object.keys(obj).forEach((key) => { const val = obj[key] const newPath = path ? `${path}${path ? "." : ""}${key}` : key if (typeof val === "object") { paths.push(newPath) if (!seen?.includes(val)) { seen.push(val) populatePaths(val, newPath, paths, seen) } } else { paths.push(newPath) } }) } catch { // nothing } }, []) const paths = React.useMemo(() => { // const paths = ["tt", "thingtime", "."] const paths = [] // populatePaths(thingtime, commandPath) populatePaths(thingtimeReference, "", paths) return paths }, [populatePaths, thingtimeReference]) // get thingtime from localstorage React.useEffect(() => { try { const thingtimeFromLocalStorage = window.localStorage.getItem("thingtime") if (thingtimeFromLocalStorage) { const parsed = parse(thingtimeFromLocalStorage) console.log( "ThingtimeProvider thingtime restored from localstorage", parsed ) if (parsed) { const localIsValid = !parsed.version || parsed.version >= force.version let newThingtime = smarts.merge(force, initialThingtime) if (localIsValid) { newThingtime = smarts.merge(parsed, newThingtime) } else { const withVersionUpdates = smarts.merge(newVersionData, parsed) newThingtime = smarts.merge(force, withVersionUpdates) } console.log( "ThingtimeProvider restoring thingtime from localStorage", newThingtime ) set(newThingtime) } } else { set(initialThingtime) } } catch (err) { console.error("There was an error getting thingtime from localStorage") } setLoading(false) }, []) // thingtime change listener React.useEffect(() => { try { window.setThingtime = setThingtime window.thingtime = thingtimeReference window.tt = thingtimeReference } catch { // nothing } if (stateRef.current.initialized) { try { console.log( "ThingtimeProvider setting thingtime to localStorage", thingtimeReference ) // setTimeout(() => { const stringified = stringify(thingtimeReference) window.localStorage.setItem("thingtime", stringified) // }, 600) } catch (err) { console.error("There was an error saving thingtime to localStorage") } } else { stateRef.current.initialized = true } thingtimeRef.current = thingtimeReference const keyListener = (e) => { // if ctrl + z, restore thingtime from localstorage history console.log("ThingtimeProvider listened to key event e?.key", e?.key) console.log( "ThingtimeProvider listened to key event e?.ctrlKey", e?.ctrlKey ) console.log( "ThingtimeProvider listened to key event e?.shiftKey", e?.shiftKey ) console.log( "ThingtimeProvider listened to key event e?.metaKey", e?.metaKey ) if ((e?.ctrlKey || e?.metaKey) && e?.key === "z") { e?.preventDefault() console.log("ThingtimeProvider detected undo/redo request") if (e.shiftKey) { // redo console.log("ThingtimeProvider redo") const redoHistoryString = window.localStorage.getItem("redoHistory") const parsedRedoHistory = JSON.parse(redoHistoryString) if (parsedRedoHistory instanceof Array) { const last = parsedRedoHistory[parsedRedoHistory.length - 1] if (last) { const parsed = parse(last.value) if (parsed) { // remove restored state from history // const currentHistory = parsedRedoHistory.pop() parsedRedoHistory.pop() // parsedRedoHistory.push(currentHistory) const newRedoHistoryString = JSON.stringify(parsedRedoHistory) window.localStorage.setItem("redoHistory", newRedoHistoryString) // save old/current state to undo history let undoHistory = [] try { const undoHistoryString = window.localStorage.getItem("undoHistory") const parsedUndoHistory = JSON.parse(undoHistoryString) if (parsedUndoHistory instanceof Array) { undoHistory = parsedUndoHistory } } catch { // nothing } try { undoHistory.push({ timestamp: Date.now(), value: stringify(thingtimeReference), }) const undoHistoryNewString = JSON.stringify(undoHistory) window.localStorage.setItem( "undoHistory", undoHistoryNewString ) } catch { // nothing } const newThingtime = parsed set(newThingtime, true) } } } } else { // undo console.log("ThingtimeProvider undo") try { const undoHistoryString = window.localStorage.getItem("undoHistory") const parsedUndoHistory = JSON.parse(undoHistoryString) if (parsedUndoHistory instanceof Array) { const last = parsedUndoHistory[parsedUndoHistory.length - 2] if (last) { const parsed = parse(last.value) if (parsed) { // remove restored state from history const currentHistory = parsedUndoHistory.pop() parsedUndoHistory.pop() parsedUndoHistory.push(currentHistory) const newUndoHistoryString = JSON.stringify(parsedUndoHistory) window.localStorage.setItem( "undoHistory", newUndoHistoryString ) // save old/current state to redo history let redoHistory = [] try { const redoHistoryString = window.localStorage.getItem("redoHistory") const parsedRedoHistory = JSON.parse(redoHistoryString) if (parsedRedoHistory instanceof Array) { redoHistory = parsedRedoHistory } } catch { // nothing } try { const newValue = stringify(thingtimeReference) // if last history is not the same as new history redoHistory.push({ timestamp: Date.now(), value: newValue, }) const redoHistoryNewString = JSON.stringify(redoHistory) window.localStorage.setItem( "redoHistory", redoHistoryNewString ) } catch { // nothing } const newThingtime = parsed set(newThingtime, true) } } } } catch { // nothing } } } } window.addEventListener("keydown", keyListener) return () => { window.removeEventListener("keydown", keyListener) } }, [setThingtime, thingtimeReference, set]) const value = { thingtime: thingtimeReference, setThingtime, getThingtime, thingtimeRef, paths, loading, } return ( {props?.children} ) }