- Introduced a new PostCardRelativeTime component for dynamic time display, improving user experience with real-time updates. - Refactored PostCard to utilize memoization for performance optimization and prevent unnecessary re-renders. - Removed the PostCardGrid component to streamline the codebase, consolidating functionality within the PostCard component. - Updated HomeScreen to leverage the new PostCard features, ensuring consistent post rendering and interaction handling.
179 lines
5.0 KiB
TypeScript
179 lines
5.0 KiB
TypeScript
import React, { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
FlatList,
|
|
RefreshControl,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
View,
|
|
Alert,
|
|
} from 'react-native';
|
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { navigationService } from '../../infrastructure/navigation/navigationService';
|
|
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common';
|
|
import { authService } from '../../services';
|
|
import { colors, spacing, borderRadius, shadows } from '../../theme';
|
|
import { User } from '../../types';
|
|
import { useResponsive } from '../../hooks';
|
|
|
|
export const BlockedUsersScreen: React.FC = () => {
|
|
const { isMobile } = useResponsive();
|
|
const insets = useSafeAreaInsets();
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [processingUserId, setProcessingUserId] = useState<string | null>(null);
|
|
|
|
// 底部间距,避免被 TabBar 遮挡
|
|
const listBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
|
|
|
const loadBlockedUsers = useCallback(async () => {
|
|
try {
|
|
const response = await authService.getBlockedUsers(1, 100);
|
|
setUsers(response.list || []);
|
|
} catch (error) {
|
|
console.error('加载黑名单失败:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadBlockedUsers();
|
|
}, [loadBlockedUsers]);
|
|
|
|
const onRefresh = useCallback(() => {
|
|
setRefreshing(true);
|
|
loadBlockedUsers();
|
|
}, [loadBlockedUsers]);
|
|
|
|
const handleUnblock = useCallback((user: User) => {
|
|
Alert.alert(
|
|
'取消拉黑',
|
|
`确定取消拉黑 ${user.nickname} 吗?`,
|
|
[
|
|
{ text: '取消', style: 'cancel' },
|
|
{
|
|
text: '确定',
|
|
onPress: async () => {
|
|
try {
|
|
setProcessingUserId(user.id);
|
|
const ok = await authService.unblockUser(user.id);
|
|
if (!ok) {
|
|
Alert.alert('失败', '取消拉黑失败,请稍后重试');
|
|
return;
|
|
}
|
|
setUsers(prev => prev.filter(u => u.id !== user.id));
|
|
} finally {
|
|
setProcessingUserId(null);
|
|
}
|
|
},
|
|
},
|
|
]
|
|
);
|
|
}, []);
|
|
|
|
const renderItem = ({ item }: { item: User }) => (
|
|
<TouchableOpacity
|
|
style={styles.item}
|
|
activeOpacity={0.75}
|
|
onPress={() => navigationService.navigate('UserProfile', { userId: item.id })}
|
|
>
|
|
<Avatar source={item.avatar} size={46} name={item.nickname} />
|
|
<View style={styles.content}>
|
|
<Text variant="body" style={styles.nickname} numberOfLines={1}>
|
|
{item.nickname}
|
|
</Text>
|
|
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
|
|
@{item.username}
|
|
</Text>
|
|
</View>
|
|
<Button
|
|
title={processingUserId === item.id ? '处理中...' : '取消拉黑'}
|
|
size="sm"
|
|
variant="outline"
|
|
onPress={() => handleUnblock(item)}
|
|
disabled={processingUserId === item.id}
|
|
/>
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
<View style={styles.loadingWrap}>
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
<ResponsiveContainer maxWidth={900}>
|
|
<FlatList
|
|
data={users}
|
|
keyExtractor={item => item.id}
|
|
renderItem={renderItem}
|
|
contentContainerStyle={[styles.listContent, users.length === 0 && styles.emptyContent, { paddingBottom: listBottomInset }]}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
colors={[colors.primary.main]}
|
|
tintColor={colors.primary.main}
|
|
/>
|
|
}
|
|
ListEmptyComponent={
|
|
<EmptyState
|
|
title="黑名单为空"
|
|
description="你还没有拉黑任何用户"
|
|
icon="account-off-outline"
|
|
variant="modern"
|
|
/>
|
|
}
|
|
/>
|
|
</ResponsiveContainer>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: colors.background.default,
|
|
},
|
|
loadingWrap: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
listContent: {
|
|
padding: spacing.md,
|
|
gap: spacing.sm,
|
|
},
|
|
emptyContent: {
|
|
flexGrow: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
item: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.background.paper,
|
|
borderRadius: borderRadius.lg,
|
|
padding: spacing.md,
|
|
...shadows.sm,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
marginLeft: spacing.md,
|
|
marginRight: spacing.sm,
|
|
},
|
|
nickname: {
|
|
fontWeight: '600',
|
|
},
|
|
});
|
|
|
|
export default BlockedUsersScreen;
|