updated note fetching logic, README, test and minor UI change. Also deleted /Diagrams
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { useAuthContext } from "@/hooks/use-auth-context"
|
||||
import { supabase } from "@/libs/supabase"
|
||||
import { deleteNoteImage, NoteImageUploadProgress, uploadNoteImage } from "@/src/notes/note-image-storage"
|
||||
import { StagedNoteImage } from "@/src/notes/image-utils"
|
||||
|
||||
const NOTES_PAGE_SIZE = 5
|
||||
|
||||
export type NoteListKey = "my-notes" | "work-notes"
|
||||
|
||||
type NoteRow = {
|
||||
id: number
|
||||
created_by: string
|
||||
@@ -47,8 +51,13 @@ export type NoteImageChange =
|
||||
type NotesContextValue = {
|
||||
notes: Note[]
|
||||
isLoading: boolean
|
||||
isLoadingMoreMyNotes: boolean
|
||||
isLoadingMoreWorkNotes: boolean
|
||||
errorMessage: string | null
|
||||
refreshNotes: () => Promise<void>
|
||||
loadMoreNotes: (listKey: NoteListKey) => Promise<void>
|
||||
hasMoreMyNotes: boolean
|
||||
hasMoreWorkNotes: boolean
|
||||
fetchNoteById: (noteId: string) => Promise<Note | null>
|
||||
addNote: (
|
||||
title: string,
|
||||
@@ -83,9 +92,18 @@ function normalizeImageSizeBytes(value: number | string | null | undefined) {
|
||||
|
||||
export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
const { claims, isLoggedIn, profile } = useAuthContext()
|
||||
const [notes, setNotes] = useState<Note[]>([])
|
||||
const [pagedNotes, setPagedNotes] = useState<Note[]>([])
|
||||
const [fetchedNotes, setFetchedNotes] = useState<Note[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isLoadingMoreMyNotes, setIsLoadingMoreMyNotes] = useState(false)
|
||||
const [isLoadingMoreWorkNotes, setIsLoadingMoreWorkNotes] = useState(false)
|
||||
const [myNotesPage, setMyNotesPage] = useState(0)
|
||||
const [workNotesPage, setWorkNotesPage] = useState(0)
|
||||
const [hasMoreMyNotes, setHasMoreMyNotes] = useState(true)
|
||||
const [hasMoreWorkNotes, setHasMoreWorkNotes] = useState(true)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const myNotesPageRef = useRef(0)
|
||||
const workNotesPageRef = useRef(0)
|
||||
|
||||
const userId = claims?.sub as string | undefined
|
||||
const creatorLabel =
|
||||
@@ -138,9 +156,72 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
imageSizeBytes: normalizeImageSizeBytes(row.image_size_bytes),
|
||||
}), [creatorLabel, userId])
|
||||
|
||||
const loadNotes = useCallback(async () => {
|
||||
const mergeNotes = useCallback((existingNotes: Note[], incomingNotes: Note[]) => {
|
||||
const notesById = new Map<string, Note>()
|
||||
|
||||
for (const note of existingNotes) {
|
||||
notesById.set(note.id, note)
|
||||
}
|
||||
|
||||
for (const note of incomingNotes) {
|
||||
notesById.set(note.id, note)
|
||||
}
|
||||
|
||||
return Array.from(notesById.values()).sort((left, right) => {
|
||||
const leftTime = new Date(left.lastChangedAt).getTime()
|
||||
const rightTime = new Date(right.lastChangedAt).getTime()
|
||||
|
||||
return rightTime - leftTime
|
||||
})
|
||||
}, [])
|
||||
|
||||
const notes = useMemo(() => mergeNotes(pagedNotes, fetchedNotes), [fetchedNotes, mergeNotes, pagedNotes])
|
||||
|
||||
const fetchNotesPage = useCallback(async (listKey: NoteListKey, page: number, pageSize = NOTES_PAGE_SIZE) => {
|
||||
if (!userId) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rangeStart = page * NOTES_PAGE_SIZE
|
||||
const rangeEnd = rangeStart + pageSize - 1
|
||||
|
||||
let query = supabase
|
||||
.from("Notes")
|
||||
.select(
|
||||
"id, created_by, title, content, created_at, updated_at, image_url, image_path, image_mime_type, image_size_bytes"
|
||||
)
|
||||
.order("updated_at", { ascending: false, nullsFirst: false })
|
||||
.order("created_at", { ascending: false })
|
||||
|
||||
query =
|
||||
listKey === "my-notes"
|
||||
? query.eq("created_by", userId)
|
||||
: query.neq("created_by", userId)
|
||||
|
||||
const { data, error } = await query.range(rangeStart, rangeEnd)
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
const rows = (data ?? []) as NoteRow[]
|
||||
const labels = await buildCreatorLabels(rows)
|
||||
|
||||
return rows.map((row) => mapNoteRow(row, labels))
|
||||
}, [buildCreatorLabels, mapNoteRow, userId])
|
||||
|
||||
const loadNotes = useCallback(async (preserveLoadedPages = false) => {
|
||||
if (!isLoggedIn) {
|
||||
setNotes([])
|
||||
setPagedNotes([])
|
||||
setFetchedNotes([])
|
||||
setIsLoadingMoreMyNotes(false)
|
||||
setIsLoadingMoreWorkNotes(false)
|
||||
setMyNotesPage(0)
|
||||
setWorkNotesPage(0)
|
||||
myNotesPageRef.current = 0
|
||||
workNotesPageRef.current = 0
|
||||
setHasMoreMyNotes(true)
|
||||
setHasMoreWorkNotes(true)
|
||||
setErrorMessage(null)
|
||||
setIsLoading(false)
|
||||
return
|
||||
@@ -149,33 +230,97 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
setIsLoading(true)
|
||||
setErrorMessage(null)
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("Notes")
|
||||
.select(
|
||||
"id, created_by, title, content, created_at, updated_at, image_url, image_path, image_mime_type, image_size_bytes"
|
||||
)
|
||||
.order("updated_at", { ascending: false, nullsFirst: false })
|
||||
.order("created_at", { ascending: false })
|
||||
try {
|
||||
const myPageCount = preserveLoadedPages ? Math.max(1, myNotesPageRef.current) : 1
|
||||
const workPageCount = preserveLoadedPages ? Math.max(1, workNotesPageRef.current) : 1
|
||||
|
||||
if (error) {
|
||||
setErrorMessage(error.message)
|
||||
setNotes([])
|
||||
const [myNotes, workNotes] = await Promise.all([
|
||||
fetchNotesPage("my-notes", 0, myPageCount * NOTES_PAGE_SIZE),
|
||||
fetchNotesPage("work-notes", 0, workPageCount * NOTES_PAGE_SIZE),
|
||||
])
|
||||
|
||||
setPagedNotes(mergeNotes(myNotes, workNotes))
|
||||
setMyNotesPage(myPageCount)
|
||||
setWorkNotesPage(workPageCount)
|
||||
myNotesPageRef.current = myPageCount
|
||||
workNotesPageRef.current = workPageCount
|
||||
setHasMoreMyNotes(myNotes.length === myPageCount * NOTES_PAGE_SIZE)
|
||||
setHasMoreWorkNotes(workNotes.length === workPageCount * NOTES_PAGE_SIZE)
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : "Failed to load notes.")
|
||||
setPagedNotes([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [fetchNotesPage, isLoggedIn, mergeNotes])
|
||||
|
||||
const refreshNotes = async () => {
|
||||
await loadNotes(true)
|
||||
}
|
||||
|
||||
const loadMoreNotes = useCallback(async (listKey: NoteListKey) => {
|
||||
if (!isLoggedIn || !userId) {
|
||||
return
|
||||
}
|
||||
|
||||
const rows = (data ?? []) as NoteRow[]
|
||||
const labels = await buildCreatorLabels(rows)
|
||||
const isMyNotes = listKey === "my-notes"
|
||||
const nextPage = isMyNotes ? myNotesPage : workNotesPage
|
||||
const hasMoreNotes = isMyNotes ? hasMoreMyNotes : hasMoreWorkNotes
|
||||
const isAlreadyLoading = isMyNotes ? isLoadingMoreMyNotes : isLoadingMoreWorkNotes
|
||||
|
||||
setNotes(
|
||||
rows.map((row) => mapNoteRow(row, labels))
|
||||
)
|
||||
setIsLoading(false)
|
||||
}, [buildCreatorLabels, isLoggedIn, mapNoteRow])
|
||||
if (!hasMoreNotes || isAlreadyLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
const refreshNotes = async () => {
|
||||
await loadNotes()
|
||||
}
|
||||
setErrorMessage(null)
|
||||
|
||||
if (isMyNotes) {
|
||||
setIsLoadingMoreMyNotes(true)
|
||||
} else {
|
||||
setIsLoadingMoreWorkNotes(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const nextNotes = await fetchNotesPage(listKey, nextPage)
|
||||
|
||||
setPagedNotes((prev) => mergeNotes(prev, nextNotes))
|
||||
|
||||
if (isMyNotes) {
|
||||
setMyNotesPage((prev) => {
|
||||
const nextValue = prev + 1
|
||||
myNotesPageRef.current = nextValue
|
||||
return nextValue
|
||||
})
|
||||
setHasMoreMyNotes(nextNotes.length === NOTES_PAGE_SIZE)
|
||||
} else {
|
||||
setWorkNotesPage((prev) => {
|
||||
const nextValue = prev + 1
|
||||
workNotesPageRef.current = nextValue
|
||||
return nextValue
|
||||
})
|
||||
setHasMoreWorkNotes(nextNotes.length === NOTES_PAGE_SIZE)
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : "Failed to load more notes.")
|
||||
} finally {
|
||||
if (isMyNotes) {
|
||||
setIsLoadingMoreMyNotes(false)
|
||||
} else {
|
||||
setIsLoadingMoreWorkNotes(false)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
fetchNotesPage,
|
||||
hasMoreMyNotes,
|
||||
hasMoreWorkNotes,
|
||||
isLoadingMoreMyNotes,
|
||||
isLoadingMoreWorkNotes,
|
||||
isLoggedIn,
|
||||
mergeNotes,
|
||||
myNotesPage,
|
||||
userId,
|
||||
workNotesPage,
|
||||
])
|
||||
|
||||
const fetchNoteById = useCallback(async (noteId: string) => {
|
||||
if (!isLoggedIn || !noteId) {
|
||||
@@ -205,17 +350,15 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
const labels = await buildCreatorLabels([row])
|
||||
const fetchedNote = mapNoteRow(row, labels)
|
||||
|
||||
setNotes((prev) => {
|
||||
const nextNotes = prev.filter((existingNote) => existingNote.id !== fetchedNote.id)
|
||||
return [fetchedNote, ...nextNotes]
|
||||
})
|
||||
setFetchedNotes((prev) => mergeNotes(prev, [fetchedNote]))
|
||||
|
||||
return fetchedNote
|
||||
}, [buildCreatorLabels, isLoggedIn, mapNoteRow])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn || !userId) {
|
||||
setNotes([])
|
||||
setPagedNotes([])
|
||||
setFetchedNotes([])
|
||||
setErrorMessage(null)
|
||||
setIsLoading(false)
|
||||
return
|
||||
@@ -230,7 +373,7 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
void loadNotes()
|
||||
void loadNotes(true)
|
||||
}, 30000)
|
||||
|
||||
return () => {
|
||||
@@ -400,7 +543,23 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
setNotes((prev) =>
|
||||
setPagedNotes((prev) =>
|
||||
prev.map((note) =>
|
||||
note.id === noteId
|
||||
? {
|
||||
...note,
|
||||
title: data.title ?? trimmedTitle,
|
||||
content: data.content ?? trimmedContent,
|
||||
lastChangedAt: data.updated_at ?? updates.updated_at ?? new Date().toISOString(),
|
||||
imageUrl: data.image_url ?? null,
|
||||
imagePath: data.image_path ?? null,
|
||||
imageMimeType: data.image_mime_type ?? null,
|
||||
imageSizeBytes: normalizeImageSizeBytes(data.image_size_bytes),
|
||||
}
|
||||
: note
|
||||
)
|
||||
)
|
||||
setFetchedNotes((prev) =>
|
||||
prev.map((note) =>
|
||||
note.id === noteId
|
||||
? {
|
||||
@@ -461,7 +620,8 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}
|
||||
|
||||
setNotes((prev) => prev.filter((note) => note.id !== noteId))
|
||||
setPagedNotes((prev) => prev.filter((note) => note.id !== noteId))
|
||||
setFetchedNotes((prev) => prev.filter((note) => note.id !== noteId))
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -470,8 +630,13 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||
value={{
|
||||
notes,
|
||||
isLoading,
|
||||
isLoadingMoreMyNotes,
|
||||
isLoadingMoreWorkNotes,
|
||||
errorMessage,
|
||||
refreshNotes,
|
||||
loadMoreNotes,
|
||||
hasMoreMyNotes,
|
||||
hasMoreWorkNotes,
|
||||
fetchNoteById,
|
||||
addNote,
|
||||
updateNote,
|
||||
|
||||
@@ -326,6 +326,77 @@ export const homeScreenStyles = StyleSheet.create({
|
||||
elevation: 8,
|
||||
},
|
||||
fabText: { fontSize: 28, lineHeight: 28, fontWeight: "700" },
|
||||
loadMoreHint: {
|
||||
position: "absolute",
|
||||
left: 16,
|
||||
right: 16,
|
||||
alignItems: "center",
|
||||
},
|
||||
loadMoreHintCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
width: "100%",
|
||||
borderWidth: 1,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.12,
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowRadius: 12,
|
||||
elevation: 6,
|
||||
},
|
||||
loadMoreHintCardCompact: {
|
||||
width: undefined,
|
||||
minWidth: 0,
|
||||
maxWidth: 220,
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
},
|
||||
loadMoreHintIconCard: {
|
||||
width: 64,
|
||||
minWidth: 64,
|
||||
maxWidth: 64,
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
loadMoreHintArrowOnly: {
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
alignSelf: "center",
|
||||
},
|
||||
loadMoreHintTextBlock: {
|
||||
flex: 1,
|
||||
gap: 4,
|
||||
},
|
||||
loadMoreHintTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: "700",
|
||||
},
|
||||
loadMoreHintSubtitle: {
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
},
|
||||
loadMoreHintGlyphColumn: {
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
width: 30,
|
||||
},
|
||||
loadMoreHintStem: {
|
||||
width: 2,
|
||||
borderRadius: 999,
|
||||
marginBottom: 4,
|
||||
},
|
||||
loadMoreHintGlyph: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
})
|
||||
|
||||
export const noteImagePanelStyles = StyleSheet.create({
|
||||
|
||||
Reference in New Issue
Block a user