import React, { createContext } from "react"
import flatted, { parse, stringify } from "flatted"
import { Subject } from "rxjs"
import { sanitise } from "~/functions/sanitise"
import { smarts } from "~/smarts"
export interface ThingtimeContextInterface {
thingtime: any
setThingtime: any
getThingtime: any
thingtimeRef: any
loading: boolean
events: Subject<any>
}
export const ThingtimeContext = createContext<ThingtimeContextInterface | null>(
null
)
try {
window.smarts = smarts
window.flatted = {
parse,
stringify,
}
} catch (err) {
// nothing
}
const force = {
settings: {
types: {
javascript: {
any: {
type: "any",
value: () => {
return null
},
},
object: {
type: "object",
value: () => {
return {}
},
},
array: {
type: "array",
value: () => {
return []
},
},
string: {
type: "string",
value: () => {
return ""
},
},
number: {
type: "number",
value: () => {
return 0
},
},
boolean: {
type: "boolean",
value: () => {
return false
},
},
function: {
type: "function",
value: () => {
return () => {}
},
},
},
custom: {
"Thingtime Logo": {
type: "chakra",
value: {
type: "chakra",
chakra: "Box",
props: {
fontSize: 12,
},
rawChildren: ["🌈 Thingtime"],
},
},
"Violet Container Centered": {
name: "Violet Container Centered",
type: "chakra",
icon: "💜",
wrap: "children",
value: {
name: "Violet Container Centered",
type: "chakra",
chakra: "Center",
props: {
bg: "#AB47BC",
padding: 4,
borderRadius: 12,
},
children: [],
},
},
"Indigo Container Centered": {
name: "Indigo Container Centered",
type: "chakra",
icon: "🩷",
wrap: "children",
value: {
name: "Indigo Container Centered",
type: "chakra",
chakra: "Center",
props: {
bg: "#5C6BC0",
padding: 4,
borderRadius: 12,
},
children: [],
},
},
"Blue Container Centered": {
name: "Blue Container Centered",
type: "chakra",
icon: "💙",
wrap: "children",
value: {
name: "Blue Container Centered",
type: "chakra",
chakra: "Center",
props: {
bg: "#42A5F5",
padding: 4,
borderRadius: 12,
},
children: [],
},
},
"Green Container Centered": {
name: "Green Container Centered",
type: "chakra",
icon: "💚",
wrap: "children",
value: {
name: "Green Container Centered",
type: "chakra",
chakra: "Center",
props: {
bg: "#66BB6A",
padding: 4,
borderRadius: 12,
},
children: [],
},
},
"Yellow Container Centered": {
name: "Yellow Container Centered",
type: "chakra",
icon: "💛",
wrap: "children",
value: {
name: "Yellow Container Centered",
type: "chakra",
chakra: "Center",
props: {
bg: "#FFEE58",
padding: 4,
borderRadius: 12,
},
children: [],
},
},
"Orange Container Centered": {
name: "Orange Container Centered",
type: "chakra",
icon: "🧡",
wrap: "children",
value: {
name: "Orange Container Centered",
type: "chakra",
chakra: "Center",
props: {
bg: "#FF7043",
padding: 4,
borderRadius: 12,
},
children: [],
},
},
"Red Container Centered": {
name: "Red Container Centered",
type: "chakra",
icon: "❤️",
wrap: "children",
value: {
name: "Red Container Centered",
type: "chakra",
chakra: "Center",
props: {
bg: "#C62828",
padding: 4,
borderRadius: 12,
},
children: [],
},
},
"Left Aligned": {
type: "chakra",
value: {
type: "chakra",
chakra: "Flex",
props: {
mr: "auto",
},
children: [],
},
},
"Right Aligned": {
type: "chakra",
value: {
type: "chakra",
chakra: "Flex",
props: {
ml: "auto",
},
children: [],
},
},
},
},
// 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)
// 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 [events, setEvents] = React.useState(null)
if (!events) {
setEvents(() => new Subject())
}
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
if (undoHistory[undoHistory.length - 1]?.value !== stringified) {
try {
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
}
}
} 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 (redoHistory[redoHistory.length - 1]?.value !== stringified) {
try {
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
}
}
} 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.getThingtime = getThingtime
window.thingtime = thingtimeReference
window.tt = thingtimeReference
window.events = events
} 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, events, getThingtime, thingtimeReference, set])
const value = {
thingtime: thingtimeReference,
setThingtime,
getThingtime,
thingtimeRef,
paths,
loading,
events,
}
return (
<ThingtimeContext.Provider value={value}>
{props?.children}
</ThingtimeContext.Provider>
)
}