feat(pagination): add updateItem for in-place list updates and optimize notifications
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m48s
Frontend CI / ota-android (push) Successful in 10m30s
Frontend CI / build-android-apk (push) Successful in 39m3s

Add `updateItem` function to cursor pagination hook for updating single items
without full list refresh. Use this in NotificationsScreen to mark messages
as read locally instead of refetching. Also add `is_blocked` detection in
postService and userProfile to show blocked state in profile screen.
This commit is contained in:
lafay
2026-04-25 11:26:13 +08:00
parent ad19bc2af7
commit f16f001f6c
6 changed files with 53 additions and 17 deletions

View File

@@ -302,6 +302,20 @@ export function useCursorPagination<T, P = void>(
[] []
); );
// 就地更新单条数据
const updateItem = useCallback(
(id: string, updates: Partial<T>) => {
setState(prev => {
const idx = prev.list.findIndex(item => getItemId(item) === id);
if (idx === -1) return prev;
const newList = [...prev.list];
newList[idx] = { ...newList[idx], ...updates };
return { ...prev, list: newList };
});
},
[]
);
// 自动加载第一页 // 自动加载第一页
useEffect(() => { useEffect(() => {
// 使用 ref 防止重复加载,不依赖 loadMore 函数引用 // 使用 ref 防止重复加载,不依赖 loadMore 函数引用
@@ -377,6 +391,7 @@ export function useCursorPagination<T, P = void>(
refresh, refresh,
reset, reset,
setList, setList,
updateItem,
}; };
} }

View File

@@ -240,6 +240,8 @@ export interface CursorPaginationActions<T> {
reset: () => void; reset: () => void;
/** 设置数据(用于外部数据注入) */ /** 设置数据(用于外部数据注入) */
setList: (list: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void; setList: (list: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void;
/** 就地更新单条数据 */
updateItem: (id: string, updates: Partial<T>) => void;
} }
/** /**

View File

@@ -110,6 +110,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
hasMore, hasMore,
loadMore, loadMore,
refresh, refresh,
updateItem,
error: paginationError, error: paginationError,
} = useCursorPagination(fetchSystemMessages, { pageSize: 20 }); } = useCursorPagination(fetchSystemMessages, { pageSize: 20 });
@@ -182,17 +183,16 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
} }
}, [refresh, setSystemUnreadCount]); }, [refresh, setSystemUnreadCount]);
// 页面加载和获得焦点时刷新,并自动标记所有消息为已读 // 页面获得焦点时刷新未读数并标记全部已读
// 使用 ref 防止重复执行,避免无限循环 const markAllReadAndRefreshRef = useRef(handleMarkAllRead);
const hasInitializedRef = useRef(false); markAllReadAndRefreshRef.current = handleMarkAllRead;
useEffect(() => { useEffect(() => {
if (isFocused && !hasInitializedRef.current) { if (isFocused) {
hasInitializedRef.current = true;
fetchUnreadCount(); fetchUnreadCount();
// 进入界面自动标记所有消息为已读 markAllReadAndRefreshRef.current();
handleMarkAllRead();
} }
}, [isFocused]); // 只依赖 isFocused函数使用 ref 稳定引用 }, [isFocused]);
// 启动入场动画 // 启动入场动画
useEffect(() => { useEffect(() => {
@@ -308,13 +308,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
const messageId = String(message.id); const messageId = String(message.id);
const wasUnread = message.is_read !== true; const wasUnread = message.is_read !== true;
await messageService.markSystemMessageRead(messageId); await messageService.markSystemMessageRead(messageId);
// 【游标分页】不再直接修改 messages 状态,而是通过刷新获取最新数据
if (wasUnread) { if (wasUnread) {
updateItem(messageId, { is_read: true });
setUnreadCount(prev => Math.max(0, prev - 1)); setUnreadCount(prev => Math.max(0, prev - 1));
decrementSystemUnreadCount(1); decrementSystemUnreadCount(1);
} }
// 更新本地未读数以及全局 TabBar 红点
fetchUnreadCount();
messageManager.fetchUnreadCount(); messageManager.fetchUnreadCount();
// 根据消息类型处理导航 // 根据消息类型处理导航

View File

@@ -47,6 +47,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
handleMessage, handleMessage,
handleBlock, handleBlock,
isBlocked, isBlocked,
isBlockedProfile,
handleSettings, handleSettings,
handleEditProfile, handleEditProfile,
isCurrentUser, isCurrentUser,
@@ -54,7 +55,18 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
} = useUserProfile({ mode, userId, isDesktop, isTablet }); } = useUserProfile({ mode, userId, isDesktop, isTablet });
// 渲染帖子列表 - Twitter 风格无边框 // 渲染帖子列表 - Twitter 风格无边框
const renderPostList = useCallback((postList: Post[], emptyTitle: string, emptyDesc: string) => { const renderPostList = useCallback((postList: Post[], emptyTitle: string, emptyDesc: string, blockedTitle?: string, blockedDesc?: string) => {
if (isBlockedProfile && blockedTitle) {
return (
<EmptyState
title={blockedTitle}
description={blockedDesc || ''}
icon="account-off-outline"
variant="modern"
/>
);
}
if (postList.length === 0) { if (postList.length === 0) {
return ( return (
<EmptyState <EmptyState
@@ -85,7 +97,7 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
})} })}
</View> </View>
); );
}, [currentUser?.id, handlePostAction, activeTab]); }, [currentUser?.id, handlePostAction, activeTab, isBlockedProfile]);
// 渲染内容 // 渲染内容
const renderContent = useCallback(() => { const renderContent = useCallback(() => {
@@ -95,7 +107,9 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
return renderPostList( return renderPostList(
posts, posts,
mode === 'self' ? '还没有帖子' : '这个用户还没有发布任何帖子', mode === 'self' ? '还没有帖子' : '这个用户还没有发布任何帖子',
mode === 'self' ? '分享你的想法,发布第一条帖子吧' : '' mode === 'self' ? '分享你的想法,发布第一条帖子吧' : '',
mode === 'other' ? '已将该用户拉黑' : undefined,
mode === 'other' ? '你已将此用户拉黑,不再显示其帖子' : undefined
); );
} }

View File

@@ -34,6 +34,7 @@ export interface UseUserProfileReturn {
loading: boolean; loading: boolean;
refreshing: boolean; refreshing: boolean;
isBlocked: boolean; isBlocked: boolean;
isBlockedProfile: boolean;
// Tab 相关 // Tab 相关
activeTab: number; activeTab: number;
@@ -82,6 +83,7 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [isBlocked, setIsBlocked] = useState(false); const [isBlocked, setIsBlocked] = useState(false);
const [isBlockedProfile, setIsBlockedProfile] = useState(false);
// 获取其他用户数据other 模式) // 获取其他用户数据other 模式)
const loadOtherUserData = useCallback(async (forceRefresh = false) => { const loadOtherUserData = useCallback(async (forceRefresh = false) => {
@@ -98,6 +100,7 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
const response = await postService.getUserPosts(userId); const response = await postService.getUserPosts(userId);
setPosts(response.list); setPosts(response.list);
setIsBlockedProfile(response.is_blocked || false);
return userData; return userData;
} catch (error) { } catch (error) {
@@ -407,6 +410,7 @@ case 'like':
loading, loading,
refreshing, refreshing,
isBlocked, isBlocked,
isBlockedProfile,
activeTab, activeTab,
setActiveTab, setActiveTab,
scrollBottomInset, scrollBottomInset,

View File

@@ -14,6 +14,7 @@ interface PostListResponse {
page: number; page: number;
page_size: number; page_size: number;
total_pages: number; total_pages: number;
is_blocked?: boolean;
} }
// 帖子响应 - API直接返回Post对象 // 帖子响应 - API直接返回Post对象
@@ -131,7 +132,7 @@ class PostService {
userId: string, userId: string,
page = 1, page = 1,
pageSize = 20 pageSize = 20
): Promise<PaginatedData<Post>> { ): Promise<PaginatedData<Post> & { is_blocked?: boolean }> {
try { try {
const response = await api.get<PostListResponse>(`/users/${userId}/posts`, { const response = await api.get<PostListResponse>(`/users/${userId}/posts`, {
page, page,
@@ -368,13 +369,13 @@ class PostService {
async getUserPostsCursor( async getUserPostsCursor(
userId: string, userId: string,
params: CursorPaginationRequest = {} params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Post>> { ): Promise<CursorPaginationResponse<Post> & { is_blocked?: boolean }> {
try { try {
const response = await api.get<any>( const response = await api.get<any>(
`/users/${userId}/posts`, `/users/${userId}/posts`,
params params
); );
const data = response.data; const data = response.data;
if (data && typeof data === 'object' && Array.isArray(data.list)) { if (data && typeof data === 'object' && Array.isArray(data.list)) {
@@ -391,6 +392,7 @@ class PostService {
next_cursor: currentPage < totalPages ? String(currentPage + 1) : null, next_cursor: currentPage < totalPages ? String(currentPage + 1) : null,
prev_cursor: currentPage > 1 ? String(currentPage - 1) : null, prev_cursor: currentPage > 1 ? String(currentPage - 1) : null,
has_more: currentPage < totalPages, has_more: currentPage < totalPages,
is_blocked: data.is_blocked,
}; };
} }
return { return {
@@ -398,6 +400,7 @@ class PostService {
next_cursor: data.next_cursor ?? null, next_cursor: data.next_cursor ?? null,
prev_cursor: data.prev_cursor ?? null, prev_cursor: data.prev_cursor ?? null,
has_more: data.has_more ?? false, has_more: data.has_more ?? false,
is_blocked: data.is_blocked,
}; };
} }