updated note fetching logic, README, test and minor UI change. Also deleted /Diagrams

This commit is contained in:
Christopher Sanden
2026-03-19 17:17:41 +01:00
parent 57daa96be6
commit 3fbc216a58
16 changed files with 822 additions and 284 deletions

View File

@@ -1,25 +1,45 @@
import { useMemo, useState } from "react"
import { FlatList, Pressable, Text, View } from "react-native"
import { useEffect, useMemo, useRef, useState } from "react"
import { ActivityIndicator, Animated, FlatList, NativeScrollEvent, NativeSyntheticEvent, Pressable, Text, View } from "react-native"
import { router } from "expo-router"
import { Ionicons } from "@expo/vector-icons"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { Image } from "expo-image"
import { useAuthContext } from "@/hooks/use-auth-context"
import { useNotes } from "@/src/notes/NotesContext"
import SignOutButton from '@/components/social-auth-buttons/sign-out-button'
import { type Note, useNotes } from "@/src/notes/NotesContext"
import SignOutButton from "@/components/social-auth-buttons/sign-out-button"
import { useAppTheme } from "@/src/theme/AppThemeProvider"
import { homeScreenStyles as styles } from "@/src/styles/app-styles"
type TabKey = "my-notes" | "work-notes"
export default function HomeScreen()
{
const PULL_DISTANCE = 120
const LOAD_MORE_TRIGGER_DISTANCE = 24
export default function HomeScreen() {
const { claims } = useAuthContext()
const { errorMessage, isLoading, notes } = useNotes()
const {
errorMessage,
isLoading,
notes,
loadMoreNotes,
hasMoreMyNotes,
hasMoreWorkNotes,
isLoadingMoreMyNotes,
isLoadingMoreWorkNotes,
} = useNotes()
const [activeTab, setActiveTab] = useState<TabKey>("my-notes")
const [showNoMoreNotesBubble, setShowNoMoreNotesBubble] = useState(false)
const insets = useSafeAreaInsets()
const { colorScheme, palette } = useAppTheme()
const userId = claims?.sub
const listRef = useRef<FlatList<Note>>(null)
const pullProgress = useRef(new Animated.Value(0)).current
const isLoadingMoreRequest = useRef<Record<TabKey, boolean>>({
"my-notes": false,
"work-notes": false,
})
const shouldLoadMoreOnRelease = useRef(false)
const hideNoMoreNotesTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const filteredNotes = useMemo(
() =>
@@ -29,6 +49,10 @@ export default function HomeScreen()
[activeTab, notes, userId]
)
const activeHasMore = activeTab === "my-notes" ? hasMoreMyNotes : hasMoreWorkNotes
const activeIsLoadingMore =
activeTab === "my-notes" ? isLoadingMoreMyNotes : isLoadingMoreWorkNotes
const emptyText =
activeTab === "my-notes"
? "No personal notes yet. Create your first note."
@@ -44,9 +68,102 @@ export default function HomeScreen()
return parsed.toLocaleString()
}
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent
const distanceFromBottom =
contentSize.height - (contentOffset.y + layoutMeasurement.height)
const progress = Math.max(0, Math.min(1, (PULL_DISTANCE - distanceFromBottom) / PULL_DISTANCE))
const canScroll = contentSize.height > layoutMeasurement.height
const isNearBottom = distanceFromBottom <= LOAD_MORE_TRIGGER_DISTANCE
pullProgress.setValue(progress)
shouldLoadMoreOnRelease.current = canScroll && contentOffset.y > 0 && isNearBottom
}
const handleLoadMore = () => {
if (!activeHasMore || activeIsLoadingMore || isLoadingMoreRequest.current[activeTab]) {
if (!activeHasMore) {
setShowNoMoreNotesBubble(true)
}
return
}
isLoadingMoreRequest.current[activeTab] = true
void loadMoreNotes(activeTab).finally(() => {
isLoadingMoreRequest.current[activeTab] = false
})
}
const handleScrollEndDrag = () => {
if (shouldLoadMoreOnRelease.current) {
if (!activeHasMore) {
setShowNoMoreNotesBubble(true)
}
handleLoadMore()
}
}
useEffect(() => {
pullProgress.setValue(0)
shouldLoadMoreOnRelease.current = false
setShowNoMoreNotesBubble(false)
listRef.current?.scrollToOffset({ offset: 0, animated: false })
}, [activeTab, pullProgress])
useEffect(() => {
if (!showNoMoreNotesBubble) {
if (hideNoMoreNotesTimer.current) {
clearTimeout(hideNoMoreNotesTimer.current)
hideNoMoreNotesTimer.current = null
}
return
}
hideNoMoreNotesTimer.current = setTimeout(() => {
setShowNoMoreNotesBubble(false)
}, 5000)
return () => {
if (hideNoMoreNotesTimer.current) {
clearTimeout(hideNoMoreNotesTimer.current)
hideNoMoreNotesTimer.current = null
}
}
}, [showNoMoreNotesBubble])
const stemHeight = pullProgress.interpolate({
inputRange: [0, 1],
outputRange: [4, 28],
extrapolate: "clamp",
})
const arrowScale = pullProgress.interpolate({
inputRange: [0, 1],
outputRange: [0.85, 1.2],
extrapolate: "clamp",
})
const arrowOpacity = pullProgress.interpolate({
inputRange: [0, 0.2, 1],
outputRange: [0, 0.4, 1],
extrapolate: "clamp",
})
const hintOpacity = activeIsLoadingMore ? 1 : arrowOpacity
return (
<View style={[styles.container, { backgroundColor: palette.background }]}>
<View style={[styles.topBar, { paddingTop: insets.top + 8, backgroundColor: palette.surface, borderBottomColor: palette.border }]}>
<View
style={[
styles.topBar,
{
paddingTop: insets.top + 8,
backgroundColor: palette.surface,
borderBottomColor: palette.border,
},
]}
>
<Text style={[styles.screenTitle, { color: palette.text }]}>FastNotes</Text>
<SignOutButton />
</View>
@@ -93,9 +210,13 @@ export default function HomeScreen()
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
<FlatList
ref={listRef}
data={filteredNotes}
keyExtractor={(n) => n.id}
contentContainerStyle={[styles.list, { paddingBottom: 120 }]}
contentContainerStyle={[styles.list, { paddingBottom: 160 }]}
onScroll={handleScroll}
onScrollEndDrag={handleScrollEndDrag}
scrollEventThrottle={16}
ListEmptyComponent={
<Text style={styles.emptyText}>
{isLoading ? "Loading notes..." : emptyText}
@@ -117,7 +238,9 @@ export default function HomeScreen()
<Text numberOfLines={2} style={[styles.notePreview, { color: palette.mutedText }]}>
{item.content}
</Text>
<Text style={[styles.noteMeta, { color: palette.mutedText }]}>Created by {item.creatorLabel}</Text>
<Text style={[styles.noteMeta, { color: palette.mutedText }]}>
Created by {item.creatorLabel}
</Text>
<Text style={[styles.noteMeta, { color: palette.mutedText }]}>
Last changed {formatTimestamp(item.lastChangedAt)}
</Text>
@@ -132,9 +255,75 @@ export default function HomeScreen()
)}
/>
<View
pointerEvents="none"
style={[
styles.loadMoreHint,
{
bottom: insets.bottom + (activeTab === "my-notes" ? 24 : 24),
},
]}
>
{activeHasMore || activeIsLoadingMore ? (
<Animated.View
style={[
styles.loadMoreHintArrowOnly,
{
opacity: hintOpacity,
},
]}
>
<View style={styles.loadMoreHintGlyphColumn}>
<Animated.View
style={[
styles.loadMoreHintStem,
{
backgroundColor: palette.accent,
height: activeIsLoadingMore ? 28 : stemHeight,
opacity: hintOpacity,
},
]}
/>
<Animated.View
style={[
styles.loadMoreHintGlyph,
{
transform: [{ scale: activeIsLoadingMore ? 1.15 : arrowScale }],
opacity: hintOpacity,
},
]}
>
{activeIsLoadingMore ? (
<ActivityIndicator size="small" color={palette.accent} />
) : (
<Ionicons name="arrow-up" size={18} color={palette.accent} />
)}
</Animated.View>
</View>
</Animated.View>
) : showNoMoreNotesBubble ? (
<View
style={[
styles.loadMoreHintCard,
{
backgroundColor: palette.surface,
borderColor: palette.border,
opacity: 0.92,
},
styles.loadMoreHintCardCompact,
]}
>
<Text style={[styles.loadMoreHintTitle, { color: palette.text }]}>No more notes</Text>
</View>
) : null}
</View>
{activeTab === "my-notes" ? (
<Pressable
style={[styles.fab, { bottom: insets.bottom + 24, right: insets.right + 40, backgroundColor: palette.accent }]}
style={[
styles.fab,
{ bottom: insets.bottom + 24, right: insets.right + 40, backgroundColor: palette.accent },
]}
onPress={() => router.push("/newNote")}
>
<Text style={[styles.fabText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>+</Text>