Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

338 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<RedBookStoreState['_temp']>) => void;
updatePublishDraft: (updates: Partial<RedBookStoreState['publishDraft']>) => 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<Note, 'title' | 'content' | 'images'>) => 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<RedBookSettings[keyof RedBookSettings]> | string | null) => void;
}
type RedBookUserUpdates = Omit<Partial<User>, 'following' | 'followers'>;
const sanitizeUserUpdates = (updates: RedBookUserUpdates): RedBookUserUpdates => {
const {
following: _following,
followers: _followers,
...safeUpdates
} = updates as any;
return safeUpdates;
};
const baseUsersById = (): Record<string, User> => 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<RedBookStoreState, RedBookActions>(
'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,
},
},
});
}
},
}),
);