dev #5
@@ -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;
|
||||||
setLoading(false);
|
|
||||||
|
const syncMembers = async () => {
|
||||||
|
const enrichedMembers = await enrichGroupMembersWithUserProfiles(members);
|
||||||
|
if (!active) return;
|
||||||
|
setLocalMembers(enrichedMembers);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
syncMembers();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
}, [members]);
|
}, [members]);
|
||||||
|
|
||||||
// 当前用户的成员信息
|
// 当前用户的成员信息
|
||||||
|
|||||||
@@ -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 : [],
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
110
src/stores/groupMemberListSources.ts
Normal file
110
src/stores/groupMemberListSources.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
46
src/stores/groupMemberProfileResolver.ts
Normal file
46
src/stores/groupMemberProfileResolver.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user