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.
237 lines
7.3 KiB
TypeScript
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;
|