import { createAppStoreWithActions } from '../../os/createAppStore'; import { REDBOOK_CONFIG } from './data'; import { getBaseDataset } from './data/loader'; import * as TimeService from '../../os/TimeService'; import { getRedBookFollowingIds, resolveRedBookRuntimeUser, type RedBookRuntimeComment, type RedBookRuntimeCommentTable, type RedBookRuntimeNoteTable, type RedBookRuntimeUserTable, } from './utils/runtimeResolvers'; import type { ChatConversation, ChatMessage, Comment, HotSearchItem, Note, Notification, RedBookTempState, RedBookPublishDraft, RedBookSettings, RedBookStorage, User, } from './types'; export type { ChatConversation, ChatMessage, Notification, RedBookTempState, RedBookPublishDraft, RedBookSettings, RedBookStorage, }; // ── State interface ───────────────────────────────────────────────── export interface RedBookStoreState { user: User; notes: RedBookRuntimeNoteTable; comments: RedBookRuntimeCommentTable; users: RedBookRuntimeUserTable; chats: ChatConversation[]; notifications: Notification[]; history: string[]; searchHistory: string[]; hotSearch: HotSearchItem[]; guessYouLike: string[]; settings: RedBookSettings; storage: RedBookStorage; _temp: RedBookTempState; publishDraft: RedBookPublishDraft; } // ── Actions interface ─────────────────────────────────────────────── export interface RedBookActions { updateHomeState: (updates: Partial) => void; updatePublishDraft: (updates: Partial) => void; resetPublishDraft: () => void; toggleLike: (noteId: string) => void; toggleCollect: (noteId: string) => void; addComment: (noteId: string, content: string, replyToCommentId?: string) => void; toggleCommentLike: (noteId: string, commentId: string) => void; followUser: (userId: string) => void; addNote: (note: Pick) => void; sendMessage: (toUserId: string, content: string) => void; logout: () => void; updateUser: (updates: RedBookUserUpdates) => void; markNotificationsAsRead: (type?: Notification['type']) => void; addToHistory: (noteId: string) => void; clearHistory: () => void; addSearchHistory: (keyword: string) => void; removeSearchHistory: (keyword: string) => void; clearSearchHistory: () => void; clearCache: () => void; updateSettings: (category: keyof RedBookSettings, updates: Partial | string | null) => void; } type RedBookUserUpdates = Omit, 'following' | 'followers'>; const sanitizeUserUpdates = (updates: RedBookUserUpdates): RedBookUserUpdates => { const { following: _following, followers: _followers, ...safeUpdates } = updates as any; return safeUpdates; }; const baseUsersById = (): Record => getBaseDataset()?.usersById ?? {}; // ── Initial state ─────────────────────────────────────────────────── const initialState: RedBookStoreState = { user: { ...REDBOOK_CONFIG.user }, notes: { ...REDBOOK_CONFIG.notes }, comments: { ...REDBOOK_CONFIG.comments }, users: { ...REDBOOK_CONFIG.users }, chats: [...REDBOOK_CONFIG.chats], notifications: [...REDBOOK_CONFIG.notifications], history: [...REDBOOK_CONFIG.history], searchHistory: [...REDBOOK_CONFIG.searchHistory], hotSearch: [...REDBOOK_CONFIG.hotSearch], guessYouLike: [...REDBOOK_CONFIG.guessYouLike], settings: { ...REDBOOK_CONFIG.settings }, storage: { ...REDBOOK_CONFIG.storage }, // `_temp` 直接在 TS 里初始化,不进 defaults.json—— // 它是 ephemeral 运行时导航状态,不属于"可替换 base data"。 _temp: { activeCategory: 'recommend', citySubTab: 'recommend' }, publishDraft: { ...REDBOOK_CONFIG.publishDraft }, }; // ── Store ────────────────────────────────────────────────────────── export const useRedBookStore = createAppStoreWithActions( 'redbook', initialState, (set, get) => ({ // ── Home nav state ───────────────────────────────────────── // 名字保留 `updateHomeState` 是为了不破坏 bench live test 的 dispatch 调用 // (test_redbook_live.py 按 action 名调)。内部写到 `_temp` 而非历史的 `homeState`: // - 不持久化(默认 partialize 排除 `_temp`) // - bench 自动忽略 diff(`apps.*._temp` 在 base.py 白名单里) updateHomeState: (updates) => { set({ _temp: { ...get()._temp, ...updates } }); }, // ── Publish draft ────────────────────────────────────────── updatePublishDraft: (updates) => { set({ publishDraft: { ...get().publishDraft, ...updates } }); }, resetPublishDraft: () => { set({ publishDraft: { text: '', templateId: 'basic', title: '', images: [] } }); }, // ── Like / Collect ───────────────────────────────────────── toggleLike: (noteId) => { const s = get(); const likedNotes = s.user.likedNotes || []; const wasLiked = likedNotes.includes(noteId); const nextLikedNotes = wasLiked ? likedNotes.filter(id => id !== noteId) : [...likedNotes, noteId]; set({ user: { ...s.user, likedNotes: nextLikedNotes }, }); }, toggleCollect: (noteId) => { const s = get(); const collectedNotes = s.user.collectedNotes || []; const wasCollected = collectedNotes.includes(noteId); const nextCollectedNotes = wasCollected ? collectedNotes.filter(id => id !== noteId) : [...collectedNotes, noteId]; set({ user: { ...s.user, collectedNotes: nextCollectedNotes }, }); }, // ── Comments ─────────────────────────────────────────────── addComment: (noteId, content, replyToCommentId) => { const s = get(); const nowTs = TimeService.now(); const newComment: Comment = { id: `c_${nowTs}`, userId: s.user.id, username: s.user.name, avatar: s.user.avatar, content, time: nowTs, likes: 0, replyToId: replyToCommentId, location: s.user.location || '上海', }; const runtimeComment: RedBookRuntimeComment = { ...newComment, noteId }; const nextUserCommentIds = [...(s.user.commentIds || []), newComment.id]; set({ user: { ...s.user, commentIds: nextUserCommentIds }, comments: { ...s.comments, [newComment.id]: runtimeComment }, }); }, toggleCommentLike: (noteId, commentId) => { const s = get(); const likedByNote = s.user.likedCommentsByNote || {}; const current = likedByNote[noteId] || []; const wasLiked = current.includes(commentId); const nextForNote = wasLiked ? current.filter(id => id !== commentId) : [...current, commentId]; const nextLikedByNote = { ...likedByNote, [noteId]: nextForNote }; set({ user: { ...s.user, likedCommentsByNote: nextLikedByNote }, }); }, // ── Follow ───────────────────────────────────────────────── followUser: (userId) => { const s = get(); const targetUser = resolveRedBookRuntimeUser(s.users, baseUsersById(), s.user, userId); if (!targetUser) return; const followingIds = getRedBookFollowingIds(s.user); const isFollowing = followingIds.includes(userId); const nextFollowingIds = isFollowing ? followingIds.filter(id => id !== userId) : [...followingIds, userId]; set({ user: { ...s.user, followingIds: nextFollowingIds, }, }); }, // ── Add note ─────────────────────────────────────────────── addNote: (noteData) => { const s = get(); const nowTs = TimeService.now(); const newNote: Note = { id: `note_${nowTs}`, ...noteData, authorId: s.user.id, likes: 0, collections: 0, comments: 0, commentList: [], createdAt: nowTs, }; set({ user: { ...s.user, publishedNoteIds: [newNote.id, ...(s.user.publishedNoteIds || [])], }, notes: { ...s.notes, [newNote.id]: newNote }, }); }, // ── Chat ─────────────────────────────────────────────────── sendMessage: (toUserId, content) => { const s = get(); const chats = [...s.chats]; const chatIndex = chats.findIndex(c => c.userId === toUserId); const nowTs = TimeService.now(); const newMessage: ChatMessage = { id: `msg_${nowTs}`, senderId: s.user.id, content, timestamp: nowTs, type: 'text', }; if (chatIndex === -1) { const targetUser = resolveRedBookRuntimeUser(s.users, baseUsersById(), s.user, toUserId) || { name: 'User ' + toUserId, avatar: '' }; chats.unshift({ userId: toUserId, username: targetUser.name, avatar: targetUser.avatar, unreadCount: 0, lastMessage: content, lastTime: nowTs, messages: [newMessage], }); } else { const chat = { ...chats[chatIndex] }; chat.messages = [...chat.messages, newMessage]; chat.lastMessage = content; chat.lastTime = nowTs; chats.splice(chatIndex, 1); chats.unshift(chat); } set({ chats }); }, // ── Auth ─────────────────────────────────────────────────── logout: () => { console.log('Logging out...'); }, // ── User ─────────────────────────────────────────────────── updateUser: (updates) => { set({ user: { ...get().user, ...sanitizeUserUpdates(updates) } }); }, // ── Notifications ────────────────────────────────────────── markNotificationsAsRead: (type?) => { const s = get(); set({ notifications: s.notifications.map(n => { if (!type || n.type === type || (type === 'like_note' && (n.type === 'collect_note' || n.type === 'like_comment'))) { return { ...n, isRead: true }; } return n; }), }); }, // ── History ──────────────────────────────────────────────── addToHistory: (noteId) => { const s = get(); set({ history: [noteId, ...s.history.filter(id => id !== noteId)] }); }, clearHistory: () => { set({ history: [] }); }, addSearchHistory: (keyword) => { const trimmed = keyword.trim(); if (!trimmed) return; const current = get().searchHistory || []; set({ searchHistory: [trimmed, ...current.filter(item => item !== trimmed)] }); }, removeSearchHistory: (keyword) => { set({ searchHistory: (get().searchHistory || []).filter(item => item !== keyword) }); }, clearSearchHistory: () => { set({ searchHistory: [] }); }, clearCache: () => { set({ storage: { cacheSizeBytes: 0 } }); }, // ── Settings ─────────────────────────────────────────────── updateSettings: (category, updates) => { const s = get(); if (category === 'language' && (typeof updates === 'string' || updates === null)) { set({ settings: { ...s.settings, language: updates } }); return; } if (typeof updates === 'object' && !Array.isArray(updates) && category !== 'language') { set({ settings: { ...s.settings, [category]: { ...(s.settings[category] as object), ...updates, }, }, }); } }, }), );