- 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.
307 lines
9.2 KiB
TypeScript
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;
|