feat(pagination): add updateItem for in-place list updates and optimize notifications
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:
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
// 根据消息类型处理导航
|
// 根据消息类型处理导航
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,7 +369,7 @@ 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`,
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user