Files
frontend/src/screens/message/GroupInviteDetailScreen.tsx
lan 798dd7c9a0 feat(navigation): add schedule tab and stack navigator
Add new ScheduleTab to main navigation with ScheduleStackNavigator for managing course schedule screens. Includes CourseDetailScreen with modal presentation. Also exports scheduleService and related types for the new schedule feature.

fix(message): add group_id parameter to invite and request handlers

Pass group_id from extra_data when calling respondInvite and reviewJoinRequest APIs, as the backend now requires this parameter.

refactor(message): extract mergeMessagesById helper method

Centralize message merging logic into a reusable private method to avoid duplication and ensure consistent message deduplication behavior.
2026-03-12 08:37:08 +08:00

237 lines
7.3 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { View, StyleSheet, ActivityIndicator, Alert, ScrollView } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { Avatar, Text } from '../../components/common';
import { RootStackParamList } from '../../navigation/types';
import { groupService } from '../../services/groupService';
import { groupManager } from '../../stores/groupManager';
import { GroupMemberResponse } from '../../types/dto';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
type Route = RouteProp<RootStackParamList, 'GroupInviteDetail'>;
type Navigation = NativeStackNavigationProp<RootStackParamList>;
const GroupInviteDetailScreen: React.FC = () => {
const route = useRoute<Route>();
const navigation = useNavigation<Navigation>();
const { message } = route.params;
const { extra_data } = message;
const [loadingMembers, setLoadingMembers] = useState(false);
const [members, setMembers] = useState<GroupMemberResponse[]>([]);
const [submitting, setSubmitting] = useState(false);
const [memberCount, setMemberCount] = useState<number | null>(null);
const [loadingGroup, setLoadingGroup] = useState(false);
const canAction = extra_data?.request_status === 'pending';
useEffect(() => {
const loadGroup = async () => {
if (!extra_data?.group_id) return;
setLoadingGroup(true);
try {
const group = await groupManager.getGroup(extra_data.group_id);
setMemberCount(group.member_count ?? null);
} catch {
setMemberCount(null);
} finally {
setLoadingGroup(false);
}
};
loadGroup();
}, [extra_data?.group_id]);
useEffect(() => {
const loadMembers = async () => {
if (!extra_data?.group_id) return;
setLoadingMembers(true);
try {
const res = await groupManager.getMembers(extra_data.group_id, 1, 100);
const admins = (res.list || []).filter(m => m.role === 'owner' || m.role === 'admin');
setMembers(admins);
} catch {
setMembers([]);
} finally {
setLoadingMembers(false);
}
};
loadMembers();
}, [extra_data?.group_id]);
const handleDecision = async (approve: boolean) => {
const flag = extra_data?.flag;
const groupId = extra_data?.group_id;
if (!flag) {
Alert.alert('提示', '缺少邀请标识,无法处理');
return;
}
if (!groupId) {
Alert.alert('提示', '缺少群组标识,无法处理');
return;
}
setSubmitting(true);
try {
await groupService.respondInvite(groupId, { flag, approve });
Alert.alert('成功', approve ? '已同意加入群聊' : '已拒绝邀请', [
{ text: '确定', onPress: () => navigation.goBack() },
]);
} catch {
Alert.alert('操作失败', '请稍后重试');
} finally {
setSubmitting(false);
}
};
const groupNo = useMemo(() => extra_data?.group_id || '-', [extra_data?.group_id]);
const formatGroupNo = (id: string) => {
if (id.length <= 12) return id;
return `${id.slice(0, 6)}...${id.slice(-4)}`;
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<GroupInfoSummaryCard
groupName={extra_data?.group_name}
groupAvatar={extra_data?.group_avatar}
groupNo={formatGroupNo(groupNo)}
groupDescription={extra_data?.group_description}
memberCountText={loadingGroup ? '人数加载中...' : `${memberCount ?? '-'}`}
/>
<View style={styles.card}>
<View style={styles.cardHeader}>
<View style={styles.cardIconContainer}>
<MaterialCommunityIcons name="account-multiple" size={18} color={colors.info.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
<Text variant="caption" color={colors.text.secondary} style={styles.memberCount}>
{members.length}
</Text>
</View>
{loadingMembers ? (
<View style={styles.loadingWrap}>
<ActivityIndicator color={colors.primary.main} />
</View>
) : (
<View style={styles.memberPreview}>
{members.slice(0, 12).map((member, index) => (
<View
key={member.id}
style={[
styles.memberAvatar,
index === 0 && styles.memberAvatarFirst,
{ zIndex: index + 1 },
]}
>
<Avatar
source={member.user?.avatar || ''}
size={44}
name={member.user?.nickname || member.nickname}
/>
{member.role === 'owner' && (
<View style={styles.ownerBadge}>
<Text variant="caption" color={colors.background.paper} style={styles.ownerBadgeText}></Text>
</View>
)}
</View>
))}
{members.length === 0 && (
<Text variant="caption" color={colors.text.secondary}>
</Text>
)}
</View>
)}
</View>
</ScrollView>
<DecisionFooter
canAction={canAction}
submitting={submitting}
onReject={() => handleDecision(false)}
onApprove={() => handleDecision(true)}
processedText="该邀请已处理"
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.sm,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
},
cardIconContainer: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: colors.info.light + '30',
alignItems: 'center',
justifyContent: 'center',
marginRight: spacing.sm,
},
cardTitle: {
fontWeight: '600',
flex: 1,
},
memberCount: {
marginRight: spacing.xs,
},
loadingWrap: {
paddingVertical: spacing.md,
alignItems: 'center',
},
memberPreview: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
},
memberAvatar: {
marginLeft: -8,
marginBottom: spacing.sm,
},
memberAvatarFirst: {
marginLeft: 0,
},
ownerBadge: {
position: 'absolute',
bottom: -2,
right: -2,
backgroundColor: colors.warning.main,
borderRadius: 8,
paddingHorizontal: 4,
paddingVertical: 1,
},
ownerBadgeText: {
fontSize: 10,
fontWeight: '700',
},
});
export default GroupInviteDetailScreen;