- **Post sync optimization**: Clear store immediately when params change to prevent flashing old posts; replace instead of merge on refresh - **ImageGallery fix**: Use idempotent download option, migrate to Asset.create() API, fix Android file path requirement, ensure proper cleanup - **UserProfileScreen**: Add ImageGallery for post image viewing with consistent implementation - **SearchScreen**: Add entrance animation and empty state - **ChatScreen**: Remove unused state variables (lastSeq, firstSeq, isProgrammaticScrollRef) and dead imports
361 lines
13 KiB
TypeScript
361 lines
13 KiB
TypeScript
import { postRepository } from '../../data/repositories/PostRepository';
|
|
import type { Post } from '../../core/entities/Post';
|
|
import type {
|
|
GetPostsParams,
|
|
CreatePostData,
|
|
UpdatePostData,
|
|
PostsResult,
|
|
SearchPostsParams,
|
|
} from '../../data/repositories/interfaces/IPostRepository';
|
|
import { PaginationStateManager } from '../../infrastructure/pagination/PaginationStateManager';
|
|
import type { PaginationState } from '../../infrastructure/pagination/types';
|
|
import { usePostListStore, type PostsState } from '../../stores/post/postListStore';
|
|
import { mergePostListWithFastPath, mergeRefreshWindow } from './postMerge';
|
|
|
|
export type { PostsState } from '../../stores/post/postListStore';
|
|
|
|
export interface PostDetailState {
|
|
post: Post | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
const DEFAULT_PAGE_SIZE = 20;
|
|
const MERGE_PERF_WARN_THRESHOLD_MS = 12;
|
|
|
|
class PostSyncService {
|
|
private static instance: PostSyncService;
|
|
private paginationManager: PaginationStateManager;
|
|
private currentUserId: string | null = null;
|
|
|
|
private constructor() {
|
|
this.paginationManager = new PaginationStateManager();
|
|
}
|
|
|
|
static getInstance(): PostSyncService {
|
|
if (!PostSyncService.instance) {
|
|
PostSyncService.instance = new PostSyncService();
|
|
}
|
|
return PostSyncService.instance;
|
|
}
|
|
|
|
initialize(currentUserId: string | null = null): void {
|
|
this.currentUserId = currentUserId;
|
|
}
|
|
|
|
destroy(): void {
|
|
usePostListStore.getState().clearAllStates();
|
|
this.paginationManager.clear();
|
|
this.currentUserId = null;
|
|
}
|
|
|
|
getPostsState(key: string = 'default'): PostsState {
|
|
return usePostListStore.getState().getPostsState(key);
|
|
}
|
|
|
|
getPostDetailState(postId: string): PostDetailState {
|
|
return usePostListStore.getState().getPostDetailState(postId);
|
|
}
|
|
|
|
applyShareCountUpdate(postId: string, sharesCount: number): void {
|
|
usePostListStore.getState().applyShareCountUpdate(postId, sharesCount);
|
|
}
|
|
|
|
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
|
|
const queryParams: GetPostsParams = {
|
|
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
|
...params,
|
|
cursor: params?.cursor !== undefined ? params.cursor : '',
|
|
};
|
|
|
|
// 判断参数是否变化:参数变化(切换分区/排序/筛选)时直接清空旧数据,
|
|
// 避免渲染期间残留上一个分区的帖子造成闪烁
|
|
const prevState = usePostListStore.getState().getPostsState(key);
|
|
const paramsChanged = JSON.stringify(prevState.lastParams) !== JSON.stringify(queryParams);
|
|
|
|
// 参数变化时立即写入空数据 + loading 状态,确保 UI 第一时间进入加载态
|
|
// 而不是先渲染上一个分区的旧帖子
|
|
usePostListStore.getState().updatePostsState(key, {
|
|
isLoading: true,
|
|
error: null,
|
|
...(paramsChanged ? { posts: [], cursor: null, currentPage: 1, hasMore: true } : {}),
|
|
});
|
|
|
|
try {
|
|
const result = await postRepository.getPosts(queryParams);
|
|
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
|
|
|
const currentState = usePostListStore.getState().getPostsState(key);
|
|
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
|
const mergeStart = Date.now();
|
|
// 参数变化时直接替换;同参数刷新则合并以保留滚动位置上的帖子
|
|
const mergedPosts = paramsChanged
|
|
? incomingPosts
|
|
: mergeRefreshWindow(currentState.posts, incomingPosts);
|
|
const mergeCost = Date.now() - mergeStart;
|
|
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
|
console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length });
|
|
}
|
|
|
|
usePostListStore.getState().updatePostsState(key, {
|
|
posts: mergedPosts,
|
|
hasMore: result.hasMore,
|
|
cursor: isCursorMode ? result.nextCursor : null,
|
|
currentPage: isCursorMode ? undefined : 1,
|
|
isLoading: false,
|
|
lastLoadTime: Date.now(),
|
|
lastParams: params,
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : '获取帖子列表失败';
|
|
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
|
|
const state = usePostListStore.getState().getPostsState(key);
|
|
usePostListStore.getState().updatePostsState(key, { isRefreshing: true, error: null });
|
|
|
|
try {
|
|
const result = await postRepository.getPosts({
|
|
pageSize: DEFAULT_PAGE_SIZE,
|
|
...state.lastParams,
|
|
cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '',
|
|
});
|
|
|
|
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
|
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
|
const mergeStart = Date.now();
|
|
// refreshPosts 是刷新操作,应直接替换当前列表,避免旧数据残留
|
|
const mergedPosts = incomingPosts;
|
|
const mergeCost = Date.now() - mergeStart;
|
|
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
|
console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length });
|
|
}
|
|
|
|
usePostListStore.getState().updatePostsState(key, {
|
|
posts: mergedPosts,
|
|
hasMore: result.hasMore,
|
|
cursor: isCursorMode ? result.nextCursor : null,
|
|
currentPage: isCursorMode ? undefined : 1,
|
|
isRefreshing: false,
|
|
lastLoadTime: Date.now(),
|
|
});
|
|
|
|
this.paginationManager.reset(key);
|
|
return result;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : '刷新帖子列表失败';
|
|
usePostListStore.getState().updatePostsState(key, { isRefreshing: false, error: errorMessage });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async loadMorePosts(key: string = 'default'): Promise<PostsResult> {
|
|
const state = usePostListStore.getState().getPostsState(key);
|
|
|
|
if (!state.hasMore) return { posts: [], hasMore: false };
|
|
if (state.isLoading) return { posts: [], hasMore: state.hasMore };
|
|
|
|
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
|
|
|
|
try {
|
|
const useCursorMode = state.cursor !== null && state.cursor !== undefined;
|
|
const base = { ...(state.lastParams ?? {}) } as GetPostsParams;
|
|
let queryParams: GetPostsParams;
|
|
|
|
if (useCursorMode) {
|
|
delete base.page;
|
|
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, cursor: state.cursor! };
|
|
} else {
|
|
const nextPage = (state.currentPage || 1) + 1;
|
|
delete base.cursor;
|
|
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, page: nextPage };
|
|
}
|
|
|
|
const result = await postRepository.getPosts(queryParams);
|
|
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
|
const mergeStart = Date.now();
|
|
const newPosts = mergePostListWithFastPath(state.posts, incomingPosts);
|
|
const mergeCost = Date.now() - mergeStart;
|
|
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
|
console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length });
|
|
}
|
|
|
|
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
|
|
|
usePostListStore.getState().updatePostsState(key, {
|
|
posts: newPosts,
|
|
hasMore: result.hasMore,
|
|
cursor: responseIsCursorMode ? result.nextCursor : null,
|
|
currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1),
|
|
isLoading: false,
|
|
lastLoadTime: Date.now(),
|
|
});
|
|
|
|
return { posts: incomingPosts, hasMore: result.hasMore, nextCursor: result.nextCursor };
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
|
|
console.error('[PostSyncService] Load more error:', errorMessage);
|
|
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async fetchPostById(id: string): Promise<Post | null> {
|
|
usePostListStore.getState().updatePostDetailState(id, { isLoading: true, error: null });
|
|
|
|
try {
|
|
const post = await postRepository.getPostById(id);
|
|
usePostListStore.getState().updatePostDetailState(id, { post, isLoading: false });
|
|
return post;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : '获取帖子详情失败';
|
|
usePostListStore.getState().updatePostDetailState(id, { isLoading: false, error: errorMessage });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async createPost(data: CreatePostData): Promise<Post> {
|
|
const post = await postRepository.createPost(data);
|
|
const store = usePostListStore.getState();
|
|
const newMap = new Map(store.postsStateMap);
|
|
for (const [key, postsState] of newMap) {
|
|
newMap.set(key, { ...postsState, posts: [post, ...postsState.posts] });
|
|
}
|
|
usePostListStore.setState({ postsStateMap: newMap });
|
|
return post;
|
|
}
|
|
|
|
async updatePost(id: string, data: UpdatePostData): Promise<Post> {
|
|
const post = await postRepository.updatePost(id, data);
|
|
usePostListStore.getState().updatePostDetailState(id, { post });
|
|
usePostListStore.getState().updatePostInLists(id, post);
|
|
return post;
|
|
}
|
|
|
|
async deletePost(id: string): Promise<void> {
|
|
await postRepository.deletePost(id);
|
|
usePostListStore.getState().clearPostDetailState(id);
|
|
usePostListStore.getState().removePostFromLists(id);
|
|
}
|
|
|
|
async likePost(id: string): Promise<Post> {
|
|
const post = await postRepository.likePost(id);
|
|
usePostListStore.getState().updatePostDetailState(id, { post });
|
|
usePostListStore.getState().updatePostInLists(id, post);
|
|
return post;
|
|
}
|
|
|
|
async unlikePost(id: string): Promise<Post> {
|
|
const post = await postRepository.unlikePost(id);
|
|
usePostListStore.getState().updatePostDetailState(id, { post });
|
|
usePostListStore.getState().updatePostInLists(id, post);
|
|
return post;
|
|
}
|
|
|
|
async favoritePost(id: string): Promise<Post> {
|
|
const post = await postRepository.favoritePost(id);
|
|
usePostListStore.getState().updatePostDetailState(id, { post });
|
|
usePostListStore.getState().updatePostInLists(id, post);
|
|
return post;
|
|
}
|
|
|
|
async unfavoritePost(id: string): Promise<Post> {
|
|
const post = await postRepository.unfavoritePost(id);
|
|
usePostListStore.getState().updatePostDetailState(id, { post });
|
|
usePostListStore.getState().updatePostInLists(id, post);
|
|
return post;
|
|
}
|
|
|
|
async searchPosts(params: SearchPostsParams, key: string = 'search'): Promise<PostsResult> {
|
|
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
|
|
|
|
try {
|
|
const result = await postRepository.searchPosts(params);
|
|
usePostListStore.getState().updatePostsState(key, {
|
|
posts: result.posts,
|
|
hasMore: result.hasMore,
|
|
cursor: result.nextCursor || null,
|
|
isLoading: false,
|
|
lastLoadTime: Date.now(),
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : '搜索帖子失败';
|
|
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async fetchPostsByUser(userId: string, params?: GetPostsParams, key?: string): Promise<PostsResult> {
|
|
const listKey = key || `user_${userId}`;
|
|
usePostListStore.getState().updatePostsState(listKey, { isLoading: true, error: null });
|
|
|
|
try {
|
|
const result = await postRepository.getPostsByUser(userId, params);
|
|
usePostListStore.getState().updatePostsState(listKey, {
|
|
posts: result.posts,
|
|
hasMore: result.hasMore,
|
|
cursor: result.nextCursor || null,
|
|
isLoading: false,
|
|
lastLoadTime: Date.now(),
|
|
});
|
|
return result;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : '获取用户帖子失败';
|
|
usePostListStore.getState().updatePostsState(listKey, { isLoading: false, error: errorMessage });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
clearPostsState(key: string = 'default'): void {
|
|
usePostListStore.getState().clearPostsState(key);
|
|
this.paginationManager.reset(key);
|
|
}
|
|
|
|
clearPostDetailState(postId: string): void {
|
|
usePostListStore.getState().clearPostDetailState(postId);
|
|
}
|
|
|
|
clearAllStates(): void {
|
|
usePostListStore.getState().clearAllStates();
|
|
this.paginationManager.clear();
|
|
}
|
|
|
|
getCurrentUserId(): string | null {
|
|
return this.currentUserId;
|
|
}
|
|
|
|
setCurrentUserId(userId: string | null): void {
|
|
this.currentUserId = userId;
|
|
}
|
|
|
|
getPaginationState(key: string): PaginationState {
|
|
return this.paginationManager.getState(key);
|
|
}
|
|
|
|
resetPagination(key: string): void {
|
|
this.paginationManager.reset(key);
|
|
}
|
|
|
|
isLoading(key: string = 'default'): boolean {
|
|
return usePostListStore.getState().getPostsState(key).isLoading;
|
|
}
|
|
|
|
hasError(key: string = 'default'): boolean {
|
|
return usePostListStore.getState().getPostsState(key).error !== null;
|
|
}
|
|
|
|
getError(key: string = 'default'): string | null {
|
|
return usePostListStore.getState().getPostsState(key).error;
|
|
}
|
|
}
|
|
|
|
export const postSyncService = PostSyncService.getInstance();
|
|
export { PostSyncService };
|
|
export default postSyncService;
|