updated note fetching logic, README, test and minor UI change. Also deleted /Diagrams
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 241 KiB |
@@ -1,42 +1,188 @@
|
|||||||
#Requirements
|
# FastNotes
|
||||||
|
|
||||||
Node.js (LTS recommended)
|
This project is an Expo React Native note-taking app built for a CS assignment submission. It supports:
|
||||||
npm (comes with Node.js)
|
|
||||||
Expo CLI (used via npx command)
|
|
||||||
Expo Go (for running on device / emulator)
|
|
||||||
|
|
||||||
#Installation
|
- Email/password authentication with Supabase Auth
|
||||||
|
- Creating, viewing, editing, and deleting notes
|
||||||
|
- Optional image upload for notes using Supabase Storage
|
||||||
|
- Push notification support through Expo and a Supabase Edge Function fallback path
|
||||||
|
|
||||||
1. Extract folder
|
## Requirements
|
||||||
2. Open terminal in project root (where you find 'package.json')
|
|
||||||
3. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
#Running the project
|
To build and run this project locally, you need:
|
||||||
|
|
||||||
Start Expo dev server:
|
- Node.js (LTS recommended)
|
||||||
```bash
|
- npm
|
||||||
npx expo start
|
- Expo Go on a physical device, or an Android/iOS emulator
|
||||||
```
|
- A Supabase project that you configure yourself
|
||||||
|
|
||||||
Then:
|
The repository does not include a committed `.env` file. That is intentional. The `.env` file is ignored by Git by design, so anyone running this project must create their own local `.env` file with their own Supabase and Expo values.
|
||||||
- Scan the QR code using Expo Go app on mobile device
|
|
||||||
**or**
|
|
||||||
- Run the app in an emulator from the Expo developer tools
|
|
||||||
|
|
||||||
#Running tests
|
## Installation
|
||||||
|
|
||||||
Run the Jest test suite from the project root:
|
1. Clone or extract the project.
|
||||||
```bash
|
2. Open a terminal in the project root.
|
||||||
npm test
|
3. Install dependencies:
|
||||||
```
|
|
||||||
|
|
||||||
Run each test file one by one:
|
```bash
|
||||||
```bash
|
npm install
|
||||||
npx jest __tests__/detail-screen.test.tsx
|
```
|
||||||
npx jest __tests__/auth-guard.test.tsx
|
|
||||||
npx jest __tests__/new-note.test.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file in the project root and define the following variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EXPO_PUBLIC_SUPABASE_URL=your_supabase_project_url
|
||||||
|
EXPO_PUBLIC_SUPABASE_KEY=your_supabase_anon_key
|
||||||
|
EXPO_PUBLIC_EAS_PROJECT_ID=your_expo_eas_project_id
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `EXPO_PUBLIC_SUPABASE_URL` is the URL of your Supabase project.
|
||||||
|
- `EXPO_PUBLIC_SUPABASE_KEY` is the public anonymous key for your Supabase project.
|
||||||
|
- `EXPO_PUBLIC_EAS_PROJECT_ID` is optional and is only used for Expo push notification registration and related build/push flows.
|
||||||
|
- The app requires `EXPO_PUBLIC_SUPABASE_URL` and `EXPO_PUBLIC_SUPABASE_KEY` at runtime. If those two values are missing, the app will not start correctly.
|
||||||
|
|
||||||
|
## Build And Run Instructions
|
||||||
|
|
||||||
|
Start the Expo development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also start a specific platform directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run android
|
||||||
|
npm run ios
|
||||||
|
npm run web
|
||||||
|
```
|
||||||
|
|
||||||
|
After the development server starts:
|
||||||
|
|
||||||
|
- Scan the QR code with Expo Go on a physical device, or
|
||||||
|
- Open the app in an emulator/simulator
|
||||||
|
|
||||||
|
## Test And Validation Commands
|
||||||
|
|
||||||
|
Run the Jest test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests in watch mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:watch
|
||||||
|
```
|
||||||
|
|
||||||
|
Run linting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Run TypeScript checks for the app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
Run type checks for the included Supabase Edge Function:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run typecheck:functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supabase Configuration Expected By The App
|
||||||
|
|
||||||
|
This app is not fully standalone. It expects your Supabase project to already contain the database tables and storage resources used by the code.
|
||||||
|
|
||||||
|
### 1. Auth
|
||||||
|
|
||||||
|
The app uses Supabase Auth with email/password sign-up and login.
|
||||||
|
|
||||||
|
### 2. `profiles` table
|
||||||
|
|
||||||
|
The app expects a `profiles` table that stores user profile information. Based on the code, it uses these columns:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `email`
|
||||||
|
- `username`
|
||||||
|
- `full_name`
|
||||||
|
|
||||||
|
The app upserts into `profiles` when a user signs up or when an authenticated session is restored.
|
||||||
|
|
||||||
|
### 3. `Notes` table
|
||||||
|
|
||||||
|
The app expects a table named `Notes` with this exact capitalization. Based on the code, it uses these columns:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `created_by`
|
||||||
|
- `title`
|
||||||
|
- `content`
|
||||||
|
- `created_at`
|
||||||
|
- `updated_at`
|
||||||
|
- `image_url`
|
||||||
|
- `image_path`
|
||||||
|
- `image_mime_type`
|
||||||
|
- `image_size_bytes`
|
||||||
|
|
||||||
|
Application behavior assumes:
|
||||||
|
|
||||||
|
- Each note belongs to a user through `created_by`
|
||||||
|
- Users can create notes
|
||||||
|
- Users can edit and delete only their own notes
|
||||||
|
- Notes are ordered by `updated_at` and `created_at`
|
||||||
|
|
||||||
|
### 4. Storage bucket
|
||||||
|
|
||||||
|
The app expects a public Supabase Storage bucket named:
|
||||||
|
|
||||||
|
```text
|
||||||
|
note-images
|
||||||
|
```
|
||||||
|
|
||||||
|
This bucket is used to upload note images. Stored image paths are then saved in the `Notes` table.
|
||||||
|
|
||||||
|
### 5. `user_push_tokens` table
|
||||||
|
|
||||||
|
For push notifications, the app expects a table named `user_push_tokens` with fields used for registering device tokens. Based on the code, it uses:
|
||||||
|
|
||||||
|
- `installation_id`
|
||||||
|
- `user_id`
|
||||||
|
- `push_token`
|
||||||
|
- `platform`
|
||||||
|
- `is_active`
|
||||||
|
- `updated_at`
|
||||||
|
|
||||||
|
## Supabase Edge Function
|
||||||
|
|
||||||
|
This repository includes a Supabase Edge Function at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supabase/functions/push/index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
That function is responsible for sending push notifications when notes are created.
|
||||||
|
|
||||||
|
If you want to use that function, your Supabase function environment will need its own server-side values, including:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SUPABASE_URL=your_supabase_project_url
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
||||||
|
EXPO_ACCESS_TOKEN=your_expo_access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are required by the function code.
|
||||||
|
- `EXPO_ACCESS_TOKEN` may be needed depending on how you configure Expo push notification delivery.
|
||||||
|
|
||||||
|
## Important Submission Note
|
||||||
|
|
||||||
|
Because `.env` is intentionally ignored by Git, this submission does not include live secrets or a working personal backend configuration. To run the project successfully, the evaluator must create their own `.env` file and connect the app to their own Supabase project configured with the expected tables, columns, and storage bucket described above.
|
||||||
|
|||||||
@@ -28,6 +28,49 @@ function createDeferred<T>(): Deferred<T> {
|
|||||||
return { promise, resolve, reject }
|
return { promise, resolve, reject }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DetailNoteResult = Promise<{
|
||||||
|
data: {
|
||||||
|
id: number
|
||||||
|
created_by: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
image_url: null
|
||||||
|
image_path: null
|
||||||
|
image_mime_type: null
|
||||||
|
image_size_bytes: null
|
||||||
|
} | null
|
||||||
|
error: null
|
||||||
|
}>
|
||||||
|
|
||||||
|
type NotesQueryMock = {
|
||||||
|
order: jest.Mock<NotesQueryMock, []>
|
||||||
|
eq: jest.Mock<NotesQueryMock, [string]>
|
||||||
|
neq: jest.Mock<NotesQueryMock, [string]>
|
||||||
|
range: jest.Mock<Promise<{ data: []; error: null }>, []>
|
||||||
|
maybeSingle: jest.Mock<DetailNoteResult, []>
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNotesSelectMock(
|
||||||
|
noteResult: DetailNoteResult | (() => DetailNoteResult)
|
||||||
|
) {
|
||||||
|
let query!: NotesQueryMock
|
||||||
|
|
||||||
|
query = {
|
||||||
|
order: jest.fn(() => query),
|
||||||
|
eq: jest.fn((_: string) => query),
|
||||||
|
neq: jest.fn((_: string) => query),
|
||||||
|
range: jest.fn(() => Promise.resolve({ data: [], error: null })),
|
||||||
|
maybeSingle: jest.fn(() => {
|
||||||
|
const result = typeof noteResult === "function" ? noteResult() : noteResult
|
||||||
|
return result
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
jest.mock("expo-router", () => ({
|
jest.mock("expo-router", () => ({
|
||||||
router: {
|
router: {
|
||||||
replace: jest.fn(),
|
replace: jest.fn(),
|
||||||
@@ -125,32 +168,23 @@ describe("DetailScreen", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function renderLoadedDetailScreen() {
|
async function renderLoadedDetailScreen() {
|
||||||
const notesQuery = {
|
const notesQuery = createNotesSelectMock(
|
||||||
order: jest.fn(),
|
Promise.resolve({
|
||||||
eq: jest.fn(),
|
data: {
|
||||||
maybeSingle: jest.fn(() =>
|
id: 42,
|
||||||
Promise.resolve({
|
created_by: "user-1",
|
||||||
data: {
|
title: "Fetched note",
|
||||||
id: 42,
|
content: "Loaded from Supabase for the integration test",
|
||||||
created_by: "user-1",
|
created_at: "2026-03-18T10:00:00.000Z",
|
||||||
title: "Fetched note",
|
updated_at: "2026-03-18T10:05:00.000Z",
|
||||||
content: "Loaded from Supabase for the integration test",
|
image_url: null,
|
||||||
created_at: "2026-03-18T10:00:00.000Z",
|
image_path: null,
|
||||||
updated_at: "2026-03-18T10:05:00.000Z",
|
image_mime_type: null,
|
||||||
image_url: null,
|
image_size_bytes: null,
|
||||||
image_path: null,
|
},
|
||||||
image_mime_type: null,
|
error: null,
|
||||||
image_size_bytes: null,
|
})
|
||||||
},
|
)
|
||||||
error: null,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
notesQuery.order
|
|
||||||
.mockImplementationOnce(() => notesQuery)
|
|
||||||
.mockImplementationOnce(() => Promise.resolve({ data: [], error: null }))
|
|
||||||
notesQuery.eq.mockReturnValue(notesQuery)
|
|
||||||
|
|
||||||
mockSupabase.from.mockImplementation((table: string) => {
|
mockSupabase.from.mockImplementation((table: string) => {
|
||||||
if (table === "Notes") {
|
if (table === "Notes") {
|
||||||
@@ -209,16 +243,7 @@ describe("DetailScreen", () => {
|
|||||||
} | null
|
} | null
|
||||||
error: null
|
error: null
|
||||||
}>()
|
}>()
|
||||||
const notesQuery = {
|
const notesQuery = createNotesSelectMock(() => deferredNote.promise)
|
||||||
order: jest.fn(),
|
|
||||||
eq: jest.fn(),
|
|
||||||
maybeSingle: jest.fn(() => deferredNote.promise),
|
|
||||||
}
|
|
||||||
|
|
||||||
notesQuery.order
|
|
||||||
.mockImplementationOnce(() => notesQuery)
|
|
||||||
.mockImplementationOnce(() => Promise.resolve({ data: [], error: null }))
|
|
||||||
notesQuery.eq.mockReturnValue(notesQuery)
|
|
||||||
|
|
||||||
mockSupabase.from.mockImplementation((table: string) => {
|
mockSupabase.from.mockImplementation((table: string) => {
|
||||||
if (table === "Notes") {
|
if (table === "Notes") {
|
||||||
|
|||||||
@@ -98,7 +98,12 @@ describe("NewNoteScreen", () => {
|
|||||||
mockUseNotes.mockReturnValue({
|
mockUseNotes.mockReturnValue({
|
||||||
notes: [],
|
notes: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isLoadingMoreMyNotes: false,
|
||||||
|
isLoadingMoreWorkNotes: false,
|
||||||
refreshNotes: jest.fn(),
|
refreshNotes: jest.fn(),
|
||||||
|
loadMoreNotes: jest.fn(),
|
||||||
|
hasMoreMyNotes: false,
|
||||||
|
hasMoreWorkNotes: false,
|
||||||
fetchNoteById: jest.fn(),
|
fetchNoteById: jest.fn(),
|
||||||
addNote: mockAddNote,
|
addNote: mockAddNote,
|
||||||
updateNote: jest.fn(),
|
updateNote: jest.fn(),
|
||||||
|
|||||||
@@ -1,25 +1,45 @@
|
|||||||
import { useMemo, useState } from "react"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { FlatList, Pressable, Text, View } from "react-native"
|
import { ActivityIndicator, Animated, FlatList, NativeScrollEvent, NativeSyntheticEvent, Pressable, Text, View } from "react-native"
|
||||||
import { router } from "expo-router"
|
import { router } from "expo-router"
|
||||||
|
import { Ionicons } from "@expo/vector-icons"
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
||||||
import { Image } from "expo-image"
|
import { Image } from "expo-image"
|
||||||
import { useAuthContext } from "@/hooks/use-auth-context"
|
import { useAuthContext } from "@/hooks/use-auth-context"
|
||||||
import { useNotes } from "@/src/notes/NotesContext"
|
import { type Note, useNotes } from "@/src/notes/NotesContext"
|
||||||
import SignOutButton from '@/components/social-auth-buttons/sign-out-button'
|
import SignOutButton from "@/components/social-auth-buttons/sign-out-button"
|
||||||
import { useAppTheme } from "@/src/theme/AppThemeProvider"
|
import { useAppTheme } from "@/src/theme/AppThemeProvider"
|
||||||
import { homeScreenStyles as styles } from "@/src/styles/app-styles"
|
import { homeScreenStyles as styles } from "@/src/styles/app-styles"
|
||||||
|
|
||||||
|
|
||||||
type TabKey = "my-notes" | "work-notes"
|
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 { 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 [activeTab, setActiveTab] = useState<TabKey>("my-notes")
|
||||||
|
const [showNoMoreNotesBubble, setShowNoMoreNotesBubble] = useState(false)
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const { colorScheme, palette } = useAppTheme()
|
const { colorScheme, palette } = useAppTheme()
|
||||||
const userId = claims?.sub
|
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(
|
const filteredNotes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -29,6 +49,10 @@ export default function HomeScreen()
|
|||||||
[activeTab, notes, userId]
|
[activeTab, notes, userId]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const activeHasMore = activeTab === "my-notes" ? hasMoreMyNotes : hasMoreWorkNotes
|
||||||
|
const activeIsLoadingMore =
|
||||||
|
activeTab === "my-notes" ? isLoadingMoreMyNotes : isLoadingMoreWorkNotes
|
||||||
|
|
||||||
const emptyText =
|
const emptyText =
|
||||||
activeTab === "my-notes"
|
activeTab === "my-notes"
|
||||||
? "No personal notes yet. Create your first note."
|
? "No personal notes yet. Create your first note."
|
||||||
@@ -44,9 +68,102 @@ export default function HomeScreen()
|
|||||||
return parsed.toLocaleString()
|
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 (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: palette.background }]}>
|
<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>
|
<Text style={[styles.screenTitle, { color: palette.text }]}>FastNotes</Text>
|
||||||
<SignOutButton />
|
<SignOutButton />
|
||||||
</View>
|
</View>
|
||||||
@@ -93,9 +210,13 @@ export default function HomeScreen()
|
|||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
|
ref={listRef}
|
||||||
data={filteredNotes}
|
data={filteredNotes}
|
||||||
keyExtractor={(n) => n.id}
|
keyExtractor={(n) => n.id}
|
||||||
contentContainerStyle={[styles.list, { paddingBottom: 120 }]}
|
contentContainerStyle={[styles.list, { paddingBottom: 160 }]}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
onScrollEndDrag={handleScrollEndDrag}
|
||||||
|
scrollEventThrottle={16}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<Text style={styles.emptyText}>
|
<Text style={styles.emptyText}>
|
||||||
{isLoading ? "Loading notes..." : emptyText}
|
{isLoading ? "Loading notes..." : emptyText}
|
||||||
@@ -117,7 +238,9 @@ export default function HomeScreen()
|
|||||||
<Text numberOfLines={2} style={[styles.notePreview, { color: palette.mutedText }]}>
|
<Text numberOfLines={2} style={[styles.notePreview, { color: palette.mutedText }]}>
|
||||||
{item.content}
|
{item.content}
|
||||||
</Text>
|
</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 }]}>
|
<Text style={[styles.noteMeta, { color: palette.mutedText }]}>
|
||||||
Last changed {formatTimestamp(item.lastChangedAt)}
|
Last changed {formatTimestamp(item.lastChangedAt)}
|
||||||
</Text>
|
</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" ? (
|
{activeTab === "my-notes" ? (
|
||||||
<Pressable
|
<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")}
|
onPress={() => router.push("/newNote")}
|
||||||
>
|
>
|
||||||
<Text style={[styles.fabText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>+</Text>
|
<Text style={[styles.fabText, { color: colorScheme === "dark" ? "#000" : "#fff" }]}>+</Text>
|
||||||
|
|||||||
275
FastNotes/package-lock.json
generated
@@ -22,10 +22,10 @@
|
|||||||
"expo-font": "~14.0.11",
|
"expo-font": "~14.0.11",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
"expo-image-manipulator": "^55.0.10",
|
"expo-image-manipulator": "~14.0.8",
|
||||||
"expo-image-picker": "^55.0.12",
|
"expo-image-picker": "~17.0.10",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-notifications": "^55.0.12",
|
"expo-notifications": "^0.32.16",
|
||||||
"expo-router": "~6.0.23",
|
"expo-router": "~6.0.23",
|
||||||
"expo-secure-store": "~15.0.8",
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
@@ -2448,25 +2448,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/require-utils": {
|
|
||||||
"version": "55.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.2.tgz",
|
|
||||||
"integrity": "sha512-dV5oCShQ1umKBKagMMT4B/N+SREsQe3lU4Zgmko5AO0rxKV0tynZT6xXs+e2JxuqT4Rz997atg7pki0BnZb4uw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/code-frame": "^7.20.0",
|
|
||||||
"@babel/core": "^7.25.2",
|
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.24.8"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.0.0 || ^5.0.0-0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@expo/schema-utils": {
|
"node_modules/@expo/schema-utils": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz",
|
||||||
@@ -2580,6 +2561,12 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ide/backoff": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/fs-minipass": {
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
@@ -5101,6 +5088,19 @@
|
|||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assert": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"is-nan": "^1.3.2",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object.assign": "^4.1.4",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async-function": {
|
"node_modules/async-function": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||||
@@ -5138,7 +5138,6 @@
|
|||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"possible-typed-array-names": "^1.0.0"
|
"possible-typed-array-names": "^1.0.0"
|
||||||
@@ -5624,7 +5623,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.0",
|
"call-bind-apply-helpers": "^1.0.0",
|
||||||
@@ -5643,7 +5641,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -5657,7 +5654,6 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@@ -6336,7 +6332,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-define-property": "^1.0.0",
|
"es-define-property": "^1.0.0",
|
||||||
@@ -6363,7 +6358,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.0.1",
|
"define-data-property": "^1.0.1",
|
||||||
@@ -6527,7 +6521,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@@ -6699,7 +6692,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -6709,7 +6701,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -6747,7 +6738,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@@ -7465,9 +7455,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-application": {
|
"node_modules/expo-application": {
|
||||||
"version": "55.0.9",
|
"version": "7.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-55.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
|
||||||
"integrity": "sha512-jXTaLKdW4cvGSUjF2UQed9ao4P/7TsEo/To7TjxM+jNa74xCSUCBSTxdQftm6hZWRzXG8KT7rSoQDEL51neh1w==",
|
"integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
@@ -7564,33 +7554,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-image-loader": {
|
"node_modules/expo-image-loader": {
|
||||||
"version": "55.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-55.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz",
|
||||||
"integrity": "sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==",
|
"integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-image-manipulator": {
|
"node_modules/expo-image-manipulator": {
|
||||||
"version": "55.0.10",
|
"version": "14.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-55.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-14.0.8.tgz",
|
||||||
"integrity": "sha512-eEiHSznWa0i5I7iNFDRuHz663XiS26s8SEFigGbsvkFDibGI9x391Qb76DPSGtnqNkJa39etuFw42lbErHphHA==",
|
"integrity": "sha512-sXsXjm7rIxLWZe0j2A41J/Ph53PpFJRdyzJ3EQ/qetxLUvS2m3K1sP5xy37px43qCf0l79N/i6XgFgenFV36/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-image-loader": "~55.0.0"
|
"expo-image-loader": "~6.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-image-picker": {
|
"node_modules/expo-image-picker": {
|
||||||
"version": "55.0.12",
|
"version": "17.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-55.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
|
||||||
"integrity": "sha512-ky8nzXTd5eLUDct5daAHng0xrWYRJyXfLCRmEdE9v/IUywYCnFU7aCnQ7PTQJvzGSzhePJJmP/POvTkVP//+qQ==",
|
"integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expo-image-loader": "~55.0.0"
|
"expo-image-loader": "~6.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*"
|
"expo": "*"
|
||||||
@@ -7650,16 +7640,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-notifications": {
|
"node_modules/expo-notifications": {
|
||||||
"version": "55.0.12",
|
"version": "0.32.16",
|
||||||
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-55.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz",
|
||||||
"integrity": "sha512-AUAH1ipq7yChZqwp9P/gfmXNoaleKWvEhnIB6/dhtWtTnZZ5VDHdxqzQIbTemYQyIK6kpUc4JZpR9eU3d59K3g==",
|
"integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/image-utils": "^0.8.12",
|
"@expo/image-utils": "^0.8.8",
|
||||||
|
"@ide/backoff": "^1.0.0",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
|
"assert": "^2.0.0",
|
||||||
"badgin": "^1.1.5",
|
"badgin": "^1.1.5",
|
||||||
"expo-application": "~55.0.9",
|
"expo-application": "~7.0.8",
|
||||||
"expo-constants": "~55.0.7"
|
"expo-constants": "~18.0.13"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"expo": "*",
|
"expo": "*",
|
||||||
@@ -7667,103 +7659,6 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-notifications/node_modules/@expo/config": {
|
|
||||||
"version": "55.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.8.tgz",
|
|
||||||
"integrity": "sha512-D7RYYHfErCgEllGxNwdYdkgzLna7zkzUECBV3snbUpf7RvIpB5l1LpCgzuVoc5KVew5h7N1Tn4LnT/tBSUZsQg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/config-plugins": "~55.0.6",
|
|
||||||
"@expo/config-types": "^55.0.5",
|
|
||||||
"@expo/json-file": "^10.0.12",
|
|
||||||
"@expo/require-utils": "^55.0.2",
|
|
||||||
"deepmerge": "^4.3.1",
|
|
||||||
"getenv": "^2.0.0",
|
|
||||||
"glob": "^13.0.0",
|
|
||||||
"resolve-from": "^5.0.0",
|
|
||||||
"resolve-workspace-root": "^2.0.0",
|
|
||||||
"semver": "^7.6.0",
|
|
||||||
"slugify": "^1.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-notifications/node_modules/@expo/config-plugins": {
|
|
||||||
"version": "55.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.6.tgz",
|
|
||||||
"integrity": "sha512-cIox6FjZlFaaX40rbQ3DvP9e87S5X85H9uw+BAxJE5timkMhuByy3GAlOsj1h96EyzSiol7Q6YIGgY1Jiz4M+A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/config-types": "^55.0.5",
|
|
||||||
"@expo/json-file": "~10.0.12",
|
|
||||||
"@expo/plist": "^0.5.2",
|
|
||||||
"@expo/sdk-runtime-versions": "^1.0.0",
|
|
||||||
"chalk": "^4.1.2",
|
|
||||||
"debug": "^4.3.5",
|
|
||||||
"getenv": "^2.0.0",
|
|
||||||
"glob": "^13.0.0",
|
|
||||||
"resolve-from": "^5.0.0",
|
|
||||||
"semver": "^7.5.4",
|
|
||||||
"slugify": "^1.6.6",
|
|
||||||
"xcode": "^3.0.1",
|
|
||||||
"xml2js": "0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-notifications/node_modules/@expo/config-types": {
|
|
||||||
"version": "55.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-55.0.5.tgz",
|
|
||||||
"integrity": "sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/expo-notifications/node_modules/@expo/env": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/env/-/env-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^4.0.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"getenv": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-notifications/node_modules/@expo/plist": {
|
|
||||||
"version": "0.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.5.2.tgz",
|
|
||||||
"integrity": "sha512-o4xdVdBpe4aTl3sPMZ2u3fJH4iG1I768EIRk1xRZP+GaFI93MaR3JvoFibYqxeTmLQ1p1kNEVqylfUjezxx45g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@xmldom/xmldom": "^0.8.8",
|
|
||||||
"base64-js": "^1.5.1",
|
|
||||||
"xmlbuilder": "^15.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-notifications/node_modules/expo-constants": {
|
|
||||||
"version": "55.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.7.tgz",
|
|
||||||
"integrity": "sha512-kdcO4TsQRRqt0USvjaY5vgQMO9H52K3kBZ/ejC7F6rz70mv08GoowrZ1CYOr5O4JpPDRlIpQfZJUucaS/c+KWQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/config": "~55.0.8",
|
|
||||||
"@expo/env": "~2.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-notifications/node_modules/semver": {
|
|
||||||
"version": "7.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-router": {
|
"node_modules/expo-router": {
|
||||||
"version": "6.0.23",
|
"version": "6.0.23",
|
||||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.23.tgz",
|
||||||
@@ -8325,7 +8220,6 @@
|
|||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-callable": "^1.2.7"
|
"is-callable": "^1.2.7"
|
||||||
@@ -8448,7 +8342,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
|
||||||
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
|
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -8476,7 +8369,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@@ -8519,7 +8411,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@@ -8682,7 +8573,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -8723,7 +8613,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-define-property": "^1.0.0"
|
"es-define-property": "^1.0.0"
|
||||||
@@ -8752,7 +8641,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -8765,7 +8653,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -9131,6 +9018,22 @@
|
|||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -9235,7 +9138,6 @@
|
|||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -9358,7 +9260,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||||
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
|
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.4",
|
"call-bound": "^1.0.4",
|
||||||
@@ -9400,6 +9301,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-nan": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-negative-zero": {
|
"node_modules/is-negative-zero": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
||||||
@@ -9459,7 +9376,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@@ -9555,7 +9471,6 @@
|
|||||||
"version": "1.1.15",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
||||||
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"which-typed-array": "^1.1.16"
|
"which-typed-array": "^1.1.16"
|
||||||
@@ -11537,7 +11452,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -12243,11 +12157,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -12257,7 +12186,6 @@
|
|||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
|
||||||
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
|
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.8",
|
"call-bind": "^1.0.8",
|
||||||
@@ -12833,7 +12761,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -13887,7 +13814,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||||
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@@ -14057,7 +13983,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.1.4",
|
"define-data-property": "^1.1.4",
|
||||||
@@ -15168,7 +15093,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -15444,6 +15369,19 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
@@ -15892,7 +15830,6 @@
|
|||||||
"version": "1.1.20",
|
"version": "1.1.20",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
||||||
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
|
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"available-typed-arrays": "^1.0.7",
|
"available-typed-arrays": "^1.0.7",
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
"expo-font": "~14.0.11",
|
"expo-font": "~14.0.11",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
"expo-image-manipulator": "^55.0.10",
|
"expo-image-manipulator": "~14.0.8",
|
||||||
"expo-image-picker": "^55.0.12",
|
"expo-image-picker": "~17.0.10",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-notifications": "^55.0.12",
|
"expo-notifications": "^0.32.16",
|
||||||
"expo-router": "~6.0.23",
|
"expo-router": "~6.0.23",
|
||||||
"expo-secure-store": "~15.0.8",
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
|
|||||||
@@ -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 { useAuthContext } from "@/hooks/use-auth-context"
|
||||||
import { supabase } from "@/libs/supabase"
|
import { supabase } from "@/libs/supabase"
|
||||||
import { deleteNoteImage, NoteImageUploadProgress, uploadNoteImage } from "@/src/notes/note-image-storage"
|
import { deleteNoteImage, NoteImageUploadProgress, uploadNoteImage } from "@/src/notes/note-image-storage"
|
||||||
import { StagedNoteImage } from "@/src/notes/image-utils"
|
import { StagedNoteImage } from "@/src/notes/image-utils"
|
||||||
|
|
||||||
|
const NOTES_PAGE_SIZE = 5
|
||||||
|
|
||||||
|
export type NoteListKey = "my-notes" | "work-notes"
|
||||||
|
|
||||||
type NoteRow = {
|
type NoteRow = {
|
||||||
id: number
|
id: number
|
||||||
created_by: string
|
created_by: string
|
||||||
@@ -47,8 +51,13 @@ export type NoteImageChange =
|
|||||||
type NotesContextValue = {
|
type NotesContextValue = {
|
||||||
notes: Note[]
|
notes: Note[]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
isLoadingMoreMyNotes: boolean
|
||||||
|
isLoadingMoreWorkNotes: boolean
|
||||||
errorMessage: string | null
|
errorMessage: string | null
|
||||||
refreshNotes: () => Promise<void>
|
refreshNotes: () => Promise<void>
|
||||||
|
loadMoreNotes: (listKey: NoteListKey) => Promise<void>
|
||||||
|
hasMoreMyNotes: boolean
|
||||||
|
hasMoreWorkNotes: boolean
|
||||||
fetchNoteById: (noteId: string) => Promise<Note | null>
|
fetchNoteById: (noteId: string) => Promise<Note | null>
|
||||||
addNote: (
|
addNote: (
|
||||||
title: string,
|
title: string,
|
||||||
@@ -83,9 +92,18 @@ function normalizeImageSizeBytes(value: number | string | null | undefined) {
|
|||||||
|
|
||||||
export function NotesProvider({ children }: { children: React.ReactNode }) {
|
export function NotesProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { claims, isLoggedIn, profile } = useAuthContext()
|
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 [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 [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||||
|
const myNotesPageRef = useRef(0)
|
||||||
|
const workNotesPageRef = useRef(0)
|
||||||
|
|
||||||
const userId = claims?.sub as string | undefined
|
const userId = claims?.sub as string | undefined
|
||||||
const creatorLabel =
|
const creatorLabel =
|
||||||
@@ -138,9 +156,72 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
|||||||
imageSizeBytes: normalizeImageSizeBytes(row.image_size_bytes),
|
imageSizeBytes: normalizeImageSizeBytes(row.image_size_bytes),
|
||||||
}), [creatorLabel, userId])
|
}), [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) {
|
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)
|
setErrorMessage(null)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
@@ -149,33 +230,97 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
|
|
||||||
const { data, error } = await supabase
|
try {
|
||||||
.from("Notes")
|
const myPageCount = preserveLoadedPages ? Math.max(1, myNotesPageRef.current) : 1
|
||||||
.select(
|
const workPageCount = preserveLoadedPages ? Math.max(1, workNotesPageRef.current) : 1
|
||||||
"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 })
|
|
||||||
|
|
||||||
if (error) {
|
const [myNotes, workNotes] = await Promise.all([
|
||||||
setErrorMessage(error.message)
|
fetchNotesPage("my-notes", 0, myPageCount * NOTES_PAGE_SIZE),
|
||||||
setNotes([])
|
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)
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [fetchNotesPage, isLoggedIn, mergeNotes])
|
||||||
|
|
||||||
|
const refreshNotes = async () => {
|
||||||
|
await loadNotes(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMoreNotes = useCallback(async (listKey: NoteListKey) => {
|
||||||
|
if (!isLoggedIn || !userId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = (data ?? []) as NoteRow[]
|
const isMyNotes = listKey === "my-notes"
|
||||||
const labels = await buildCreatorLabels(rows)
|
const nextPage = isMyNotes ? myNotesPage : workNotesPage
|
||||||
|
const hasMoreNotes = isMyNotes ? hasMoreMyNotes : hasMoreWorkNotes
|
||||||
|
const isAlreadyLoading = isMyNotes ? isLoadingMoreMyNotes : isLoadingMoreWorkNotes
|
||||||
|
|
||||||
setNotes(
|
if (!hasMoreNotes || isAlreadyLoading) {
|
||||||
rows.map((row) => mapNoteRow(row, labels))
|
return
|
||||||
)
|
}
|
||||||
setIsLoading(false)
|
|
||||||
}, [buildCreatorLabels, isLoggedIn, mapNoteRow])
|
|
||||||
|
|
||||||
const refreshNotes = async () => {
|
setErrorMessage(null)
|
||||||
await loadNotes()
|
|
||||||
}
|
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) => {
|
const fetchNoteById = useCallback(async (noteId: string) => {
|
||||||
if (!isLoggedIn || !noteId) {
|
if (!isLoggedIn || !noteId) {
|
||||||
@@ -205,17 +350,15 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const labels = await buildCreatorLabels([row])
|
const labels = await buildCreatorLabels([row])
|
||||||
const fetchedNote = mapNoteRow(row, labels)
|
const fetchedNote = mapNoteRow(row, labels)
|
||||||
|
|
||||||
setNotes((prev) => {
|
setFetchedNotes((prev) => mergeNotes(prev, [fetchedNote]))
|
||||||
const nextNotes = prev.filter((existingNote) => existingNote.id !== fetchedNote.id)
|
|
||||||
return [fetchedNote, ...nextNotes]
|
|
||||||
})
|
|
||||||
|
|
||||||
return fetchedNote
|
return fetchedNote
|
||||||
}, [buildCreatorLabels, isLoggedIn, mapNoteRow])
|
}, [buildCreatorLabels, isLoggedIn, mapNoteRow])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn || !userId) {
|
if (!isLoggedIn || !userId) {
|
||||||
setNotes([])
|
setPagedNotes([])
|
||||||
|
setFetchedNotes([])
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
@@ -230,7 +373,7 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
void loadNotes()
|
void loadNotes(true)
|
||||||
}, 30000)
|
}, 30000)
|
||||||
|
|
||||||
return () => {
|
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) =>
|
prev.map((note) =>
|
||||||
note.id === noteId
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,8 +630,13 @@ export function NotesProvider({ children }: { children: React.ReactNode }) {
|
|||||||
value={{
|
value={{
|
||||||
notes,
|
notes,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isLoadingMoreMyNotes,
|
||||||
|
isLoadingMoreWorkNotes,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
refreshNotes,
|
refreshNotes,
|
||||||
|
loadMoreNotes,
|
||||||
|
hasMoreMyNotes,
|
||||||
|
hasMoreWorkNotes,
|
||||||
fetchNoteById,
|
fetchNoteById,
|
||||||
addNote,
|
addNote,
|
||||||
updateNote,
|
updateNote,
|
||||||
|
|||||||
@@ -326,6 +326,77 @@ export const homeScreenStyles = StyleSheet.create({
|
|||||||
elevation: 8,
|
elevation: 8,
|
||||||
},
|
},
|
||||||
fabText: { fontSize: 28, lineHeight: 28, fontWeight: "700" },
|
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({
|
export const noteImagePanelStyles = StyleSheet.create({
|
||||||
|
|||||||