Files
frontend/src/screens/message/components/MutualFollowSelectorModal.tsx
lafay 4ee3079b9f
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 8m15s
Frontend CI / ota-android (push) Successful in 10m56s
Frontend CI / build-android-apk (push) Successful in 1h3m22s
feat(Theme): enhance theming and UI consistency across components
- Updated app.json to set userInterfaceStyle to automatic for improved theme adaptability.
- Refactored layout components to utilize useAppColors for dynamic theming, ensuring consistent color usage.
- Introduced SystemChrome component to manage system UI background color based on theme.
- Enhanced TabsLayout, ProfileStackLayout, and other components to leverage new theming structure.
- Improved QRCodeScanner, SearchBar, and CommentItem styles to align with the updated theme system.
- Consolidated styles in SystemMessageItem and TabBar for better maintainability and visual coherence.
2026-03-25 05:16:54 +08:00

307 lines
9.2 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import {
View,
StyleSheet,
TouchableOpacity,
Modal,
KeyboardAvoidingView,
Platform,
FlatList,
ListRenderItem,
Alert,
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../../theme';
import { authService } from '../../../services/authService';
import { useAuthStore } from '../../../stores';
import { Avatar, EmptyState, Loading, Text } from '../../../components/common';
import { User } from '../../../types';
type MutualFollowSelectorModalProps = {
visible: boolean;
title?: string;
confirmText?: string;
confirmingText?: string;
confirmLoading?: boolean;
excludeUserIds?: string[];
initialSelectedIds?: string[];
onClose: () => void;
onConfirm: (selectedUsers: User[]) => void | Promise<void>;
};
const MutualFollowSelectorModal: React.FC<MutualFollowSelectorModalProps> = ({
visible,
title = '邀请成员',
confirmText = '确认',
confirmingText = '处理中...',
confirmLoading = false,
excludeUserIds = [],
initialSelectedIds = [],
onClose,
onConfirm,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createMutualFollowSelectorStyles(colors), [colors]);
const { currentUser } = useAuthStore();
const [friendList, setFriendList] = useState<User[]>([]);
const [friendLoading, setFriendLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const currentUserId = currentUser?.id;
const excludeIdsKey = useMemo(() => excludeUserIds.join(','), [excludeUserIds]);
const initialSelectedIdsKey = useMemo(() => initialSelectedIds.join(','), [initialSelectedIds]);
const stableExcludeUserIds = useMemo(() => excludeUserIds, [excludeIdsKey]);
const stableInitialSelectedIds = useMemo(() => initialSelectedIds, [initialSelectedIdsKey]);
useEffect(() => {
if (!visible) return;
if (!currentUserId) return;
const nextSelectedIds = new Set(stableInitialSelectedIds);
setSelectedIds(prev => {
if (prev.size !== nextSelectedIds.size) return nextSelectedIds;
for (const id of prev) {
if (!nextSelectedIds.has(id)) return nextSelectedIds;
}
return prev;
});
let cancelled = false;
const loadMutualFollows = async () => {
setFriendLoading(true);
try {
const excludedIdSet = new Set(stableExcludeUserIds);
const [following, followers] = await Promise.all([
authService.getFollowingList(currentUserId, 1, 100),
authService.getFollowersList(currentUserId, 1, 100),
]);
if (cancelled) return;
const followerIdSet = new Set(followers.map(user => user.id));
const mutualFriends = following.filter(user => followerIdSet.has(user.id));
const availableFriends = mutualFriends.filter(user => !excludedIdSet.has(user.id));
setFriendList(availableFriends);
} catch (error) {
console.error('获取互关好友失败:', error);
Alert.alert('错误', '获取互关好友失败');
} finally {
if (!cancelled) {
setFriendLoading(false);
}
}
};
loadMutualFollows();
return () => {
cancelled = true;
};
}, [visible, currentUserId, stableExcludeUserIds, stableInitialSelectedIds]);
const toggleSelection = (user: User) => {
const next = new Set(selectedIds);
if (next.has(user.id)) {
next.delete(user.id);
} else {
next.add(user.id);
}
setSelectedIds(next);
};
const handleConfirm = () => {
const selectedUsers = friendList.filter(user => selectedIds.has(user.id));
onConfirm(selectedUsers);
};
const renderFriendItem: ListRenderItem<User> = ({ item }) => {
const isSelected = selectedIds.has(item.id);
return (
<TouchableOpacity
style={[styles.friendItem, isSelected && styles.friendItemSelected]}
onPress={() => toggleSelection(item)}
activeOpacity={0.7}
>
<Avatar source={item.avatar} size={48} name={item.nickname} />
<View style={styles.friendInfo}>
<Text variant="body" style={styles.nickname} numberOfLines={1}>
{item.nickname}
</Text>
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
@{item.username}
</Text>
</View>
<View style={[styles.checkbox, isSelected && styles.checkboxSelected]}>
{isSelected && (
<MaterialCommunityIcons name="check" size={16} color={colors.background.paper} />
)}
</View>
</TouchableOpacity>
);
};
return (
<Modal
visible={visible}
animationType="slide"
transparent
onRequestClose={onClose}
>
<KeyboardAvoidingView
style={styles.modalOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={onClose} style={styles.modalHeaderButton}>
<Text variant="body" color={colors.text.secondary}></Text>
</TouchableOpacity>
<Text variant="h3" style={styles.modalTitle}>{title}</Text>
<TouchableOpacity
onPress={handleConfirm}
disabled={selectedIds.size === 0 || confirmLoading}
style={styles.modalHeaderButton}
>
<Text
variant="body"
color={selectedIds.size === 0 || confirmLoading ? colors.text.disabled : colors.primary.main}
style={styles.confirmButton}
>
{confirmLoading ? confirmingText : confirmText}
</Text>
</TouchableOpacity>
</View>
<View style={styles.tipContainer}>
<Text variant="body" color={colors.primary.main} style={styles.tipText}>
</Text>
</View>
{selectedIds.size > 0 && (
<View style={styles.selectedBadge}>
<Text variant="caption" color={colors.primary.main}>
{selectedIds.size}
</Text>
</View>
)}
{friendLoading ? (
<Loading />
) : (
<FlatList
data={friendList}
renderItem={renderFriendItem}
keyExtractor={item => item.id}
contentContainerStyle={styles.friendListContent}
showsVerticalScrollIndicator={false}
ListEmptyComponent={(
<EmptyState
title="暂无可选择的互关好友"
description="互相关注后才可邀请加入群聊"
icon="account-plus-outline"
/>
)}
/>
)}
</View>
</KeyboardAvoidingView>
</Modal>
);
};
function createMutualFollowSelectorStyles(colors: AppColors) {
return StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.background.paper,
borderTopLeftRadius: borderRadius['2xl'],
borderTopRightRadius: borderRadius['2xl'],
height: '80%',
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
paddingBottom: spacing.xl,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.lg,
},
modalHeaderButton: {
paddingVertical: spacing.sm,
minWidth: 60,
},
modalTitle: {
fontWeight: '700',
fontSize: fontSizes.xl,
color: colors.text.primary,
},
confirmButton: {
fontWeight: '600',
textAlign: 'right',
},
tipContainer: {
flexDirection: 'row',
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
padding: spacing.xs,
marginBottom: spacing.md,
},
tipText: {
fontWeight: '600',
},
selectedBadge: {
backgroundColor: colors.primary.light + '20',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: borderRadius.sm,
alignSelf: 'flex-start',
marginBottom: spacing.md,
},
friendListContent: {
paddingBottom: spacing.xl,
},
friendItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.sm,
borderRadius: borderRadius.lg,
marginBottom: spacing.xs,
},
friendItemSelected: {
backgroundColor: colors.primary.light + '15',
},
friendInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '500',
marginBottom: 2,
},
checkbox: {
width: 26,
height: 26,
borderRadius: 13,
borderWidth: 2,
borderColor: colors.divider,
justifyContent: 'center',
alignItems: 'center',
},
checkboxSelected: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
});
}
export default MutualFollowSelectorModal;