项目文件夹

文件
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 行
13 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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,
},
},
});
}
},
}),
);