From aaf53d1c3ba44165152f813e09101f474efae0d4 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Mon, 23 Mar 2026 23:38:53 +0800 Subject: [PATCH] refactor(GroupMembersScreen): enhance member list management with remote data source - Introduced a new remote data source for managing group members using cursor pagination. - Updated member list loading logic to utilize the new data source, improving data synchronization and loading states. - Enhanced member enrichment process by integrating user profiles into the member list. - Refactored related imports and hooks for better organization and clarity. --- src/screens/message/GroupMembersScreen.tsx | 41 ++++++-- src/services/commentService.ts | 4 +- src/services/groupService.ts | 8 +- src/services/messageService.ts | 4 +- src/services/notificationService.ts | 2 +- src/stores/groupMemberListSources.ts | 110 +++++++++++++++++++++ src/stores/groupMemberProfileResolver.ts | 46 +++++++++ src/stores/index.ts | 9 ++ 8 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 src/stores/groupMemberListSources.ts create mode 100644 src/stores/groupMemberProfileResolver.ts diff --git a/src/screens/message/GroupMembersScreen.tsx b/src/screens/message/GroupMembersScreen.tsx index 6f7b189..15ffa5d 100644 --- a/src/screens/message/GroupMembersScreen.tsx +++ b/src/screens/message/GroupMembersScreen.tsx @@ -25,7 +25,12 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'; import { colors, spacing, fontSizes, borderRadius } from '../../theme'; import { useAuthStore } from '../../stores'; import { groupService } from '../../services/groupService'; -import { groupManager } from '../../stores/groupManager'; +import { + createRemoteGroupMemberListSource, + GroupMemberListSourceKind, + IGroupMemberListPagedSource, +} from '../../stores/groupMemberListSources'; +import { enrichGroupMembersWithUserProfiles } from '../../stores/groupMemberProfileResolver'; import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { useCursorPagination } from '../../hooks/useCursorPagination'; @@ -36,6 +41,7 @@ import { import { RootStackParamList } from '../../navigation/types'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const GROUP_MEMBER_REMOTE_LIST_KIND: GroupMemberListSourceKind = 'cursor'; // 网格布局配置 const GRID_CONFIG = { @@ -70,7 +76,11 @@ const GroupMembersScreen: React.FC = () => { return GRID_CONFIG.mobile; }, [width]); - // 使用游标分页 Hook 管理成员列表 + const memberListSource = useMemo(() => { + return createRemoteGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50); + }, [groupId]); + + // 使用统一数据源 + 游标 Hook 管理成员列表 const { list: members, isLoading, @@ -80,11 +90,12 @@ const GroupMembersScreen: React.FC = () => { refresh, error, } = useCursorPagination( - async ({ cursor, pageSize }) => { - return await groupService.getGroupMembersCursor(groupId, { - cursor, - page_size: pageSize, - }); + async ({ cursor }) => { + // cursor 为空表示第一页/刷新,此时重置数据源状态 + if (!cursor) { + memberListSource.restart(); + } + return await memberListSource.loadNext(); }, { pageSize: 50 } ); @@ -95,8 +106,20 @@ const GroupMembersScreen: React.FC = () => { // 同步分页数据到本地状态 useEffect(() => { - setLocalMembers(members); - setLoading(false); + let active = true; + + const syncMembers = async () => { + const enrichedMembers = await enrichGroupMembersWithUserProfiles(members); + if (!active) return; + setLocalMembers(enrichedMembers); + setLoading(false); + }; + + syncMembers(); + + return () => { + active = false; + }; }, [members]); // 当前用户的成员信息 diff --git a/src/services/commentService.ts b/src/services/commentService.ts index 4049ec5..698c0b7 100644 --- a/src/services/commentService.ts +++ b/src/services/commentService.ts @@ -255,7 +255,7 @@ class CommentService { params: CursorPaginationRequest = {} ): Promise> { try { - const response = await api.get(`/comments/post/${postId}/cursor`, { params }); + const response = await api.get(`/comments/post/${postId}/cursor`, params); const raw = response.data; return { list: Array.isArray(raw?.list) ? raw.list : [], @@ -285,7 +285,7 @@ class CommentService { params: CursorPaginationRequest = {} ): Promise> { try { - const response = await api.get(`/comments/${commentId}/replies/cursor`, { params }); + const response = await api.get(`/comments/${commentId}/replies/cursor`, params); const raw = response.data; return { list: Array.isArray(raw?.list) ? raw.list : [], diff --git a/src/services/groupService.ts b/src/services/groupService.ts index f6ca233..5efb847 100644 --- a/src/services/groupService.ts +++ b/src/services/groupService.ts @@ -334,9 +334,7 @@ class GroupService { params: CursorPaginationRequest = {} ): Promise> { try { - const response = await api.get>('/groups/cursor', { - params, - }); + const response = await api.get>('/groups/cursor', params); return response.data; } catch (error) { console.error('获取群组列表失败:', error); @@ -362,7 +360,7 @@ class GroupService { try { const response = await api.get>( `/groups/${encodeURIComponent(String(groupId))}/members/cursor`, - { params } + params ); return response.data; } catch (error) { @@ -389,7 +387,7 @@ class GroupService { try { const response = await api.get>( `/groups/${encodeURIComponent(String(groupId))}/announcements/cursor`, - { params } + params ); return response.data; } catch (error) { diff --git a/src/services/messageService.ts b/src/services/messageService.ts index 1bcab13..270ca1f 100644 --- a/src/services/messageService.ts +++ b/src/services/messageService.ts @@ -571,7 +571,7 @@ class MessageService { try { const response = await api.get>( '/conversations/cursor', - { params } + params ); const raw = response.data; return { @@ -604,7 +604,7 @@ class MessageService { try { const response = await api.get( `/conversations/${encodeURIComponent(conversationId)}/messages/cursor`, - { params } + params ); const raw = response.data; return { diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index b5de12e..99a912d 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -166,7 +166,7 @@ class NotificationService { try { const response = await api.get>( '/notifications/cursor', - { params } + params ); return response.data; } catch (error) { diff --git a/src/stores/groupMemberListSources.ts b/src/stores/groupMemberListSources.ts new file mode 100644 index 0000000..a9de187 --- /dev/null +++ b/src/stores/groupMemberListSources.ts @@ -0,0 +1,110 @@ +import { groupService } from '../services/groupService'; +import { CursorPaginationResponse, GroupMemberResponse } from '../types/dto'; + +export const GROUP_MEMBER_LIST_PAGE_SIZE = 50; + +export type GroupMemberListSourceKind = 'cursor' | 'offset'; + +export interface IGroupMemberListPagedSource { + restart(): void; + loadNext(): Promise>; + readonly hasMore: boolean; +} + +export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberListPagedSource { + private nextCursor: string | null = null; + private hasMoreAfterLastLoad = false; + private loadedOnce = false; + private readonly groupId: number | string; + private readonly pageSize: number; + + constructor(groupId: number | string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) { + this.groupId = groupId; + this.pageSize = pageSize; + } + + restart(): void { + this.nextCursor = null; + this.hasMoreAfterLastLoad = false; + this.loadedOnce = false; + } + + get hasMore(): boolean { + return this.loadedOnce && this.hasMoreAfterLastLoad; + } + + async loadNext(): Promise> { + const response = await groupService.getGroupMembersCursor( + this.groupId, + this.nextCursor == null + ? { page_size: this.pageSize } + : { cursor: this.nextCursor, page_size: this.pageSize } + ); + + this.nextCursor = response.next_cursor ?? null; + this.hasMoreAfterLastLoad = Boolean(response.has_more && response.next_cursor != null); + this.loadedOnce = true; + + return { + list: response.list || [], + next_cursor: response.next_cursor ?? null, + prev_cursor: response.prev_cursor ?? null, + has_more: response.has_more ?? false, + }; + } +} + +export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberListPagedSource { + private nextPage = 1; + private hasMoreAfterLastLoad = false; + private loadedOnce = false; + private readonly groupId: number | string; + private readonly pageSize: number; + + constructor(groupId: number | string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) { + this.groupId = groupId; + this.pageSize = pageSize; + } + + restart(): void { + this.nextPage = 1; + this.hasMoreAfterLastLoad = false; + this.loadedOnce = false; + } + + get hasMore(): boolean { + return this.loadedOnce && this.hasMoreAfterLastLoad; + } + + async loadNext(): Promise> { + const response = await groupService.getMembers(this.groupId, this.nextPage, this.pageSize); + const list = response.list || []; + const currentPage = response.page ?? this.nextPage; + const totalPages = response.total_pages ?? currentPage; + const hasMore = currentPage < totalPages; + const nextPageCursor = hasMore ? String(currentPage + 1) : null; + const prevPageCursor = currentPage > 1 ? String(currentPage - 1) : null; + + this.nextPage = currentPage + 1; + this.hasMoreAfterLastLoad = hasMore; + this.loadedOnce = true; + + return { + list, + next_cursor: nextPageCursor, + prev_cursor: prevPageCursor, + has_more: hasMore, + }; + } +} + +export function createRemoteGroupMemberListSource( + kind: GroupMemberListSourceKind, + groupId: number | string, + pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE +): IGroupMemberListPagedSource { + return kind === 'offset' + ? new NetworkOffsetGroupMemberListPagedSource(groupId, pageSize) + : new NetworkCursorGroupMemberListPagedSource(groupId, pageSize); +} + diff --git a/src/stores/groupMemberProfileResolver.ts b/src/stores/groupMemberProfileResolver.ts new file mode 100644 index 0000000..0b14c63 --- /dev/null +++ b/src/stores/groupMemberProfileResolver.ts @@ -0,0 +1,46 @@ +import { GroupMemberResponse, UserDTO } from '../types/dto'; +import { getUserCache } from '../services/database'; +import { userManager } from './userManager'; + +/** + * 为群成员列表补全用户资料(昵称、头像、用户名)。 + * cursor 接口仅返回 user_id 时,通过本地缓存和用户接口回填。 + */ +export async function enrichGroupMembersWithUserProfiles( + members: GroupMemberResponse[] +): Promise { + if (!Array.isArray(members) || members.length === 0) { + return []; + } + + const userIds = [...new Set(members.map((member) => String(member.user_id)))]; + const userMap = new Map(); + + await Promise.all( + userIds.map(async (userId) => { + try { + const cachedUser = await getUserCache(userId); + if (cachedUser) { + userMap.set(userId, cachedUser); + return; + } + + const fetchedUser = await userManager.getUserById(userId); + if (fetchedUser) { + userMap.set(userId, fetchedUser); + } + } catch (error) { + console.error('补全群成员用户资料失败:', error); + } + }) + ); + + return members.map((member) => { + if (member.user?.nickname || member.user?.username) { + return member; + } + const user = userMap.get(String(member.user_id)); + return user ? { ...member, user } : member; + }); +} + diff --git a/src/stores/index.ts b/src/stores/index.ts index 927185a..fe00c21 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -30,6 +30,15 @@ export { SqliteConversationListPagedSource, createRemoteConversationListSource, } from './conversationListSources'; +export { + GROUP_MEMBER_LIST_PAGE_SIZE, + type GroupMemberListSourceKind, + type IGroupMemberListPagedSource, + NetworkCursorGroupMemberListPagedSource, + NetworkOffsetGroupMemberListPagedSource, + createRemoteGroupMemberListSource, +} from './groupMemberListSources'; +export { enrichGroupMembersWithUserProfiles } from './groupMemberProfileResolver'; export { postManager } from './postManager'; export { groupManager } from './groupManager'; export { userManager } from './userManager';