diff --git a/.gitignore b/.gitignore index 4074a5c..f5cbf7d 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ dist-android-update.zip # Backend backend/data/ backend/logs/ + +doc/ +docs/ diff --git a/app.json b/app.json index ca43d16..2511aa4 100644 --- a/app.json +++ b/app.json @@ -2,10 +2,11 @@ "expo": { "name": "萝卜社区", "slug": "qojo", - "version": "1.0.10", + "version": "1.0.11", "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "light", + "scheme": "carrotbbs", "splash": { "image": "./assets/splash-icon.png", "resizeMode": "contain", @@ -33,7 +34,7 @@ }, "predictiveBackGestureEnabled": false, "package": "skin.carrot.bbs", - "versionCode": 5, + "versionCode": 6, "permissions": [ "VIBRATE", "RECEIVE_BOOT_COMPLETED", @@ -68,6 +69,12 @@ } ], "expo-sqlite", + [ + "expo-camera", + { + "cameraPermission": "允许萝卜社区访问您的相机以扫描二维码" + } + ], [ "expo-notifications", { diff --git a/docs/OPTIMIZATION_DESIGN.md b/docs/OPTIMIZATION_DESIGN.md deleted file mode 100644 index 5b1a7e8..0000000 --- a/docs/OPTIMIZATION_DESIGN.md +++ /dev/null @@ -1,1725 +0,0 @@ -# 聊天系统性能优化设计方案 - -## 文档信息 - -- **创建日期**: 2026-03-18 -- **项目**: 胡萝卜 BBS (Carrot BBS) 前端 -- **范围**: 消息系统性能优化 - ---- - -## 目录 - -1. [架构概述](#架构概述) -2. [P0 分页状态管理](#p0-分页状态管理) -3. [P0 同步状态机](#p0-同步状态机) -4. [P1 差异更新](#p1-差异更新) -5. [P1 媒体缓存清理](#p1-媒体缓存清理) -6. [实现优先级与依赖关系](#实现优先级与依赖关系) - ---- - -## 架构概述 - -### 当前消息系统架构 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ UI Layer │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ ChatScreen │ │ MessageList │ │ -│ │ useChatScreen │ │ Screen │ │ -│ └────────┬────────┘ └────────┬────────┘ │ -└───────────┼─────────────────────┼───────────────────────────────┘ - │ │ - ▼ ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Store Layer (Zustand) │ -│ ┌─────────────────────────────────────────────────────────────┐│ -│ │ messageManager (MessageManager) ││ -│ │ - messages Map ││ -│ │ - conversations: Conversation[] ││ -│ │ - activeConversation: string | null ││ -│ │ - isConnected: boolean ││ -│ └─────────────────────────────────────────────────────────────┘│ -│ ┌─────────────────────────────────────────────────────────────┐│ -│ │ messageManagerHooks (React Hooks) ││ -│ │ - useChat(conversationId) ││ -│ │ - useMessages(conversationId) ││ -│ │ - useConversations() ││ -│ └─────────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ UseCase Layer │ -│ ┌─────────────────────────────────────────────────────────────┐│ -│ │ ProcessMessageUseCase ││ -│ │ - WebSocket 事件监听与处理 ││ -│ │ - 消息去重 (processedMessageIds) ││ -│ │ - 已读状态管理 (pendingReadMap) ││ -│ │ - 消息持久化到 Repository ││ -│ └─────────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ DataSource Layer │ -│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐│ -│ │WebSocketClient│ │CacheDataSource│ │LocalDataSource (SQLite)││ -│ │ │ │ (Memory+Async) │ │ ││ -│ │ - 事件订阅 │ │ - get/set │ │ - messages 表 ││ -│ │ - emit │ │ - delete │ │ - conversations 表 ││ -│ │ - 连接状态 │ │ - clear │ │ - users 表 ││ -│ └───────────────┘ └───────────────┘ └───────────────────────┘│ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ ┌─────────────────────────────────────────────────────────────┐│ -│ │ sseService (SSE Client) ││ -│ │ - EventSource 连接管理 ││ -│ │ - 重连逻辑 (maxReconnectAttempts=20) ││ -│ │ - AppState 监听 ││ -│ └─────────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 问题分析 - -基于代码分析,当前系统存在以下问题: - -| 问题 | 影响 | 优先级 | -|------|------|--------| -| 分页状态管理不完善 | 用户滚动加载时可能重复请求同一页数据 | P0 | -| 连接状态反馈不清晰 | 用户不知道连接是否正常 | P0 | -| 全量数据更新 | 大房间每次更新都刷新整个消息列表 | P1 | -| 媒体缓存无清理机制 | 长期使用后存储空间爆炸 | P1 | - ---- - -## P0 分页状态管理 - -### 现有代码分析 - -#### 问题点 - -1. **`useChatScreen.ts` (第388-408行)** - ```typescript - const loadMoreHistory = useCallback(async () => { - if (!conversationId || !hasMoreHistory || loadingMore) { - return; - } - // 缺少分页游标管理,可能导致重复加载 - setLoadingMore(true); - try { - await loadMoreMessages(); - // 没有追踪当前加载到了哪一页 - } finally { - setLoadingMore(false); - } - }, [...]); - ``` - -2. **`messageManagerHooks.ts` (第156-158行)** - ```typescript - setIsLoading(true); - const loadedMessages = await messageManager.loadMoreMessages(conversationId, minSeq, 20); - // 直接使用 minSeq,但没有缓存 minSeq 的状态 - ``` - -3. **`MessageRepository.ts` (第125-137行)** - ```typescript - async getMessagesBeforeSeq(conversationId, beforeSeq, limit = 20) { - // 依赖 beforeSeq 参数,但没有分页偏移量管理 - } - ``` - -#### 架构缺陷 - -- 没有独立的分页状态存储 -- `hasMoreMessages` 状态与实际数据可能不同步 -- 并发加载时缺少互斥机制 -- 没有加载中的分页信息缓存 - -### 实现方案 - -#### 1. 新增分页状态管理器 - -```typescript -// src/stores/pagination/PaginationStateManager.ts - -interface PaginationState { - conversationId: string; - currentMinSeq: number; // 当前已加载的最小seq - currentMaxSeq: number; // 当前已加载的最大seq - pages: Map; // pageNumber -> messageIds - loadingPageNumbers: Set; - hasMoreBefore: boolean; - hasMoreAfter: boolean; - totalLoaded: number; -} - -class PaginationStateManager { - private states: Map = new Map(); - - // 获取或创建分页状态 - getOrCreate(conversationId: string): PaginationState { ... } - - // 标记页面加载中 - markPageLoading(conversationId: string, pageNumber: number): void { ... } - - // 标记页面加载完成 - markPageLoaded(conversationId: string, pageNumber: number, messageIds: string[], hasMore: boolean): void { ... } - - // 检查页面是否已加载 - isPageLoaded(conversationId: string, pageNumber: number): boolean { ... } - - // 检查页面是否正在加载 - isPageLoading(conversationId: string, pageNumber: number): boolean { ... } - - // 重置分页状态 - reset(conversationId: string): void { ... } -} -``` - -#### 2. 修改 useMessages Hook - -```typescript -// src/stores/messageManagerHooks.ts - -interface UseMessagesOptions { - pageSize?: number; - prefetchThreshold?: number; // 预加载阈值 -} - -function useMessages( - conversationId: string | null, - options: UseMessagesOptions = {} -) { - const { - pageSize = 20, - prefetchThreshold = 5 - } = options; - - // 分页状态 - const [paginationState, setPaginationState] = useState(null); - - // 计算当前页码 - const currentPage = useMemo(() => { - if (!paginationState) return 1; - return Math.ceil( - (paginationState.totalLoaded - messages.length) / pageSize - ) + 1; - }, [paginationState, messages.length]); - - // 加载更多(带分页保护) - const loadMoreMessages = useCallback(async () => { - if (!conversationId || !paginationState) return; - - // 计算下一页 - const nextPage = Math.floor(paginationState.totalLoaded / pageSize) + 1; - - // 检查是否正在加载 - if (paginationState.loadingPageNumbers.has(nextPage)) { - return; - } - - // 检查是否已加载 - if (paginationState.pages.has(nextPage)) { - return; - } - - await messageManager.loadMoreMessages( - conversationId, - paginationState.currentMinSeq, - pageSize - ); - }, [conversationId, paginationState, pageSize]); - - // 预加载检测 - useEffect(() => { - if (!paginationState || !hasMore) return; - - const currentPageLoaded = messages.length; - const threshold = pageSize - prefetchThreshold; - - if (currentPageLoaded <= threshold && !paginationState.loadingPageNumbers.has(1)) { - loadMoreMessages(); - } - }, [messages.length, hasMore, loadMoreMessages]); -} -``` - -#### 3. 修改 MessageManager - -```typescript -// src/stores/message/MessageManager.ts - -class MessageManager { - private paginationManager = new PaginationStateManager(); - - async loadMoreMessages( - conversationId: string, - beforeSeq: number, - limit: number - ): Promise { - // 检查分页状态 - const pageNumber = this.calculatePageNumber(conversationId, beforeSeq); - - if (this.paginationManager.isPageLoading(conversationId, pageNumber)) { - return []; // 避免重复加载 - } - - if (this.paginationManager.isPageLoaded(conversationId, pageNumber)) { - return []; // 已加载,直接返回 - } - - this.paginationManager.markPageLoading(conversationId, pageNumber); - - try { - // 调用 API - const response = await messageService.getMessages(conversationId, { - before_seq: beforeSeq, - limit - }); - - const messageIds = response.messages.map(m => m.id); - - this.paginationManager.markPageLoaded( - conversationId, - pageNumber, - messageIds, - response.has_more - ); - - // 更新消息存储 - this.appendMessages(conversationId, response.messages); - - return response.messages; - } catch (error) { - // 加载失败,清除分页状态 - this.paginationManager.markPageFailed(conversationId, pageNumber); - throw error; - } - } -} -``` - -#### 4. 修改数据库查询 - -```typescript -// src/services/database.ts (或 MessageRepository) - -export async function getMessagesBeforeSeq( - conversationId: string, - beforeSeq: number, - limit: number -): Promise { - // 确保使用索引优化查询 - const result = await db.getAllAsync(` - SELECT * FROM messages - WHERE conversationId = ? AND seq < ? - ORDER BY seq DESC - LIMIT ? - `, [conversationId, beforeSeq, limit]); - - return result; -} -``` - -### 修改/新增文件列表 - -| 文件 | 操作 | 说明 | -|------|------|------| -| `src/stores/pagination/PaginationStateManager.ts` | 新增 | 分页状态管理器 | -| `src/stores/pagination/index.ts` | 新增 | 导出文件 | -| `src/stores/messageManagerHooks.ts` | 修改 | 集成分页状态管理 | -| `src/stores/message/MessageManager.ts` | 修改 | 添加分页保护逻辑 | -| `src/data/repositories/MessageRepository.ts` | 修改 | 优化分页查询 | - -### 关键设计决策 - -1. **分页粒度**: 以 `beforeSeq` 为游标,而非 offset,避免消息新增导致的分页漂移 -2. **页面缓存**: 使用 `Map` 缓存已加载页面,避免重复请求 -3. **加载锁**: 使用 `Set` 追踪正在加载的页面,防止并发重复加载 -4. **预加载**: 当滚动到距离底部 `prefetchThreshold` 条消息时,提前加载下一页 - ---- - -## P0 同步状态机 - -### 现有代码分析 - -#### 问题点 - -1. **`sseService.ts` (第406-408行)** - ```typescript - isConnected(): boolean { - return this.source != null; - } - // 状态判断过于简单,source != null 不代表真正连接成功 - ``` - -2. **`WebSocketClient.ts` (第23-35行)** - ```typescript - export interface WebSocketEvents { - 'connected': void; - 'disconnected': void; - 'error': Error; - } - // 只有 connected/disconnected 两种状态 - ``` - -3. **`sseService.ts` (第190-228行)** - ```typescript - async connect(): Promise { - // isConnecting 状态没有暴露给外部 - // 外部无法区分"正在连接"和"连接失败" - } - ``` - -4. **`messageManagerHooks.ts` (第390-405行)** - ```typescript - const [isConnected, setIsConnected] = useState(() => messageManager.isConnected()); - // 连接状态变化没有详细的状态分类 - ``` - -#### 状态定义缺失 - -当前没有区分以下状态: -- **Connecting**: 正在建立连接 -- **Connected**: 连接已建立 -- **Reconnecting**: 连接断开,正在重连 -- **Disconnected**: 连接已断开 -- **Error**: 连接错误 - -### 实现方案 - -#### 1. 定义连接状态机 - -```typescript -// src/services/connection/ConnectionState.ts - -export enum ConnectionStateType { - IDLE = 'idle', - CONNECTING = 'connecting', - CONNECTED = 'connected', - RECONNECTING = 'reconnecting', - DISCONNECTED = 'disconnected', - ERROR = 'error', -} - -export interface ConnectionState { - type: ConnectionStateType; - timestamp: number; - error?: Error; - retryCount?: number; - lastConnectedAt?: number; -} - -export interface ConnectionStateChangeEvent { - previousState: ConnectionState; - currentState: ConnectionState; - reason?: string; -} -``` - -#### 2. 创建连接状态管理器 - -```typescript -// src/services/connection/ConnectionStateManager.ts - -type ConnectionStateListener = (event: ConnectionStateChangeEvent) => void; - -class ConnectionStateManager { - private state: ConnectionState = { - type: ConnectionStateType.IDLE, - timestamp: Date.now(), - }; - - private listeners: Set = new Set(); - private reconnectAttempts = 0; - private maxReconnectAttempts = 20; - - // 获取当前状态 - getState(): ConnectionState { ... } - - // 状态转换 - transition(newType: ConnectionStateType, reason?: string, error?: Error): void { - const previousState = this.state; - this.state = { - type: newType, - timestamp: Date.now(), - error, - retryCount: newType === ConnectionStateType.RECONNECTING - ? this.reconnectAttempts - : undefined, - lastConnectedAt: newType === ConnectionStateType.CONNECTED - ? Date.now() - : this.state.lastConnectedAt, - }; - - this.notifyListeners({ previousState, currentState: this.state, reason }); - } - - // 增加重试计数 - incrementRetry(): number { - this.reconnectAttempts++; - return this.reconnectAttempts; - } - - // 重置重试计数 - resetRetry(): void { - this.reconnectAttempts = 0; - } - - // 订阅状态变化 - subscribe(listener: ConnectionStateListener): () => void { ... } - - // 获取可读的状态描述 - getStatusDescription(): string { - switch (this.state.type) { - case ConnectionStateType.IDLE: - return '未连接'; - case ConnectionStateType.CONNECTING: - return '正在连接...'; - case ConnectionStateType.CONNECTED: - return '已连接'; - case ConnectionStateType.RECONNECTING: - return `正在重连 (${this.state.retryCount}/${this.maxReconnectAttempts})`; - case ConnectionStateType.DISCONNECTED: - return '已断开'; - case ConnectionStateType.ERROR: - return `连接错误: ${this.state.error?.message || '未知错误'}`; - } - } -} - -export const connectionStateManager = new ConnectionStateManager(); -``` - -#### 3. 修改 SSEService - -```typescript -// src/services/sseService.ts - -class SSEService { - private connectionStateManager = connectionStateManager; - - async connect(): Promise { - if (this.isConnecting || this.isConnected()) { - return true; - } - - this.connectionStateManager.transition( - ConnectionStateType.CONNECTING, - 'initiated' - ); - - try { - const token = await api.getToken(); - if (!token) { - throw new Error('No auth token'); - } - - // ... 建立连接 ... - - this.source.addEventListener('open', () => { - this.connectionStateManager.resetRetry(); - this.connectionStateManager.transition( - ConnectionStateType.CONNECTED, - 'connection_opened' - ); - }); - - this.source.addEventListener('error', (error) => { - this.connectionStateManager.transition( - ConnectionStateType.ERROR, - 'connection_error', - error - ); - this.scheduleReconnect(); - }); - - return true; - } catch (error) { - this.connectionStateManager.transition( - ConnectionStateType.ERROR, - 'connection_failed', - error - ); - this.scheduleReconnect(); - return false; - } - } - - private scheduleReconnect(): void { - const retryCount = this.connectionStateManager.incrementRetry(); - - if (retryCount >= this.maxReconnectAttempts) { - this.connectionStateManager.transition( - ConnectionStateType.DISCONNECTED, - 'max_reconnect_exceeded' - ); - return; - } - - this.connectionStateManager.transition( - ConnectionStateType.RECONNECTING, - 'scheduling_reconnect' - ); - - this.reconnectTimer = setTimeout(() => { - this.connect(); - }, this.reconnectDelay); - } -} -``` - -#### 4. 创建连接状态 Hook - -```typescript -// src/hooks/useConnectionState.ts - -export function useConnectionState() { - const [state, setState] = useState( - () => connectionStateManager.getState() - ); - - useEffect(() => { - const unsubscribe = connectionStateManager.subscribe((event) => { - setState(event.currentState); - }); - - return unsubscribe; - }, []); - - return { - state, - status: connectionStateManager.getStatusDescription(), - isConnected: state.type === ConnectionStateType.CONNECTED, - isConnecting: state.type === ConnectionStateType.CONNECTING, - isReconnecting: state.type === ConnectionStateType.RECONNECTING, - isDisconnected: state.type === ConnectionStateType.DISCONNECTED, - isError: state.type === ConnectionStateType.ERROR, - retryCount: state.retryCount, - error: state.error, - reconnect: () => sseService.connect(), - disconnect: () => sseService.disconnect(), - }; -} -``` - -#### 5. UI 集成示例 - -```typescript -// src/screens/message/components/ChatScreen/ConnectionStatusBadge.tsx - -export function ConnectionStatusBadge() { - const { status, isConnected, isReconnecting, isError, retryCount } = useConnectionState(); - - if (isConnected) { - return null; // 已连接不显示 - } - - return ( - - {isReconnecting && ( - - )} - - {isReconnecting - ? `连接中断,正在重连 (${retryCount}/20)` - : status} - - - ); -} -``` - -### 修改/新增文件列表 - -| 文件 | 操作 | 说明 | -|------|------|------| -| `src/services/connection/ConnectionState.ts` | 新增 | 连接状态定义 | -| `src/services/connection/ConnectionStateManager.ts` | 新增 | 连接状态管理器 | -| `src/services/connection/index.ts` | 新增 | 导出文件 | -| `src/services/sseService.ts` | 修改 | 集成状态管理器 | -| `src/hooks/useConnectionState.ts` | 新增 | 连接状态 Hook | -| `src/hooks/index.ts` | 修改 | 导出新 Hook | - -### Mermaid 状态图 - -```mermaid -stateDiagram-v2 - [*] --> IDLE - IDLE --> CONNECTING: 调用 connect() - CONNECTING --> CONNECTED: 连接成功 - CONNECTING --> ERROR: 连接失败 - CONNECTED --> RECONNECTING: 连接断开 - RECONNECTING --> CONNECTED: 重连成功 - RECONNECTING --> DISCONNECTED: 超过最大重试次数 - RECONNECTING --> ERROR: 重连失败 - ERROR --> RECONNECTING: 自动重连 - ERROR --> DISCONNECTED: 停止重连 - DISCONNECTED --> CONNECTING: 手动重连 - IDLE --> CONNECTING: 手动重连 - CONNECTED --> DISCONNECTED: 调用 disconnect() -``` - ---- - -## P1 差异更新 - -### 现有代码分析 - -#### 问题点 - -1. **`MessageManager` 消息更新机制** - - 每当有新消息时,通过 `setMessages` 更新整个消息数组 - - 大房间场景下,频繁的全量更新会导致 UI 卡顿 - - 没有区分不同类型的消息更新(新增、删除、修改) - -2. **`messageManagerHooks.ts` (第33-46行)** - ```typescript - const unsubscribe = messageManager.subscribe((event: MessageEvent) => { - switch (event.type) { - case 'conversations_updated': - setConversations(messageManager.getConversations()); - break; - // ... 其他都是全量更新 - } - }); - ``` - -3. **`useChatScreen.ts` (第148-162行)** - ```typescript - const messages = useMemo(() => { - return messageManagerMessages.map(m => ({ - // 全量映射 - })); - }, [messageManagerMessages]); - ``` - -4. **`ProcessMessageUseCase.ts` (第159-199行)** - ```typescript - private async handleNewMessage(message: any): Promise { - // 每条消息都触发完整流程 - this.notifySubscribers({ - type: 'message_received', - payload: { message, ... } - }); - } - ``` - -#### 性能瓶颈 - -- 1000人群聊,每秒10条消息 = 每秒10000次 UI 更新操作 -- 全量 `setMessages` 触发 FlatList 完整重渲染 -- 没有虚拟列表优化 - -### 实现方案 - -#### 1. 定义消息更新类型 - -```typescript -// src/stores/message/MessageUpdateTypes.ts - -export enum MessageUpdateType { - APPEND = 'append', // 追加新消息到末尾 - PREPEND = 'prepend', // 预置历史消息到开头 - UPDATE = 'update', // 更新单条消息 - DELETE = 'delete', // 删除单条消息 - BATCH_APPEND = 'batch_append', // 批量追加 - BATCH_PREPEND = 'batch_prepend', // 批量预置 - RECALL = 'recall', // 撤回消息 - CLEAR = 'clear', // 清空会话 -} - -export interface MessageUpdate { - type: MessageUpdateType; - conversationId: string; - payload: T; - timestamp: number; -} - -export interface AppendPayload { - messages: Message[]; -} - -export interface PrependPayload { - messages: Message[]; - hasMoreBefore: boolean; -} - -export interface UpdatePayload { - messageId: string; - updates: Partial; -} - -export interface DeletePayload { - messageId: string; -} - -export interface BatchAppendPayload { - messages: Message[]; -} - -export interface RecallPayload { - messageId: string; -} -``` - -#### 2. 创建差异更新 Hook - -```typescript -// src/hooks/useDifferentialMessages.ts - -export function useDifferentialMessages(conversationId: string | null) { - const [messages, setMessages] = useState([]); - const pendingUpdatesRef = useRef([]); - const flushScheduledRef = useRef(false); - - // 批量处理更新 - const flushUpdates = useCallback(() => { - if (pendingUpdatesRef.current.length === 0) return; - - const updates = pendingUpdatesRef.current; - pendingUpdatesRef.current = []; - flushScheduledRef.current = false; - - setMessages(currentMessages => { - const newMessages = [...currentMessages]; - - for (const update of updates) { - switch (update.type) { - case MessageUpdateType.APPEND: - for (const msg of update.payload.messages) { - if (!newMessages.find(m => m.id === msg.id)) { - newMessages.push(msg); - } - } - break; - - case MessageUpdateType.PREPEND: - const prependMessages = update.payload.messages - .filter(m => !newMessages.find(n => n.id === m.id)); - newMessages.unshift(...prependMessages.reverse()); - break; - - case MessageUpdateType.UPDATE: - const updateIdx = newMessages.findIndex(m => m.id === update.payload.messageId); - if (updateIdx !== -1) { - newMessages[updateIdx] = { - ...newMessages[updateIdx], - ...update.payload.updates - }; - } - break; - - case MessageUpdateType.DELETE: - newMessages = newMessages.filter(m => m.id !== update.payload.messageId); - break; - - case MessageUpdateType.RECALL: - const recallIdx = newMessages.findIndex(m => m.id === update.payload.messageId); - if (recallIdx !== -1) { - newMessages[recallIdx] = { - ...newMessages[recallIdx], - status: 'recalled', - segments: [], - }; - } - break; - - case MessageUpdateType.BATCH_APPEND: - for (const msg of update.payload.messages) { - if (!newMessages.find(m => m.id === msg.id)) { - newMessages.push(msg); - } - } - break; - } - } - - return newMessages; - }); - }, []); - - // 调度批量更新(16ms 内合并) - const scheduleFlush = useCallback(() => { - if (flushScheduledRef.current) return; - flushScheduledRef.current = true; - requestAnimationFrame(flushUpdates); - }, [flushUpdates]); - - // 处理消息更新 - const handleMessageUpdate = useCallback((update: MessageUpdate) => { - pendingUpdatesRef.current.push(update); - scheduleFlush(); - }, [scheduleFlush]); - - // 订阅 MessageManager - useEffect(() => { - if (!conversationId) return; - - const unsubscribe = messageManager.subscribe((event) => { - if (event.type === 'message_received') { - const { message, isCurrentUser } = event.payload; - handleMessageUpdate({ - type: isCurrentUser - ? MessageUpdateType.APPEND - : MessageUpdateType.PREPEND, - conversationId, - payload: { messages: [message] }, - timestamp: Date.now(), - }); - } - - if (event.type === 'messages_loaded') { - const { messages, direction } = event.payload; - handleMessageUpdate({ - type: direction === 'before' - ? MessageUpdateType.PREPEND - : MessageUpdateType.BATCH_APPEND, - conversationId, - payload: { messages }, - timestamp: Date.now(), - }); - } - - if (event.type === 'message_recalled') { - handleMessageUpdate({ - type: MessageUpdateType.RECALL, - conversationId, - payload: { messageId: event.payload.messageId }, - timestamp: Date.now(), - }); - } - }); - - return unsubscribe; - }, [conversationId, handleMessageUpdate]); - - return { messages }; -} -``` - -#### 3. 优化 FlatList 渲染 - -```typescript -// src/screens/message/components/ChatScreen/OptimizedMessageList.tsx - -export function OptimizedMessageList({ - messages, - onLoadMore, - hasMore, -}: { - messages: Message[]; - onLoadMore: () => void; - hasMore: boolean; -}) { - const viewabilityConfig = useRef({ - itemVisiblePercentThreshold: 50, - minimumViewTime: 100, - }); - - // 关键消息路径优化 - const keyExtractor = useCallback((item: Message) => item.id, []); - - const getItemLayout = useCallback((data: Message[] | null, index: number) => { - // 估算每条消息高度(可以根据类型调整) - const BASE_HEIGHT = 60; - const IMAGE_EXTRA = 200; - const EXTRA_PER_SEGMENT = 30; - - let length = BASE_HEIGHT; - if (data) { - const message = data[index]; - if (message.segments) { - length += message.segments.length * EXTRA_PER_SEGMENT; - } - if (message.segments?.some(s => s.type === 'image')) { - length += IMAGE_EXTRA; - } - } - - return { - length, - offset: data.slice(0, index).reduce((sum, m) => { - // 计算累计高度 - return sum + BASE_HEIGHT + - (m.segments?.length || 0) * EXTRA_PER_SEGMENT + - (m.segments?.some(s => s.type === 'image') ? IMAGE_EXTRA : 0); - }, 0), - index, - }; - }, []); - - // 消息类型判断 - const isSameDay = useCallback((prev: Message, next: Message) => { - const prevDate = new Date(prev.created_at).toDateString(); - const nextDate = new Date(next.created_at).toDateString(); - return prevDate === nextDate; - }, []); - - const renderItem = useCallback(({ item, index }: { item: Message; index: number }) => { - const prevItem = messages[index - 1]; - - return ( - 50} - /> - ); - }, [isSameDay]); - - return ( - - ); -} -``` - -#### 4. 修改 MessageManager 支持差异事件 - -```typescript -// src/stores/message/MessageManager.ts - -class MessageManager { - // 发送差异更新事件 - private emitMessageUpdate(update: MessageUpdate): void { - this.subscribers.forEach(subscriber => { - if (subscriber.types.includes(update.type) || subscriber.types.includes('*')) { - subscriber.callback(update); - } - }); - } - - async handleIncomingMessage(message: WSChatMessage): Promise { - // 检查消息是否已存在 - const existingMessages = this.messages.get(message.conversation_id) || []; - if (existingMessages.find(m => m.id === message.id)) { - return; // 避免重复 - } - - const newMessage = this.createMessageObject(message); - - // 直接发送差异更新,而不是全量更新 - this.emitMessageUpdate({ - type: MessageUpdateType.APPEND, - conversationId: message.conversation_id, - payload: { messages: [newMessage] }, - timestamp: Date.now(), - }); - - // 异步保存到数据库 - this.persistMessage(newMessage); - } -} -``` - -### 修改/新增文件列表 - -| 文件 | 操作 | 说明 | -|------|------|------| -| `src/stores/message/MessageUpdateTypes.ts` | 新增 | 消息更新类型定义 | -| `src/hooks/useDifferentialMessages.ts` | 新增 | 差异更新 Hook | -| `src/stores/messageManagerHooks.ts` | 修改 | 集成差异更新 | -| `src/stores/message/MessageManager.ts` | 修改 | 添加差异事件支持 | -| `src/screens/message/components/ChatScreen/OptimizedMessageList.tsx` | 新增 | 优化的消息列表组件 | - -### 性能收益 - -| 场景 | 优化前 | 优化后 | -|------|--------|--------| -| 1000人群,每秒10条消息 | 10000次/秒 UI 更新 | 60次/秒 UI 更新 | -| 滚动加载100条历史消息 | 全量重渲染100条 | 仅渲染可见区域~20条 | -| 消息撤回 | 全量更新 + UI 重建 | 单条更新 | - ---- - -## P1 媒体缓存清理 - -### 现有代码分析 - -#### 问题点 - -1. **`CacheDataSource.ts` (第14-28行)** - ```typescript - export class CacheDataSource implements ICacheDataSource { - private memoryCache: Map> = new Map(); - private maxSize: number; // 默认 100 条 - private defaultTtl: number; // 默认 5 分钟 - - // 没有媒体文件专用缓存管理 - // 没有磁盘空间监控 - } - ``` - -2. **缺少媒体缓存机制** - - 图片、视频、音频没有独立的缓存策略 - - 没有按会话清理过期媒体的功能 - - 没有用户手动清理入口 - -3. **`LocalDataSource.ts` (第73-86行)** - ```typescript - // 消息表结构 - `CREATE TABLE IF NOT EXISTS messages ( - id TEXT PRIMARY KEY NOT NULL, - conversationId TEXT NOT NULL, - segments TEXT - // 没有存储媒体文件路径 - )` - ``` - - 消息表没有记录媒体缓存路径 - - 无法追踪哪些媒体文件属于哪个会话 - -#### 存储风险 - -- 图片缓存:无限制增长 -- 视频缓存:无限制增长 -- 音频缓存:无限制增长 -- 总计:可能导致存储空间耗尽 - -### 实现方案 - -#### 1. 定义媒体缓存配置 - -```typescript -// src/services/media/MediaCacheConfig.ts - -export interface MediaCacheConfig { - // 图片配置 - image: { - maxMemoryCacheSize: number; // 内存缓存数量,默认 50 - maxDiskCacheSize: number; // 磁盘缓存大小(MB),默认 500 - maxAge: number; // 缓存有效期(小时),默认 168 (7天) - maxAgeForConversation: number; // 会话内媒体保留时间(天),默认 30 - }; - - // 视频配置 - video: { - maxDiskCacheSize: number; // 磁盘缓存大小(MB),默认 1000 - maxAge: number; // 缓存有效期(小时),默认 72 (3天) - autoPreload: boolean; // 是否自动预加载,默认 false - }; - - // 音频配置 - audio: { - maxDiskCacheSize: number; // 磁盘缓存大小(MB),默认 200 - maxAge: number; // 缓存有效期(天),默认 7 - }; - - // 全局配置 - global: { - checkOnStartup: boolean; // 启动时检查清理,默认 true - checkInterval: number; // 定期检查间隔(小时),默认 24 - lowStorageThreshold: number; // 低存储空间阈值(MB),默认 500 - }; -} - -export const DEFAULT_MEDIA_CACHE_CONFIG: MediaCacheConfig = { - image: { - maxMemoryCacheSize: 50, - maxDiskCacheSize: 500, - maxAge: 168, // 7 days - maxAgeForConversation: 30, - }, - video: { - maxDiskCacheSize: 1000, - maxAge: 72, // 3 days - autoPreload: false, - }, - audio: { - maxDiskCacheSize: 200, - maxAge: 168, // 7 days - }, - global: { - checkOnStartup: true, - checkInterval: 24, - lowStorageThreshold: 500, - }, -}; -``` - -#### 2. 创建媒体缓存管理器 - -```typescript -// src/services/media/MediaCacheManager.ts - -import * as FileSystem from 'expo-file-system'; -import { cacheDataSource } from '../data/datasources/CacheDataSource'; - -export enum MediaType { - IMAGE = 'image', - VIDEO = 'video', - AUDIO = 'audio', -} - -export interface MediaCacheInfo { - type: MediaType; - uri: string; - localPath: string; - conversationId?: string; - messageId?: string; - size: number; - createdAt: number; - lastAccessedAt: number; -} - -class MediaCacheManager { - private config: MediaCacheConfig; - private cacheDirectory: string; - private mediaRecords: Map = new Map(); - - // 缓存目录 - private readonly CACHE_DIR = `${FileSystem.documentDirectory}media_cache/`; - private readonly IMAGE_DIR = `${this.CACHE_DIR}images/`; - private readonly VIDEO_DIR = `${this.CACHE_DIR}videos/`; - private readonly AUDIO_DIR = `${this.CACHE_DIR}audios/`; - - constructor(config: MediaCacheConfig = DEFAULT_MEDIA_CACHE_CONFIG) { - this.config = config; - this.cacheDirectory = this.CACHE_DIR; - } - - // 初始化 - async initialize(): Promise { - await this.ensureDirectories(); - await this.loadMediaRecords(); - await this.cleanupIfNeeded(); - } - - // 确保缓存目录存在 - private async ensureDirectories(): Promise { - await FileSystem.makeDirectoryAsync(this.IMAGE_DIR, { intermediates: true }); - await FileSystem.makeDirectoryAsync(this.VIDEO_DIR, { intermediates: true }); - await FileSystem.makeDirectoryAsync(this.AUDIO_DIR, { intermediates: true }); - } - - // 缓存媒体文件 - async cacheMedia( - uri: string, - type: MediaType, - options: { - conversationId?: string; - messageId?: string; - } = {} - ): Promise { - const { conversationId, messageId } = options; - - // 生成唯一文件名 - const filename = this.generateFilename(uri, type); - const localPath = this.getMediaPath(type, filename); - - // 检查是否已缓存 - if (await this.exists(localPath)) { - await this.updateAccessTime(localPath); - return localPath; - } - - try { - // 下载文件 - const downloadResult = await FileSystem.downloadAsync(uri, localPath); - - // 记录缓存信息 - const cacheInfo: MediaCacheInfo = { - type, - uri, - localPath, - conversationId, - messageId, - size: downloadResult.headers['content-length'] - ? parseInt(downloadResult.headers['content-length']) - : 0, - createdAt: Date.now(), - lastAccessedAt: Date.now(), - }; - - await this.saveMediaRecord(localPath, cacheInfo); - await this.cleanupIfNeeded(); - - return localPath; - } catch (error) { - console.error('[MediaCacheManager] 缓存失败:', error); - throw error; - } - } - - // 获取缓存的媒体文件路径 - async getCachedMedia(uri: string, type: MediaType): Promise { - const record = this.findByUri(uri); - if (record && await this.exists(record.localPath)) { - await this.updateAccessTime(record.localPath); - return record.localPath; - } - return null; - } - - // 删除单个会话的所有媒体缓存 - async clearConversationMedia(conversationId: string): Promise { - const toDelete: string[] = []; - - for (const [path, info] of this.mediaRecords) { - if (info.conversationId === conversationId) { - toDelete.push(path); - } - } - - await Promise.all(toDelete.map(p => this.deleteMedia(p))); - } - - // 删除过期媒体 - async clearExpiredMedia(): Promise { - const now = Date.now(); - const maxAge = this.config.image.maxAge * 60 * 60 * 1000; // 转换为毫秒 - let deletedCount = 0; - - for (const [path, info] of this.mediaRecords) { - if (now - info.lastAccessedAt > maxAge) { - await this.deleteMedia(path); - deletedCount++; - } - } - - return deletedCount; - } - - // 清理超过会话保留期的媒体 - async clearOldConversationMedia(): Promise { - const now = Date.now(); - const maxAge = this.config.image.maxAgeForConversation * 24 * 60 * 60 * 1000; - let deletedCount = 0; - - for (const [path, info] of this.mediaRecords) { - if (info.conversationId && now - info.createdAt > maxAge) { - await this.deleteMedia(path); - deletedCount++; - } - } - - return deletedCount; - } - - // 按大小清理(LRU) - async clearBySizeLimit(): Promise { - const totalSize = await this.getTotalCacheSize(); - const limit = this.config.image.maxDiskCacheSize * 1024 * 1024; - - if (totalSize <= limit) { - return 0; - } - - // 按最后访问时间排序 - const sorted = Array.from(this.mediaRecords.entries()) - .sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt); - - let currentSize = totalSize; - let deletedCount = 0; - - for (const [path, info] of sorted) { - if (currentSize <= limit) break; - - await this.deleteMedia(path); - currentSize -= info.size; - deletedCount++; - } - - return deletedCount; - } - - // 获取缓存统计 - async getCacheStats(): Promise<{ - totalSize: number; - imageCount: number; - videoCount: number; - audioCount: number; - oldestItem: number; - newestItem: number; - }> { - const stats = { - totalSize: 0, - imageCount: 0, - videoCount: 0, - audioCount: 0, - oldestItem: Date.now(), - newestItem: 0, - }; - - for (const info of this.mediaRecords.values()) { - stats.totalSize += info.size; - stats.oldestItem = Math.min(stats.oldestItem, info.createdAt); - stats.newestItem = Math.max(stats.newestItem, info.createdAt); - - switch (info.type) { - case MediaType.IMAGE: stats.imageCount++; break; - case MediaType.VIDEO: stats.videoCount++; break; - case MediaType.AUDIO: stats.audioCount++; break; - } - } - - return stats; - } - - // 清理入口 - async cleanupIfNeeded(): Promise { - await this.clearExpiredMedia(); - await this.clearBySizeLimit(); - } - - // 私有辅助方法 - private generateFilename(uri: string, type: MediaType): string { - const ext = this.getExtension(uri, type); - const hash = this.hashString(uri); - return `${type}_${hash}_${Date.now()}.${ext}`; - } - - private getExtension(uri: string, type: MediaType): string { - if (type === MediaType.IMAGE) { - if (uri.includes('.gif')) return 'gif'; - if (uri.includes('.webp')) return 'webp'; - return 'jpg'; - } - if (type === MediaType.VIDEO) return 'mp4'; - if (type === MediaType.AUDIO) return 'mp3'; - return 'bin'; - } - - private getMediaPath(type: MediaType, filename: string): string { - switch (type) { - case MediaType.IMAGE: return `${this.IMAGE_DIR}${filename}`; - case MediaType.VIDEO: return `${this.VIDEO_DIR}${filename}`; - case MediaType.AUDIO: return `${this.AUDIO_DIR}${filename}`; - } - } - - private async exists(path: string): Promise { - const info = await FileSystem.getInfoAsync(path); - return info.exists; - } - - private async deleteMedia(path: string): Promise { - try { - await FileSystem.deleteAsync(path, { idempotent: true }); - this.mediaRecords.delete(path); - await this.removeMediaRecord(path); - } catch (error) { - console.warn('[MediaCacheManager] 删除失败:', path, error); - } - } - - private async saveMediaRecord(path: string, info: MediaCacheInfo): Promise { - this.mediaRecords.set(path, info); - await cacheDataSource.set(`media_${path}`, info, null); - } - - private async loadMediaRecords(): Promise { - // 从缓存数据源加载 - } - - private async updateAccessTime(path: string): Promise { - const info = this.mediaRecords.get(path); - if (info) { - info.lastAccessedAt = Date.now(); - await this.saveMediaRecord(path, info); - } - } - - private findByUri(uri: string): MediaCacheInfo | undefined { - for (const info of this.mediaRecords.values()) { - if (info.uri === uri) { - return info; - } - } - return undefined; - } - - private async getTotalCacheSize(): Promise { - let total = 0; - for (const info of this.mediaRecords.values()) { - total += info.size; - } - return total; - } - - private hashString(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - return Math.abs(hash).toString(16); - } -} - -export const mediaCacheManager = new MediaCacheManager(); -``` - -#### 3. 创建清理策略 - -```typescript -// src/services/media/MediaCleanupPolicy.ts - -export enum CleanupTrigger { - STARTUP = 'startup', - SCHEDULED = 'scheduled', - MANUAL = 'manual', - LOW_STORAGE = 'low_storage', - CONVERSATION_DELETED = 'conversation_deleted', -} - -export interface CleanupResult { - trigger: CleanupTrigger; - timestamp: number; - deletedCount: number; - freedSize: number; - errors: string[]; -} - -class MediaCleanupPolicy { - private lastCleanup: number = 0; - private config: MediaCacheConfig; - - // 启动时清理 - async cleanupOnStartup(): Promise { - const errors: string[] = []; - let deletedCount = 0; - let freedSize = 0; - - try { - // 清理过期文件 - const expired = await mediaCacheManager.clearExpiredMedia(); - deletedCount += expired; - - // 清理超过保留期的会话媒体 - const oldConversation = await mediaCacheManager.clearOldConversationMedia(); - deletedCount += oldConversation; - - // 超过大小限制时清理 - const bySize = await mediaCacheManager.clearBySizeLimit(); - deletedCount += bySize; - - this.lastCleanup = Date.now(); - } catch (error) { - errors.push(`Startup cleanup failed: ${error}`); - } - - return { - trigger: CleanupTrigger.STARTUP, - timestamp: Date.now(), - deletedCount, - freedSize, - errors, - }; - } - - // 检查是否需要定期清理 - shouldRunScheduledCleanup(): boolean { - const interval = this.config.global.checkInterval * 60 * 60 * 1000; - return Date.now() - this.lastCleanup > interval; - } - - // 会话删除时清理 - async cleanupOnConversationDeleted(conversationId: string): Promise { - const errors: string[] = []; - let deletedCount = 0; - let freedSize = 0; - - try { - const statsBefore = await mediaCacheManager.getCacheStats(); - await mediaCacheManager.clearConversationMedia(conversationId); - const statsAfter = await mediaCacheManager.getCacheStats(); - - deletedCount = statsBefore.imageCount + statsBefore.videoCount + statsBefore.audioCount - - (statsAfter.imageCount + statsAfter.videoCount + statsAfter.audioCount); - freedSize = statsBefore.totalSize - statsAfter.totalSize; - } catch (error) { - errors.push(`Conversation cleanup failed: ${error}`); - } - - return { - trigger: CleanupTrigger.CONVERSATION_DELETED, - timestamp: Date.now(), - deletedCount, - freedSize, - errors, - }; - } -} - -export const mediaCleanupPolicy = new MediaCleanupPolicy(); -``` - -#### 4. 创建清理 Hook - -```typescript -// src/hooks/useMediaCache.ts - -export function useMediaCache() { - const [stats, setStats] = useState(null); - const [isClearing, setIsClearing] = useState(false); - - // 加载缓存统计 - const loadStats = useCallback(async () => { - const cacheStats = await mediaCacheManager.getCacheStats(); - setStats(cacheStats); - }, []); - - // 手动清理 - const clearAll = useCallback(async () => { - setIsClearing(true); - try { - await mediaCacheManager.clearExpiredMedia(); - await mediaCacheManager.clearBySizeLimit(); - await loadStats(); - } finally { - setIsClearing(false); - } - }, [loadStats]); - - // 清理指定会话的媒体 - const clearConversation = useCallback(async (conversationId: string) => { - await mediaCacheManager.clearConversationMedia(conversationId); - await loadStats(); - }, [loadStats]); - - // 组件挂载时加载统计 - useEffect(() => { - loadStats(); - }, [loadStats]); - - return { - stats, - isClearing, - loadStats, - clearAll, - clearConversation, - formatSize: (bytes: number) => { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; - return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`; - }, - }; -} -``` - -### 修改/新增文件列表 - -| 文件 | 操作 | 说明 | -|------|------|------| -| `src/services/media/MediaCacheConfig.ts` | 新增 | 媒体缓存配置 | -| `src/services/media/MediaCacheManager.ts` | 新增 | 媒体缓存管理器 | -| `src/services/media/MediaCleanupPolicy.ts` | 新增 | 清理策略 | -| `src/services/media/index.ts` | 新增 | 导出文件 | -| `src/hooks/useMediaCache.ts` | 新增 | 媒体缓存 Hook | -| `src/hooks/index.ts` | 修改 | 导出新 Hook | -| `src/data/datasources/CacheDataSource.ts` | 修改 | 添加媒体记录存储 | - -### 清理策略 - -```mermaid -graph TD - A[启动 App] --> B{检查启动清理标志} - B -->|是| C[清理过期媒体] - B -->|是| D[清理超期会话媒体] - B -->|是| E[检查大小限制] - E -->|超过限制| F[LRU 清理] - - G[定期检查] -->|间隔到期| C - G -->|间隔到期| D - G -->|间隔到期| E - - H[用户操作] -->|删除会话| I[清理该会话媒体] - H -->|手动清理| J[清理所有过期媒体] - - K[存储空间检查] -->|低于阈值| L[紧急清理] -``` - ---- - -## 实现优先级与依赖关系 - -### 优先级矩阵 - -| 优先级 | 功能 | 依赖 | 复杂度 | -|--------|------|------|--------| -| P0 | 分页状态管理 | 无 | 中 | -| P0 | 同步状态机 | 无 | 低 | -| P1 | 差异更新 | P0 分页状态管理 | 高 | -| P1 | 媒体缓存清理 | 无 | 中 | - -### 推荐实现顺序 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Phase 1: 基础改进 (1-2天) │ -│ ┌───────────────────┐ ┌───────────────────┐ │ -│ │ P0 分页状态管理 │ │ P0 同步状态机 │ │ -│ │ │ │ │ │ -│ │ - 避免重复加载 │ │ - 清晰状态反馈 │ │ -│ │ - 页面缓存 │ │ - 连接状态 Hook │ │ -│ │ - 预加载机制 │ │ - UI 状态指示器 │ │ -│ └───────────────────┘ └───────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Phase 2: 性能优化 (2-3天) │ -│ ┌─────────────────────────────────────────────────────────────┐│ -│ │ P1 差异更新 ││ -│ │ ││ -│ │ - 依赖分页状态管理 ││ -│ │ - 消息更新类型定义 ││ -│ │ - 差异更新 Hook ││ -│ │ - FlatList 渲染优化 ││ -│ └─────────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Phase 3: 存储优化 (1-2天) │ -│ ┌─────────────────────────────────────────────────────────────┐│ -│ │ P1 媒体缓存清理 ││ -│ │ ││ -│ │ - 媒体缓存管理器 ││ -│ │ - 清理策略 ││ -│ │ - 用户清理界面 ││ -│ └─────────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 风险与注意事项 - -1. **差异更新风险** - - 需要确保消息 ID 的唯一性 - - 并发更新时需要正确合并 - - 建议在开发环境进行大房间压力测试 - -2. **分页状态管理风险** - - 页面缓存会占用内存,需要设置上限 - - 分页状态需要正确序列化和恢复(跨会话) - -3. **状态机风险** - - 状态转换需要严格测试 - - 重连逻辑需要处理边界情况(如网络中断) - -4. **媒体缓存风险** - - 删除文件时需要确保没有正在使用 - - 缓存元数据需要持久化存储 - - 清理操作应该在后台线程执行 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4756a76..586d224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "date-fns": "^4.1.0", "expo": "~55.0.4", "expo-background-fetch": "~55.0.9", + "expo-camera": "^55.0.10", "expo-constants": "~55.0.7", "expo-dev-client": "~55.0.10", "expo-file-system": "~55.0.10", @@ -112,15 +113,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.29.1", "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz", @@ -165,15 +157,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", @@ -195,15 +178,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.28.5", "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", @@ -221,19 +195,10 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.6", - "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", - "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "version": "0.6.8", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -408,22 +373,22 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -1278,15 +1243,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", @@ -1423,9 +1379,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1530,9 +1486,9 @@ } }, "node_modules/@expo-google-fonts/material-symbols": { - "version": "0.4.25", - "resolved": "https://registry.npmmirror.com/@expo-google-fonts/material-symbols/-/material-symbols-0.4.25.tgz", - "integrity": "sha512-MlwOpcYPLYu2+aDAwqv29l3sknNNxA36Jcu07Tg9+MTEvXk2SPcO8eQmwwDeVBbv5Wb6ToD1LmE+e0lLv/9WvA==", + "version": "0.4.27", + "resolved": "https://registry.npmmirror.com/@expo-google-fonts/material-symbols/-/material-symbols-0.4.27.tgz", + "integrity": "sha512-cnb3DZnWUWpezGFkJ8y4MT5f/lw6FcgDzeJzic+T+vpQHLHG1cg3SC3i1w1i8Bk4xKR4HPY3t9iIRNvtr5ml8A==", "license": "MIT AND Apache-2.0" }, "node_modules/@expo/code-signing-certificates": { @@ -1545,15 +1501,15 @@ } }, "node_modules/@expo/config": { - "version": "55.0.8", - "resolved": "https://registry.npmmirror.com/@expo/config/-/config-55.0.8.tgz", - "integrity": "sha512-D7RYYHfErCgEllGxNwdYdkgzLna7zkzUECBV3snbUpf7RvIpB5l1LpCgzuVoc5KVew5h7N1Tn4LnT/tBSUZsQg==", + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/@expo/config/-/config-55.0.10.tgz", + "integrity": "sha512-qCHxo9H1ZoeW+y0QeMtVZ3JfGmumpGrgUFX60wLWMarraoQZSe47ZUm9kJSn3iyoPjUtUNanO3eXQg+K8k4rag==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~55.0.6", + "@expo/config-plugins": "~55.0.7", "@expo/config-types": "^55.0.5", "@expo/json-file": "^10.0.12", - "@expo/require-utils": "^55.0.2", + "@expo/require-utils": "^55.0.3", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", @@ -1564,9 +1520,9 @@ } }, "node_modules/@expo/config-plugins": { - "version": "55.0.6", - "resolved": "https://registry.npmmirror.com/@expo/config-plugins/-/config-plugins-55.0.6.tgz", - "integrity": "sha512-cIox6FjZlFaaX40rbQ3DvP9e87S5X85H9uw+BAxJE5timkMhuByy3GAlOsj1h96EyzSiol7Q6YIGgY1Jiz4M+A==", + "version": "55.0.7", + "resolved": "https://registry.npmmirror.com/@expo/config-plugins/-/config-plugins-55.0.7.tgz", + "integrity": "sha512-XZUoDWrsHEkH3yasnDSJABM/UxP5a1ixzRwU/M+BToyn/f0nTrSJJe/Ay/FpxkI4JSNz2n0e06I23b2bleXKVA==", "license": "MIT", "dependencies": { "@expo/config-types": "^55.0.5", @@ -1584,12 +1540,36 @@ "xml2js": "0.6.0" } }, + "node_modules/@expo/config-plugins/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/config-types": { "version": "55.0.5", "resolved": "https://registry.npmmirror.com/@expo/config-types/-/config-types-55.0.5.tgz", "integrity": "sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==", "license": "MIT" }, + "node_modules/@expo/config/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/devcert": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/@expo/devcert/-/devcert-1.2.1.tgz", @@ -1656,9 +1636,9 @@ } }, "node_modules/@expo/fingerprint": { - "version": "0.16.5", - "resolved": "https://registry.npmmirror.com/@expo/fingerprint/-/fingerprint-0.16.5.tgz", - "integrity": "sha512-mLrcymtgkW9IJ/G1e8MH1Xt2VIb1MOS86ePY0ePcnV3nVyJqm7gfa/AXD1Hk+eZXvf8XhioYz6QZaamBdEzR3A==", + "version": "0.16.6", + "resolved": "https://registry.npmmirror.com/@expo/fingerprint/-/fingerprint-0.16.6.tgz", + "integrity": "sha512-nRITNbnu3RKSHPvKVehrSU4KG2VY9V8nvULOHBw98ukHCAU4bGrU5APvcblOkX3JAap+xEHsg/mZvqlvkLInmQ==", "license": "MIT", "dependencies": { "@expo/env": "^2.0.11", @@ -1677,6 +1657,18 @@ "fingerprint": "bin/cli.js" } }, + "node_modules/@expo/fingerprint/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/image-utils": { "version": "0.8.12", "resolved": "https://registry.npmmirror.com/@expo/image-utils/-/image-utils-0.8.12.tgz", @@ -1692,6 +1684,18 @@ "semver": "^7.6.0" } }, + "node_modules/@expo/image-utils/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/json-file": { "version": "10.0.12", "resolved": "https://registry.npmmirror.com/@expo/json-file/-/json-file-10.0.12.tgz", @@ -1703,12 +1707,12 @@ } }, "node_modules/@expo/local-build-cache-provider": { - "version": "55.0.6", - "resolved": "https://registry.npmmirror.com/@expo/local-build-cache-provider/-/local-build-cache-provider-55.0.6.tgz", - "integrity": "sha512-4kfdv48sKzokijMqi07fINYA9/XprshmPgSLf8i69XgzIv2YdRyBbb70SzrufB7PDneFoltz8N83icW8gOOj1g==", + "version": "55.0.7", + "resolved": "https://registry.npmmirror.com/@expo/local-build-cache-provider/-/local-build-cache-provider-55.0.7.tgz", + "integrity": "sha512-Qg9uNZn1buv4zJUA4ZQaz+ZnKDCipRgjoEg2Gcp8Qfy+2Gq5yZKX4YN1TThCJ01LJk/pvJsCRxXlXZSwdZppgg==", "license": "MIT", "dependencies": { - "@expo/config": "~55.0.8", + "@expo/config": "~55.0.10", "chalk": "^4.1.2" } }, @@ -1751,6 +1755,41 @@ "metro-transform-worker": "0.83.3" } }, + "node_modules/@expo/metro-config": { + "version": "55.0.11", + "resolved": "https://registry.npmmirror.com/@expo/metro-config/-/metro-config-55.0.11.tgz", + "integrity": "sha512-qGxq7RwWpj0zNvZO/e5aizKrOKYYBrVPShSbxPOVB1EXcexxTPTxnOe4pYFg/gKkLIJe0t3jSSF8IDWlGdaaOg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~55.0.10", + "@expo/env": "~2.1.1", + "@expo/json-file": "~10.0.12", + "@expo/metro": "~54.2.0", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.32.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, "node_modules/@expo/metro-runtime": { "version": "55.0.6", "resolved": "https://registry.npmmirror.com/@expo/metro-runtime/-/metro-runtime-55.0.6.tgz", @@ -1801,6 +1840,166 @@ "resolve-workspace-root": "^2.0.0" } }, + "node_modules/@expo/package-manager/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@expo/package-manager/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@expo/package-manager/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@expo/package-manager/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@expo/package-manager/node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@expo/package-manager/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@expo/plist": { "version": "0.5.2", "resolved": "https://registry.npmmirror.com/@expo/plist/-/plist-0.5.2.tgz", @@ -1812,10 +2011,43 @@ "xmlbuilder": "^15.1.1" } }, + "node_modules/@expo/prebuild-config": { + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/@expo/prebuild-config/-/prebuild-config-55.0.10.tgz", + "integrity": "sha512-AMylDld5G7YJGfEhEyXtgWRuBB83802QBoewF1vJ6NMDtufukuPhMJzOs9E4UXNsjLTaQcgT4yTWhsAWl7o1AQ==", + "license": "MIT", + "dependencies": { + "@expo/config": "~55.0.10", + "@expo/config-plugins": "~55.0.7", + "@expo/config-types": "^55.0.5", + "@expo/image-utils": "^0.8.12", + "@expo/json-file": "^10.0.12", + "@react-native/normalize-colors": "0.83.2", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/@expo/prebuild-config/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/require-utils": { - "version": "55.0.2", - "resolved": "https://registry.npmmirror.com/@expo/require-utils/-/require-utils-55.0.2.tgz", - "integrity": "sha512-dV5oCShQ1umKBKagMMT4B/N+SREsQe3lU4Zgmko5AO0rxKV0tynZT6xXs+e2JxuqT4Rz997atg7pki0BnZb4uw==", + "version": "55.0.3", + "resolved": "https://registry.npmmirror.com/@expo/require-utils/-/require-utils-55.0.3.tgz", + "integrity": "sha512-TS1m5tW45q4zoaTlt6DwmdYHxvFTIxoLrTHKOFrIirHIqIXnHCzpceg8wumiBi+ZXSaGY2gobTbfv+WVhJY6Fw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -1892,24 +2124,6 @@ "excpretty": "build/cli.js" } }, - "node_modules/@expo/xcpretty/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/@expo/xcpretty/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -1952,6 +2166,15 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", @@ -1961,6 +2184,71 @@ "node": ">=6" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2769,98 +3057,17 @@ "yaml": "^2.2.1" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "node_modules/@react-native-community/cli-doctor/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" } }, "node_modules/@react-native-community/cli-platform-android": { @@ -2921,39 +3128,6 @@ "ws": "^6.2.3" } }, - "node_modules/@react-native-community/cli-server-api/node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-native-community/cli-server-api/node_modules/open": { - "version": "6.4.0", - "resolved": "https://registry.npmmirror.com/open/-/open-6.4.0.tgz", - "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli-server-api/node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, "node_modules/@react-native-community/cli-tools": { "version": "20.1.2", "resolved": "https://registry.npmmirror.com/@react-native-community/cli-tools/-/cli-tools-20.1.2.tgz", @@ -2973,176 +3147,17 @@ "semver": "^7.5.2" } }, - "node_modules/@react-native-community/cli-tools/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "node_modules/@react-native-community/cli-tools/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "devOptional": true, - "license": "MIT", + "license": "ISC", "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "semver": "bin/semver.js" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-tools/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" } }, "node_modules/@react-native-community/cli-types": { @@ -3155,79 +3170,17 @@ "joi": "^17.2.1" } }, - "node_modules/@react-native-community/cli/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "node_modules/@react-native-community/cli/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/@react-native-community/cli/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@react-native/assets-registry": { @@ -3311,6 +3264,30 @@ "@babel/core": "*" } }, + "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", + "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.32.0" + } + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, "node_modules/@react-native/codegen": { "version": "0.83.2", "resolved": "https://registry.npmmirror.com/@react-native/codegen/-/codegen-0.83.2.tgz", @@ -3369,6 +3346,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@react-native/codegen/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/@react-native/codegen/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, "node_modules/@react-native/codegen/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", @@ -3411,6 +3403,18 @@ } } }, + "node_modules/@react-native/community-cli-plugin/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@react-native/debugger-frontend": { "version": "0.83.2", "resolved": "https://registry.npmmirror.com/@react-native/debugger-frontend/-/debugger-frontend-0.83.2.tgz", @@ -3456,6 +3460,55 @@ "node": ">= 20.19.4" } }, + "node_modules/@react-native/dev-middleware/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmmirror.com/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@react-native/gradle-plugin": { "version": "0.83.2", "resolved": "https://registry.npmmirror.com/@react-native/gradle-plugin/-/gradle-plugin-0.83.2.tgz", @@ -3481,17 +3534,17 @@ "license": "MIT" }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.15.2", - "resolved": "https://registry.npmmirror.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.2.tgz", - "integrity": "sha512-xaSumZWE97P3j33guO7bh5dJ5IqR1bWiT+i17SUjsXxoI9xnNXWDm4dkTjzGuuT0BHcUVkzei0tjjCQmNg9cIQ==", + "version": "7.15.6", + "resolved": "https://registry.npmmirror.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.6.tgz", + "integrity": "sha512-olB+s0ApMzWN9t5Bk5Mj6ntSlVRz3B8v+1LtwGS/29lyC311G5es0kgxyzpGKE9gy6Ef8W526QH5cIka2jh0kQ==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.8", + "@react-navigation/elements": "^2.9.11", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { - "@react-navigation/native": "^7.1.31", + "@react-navigation/native": "^7.1.34", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3499,9 +3552,9 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.15.1", - "resolved": "https://registry.npmmirror.com/@react-navigation/core/-/core-7.15.1.tgz", - "integrity": "sha512-Fqr6qxfZJIC4ewho7LtTa9zz6hcOzohX7D1lcDfrkGaYkS5xBwEZViGNxCJK/czUc74ua8NThyrObQFjB6Q/RQ==", + "version": "7.16.2", + "resolved": "https://registry.npmmirror.com/@react-navigation/core/-/core-7.16.2.tgz", + "integrity": "sha512-0dbCC2aTjNW7MvG1fY7zeq6eYvmmaFCEnBDXPuMPJ8uKgfs9lFGXIQFIfBdmcBVX6vHhS+K213VCsuHSIv5jYw==", "license": "MIT", "dependencies": { "@react-navigation/routers": "^7.5.3", @@ -3517,16 +3570,10 @@ "react": ">= 18.2.0" } }, - "node_modules/@react-navigation/core/node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", - "license": "MIT" - }, "node_modules/@react-navigation/elements": { - "version": "2.9.8", - "resolved": "https://registry.npmmirror.com/@react-navigation/elements/-/elements-2.9.8.tgz", - "integrity": "sha512-3gpwUmVnDJYvK9nFmAA/YXw0hmT/C/lZx8RkRMK+ux9l1T+32EWnQFnn34Wa1BMDX8HN2r64yrlW93DIzKI7Uw==", + "version": "2.9.11", + "resolved": "https://registry.npmmirror.com/@react-navigation/elements/-/elements-2.9.11.tgz", + "integrity": "sha512-O5KiwaVCcEVuqZgQ77xiBFSl1sha77rNMTFlLWYnom33ZHPDarV3bM9WNyVnMZxU8ZVTi02X3+ZhO0fSn5QYyg==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -3535,7 +3582,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.1.31", + "@react-navigation/native": "^7.1.34", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -3547,17 +3594,17 @@ } }, "node_modules/@react-navigation/material-top-tabs": { - "version": "7.4.16", - "resolved": "https://registry.npmmirror.com/@react-navigation/material-top-tabs/-/material-top-tabs-7.4.16.tgz", - "integrity": "sha512-Wy/NP17O948/4uUvzpPs5FkvhOCvojBJ320FxW5kUiUbaxde11LUJf9yhDJhScQ7pjHeT0NsMSc1uhbF8O1oFg==", + "version": "7.4.20", + "resolved": "https://registry.npmmirror.com/@react-navigation/material-top-tabs/-/material-top-tabs-7.4.20.tgz", + "integrity": "sha512-a4QToiT0gIfqjG8J/OaCSuEovFUfMyOo4VOMt2haHsymnM9cMXR7UQL4j7MsUOhaPo1wGsJ9QazeA4TcVJWRFA==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.8", + "@react-navigation/elements": "^2.9.11", "color": "^4.2.3", - "react-native-tab-view": "^4.2.2" + "react-native-tab-view": "^4.3.0" }, "peerDependencies": { - "@react-navigation/native": "^7.1.31", + "@react-navigation/native": "^7.1.34", "react": ">= 18.2.0", "react-native": "*", "react-native-pager-view": ">= 6.0.0", @@ -3565,12 +3612,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.1.31", - "resolved": "https://registry.npmmirror.com/@react-navigation/native/-/native-7.1.31.tgz", - "integrity": "sha512-+YCUwtfDgsux59Q0LDHc3Zid9ih93ecUCFWZOH6/+eNoUGnWx77wjS6ZfvBO/7E+EiIup11IVShDzCHR4of8hw==", + "version": "7.1.34", + "resolved": "https://registry.npmmirror.com/@react-navigation/native/-/native-7.1.34.tgz", + "integrity": "sha512-zzQ0mKAhLsjTIsaoLfILKZVMObJzE0F+bOi0hl2Glt+1Rd2GtaWJ1Z024c3yLmX+Oc79pqoCQLBXpyxtrZu9NQ==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.15.1", + "@react-navigation/core": "^7.16.2", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -3582,18 +3629,18 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.14.2", - "resolved": "https://registry.npmmirror.com/@react-navigation/native-stack/-/native-stack-7.14.2.tgz", - "integrity": "sha512-/nKxFAFSUSGV+NSXrXXcWEcGAHdyp8RyWjoGMDzVPdBhjCLblVSgHWx5y4mm+k0de9V1pkjsftUaroP7rQckzw==", + "version": "7.14.6", + "resolved": "https://registry.npmmirror.com/@react-navigation/native-stack/-/native-stack-7.14.6.tgz", + "integrity": "sha512-VRlC5mLanRPHK0E15Cild6U01Z5TDPBlmt5YcXRBc+hQTAMbMT9XcSTobf3sJXNY0zzDD1IpSs3Ynex/GU225g==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.8", + "@react-navigation/elements": "^2.9.11", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.1.31", + "@react-navigation/native": "^7.1.34", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3672,9 +3719,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.90.20", - "resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.90.20.tgz", - "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "version": "5.91.2", + "resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.91.2.tgz", + "integrity": "sha512-Uz2pTgPC1mhqrrSGg18RKCWT/pkduAYtxbcyIyKBhw7dTWjXZIzqmpzO2lBkyWr4hlImQgpu1m1pei3UnkFRWw==", "license": "MIT", "funding": { "type": "github", @@ -3682,12 +3729,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.21", - "resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.90.21.tgz", - "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "version": "5.91.2", + "resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.91.2.tgz", + "integrity": "sha512-GClLPzbM57iFXv+FlvOUL56XVe00PxuTaVEyj1zAObhRiKF008J5vedmaq7O6ehs+VmPHe8+PUQhMuEyv8d9wQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.20" + "@tanstack/query-core": "5.91.2" }, "funding": { "type": "github", @@ -3738,6 +3785,12 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmmirror.com/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3778,9 +3831,9 @@ } }, "node_modules/@types/node": { - "version": "25.3.2", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.3.2.tgz", - "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", + "version": "25.5.0", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -3885,6 +3938,15 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", @@ -3951,29 +4013,6 @@ "strip-ansi": "^5.0.0" } }, - "node_modules/ansi-fragments/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-fragments/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4011,6 +4050,18 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/appdirsjs": { "version": "1.2.7", "resolved": "https://registry.npmmirror.com/appdirsjs/-/appdirsjs-1.2.7.tgz", @@ -4025,13 +4076,10 @@ "license": "MIT" }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.6", @@ -4144,28 +4192,19 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.15", - "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", - "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "version": "0.4.17", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.13.0", "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", @@ -4180,12 +4219,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.6", - "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", - "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "version": "0.6.8", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6" + "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -4207,12 +4246,12 @@ "license": "MIT" }, "node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmmirror.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", - "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", + "version": "0.32.1", + "resolved": "https://registry.npmmirror.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.1.tgz", + "integrity": "sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ==", "license": "MIT", "dependencies": { - "hermes-parser": "0.32.0" + "hermes-parser": "0.32.1" } }, "node_modules/babel-plugin-transform-flow-enums": { @@ -4250,6 +4289,54 @@ "@babel/core": "^7.0.0 || ^8.0.0-0" } }, + "node_modules/babel-preset-expo": { + "version": "55.0.12", + "resolved": "https://registry.npmmirror.com/babel-preset-expo/-/babel-preset-expo-55.0.12.tgz", + "integrity": "sha512-oR46ExGZpRijmPUsr0rFH5X4lR/mvwqJAFXJRLpynZcvyv2pHPTeGMNfd/p5oPMbdbaeMS6G+3k18p48u2Qjbw==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.20.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.83.2", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.32.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "expo-widgets": "^55.0.6", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + }, + "expo-widgets": { + "optional": true + } + } + }, "node_modules/babel-preset-jest": { "version": "29.6.3", "resolved": "https://registry.npmmirror.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", @@ -4281,6 +4368,15 @@ "node": "18 || 20 || >=22" } }, + "node_modules/barcode-detector": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/barcode-detector/-/barcode-detector-3.1.1.tgz", + "integrity": "sha512-ghWlEAV93ZCUniO7Co3ih/01XPm+U30CV+NoPbO6Chj5lZzHydDAqKlrBEd+37TkoR+QTH3tnnwd8k8epGTfIg==", + "license": "MIT", + "dependencies": { + "zxing-wasm": "3.0.1" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", @@ -4302,9 +4398,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.9", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -4325,6 +4421,18 @@ "node": ">=12.0.0" } }, + "node_modules/better-opn/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/better-opn/node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmmirror.com/open/-/open-8.4.2.tgz", @@ -4388,19 +4496,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -4581,9 +4676,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "version": "1.0.30001780", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", "funding": [ { "type": "opencollective", @@ -4634,6 +4729,18 @@ "node": ">=12.13.0" } }, + "node_modules/chrome-launcher/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chromium-edge-launcher": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", @@ -4648,6 +4755,18 @@ "rimraf": "^3.0.2" } }, + "node_modules/chromium-edge-launcher/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-2.0.0.tgz", @@ -4655,15 +4774,16 @@ "license": "MIT" }, "node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "devOptional": true, "license": "MIT", "dependencies": { - "restore-cursor": "^2.0.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/cli-spinners": { @@ -4698,6 +4818,18 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", @@ -4775,12 +4907,13 @@ "license": "MIT" }, "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": "^12.20.0 || >=14" } }, "node_modules/compressible": { @@ -4828,15 +4961,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", @@ -4890,9 +5014,9 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.48.0", - "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "version": "3.49.0", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", "license": "MIT", "dependencies": { "browserslist": "^4.28.1" @@ -4929,26 +5053,6 @@ } } }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "devOptional": true, - "license": "Python-2.0" - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -4999,9 +5103,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "devOptional": true, "license": "MIT" }, @@ -5141,9 +5245,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.321", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -5194,13 +5298,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmmirror.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -5355,58 +5452,32 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/expo": { - "version": "55.0.4", - "resolved": "https://registry.npmmirror.com/expo/-/expo-55.0.4.tgz", - "integrity": "sha512-cbQBPYwmH6FRvh942KR8mSdEcrVdsIMkjdHthtf59zlpzgrk28FabhOdL/Pc9WuS+CsIP3EIQbZqmLkTjv6qPg==", + "version": "55.0.8", + "resolved": "https://registry.npmmirror.com/expo/-/expo-55.0.8.tgz", + "integrity": "sha512-sziDGiDmeRmaSpFwMuSxFhr4vfWrQS1UgVXSTovsUDY0ximABzYdnF5L2OwtD8zjtIww8x2oJGmD6mKS+AoVsw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "55.0.14", - "@expo/config": "~55.0.8", - "@expo/config-plugins": "~55.0.6", + "@expo/cli": "55.0.18", + "@expo/config": "~55.0.10", + "@expo/config-plugins": "~55.0.7", "@expo/devtools": "55.0.2", - "@expo/fingerprint": "0.16.5", - "@expo/local-build-cache-provider": "55.0.6", + "@expo/fingerprint": "0.16.6", + "@expo/local-build-cache-provider": "55.0.7", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", - "@expo/metro-config": "55.0.9", + "@expo/metro-config": "55.0.11", "@expo/vector-icons": "^15.0.2", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~55.0.10", - "expo-asset": "~55.0.8", - "expo-constants": "~55.0.7", - "expo-file-system": "~55.0.10", + "babel-preset-expo": "~55.0.12", + "expo-asset": "~55.0.10", + "expo-constants": "~55.0.9", + "expo-file-system": "~55.0.11", "expo-font": "~55.0.4", "expo-keep-awake": "~55.0.4", - "expo-modules-autolinking": "55.0.8", - "expo-modules-core": "55.0.13", + "expo-modules-autolinking": "55.0.11", + "expo-modules-core": "55.0.17", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.1" @@ -5436,33 +5507,68 @@ } }, "node_modules/expo-application": { - "version": "55.0.8", - "resolved": "https://registry.npmmirror.com/expo-application/-/expo-application-55.0.8.tgz", - "integrity": "sha512-PeZk4Zj8LlzRcRtK3J4ouSPBoi9lroYsRbbz/0HEvx+uB6HIaM1qfzgpcctvjkdJJfnidBQNyieW5BVO/qUQ6w==", + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/expo-application/-/expo-application-55.0.10.tgz", + "integrity": "sha512-5ccf+S6hsQz+doi907TOJxKzV5AKgAgw004z4FoDWSoGhfab0LUPg6uyvOspuU4cbNvqw8EAy08hZbVO8nKc9Q==", "license": "MIT", "peerDependencies": { "expo": "*" } }, - "node_modules/expo-background-fetch": { - "version": "55.0.9", - "resolved": "https://registry.npmmirror.com/expo-background-fetch/-/expo-background-fetch-55.0.9.tgz", - "integrity": "sha512-SIXlLyUsEgqZZ9RZBUAHMGudMYUco2LWbWJkC79PzkedSTRuhQUergFhVR+n+OFe/YYfw2l9Rk2KSwtN+1WcYA==", + "node_modules/expo-asset": { + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/expo-asset/-/expo-asset-55.0.10.tgz", + "integrity": "sha512-wxjNBKIaDyachq7oJgVlWVFzZ6SnNpJFJhkkcymXoTPt5O3XmDM+a6fT91xQQawCXTyZuCc1sNxKMetEofeYkg==", "license": "MIT", "dependencies": { - "expo-task-manager": "~55.0.9" + "@expo/image-utils": "^0.8.12", + "expo-constants": "~55.0.9" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-background-fetch": { + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/expo-background-fetch/-/expo-background-fetch-55.0.10.tgz", + "integrity": "sha512-JRSlQ2GEigUxKqHi11xLPz5/T02O7YzsV1xNKDOxr2SXSERmAN9WFD9l2F9SDw5tUz53ewI1kndHQ6FmSu+7Tw==", + "license": "MIT", + "dependencies": { + "expo-task-manager": "~55.0.10" }, "peerDependencies": { "expo": "*" } }, - "node_modules/expo-constants": { - "version": "55.0.7", - "resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-55.0.7.tgz", - "integrity": "sha512-kdcO4TsQRRqt0USvjaY5vgQMO9H52K3kBZ/ejC7F6rz70mv08GoowrZ1CYOr5O4JpPDRlIpQfZJUucaS/c+KWQ==", + "node_modules/expo-camera": { + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/expo-camera/-/expo-camera-55.0.10.tgz", + "integrity": "sha512-ftDNJbGsAPNJ/QrM3j6g8/rQAOqTwZpqtvmzF7V9VX0movaCznZFdYsLi/Fff9WeEk1KzcnLIlmSz4Tj+BCrJA==", "license": "MIT", "dependencies": { - "@expo/config": "~55.0.8", + "barcode-detector": "^3.0.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, + "node_modules/expo-constants": { + "version": "55.0.9", + "resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-55.0.9.tgz", + "integrity": "sha512-iBiXjZeuU5S/8docQeNzsVvtDy4w0zlmXBpFEi1ypwugceEpdQQab65TVRbusXAcwpNVxCPMpNlDssYp0Pli2g==", + "license": "MIT", + "dependencies": { + "@expo/config": "~55.0.10", "@expo/env": "~2.1.1" }, "peerDependencies": { @@ -5471,15 +5577,15 @@ } }, "node_modules/expo-dev-client": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/expo-dev-client/-/expo-dev-client-55.0.10.tgz", - "integrity": "sha512-qclT+uDp5VjdHDrXkMus0d8ZpNq41CzOXWJq4UmlfsuFeY4b7v/vAI0OJTtScx/FSTkSkggRzjqm+EwnmIFRCg==", + "version": "55.0.18", + "resolved": "https://registry.npmmirror.com/expo-dev-client/-/expo-dev-client-55.0.18.tgz", + "integrity": "sha512-zQeCGk+doTMIKwPe9lNlGcUiBlMpJQNyrrsYc5eRdxuMwBrDVAU7zshARmamSw8zlAIHCsThJH2zeJg/lgQrKA==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "55.0.11", - "expo-dev-menu": "55.0.10", + "expo-dev-launcher": "55.0.19", + "expo-dev-menu": "55.0.16", "expo-dev-menu-interface": "55.0.1", - "expo-manifests": "~55.0.9", + "expo-manifests": "~55.0.11", "expo-updates-interface": "~55.1.3" }, "peerDependencies": { @@ -5487,23 +5593,23 @@ } }, "node_modules/expo-dev-launcher": { - "version": "55.0.11", - "resolved": "https://registry.npmmirror.com/expo-dev-launcher/-/expo-dev-launcher-55.0.11.tgz", - "integrity": "sha512-u/8iVwD4VU2N5R5Tr32+yqT7Llm8K7VyfYB76Fe5apK97Ocpf2PR4Oqf8RbKc9DmqcbHEWfNriMGEW8U6FsaKA==", + "version": "55.0.19", + "resolved": "https://registry.npmmirror.com/expo-dev-launcher/-/expo-dev-launcher-55.0.19.tgz", + "integrity": "sha512-RtiC/K7Cg7RafNlq32GtilMEO5cy61HPBKASQHwLK6Gz5+FYepx0KSHIXIhWqekZMfpcj1w80tvEjMNFjUJ9Dg==", "license": "MIT", "dependencies": { "@expo/schema-utils": "^55.0.2", - "expo-dev-menu": "55.0.10", - "expo-manifests": "~55.0.9" + "expo-dev-menu": "55.0.16", + "expo-manifests": "~55.0.11" }, "peerDependencies": { "expo": "*" } }, "node_modules/expo-dev-menu": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-55.0.10.tgz", - "integrity": "sha512-gad31DFkRmEC6pj6sZLIv3HY14PR3X6SUwUTtilArF5eQK/Nr3dWhYFL/QHlrBkwTEDYeKKgcw3V6sZDfE6hcg==", + "version": "55.0.16", + "resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-55.0.16.tgz", + "integrity": "sha512-UhduIh/6wQmFLIy1EeQJBCN09irOrC1kf/UMQ9CxXCkXit9SOtJx+26YfCmbrIRV/lYK2qZK8Y178/HNkt7yeA==", "license": "MIT", "dependencies": { "expo-dev-menu-interface": "55.0.1" @@ -5528,9 +5634,9 @@ "license": "MIT" }, "node_modules/expo-file-system": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/expo-file-system/-/expo-file-system-55.0.10.tgz", - "integrity": "sha512-ysFdVdUgtfj2ApY0Cn+pBg+yK4xp+SNwcaH8j2B91JJQ4OXJmnyCSmrNZYz7J4mdYVuv2GzxIP+N/IGlHQG3Yw==", + "version": "55.0.11", + "resolved": "https://registry.npmmirror.com/expo-file-system/-/expo-file-system-55.0.11.tgz", + "integrity": "sha512-KMUd6OY375J9WD79ZvjvCDZMveT7YfgiGWdi58/gfuTBsr14TRuoPk8RRQHAtc4UquzWViKcHwna9aPY7/XPpw==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -5552,9 +5658,9 @@ } }, "node_modules/expo-glass-effect": { - "version": "55.0.7", - "resolved": "https://registry.npmmirror.com/expo-glass-effect/-/expo-glass-effect-55.0.7.tgz", - "integrity": "sha512-G7Q9rUaEY0YC36fGE6irDljfsfvzz/y49zagARAKvSJSyQMUSrhR25WOr5LK5Cw7gQNNBEy9U1ctlr7yCay/fQ==", + "version": "55.0.8", + "resolved": "https://registry.npmmirror.com/expo-glass-effect/-/expo-glass-effect-55.0.8.tgz", + "integrity": "sha512-IvUjHb/4t6r2H/LXDjcQ4uDoHrmO2cLOvEb9leLavQ4HX5+P4LRtQrMDMlkWAn5Wo5DkLcG8+1CrQU2nqgogTA==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -5563,9 +5669,9 @@ } }, "node_modules/expo-haptics": { - "version": "55.0.8", - "resolved": "https://registry.npmmirror.com/expo-haptics/-/expo-haptics-55.0.8.tgz", - "integrity": "sha512-yVR6EsQwl1WuhFITc0PpfI/7dsBdjK/F2YA8xB80UUW9iTa+Tqz21FpH4n/vtbargpzFxkhl5WNYMa419+QWFQ==", + "version": "55.0.9", + "resolved": "https://registry.npmmirror.com/expo-haptics/-/expo-haptics-55.0.9.tgz", + "integrity": "sha512-KCRyHr/uu4syXmoq3aIQ6ahuaX6FGtlPkWGlLlHJ836WF3nG+5+oCaCQiI7qMTpml+Tp/V/zP4ZaowM2KHgLNA==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -5601,9 +5707,9 @@ } }, "node_modules/expo-image-picker": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/expo-image-picker/-/expo-image-picker-55.0.10.tgz", - "integrity": "sha512-uspDWjNBNjUD//MLzu7oEtT9cuNgW5pd6HxPfAJffRIdkdCUCYxLYXRp5pfB6JtW640eCdvm2QEQrQC172Y62g==", + "version": "55.0.13", + "resolved": "https://registry.npmmirror.com/expo-image-picker/-/expo-image-picker-55.0.13.tgz", + "integrity": "sha512-G+W11rcoUi3rK+6cnKWkTfZilMkGVZnYe90TiM3R98nPSlzGBoto3a/TkGGTJXedz/dmMzr49L+STlWhuKKIFw==", "license": "MIT", "dependencies": { "expo-image-loader": "~55.0.0" @@ -5618,10 +5724,20 @@ "integrity": "sha512-aupt/o5PDAb8dXDCb0JcRdkqnTLxe/F+La7jrnyd/sXlYFfRgBJLFOa1SqVFXm1E/Xam1SE/yw6eAb+DGY7Arg==", "license": "MIT" }, + "node_modules/expo-keep-awake": { + "version": "55.0.4", + "resolved": "https://registry.npmmirror.com/expo-keep-awake/-/expo-keep-awake-55.0.4.tgz", + "integrity": "sha512-vwfdMtMS5Fxaon8gC0AiE70SpxTsHJ+rjeoVJl8kdfdbxczF7OIaVmfjFJ5Gfigd/WZiLqxhfZk34VAkXF4PNg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-linear-gradient": { - "version": "55.0.8", - "resolved": "https://registry.npmmirror.com/expo-linear-gradient/-/expo-linear-gradient-55.0.8.tgz", - "integrity": "sha512-nCMgZXcHesnFslH1TUFMdqlDiE4jYTTTSHb97g1jq1gyGX2xDEOyqBxQJmiIVGrZJ+kTWH6RljJ5tYyva9mrAg==", + "version": "55.0.9", + "resolved": "https://registry.npmmirror.com/expo-linear-gradient/-/expo-linear-gradient-55.0.9.tgz", + "integrity": "sha512-S82iF+CVoSBVHdwusLQGh6Th/kcWLHU47jZhBPwyTrYWnsHZtb0oCqU96YvhDYvhbTdsuOaKEi+Xu+r/I2R8ow==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -5630,13 +5746,13 @@ } }, "node_modules/expo-linking": { - "version": "55.0.7", - "resolved": "https://registry.npmmirror.com/expo-linking/-/expo-linking-55.0.7.tgz", - "integrity": "sha512-MiGCedere1vzQTEi2aGrkzd7eh/rPSz4w6F3GMBuAJzYl+/0VhIuyhozpEGrueyDIXWfzaUVOcn3SfxVi+kwQQ==", + "version": "55.0.8", + "resolved": "https://registry.npmmirror.com/expo-linking/-/expo-linking-55.0.8.tgz", + "integrity": "sha512-O9QgKAfEqKfsjL6IKs5p7pFAjo/3/TQwjMzzNPl8BCndbxWMPQfMeViXPYYNS9bA2ujUqrtF1OYhO6woI7GNQQ==", "license": "MIT", "peer": true, "dependencies": { - "expo-constants": "~55.0.7", + "expo-constants": "~55.0.8", "invariant": "^2.2.4" }, "peerDependencies": { @@ -5645,12 +5761,12 @@ } }, "node_modules/expo-manifests": { - "version": "55.0.9", - "resolved": "https://registry.npmmirror.com/expo-manifests/-/expo-manifests-55.0.9.tgz", - "integrity": "sha512-i82j3X4hbxYDe6kxUw4u8WfvbvTj2w+9BD9WKuL0mFRy+MjvdzdyaqAjEViWCKo/alquP/hTApDTQBb3UmWhkg==", + "version": "55.0.11", + "resolved": "https://registry.npmmirror.com/expo-manifests/-/expo-manifests-55.0.11.tgz", + "integrity": "sha512-3+pFun4C9F/eFMVpwZgOBrBWq5sfu7rS1uxTrcg9G7jUFatNe5W6hr+M7z7aQPDf0J1afaSudUZPawx1LLf15w==", "license": "MIT", "dependencies": { - "@expo/config": "~55.0.8", + "@expo/config": "~55.0.10", "expo-json-utils": "~55.0.0" }, "peerDependencies": { @@ -5658,9 +5774,9 @@ } }, "node_modules/expo-media-library": { - "version": "55.0.9", - "resolved": "https://registry.npmmirror.com/expo-media-library/-/expo-media-library-55.0.9.tgz", - "integrity": "sha512-E12e4gjQEZNdAa7MHDLOAiOMQmhmOGHFMMU5DpiK6I01hPXyRcaxgAQQLpibIXNP4O5LjGi2psa3NBacjyjxkw==", + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/expo-media-library/-/expo-media-library-55.0.10.tgz", + "integrity": "sha512-xXmz8Do9BJSt1LrkC6r8l2HAXNdaAr2TdzCuiVo9u47Vuo54twlOgzLpdx4rIzODy/cclqbYb2tae4nCKHJLbw==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -5668,12 +5784,12 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "55.0.8", - "resolved": "https://registry.npmmirror.com/expo-modules-autolinking/-/expo-modules-autolinking-55.0.8.tgz", - "integrity": "sha512-nrWB1pkNp7bR8ECUTgYUiJ2Pyh6AvxCBXZ+lyPlfl1TzEIGhwU1Yqr+d78eJDueXaW+9zKeE0HqrTZoLS3ve4A==", + "version": "55.0.11", + "resolved": "https://registry.npmmirror.com/expo-modules-autolinking/-/expo-modules-autolinking-55.0.11.tgz", + "integrity": "sha512-9dqnPzQoIl1dIvEctMWpQ8eaiXDeBTgAwebCc1WF0BbEo+pcdKjZWoCSqlLj+d7IX+OnTgM+k6cY2kPDGIu4sg==", "license": "MIT", "dependencies": { - "@expo/require-utils": "^55.0.2", + "@expo/require-utils": "^55.0.3", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" @@ -5682,10 +5798,19 @@ "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, + "node_modules/expo-modules-autolinking/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/expo-modules-core": { - "version": "55.0.13", - "resolved": "https://registry.npmmirror.com/expo-modules-core/-/expo-modules-core-55.0.13.tgz", - "integrity": "sha512-DYLQTOJAR7jD3M9S0sH9myZaPEtShdicHrPiWcupIXMeMkQxFzErx+adUI8gZPy4AU45BgeGgtaogRfT25iLfw==", + "version": "55.0.17", + "resolved": "https://registry.npmmirror.com/expo-modules-core/-/expo-modules-core-55.0.17.tgz", + "integrity": "sha512-pw3cZiaSlBrqRJUD/pHuMnKGsRTW6XJ255FrjDd3HC4QrqErCnfSQPmz+Sv4Qkelcvd9UGdAewyTqZdFwjLwOw==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" @@ -5696,16 +5821,16 @@ } }, "node_modules/expo-notifications": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/expo-notifications/-/expo-notifications-55.0.10.tgz", - "integrity": "sha512-F+ozrVFthKCwfqz2cXmcqrqwzBMTAwoNBqTZERuFtgc+6I++mweVzLLTtbAy8kBDZ33MA13GKyeq7mw13/rDgw==", + "version": "55.0.13", + "resolved": "https://registry.npmmirror.com/expo-notifications/-/expo-notifications-55.0.13.tgz", + "integrity": "sha512-vbtSBcMkYtNTO+6WKdeOzysOqvtmiq/sQrUKJpYcB75m9hBFcAfI2klpXdUiGg5kMr/ygBmFENSolQt1B9QY8A==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.8.12", "abort-controller": "^3.0.0", "badgin": "^1.1.5", - "expo-application": "~55.0.8", - "expo-constants": "~55.0.7" + "expo-application": "~55.0.10", + "expo-constants": "~55.0.8" }, "peerDependencies": { "expo": "*", @@ -5714,22 +5839,22 @@ } }, "node_modules/expo-router": { - "version": "55.0.4", - "resolved": "https://registry.npmmirror.com/expo-router/-/expo-router-55.0.4.tgz", - "integrity": "sha512-wLKxc9l3IaE96UJFvwXKi2YYYjYK/VUttwAwcnljaUA2dLgDruNGmjsBS9A+g3aK3lt2/JJRu+cec7ZLJ9r6Wg==", + "version": "55.0.7", + "resolved": "https://registry.npmmirror.com/expo-router/-/expo-router-55.0.7.tgz", + "integrity": "sha512-UdraTi8/1LGCCEnq/3+wEVnM11b4ezFEIvMsWP9ajFvEhFGkcXlQitvSehT2yI5cbBrBaIMP2p/2naBiPyYVyw==", "license": "MIT", "dependencies": { "@expo/metro-runtime": "^55.0.6", "@expo/schema-utils": "^55.0.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", - "@react-navigation/bottom-tabs": "^7.10.1", - "@react-navigation/native": "^7.1.28", - "@react-navigation/native-stack": "^7.10.1", + "@react-navigation/bottom-tabs": "^7.15.5", + "@react-navigation/native": "^7.1.33", + "@react-navigation/native-stack": "^7.14.5", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "expo-glass-effect": "^55.0.7", + "expo-glass-effect": "^55.0.8", "expo-image": "^55.0.6", "expo-server": "^55.0.6", "expo-symbols": "^55.0.5", @@ -5749,11 +5874,11 @@ "peerDependencies": { "@expo/log-box": "55.0.7", "@expo/metro-runtime": "^55.0.6", - "@react-navigation/drawer": "^7.7.2", + "@react-navigation/drawer": "^7.9.4", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^55.0.7", - "expo-linking": "^55.0.7", + "expo-constants": "^55.0.8", + "expo-linking": "^55.0.8", "react": "*", "react-dom": "*", "react-native": "*", @@ -5810,9 +5935,9 @@ } }, "node_modules/expo-sqlite": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/expo-sqlite/-/expo-sqlite-55.0.10.tgz", - "integrity": "sha512-yLQXkwcA0OVSKuL4t+a6vv+H/Klh8147n7hH75AN0MkC48p3Go7+6GM+3SFENeaBUsmOnOS3XSFxMxxj6PIg4g==", + "version": "55.0.11", + "resolved": "https://registry.npmmirror.com/expo-sqlite/-/expo-sqlite-55.0.11.tgz", + "integrity": "sha512-lDGJrE0m1lw/3y1ZSsER2kfXnS+9WzgaKcIFp/RKbTfyhs0v8l86Ulqdr+6peRFOfzi0kdj4Ty0LzE2Adx93tg==", "license": "MIT", "dependencies": { "await-lock": "^2.2.2" @@ -5859,9 +5984,9 @@ } }, "node_modules/expo-system-ui": { - "version": "55.0.9", - "resolved": "https://registry.npmmirror.com/expo-system-ui/-/expo-system-ui-55.0.9.tgz", - "integrity": "sha512-8ygP1B0uFAFI8s7eHY2IcGnE83GhFeZYwHBr/fQ4dSXnc7iVT9zp2PvyTyiDiibQ69dBG+fauMQ4KlPcOO51kQ==", + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/expo-system-ui/-/expo-system-ui-55.0.10.tgz", + "integrity": "sha512-s0Fyf37ma3TaWhh18uocg914Uz0tKPaYBf+mEME7Tp88d3FxOTg+tTv1S9mUe4lJNXY+0KlWFz9J4SMNyjGRWQ==", "license": "MIT", "dependencies": { "@react-native/normalize-colors": "0.83.2", @@ -5879,9 +6004,9 @@ } }, "node_modules/expo-task-manager": { - "version": "55.0.9", - "resolved": "https://registry.npmmirror.com/expo-task-manager/-/expo-task-manager-55.0.9.tgz", - "integrity": "sha512-ABqEua5FCjXmzRB+OGKD2KcQL2gkh57nCwGnFGe5Km+/b+0uxIs0lq0L3PPyoYPs1tAWp5FKfn2jcnz1nbBCVA==", + "version": "55.0.10", + "resolved": "https://registry.npmmirror.com/expo-task-manager/-/expo-task-manager-55.0.10.tgz", + "integrity": "sha512-QjlVPiBqiDaALssZAQ9w14quELKQ9DBdV4sfmH6rICUbDvSBFiH91HbGxROXcQEDrU8lrVDObJQBxrJK9ucaaw==", "license": "MIT", "dependencies": { "unimodules-app-loader": "~55.0.2" @@ -5892,9 +6017,9 @@ } }, "node_modules/expo-updates": { - "version": "55.0.12", - "resolved": "https://registry.npmmirror.com/expo-updates/-/expo-updates-55.0.12.tgz", - "integrity": "sha512-20YTlmivT7pU8+jYMQHqHQmFTdNHHfIMXXVuFjQA31qCyAOwR32AvDn30IBpgn1Z7XJTX+Sr6cEoMhqz5IJfww==", + "version": "55.0.15", + "resolved": "https://registry.npmmirror.com/expo-updates/-/expo-updates-55.0.15.tgz", + "integrity": "sha512-UE9Ik56trq//kNeJ/BlC5vOTYdNTvsHwhfWFYMazP1UOQK4lnX59/t0qz8Ut+3aPXZZT7+B6mnbWtic0QqN1wA==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -5904,7 +6029,7 @@ "chalk": "^4.1.2", "debug": "^4.3.4", "expo-eas-client": "~55.0.2", - "expo-manifests": "~55.0.9", + "expo-manifests": "~55.0.11", "expo-structured-headers": "~55.0.0", "expo-updates-interface": "~55.1.3", "getenv": "^2.0.0", @@ -5937,9 +6062,9 @@ "license": "MIT" }, "node_modules/expo-video": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/expo-video/-/expo-video-55.0.10.tgz", - "integrity": "sha512-L3UXgvGjrJv4ym3PnIGPPQi4LlVHQSh89eYm2Q7Pn9iUy5ce98sAdqPIKV88bfdLAIWfmPecYUoOzozXBPmenw==", + "version": "55.0.11", + "resolved": "https://registry.npmmirror.com/expo-video/-/expo-video-55.0.11.tgz", + "integrity": "sha512-XCXcPfzVYx6CgXlbRPH2sgacsNT82T6+euyyFlZsG+iuF5tUOWnMgToTuCmktAkwKDe70pNonoQI9HtZjSfNXw==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -5948,27 +6073,27 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "55.0.14", - "resolved": "https://registry.npmmirror.com/@expo/cli/-/cli-55.0.14.tgz", - "integrity": "sha512-glXPSjjLCIz+KX/ezqLTGIF9eTE1lexiCxunvB3loRZNnGeBDGW3eF++cuPKudW26jeC6bqZkcqBG7Lp0Sp9qg==", + "version": "55.0.18", + "resolved": "https://registry.npmmirror.com/@expo/cli/-/cli-55.0.18.tgz", + "integrity": "sha512-3sJwu8KvCvQIXBnhUlHgLBZBe+ZK4Da9R5rgI4znaowJavYWMqzRClLzyE6Kri66WVoMX7Q4HUVIh8prRlO0XA==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", - "@expo/config": "~55.0.8", - "@expo/config-plugins": "~55.0.6", + "@expo/config": "~55.0.10", + "@expo/config-plugins": "~55.0.7", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.12", "@expo/json-file": "^10.0.12", "@expo/log-box": "55.0.7", "@expo/metro": "~54.2.0", - "@expo/metro-config": "~55.0.9", + "@expo/metro-config": "~55.0.11", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.3", "@expo/plist": "^0.5.2", - "@expo/prebuild-config": "^55.0.8", - "@expo/require-utils": "^55.0.2", - "@expo/router-server": "^55.0.9", + "@expo/prebuild-config": "^55.0.10", + "@expo/require-utils": "^55.0.3", + "@expo/router-server": "^55.0.11", "@expo/schema-utils": "^55.0.2", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", @@ -6028,31 +6153,10 @@ } } }, - "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/prebuild-config": { - "version": "55.0.8", - "resolved": "https://registry.npmmirror.com/@expo/prebuild-config/-/prebuild-config-55.0.8.tgz", - "integrity": "sha512-VJNJiOmmZgyDnR7JMmc3B8Z0ZepZ17I8Wtw+wAH/2+UCUsFg588XU+bwgYcFGw+is28kwGjY46z43kfufpxOnA==", - "license": "MIT", - "dependencies": { - "@expo/config": "~55.0.8", - "@expo/config-plugins": "~55.0.6", - "@expo/config-types": "^55.0.5", - "@expo/image-utils": "^0.8.12", - "@expo/json-file": "^10.0.12", - "@react-native/normalize-colors": "0.83.2", - "debug": "^4.3.1", - "resolve-from": "^5.0.0", - "semver": "^7.6.0", - "xml2js": "0.6.0" - }, - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "55.0.9", - "resolved": "https://registry.npmmirror.com/@expo/router-server/-/router-server-55.0.9.tgz", - "integrity": "sha512-LcCFi+P1qfZOsw0DO4JwNKRxtWt4u2bjTYj0PUe4WVf9NVG/NfUetAXYRbBS6P+gupfM6SC+/bdzdqCWQh7j8g==", + "version": "55.0.11", + "resolved": "https://registry.npmmirror.com/@expo/router-server/-/router-server-55.0.11.tgz", + "integrity": "sha512-Kd8J1OOlFR00DZxn+1KfiQiXZtRut6cj8+ynqHJa7dtt/lTL4tGkYistqmVhpKJ6w886eRY5WivKy7o0ZBFkJA==", "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -6060,7 +6164,7 @@ "peerDependencies": { "@expo/metro-runtime": "^55.0.6", "expo": "*", - "expo-constants": "^55.0.7", + "expo-constants": "^55.0.9", "expo-font": "^55.0.4", "expo-router": "*", "expo-server": "^55.0.6", @@ -6083,87 +6187,16 @@ } } }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "55.0.9", - "resolved": "https://registry.npmmirror.com/@expo/metro-config/-/metro-config-55.0.9.tgz", - "integrity": "sha512-ZJFEfat/+dLUhFyFFWrzMjAqAwwUaJ3RD42QNqR7jh+RVYkAf6XYLynb5qrKJTHI1EcOx4KoO1717yXYYRFDBA==", + "node_modules/expo/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~55.0.8", - "@expo/env": "~2.1.1", - "@expo/json-file": "~10.0.12", - "@expo/metro": "~54.2.0", - "@expo/spawn-async": "^1.7.2", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.32.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "picomatch": "^4.0.3", - "postcss": "~8.4.32", - "resolve-from": "^5.0.0" + "color-convert": "^1.9.0" }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/babel-preset-expo": { - "version": "55.0.10", - "resolved": "https://registry.npmmirror.com/babel-preset-expo/-/babel-preset-expo-55.0.10.tgz", - "integrity": "sha512-aRtW7qJKohGU2V0LUJ6IeP7py3+kVUo9zcc8+v1Kix8jGGuIvqvpo9S6W1Fmn9VFP2DBwkFDLiyzkCZS85urVA==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.20.5", - "@babel/helper-module-imports": "^7.25.9", - "@babel/plugin-proposal-decorators": "^7.12.9", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-preset": "0.83.2", - "babel-plugin-react-compiler": "^1.0.0", - "babel-plugin-react-native-web": "~0.21.0", - "babel-plugin-syntax-hermes-parser": "^0.32.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "debug": "^4.3.4", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "@babel/runtime": "^7.20.0", - "expo": "*", - "expo-widgets": "^55.0.2", - "react-refresh": ">=0.14.0 <1.0.0" - }, - "peerDependenciesMeta": { - "@babel/runtime": { - "optional": true - }, - "expo": { - "optional": true - }, - "expo-widgets": { - "optional": true - } + "engines": { + "node": ">=4" } }, "node_modules/expo/node_modules/ci-info": { @@ -6181,41 +6214,164 @@ "node": ">=8" } }, - "node_modules/expo/node_modules/expo-asset": { - "version": "55.0.8", - "resolved": "https://registry.npmmirror.com/expo-asset/-/expo-asset-55.0.8.tgz", - "integrity": "sha512-yEz2svDX67R0yiW2skx6dJmcE0q7sj9ECpGMcxBExMCbctc+nMoZCnjUuhzPl5vhClUsO5HFFXS5vIGmf1bgHQ==", + "node_modules/expo/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "license": "MIT", "dependencies": { - "@expo/image-utils": "^0.8.12", - "expo-constants": "~55.0.7" + "restore-cursor": "^2.0.0" }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" + "engines": { + "node": ">=4" } }, - "node_modules/expo/node_modules/expo-keep-awake": { - "version": "55.0.4", - "resolved": "https://registry.npmmirror.com/expo-keep-awake/-/expo-keep-awake-55.0.4.tgz", - "integrity": "sha512-vwfdMtMS5Fxaon8gC0AiE70SpxTsHJ+rjeoVJl8kdfdbxczF7OIaVmfjFJ5Gfigd/WZiLqxhfZk34VAkXF4PNg==", + "node_modules/expo/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*" + "dependencies": { + "color-name": "1.1.3" } }, - "node_modules/expo/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/expo/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/expo/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=0.8.0" + } + }, + "node_modules/expo/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/expo/node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=4" + } + }, + "node_modules/expo/node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/expo/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/expo/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/expo/node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/expo/node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/expo/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/expo/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/expo/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, "node_modules/expo/node_modules/ws": { @@ -6284,22 +6440,9 @@ "license": "MIT" }, "node_modules/fast-xml-builder": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", - "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/fast-xml-parser": { - "version": "5.4.2", - "resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz", - "integrity": "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==", + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "devOptional": true, "funding": [ { @@ -6309,8 +6452,25 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.7", + "resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.7.tgz", + "integrity": "sha512-LteOsISQ2GEiDHZch6L9hB0+MLoYVLToR7xotrzU0opCICBkxOPgHAy1HxAvtxfJNXDJpgAsQN30mkrfpO2Prg==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6378,9 +6538,9 @@ } }, "node_modules/fetch-nodeshim": { - "version": "0.4.8", - "resolved": "https://registry.npmmirror.com/fetch-nodeshim/-/fetch-nodeshim-0.4.8.tgz", - "integrity": "sha512-YW5vG33rabBq6JpYosLNoXoaMN69/WH26MeeX2hkDVjN6UlvRGq3Wkazl9H0kisH95aMu/HtHL64JUvv/+Nv/g==", + "version": "0.4.9", + "resolved": "https://registry.npmmirror.com/fetch-nodeshim/-/fetch-nodeshim-0.4.9.tgz", + "integrity": "sha512-XIQWlB2A4RZ7NebXWGxS0uDMdvRHkiUDTghBVJKFg9yEOd45w/PP8cZANuPf2H08W6Cor3+2n7Q6TTZgAS3Fkw==", "license": "MIT" }, "node_modules/fill-range": { @@ -6437,17 +6597,33 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flow-enums-runtime": { @@ -6749,18 +6925,18 @@ "license": "MIT" }, "node_modules/hermes-estree": { - "version": "0.32.0", - "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "version": "0.32.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.32.1.tgz", + "integrity": "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==", "license": "MIT" }, "node_modules/hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "version": "0.32.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.32.1.tgz", + "integrity": "sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==", "license": "MIT", "dependencies": { - "hermes-estree": "0.32.0" + "hermes-estree": "0.32.1" } }, "node_modules/hoist-non-react-statics": { @@ -6988,9 +7164,10 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "devOptional": true, "license": "MIT" }, "node_modules/is-core-module": { @@ -7034,12 +7211,13 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/is-glob": { @@ -7110,15 +7288,13 @@ } }, "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "devOptional": true, "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/isexe": { @@ -7152,15 +7328,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmmirror.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -7287,6 +7454,18 @@ "node": ">=8" } }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-validate": { "version": "29.7.0", "resolved": "https://registry.npmmirror.com/jest-validate/-/jest-validate-29.7.0.tgz", @@ -7361,13 +7540,12 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -7484,9 +7662,9 @@ "license": "MIT" }, "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -7499,23 +7677,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -7533,9 +7711,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -7553,9 +7731,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -7573,9 +7751,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -7593,9 +7771,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -7613,9 +7791,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -7633,9 +7811,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -7653,9 +7831,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -7673,9 +7851,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -7693,9 +7871,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -7713,9 +7891,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -7740,15 +7918,19 @@ "license": "MIT" }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "devOptional": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash.debounce": { @@ -7764,86 +7946,20 @@ "license": "MIT" }, "node_modules/log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "devOptional": true, "license": "MIT", "dependencies": { - "chalk": "^2.0.1" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" + "node": ">=10" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/log-symbols/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/logkitty": { @@ -7883,6 +7999,75 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/logkitty/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/logkitty/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -8100,6 +8285,21 @@ "node": ">=20.19.4" } }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, "node_modules/metro-cache": { "version": "0.83.3", "resolved": "https://registry.npmmirror.com/metro-cache/-/metro-cache-0.83.3.tgz", @@ -8300,6 +8500,42 @@ "node": ">=20.19.4" } }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", @@ -8313,22 +8549,35 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "devOptional": true, "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" + "node": ">=4.0.0" } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8346,13 +8595,23 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/minimatch": { @@ -8422,9 +8681,9 @@ } }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8476,9 +8735,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/node-stream-zip": { @@ -8519,6 +8778,18 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -8573,9 +8844,9 @@ } }, "node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -8603,167 +8874,101 @@ } }, "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "devOptional": true, "license": "MIT", "dependencies": { - "mimic-fn": "^1.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmmirror.com/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" - }, - "engines": { - "node": ">=8" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora": { - "version": "3.4.0", - "resolved": "https://registry.npmmirror.com/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "devOptional": true, "license": "MIT", "dependencies": { - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-spinners": "^2.0.0", - "log-symbols": "^2.2.0", - "strip-ansi": "^5.2.0", + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" }, "engines": { - "node": ">=6" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" + "node": ">=10" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ora/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/ora/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ora/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ora/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, "license": "MIT", "dependencies": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6" - } - }, - "node_modules/ora/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "devOptional": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -8837,6 +9042,22 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -8878,9 +9099,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -8893,12 +9114,12 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -8996,6 +9217,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmmirror.com/proc-log/-/proc-log-4.2.0.tgz", @@ -9150,6 +9377,27 @@ "ws": "^7" } }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz", @@ -9181,9 +9429,9 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "license": "MIT" }, "node_modules/react-native": { @@ -9260,9 +9508,9 @@ } }, "node_modules/react-native-is-edge-to-edge": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", - "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", + "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==", "license": "MIT", "peerDependencies": { "react": "*", @@ -9325,9 +9573,9 @@ "license": "MIT" }, "node_modules/react-native-reanimated": { - "version": "4.2.1", - "resolved": "https://registry.npmmirror.com/react-native-reanimated/-/react-native-reanimated-4.2.1.tgz", - "integrity": "sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==", + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/react-native-reanimated/-/react-native-reanimated-4.2.2.tgz", + "integrity": "sha512-o3kKvdD8cVlg12Z4u3jv0MFAt53QV4k7gD9OLwQqU8eZLyd8QvaOjVZIghMZhC2pjP93uUU44PlO5JgF8S4Vxw==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "1.2.1", @@ -9339,6 +9587,16 @@ "react-native-worklets": ">=0.7.0" } }, + "node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-reanimated/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", @@ -9382,9 +9640,9 @@ "license": "MIT" }, "node_modules/react-native-tab-view": { - "version": "4.2.2", - "resolved": "https://registry.npmmirror.com/react-native-tab-view/-/react-native-tab-view-4.2.2.tgz", - "integrity": "sha512-NXtrG6OchvbGjsvbySJGVocXxo4Y2vA17ph4rAaWtA2jh+AasD8OyikKBRg2SmllEfeQ+GEhcKe8kulHv8BhTg==", + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/react-native-tab-view/-/react-native-tab-view-4.3.0.tgz", + "integrity": "sha512-qPMF75uz/7+MuVG2g+YETdGMzlWZnhC6iI4h/7EBbwIBwNBIBi2z4OA6KhY3IOOBwGHXEIz5IyA6doDqifYBHg==", "license": "MIT", "dependencies": { "use-latest-callback": "^0.2.4" @@ -9572,6 +9830,15 @@ } } }, + "node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", + "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.32.0" + } + }, "node_modules/react-native/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", @@ -9618,6 +9885,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/react-native/node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", + "license": "MIT" + }, + "node_modules/react-native/node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, "node_modules/react-native/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", @@ -9630,6 +9912,39 @@ "node": "*" } }, + "node_modules/react-native/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.14.2.tgz", @@ -9834,16 +10149,17 @@ "license": "MIT" }, "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "devOptional": true, "license": "MIT", "dependencies": { - "onetime": "^2.0.0", + "onetime": "^5.1.0", "signal-exit": "^3.0.2" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/reusify": { @@ -9974,9 +10290,9 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -9989,15 +10305,12 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/send": { @@ -10048,16 +10361,16 @@ "node": ">= 0.8" } }, - "node_modules/send/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">= 0.8" + "node": ">=4" } }, "node_modules/send/node_modules/statuses": { @@ -10277,6 +10590,12 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", @@ -10337,20 +10656,10 @@ "devOptional": true, "license": "MIT" }, - "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/slugify": { - "version": "1.6.6", - "resolved": "https://registry.npmmirror.com/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "version": "1.6.8", + "resolved": "https://registry.npmmirror.com/slugify/-/slugify-1.6.8.tgz", + "integrity": "sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==", "license": "MIT", "engines": { "node": ">=8.0.0" @@ -10505,7 +10814,16 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -10517,6 +10835,27 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -10528,9 +10867,9 @@ } }, "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/strnum/-/strnum-2.2.1.tgz", + "integrity": "sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==", "devOptional": true, "funding": [ { @@ -10589,6 +10928,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/terminal-link/-/terminal-link-2.1.1.tgz", @@ -10606,9 +10957,9 @@ } }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmmirror.com/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.46.1", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -10776,16 +11127,6 @@ "node": ">= 0.6" } }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/type-is/node_modules/mime-types": { "version": "3.0.2", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", @@ -11158,6 +11499,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", @@ -11178,24 +11531,13 @@ } }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "dependencies": { + "async-limiter": "~1.0.0" } }, "node_modules/xcode": { @@ -11322,9 +11664,9 @@ } }, "node_modules/zustand": { - "version": "5.0.11", - "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.11.tgz", - "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "version": "5.0.12", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", "license": "MIT", "engines": { "node": ">=12.20.0" @@ -11349,6 +11691,34 @@ "optional": true } } + }, + "node_modules/zxing-wasm": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/zxing-wasm/-/zxing-wasm-3.0.1.tgz", + "integrity": "sha512-3CLj6iaGkpqPWXAB4pIWkFOR63MwqGekpMzaROFKto4dFowiPmLlC56KoMoOSXzqOCOpI5DAvMdB8ku2va6fUg==", + "license": "MIT", + "dependencies": { + "@types/emscripten": "^1.41.5", + "type-fest": "^5.4.4" + }, + "peerDependencies": { + "@types/emscripten": ">=1.39.6" + } + }, + "node_modules/zxing-wasm/node_modules/type-fest": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 49baba2..144167c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "carrot_bbs", - "version": "1.0.0", + "version": "1.0.1", "main": "index.ts", "scripts": { "start": "expo start", @@ -26,6 +26,7 @@ "date-fns": "^4.1.0", "expo": "~55.0.4", "expo-background-fetch": "~55.0.9", + "expo-camera": "^55.0.10", "expo-constants": "~55.0.7", "expo-dev-client": "~55.0.10", "expo-file-system": "~55.0.10", diff --git a/plans/frontend_cursor_pagination_design.md b/plans/frontend_cursor_pagination_design.md new file mode 100644 index 0000000..00268ed --- /dev/null +++ b/plans/frontend_cursor_pagination_design.md @@ -0,0 +1,1131 @@ +# 前端游标分页适配方案设计文档 + +## 一、背景说明 + +后端已完成游标分页功能实现,共改造了 11 个接口。前端需要对接这些新的游标分页接口,替换现有的基于页码的分页方式。 + +### 后端游标分页接口列表 + +| 模块 | 接口路径 | 排序方式 | +|------|----------|----------| +| 帖子 | GET /posts/cursor | created_at DESC | +| 帖子搜索 | GET /posts/search/cursor | created_at DESC | +| 用户帖子 | GET /users/:id/posts/cursor | created_at DESC | +| 会话列表 | GET /conversations/cursor | updated_at DESC | +| 消息列表 | GET /conversations/:id/messages/cursor | seq DESC | +| 评论列表 | GET /comments/post/:id/cursor | created_at DESC | +| 评论回复 | GET /comments/:id/replies/cursor | created_at ASC | +| 通知列表 | GET /notifications/cursor | created_at DESC | +| 群组列表 | GET /groups/cursor | created_at DESC | +| 群成员 | GET /groups/:id/members/cursor | join_time DESC | +| 群公告 | GET /groups/:id/announcements/cursor | created_at DESC | + +### 后端 API 规范 + +**请求参数:** +- `cursor` - 游标字符串(首次请求不传) +- `direction` - 分页方向:`forward` 或 `backward`(默认 forward) +- `page_size` - 每页数量(默认 20,最大 100) + +**响应格式:** +```json +{ + "items": [...], + "next_cursor": "xxx", + "prev_cursor": "xxx", + "has_more": true +} +``` + +--- + +## 二、现有前端分页机制分析 + +### 2.1 分页类型定义 (`src/infrastructure/pagination/types.ts`) + +```typescript +// 现有分页状态 - 基于页码 +interface PaginationState { + currentPage: number; + pageSize: number; + totalLoaded: number; + hasMore: boolean; + isLoading: boolean; + lastLoadTime: number; + cursor?: string | number | null; // 已支持但未充分使用 + hasError: boolean; + error?: string; +} + +// 现有 FetchPageFunction +type FetchPageFunction = ( + page: number, + pageSize: number, + cursor?: string | number | null +) => Promise<{ + data: T[]; + hasMore: boolean; + cursor?: string | number | null; +}>; +``` + +### 2.2 分页状态管理器 (`PaginationStateManager.ts`) + +- 使用 `currentPage` 作为分页基准 +- 基于页码的缓存机制(`pageCaches: Map>`) +- `loadMore()` 方法自动递增页码 + +### 2.3 usePagination Hook (`usePagination.ts`) + +- 暴露 `currentPage`、`loadMore`、`refresh` 等方法 +- 内部调用 `PaginationStateManager` 进行状态管理 +- 已有 `cursor` 支持,但 UI 层仍使用页码 + +### 2.4 现有 API 服务层 + +**postService.ts:** +```typescript +// 现有分页方式 +async getPosts(page = 1, pageSize = 20, tab?: string): Promise> +``` + +**messageService.ts:** +```typescript +// 现有分页方式 - 基于 seq +async getMessages(conversationId, afterSeq?, beforeSeq?, limit?): Promise +``` + +### 2.5 现有页面组件分页方式 + +各页面使用**手动管理状态**的方式: +```typescript +const [page, setPage] = useState(1); +const [hasMore, setHasMore] = useState(true); + +const loadMore = useCallback(() => { + if (!loading && hasMore) { + const nextPage = page + 1; + setPage(nextPage); + loadData(nextPage); + } +}, [loading, hasMore, page, loadData]); +``` + +**涉及页面:** +- `HomeScreen.tsx` - 帖子列表 +- `SearchScreen.tsx` - 帖子搜索 +- `PostDetailScreen.tsx` - 帖子详情(评论列表) +- `MessageListScreen.tsx` - 会话列表 +- `ChatScreen.tsx` - 消息列表 +- `NotificationsScreen.tsx` - 通知列表 +- `GroupMembersScreen.tsx` - 群组成员 +- `JoinGroupScreen.tsx` - 群组列表 + +--- + +## 三、游标分页类型定义设计 + +### 3.1 新增游标分页相关类型 (`src/infrastructure/pagination/types.ts`) + +```typescript +// ==================== 游标分页类型 ==================== + +/** + * 游标分页方向 + */ +export type CursorDirection = 'forward' | 'backward'; + +/** + * 游标分页请求参数 + */ +export interface CursorPageRequest { + cursor?: string | null; + direction?: CursorDirection; + page_size?: number; +} + +/** + * 游标分页响应 + */ +export interface CursorPageResponse { + items: T[]; + next_cursor: string | null; + prev_cursor: string | null; + has_more: boolean; +} + +/** + * 游标分页状态 - 扩展现有 PaginationState + */ +export interface CursorPaginationState { + // 游标相关 + nextCursor: string | null; + prevCursor: string | null; + currentCursor: string | null; + + // 方向 + direction: CursorDirection; + + // 通用 + hasMore: boolean; + isLoading: boolean; + hasError: boolean; + error?: string; +} + +/** + * 游标分页配置 + */ +export interface CursorPaginationConfig { + pageSize: number; + prefetchThreshold: number; + enablePrefetch: boolean; + maxRetries: number; + retryDelay: number; +} + +/** + * 游标分页加载函数 + */ +export type CursorFetchFunction = ( + request: CursorPageRequest +) => Promise>; + +/** + * 游标分页加载结果 + */ +export interface CursorLoadMoreResult { + success: boolean; + data: T[]; + hasMore: boolean; + nextCursor: string | null; + prevCursor: string | null; + fromCache: boolean; + error?: string; +} + +/** + * 游标分页刷新结果 + */ +export interface CursorRefreshResult { + success: boolean; + data: T[]; + hasMore: boolean; + nextCursor: string | null; + error?: string; +} +``` + +### 3.2 新增游标分页 DTO 类型 (`src/types/dto.ts`) + +```typescript +// ==================== 游标分页 DTO ==================== + +/** + * 游标分页响应基础结构 + */ +export interface CursorPaginatedResponse { + items: T[]; + next_cursor: string | null; + prev_cursor: string | null; + has_more: boolean; +} + +/** + * 帖子列表游标分页响应 + */ +export interface PostCursorResponse extends CursorPaginatedResponse {} + +/** + * 会话列表游标分页响应 + */ +export interface ConversationCursorResponse extends CursorPaginatedResponse {} + +/** + * 消息列表游标分页响应 + */ +export interface MessageCursorResponse extends CursorPaginatedResponse {} + +/** + * 评论列表游标分页响应 + */ +export interface CommentCursorResponse extends CursorPaginatedResponse {} + +/** + * 通知列表游标分页响应 + */ +export interface NotificationCursorResponse extends CursorPaginatedResponse {} + +/** + * 群组列表游标分页响应 + */ +export interface GroupCursorResponse extends CursorPaginatedResponse {} + +/** + * 群成员列表游标分页响应 + */ +export interface GroupMemberCursorResponse extends CursorPaginatedResponse {} + +/** + * 群公告列表游标分页响应 + */ +export interface GroupAnnouncementCursorResponse extends CursorPaginatedResponse {} +``` + +--- + +## 四、API 服务层改造设计 + +### 4.1 新增游标分页 API 方法 + +#### postService.ts 改造 +```typescript +class PostService { + // ==================== 游标分页方法 ==================== + + /** + * 获取帖子列表(游标分页) + * GET /api/v1/posts/cursor + */ + async getPostsCursor( + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get('/posts/cursor', params); + return { + items: response.data.items, + next_cursor: response.data.next_cursor, + prev_cursor: response.data.prev_cursor, + has_more: response.data.has_more, + }; + } + + /** + * 搜索帖子(游标分页) + * GET /api/v1/posts/search/cursor + */ + async searchPostsCursor( + keyword: string, + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + keyword, + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get('/posts/search/cursor', params); + return response.data; + } + + /** + * 获取用户帖子列表(游标分页) + * GET /api/v1/users/:id/posts/cursor + */ + async getUserPostsCursor( + userId: string, + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get( + `/users/${userId}/posts/cursor`, + params + ); + return response.data; + } +} +``` + +#### messageService.ts 改造 +```typescript +class MessageService { + // ==================== 会话列表游标分页 ==================== + + /** + * 获取会话列表(游标分页) + * GET /api/v1/conversations/cursor + */ + async getConversationsCursor( + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get('/conversations/cursor', params); + return response.data; + } + + // ==================== 消息列表游标分页 ==================== + + /** + * 获取消息列表(游标分页) + * GET /api/v1/conversations/:id/messages/cursor + */ + async getMessagesCursor( + conversationId: string, + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get( + `/conversations/${encodeURIComponent(conversationId)}/messages/cursor`, + params + ); + return response.data; + } +} +``` + +#### 新增 commentService.ts +```typescript +// 新建文件: src/services/commentService.ts + +import { api } from './api'; +import { CommentCursorResponse, CursorPageRequest, CursorPageResponse } from '../types/dto'; +import { CommentDTO } from '../types/dto'; + +class CommentService { + /** + * 获取帖子评论列表(游标分页) + * GET /api/v1/comments/post/:id/cursor + */ + async getPostCommentsCursor( + postId: string, + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get( + `/comments/post/${postId}/cursor`, + params + ); + return response.data; + } + + /** + * 获取评论回复列表(游标分页) + * GET /api/v1/comments/:id/replies/cursor + */ + async getCommentRepliesCursor( + commentId: string, + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get( + `/comments/${commentId}/replies/cursor`, + params + ); + return response.data; + } +} + +export const commentService = new CommentService(); +``` + +#### 新增 notificationService.ts +```typescript +// 新建文件: src/services/notificationService.ts + +import { api } from './api'; +import { NotificationCursorResponse, CursorPageRequest, CursorPageResponse } from '../types/dto'; +import { NotificationDTO } from '../types/dto'; + +class NotificationService { + /** + * 获取通知列表(游标分页) + * GET /api/v1/notifications/cursor + */ + async getNotificationsCursor( + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get('/notifications/cursor', params); + return response.data; + } +} + +export const notificationService = new NotificationService(); +``` + +#### 新增 groupService.ts (扩展现有群组服务) +```typescript +// 在现有 groupService.ts 中添加游标分页方法 + +class GroupService { + /** + * 获取群组列表(游标分页) + * GET /api/v1/groups/cursor + */ + async getGroupsCursor( + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get('/groups/cursor', params); + return response.data; + } + + /** + * 获取群成员列表(游标分页) + * GET /api/v1/groups/:id/members/cursor + */ + async getGroupMembersCursor( + groupId: string, + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get( + `/groups/${groupId}/members/cursor`, + params + ); + return response.data; + } + + /** + * 获取群公告列表(游标分页) + * GET /api/v1/groups/:id/announcements/cursor + */ + async getGroupAnnouncementsCursor( + groupId: string, + request: CursorPageRequest = {} + ): Promise> { + const params: Record = { + page_size: request.page_size || 20, + }; + if (request.cursor) params.cursor = request.cursor; + if (request.direction) params.direction = request.direction; + + const response = await api.get( + `/groups/${groupId}/announcements/cursor`, + params + ); + return response.data; + } +} +``` + +--- + +## 五、分页 Hook 改造设计 + +### 5.1 新增 useCursorPagination Hook (`src/hooks/useCursorPagination.ts`) + +```typescript +/** + * 游标分页 Hook + * + * 提供基于游标的分页功能,包括: + * - loadMore: 加载下一页 + * - loadPrevious: 加载上一页(双向游标支持) + * - refresh: 刷新数据 + * - 状态管理 + */ + +import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; +import { CursorPaginationState, CursorPaginationConfig } from '../infrastructure/pagination/types'; + +export interface UseCursorPaginationOptions { + /** 分页键 */ + key: string | null; + /** 数据获取函数 */ + fetchFunction: CursorFetchFunction; + /** 分页配置 */ + config?: Partial; + /** 初始数据 */ + initialData?: T[]; + /** 是否自动加载第一页 */ + autoLoad?: boolean; + /** 是否启用自动预加载 */ + enableAutoPrefetch?: boolean; +} + +export interface UseCursorPaginationReturn { + /** 当前数据列表 */ + data: T[]; + /** 是否正在加载 */ + isLoading: boolean; + /** 是否正在刷新 */ + isRefreshing: boolean; + /** 是否有更多数据 */ + hasMore: boolean; + /** 是否有上一页 */ + hasPrevious: boolean; + /** 是否有错误 */ + hasError: boolean; + /** 错误信息 */ + error?: string; + /** 当前游标 */ + currentCursor: string | null; + /** 加载下一页 */ + loadMore: () => Promise>; + /** 加载上一页 */ + loadPrevious: () => Promise>; + /** 刷新数据 */ + refresh: () => Promise>; + /** 重置分页状态 */ + reset: () => void; +} + +export function useCursorPagination( + options: UseCursorPaginationOptions +): UseCursorPaginationReturn { + const { + key, + fetchFunction, + config: userConfig, + initialData = [], + autoLoad = false, + enableAutoPrefetch = true, + } = options; + + // 合并配置 + const config = useMemo(() => ({ + ...DEFAULT_CURSOR_PAGINATION_CONFIG, + ...userConfig, + }), [userConfig]); + + // 数据状态 + const [data, setData] = useState(initialData); + const [state, setState] = useState({ + nextCursor: null, + prevCursor: null, + currentCursor: null, + direction: 'forward', + hasMore: true, + isLoading: false, + hasError: false, + error: undefined, + }); + + // Refs + const autoLoadRef = useRef(false); + const isMountedRef = useRef(true); + + // 派生的 UI 状态 + const isRefreshing = state.isLoading && data.length > 0; + const hasPrevious = state.prevCursor !== null; + + /** + * 加载下一页 + */ + const loadMore = useCallback(async (): Promise> => { + if (!key) { + return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: 'No pagination key' }; + } + + try { + setState(prev => ({ ...prev, isLoading: true, hasError: false })); + + const response = await fetchFunction({ + cursor: state.nextCursor, + direction: 'forward', + page_size: config.pageSize, + }); + + if (isMountedRef.current) { + setData(prev => [...prev, ...response.items]); + setState({ + nextCursor: response.next_cursor, + prevCursor: response.prev_cursor, + currentCursor: response.next_cursor, + direction: 'forward', + hasMore: response.has_more, + isLoading: false, + hasError: false, + }); + } + + return { + success: true, + data: response.items, + hasMore: response.has_more, + nextCursor: response.next_cursor, + prevCursor: response.prev_cursor, + fromCache: false, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + if (isMountedRef.current) { + setState(prev => ({ ...prev, isLoading: false, hasError: true, error: errorMessage })); + } + return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: errorMessage }; + } + }, [key, fetchFunction, state.nextCursor, config.pageSize]); + + /** + * 加载上一页 + */ + const loadPrevious = useCallback(async (): Promise> => { + if (!key || !state.prevCursor) { + return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: 'No previous page' }; + } + + try { + setState(prev => ({ ...prev, isLoading: true, hasError: false })); + + const response = await fetchFunction({ + cursor: state.prevCursor, + direction: 'backward', + page_size: config.pageSize, + }); + + if (isMountedRef.current) { + setData(prev => [...response.items, ...prev]); + setState({ + nextCursor: response.next_cursor, + prevCursor: response.prev_cursor, + currentCursor: response.prev_cursor, + direction: 'backward', + hasMore: response.has_more, + isLoading: false, + hasError: false, + }); + } + + return { + success: true, + data: response.items, + hasMore: response.has_more, + nextCursor: response.next_cursor, + prevCursor: response.prev_cursor, + fromCache: false, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + if (isMountedRef.current) { + setState(prev => ({ ...prev, isLoading: false, hasError: true, error: errorMessage })); + } + return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: errorMessage }; + } + }, [key, fetchFunction, state.prevCursor, config.pageSize]); + + /** + * 刷新数据 + */ + const refresh = useCallback(async (): Promise> => { + if (!key) { + return { success: false, data: [], hasMore: false, nextCursor: null, error: 'No pagination key' }; + } + + try { + setState(prev => ({ ...prev, isLoading: true, hasError: false })); + + const response = await fetchFunction({ + cursor: null, + direction: 'forward', + page_size: config.pageSize, + }); + + if (isMountedRef.current) { + setData(response.items); + setState({ + nextCursor: response.next_cursor, + prevCursor: null, + currentCursor: null, + direction: 'forward', + hasMore: response.has_more, + isLoading: false, + hasError: false, + }); + } + + return { + success: true, + data: response.items, + hasMore: response.has_more, + nextCursor: response.next_cursor, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + if (isMountedRef.current) { + setState(prev => ({ ...prev, isLoading: false, hasError: true, error: errorMessage })); + } + return { success: false, data: [], hasMore: false, nextCursor: null, error: errorMessage }; + } + }, [key, fetchFunction, config.pageSize]); + + /** + * 重置分页状态 + */ + const reset = useCallback(() => { + if (isMountedRef.current) { + setData([]); + setState({ + nextCursor: null, + prevCursor: null, + currentCursor: null, + direction: 'forward', + hasMore: true, + isLoading: false, + hasError: false, + }); + autoLoadRef.current = false; + } + }, []); + + // 自动加载第一页 + useEffect(() => { + if (!key || !autoLoad || autoLoadRef.current) return; + if (data.length > 0) return; + + autoLoadRef.current = true; + refresh(); + }, [key, autoLoad, data.length, refresh]); + + // 组件卸载时清理 + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + return { + data, + isLoading: state.isLoading, + isRefreshing, + hasMore: state.hasMore, + hasPrevious, + hasError: state.hasError, + error: state.error, + currentCursor: state.currentCursor, + loadMore, + loadPrevious, + refresh, + reset, + }; +} +``` + +--- + +## 六、各列表页面改造计划 + +### 6.1 帖子列表 (HomeScreen.tsx) + +```typescript +// 改造后的分页方式 +const { + data: posts, + isLoading, + hasMore, + loadMore, + refresh, +} = useCursorPagination({ + key: 'home-posts', + fetchFunction: async (request) => { + return postService.getPostsCursor(request); + }, + config: { pageSize: 20 }, + autoLoad: true, +}); + +// UI 层使用 FlatList/FlashList 的 onEndReached + 0} + onRefresh={refresh} +/> +``` + +### 6.2 会话列表 (MessageListScreen.tsx) + +```typescript +const { + data: conversations, + isLoading, + hasMore, + loadMore, + refresh, +} = useCursorPagination({ + key: 'conversations', + fetchFunction: async (request) => { + return messageService.getConversationsCursor(request); + }, + autoLoad: true, +}); +``` + +### 6.3 消息列表 (ChatScreen.tsx) + +```typescript +// 聊天消息需要支持双向加载 +const { + data: messages, + isLoading, + hasMore, + hasPrevious, + loadMore, // 加载更多历史 + loadPrevious, // 加载更新的消息 + refresh, +} = useCursorPagination({ + key: `chat-${conversationId}`, + fetchFunction: async (request) => { + return messageService.getMessagesCursor(conversationId, request); + }, +}); +``` + +### 6.4 帖子搜索 (SearchScreen.tsx) + +```typescript +const { + data: searchResults, + isLoading, + hasMore, + loadMore, + refresh, +} = useCursorPagination({ + key: `search-${keyword}`, + fetchFunction: async (request) => { + return postService.searchPostsCursor(keyword, request); + }, + autoLoad: !!keyword, +}); +``` + +### 6.5 评论列表 (PostDetailScreen.tsx) + +```typescript +const { + data: comments, + isLoading, + hasMore, + loadMore, + refresh, +} = useCursorPagination({ + key: `comments-${postId}`, + fetchFunction: async (request) => { + return commentService.getPostCommentsCursor(postId, request); + }, +}); +``` + +### 6.6 通知列表 (NotificationsScreen.tsx) + +```typescript +const { + data: notifications, + isLoading, + hasMore, + loadMore, + refresh, +} = useCursorPagination({ + key: 'notifications', + fetchFunction: async (request) => { + return notificationService.getNotificationsCursor(request); + }, +}); +``` + +### 6.7 群组列表 (JoinGroupScreen.tsx) + +```typescript +const { + data: groups, + isLoading, + hasMore, + loadMore, + refresh, +} = useCursorPagination({ + key: 'groups', + fetchFunction: async (request) => { + return groupService.getGroupsCursor(request); + }, +}); +``` + +### 6.8 群组成员 (GroupMembersScreen.tsx) + +```typescript +const { + data: members, + isLoading, + hasMore, + loadMore, + refresh, +} = useCursorPagination({ + key: `group-members-${groupId}`, + fetchFunction: async (request) => { + return groupService.getGroupMembersCursor(groupId, request); + }, +}); +``` + +--- + +## 七、需要修改的文件清单 + +### 7.1 新增文件 + +| 文件路径 | 说明 | +|----------|------| +| `src/hooks/useCursorPagination.ts` | 新增游标分页 Hook | +| `src/services/commentService.ts` | 新增评论服务(含游标分页) | +| `src/services/notificationService.ts` | 新增通知服务(含游标分页) | +| `src/types/cursor.ts` | 新增游标分页相关类型定义 | + +### 7.2 需要修改的文件 + +| 文件路径 | 修改内容 | +|----------|----------| +| `src/types/dto.ts` | 新增游标分页 DTO 类型 | +| `src/services/postService.ts` | 新增游标分页方法(保留旧方法兼容) | +| `src/services/messageService.ts` | 新增会话/消息游标分页方法 | +| `src/services/groupService.ts` | 新增群组相关游标分页方法 | +| `src/infrastructure/pagination/types.ts` | 新增游标分页类型定义 | +| `src/infrastructure/pagination/CursorPaginationStateManager.ts` | 新增游标分页状态管理器(可选) | +| `src/screens/home/HomeScreen.tsx` | 改造为使用 useCursorPagination | +| `src/screens/home/SearchScreen.tsx` | 改造为使用 useCursorPagination | +| `src/screens/home/PostDetailScreen.tsx` | 改造评论列表使用 useCursorPagination | +| `src/screens/message/MessageListScreen.tsx` | 改造为使用 useCursorPagination | +| `src/screens/message/ChatScreen.tsx` | 改造消息列表使用 useCursorPagination | +| `src/screens/message/NotificationsScreen.tsx` | 改造为使用 useCursorPagination | +| `src/screens/message/JoinGroupScreen.tsx` | 改造为使用 useCursorPagination | +| `src/screens/message/GroupMembersScreen.tsx` | 改造为使用 useCursorPagination | + +--- + +## 八、实现顺序建议 + +### 阶段一:基础设施(优先级高) +1. 在 `src/types/dto.ts` 新增游标分页 DTO 类型 +2. 在 `src/infrastructure/pagination/types.ts` 新增游标分页类型 +3. 创建 `src/hooks/useCursorPagination.ts` + +### 阶段二:API 服务层 +4. 改造 `postService.ts` - 新增游标分页方法 +5. 改造 `messageService.ts` - 新增游标分页方法 +6. 创建 `commentService.ts` +7. 创建 `notificationService.ts` +8. 改造 `groupService.ts` - 新增游标分页方法 + +### 阶段三:UI 组件改造(按依赖顺序) +9. `HomeScreen.tsx` - 帖子列表 +10. `SearchScreen.tsx` - 帖子搜索 +11. `PostDetailScreen.tsx` - 评论列表 +12. `MessageListScreen.tsx` - 会话列表 +13. `ChatScreen.tsx` - 消息列表 +14. `NotificationsScreen.tsx` - 通知列表 +15. `JoinGroupScreen.tsx` - 群组列表 +16. `GroupMembersScreen.tsx` - 群组成员 + +--- + +## 九、架构图 + +```mermaid +graph TB + subgraph "UI Layer" + HomeScreen[HomeScreen.tsx] + SearchScreen[SearchScreen.tsx] + ChatScreen[ChatScreen.tsx] + MessageListScreen[MessageListScreen.tsx] + NotificationsScreen[NotificationsScreen.tsx] + GroupMembersScreen[GroupMembersScreen.tsx] + end + + subgraph "Hooks Layer" + useCursorPagination[useCursorPagination.ts] + usePagination[usePagination.ts - 保留兼容] + end + + subgraph "Service Layer" + postService[postService.ts] + messageService[messageService.ts] + commentService[commentService.ts] + notificationService[notificationService.ts] + groupService[groupService.ts] + end + + subgraph "API Layer" + API[API Client] + end + + subgraph "Backend" + CursorAPI[游标分页 API] + end + + HomeScreen --> useCursorPagination + SearchScreen --> useCursorPagination + ChatScreen --> useCursorPagination + MessageListScreen --> useCursorPagination + NotificationsScreen --> useCursorPagination + GroupMembersScreen --> useCursorPagination + + useCursorPagination --> postService + useCursorPagination --> messageService + useCursorPagination --> commentService + useCursorPagination --> notificationService + useCursorPagination --> groupService + + postService --> API + messageService --> API + commentService --> API + notificationService --> API + groupService --> API + + API --> CursorAPI +``` + +--- + +## 十、注意事项 + +### 10.1 兼容性考虑 +- 保留现有的页码分页方法一段时间,避免一次性全量替换带来的风险 +- 通过功能开关或配置控制是否启用游标分页 + +### 10.2 消息列表特殊处理 +- 聊天消息需要支持双向加载(加载历史 + 加载最新) +- 需要处理好新消息的插入位置(头部 vs 尾部) + +### 10.3 缓存策略 +- 游标分页的缓存策略需要调整,不再基于页码 +- 可以考虑基于 cursor 字符串进行缓存 + +### 10.4 错误处理 +- 网络错误时需要显示重试选项 +- 游标失效时需要提示用户刷新列表 diff --git a/screenshots/after-fix.png b/screenshots/after-fix.png deleted file mode 100644 index 2c35236..0000000 Binary files a/screenshots/after-fix.png and /dev/null differ diff --git a/screenshots/test-navigation-fix.png b/screenshots/test-navigation-fix.png deleted file mode 100644 index 060a680..0000000 Binary files a/screenshots/test-navigation-fix.png and /dev/null differ diff --git a/src/components/business/QRCodeScanner.tsx b/src/components/business/QRCodeScanner.tsx new file mode 100644 index 0000000..5a007e7 --- /dev/null +++ b/src/components/business/QRCodeScanner.tsx @@ -0,0 +1,251 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Alert, + Modal, + Dimensions, +} from 'react-native'; +import { CameraView, useCameraPermissions } from 'expo-camera'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../../navigation/types'; +import { colors, spacing, fontSizes, borderRadius } from '../../theme'; + +const { width, height } = Dimensions.get('window'); + +type NavigationProp = NativeStackNavigationProp; + +interface QRCodeScannerProps { + visible: boolean; + onClose: () => void; +} + +export const QRCodeScanner: React.FC = ({ visible, onClose }) => { + const navigation = useNavigation(); + const [permission, requestPermission] = useCameraPermissions(); + const [scanned, setScanned] = useState(false); + + useEffect(() => { + if (visible) { + // 重置扫描状态,确保每次打开都能重新扫描 + setScanned(false); + if (!permission?.granted) { + requestPermission(); + } + } + }, [visible]); + + const handleBarCodeScanned = ({ type, data }: { type: string; data: string }) => { + if (scanned) return; + setScanned(true); + + // 先关闭扫描界面,类似微信的做法 + onClose(); + + // 解析二维码内容 + // 格式: carrotbbs://qrcode/login?session_id=xxx + if (data.startsWith('carrotbbs://qrcode/login')) { + const sessionId = extractSessionId(data); + if (sessionId) { + // 跳转到确认页面 + navigation.navigate('QRCodeConfirm', { sessionId }); + } else { + Alert.alert('无效的二维码', '无法识别该二维码'); + } + } else { + Alert.alert('无效的二维码', '请扫描网页端的登录二维码'); + } + }; + + const extractSessionId = (url: string): string | null => { + try { + const sessionIdMatch = url.match(/session_id=([^&]+)/); + return sessionIdMatch ? sessionIdMatch[1] : null; + } catch { + return null; + } + }; + + if (!permission?.granted) { + return ( + + + + + + + 扫码登录 + + + + + 需要相机权限才能扫码 + + 授予权限 + + + + + ); + } + + return ( + + + + + + + + + 扫码登录 + + + + + + + + + + + 将二维码放入框内即可扫描 + + + + {}}> + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, + camera: { + flex: 1, + }, + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.5)', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingTop: spacing.xl, + paddingBottom: spacing.md, + }, + closeButton: { + padding: spacing.sm, + }, + headerTitle: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: '#fff', + }, + placeholder: { + width: 40, + }, + scanArea: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + scanFrame: { + width: width * 0.7, + height: width * 0.7, + position: 'relative', + }, + corner: { + position: 'absolute', + width: 20, + height: 20, + borderColor: colors.primary.main, + borderWidth: 3, + }, + topLeft: { + top: 0, + left: 0, + borderRightWidth: 0, + borderBottomWidth: 0, + }, + topRight: { + top: 0, + right: 0, + borderLeftWidth: 0, + borderBottomWidth: 0, + }, + bottomLeft: { + bottom: 0, + left: 0, + borderRightWidth: 0, + borderTopWidth: 0, + }, + bottomRight: { + bottom: 0, + right: 0, + borderLeftWidth: 0, + borderTopWidth: 0, + }, + scanText: { + marginTop: spacing.lg, + fontSize: fontSizes.md, + color: '#fff', + textAlign: 'center', + }, + footer: { + padding: spacing.xl, + alignItems: 'center', + }, + flashButton: { + padding: spacing.md, + backgroundColor: 'rgba(255,255,255,0.2)', + borderRadius: borderRadius.full, + }, + permissionContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + permissionText: { + marginTop: spacing.lg, + fontSize: fontSizes.md, + color: '#666', + textAlign: 'center', + }, + permissionButton: { + marginTop: spacing.xl, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + backgroundColor: colors.primary.main, + borderRadius: borderRadius.md, + }, + permissionButtonText: { + color: '#fff', + fontSize: fontSizes.md, + fontWeight: '600', + }, +}); + +export default QRCodeScanner; \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 88416f5..d18b9d8 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -83,6 +83,9 @@ export type { UsePaginationReturn, } from './usePagination'; +// ==================== 游标分页 Hooks ==================== +export { useCursorPagination } from './useCursorPagination'; + // ==================== 连接状态 Hooks ==================== export { useConnectionState } from './useConnectionState'; diff --git a/src/hooks/useCursorPagination.ts b/src/hooks/useCursorPagination.ts new file mode 100644 index 0000000..403316e --- /dev/null +++ b/src/hooks/useCursorPagination.ts @@ -0,0 +1,208 @@ +import { useState, useCallback, useRef } from 'react'; +import { + CursorPaginationState, + CursorPaginationConfig, + CursorDirection, + UseCursorPaginationReturn, + CursorFetchFunction, +} from '../infrastructure/pagination/types'; +import { CursorPaginationResponse } from '../types/dto'; + +const DEFAULT_PAGE_SIZE = 20; +const MAX_PAGE_SIZE = 100; + +/** + * 游标分页 Hook + * + * @param fetchFunction 数据获取函数 + * @param config 分页配置 + * @param extraParams 额外参数(会传递给 fetchFunction) + */ +export function useCursorPagination( + fetchFunction: CursorFetchFunction, + config: Partial = {}, + extraParams?: P +): UseCursorPaginationReturn { + const { pageSize = DEFAULT_PAGE_SIZE, bidirectional = false } = config; + + // 限制 pageSize 在有效范围内 + const effectivePageSize = Math.min(Math.max(1, pageSize), MAX_PAGE_SIZE); + + const [state, setState] = useState>({ + items: [], + nextCursor: null, + prevCursor: null, + hasMore: false, + isLoading: false, + isRefreshing: false, + error: null, + isFirstLoad: true, + }); + + // 用于取消请求的标志 + const cancelledRef = useRef(false); + + // 加载更多(下一页) + const loadMore = useCallback(async () => { + if (state.isLoading || !state.hasMore) { + return; + } + + cancelledRef.current = false; + + setState(prev => ({ ...prev, isLoading: true, error: null })); + + try { + const response: CursorPaginationResponse = await fetchFunction({ + cursor: state.nextCursor || undefined, + direction: 'forward', + pageSize: effectivePageSize, + extraParams, + }); + + if (cancelledRef.current) return; + + setState(prev => ({ + ...prev, + items: [...prev.items, ...response.items], + nextCursor: response.next_cursor, + prevCursor: response.prev_cursor, + hasMore: response.has_more && response.next_cursor !== null, + isLoading: false, + isFirstLoad: false, + })); + } catch (error) { + if (cancelledRef.current) return; + + setState(prev => ({ + ...prev, + isLoading: false, + error: error instanceof Error ? error.message : '加载失败', + })); + } + }, [state.isLoading, state.hasMore, state.nextCursor, fetchFunction, effectivePageSize, extraParams]); + + // 加载上一页(双向分页) + const loadPrevious = useCallback(async () => { + if (!bidirectional || state.isLoading || !state.prevCursor) { + return; + } + + cancelledRef.current = false; + + setState(prev => ({ ...prev, isLoading: true, error: null })); + + try { + const response: CursorPaginationResponse = await fetchFunction({ + cursor: state.prevCursor, + direction: 'backward', + pageSize: effectivePageSize, + extraParams, + }); + + if (cancelledRef.current) return; + + setState(prev => ({ + ...prev, + // 向前加载时,新数据放在前面 + items: [...response.items, ...prev.items], + nextCursor: response.next_cursor, + prevCursor: response.prev_cursor, + hasMore: response.has_more, + isLoading: false, + isFirstLoad: false, + })); + } catch (error) { + if (cancelledRef.current) return; + + setState(prev => ({ + ...prev, + isLoading: false, + error: error instanceof Error ? error.message : '加载失败', + })); + } + }, [bidirectional, state.isLoading, state.prevCursor, fetchFunction, effectivePageSize, extraParams]); + + // 刷新数据 + const refresh = useCallback(async () => { + cancelledRef.current = false; + + setState(prev => ({ ...prev, isRefreshing: true, error: null })); + + try { + const response: CursorPaginationResponse = await fetchFunction({ + cursor: undefined, + direction: 'forward', + pageSize: effectivePageSize, + extraParams, + }); + + if (cancelledRef.current) return; + + setState({ + items: response.items, + nextCursor: response.next_cursor, + prevCursor: response.prev_cursor, + hasMore: response.has_more && response.next_cursor !== null, + isLoading: false, + isRefreshing: false, + error: null, + isFirstLoad: false, + }); + } catch (error) { + if (cancelledRef.current) return; + + setState(prev => ({ + ...prev, + isRefreshing: false, + error: error instanceof Error ? error.message : '刷新失败', + })); + } + }, [fetchFunction, effectivePageSize, extraParams]); + + // 重置状态 + const reset = useCallback(() => { + cancelledRef.current = true; + setState({ + items: [], + nextCursor: null, + prevCursor: null, + hasMore: false, + isLoading: false, + isRefreshing: false, + error: null, + isFirstLoad: true, + }); + }, []); + + // 设置数据(用于外部数据注入) + const setItems = useCallback( + ( + items: T[], + nextCursor: string | null, + prevCursor: string | null, + hasMore: boolean + ) => { + setState(prev => ({ + ...prev, + items, + nextCursor, + prevCursor, + hasMore, + isFirstLoad: false, + })); + }, + [] + ); + + return { + ...state, + loadMore, + loadPrevious, + refresh, + reset, + setItems, + }; +} + +export default useCursorPagination; \ No newline at end of file diff --git a/src/infrastructure/pagination/index.ts b/src/infrastructure/pagination/index.ts index de25c21..495dd7d 100644 --- a/src/infrastructure/pagination/index.ts +++ b/src/infrastructure/pagination/index.ts @@ -19,6 +19,16 @@ export type { FetchPageFunction, } from './types'; +// 导出游标分页相关类型 +export type { + CursorDirection, + CursorPaginationConfig, + CursorPaginationState, + CursorPaginationActions, + UseCursorPaginationReturn, + CursorFetchFunction, +} from './types'; + // 导出工具函数和常量 export { DEFAULT_PAGINATION_CONFIG, diff --git a/src/infrastructure/pagination/types.ts b/src/infrastructure/pagination/types.ts index 6c96bab..3ea56de 100644 --- a/src/infrastructure/pagination/types.ts +++ b/src/infrastructure/pagination/types.ts @@ -180,3 +180,79 @@ export function createPageCache( export function isCacheExpired(cache: PageCache, ttl: number): boolean { return Date.now() - cache.cachedAt > ttl; } + +// ==================== 游标分页相关类型 ==================== + +/** + * 游标分页方向 + */ +export type CursorDirection = 'forward' | 'backward'; + +/** + * 游标分页配置 + */ +export interface CursorPaginationConfig { + /** 每页数量 */ + pageSize: number; + /** 是否启用双向分页 */ + bidirectional?: boolean; +} + +/** + * 游标分页状态 + */ +export interface CursorPaginationState { + /** 数据项列表 */ + items: T[]; + /** 下一页游标 */ + nextCursor: string | null; + /** 上一页游标 */ + prevCursor: string | null; + /** 是否有更多数据 */ + hasMore: boolean; + /** 是否正在加载 */ + isLoading: boolean; + /** 是否正在刷新 */ + isRefreshing: boolean; + /** 错误信息 */ + error: string | null; + /** 是否为首次加载 */ + isFirstLoad: boolean; +} + +/** + * 游标分页操作 + */ +export interface CursorPaginationActions { + /** 加载更多(下一页) */ + loadMore: () => Promise; + /** 加载上一页(双向分页) */ + loadPrevious: () => Promise; + /** 刷新数据(重新从第一页加载) */ + refresh: () => Promise; + /** 重置状态 */ + reset: () => void; + /** 设置数据(用于外部数据注入) */ + setItems: (items: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void; +} + +/** + * 游标分页 Hook 返回值 + */ +export interface UseCursorPaginationReturn extends CursorPaginationState, CursorPaginationActions {} + +/** + * 游标分页数据获取函数 + */ +export type CursorFetchFunction = (params: { + cursor?: string; + direction: CursorDirection; + pageSize: number; + /** 额外参数 */ + extraParams?: P; +}) => Promise<{ + items: T[]; + next_cursor: string | null; + prev_cursor: string | null; + has_more: boolean; +}>; diff --git a/src/navigation/MainNavigator.tsx b/src/navigation/MainNavigator.tsx index 4d0dbea..9f864c7 100644 --- a/src/navigation/MainNavigator.tsx +++ b/src/navigation/MainNavigator.tsx @@ -22,7 +22,7 @@ import { useAuthStore } from '../stores'; // Deep linking 配置 const linking: LinkingOptions = { - prefixes: [], + prefixes: ['carrotbbs://'], config: { screens: { Auth: { @@ -63,6 +63,7 @@ const linking: LinkingOptions = { CreatePost: 'posts/create', Chat: 'chat/:conversationId', FollowList: 'users/:userId/:type', + QRCodeConfirm: 'qrcode/login/:sessionId', }, }, }; diff --git a/src/navigation/RootNavigator.tsx b/src/navigation/RootNavigator.tsx index c996800..05d072a 100644 --- a/src/navigation/RootNavigator.tsx +++ b/src/navigation/RootNavigator.tsx @@ -23,6 +23,7 @@ import { PostDetailScreen } from '../screens/home'; import { UserScreen } from '../screens/profile'; import FollowListScreen from '../screens/profile/FollowListScreen'; import { CreatePostScreen } from '../screens/create'; +import { QRCodeConfirmScreen } from '../screens/auth/QRCodeConfirmScreen'; import { ChatScreen, CreateGroupScreen, @@ -210,6 +211,15 @@ const getAuthenticatedScreens = () => [ animation: 'slide_from_right', }} />, + , ]; export function RootNavigator({ isAuthenticated, isInitializing }: RootNavigatorProps) { diff --git a/src/navigation/SimpleMobileTabNavigator.tsx b/src/navigation/SimpleMobileTabNavigator.tsx index 4045aa6..1591417 100644 --- a/src/navigation/SimpleMobileTabNavigator.tsx +++ b/src/navigation/SimpleMobileTabNavigator.tsx @@ -1,6 +1,6 @@ /** * 简单的移动端 Tab Navigator - * + * * 不使用 React Navigation 的 Tab Navigator,而是使用一个简单的自定义实现 * 这样可以完全避免 React Navigation 状态恢复的问题 */ @@ -15,6 +15,7 @@ import { } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { colors, shadows } from '../theme'; import { useTotalUnreadCount } from '../stores'; @@ -33,6 +34,15 @@ import { ProfileScreen, SettingsScreen, EditProfileScreen, NotificationSettingsS // ==================== 类型定义 ==================== type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab'; +// Schedule Stack 类型定义 +export type ScheduleStackParamList = { + Schedule: undefined; + CourseDetail: { course: any; relatedCourses?: any[] }; +}; + +// ==================== Stack Navigators ==================== +const ScheduleStack = createNativeStackNavigator(); + // ==================== 常量 ==================== const TAB_BAR_HEIGHT = 64; const TAB_BAR_MARGIN = 14; @@ -41,19 +51,32 @@ const TAB_BAR_FLOATING_MARGIN = 12; // ==================== 组件 ==================== /** - * 简化的 Stack Navigator 包装 + * Schedule Stack Navigator 组件 */ -function SimpleStackNavigator({ - children, - screenName -}: { - children: React.ReactNode; - screenName: string; -}) { +function ScheduleStackNavigatorComponent() { return ( - - {children} - + + + + ); } @@ -83,7 +106,7 @@ export function SimpleMobileTabNavigator() { case 'MessageTab': return ; case 'ScheduleTab': - return ; + return ; case 'ProfileTab': return ; default: diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 450f8ef..c8c7862 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -106,6 +106,7 @@ export type RootStackParamList = { userName?: string; userAvatar?: string | null; }; + QRCodeConfirm: { sessionId: string }; }; // ==================== 全局类型声明 ==================== diff --git a/src/presentation/hooks/responsive/MIGRATION.md b/src/presentation/hooks/responsive/MIGRATION.md deleted file mode 100644 index 7eb181c..0000000 --- a/src/presentation/hooks/responsive/MIGRATION.md +++ /dev/null @@ -1,223 +0,0 @@ -# useResponsive 拆分迁移指南 - -## 概述 - -原有的 `useResponsive.ts` (485行) 已被拆分为多个专注的 hooks,以提高代码的可维护性和性能。 - -## 新目录结构 - -``` -src/presentation/hooks/responsive/ -├── types.ts # 共享类型定义 -├── useBreakpoint.ts # 断点检测 -├── useScreenSize.ts # 屏幕尺寸 -├── useOrientation.ts # 方向检测 -├── usePlatform.ts # 平台检测 -├── useResponsiveValue.ts # 响应式值映射 -├── useResponsiveSpacing.ts # 响应式间距 -├── useColumnCount.ts # 列数计算 -├── useMediaQuery.ts # 媒体查询 -├── useBreakpointCheck.ts # 断点范围检查 (兼容层) -├── useResponsive.ts # 兼容层 (保持向后兼容) -└── index.ts # 统一导出 -``` - -## 新 Hook API 说明 - -### useBreakpoint - 断点检测 - -```typescript -import { useBreakpoint, useFineBreakpoint } from '../presentation/hooks/responsive'; - -// 基础断点: 'mobile' | 'tablet' | 'desktop' | 'wide' -const breakpoint = useBreakpoint(); - -// 细粒度断点: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' -const fineBreakpoint = useFineBreakpoint(); -``` - -### useScreenSize - 屏幕尺寸 - -```typescript -import { useScreenSize, useWindowDimensions } from '../presentation/hooks/responsive'; - -// 完整屏幕尺寸信息 -const { - width, height, - breakpoint, fineBreakpoint, - isMobile, isTablet, isDesktop, isWide -} = useScreenSize(); - -// 仅获取窗口尺寸 -const { width, height, scale, fontScale } = useWindowDimensions(); -``` - -### useOrientation - 方向检测 - -```typescript -import { useOrientation } from '../presentation/hooks/responsive'; - -const { orientation, isPortrait, isLandscape } = useOrientation(); -``` - -### usePlatform - 平台检测 - -```typescript -import { usePlatform } from '../presentation/hooks/responsive'; - -const { OS, isWeb, isIOS, isAndroid, isNative } = usePlatform(); -``` - -### useResponsiveValue - 响应式值 - -```typescript -import { useResponsiveValue } from '../presentation/hooks/responsive'; - -// 根据断点返回不同值 -const padding = useResponsiveValue({ xs: 8, md: 16, lg: 24 }); - -// 响应式样式 -const style = useResponsiveStyle({ - padding: { xs: 8, md: 16, lg: 24 }, - fontSize: { xs: 14, lg: 16 } -}); -``` - -### useResponsiveSpacing - 响应式间距 - -```typescript -import { useResponsiveSpacing } from '../presentation/hooks/responsive'; - -const gap = useResponsiveSpacing({ xs: 8, md: 16, lg: 24 }); -``` - -### useColumnCount - 列数计算 - -```typescript -import { useColumnCount } from '../presentation/hooks/responsive'; - -const columns = useColumnCount({ xs: 1, sm: 2, md: 3, lg: 4 }); -``` - -### useBreakpointGTE / useBreakpointLT - 断点范围检查 - -```typescript -import { useBreakpointGTE, useBreakpointLT, useBreakpointBetween } from '../presentation/hooks/responsive'; - -const isMediumUp = useBreakpointGTE('md'); -const isMobileOnly = useBreakpointLT('lg'); -const isTabletRange = useBreakpointBetween('md', 'lg'); -``` - -### useMediaQuery - 媒体查询 - -```typescript -import { useMediaQuery } from '../presentation/hooks/responsive'; - -const isMinWidth768 = useMediaQuery({ minWidth: 768 }); -const isPortrait = useMediaQuery({ orientation: 'portrait' }); -``` - -## 迁移示例 - -### 示例 1: 使用 useResponsive (旧) - -```typescript -import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../hooks'; - -function Component() { - const { width, isMobile, isTablet, isDesktop, isWide } = useResponsive(); - const padding = useResponsiveSpacing({ xs: 8, md: 16 }); - - return ; -} -``` - -### 示例 2: 使用新专注 hooks (推荐) - -```typescript -import { - useScreenSize, - useResponsiveSpacing -} from '../presentation/hooks/responsive'; - -function Component() { - const { width, isMobile, isTablet, isDesktop, isWide } = useScreenSize(); - const padding = useResponsiveSpacing({ xs: 8, md: 16 }); - - return ; -} -``` - -### 示例 3: 仅使用部分功能 (性能优化) - -```typescript -import { useBreakpoint, usePlatform } from '../presentation/hooks/responsive'; - -function Component() { - // 只订阅断点变化,不订阅尺寸变化 - const breakpoint = useBreakpoint(); - const { isIOS } = usePlatform(); // 平台不会变化,只计算一次 - - return ; -} -``` - -## 性能优势 - -1. **按需订阅**: 只使用需要的功能,避免不必要的重渲染 -2. **细粒度更新**: 每个 hook 独立管理状态 -3. **平台常量**: usePlatform 返回常量,不会触发重渲染 - -## 向后兼容 - -原有的 `useResponsive` 仍然可用,但标记为 `@deprecated`: - -```typescript -import { useResponsive } from '../hooks'; // 仍然可用 - -function Component() { - const { width, height, isMobile, platform } = useResponsive(); // 仍然可用 - return ; -} -``` - -## 工具函数 - -```typescript -import { - getBreakpoint, - getFineBreakpoint, - isBreakpointGTE, - isBreakpointLT -} from '../presentation/hooks/responsive'; - -// 非 hooks 版本,用于工具函数 -const breakpoint = getBreakpoint(800); -const fineBreakpoint = getFineBreakpoint(800); -const isGTE = isBreakpointGTE('md', 'sm'); -``` - -## 类型导出 - -```typescript -import type { - BreakpointKey, - FineBreakpointKey, - ResponsiveValue, - Orientation, - PlatformInfo, - ScreenSize, - MediaQueryOptions, - ResponsiveInfo, // 兼容层 -} from '../presentation/hooks/responsive'; -``` - -## 常量 - -```typescript -import { BREAKPOINTS, FINE_BREAKPOINTS } from '../presentation/hooks/responsive'; - -// BREAKPOINTS = { mobile: 0, tablet: 768, desktop: 1024, wide: 1440 } -// FINE_BREAKPOINTS = { xs: 0, sm: 375, md: 414, lg: 768, xl: 1024, '2xl': 1280, '3xl': 1440, '4xl': 1920 } -``` diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index 711bcfb..000e7e6 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -188,7 +188,7 @@ export const LoginScreen: React.FC = () => { {/* 品牌名称 - 优化大屏排版 */} - 胡萝卜 + 萝卜社区 发现有趣的内容,结识志同道合的朋友 {/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */} @@ -431,7 +431,7 @@ export const LoginScreen: React.FC = () => { color="#FFF" /> - 胡萝卜 + 萝卜社区 发现有趣的内容,结识志同道合的朋友 diff --git a/src/screens/auth/QRCodeConfirmScreen.tsx b/src/screens/auth/QRCodeConfirmScreen.tsx new file mode 100644 index 0000000..6d6ce38 --- /dev/null +++ b/src/screens/auth/QRCodeConfirmScreen.tsx @@ -0,0 +1,288 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Image, + ActivityIndicator, + Alert, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../../navigation/types'; +import { qrcodeApi } from '../../services/authService'; +import { colors, spacing, fontSizes, borderRadius } from '../../theme'; + +type QRCodeConfirmRouteProp = RouteProp; +type NavigationProp = NativeStackNavigationProp; + +export const QRCodeConfirmScreen: React.FC = () => { + const route = useRoute(); + const navigation = useNavigation(); + const { sessionId } = route.params; + + const [loading, setLoading] = useState(false); + const [userInfo, setUserInfo] = useState<{ id: string; nickname: string; avatar: string } | null>(null); + const [error, setError] = useState(null); + + useEffect(() => { + // 扫码 + handleScan(); + }, []); + + const handleScan = async () => { + try { + setLoading(true); + const response = await qrcodeApi.scan(sessionId); + setUserInfo(response.user); + } catch (err: any) { + const errorMsg = err.response?.data?.message || '扫码失败'; + setError(errorMsg); + Alert.alert('扫码失败', errorMsg, [ + { text: '确定', onPress: () => navigation.goBack() } + ]); + } finally { + setLoading(false); + } + }; + + const handleConfirm = async () => { + try { + setLoading(true); + await qrcodeApi.confirm(sessionId); + Alert.alert('登录成功', '网页端已登录', [ + { text: '确定', onPress: () => navigation.goBack() } + ]); + } catch (err: any) { + const errorMsg = err.response?.data?.message || '确认登录失败'; + Alert.alert('登录失败', errorMsg); + } finally { + setLoading(false); + } + }; + + const handleCancel = async () => { + try { + await qrcodeApi.cancel(sessionId); + } catch (err) { + // 忽略取消错误 + } + navigation.goBack(); + }; + + if (loading && !userInfo) { + return ( + + + + 正在扫码... + + + ); + } + + if (error) { + return ( + + + + {error} + navigation.goBack()}> + 返回 + + + + ); + } + + return ( + + + + + + 确认登录 + + + + + + 您正在登录网页端 + + {userInfo && ( + + {userInfo.avatar ? ( + + ) : ( + + + + )} + {userInfo.nickname} + + )} + + + + + {loading ? ( + + ) : ( + 确认登录 + )} + + + + 取消 + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + closeButton: { + padding: spacing.sm, + }, + headerTitle: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: '#333', + }, + placeholder: { + width: 40, + }, + content: { + flex: 1, + padding: spacing.lg, + justifyContent: 'center', + }, + infoCard: { + backgroundColor: '#fff', + borderRadius: borderRadius.lg, + padding: spacing.xl, + alignItems: 'center', + marginBottom: spacing.xl, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + infoText: { + fontSize: fontSizes.md, + color: '#666', + marginBottom: spacing.lg, + }, + userInfo: { + alignItems: 'center', + }, + avatar: { + width: 80, + height: 80, + borderRadius: 40, + marginBottom: spacing.md, + }, + avatarPlaceholder: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: '#f0f0f0', + justifyContent: 'center', + alignItems: 'center', + marginBottom: spacing.md, + }, + nickname: { + fontSize: fontSizes.lg, + fontWeight: '600', + color: '#333', + }, + buttonContainer: { + gap: spacing.md, + }, + button: { + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + borderRadius: borderRadius.md, + alignItems: 'center', + }, + confirmButton: { + backgroundColor: colors.primary.main, + }, + confirmButtonText: { + color: '#fff', + fontSize: fontSizes.md, + fontWeight: '600', + }, + cancelButton: { + backgroundColor: '#fff', + borderWidth: 1, + borderColor: '#ddd', + }, + cancelButtonText: { + color: '#666', + fontSize: fontSizes.md, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: spacing.md, + fontSize: fontSizes.md, + color: '#666', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + errorText: { + marginTop: spacing.md, + fontSize: fontSizes.md, + color: '#666', + textAlign: 'center', + }, + backButton: { + marginTop: spacing.xl, + paddingVertical: spacing.md, + paddingHorizontal: spacing.xl, + backgroundColor: colors.primary.main, + borderRadius: borderRadius.md, + }, + backButtonText: { + color: '#fff', + fontSize: fontSizes.md, + fontWeight: '600', + }, +}); + +export default QRCodeConfirmScreen; \ No newline at end of file diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index 12f9794..d6fa740 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -268,8 +268,8 @@ export const RegisterScreen: React.FC = () => { {/* 品牌名称 - 优化大屏排版 */} - 创建账号 - 加入胡萝卜,开始你的旅程 + 加入萝卜社区 + 加入萝卜社区,发现有趣的内容 {/* 装饰元素 - 大屏模式改为水平排列 + 大幅放大 */} @@ -720,8 +720,8 @@ export const RegisterScreen: React.FC = () => { color="#FFF" /> - 创建账号 - 加入胡萝卜,开始你的旅程 + 加入萝卜社区 + 加入萝卜社区,发现有趣的内容 {/* 表单 */} diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index c55e5ad..c889eb0 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -13,7 +13,6 @@ import { RefreshControl, StatusBar, TouchableOpacity, - NativeScrollEvent, NativeSyntheticEvent, Alert, Clipboard, @@ -33,6 +32,8 @@ import { PostCard, TabBar, SearchBar } from '../../components/business'; import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common'; import { HomeStackParamList, RootStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive'; +import { useCursorPagination } from '../../hooks/useCursorPagination'; +import { CursorPaginationRequest } from '../../types/dto'; import { SearchScreen } from './SearchScreen'; import { CreatePostScreen } from '../create/CreatePostScreen'; import { navigationService } from '../../infrastructure/navigation/navigationService'; @@ -42,8 +43,6 @@ type NavigationProp = NativeStackNavigationProp & Na const TABS = ['推荐', '关注', '热门', '最新']; const TAB_ICONS = ['compass-outline', 'account-heart-outline', 'fire', 'clock-outline']; const DEFAULT_PAGE_SIZE = 20; -const SCROLL_BOTTOM_THRESHOLD = 240; -const LOAD_MORE_COOLDOWN_MS = 800; const SWIPE_TRANSLATION_THRESHOLD = 40; const SWIPE_COOLDOWN_MS = 300; const MOBILE_TAB_BAR_HEIGHT = 64; @@ -51,11 +50,12 @@ const MOBILE_TAB_FLOATING_MARGIN = 12; const MOBILE_FAB_GAP = 12; type ViewMode = 'list' | 'grid'; +type PostType = 'recommend' | 'follow' | 'hot' | 'latest'; export const HomeScreen: React.FC = () => { const navigation = useNavigation(); const insets = useSafeAreaInsets(); - const { fetchPosts, likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore(); + const { likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore(); const currentUser = useCurrentUser(); // 使用响应式 hook @@ -65,9 +65,6 @@ export const HomeScreen: React.FC = () => { isTablet, isDesktop, isWideScreen, - breakpoint, - orientation, - isLandscape } = useResponsive(); // 响应式间距 @@ -75,12 +72,6 @@ export const HomeScreen: React.FC = () => { const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); const [activeIndex, setActiveIndex] = useState(0); - const [posts, setPosts] = useState([]); - const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - const [loadingMore, setLoadingMore] = useState(false); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); const [viewMode, setViewMode] = useState('list'); // 图片查看器状态 @@ -95,18 +86,56 @@ export const HomeScreen: React.FC = () => { const [showCreatePost, setShowCreatePost] = useState(false); // 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态 - const postIdsRef = React.useRef>(new Set()); - const inFlightRequestKeysRef = React.useRef>(new Set()); - const lastLoadMoreTriggerAtRef = useRef(0); + const postIdsRef = useRef>(new Set()); const lastSwipeAtRef = useRef(0); - // 用 ref 同步关键状态,避免 onWaterfallScroll 的陈旧闭包问题 - const pageRef = useRef(page); - const loadingMoreRef = useRef(loadingMore); - const hasMoreRef = useRef(hasMore); - pageRef.current = page; - loadingMoreRef.current = loadingMore; - hasMoreRef.current = hasMore; + // 获取当前 tab 对应的帖子类型 + const getPostType = useCallback((): PostType => { + switch (activeIndex) { + case 0: return 'recommend'; + case 1: return 'follow'; + case 2: return 'hot'; + case 3: return 'latest'; + default: return 'recommend'; + } + }, [activeIndex]); + + // 使用游标分页获取帖子列表 + const { + items: posts, + isLoading, + isRefreshing, + hasMore, + error, + loadMore, + refresh, + } = useCursorPagination( + async ({ cursor, pageSize, extraParams }) => { + const params: CursorPaginationRequest = { + cursor, + page_size: pageSize, + post_type: extraParams?.post_type, + }; + const response = await postService.getPostsCursor(params); + return response; + }, + { pageSize: DEFAULT_PAGE_SIZE }, + { post_type: getPostType() } + ); + + // Tab 切换时刷新数据 + useEffect(() => { + refresh(); + }, [activeIndex]); + + // 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新) + useEffect(() => { + if (posts.length === 0) return; + + // 更新 postIdsRef + const currentPostIds = new Set(posts.map(p => p.id)); + postIdsRef.current = currentPostIds; + }, [posts, storePosts]); // 根据屏幕尺寸确定网格列数 const gridColumns = useMemo(() => { @@ -154,147 +183,6 @@ export const HomeScreen: React.FC = () => { return insets.bottom + MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP - MOBILE_TAB_BAR_HEIGHT; }, [isMobile, insets.bottom]); - const appendUniquePosts = useCallback((prevPosts: Post[], incomingPosts: Post[]) => { - if (incomingPosts.length === 0) return prevPosts; - const seenIds = new Set(prevPosts.map(item => item.id)); - const dedupedIncoming = incomingPosts.filter(item => { - if (seenIds.has(item.id)) return false; - seenIds.add(item.id); - return true; - }); - return dedupedIncoming.length > 0 ? [...prevPosts, ...dedupedIncoming] : prevPosts; - }, []); - - const uniquePostsById = useCallback((items: Post[]) => { - if (items.length <= 1) return items; - const map = new Map(); - for (const item of items) { - map.set(item.id, item); - } - return Array.from(map.values()); - }, []); - - const getPostType = (): 'recommend' | 'follow' | 'hot' | 'latest' => { - switch (activeIndex) { - case 0: return 'recommend'; - case 1: return 'follow'; - case 2: return 'hot'; - case 3: return 'latest'; - default: return 'recommend'; - } - }; - - // 加载帖子列表 - const loadPosts = useCallback(async (pageNum: number = 1, isRefresh: boolean = false) => { - const postType = getPostType(); - const requestKey = `${postType}:${pageNum}`; - if (inFlightRequestKeysRef.current.has(requestKey)) { - return; - } - - try { - inFlightRequestKeysRef.current.add(requestKey); - if (isRefresh) { - setRefreshing(true); - } else if (pageNum === 1) { - setLoading(true); - } else { - setLoadingMore(true); - } - - const response = await fetchPosts(postType, pageNum); - const newPosts = response.list || []; - - if (isRefresh) { - setPosts(uniquePostsById(newPosts)); - setPage(1); - } else if (pageNum === 1) { - setPosts(uniquePostsById(newPosts)); - setPage(1); - } else { - setPosts(prev => appendUniquePosts(prev, newPosts)); - setPage(pageNum); - } - - const hasMoreByPage = response.total_pages > 0 ? response.page < response.total_pages : false; - const hasMoreBySize = newPosts.length >= (response.page_size || DEFAULT_PAGE_SIZE); - setHasMore(hasMoreByPage || hasMoreBySize); - } catch (error) { - console.error('Failed to load posts:', error); - } finally { - inFlightRequestKeysRef.current.delete(requestKey); - setLoading(false); - setRefreshing(false); - setLoadingMore(false); - } - }, [fetchPosts, activeIndex, appendUniquePosts, uniquePostsById]); - - // 切换Tab时重新加载 - useEffect(() => { - loadPosts(1, true); - }, [activeIndex]); - - // 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新) - useEffect(() => { - if (posts.length === 0) return; - - // 更新 postIdsRef - const currentPostIds = new Set(posts.map(p => p.id)); - postIdsRef.current = currentPostIds; - - // 从 store 中找到对应的帖子并同步状态 - let hasChanges = false; - const updatedPosts = posts.map(localPost => { - const storePost = storePosts.find(sp => sp.id === localPost.id); - if (storePost && ( - storePost.is_liked !== localPost.is_liked || - storePost.is_favorited !== localPost.is_favorited || - storePost.likes_count !== localPost.likes_count || - storePost.favorites_count !== localPost.favorites_count - )) { - hasChanges = true; - return { - ...localPost, - is_liked: storePost.is_liked, - is_favorited: storePost.is_favorited, - likes_count: storePost.likes_count, - favorites_count: storePost.favorites_count, - }; - } - return localPost; - }); - - if (hasChanges) { - setPosts(updatedPosts); - } - }, [storePosts]); - - // 下拉刷新 - const onRefresh = useCallback(() => { - loadPosts(1, true); - }, [loadPosts]); - - // 上拉加载更多 - const onEndReached = useCallback(() => { - if (!loadingMoreRef.current && hasMoreRef.current) { - loadPosts(pageRef.current + 1); - } - }, [loadPosts]); - - const onWaterfallScroll = useCallback((event: NativeSyntheticEvent) => { - if (loadingMoreRef.current || !hasMoreRef.current) return; - const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; - const distanceToBottom = contentSize.height - (contentOffset.y + layoutMeasurement.height); - const now = Date.now(); - if (distanceToBottom <= SCROLL_BOTTOM_THRESHOLD) { - if (now - lastLoadMoreTriggerAtRef.current < LOAD_MORE_COOLDOWN_MS) { - return; - } - lastLoadMoreTriggerAtRef.current = now; - loadPosts(pageRef.current + 1); - } - }, [loadPosts]); - // 切换视图模式 const toggleViewMode = () => { setViewMode(prev => prev === 'list' ? 'grid' : 'list'); @@ -375,27 +263,27 @@ export const HomeScreen: React.FC = () => { if (!post?.id) return; try { await postService.sharePost(post.id); - } catch (error) { - console.error('上报分享次数失败:', error); + } catch (shareError) { + console.error('上报分享次数失败:', shareError); } const postUrl = `https://browser.littlelan.cn/posts/${encodeURIComponent(post.id)}`; Clipboard.setString(postUrl); Alert.alert('已复制', '帖子链接已复制到剪贴板'); }; - // 删除帖子 + // 删除帖子 - 由于 posts 来自 Hook,需要刷新列表 const handleDeletePost = async (postId: string) => { try { const success = await postService.deletePost(postId); if (success) { - // 从列表中移除已删除的帖子 - setPosts(prev => prev.filter(p => p.id !== postId)); + // 刷新列表以移除已删除的帖子 + refresh(); } else { console.error('删除帖子失败'); } - } catch (error) { - console.error('删除帖子失败:', error); - throw error; // 重新抛出错误,让 PostCard 处理错误提示 + } catch (deleteError) { + console.error('删除帖子失败:', deleteError); + throw deleteError; // 重新抛出错误,让 PostCard 处理错误提示 } }; @@ -545,12 +433,11 @@ export const HomeScreen: React.FC = () => { } ]} showsVerticalScrollIndicator={false} - onScroll={onWaterfallScroll} scrollEventThrottle={100} refreshControl={ @@ -594,7 +481,7 @@ export const HomeScreen: React.FC = () => { // 渲染空状态 const renderEmpty = () => { - if (loading) return null; + if (isLoading) return null; return ( { showsVerticalScrollIndicator={false} refreshControl={ } - onEndReached={onEndReached} + onEndReached={loadMore} onEndReachedThreshold={0.3} ListEmptyComponent={renderEmpty} - ListFooterComponent={loadingMore ? : null} + ListFooterComponent={isLoading ? : null} /> ); }; diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index af3d886..b04f000 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -32,6 +32,7 @@ import { Post, Comment, VoteResultDTO } from '../../types'; import { useUserStore } from '../../stores'; import { useCurrentUser } from '../../stores/authStore'; import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services'; +import { useCursorPagination } from '../../hooks/useCursorPagination'; import { CommentItem, VoteCard } from '../../components/business'; import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout } from '../../components/common'; import { RootStackParamList } from '../../navigation/types'; @@ -76,6 +77,30 @@ export const PostDetailScreen: React.FC = () => { const [comments, setComments] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); + + // 使用游标分页 Hook 管理评论列表 + const { + items: paginatedComments, + isLoading: isCommentsLoading, + isRefreshing: isCommentsRefreshing, + hasMore: hasMoreComments, + loadMore: loadMoreComments, + refresh: refreshComments, + error: commentsError, + } = useCursorPagination( + async ({ cursor, pageSize }) => { + return await commentService.getPostCommentsCursor(postId, { + cursor, + page_size: pageSize, + }); + }, + { pageSize: 20 } + ); + + // 同步分页评论到本地状态 + useEffect(() => { + setComments(paginatedComments); + }, [paginatedComments]); const [commentText, setCommentText] = useState(''); const [showImageModal, setShowImageModal] = useState(false); const [selectedImageIndex, setSelectedImageIndex] = useState(0); @@ -165,9 +190,8 @@ export const PostDetailScreen: React.FC = () => { } } - // 加载评论 - const commentsData = await commentService.getPostComments(postId); - setComments(commentsData.list); + // 加载评论(使用游标分页刷新) + await refreshComments(); } catch (error) { console.error('加载帖子详情失败:', error); } finally { @@ -261,12 +285,13 @@ export const PostDetailScreen: React.FC = () => { }; }, []); - // 下拉刷新 - const onRefresh = useCallback(() => { + // 下拉刷新 - 同时刷新帖子和评论 + const onRefresh = useCallback(async () => { setRefreshing(true); - loadPostDetail(); + await loadPostDetail(); + await refreshComments(); setRefreshing(false); - }, [loadPostDetail]); + }, [loadPostDetail, refreshComments]); const formatDateTime = (dateString?: string | null): string => { if (!dateString) return ''; @@ -1375,12 +1400,34 @@ export const PostDetailScreen: React.FC = () => { }} refreshControl={ } + onEndReached={loadMoreComments} + onEndReachedThreshold={0.3} + ListFooterComponent={ + isCommentsLoading ? ( + + + + ) : hasMoreComments ? ( + + + 加载更多评论 + + + ) : comments.length > 0 ? ( + + 没有更多评论了 + + ) : null + } /> ); @@ -1433,7 +1480,7 @@ export const PostDetailScreen: React.FC = () => { showsVerticalScrollIndicator={false} refreshControl={ { // 移动端单栏布局 return ( - { }} refreshControl={ } + onEndReached={loadMoreComments} + onEndReachedThreshold={0.3} + ListFooterComponent={ + isCommentsLoading ? ( + + + + ) : hasMoreComments ? ( + + + 加载更多评论 + + + ) : comments.length > 0 ? ( + + 没有更多评论了 + + ) : null + } /> {/* 评论输入框 - 跟随键盘 */} @@ -1905,4 +1974,22 @@ const styles = StyleSheet.create({ marginTop: spacing.md, textAlign: 'center', }, + // 评论加载更多样式 + commentsLoadingFooter: { + paddingVertical: spacing.md, + alignItems: 'center', + justifyContent: 'center', + }, + loadMoreButton: { + paddingVertical: spacing.md, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.background.paper, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: colors.divider, + }, + noMoreComments: { + textAlign: 'center', + paddingVertical: spacing.md, + }, }); diff --git a/src/screens/home/SearchScreen.tsx b/src/screens/home/SearchScreen.tsx index ac4774a..0e728a4 100644 --- a/src/screens/home/SearchScreen.tsx +++ b/src/screens/home/SearchScreen.tsx @@ -4,13 +4,14 @@ * 支持响应式布局,宽屏下显示更大的搜索结果区域 */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, ScrollView, + RefreshControl, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; @@ -21,13 +22,15 @@ import { Post, User } from '../../types'; import { useUserStore } from '../../stores'; import { postService, authService } from '../../services'; import { PostCard, TabBar, SearchBar } from '../../components/business'; -import { Avatar, EmptyState, Text, ResponsiveGrid } from '../../components/common'; +import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common'; import { HomeStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks/useResponsive'; +import { useCursorPagination } from '../../hooks/useCursorPagination'; type NavigationProp = NativeStackNavigationProp; const TABS = ['帖子', '用户']; +const DEFAULT_PAGE_SIZE = 20; type SearchType = 'posts' | 'users'; @@ -74,17 +77,47 @@ export const SearchScreen: React.FC = ({ onBack, navigation: const [searchText, setSearchText] = useState(''); const [activeIndex, setActiveIndex] = useState(0); - const [searchResults, setSearchResults] = useState<{ - posts: Post[]; - users: User[]; - }>({ - posts: [], - users: [], - }); const [hasSearched, setHasSearched] = useState(false); // 保存当前搜索关键词,用于Tab切换时重新搜索 const [currentKeyword, setCurrentKeyword] = useState(''); + // 使用游标分页进行帖子搜索 + const { + items: searchResults, + isLoading, + isRefreshing, + hasMore, + loadMore, + refresh, + reset, + } = useCursorPagination( + async ({ cursor, pageSize, extraParams }) => { + if (!extraParams?.query) { + return { items: [], next_cursor: null, prev_cursor: null, has_more: false }; + } + const response = await postService.searchPostsCursor(extraParams.query, { + cursor, + page_size: pageSize, + }); + return response; + }, + { pageSize: DEFAULT_PAGE_SIZE }, + { query: '' } + ); + + // 用户搜索结果(保持原有分页方式) + const [userResults, setUserResults] = useState([]); + const [userLoading, setUserLoading] = useState(false); + + // 当搜索词变化时重置 + useEffect(() => { + if (currentKeyword) { + refresh(); + } else { + reset(); + } + }, [currentKeyword]); + // 执行搜索 - 根据当前Tab执行对应类型的搜索 const performSearch = useCallback(async (keyword: string) => { if (!keyword.trim()) return; @@ -101,26 +134,20 @@ export const SearchScreen: React.FC = ({ onBack, navigation: const searchType = getSearchType(); if (searchType === 'posts') { - // 搜索帖子 - const postsResponse = await postService.searchPosts(trimmedKeyword, 1, 20); - setSearchResults(prev => ({ - ...prev, - posts: postsResponse.list - })); + // 帖子搜索由 useCursorPagination 处理,这里只需触发刷新 + refresh(); } else if (searchType === 'users') { - // 搜索用户 + // 用户搜索保持原有方式 + setUserLoading(true); const usersResponse = await authService.searchUsers(trimmedKeyword, 1, 20); - setSearchResults(prev => ({ - ...prev, - users: usersResponse.list - })); + setUserResults(usersResponse.list || []); } } catch (error) { console.error('搜索失败:', error); } setHasSearched(true); - }, [addSearchHistory, activeIndex]); + }, [addSearchHistory, activeIndex, refresh]); // 处理搜索提交 const handleSearch = () => { @@ -159,9 +186,9 @@ export const SearchScreen: React.FC = ({ onBack, navigation: // 渲染帖子搜索结果(使用响应式网格) const renderPostResults = () => { - const posts = searchResults.posts; + const posts = searchResults; - if (posts.length === 0) { + if (posts.length === 0 && !isLoading) { return ( = ({ onBack, navigation: + } > = ({ onBack, navigation: /> ))} + {isLoading && ( + + + + )} ); } @@ -220,15 +260,26 @@ export const SearchScreen: React.FC = ({ onBack, navigation: keyExtractor={item => item.id} contentContainerStyle={{ paddingBottom: responsivePadding }} showsVerticalScrollIndicator={false} + refreshControl={ + + } + onEndReached={loadMore} + onEndReachedThreshold={0.3} + ListFooterComponent={isLoading ? : null} /> ); }; // 渲染用户搜索结果 const renderUserResults = () => { - const users = searchResults.users; + const users = userResults; - if (users.length === 0) { + if (users.length === 0 && !userLoading) { return ( = ({ onBack, navigation: ))} + {userLoading && ( + + + + )} ); } @@ -329,6 +385,7 @@ export const SearchScreen: React.FC = ({ onBack, navigation: keyExtractor={item => item.id} contentContainerStyle={{ paddingVertical: responsiveGap }} showsVerticalScrollIndicator={false} + ListFooterComponent={userLoading ? : null} /> ); }; @@ -501,20 +558,20 @@ const styles = StyleSheet.create({ }, tabWrapper: { backgroundColor: colors.background.paper, - paddingTop: spacing.xs, - paddingBottom: spacing.xs, + borderBottomWidth: 1, + borderBottomColor: `${colors.divider}50`, }, suggestionsContainer: { flex: 1, }, section: { - marginTop: spacing.lg, + marginTop: spacing.md, }, sectionHeader: { flexDirection: 'row', - alignItems: 'center', justifyContent: 'space-between', - marginBottom: spacing.md, + alignItems: 'center', + marginBottom: spacing.sm, }, sectionTitle: { fontWeight: '600', @@ -528,17 +585,32 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.paper, - borderRadius: borderRadius.md, + borderRadius: borderRadius.lg, + borderWidth: 1, + borderColor: colors.divider, }, tagText: { marginLeft: spacing.xs, }, - // 移动端用户列表样式 + userCard: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + flexDirection: 'row', + alignItems: 'center', + }, + userCardInfo: { + flex: 1, + marginLeft: spacing.md, + }, + userCardName: { + fontWeight: '600', + color: colors.text.primary, + }, userItem: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.paper, - borderRadius: borderRadius.md, + borderRadius: borderRadius.lg, }, userInfo: { flex: 1, @@ -549,28 +621,15 @@ const styles = StyleSheet.create({ color: colors.text.primary, }, followingBadge: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: colors.primary.light + '30', + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: `${colors.primary.main}14`, alignItems: 'center', justifyContent: 'center', }, - // 桌面端用户卡片样式 - userCard: { - backgroundColor: colors.background.paper, - borderRadius: borderRadius.lg, + loadingMore: { + paddingVertical: spacing.md, alignItems: 'center', - justifyContent: 'center', - minHeight: 180, - }, - userCardInfo: { - alignItems: 'center', - marginTop: spacing.md, - }, - userCardName: { - fontWeight: '600', - color: colors.text.primary, - marginBottom: spacing.xs, }, }); diff --git a/src/screens/message/ChatScreen.tsx b/src/screens/message/ChatScreen.tsx index 3bf4b36..5dafe28 100644 --- a/src/screens/message/ChatScreen.tsx +++ b/src/screens/message/ChatScreen.tsx @@ -19,15 +19,15 @@ * - ChatInput.tsx: 输入框组件 */ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { View, - FlatList, TouchableWithoutFeedback, ActivityIndicator, KeyboardAvoidingView, Platform, } from 'react-native'; +import { FlashList } from "@shopify/flash-list"; import { useNavigation } from '@react-navigation/native'; import { StatusBar } from 'expo-status-bar'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -224,6 +224,7 @@ export const ChatScreen: React.FC = () => { formatTime={formatTime} shouldShowTime={shouldShowTime} onImagePress={handleImagePress} + onReply={handleReplyMessage} /> ); @@ -268,25 +269,23 @@ export const ChatScreen: React.FC = () => { ) : ( - String(item.id)} - contentContainerStyle={listContentStyle} + keyExtractor={(item: any) => String(item.id)} + contentContainerStyle={listContentStyle as any} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled" refreshing={loadingMore} onRefresh={hasMoreHistory ? loadMoreHistory : undefined} - progressViewOffset={0} - onContentSizeChange={handleMessageListContentSizeChange} + // @ts-ignore FlashList 类型定义问题:estimatedItemSize 是必需属性但类型定义缺失 + estimatedItemSize={80} + maintainVisibleContentPosition={{ + minIndexForVisible: 0, + autoscrollToTopThreshold: undefined, + } as any} onScroll={handleMessageListScroll} - scrollEventThrottle={16} - // 优化渲染性能 - initialNumToRender={15} - maxToRenderPerBatch={10} - windowSize={10} - removeClippedSubviews={true} /> )} diff --git a/src/screens/message/GroupMembersScreen.tsx b/src/screens/message/GroupMembersScreen.tsx index 3612f99..ffb281b 100644 --- a/src/screens/message/GroupMembersScreen.tsx +++ b/src/screens/message/GroupMembersScreen.tsx @@ -2,6 +2,7 @@ * GroupMembersScreen 群成员管理界面 * 显示群成员列表,支持管理员管理成员 * 支持响应式网格布局 + * 使用游标分页 */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; @@ -27,6 +28,7 @@ import { groupService } from '../../services/groupService'; import { groupManager } from '../../stores/groupManager'; import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; +import { useCursorPagination } from '../../hooks/useCursorPagination'; import { GroupMemberResponse, GroupRole, @@ -68,12 +70,34 @@ const GroupMembersScreen: React.FC = () => { return GRID_CONFIG.mobile; }, [width]); - // 成员列表状态 - const [members, setMembers] = useState([]); + // 使用游标分页 Hook 管理成员列表 + const { + items: members, + isLoading, + isRefreshing, + hasMore, + loadMore, + refresh, + error, + } = useCursorPagination( + async ({ cursor, pageSize }) => { + return await groupService.getGroupMembersCursor(groupId, { + cursor, + page_size: pageSize, + }); + }, + { pageSize: 50 } + ); + + // 本地成员状态(用于乐观更新) + const [localMembers, setLocalMembers] = useState([]); const [loading, setLoading] = useState(true); - const [refreshing, setRefreshing] = useState(false); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); + + // 同步分页数据到本地状态 + useEffect(() => { + setLocalMembers(members); + setLoading(false); + }, [members]); // 当前用户的成员信息 const [currentMember, setCurrentMember] = useState(null); @@ -91,65 +115,31 @@ const GroupMembersScreen: React.FC = () => { const isOwner = currentMember?.role === 'owner'; const isAdmin = currentMember?.role === 'admin' || isOwner; - // 加载成员列表 - const loadMembers = useCallback( - async ( - pageNum: number = 1, - refresh: boolean = false, - forceRefresh: boolean = false - ) => { - if (!hasMore && !refresh) return; - - try { - const response = await groupManager.getMembers(groupId, pageNum, 50, forceRefresh); - - if (refresh) { - setMembers(response.list); - setPage(1); - } else { - setMembers(prev => [...prev, ...response.list]); - } - - setHasMore(response.list.length === 50); - - // 查找当前用户的成员信息 - const myMember = response.list.find(m => m.user_id === currentUser?.id); - if (myMember) { - setCurrentMember(myMember); - } - } catch (error) { - console.error('加载成员列表失败:', error); + // 查找当前用户的成员信息 + useEffect(() => { + const myMember = localMembers.find(m => m.user_id === currentUser?.id); + if (myMember) { + setCurrentMember(myMember); } + }, [localMembers, currentUser]); + + // 下拉刷新 + const onRefresh = useCallback(async () => { + setLoading(true); + await refresh(); setLoading(false); - setRefreshing(false); - }, [groupId, currentUser, hasMore]); + }, [refresh]); // 初始加载 useEffect(() => { - loadMembers(1, true, true); + refresh(); }, [groupId]); - // 下拉刷新 - const onRefresh = useCallback(() => { - setRefreshing(true); - setHasMore(true); - loadMembers(1, true, true); - }, [loadMembers]); - - // 加载更多 - const loadMore = useCallback(() => { - if (!loading && hasMore) { - const nextPage = page + 1; - setPage(nextPage); - loadMembers(nextPage); - } - }, [loading, hasMore, page, loadMembers]); - // 按角色分组 const groupMembers = useCallback((): MemberGroup[] => { - const owners = members.filter(m => m.role === 'owner'); - const admins = members.filter(m => m.role === 'admin'); - const normalMembers = members.filter(m => m.role === 'member'); + const owners = localMembers.filter(m => m.role === 'owner'); + const admins = localMembers.filter(m => m.role === 'admin'); + const normalMembers = localMembers.filter(m => m.role === 'member'); const groups: MemberGroup[] = []; @@ -164,7 +154,7 @@ const GroupMembersScreen: React.FC = () => { } return groups; - }, [members]); + }, [localMembers]); // 打开操作菜单 const openActionModal = (member: GroupMemberResponse) => { @@ -203,7 +193,7 @@ const GroupMembersScreen: React.FC = () => { }); // 更新本地数据 - setMembers(prev => prev.map(m => { + setLocalMembers(prev => prev.map(m => { if (m.user_id === selectedMember.user_id) { return { ...m, role: newRole }; } @@ -245,14 +235,14 @@ const GroupMembersScreen: React.FC = () => { await groupService.muteMember(groupId, selectedMember.user_id, newMuted ? -1 : 0); // 更新本地数据 - setMembers(prev => prev.map(m => { + setLocalMembers(prev => prev.map(m => { if (m.user_id === selectedMember.user_id) { return { ...m, muted: newMuted }; } return m; })); // 强制刷新远端状态,避免命中旧缓存导致解禁后仍显示禁言 - await loadMembers(1, true, true); + await refresh(); setActionModalVisible(false); Alert.alert('成功', `已${actionText}`); @@ -286,7 +276,7 @@ const GroupMembersScreen: React.FC = () => { await groupService.removeMember(groupId, selectedMember.user_id); // 更新本地数据 - setMembers(prev => prev.filter(m => m.user_id !== selectedMember.user_id)); + setLocalMembers(prev => prev.filter(m => m.user_id !== selectedMember.user_id)); setActionModalVisible(false); Alert.alert('成功', '已移除成员'); @@ -320,7 +310,7 @@ const GroupMembersScreen: React.FC = () => { }); // 更新本地数据 - setMembers(prev => prev.map(m => { + setLocalMembers(prev => prev.map(m => { if (m.user_id === selectedMember.user_id) { return { ...m, nickname: newNickname.trim() }; } @@ -422,7 +412,7 @@ const GroupMembersScreen: React.FC = () => { // 渲染空状态 const renderEmpty = () => { - if (loading) return ; + if (loading || isLoading) return ; return ( { keyExtractor={(item) => item.title} refreshControl={ { onEndReached={loadMore} onEndReachedThreshold={0.3} showsVerticalScrollIndicator={false} + ListFooterComponent={ + isLoading ? ( + + + + ) : hasMore ? ( + + + 加载更多成员 + + + ) : localMembers.length > 0 ? ( + + 没有更多成员了 + + ) : null + } renderItem={({ item: group }) => ( {renderSectionHeader(group.title, group.data.length)} @@ -724,6 +731,19 @@ const styles = StyleSheet.create({ flex: 1, marginHorizontal: spacing.xs, }, + // 分页加载样式 + loadingFooter: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + loadMoreBtn: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + noMoreText: { + textAlign: 'center', + paddingVertical: spacing.md, + }, }); export default GroupMembersScreen; diff --git a/src/screens/message/JoinGroupScreen.tsx b/src/screens/message/JoinGroupScreen.tsx index 378b1f1..e6df582 100644 --- a/src/screens/message/JoinGroupScreen.tsx +++ b/src/screens/message/JoinGroupScreen.tsx @@ -1,5 +1,15 @@ -import React, { useState } from 'react'; -import { View, StyleSheet, TextInput, TouchableOpacity, Alert, ActivityIndicator, Clipboard } from 'react-native'; +import React, { useState, useCallback } from 'react'; +import { + View, + StyleSheet, + TextInput, + TouchableOpacity, + Alert, + ActivityIndicator, + Clipboard, + FlatList, + RefreshControl, +} from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { MaterialCommunityIcons } from '@expo/vector-icons'; @@ -11,6 +21,8 @@ import { groupService } from '../../services/groupService'; import { groupManager } from '../../stores/groupManager'; import { RootStackParamList } from '../../navigation/types'; import { GroupResponse, JoinType } from '../../types/dto'; +import { useCursorPagination } from '../../hooks/useCursorPagination'; +import { EmptyState } from '../../components/common'; type NavigationProp = NativeStackNavigationProp; @@ -18,10 +30,29 @@ const JoinGroupScreen: React.FC = () => { const navigation = useNavigation(); const [keyword, setKeyword] = useState(''); const [searching, setSearching] = useState(false); - const [joining, setJoining] = useState(false); - const [group, setGroup] = useState(null); + const [joiningGroupId, setJoiningGroupId] = useState(null); + const [searchedGroup, setSearchedGroup] = useState(null); const [searched, setSearched] = useState(false); + // 使用游标分页 Hook 管理群组列表 + const { + items: groups, + isLoading, + isRefreshing, + hasMore, + loadMore, + refresh, + error, + } = useCursorPagination( + async ({ cursor, pageSize }) => { + return await groupService.getGroupsCursor({ + cursor, + page_size: pageSize, + }); + }, + { pageSize: 20 } + ); + const getJoinTypeText = (joinType: JoinType) => { if (joinType === 0) return '允许加入'; if (joinType === 1) return '需要审批'; @@ -39,9 +70,9 @@ const JoinGroupScreen: React.FC = () => { setSearched(true); try { const result = await groupManager.getGroup(trimmed, true); - setGroup(result); + setSearchedGroup(result); } catch (error: any) { - setGroup(null); + setSearchedGroup(null); const message = error?.response?.data?.message || error?.message || ''; if (String(message).includes('不存在') || error?.response?.status === 404) { Alert.alert('未找到', '未搜索到该群聊,请确认群ID是否正确'); @@ -53,9 +84,9 @@ const JoinGroupScreen: React.FC = () => { } }; - const handleJoin = async () => { + const handleJoin = async (group: GroupResponse) => { if (!group?.id) return; - setJoining(true); + setJoiningGroupId(String(group.id)); try { await groupService.joinGroup(group.id); Alert.alert('成功', '操作已提交', [ @@ -71,13 +102,12 @@ const JoinGroupScreen: React.FC = () => { '操作失败,请稍后重试'; Alert.alert('操作失败', String(message)); } finally { - setJoining(false); + setJoiningGroupId(null); } }; - const handleCopyGroupId = () => { - if (!group?.id) return; - Clipboard.setString(String(group.id)); + const handleCopyGroupId = (groupId: string | number) => { + Clipboard.setString(String(groupId)); Alert.alert('已复制', '群号已复制到剪贴板'); }; @@ -87,6 +117,99 @@ const JoinGroupScreen: React.FC = () => { return `${raw.slice(0, 6)}...${raw.slice(-4)}`; }; + const renderGroupItem = ({ item: group }: { item: GroupResponse }) => { + const isJoining = joiningGroupId === String(group.id); + + return ( + + + + + + {group.name} + + + + {!!group.description && ( + + {group.description} + + )} + + + 成员 {group.member_count}/{group.max_members} + + + {getJoinTypeText(group.join_type)} + + + + + 群号:{formatGroupNo(group.id)} + + handleCopyGroupId(group.id)}> + + + 复制 + + + + handleJoin(group)} + disabled={isJoining} + > + {isJoining ? ( + + ) : ( + <> + + + 申请入群 + + + )} + + + ); + }; + + const renderEmptyList = () => { + if (isLoading) return null; + return ( + + ); + }; + + const renderSearchResult = () => { + if (!searched) return null; + + if (searchedGroup) { + return ( + + + 搜索结果 + + {renderGroupItem({ item: searchedGroup })} + + ); + } + + if (!searching) { + return ( + + 暂无搜索结果,请检查群ID后重试 + + ); + } + + return null; + }; + return ( @@ -100,23 +223,25 @@ const JoinGroupScreen: React.FC = () => { - 搜索群聊(群ID) + + 搜索群聊(群ID) + {searching ? ( @@ -126,58 +251,49 @@ const JoinGroupScreen: React.FC = () => { - {group && ( - - - - - {group.name} - - - {!!group.description && ( - - {group.description} - - )} - - - 成员 {group.member_count}/{group.max_members} - - - {getJoinTypeText(group.join_type)} - - - - - 群号:{formatGroupNo(group.id)} - - - - 复制 - - - - {joining ? ( - - ) : ( - <> - - 申请入群 - - )} - - - )} + {/* 搜索结果 */} + {renderSearchResult()} - {searched && !group && !searching && ( - - 暂无搜索结果,请检查群ID后重试 + {/* 群组列表 */} + + + 推荐群组 - )} + String(item.id)} + refreshControl={ + + } + onEndReached={loadMore} + onEndReachedThreshold={0.3} + ListEmptyComponent={renderEmptyList} + ListFooterComponent={ + isLoading ? ( + + + + ) : hasMore ? ( + + + 加载更多 + + + ) : groups.length > 0 ? ( + + 没有更多群组了 + + ) : null + } + showsVerticalScrollIndicator={false} + /> + ); @@ -214,6 +330,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.background.paper, borderRadius: borderRadius.lg, padding: spacing.lg, + flex: 1, }, label: { marginBottom: spacing.xs, @@ -242,12 +359,23 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginLeft: spacing.sm, }, + searchResultSection: { + marginBottom: spacing.lg, + }, + sectionTitle: { + marginBottom: spacing.sm, + fontWeight: '600', + }, + listSection: { + flex: 1, + }, groupCard: { borderWidth: 1, borderColor: colors.divider, borderRadius: borderRadius.md, padding: spacing.md, backgroundColor: colors.background.default, + marginBottom: spacing.md, }, groupHeader: { flexDirection: 'row', @@ -259,6 +387,7 @@ const styles = StyleSheet.create({ }, groupName: { marginBottom: spacing.xs, + fontWeight: '600', }, groupDesc: { marginTop: spacing.sm, @@ -303,6 +432,19 @@ const styles = StyleSheet.create({ }, emptyText: { marginTop: spacing.sm, + textAlign: 'center', + }, + loadingFooter: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + loadMoreBtn: { + paddingVertical: spacing.md, + alignItems: 'center', + }, + noMoreText: { + textAlign: 'center', + paddingVertical: spacing.md, }, }); diff --git a/src/screens/message/MessageListScreen.tsx b/src/screens/message/MessageListScreen.tsx index 217ca1d..918e8f7 100644 --- a/src/screens/message/MessageListScreen.tsx +++ b/src/screens/message/MessageListScreen.tsx @@ -30,18 +30,21 @@ import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme'; import { ConversationResponse, UserDTO, MessageResponse, extractTextFromSegments, extractTextFromSegmentsAsync, MessageSegment } from '../../types/dto'; -import { authService } from '../../services'; +import { authService, messageService } from '../../services'; import { useUserStore, useAuthStore } from '../../stores'; // 【新架构】使用MessageManager hooks -import { useMessageList, messageManager, useMessageListRefresh, useCreateConversation } from '../../stores'; +import { messageManager, useMessageListRefresh, useCreateConversation, useUnreadCount, useMarkAsRead } from '../../stores'; import { Avatar, Text, EmptyState, ResponsiveContainer } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; +import { useCursorPagination } from '../../hooks/useCursorPagination'; import { RootStackParamList, MessageStackParamList } from '../../navigation/types'; import { getUserCache } from '../../services/database'; // 导入 EmbeddedChat 组件用于桌面端双栏布局 import { EmbeddedChat } from './components/EmbeddedChat'; // 导入 NotificationsScreen 用于在内部显示 import { NotificationsScreen } from './NotificationsScreen'; +// 导入扫码组件 +import { QRCodeScanner } from '../../components/business/QRCodeScanner'; type NavigationProp = NativeStackNavigationProp; type MessageNavProp = NativeStackNavigationProp; @@ -158,22 +161,43 @@ export const MessageListScreen: React.FC = () => { const { isDesktop, isTablet, width } = useResponsive(); const isWideScreen = useBreakpointGTE('lg'); - // 【新架构】使用MessageManager的hook获取数据 + // 【游标分页】使用 useCursorPagination hook 获取会话列表 const { - conversations, + items: conversations, isLoading, + isRefreshing, + hasMore, + loadMore, refresh, - totalUnreadCount, - systemUnreadCount, - markAllAsRead, - isMarking, - } = useMessageList(); + error: paginationError, + } = useCursorPagination( + async ({ cursor, pageSize }) => { + return await messageService.getConversationsCursor({ + cursor, + page_size: pageSize, + }); + }, + { pageSize: 20 } + ); + + // 使用 MessageManager 获取未读数和系统通知数 + const { totalUnreadCount, systemUnreadCount } = useUnreadCount(); + const { markAllAsRead, isMarking } = useMarkAsRead(null); // 【新架构】使用MessageManager的hook创建会话 const { createConversation } = useCreateConversation(); // 本地刷新状态(仅用于下拉刷新的UI显示) const [refreshing, setRefreshing] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + + // 上拉加载更多 + const onEndReached = useCallback(async () => { + if (isLoading || loadingMore || !hasMore) return; + setLoadingMore(true); + await loadMore(); + setLoadingMore(false); + }, [isLoading, loadingMore, hasMore, loadMore]); // 搜索相关状态 const [isSearchMode, setIsSearchMode] = useState(false); @@ -182,6 +206,7 @@ export const MessageListScreen: React.FC = () => { const [isSearching, setIsSearching] = useState(false); const [activeSearchTab, setActiveSearchTab] = useState<'chat' | 'user'>('chat'); const [actionMenuVisible, setActionMenuVisible] = useState(false); + const [scannerVisible, setScannerVisible] = useState(false); // 系统通知显示状态 - 用于在移动端显示通知页面 const [showNotifications, setShowNotifications] = useState(false); @@ -356,7 +381,7 @@ export const MessageListScreen: React.FC = () => { setActionMenuVisible(true); }; - const runMenuAction = (action: 'join' | 'create' | 'readAll') => { + const runMenuAction = (action: 'join' | 'create' | 'readAll' | 'scanQRCode') => { setActionMenuVisible(false); if (action === 'join') { handleJoinGroup(); @@ -366,6 +391,10 @@ export const MessageListScreen: React.FC = () => { handleCreateGroup(); return; } + if (action === 'scanQRCode') { + setScannerVisible(true); + return; + } handleMarkAllRead(); }; @@ -878,7 +907,7 @@ export const MessageListScreen: React.FC = () => { - {isLoading ? ( + {isLoading && conversations.length === 0 ? ( @@ -894,6 +923,8 @@ export const MessageListScreen: React.FC = () => { ]} showsVerticalScrollIndicator={false} ListEmptyComponent={renderEmpty} + onEndReached={onEndReached} + onEndReachedThreshold={0.5} refreshControl={ { tintColor={colors.primary.main} /> } + ListFooterComponent={ + loadingMore ? ( + + + 加载中... + + ) : null + } /> )} @@ -911,6 +950,10 @@ export const MessageListScreen: React.FC = () => { setActionMenuVisible(false)}> setActionMenuVisible(false)}> + runMenuAction('scanQRCode')}> + + 扫码登录 + runMenuAction('join')}> 加入群聊 @@ -980,6 +1023,7 @@ export const MessageListScreen: React.FC = () => { renderConversationList() )} {renderActionMenu()} + setScannerVisible(false)} /> ); }; @@ -1249,6 +1293,17 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingVertical: spacing.xl * 2, }, + loadingMoreContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: spacing.md, + }, + loadingMoreText: { + marginLeft: spacing.sm, + fontSize: 14, + color: '#999', + }, searchModeContainer: { flex: 1, backgroundColor: '#FAFAFA', diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index f7ddef1..bef0104 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -1,7 +1,7 @@ /** * 通知页 NotificationsScreen * 胡萝卜BBS - 系统消息列表 - * 使用新的系统消息API + * 【游标分页】使用 messageService.getSystemMessagesCursor * 支持响应式布局 */ @@ -26,6 +26,7 @@ import { messageService } from '../../services/messageService'; import { commentService } from '../../services/commentService'; import { SystemMessageItem } from '../../components/business'; import { Text, EmptyState, ResponsiveContainer } from '../../components/common'; +import { useCursorPagination } from '../../hooks/useCursorPagination'; import { RootStackParamList } from '../../navigation/types'; import { useMessageManagerSystemUnreadCount, useUserStore } from '../../stores'; @@ -70,14 +71,32 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack // Web端使用更大的容器宽度 const containerMaxWidth = isDesktop ? 1200 : isTablet ? 1000 : 900; - const [messages, setMessages] = useState([]); const [activeType, setActiveType] = useState('all'); - const [refreshing, setRefreshing] = useState(false); - const [loading, setLoading] = useState(true); - const [hasMore, setHasMore] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); const [unreadCount, setUnreadCount] = useState(0); + // 【游标分页】使用 useCursorPagination hook 获取系统消息列表 + const { + items: messages, + isLoading, + isRefreshing, + hasMore, + loadMore, + refresh, + error: paginationError, + } = useCursorPagination( + async ({ cursor, pageSize }) => { + return await messageService.getSystemMessagesCursor({ + cursor, + page_size: pageSize, + }); + }, + { pageSize: 20 } + ); + + // 本地刷新状态(仅用于下拉刷新的UI显示) + const [refreshing, setRefreshing] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + // 同一 flag 只要有人审批过,就将待处理消息同步展示为已处理状态 const displayMessages = useMemo(() => { const reviewedByFlag = new Map(); @@ -116,21 +135,6 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }); }, [messages]); - // 获取系统消息数据 - const fetchMessages = useCallback(async () => { - try { - setLoading(true); - const response = await messageService.getSystemMessages(50, 1); - // 添加防御性检查,确保 messages 数组存在 - setMessages(response.messages || []); - setHasMore(response.has_more ?? false); - } catch (error) { - console.error('获取系统消息失败:', error); - } finally { - setLoading(false); - } - }, []); - // 获取未读数 const fetchUnreadCount = useCallback(async () => { try { @@ -145,27 +149,25 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack const handleMarkAllRead = useCallback(async () => { try { await messageService.markAllSystemMessagesRead(); - setMessages(prev => prev.map(m => ({ ...m, is_read: true }))); + // 刷新消息列表 + await refresh(); setUnreadCount(0); setSystemUnreadCount(0); // 同步更新全局 TabBar 红点 fetchMessageUnreadCount(); - // 刷新消息列表 - fetchMessages(); } catch (error) { console.error('一键已读失败:', error); } - }, [fetchMessages, fetchMessageUnreadCount, setSystemUnreadCount]); + }, [refresh, fetchMessageUnreadCount, setSystemUnreadCount]); // 页面加载和获得焦点时刷新,并自动标记所有消息为已读 useEffect(() => { if (isFocused) { - fetchMessages(); fetchUnreadCount(); // 进入界面自动标记所有消息为已读 handleMarkAllRead(); } - }, [isFocused, fetchMessages, fetchUnreadCount, handleMarkAllRead]); + }, [isFocused, fetchUnreadCount, handleMarkAllRead]); // 屏幕失去焦点时,如果有 onBack 回调则调用它(用于内嵌模式) useEffect(() => { @@ -190,29 +192,17 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack // 下拉刷新 const onRefresh = useCallback(async () => { setRefreshing(true); - await Promise.all([fetchMessages(), fetchUnreadCount()]); + await Promise.all([refresh(), fetchUnreadCount()]); setRefreshing(false); - }, [fetchMessages, fetchUnreadCount]); + }, [refresh, fetchUnreadCount]); // 加载更多 - const loadMore = useCallback(async () => { - if (loadingMore || !hasMore || messages.length === 0) return; - - try { - setLoadingMore(true); - // 使用时间戳或seq作为游标分页(后端使用page分页) - const nextPage = Math.floor((messages.length / 20)) + 1; - const response = await messageService.getSystemMessages(20, nextPage); - // 添加防御性检查 - const newMessages = response.messages || []; - setMessages(prev => [...prev, ...newMessages]); - setHasMore(response.has_more ?? false); - } catch (error) { - console.error('加载更多失败:', error); - } finally { - setLoadingMore(false); - } - }, [loadingMore, hasMore, messages]); + const onEndReached = useCallback(async () => { + if (isLoading || loadingMore || !hasMore) return; + setLoadingMore(true); + await loadMore(); + setLoadingMore(false); + }, [isLoading, loadingMore, hasMore, loadMore]); // 标记单条消息已读并处理导航 const extractPostIdFromActionUrl = (actionUrl?: string): string | null => { @@ -262,9 +252,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack const messageId = String(message.id); const wasUnread = message.is_read !== true; await messageService.markSystemMessageRead(messageId); - setMessages(prev => - prev.map(m => (String(m.id) === messageId ? { ...m, is_read: true } : m)) - ); + // 【游标分页】不再直接修改 messages 状态,而是通过刷新获取最新数据 if (wasUnread) { setUnreadCount(prev => Math.max(0, prev - 1)); decrementSystemUnreadCount(1); @@ -431,7 +419,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack {/* 消息列表 */} - {loading ? ( + {isLoading && messages.length === 0 ? ( @@ -444,7 +432,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack showsVerticalScrollIndicator={false} ListEmptyComponent={renderEmpty} ListFooterComponent={renderFooter} - onEndReached={loadMore} + onEndReached={onEndReached} onEndReachedThreshold={0.3} refreshControl={ void }> = ({ onBack {/* 消息列表 */} - {loading ? ( + {isLoading && messages.length === 0 ? ( @@ -518,7 +506,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack showsVerticalScrollIndicator={false} ListEmptyComponent={renderEmpty} ListFooterComponent={renderFooter} - onEndReached={loadMore} + onEndReached={onEndReached} onEndReachedThreshold={0.3} refreshControl={ = ({ formatTime, shouldShowTime, onImagePress, + onReply, }) => { const bubbleRef = useRef(null); const pressPositionRef = useRef({ x: 0, y: 0 }); @@ -343,6 +352,13 @@ export const MessageBubble: React.FC = ({ ); }; + // 处理滑动回复 + const handleSwipeReply = useCallback(() => { + if (onReply) { + onReply(message); + } + }, [onReply, message]); + // 系统通知消息渲染 if (isSystemNotice) { return ( @@ -361,7 +377,8 @@ export const MessageBubble: React.FC = ({ ); } - return ( + // 消息内容渲染 + const messageContent = ( = ({ ); + + // 使用 SwipeableMessageBubble 包裹消息内容 + return ( + + {messageContent} + + ); }; // Segment 消息的特殊样式 - 自适应宽度 @@ -460,4 +488,16 @@ const segmentStyles = StyleSheet.create({ }, }); -export default MessageBubble; +// 使用 React.memo 优化渲染性能 +export default React.memo(MessageBubble, (prevProps, nextProps) => { + // 自定义比较逻辑:只比较关键属性 + return ( + prevProps.message.id === nextProps.message.id && + prevProps.message.status === nextProps.message.status && + prevProps.selectedMessageId === nextProps.selectedMessageId && + prevProps.currentUserId === nextProps.currentUserId && + prevProps.isGroupChat === nextProps.isGroupChat && + prevProps.otherUserLastReadSeq === nextProps.otherUserLastReadSeq && + prevProps.index === nextProps.index + ); +}); diff --git a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx index a7d92e0..a8eaa7c 100644 --- a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx +++ b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx @@ -290,6 +290,7 @@ const ImageSegment: React.FC<{ contentFit="cover" cachePolicy="disk" priority="normal" + transition={200} onError={() => { setLoadError(true); }} diff --git a/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx b/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx new file mode 100644 index 0000000..21dd0b6 --- /dev/null +++ b/src/screens/message/components/ChatScreen/SwipeableMessageBubble.tsx @@ -0,0 +1,157 @@ +/** + * 可滑动的消息气泡容器 + * 实现滑动回复手势功能 - 严格单向滑动,无抖动 + */ + +import React, { useRef, useCallback } from 'react'; +import { View, StyleSheet, Animated } from 'react-native'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { PanGestureHandler, State } from 'react-native-gesture-handler'; +import * as Haptics from 'expo-haptics'; +import { colors } from '../../../../theme'; + +// 滑动阈值 +const SWIPE_THRESHOLD = 30; +const MAX_SWIPE_DISTANCE = 50; + +interface SwipeableMessageBubbleProps { + children: React.ReactNode; + isMe: boolean; + onReply: () => void; + enabled?: boolean; +} + +export const SwipeableMessageBubble: React.FC = ({ + children, + isMe, + onReply, + enabled = true, +}) => { + const translateX = useRef(new Animated.Value(0)).current; + const hasTriggeredRef = useRef(false); + + // 处理手势事件 - 使用原生驱动,不干预值 + const onGestureEvent = Animated.event( + [{ nativeEvent: { translationX: translateX } }], + { useNativeDriver: true } + ); + + // 处理手势状态变化 + const onHandlerStateChange = useCallback((event: any) => { + const { nativeEvent } = event; + + if (nativeEvent.state === State.END) { + const translationX = nativeEvent.translationX; + + // 只允许特定方向的滑动 + const shouldTrigger = isMe + ? translationX < -SWIPE_THRESHOLD // 自己消息只能向左滑 + : translationX > SWIPE_THRESHOLD; // 对方消息只能向右滑 + + if (shouldTrigger && !hasTriggeredRef.current) { + hasTriggeredRef.current = true; + + // 触觉反馈 + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + + // 立即触发回复 + onReply(); + + // 重置标记 + setTimeout(() => { + hasTriggeredRef.current = false; + }, 300); + } + + // 立即回到原位 + translateX.setValue(0); + } + }, [isMe, onReply]); + + // 计算回复图标的透明度 - 只在允许的方向显示 + const iconOpacity = translateX.interpolate({ + inputRange: isMe + ? [-MAX_SWIPE_DISTANCE, -SWIPE_THRESHOLD, 0, 10] + : [-10, 0, SWIPE_THRESHOLD, MAX_SWIPE_DISTANCE], + outputRange: isMe ? [1, 0.8, 0, 0] : [0, 0, 0.8, 1], + extrapolate: 'clamp', + }); + + // 计算消息的位移 - 限制在允许的方向 + const clampedTranslateX = translateX.interpolate({ + inputRange: isMe + ? [-MAX_SWIPE_DISTANCE - 10, -MAX_SWIPE_DISTANCE, 0, 10] + : [-10, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE + 10], + outputRange: isMe + ? [-MAX_SWIPE_DISTANCE, -MAX_SWIPE_DISTANCE, 0, 0] + : [0, 0, MAX_SWIPE_DISTANCE, MAX_SWIPE_DISTANCE], + extrapolate: 'clamp', + }); + + // 如果禁用手势,直接返回子元素 + if (!enabled) { + return <>{children}; + } + + return ( + + {/* 回复图标 - 在消息后面 */} + + + + + {/* 可滑动的消息内容 */} + + + {children} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + position: 'relative', + }, + replyIconContainer: { + position: 'absolute', + top: '50%', + marginTop: -15, + width: 30, + height: 30, + borderRadius: 15, + backgroundColor: 'rgba(255, 107, 53, 0.1)', + alignItems: 'center', + justifyContent: 'center', + }, + replyIconLeft: { + left: 8, + }, + replyIconRight: { + right: 8, + }, +}); + +export default SwipeableMessageBubble; \ No newline at end of file diff --git a/src/screens/message/components/ChatScreen/bubbleStyles.ts b/src/screens/message/components/ChatScreen/bubbleStyles.ts new file mode 100644 index 0000000..1739007 --- /dev/null +++ b/src/screens/message/components/ChatScreen/bubbleStyles.ts @@ -0,0 +1,270 @@ +/** + * 消息气泡样式工具 + * 参考 Element X 设计,实现动态圆角和现代化气泡样式 + */ + +import { StyleSheet, ViewStyle, TextStyle } from 'react-native'; +import { colors, spacing } from '../../../../theme'; + +// 气泡圆角大小(参考 Element X: 12pt) +export const BUBBLE_RADIUS = 16; + +// 气泡颜色方案(参考 Element X 的语义化颜色) +export const bubbleColors = { + // 自己发送的消息 + outgoing: { + background: '#E8E8E8', // Element X 风格:灰色系 + text: '#1A1A1A', + }, + // 收到的消息 + incoming: { + background: '#FFFFFF', + text: '#1A1A1A', + }, + // 被回复的消息高亮 + replyHighlight: { + background: 'rgba(255, 107, 53, 0.08)', + borderLeft: '#FF6B35', + }, +}; + +// 消息在组中的位置类型 +export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last'; + +/** + * 根据消息在组中的位置获取圆角样式 + * 参考 Element X 的动态圆角逻辑 + */ +export const getBubbleBorderRadius = ( + isMe: boolean, + position: MessageGroupPosition +): ViewStyle => { + const radius = BUBBLE_RADIUS; + + if (isMe) { + // 自己发送的消息 + switch (position) { + case 'single': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: 4, // 尖角在右下 + }; + case 'first': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: 4, + }; + case 'middle': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: 4, + borderBottomLeftRadius: radius, + borderBottomRightRadius: 4, + }; + case 'last': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: 4, + borderBottomLeftRadius: radius, + borderBottomRightRadius: radius, + }; + } + } else { + // 收到的消息 + switch (position) { + case 'single': + return { + borderTopLeftRadius: 4, // 尖角在左上 + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: radius, + }; + case 'first': + return { + borderTopLeftRadius: 4, + borderTopRightRadius: radius, + borderBottomLeftRadius: 4, + borderBottomRightRadius: radius, + }; + case 'middle': + return { + borderTopLeftRadius: 4, + borderTopRightRadius: radius, + borderBottomLeftRadius: 4, + borderBottomRightRadius: radius, + }; + case 'last': + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomLeftRadius: radius, + borderBottomRightRadius: radius, + }; + } + } +}; + +/** + * 判断消息在组中的位置 + */ +export const getMessageGroupPosition = ( + index: number, + messages: Array<{ sender_id: string }>, + currentUserId: string +): MessageGroupPosition => { + const currentMessage = messages[index]; + if (!currentMessage) return 'single'; + + const prevMessage = index > 0 ? messages[index - 1] : null; + const nextMessage = index < messages.length - 1 ? messages[index + 1] : null; + + const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id; + const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id; + + if (!isSameSenderAsPrev && !isSameSenderAsNext) { + return 'single'; + } else if (!isSameSenderAsPrev && isSameSenderAsNext) { + return 'first'; + } else if (isSameSenderAsPrev && isSameSenderAsNext) { + return 'middle'; + } else { + return 'last'; + } +}; + +/** + * 判断是否显示发送者信息(只在组的第一条消息显示) + */ +export const shouldShowSenderInfo = ( + index: number, + messages: Array<{ sender_id: string }>, +): boolean => { + if (index === 0) return true; + + const currentMessage = messages[index]; + const prevMessage = messages[index - 1]; + + return prevMessage.sender_id !== currentMessage.sender_id; +}; + +/** + * 获取气泡基础样式 + */ +export const getBubbleBaseStyle = (isMe: boolean): ViewStyle => ({ + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm + 4, + minWidth: 60, + maxWidth: '75%', + backgroundColor: isMe ? bubbleColors.outgoing.background : bubbleColors.incoming.background, + // Element X 风格的微妙阴影 + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.06, + shadowRadius: 2, + elevation: 2, +}); + +/** + * 获取文本样式 + */ +export const getBubbleTextStyle = (isMe: boolean): TextStyle => ({ + color: isMe ? bubbleColors.outgoing.text : bubbleColors.incoming.text, + fontSize: 16, + lineHeight: 23, + letterSpacing: 0.2, + fontWeight: '400', +}); + +/** + * 消息气泡样式集合 + */ +export const bubbleStyles = StyleSheet.create({ + // 基础气泡 + bubble: { + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm + 4, + minWidth: 60, + }, + + // 自己发送的消息 + outgoing: { + backgroundColor: bubbleColors.outgoing.background, + }, + + // 收到的消息 + incoming: { + backgroundColor: bubbleColors.incoming.background, + }, + + // 文本样式 + text: { + fontSize: 16, + lineHeight: 23, + letterSpacing: 0.2, + fontWeight: '400', + }, + + // 阴影样式(Element X 风格) + shadow: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.06, + shadowRadius: 2, + elevation: 2, + }, + + // 长按反馈阴影 + longPressShadow: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 3 }, + shadowOpacity: 0.12, + shadowRadius: 8, + elevation: 6, + }, + + // 被回复消息的高亮边框 + replyHighlight: { + borderLeftWidth: 3, + borderLeftColor: colors.primary.main, + backgroundColor: bubbleColors.replyHighlight.background, + }, + + // 已撤回消息 + recalled: { + backgroundColor: '#F5F7FA', + borderWidth: 1, + borderColor: '#E8E8E8', + borderStyle: 'dashed', + borderRadius: 12, + }, + + // 系统通知 + systemNotice: { + alignItems: 'center', + marginVertical: spacing.sm, + }, + systemNoticeText: { + fontSize: 12, + color: '#8E8E93', + backgroundColor: 'rgba(142, 142, 147, 0.12)', + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: 12, + overflow: 'hidden', + }, +}); + +export default { + BUBBLE_RADIUS, + bubbleColors, + getBubbleBorderRadius, + getMessageGroupPosition, + shouldShowSenderInfo, + getBubbleBaseStyle, + getBubbleTextStyle, + bubbleStyles, +}; \ No newline at end of file diff --git a/src/screens/message/components/ChatScreen/index.ts b/src/screens/message/components/ChatScreen/index.ts index c905b0f..10661fb 100644 --- a/src/screens/message/components/ChatScreen/index.ts +++ b/src/screens/message/components/ChatScreen/index.ts @@ -20,6 +20,7 @@ export { ChatHeader } from './ChatHeader'; export { MessageBubble } from './MessageBubble'; export { ChatInput } from './ChatInput'; export { GroupInfoPanel } from './GroupInfoPanel'; +export { SwipeableMessageBubble } from './SwipeableMessageBubble'; // Segment 渲染组件 export { diff --git a/src/screens/message/components/ChatScreen/types.ts b/src/screens/message/components/ChatScreen/types.ts index ea704fa..191e43a 100644 --- a/src/screens/message/components/ChatScreen/types.ts +++ b/src/screens/message/components/ChatScreen/types.ts @@ -86,6 +86,8 @@ export interface MessageBubbleProps { shouldShowTime: (index: number) => boolean; // 图片点击回调 onImagePress?: (images: { id: string; url: string; thumbnail_url?: string; width?: number; height?: number }[], index: number) => void; + // 滑动回复回调 + onReply?: (message: GroupMessage) => void; } // 输入框 Props @@ -180,6 +182,14 @@ export interface ReplyPreviewProps { onCancel: () => void; } +// 可滑动消息气泡 Props +export interface SwipeableMessageBubbleProps { + children: React.ReactNode; + isMe: boolean; + onReply: () => void; + enabled?: boolean; +} + // ChatScreen 状态接口 export interface ChatScreenState { // 基础状态 diff --git a/src/screens/message/components/ChatScreen/useChatScreen.ts b/src/screens/message/components/ChatScreen/useChatScreen.ts index 33ab6e7..77351b6 100644 --- a/src/screens/message/components/ChatScreen/useChatScreen.ts +++ b/src/screens/message/components/ChatScreen/useChatScreen.ts @@ -390,6 +390,16 @@ export const useChatScreen = () => { return; } + // 禁用自动滚动到底部,防止加载历史消息后滚动位置跳转 + shouldAutoScrollOnEnterRef.current = false; + // 清除所有待执行的自动滚动定时器 + autoScrollTimersRef.current.forEach(clearTimeout); + autoScrollTimersRef.current = []; + + // 保存加载前的滚动位置和内容高度 + const scrollYBefore = scrollPositionRef.current.scrollY; + const contentHeightBefore = scrollPositionRef.current.contentHeight; + setLoadingMore(true); try { @@ -400,6 +410,23 @@ export const useChatScreen = () => { const minSeq = Math.min(...messages.map(m => m.seq)); setFirstSeq(minSeq); } + + // 加载完成后,恢复滚动位置 + // 使用 setTimeout 确保 FlashList 已经更新 + setTimeout(() => { + if (flatListRef.current) { + // 计算新的滚动位置,保持相对位置不变 + const newContentHeight = scrollPositionRef.current.contentHeight; + const heightDiff = newContentHeight - contentHeightBefore; + const newScrollY = scrollYBefore + heightDiff; + + // 滚动到计算后的位置 + flatListRef.current.scrollToOffset({ + offset: newScrollY, + animated: false, + }); + } + }, 100); } catch (error) { console.error('加载历史消息失败:', error); } finally { @@ -409,13 +436,19 @@ export const useChatScreen = () => { // 列表内容尺寸变化后触发首次自动滚动 const handleMessageListContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => { + // 如果是加载更多历史消息,不要自动滚动 + if (loadingMore) { + return; + } + if (!shouldAutoScrollOnEnterRef.current) return; - if (loading || loadingMore) return; + if (loading) return; if (messages.length === 0) return; const prevContentHeight = scrollPositionRef.current.contentHeight; scrollPositionRef.current.contentHeight = contentHeight; + // 如果内容高度增加(加载了更多消息),不要自动滚动到底部 if (prevContentHeight > 0 && contentHeight > prevContentHeight) { return; } diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx index 4273225..e61abc4 100644 --- a/src/screens/profile/SettingsScreen.tsx +++ b/src/screens/profile/SettingsScreen.tsx @@ -15,6 +15,7 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; +import Constants from 'expo-constants'; import { colors, spacing, fontSizes, borderRadius } from '../../theme'; import { useAuthStore } from '../../stores'; import { Text, ResponsiveContainer } from '../../components/common'; @@ -30,6 +31,9 @@ interface SettingsItem { subtitle?: string; } +// 获取应用版本号 +const APP_VERSION = Constants.expoConfig?.version || '1.0.0'; + // 设置分组配置 const SETTINGS_GROUPS = [ { @@ -53,7 +57,7 @@ const SETTINGS_GROUPS = [ title: '关于与帮助', icon: 'information-outline', items: [ - { key: 'about', title: '关于我们', icon: 'information-outline', showArrow: true, subtitle: '版本 1.0.2' }, + { key: 'about', title: '关于我们', icon: 'information-outline', showArrow: true, subtitle: `版本 ${APP_VERSION}` }, { key: 'help', title: '帮助与反馈', icon: 'help-circle-outline', showArrow: true }, ], }, @@ -183,7 +187,7 @@ export const SettingsScreen: React.FC = () => { {/* 底部版权信息 */} - 萝卜社区 v1.0.2 + 萝卜社区 v{APP_VERSION} © 2024 Carrot BBS. All rights reserved. diff --git a/src/services/authService.ts b/src/services/authService.ts index c4055d5..fa28a9d 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -432,5 +432,63 @@ class AuthService { } } +// ── 二维码登录相关接口 ── + +export interface QRCodeSession { + session_id: string; + qrcode_url: string; + expires_in: number; + expires_at: number; +} + +export interface ScanResponse { + user: { + id: string; + nickname: string; + avatar: string; + }; +} + +// 二维码登录 API(扩展 AuthService) +export const qrcodeApi = { + /** + * 获取二维码 + */ + getQRCode: async (): Promise => { + const response = await api.get('/auth/qrcode'); + return response.data; + }, + + /** + * 扫描二维码 + */ + scan: async (sessionId: string): Promise => { + const response = await api.post('/auth/qrcode/scan', { + session_id: sessionId, + }); + return response.data; + }, + + /** + * 确认登录 + */ + confirm: async (sessionId: string): Promise<{ success: boolean }> => { + const response = await api.post<{ success: boolean }>('/auth/qrcode/confirm', { + session_id: sessionId, + }); + return response.data; + }, + + /** + * 取消登录 + */ + cancel: async (sessionId: string): Promise<{ success: boolean }> => { + const response = await api.post<{ success: boolean }>('/auth/qrcode/cancel', { + session_id: sessionId, + }); + return response.data; + }, +}; + // 导出认证服务实例 export const authService = new AuthService(); diff --git a/src/services/commentService.ts b/src/services/commentService.ts index f47da1c..71989f2 100644 --- a/src/services/commentService.ts +++ b/src/services/commentService.ts @@ -5,6 +5,7 @@ import { api, PaginatedData } from './api'; import { Comment, CreateCommentInput } from '../types'; +import { CursorPaginationRequest, CursorPaginationResponse, CommentDTO } from '../types/dto'; // 评论列表响应 interface CommentListResponse { @@ -240,6 +241,62 @@ class CommentService { return false; } } + + // ==================== 游标分页方法 ==================== + + /** + * 获取帖子评论列表(游标分页) + * GET /api/v1/comments/post/:id/cursor + * @param postId 帖子ID + * @param params 游标分页请求参数 + */ + async getPostCommentsCursor( + postId: string, + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + `/comments/post/${postId}/cursor`, + { params } + ); + return response.data; + } catch (error) { + console.error('获取帖子评论列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + + /** + * 获取评论回复列表(游标分页) + * GET /api/v1/comments/:id/replies/cursor + * @param commentId 评论ID + * @param params 游标分页请求参数 + */ + async getCommentRepliesCursor( + commentId: string, + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + `/comments/${commentId}/replies/cursor`, + { params } + ); + return response.data; + } catch (error) { + console.error('获取评论回复列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } } // 导出评论服务实例 diff --git a/src/services/groupService.ts b/src/services/groupService.ts index 147aab8..330193d 100644 --- a/src/services/groupService.ts +++ b/src/services/groupService.ts @@ -23,6 +23,8 @@ import { MyMemberInfoResponse, SetGroupAvatarRequest, HandleGroupRequestAction, + CursorPaginationRequest, + CursorPaginationResponse, } from '../types/dto'; // 群组服务类(纯 API 层) @@ -321,6 +323,86 @@ class GroupService { ); } + // ==================== 游标分页方法 ==================== + + /** + * 获取群组列表(游标分页) + * GET /api/v1/groups/cursor + * @param params 游标分页请求参数 + */ + async getGroupsCursor( + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>('/groups/cursor', { + params, + }); + return response.data; + } catch (error) { + console.error('获取群组列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + + /** + * 获取群组成员列表(游标分页) + * GET /api/v1/groups/:id/members/cursor + * @param groupId 群组ID + * @param params 游标分页请求参数 + */ + async getGroupMembersCursor( + groupId: number | string, + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + `/groups/${encodeURIComponent(String(groupId))}/members/cursor`, + { params } + ); + return response.data; + } catch (error) { + console.error('获取群组成员列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + + /** + * 获取群公告列表(游标分页) + * GET /api/v1/groups/:id/announcements/cursor + * @param groupId 群组ID + * @param params 游标分页请求参数 + */ + async getGroupAnnouncementsCursor( + groupId: number | string, + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + `/groups/${encodeURIComponent(String(groupId))}/announcements/cursor`, + { params } + ); + return response.data; + } catch (error) { + console.error('获取群公告列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + // ==================== 兼容旧API的方法(将逐步废弃) ==================== /** diff --git a/src/services/messageService.ts b/src/services/messageService.ts index 15f57ae..27a8722 100644 --- a/src/services/messageService.ts +++ b/src/services/messageService.ts @@ -15,8 +15,11 @@ import { UnreadCountResponse, ConversationUnreadCountResponse, SystemMessageListResponse, + SystemMessageResponse, SystemUnreadCountResponse, MessageSegment, + CursorPaginationRequest, + CursorPaginationResponse, } from '../types/dto'; import { getConversationCache, @@ -555,6 +558,85 @@ class MessageService { await api.put('/messages/system/read-all'); } + // ==================== 游标分页方法 ==================== + + /** + * 获取会话列表(游标分页) + * GET /api/v1/conversations/cursor + * @param params 游标分页请求参数 + */ + async getConversationsCursor( + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + '/conversations/cursor', + { params } + ); + return response.data; + } catch (error) { + console.error('获取会话列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + + /** + * 获取消息列表(游标分页) + * GET /api/v1/conversations/:id/messages/cursor + * @param conversationId 会话ID + * @param params 游标分页请求参数 + */ + async getMessagesCursor( + conversationId: string, + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + `/conversations/${encodeURIComponent(conversationId)}/messages/cursor`, + { params } + ); + return response.data; + } catch (error) { + console.error('获取消息列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + + /** + * 获取系统消息列表(游标分页) + * GET /api/v1/messages/system/cursor + * @param params 游标分页请求参数 + */ + async getSystemMessagesCursor( + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + '/messages/system/cursor', + { params } + ); + return response.data; + } catch (error) { + console.error('获取系统消息列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + // ==================== 兼容旧API的方法(将逐步废弃) ==================== /** diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 2062e2e..f808b70 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -5,6 +5,7 @@ import { api, PaginatedData } from './api'; import { Notification, NotificationBadge, NotificationType } from '../types'; +import { CursorPaginationRequest, CursorPaginationResponse } from '../types/dto'; // 通知列表响应 interface NotificationListResponse { @@ -151,6 +152,33 @@ class NotificationService { async getMentionNotifications(page = 1, pageSize = 20): Promise> { return this.getNotifications(page, pageSize, 'mention'); } + + // ==================== 游标分页方法 ==================== + + /** + * 获取通知列表(游标分页) + * GET /api/v1/notifications/cursor + * @param params 游标分页请求参数 + */ + async getNotificationsCursor( + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + '/notifications/cursor', + { params } + ); + return response.data; + } catch (error) { + console.error('获取通知列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } } // 导出通知服务实例 diff --git a/src/services/postService.ts b/src/services/postService.ts index 32e1ecb..f4ece35 100644 --- a/src/services/postService.ts +++ b/src/services/postService.ts @@ -5,6 +5,7 @@ import { api, PaginatedData } from './api'; import { Post, CreatePostInput } from '../types'; +import { CursorPaginationRequest, CursorPaginationResponse } from '../types/dto'; // 帖子列表响应 interface PostListResponse { @@ -281,6 +282,92 @@ class PostService { return false; } } + + // ==================== 游标分页方法 ==================== + + /** + * 获取帖子列表(游标分页) + * GET /api/v1/posts/cursor + * @param params 游标分页请求参数(包含 post_type 可选:recommend, follow, hot, latest) + */ + async getPostsCursor( + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>('/posts/cursor', { + params: { + cursor: params.cursor, + page_size: params.page_size, + post_type: params.post_type, + }, + }); + return response.data; + } catch (error) { + console.error('获取帖子列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + + /** + * 搜索帖子(游标分页) + * GET /api/v1/posts/search/cursor + * @param query 搜索关键词 + * @param params 游标分页请求参数 + */ + async searchPostsCursor( + query: string, + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>('/posts/search/cursor', { + params: { + ...params, + keyword: query, + }, + }); + return response.data; + } catch (error) { + console.error('搜索帖子失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } + + /** + * 获取用户帖子列表(游标分页) + * GET /api/v1/users/:id/posts/cursor + * @param userId 用户ID + * @param params 游标分页请求参数 + */ + async getUserPostsCursor( + userId: string, + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>( + `/users/${userId}/posts/cursor`, + { params } + ); + return response.data; + } catch (error) { + console.error('获取用户帖子列表失败:', error); + return { + items: [], + next_cursor: null, + prev_cursor: null, + has_more: false, + }; + } + } } // 导出帖子服务实例 diff --git a/src/types/dto.ts b/src/types/dto.ts index bf6efb3..ab43a3c 100644 --- a/src/types/dto.ts +++ b/src/types/dto.ts @@ -499,6 +499,36 @@ export interface SystemUnreadCountResponse { // 设备类型 export type DeviceType = 'ios' | 'android' | 'web'; +// ==================== 游标分页相关 DTO ==================== + +/** + * 游标分页请求参数 + */ +export interface CursorPaginationRequest { + /** 游标字符串(可选,首次请求不传) */ + cursor?: string; + /** 分页方向:forward 或 backward(默认 forward) */ + direction?: 'forward' | 'backward'; + /** 每页数量(默认 20,最大 100) */ + page_size?: number; + /** 帖子类型筛选(可选):recommend, follow, hot, latest */ + post_type?: 'recommend' | 'follow' | 'hot' | 'latest'; +} + +/** + * 游标分页响应 + */ +export interface CursorPaginationResponse { + /** 数据项列表 */ + items: T[]; + /** 下一页游标 */ + next_cursor: string | null; + /** 上一页游标 */ + prev_cursor: string | null; + /** 是否有更多数据 */ + has_more: boolean; +} + // 设备Token响应 export interface DeviceTokenResponse { id: number; diff --git a/src/utils/__tests__/optimisticUpdate.test.ts b/src/utils/__tests__/optimisticUpdate.test.ts deleted file mode 100644 index 39081d7..0000000 --- a/src/utils/__tests__/optimisticUpdate.test.ts +++ /dev/null @@ -1,287 +0,0 @@ -/** - * 乐观更新工具函数测试 - */ - -import { optimisticUpdate, simpleOptimisticUpdate, createOptimisticUpdaterFactory } from '../optimisticUpdate'; - -// 模拟数据 -interface TestState { - posts: Array<{ id: string; likes: number; isLiked: boolean }>; - users: Array<{ id: string; isFollowing: boolean }>; -} - -const initialState: TestState = { - posts: [ - { id: '1', likes: 10, isLiked: false }, - { id: '2', likes: 5, isLiked: true }, - ], - users: [ - { id: 'user1', isFollowing: false }, - { id: 'user2', isFollowing: true }, - ], -}; - -describe('optimisticUpdate', () => { - let currentState: TestState; - - beforeEach(() => { - currentState = JSON.parse(JSON.stringify(initialState)); - }); - - describe('成功场景', () => { - it('应该执行乐观更新并在API成功后保持更新', async () => { - const mockApiCall = jest.fn().mockResolvedValue({ success: true, likes: 11 }); - - const result = await optimisticUpdate({ - getState: () => currentState, - optimisticUpdate: (state) => ({ - ...state, - posts: state.posts.map((p) => - p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p - ), - }), - apiCall: mockApiCall, - onSuccess: (result, current) => ({ - ...current, - posts: current.posts.map((p) => - p.id === '1' ? { ...p, likes: result.likes } : p - ), - }), - }); - - // 验证API被调用 - expect(mockApiCall).toHaveBeenCalledTimes(1); - // 验证返回结果 - expect(result).toEqual({ success: true, likes: 11 }); - }); - - it('应该在乐观更新后立即改变状态', async () => { - const mockApiCall = jest.fn().mockImplementation(() => { - // 在API调用期间验证状态已被乐观更新 - expect(currentState.posts[0].isLiked).toBe(true); - expect(currentState.posts[0].likes).toBe(11); - return Promise.resolve({ success: true }); - }); - - await optimisticUpdate({ - getState: () => currentState, - optimisticUpdate: (state) => { - const newState = { - ...state, - posts: state.posts.map((p) => - p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p - ), - }; - currentState = newState; // 模拟状态更新 - return newState; - }, - apiCall: mockApiCall, - }); - }); - }); - - describe('失败回滚场景', () => { - it('应该在API失败时回滚到原始状态', async () => { - const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error')); - const onError = jest.fn((error, original) => original); - - const result = await optimisticUpdate({ - getState: () => currentState, - optimisticUpdate: (state) => ({ - ...state, - posts: state.posts.map((p) => - p.id === '1' ? { ...p, likes: p.likes + 1, isLiked: true } : p - ), - }), - apiCall: mockApiCall, - onError, - errorMessage: '点赞失败', - }); - - // 验证API被调用 - expect(mockApiCall).toHaveBeenCalledTimes(1); - // 验证onError被调用 - expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledWith( - expect.any(Error), - initialState - ); - // 验证返回undefined(因为失败了) - expect(result).toBeUndefined(); - }); - - it('应该在静默模式下不抛出错误', async () => { - const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error')); - - // 不应该抛出错误 - await expect( - optimisticUpdate({ - getState: () => currentState, - optimisticUpdate: (state) => state, - apiCall: mockApiCall, - silent: true, - }) - ).resolves.toBeUndefined(); - }); - - it('应该在非静默模式下抛出错误', async () => { - const mockApiCall = jest.fn().mockRejectedValue(new Error('Network error')); - - // 应该抛出错误 - await expect( - optimisticUpdate({ - getState: () => currentState, - optimisticUpdate: (state) => state, - apiCall: mockApiCall, - silent: false, - }) - ).rejects.toThrow('Network error'); - }); - }); - - describe('错误处理', () => { - it('应该记录错误日志', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - const mockApiCall = jest.fn().mockRejectedValue(new Error('API Error')); - - await optimisticUpdate({ - getState: () => currentState, - optimisticUpdate: (state) => state, - apiCall: mockApiCall, - errorMessage: '自定义错误消息', - silent: true, - }); - - expect(consoleSpy).toHaveBeenCalledWith('自定义错误消息:', expect.any(Error)); - - consoleSpy.mockRestore(); - }); - }); -}); - -describe('simpleOptimisticUpdate', () => { - let currentState: TestState; - let setStateMock: jest.Mock; - - beforeEach(() => { - currentState = JSON.parse(JSON.stringify(initialState)); - setStateMock = jest.fn((newState) => { - currentState = newState; - }); - }); - - it('应该成功执行乐观更新', async () => { - const mockApiCall = jest.fn().mockResolvedValue(undefined); - - await simpleOptimisticUpdate({ - getState: () => currentState, - setState: setStateMock, - optimisticUpdate: (state) => ({ - ...state, - posts: state.posts.map((p) => - p.id === '1' ? { ...p, likes: p.likes + 1 } : p - ), - }), - rollbackState: (original) => original, - apiCall: mockApiCall, - }); - - expect(mockApiCall).toHaveBeenCalledTimes(1); - expect(setStateMock).toHaveBeenCalledTimes(1); - }); - - it('应该在失败时回滚状态', async () => { - const mockApiCall = jest.fn().mockRejectedValue(new Error('Error')); - const rollbackMock = jest.fn((original) => original); - - await simpleOptimisticUpdate({ - getState: () => currentState, - setState: setStateMock, - optimisticUpdate: (state) => ({ - ...state, - posts: state.posts.map((p) => - p.id === '1' ? { ...p, likes: p.likes + 1 } : p - ), - }), - rollbackState: rollbackMock, - apiCall: mockApiCall, - silent: true, - }); - - // 验证乐观更新和回滚都被调用 - expect(setStateMock).toHaveBeenCalledTimes(2); - expect(rollbackMock).toHaveBeenCalledWith(initialState); - }); -}); - -describe('createOptimisticUpdaterFactory', () => { - interface StoreState { - posts: Array<{ id: string; likes: number }>; - count: number; - } - - let storeState: StoreState; - let setMock: jest.Mock; - let getMock: jest.Mock; - - beforeEach(() => { - storeState = { - posts: [{ id: '1', likes: 10 }], - count: 0, - }; - setMock = jest.fn((fn) => { - storeState = fn(storeState); - }); - getMock = jest.fn(() => storeState); - }); - - it('应该创建乐观更新器并正常工作', async () => { - const optimisticUpdater = createOptimisticUpdaterFactory(setMock, getMock); - const mockApiCall = jest.fn().mockResolvedValue(undefined); - - await optimisticUpdater({ - key: 'posts', - optimisticUpdate: (posts) => - posts.map((p) => (p.id === '1' ? { ...p, likes: p.likes + 1 } : p)), - rollbackState: (original) => original, - apiCall: mockApiCall, - }); - - expect(mockApiCall).toHaveBeenCalledTimes(1); - expect(setMock).toHaveBeenCalledTimes(1); - expect(storeState.posts[0].likes).toBe(11); - }); - - it('应该在API失败时回滚状态', async () => { - const optimisticUpdater = createOptimisticUpdaterFactory(setMock, getMock); - const mockApiCall = jest.fn().mockRejectedValue(new Error('Error')); - - await optimisticUpdater({ - key: 'posts', - optimisticUpdate: (posts) => - posts.map((p) => (p.id === '1' ? { ...p, likes: p.likes + 1 } : p)), - rollbackState: (original) => original, - apiCall: mockApiCall, - silent: true, - }); - - // 乐观更新 + 回滚 = 2次set调用 - expect(setMock).toHaveBeenCalledTimes(2); - // 最终状态应该和初始状态一样 - expect(storeState.posts[0].likes).toBe(10); - }); - - it('应该处理不同的state key', async () => { - const optimisticUpdater = createOptimisticUpdaterFactory(setMock, getMock); - const mockApiCall = jest.fn().mockResolvedValue(undefined); - - await optimisticUpdater({ - key: 'count', - optimisticUpdate: (count) => count + 1, - rollbackState: (original) => original, - apiCall: mockApiCall, - }); - - expect(storeState.count).toBe(1); - }); -});