2026-03-09 21:29:03 +08:00
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
|
|
|
|
import { View, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
|
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { Avatar, Text } from '../../components/common';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import * as hrefs from '../../navigation/hrefs';
|
2026-04-13 04:56:58 +08:00
|
|
|
|
import { routePayloadCache } from '../../stores';
|
2026-04-13 01:30:37 +08:00
|
|
|
|
import { groupService } from '@/services/message';
|
2026-04-13 04:56:58 +08:00
|
|
|
|
import { groupManager } from '../../stores/group';
|
|
|
|
|
|
import { userManager } from '../../stores/user';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
|
|
|
|
|
|
|
|
|
|
|
|
const GroupRequestDetailScreen: React.FC = () => {
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const colors = useAppColors();
|
|
|
|
|
|
const styles = useMemo(() => createGroupRequestDetailStyles(colors), [colors]);
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
|
|
|
|
|
|
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
|
|
|
|
|
|
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
|
|
|
|
<View style={styles.emptyWrap}>
|
|
|
|
|
|
<Text variant="body" color="secondary">
|
|
|
|
|
|
无法加载该消息,请从通知列表重新打开。
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
|
|
|
|
|
<Text variant="body">返回</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
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 {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const group = await groupManager.getGroup(String(extra_data.group_id));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
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;
|
2026-03-12 08:37:08 +08:00
|
|
|
|
const groupId = extra_data?.group_id;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
if (!flag) {
|
|
|
|
|
|
Alert.alert('提示', '缺少请求标识,无法处理');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-12 08:37:08 +08:00
|
|
|
|
if (!groupId) {
|
|
|
|
|
|
Alert.alert('提示', '缺少群组标识,无法处理');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (message.system_type === 'group_invite') {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
await groupService.respondInvite(String(groupId), {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
flag,
|
|
|
|
|
|
approve,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
await groupService.reviewJoinRequest(String(groupId), {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
flag,
|
|
|
|
|
|
approve,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
Alert.alert('成功', approve ? '已同意申请' : '已拒绝申请', [
|
2026-03-24 14:21:31 +08:00
|
|
|
|
{ text: '确定', onPress: () => router.back() },
|
2026-03-09 21:29:03 +08:00
|
|
|
|
]);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
Alert.alert('操作失败', '请稍后重试');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleOpenUser = async () => {
|
|
|
|
|
|
if (!applicantId) return;
|
|
|
|
|
|
const user = await userManager.getUserById(applicantId);
|
|
|
|
|
|
if (user?.id) {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefUserProfile(String(user.id)));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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}
|
2026-03-24 14:21:31 +08:00
|
|
|
|
groupNo={String(extra_data?.group_id ?? '')}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
|
function createGroupRequestDetailStyles(colors: AppColors) {
|
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
emptyWrap: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
padding: spacing.lg,
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
backBtn: {
|
|
|
|
|
|
padding: spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
export default GroupRequestDetailScreen;
|