Files
frontend/src/screens/message/GroupRequestDetailScreen.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

203 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useMemo, useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
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 { userManager } from '../../stores/userManager';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
type Route = RouteProp<RootStackParamList, 'GroupRequestDetail'>;
type Navigation = NativeStackNavigationProp<RootStackParamList>;
const GroupRequestDetailScreen: React.FC = () => {
const route = useRoute<Route>();
const navigation = useNavigation<Navigation>();
const { message } = route.params;
const { extra_data } = message;
const [submitting, setSubmitting] = useState(false);
const [memberCount, setMemberCount] = useState<number | null>(null);
const [loadingGroup, setLoadingGroup] = useState(false);
const requestType = extra_data?.request_type;
const requestStatus = extra_data?.request_status;
const reviewerName = extra_data?.actor_name || extra_data?.operator_name || '管理员';
const canAction =
requestStatus === 'pending' &&
(message.system_type === 'group_join_apply' || message.system_type === 'group_invite');
const processedText =
requestStatus === 'accepted'
? `${reviewerName}已同意`
: requestStatus === 'rejected'
? `${reviewerName}已拒绝`
: '该请求已处理';
const applicantName = useMemo(() => {
if (requestType === 'invite') {
return extra_data?.target_user_name || '被邀请用户';
}
return extra_data?.actor_name || '申请用户';
}, [requestType, extra_data]);
const applicantAvatar = useMemo(() => {
if (requestType === 'invite') {
return extra_data?.target_user_avatar || '';
}
return extra_data?.avatar_url || '';
}, [requestType, extra_data]);
const applicantId = useMemo(() => {
if (requestType === 'invite') {
return extra_data?.target_user_id;
}
return extra_data?.actor_id_str;
}, [requestType, extra_data]);
useEffect(() => {
const loadGroupInfo = 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);
}
};
loadGroupInfo();
}, [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 {
if (message.system_type === 'group_invite') {
await groupService.respondInvite(groupId, {
flag,
approve,
});
} else {
await groupService.reviewJoinRequest(groupId, {
flag,
approve,
});
}
Alert.alert('成功', approve ? '已同意申请' : '已拒绝申请', [
{ text: '确定', onPress: () => navigation.goBack() },
]);
} catch (error) {
Alert.alert('操作失败', '请稍后重试');
} finally {
setSubmitting(false);
}
};
const handleOpenUser = async () => {
if (!applicantId) return;
const user = await userManager.getUserById(applicantId);
if (user?.id) {
navigation.navigate('UserProfile', { userId: String(user.id) });
}
};
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={extra_data?.group_id}
groupDescription={extra_data?.group_description}
memberCountText={loadingGroup ? '人数加载中...' : `${memberCount ?? '-'}`}
/>
<View style={styles.card}>
<Text variant="label" style={styles.sectionTitle}></Text>
<TouchableOpacity style={styles.row} activeOpacity={0.7} onPress={handleOpenUser} disabled={!applicantId}>
<Avatar source={applicantAvatar} size={44} name={applicantName} />
<View style={styles.meta}>
<Text variant="body" style={styles.name}>{applicantName}</Text>
<Text variant="caption" color={colors.text.secondary}>
{requestType === 'invite' ? '被邀请加入' : '申请加入'}
</Text>
</View>
{applicantId ? (
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
) : null}
</TouchableOpacity>
{requestType === 'invite' && (
<Text variant="caption" color={colors.text.secondary} style={styles.subDesc}>
{extra_data?.actor_name || '群成员'}
</Text>
)}
</View>
</ScrollView>
<DecisionFooter
canAction={canAction}
submitting={submitting}
onReject={() => handleDecision(false)}
onApprove={() => handleDecision(true)}
processedText={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,
marginBottom: spacing.md,
...shadows.sm,
},
sectionTitle: {
marginBottom: spacing.sm,
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
meta: {
marginLeft: spacing.md,
flex: 1,
},
name: {
marginBottom: 2,
},
subDesc: {
marginTop: spacing.sm,
},
});
export default GroupRequestDetailScreen;