refactor(GroupMembersScreen): enhance member list management with remote data source
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m45s
Frontend CI / ota-android (push) Successful in 13m15s
Frontend CI / build-android-apk (push) Successful in 1h1m10s

- 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.
This commit is contained in:
lafay
2026-03-23 23:38:53 +08:00
parent c98f1917f7
commit aaf53d1c3b
8 changed files with 205 additions and 19 deletions

View File

@@ -25,7 +25,12 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../theme'; import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
import { groupService } from '../../services/groupService'; 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 { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
import { useCursorPagination } from '../../hooks/useCursorPagination'; import { useCursorPagination } from '../../hooks/useCursorPagination';
@@ -36,6 +41,7 @@ import {
import { RootStackParamList } from '../../navigation/types'; import { RootStackParamList } from '../../navigation/types';
const { width: SCREEN_WIDTH } = Dimensions.get('window'); const { width: SCREEN_WIDTH } = Dimensions.get('window');
const GROUP_MEMBER_REMOTE_LIST_KIND: GroupMemberListSourceKind = 'cursor';
// 网格布局配置 // 网格布局配置
const GRID_CONFIG = { const GRID_CONFIG = {
@@ -70,7 +76,11 @@ const GroupMembersScreen: React.FC = () => {
return GRID_CONFIG.mobile; return GRID_CONFIG.mobile;
}, [width]); }, [width]);
// 使用游标分页 Hook 管理成员列表 const memberListSource = useMemo<IGroupMemberListPagedSource>(() => {
return createRemoteGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50);
}, [groupId]);
// 使用统一数据源 + 游标 Hook 管理成员列表
const { const {
list: members, list: members,
isLoading, isLoading,
@@ -80,11 +90,12 @@ const GroupMembersScreen: React.FC = () => {
refresh, refresh,
error, error,
} = useCursorPagination( } = useCursorPagination(
async ({ cursor, pageSize }) => { async ({ cursor }) => {
return await groupService.getGroupMembersCursor(groupId, { // cursor 为空表示第一页/刷新,此时重置数据源状态
cursor, if (!cursor) {
page_size: pageSize, memberListSource.restart();
}); }
return await memberListSource.loadNext();
}, },
{ pageSize: 50 } { pageSize: 50 }
); );
@@ -95,8 +106,20 @@ const GroupMembersScreen: React.FC = () => {
// 同步分页数据到本地状态 // 同步分页数据到本地状态
useEffect(() => { useEffect(() => {
setLocalMembers(members); let active = true;
const syncMembers = async () => {
const enrichedMembers = await enrichGroupMembersWithUserProfiles(members);
if (!active) return;
setLocalMembers(enrichedMembers);
setLoading(false); setLoading(false);
};
syncMembers();
return () => {
active = false;
};
}, [members]); }, [members]);
// 当前用户的成员信息 // 当前用户的成员信息

View File

@@ -255,7 +255,7 @@ class CommentService {
params: CursorPaginationRequest = {} params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Comment>> { ): Promise<CursorPaginationResponse<Comment>> {
try { try {
const response = await api.get<any>(`/comments/post/${postId}/cursor`, { params }); const response = await api.get<any>(`/comments/post/${postId}/cursor`, params);
const raw = response.data; const raw = response.data;
return { return {
list: Array.isArray(raw?.list) ? raw.list : [], list: Array.isArray(raw?.list) ? raw.list : [],
@@ -285,7 +285,7 @@ class CommentService {
params: CursorPaginationRequest = {} params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Comment>> { ): Promise<CursorPaginationResponse<Comment>> {
try { try {
const response = await api.get<any>(`/comments/${commentId}/replies/cursor`, { params }); const response = await api.get<any>(`/comments/${commentId}/replies/cursor`, params);
const raw = response.data; const raw = response.data;
return { return {
list: Array.isArray(raw?.list) ? raw.list : [], list: Array.isArray(raw?.list) ? raw.list : [],

View File

@@ -334,9 +334,7 @@ class GroupService {
params: CursorPaginationRequest = {} params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<GroupResponse>> { ): Promise<CursorPaginationResponse<GroupResponse>> {
try { try {
const response = await api.get<CursorPaginationResponse<GroupResponse>>('/groups/cursor', { const response = await api.get<CursorPaginationResponse<GroupResponse>>('/groups/cursor', params);
params,
});
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('获取群组列表失败:', error); console.error('获取群组列表失败:', error);
@@ -362,7 +360,7 @@ class GroupService {
try { try {
const response = await api.get<CursorPaginationResponse<GroupMemberResponse>>( const response = await api.get<CursorPaginationResponse<GroupMemberResponse>>(
`/groups/${encodeURIComponent(String(groupId))}/members/cursor`, `/groups/${encodeURIComponent(String(groupId))}/members/cursor`,
{ params } params
); );
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -389,7 +387,7 @@ class GroupService {
try { try {
const response = await api.get<CursorPaginationResponse<GroupAnnouncementResponse>>( const response = await api.get<CursorPaginationResponse<GroupAnnouncementResponse>>(
`/groups/${encodeURIComponent(String(groupId))}/announcements/cursor`, `/groups/${encodeURIComponent(String(groupId))}/announcements/cursor`,
{ params } params
); );
return response.data; return response.data;
} catch (error) { } catch (error) {

View File

@@ -571,7 +571,7 @@ class MessageService {
try { try {
const response = await api.get<CursorPaginationResponse<ConversationResponse>>( const response = await api.get<CursorPaginationResponse<ConversationResponse>>(
'/conversations/cursor', '/conversations/cursor',
{ params } params
); );
const raw = response.data; const raw = response.data;
return { return {
@@ -604,7 +604,7 @@ class MessageService {
try { try {
const response = await api.get<any>( const response = await api.get<any>(
`/conversations/${encodeURIComponent(conversationId)}/messages/cursor`, `/conversations/${encodeURIComponent(conversationId)}/messages/cursor`,
{ params } params
); );
const raw = response.data; const raw = response.data;
return { return {

View File

@@ -166,7 +166,7 @@ class NotificationService {
try { try {
const response = await api.get<CursorPaginationResponse<Notification>>( const response = await api.get<CursorPaginationResponse<Notification>>(
'/notifications/cursor', '/notifications/cursor',
{ params } params
); );
return response.data; return response.data;
} catch (error) { } catch (error) {

View File

@@ -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<CursorPaginationResponse<GroupMemberResponse>>;
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<CursorPaginationResponse<GroupMemberResponse>> {
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<CursorPaginationResponse<GroupMemberResponse>> {
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);
}

View File

@@ -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<GroupMemberResponse[]> {
if (!Array.isArray(members) || members.length === 0) {
return [];
}
const userIds = [...new Set(members.map((member) => String(member.user_id)))];
const userMap = new Map<string, UserDTO>();
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;
});
}

View File

@@ -30,6 +30,15 @@ export {
SqliteConversationListPagedSource, SqliteConversationListPagedSource,
createRemoteConversationListSource, createRemoteConversationListSource,
} from './conversationListSources'; } 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 { postManager } from './postManager';
export { groupManager } from './groupManager'; export { groupManager } from './groupManager';
export { userManager } from './userManager'; export { userManager } from './userManager';