Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
75 changes: 74 additions & 1 deletion src/global-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import {
githubUserSchema,
templateSchema,
} from "./schema"
import {
Config,
CONFIG_FILE_PATH,
DEFAULT_CONFIG,
getNoteIdFromFilepath,
normalizeDirectoryPath,
parseConfigFromJson,
serializeConfig,
} from "./utils/config"
import { fs, fsWipe } from "./utils/fs"
import {
REPO_DIR,
Expand All @@ -38,6 +47,7 @@ import { startTimer } from "./utils/timer"

const GITHUB_USER_STORAGE_KEY = "github_user" as const
const MARKDOWN_FILES_STORAGE_KEY = "markdown_files" as const
const CONFIG_STORAGE_KEY = "lumen_config" as const

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This key should be repo specific because eventually we might want to have multiple repos cloned at once


type Context = {
githubUser: GitHubUser | null
Expand Down Expand Up @@ -626,6 +636,67 @@ export const isSignedOutAtom = selectAtom(globalStateMachineAtom, (state) =>
state.matches("signedOut"),
)

// -----------------------------------------------------------------------------
// Config
// -----------------------------------------------------------------------------

/** Get cached config from localStorage */
function getConfigFromLocalStorage(): Config {
try {
const stored = localStorage.getItem(CONFIG_STORAGE_KEY)
if (stored) {
return parseConfigFromJson(stored)
}
} catch {
// Ignore errors
}
return DEFAULT_CONFIG
}

/** Save config to localStorage */
function setConfigToLocalStorage(config: Config) {
localStorage.setItem(CONFIG_STORAGE_KEY, serializeConfig(config))
}

/** Primitive atom to hold the config state */
const configPrimitiveAtom = atom<Config>(getConfigFromLocalStorage())

/** Read-only atom for consuming the config */
export const configAtom = atom((get) => get(configPrimitiveAtom))

/** Writable atom for updating the config */
export const setConfigAtom = atom(null, (get, set, config: Config) => {
set(configPrimitiveAtom, config)
setConfigToLocalStorage(config)
})

/** Helper atom for the calendar notes directory (normalized) */
export const calendarNotesDirectoryAtom = atom((get) => {
const config = get(configAtom)
return normalizeDirectoryPath(config.calendarNotesDirectory)
})

/** Function to read config from filesystem and update the atom */
export async function loadConfigFromFs(): Promise<Config> {
try {
const configPath = `${REPO_DIR}/${CONFIG_FILE_PATH}`
const content = await fs.promises.readFile(configPath, "utf8")
// fs.promises.readFile can return string or Uint8Array
const contentStr = typeof content === "string" ? content : new TextDecoder().decode(content)
return parseConfigFromJson(contentStr)
} catch {
// Config file doesn't exist, return default
return DEFAULT_CONFIG
}
}

/** Atom to trigger config loading from filesystem */
export const loadConfigAtom = atom(null, async (get, set) => {
const config = await loadConfigFromFs()
set(configPrimitiveAtom, config)
setConfigToLocalStorage(config)
})

// -----------------------------------------------------------------------------
// GitHub
// -----------------------------------------------------------------------------
Expand All @@ -646,11 +717,13 @@ export const githubRepoAtom = selectAtom(

export const notesAtom = atom((get) => {
const markdownFiles = get(markdownFilesAtom)
const calendarNotesDir = get(calendarNotesDirectoryAtom)
const notes: Map<NoteId, Note> = new Map()

// Parse notes
for (const filepath in markdownFiles) {
const id = filepath.replace(/\.md$/, "")
// Derive note ID, considering the calendar notes directory
const id = getNoteIdFromFilepath(filepath, calendarNotesDir)
const content = markdownFiles[filepath]
notes.set(id, parseNote(id, content))
}
Expand Down
59 changes: 59 additions & 0 deletions src/hooks/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useAtomValue, useSetAtom } from "jotai"
import React from "react"
import {
calendarNotesDirectoryAtom,
configAtom,
globalStateMachineAtom,
isRepoClonedAtom,
loadConfigAtom,
setConfigAtom,
} from "../global-state"
import { Config, CONFIG_FILE_PATH, normalizeDirectoryPath, serializeConfig } from "../utils/config"

export function useConfig() {
return useAtomValue(configAtom)
}

export function useCalendarNotesDirectory() {
return useAtomValue(calendarNotesDirectoryAtom)
}

/** Load config from filesystem when repo is cloned */
export function useLoadConfigOnMount() {
const isRepoCloned = useAtomValue(isRepoClonedAtom)
const loadConfig = useSetAtom(loadConfigAtom)
const hasLoadedRef = React.useRef(false)

React.useEffect(() => {
if (isRepoCloned && !hasLoadedRef.current) {
hasLoadedRef.current = true
loadConfig()
}
}, [isRepoCloned, loadConfig])
}

export function useSaveConfig() {
const send = useSetAtom(globalStateMachineAtom)
const setConfig = useSetAtom(setConfigAtom)

return React.useCallback(
(config: Config) => {
// Normalize the config before saving
const normalizedConfig: Config = {
...config,
calendarNotesDirectory: normalizeDirectoryPath(config.calendarNotesDirectory) || undefined,
}

// Update the config atom (and localStorage)
setConfig(normalizedConfig)

// Write the config file to the repo
send({
type: "WRITE_FILES",
markdownFiles: { [CONFIG_FILE_PATH]: serializeConfig(normalizedConfig) },
commitMessage: "Update Lumen config",
})
},
[send, setConfig],
)
}
38 changes: 27 additions & 11 deletions src/hooks/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
import React from "react"
import {
backlinksIndexAtom,
calendarNotesDirectoryAtom,
githubRepoAtom,
githubUserAtom,
globalStateMachineAtom,
markdownFilesAtom,
notesAtom,
} from "../global-state"
import { Note, NoteId } from "../schema"
import { getNoteFilepath, getNoteIdFromFilepath } from "../utils/config"

Check failure on line 14 in src/hooks/note.ts

View workflow job for this annotation

GitHub Actions / lint

'getNoteIdFromFilepath' is defined but never used
import { parseFrontmatter, updateFrontmatterValue } from "../utils/frontmatter"
import { deleteGist, updateGist } from "../utils/gist"
import { isValidNoteId } from "../utils/note-id"
import { parseNote } from "../utils/parse-note"
import { updateWikilinks } from "../utils/update-wikilinks"
import { isValidNoteId } from "../utils/note-id"

const EMPTY_BACKLINKS: NoteId[] = []

Expand Down Expand Up @@ -54,6 +56,7 @@
const send = useSetAtom(globalStateMachineAtom)
const githubUser = useAtomValue(githubUserAtom)
const githubRepo = useAtomValue(githubRepoAtom)
const calendarNotesDir = useAtomValue(calendarNotesDirectoryAtom)

const saveNote = React.useCallback(
async ({ id, content }: Pick<Note, "id" | "content">) => {
Expand All @@ -63,9 +66,12 @@
properties: { updated_at: new Date() },
})

// Determine the correct filepath based on whether this is a calendar note
const filepath = getNoteFilepath(id, calendarNotesDir)

send({
type: "WRITE_FILES",
markdownFiles: { [`${id}.md`]: contentWithTimestamp },
markdownFiles: { [filepath]: contentWithTimestamp },
})

// If the note has a gist ID, update the gist
Expand All @@ -79,7 +85,7 @@
})
}
},
[send, githubUser, githubRepo],
[send, githubUser, githubRepo, calendarNotesDir],
)

return saveNote
Expand All @@ -91,15 +97,19 @@

export function useRenameNote() {
const getMarkdownFiles = useAtomCallback(React.useCallback((get) => get(markdownFilesAtom), []))
const getCalendarNotesDir = useAtomCallback(
React.useCallback((get) => get(calendarNotesDirectoryAtom), []),
)
const send = useSetAtom(globalStateMachineAtom)

return React.useCallback(
(params: { oldName: string; newName: string; content: string }): RenameNoteResult => {
const { oldName, newName, content } = params
const calendarNotesDir = getCalendarNotesDir()

const markdownFiles = getMarkdownFiles()
const oldFilepath = `${oldName}.md`
const newFilepath = `${newName}.md`
const oldFilepath = getNoteFilepath(oldName, calendarNotesDir)
const newFilepath = getNoteFilepath(newName, calendarNotesDir)

// Guard against no-op renames
if (!oldName || !newName || oldName === newName) {
Expand All @@ -120,10 +130,14 @@
const updatedMarkdownFiles: Record<string, string | null> = {}

// Update wikilinks in all other notes
for (const [filepath, content] of Object.entries(markdownFiles)) {
for (const [filepath, fileContent] of Object.entries(markdownFiles)) {
if (filepath === oldFilepath) continue
const newContent = updateWikilinks({ fileContent: content, oldId: oldName, newId: newName })
if (newContent !== content) {
const newContent = updateWikilinks({
fileContent,
oldId: oldName,
newId: newName,
})
if (newContent !== fileContent) {
updatedMarkdownFiles[filepath] = newContent
}
}
Expand All @@ -148,13 +162,14 @@

return { success: true }
},
[getMarkdownFiles, send],
[getMarkdownFiles, getCalendarNotesDir, send],
)
}

export function useDeleteNote() {
const send = useSetAtom(globalStateMachineAtom)
const githubUser = useAtomValue(githubUserAtom)
const calendarNotesDir = useAtomValue(calendarNotesDirectoryAtom)
const getNoteById = useAtomCallback(
React.useCallback((get, set, id: NoteId) => {
const notes = get(notesAtom)
Expand All @@ -173,9 +188,10 @@
})
}

send({ type: "DELETE_FILE", filepath: `${id}.md` })
const filepath = getNoteFilepath(id, calendarNotesDir)
send({ type: "DELETE_FILE", filepath })
},
[send, githubUser, getNoteById],
[send, githubUser, calendarNotesDir, getNoteById],
)

return deleteNote
Expand Down
Loading
Loading