UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

405 lines (404 loc) 12 kB
"use strict"; import React, { useEffect, useState, useCallback } from 'react'; import { View, Text, StyleSheet, ActivityIndicator, FlatList, TouchableOpacity, RefreshControl } from 'react-native'; import { useThemeColors } from "../styles/index.js"; import Avatar from "../components/Avatar.js"; import { FollowButton } from "../components/index.js"; import { Ionicons } from '@expo/vector-icons'; import { useI18n } from "../hooks/useI18n.js"; import { useOxy } from "../context/OxyContext.js"; import { logger } from '@oxyhq/core'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const PAGE_SIZE = 20; const UserListScreen = ({ userId, mode, initialCount, theme, goBack, navigate }) => { const { oxyServices, user: currentUser } = useOxy(); const [users, setUsers] = useState([]); const [total, setTotal] = useState(initialCount ?? 0); const [isLoading, setIsLoading] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [hasMore, setHasMore] = useState(true); const colors = useThemeColors(theme ?? 'light'); const styles = createStyles(colors); const { t } = useI18n(); const currentUserId = currentUser?.id || currentUser?._id; const fetchUsers = useCallback(async (offset = 0, isRefresh = false) => { if (!userId) { setError('No user ID provided'); setIsLoading(false); return; } try { if (isRefresh) { setIsRefreshing(true); } else if (offset === 0) { setIsLoading(true); } else { setIsLoadingMore(true); } setError(null); const response = mode === 'followers' ? await oxyServices.getUserFollowers(userId, { limit: PAGE_SIZE, offset }) : await oxyServices.getUserFollowing(userId, { limit: PAGE_SIZE, offset }); const newUsers = mode === 'followers' ? response.followers : response.following; if (offset === 0 || isRefresh) { setUsers(newUsers); } else { setUsers(prev => [...prev, ...newUsers]); } setTotal(response.total); setHasMore(response.hasMore); } catch (err) { logger.error(`Failed to fetch ${mode}`, err instanceof Error ? err : new Error(String(err)), { component: 'UserListScreen' }); setError(`Failed to load ${mode}. Please try again.`); } finally { setIsLoading(false); setIsLoadingMore(false); setIsRefreshing(false); } }, [userId, mode, oxyServices]); useEffect(() => { fetchUsers(0); }, [fetchUsers]); const handleLoadMore = useCallback(() => { if (!isLoadingMore && hasMore && !isLoading) { fetchUsers(users.length); } }, [isLoadingMore, hasMore, isLoading, users.length, fetchUsers]); const handleRefresh = useCallback(() => { fetchUsers(0, true); }, [fetchUsers]); const handleUserPress = useCallback(user => { const targetUserId = user.id || user._id; if (targetUserId && navigate) { navigate('Profile', { userId: targetUserId }); } }, [navigate]); const renderUser = useCallback(({ item }) => { const itemUserId = item.id || item._id || ''; const isCurrentUser = itemUserId === currentUserId; const description = typeof item.description === 'string' ? item.description : ''; return /*#__PURE__*/_jsxs(TouchableOpacity, { style: styles.userItem, onPress: () => handleUserPress(item), activeOpacity: 0.7, children: [/*#__PURE__*/_jsx(Avatar, { uri: item.avatar ? oxyServices.getFileDownloadUrl(item.avatar, 'thumb') : undefined, name: item.username || item.name?.full, size: 48 }), /*#__PURE__*/_jsxs(View, { style: styles.userInfo, children: [/*#__PURE__*/_jsx(Text, { style: styles.userName, numberOfLines: 1, children: item.name?.full || item.username || 'Unknown User' }), item.username && /*#__PURE__*/_jsxs(Text, { style: styles.userHandle, numberOfLines: 1, children: ["@", item.username] }), description ? /*#__PURE__*/_jsx(Text, { style: styles.userBio, numberOfLines: 2, children: description }) : null] }), !isCurrentUser && itemUserId ? /*#__PURE__*/_jsx(View, { style: styles.followButtonWrapper, children: /*#__PURE__*/_jsx(FollowButton, { userId: itemUserId, size: "small" }) }) : null] }); }, [colors, styles, handleUserPress, currentUserId, oxyServices]); const renderEmpty = useCallback(() => { if (isLoading) return null; return /*#__PURE__*/_jsxs(View, { style: styles.emptyContainer, children: [/*#__PURE__*/_jsx(Ionicons, { name: mode === 'followers' ? 'people-outline' : 'heart-outline', size: 64, color: colors.secondaryText }), /*#__PURE__*/_jsx(Text, { style: styles.emptyTitle, children: mode === 'followers' ? t('userList.noFollowers') || 'No followers yet' : t('userList.noFollowing') || 'Not following anyone' }), /*#__PURE__*/_jsx(Text, { style: styles.emptySubtitle, children: mode === 'followers' ? t('userList.noFollowersDesc') || 'When people follow this user, they will appear here.' : t('userList.noFollowingDesc') || 'When this user follows people, they will appear here.' })] }); }, [isLoading, mode, colors, styles, t]); const renderFooter = useCallback(() => { if (!isLoadingMore) return null; return /*#__PURE__*/_jsx(View, { style: styles.footerLoader, children: /*#__PURE__*/_jsx(ActivityIndicator, { size: "small", color: colors.primary }) }); }, [isLoadingMore, colors, styles]); const title = mode === 'followers' ? t('userList.followers') || 'Followers' : t('userList.following') || 'Following'; if (isLoading && users.length === 0) { return /*#__PURE__*/_jsxs(View, { style: styles.container, children: [/*#__PURE__*/_jsxs(View, { style: styles.header, children: [goBack && /*#__PURE__*/_jsx(TouchableOpacity, { onPress: goBack, style: styles.backButton, children: /*#__PURE__*/_jsx(Ionicons, { name: "arrow-back", size: 24, color: colors.text }) }), /*#__PURE__*/_jsx(Text, { style: styles.headerTitle, children: title }), /*#__PURE__*/_jsx(View, { style: styles.headerRight })] }), /*#__PURE__*/_jsx(View, { style: styles.loadingContainer, children: /*#__PURE__*/_jsx(ActivityIndicator, { size: "large", color: colors.primary }) })] }); } if (error) { return /*#__PURE__*/_jsxs(View, { style: styles.container, children: [/*#__PURE__*/_jsxs(View, { style: styles.header, children: [goBack && /*#__PURE__*/_jsx(TouchableOpacity, { onPress: goBack, style: styles.backButton, children: /*#__PURE__*/_jsx(Ionicons, { name: "arrow-back", size: 24, color: colors.text }) }), /*#__PURE__*/_jsx(Text, { style: styles.headerTitle, children: title }), /*#__PURE__*/_jsx(View, { style: styles.headerRight })] }), /*#__PURE__*/_jsxs(View, { style: styles.errorContainer, children: [/*#__PURE__*/_jsx(Ionicons, { name: "alert-circle", size: 48, color: colors.error }), /*#__PURE__*/_jsx(Text, { style: styles.errorText, children: error }), /*#__PURE__*/_jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => fetchUsers(0), children: /*#__PURE__*/_jsx(Text, { style: styles.retryButtonText, children: t('common.retry') || 'Retry' }) })] })] }); } return /*#__PURE__*/_jsxs(View, { style: styles.container, children: [/*#__PURE__*/_jsxs(View, { style: styles.header, children: [goBack && /*#__PURE__*/_jsx(TouchableOpacity, { onPress: goBack, style: styles.backButton, children: /*#__PURE__*/_jsx(Ionicons, { name: "arrow-back", size: 24, color: colors.text }) }), /*#__PURE__*/_jsxs(View, { style: styles.headerTitleContainer, children: [/*#__PURE__*/_jsx(Text, { style: styles.headerTitle, children: title }), total > 0 && /*#__PURE__*/_jsx(Text, { style: styles.headerCount, children: total })] }), /*#__PURE__*/_jsx(View, { style: styles.headerRight })] }), /*#__PURE__*/_jsx(FlatList, { data: users, renderItem: renderUser, keyExtractor: (item, index) => item.id || item._id || `user-${index}`, contentContainerStyle: styles.listContent, ItemSeparatorComponent: () => /*#__PURE__*/_jsx(View, { style: styles.separator }), ListEmptyComponent: renderEmpty, ListFooterComponent: renderFooter, onEndReached: handleLoadMore, onEndReachedThreshold: 0.3, refreshControl: /*#__PURE__*/_jsx(RefreshControl, { refreshing: isRefreshing, onRefresh: handleRefresh, tintColor: colors.primary, colors: [colors.primary] }) })] }); }; const createStyles = colors => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.border }, backButton: { padding: 8, marginRight: 8 }, headerTitleContainer: { flex: 1, flexDirection: 'row', alignItems: 'center' }, headerTitle: { fontSize: 18, fontWeight: '600', color: colors.text }, headerCount: { fontSize: 16, color: colors.secondaryText, marginLeft: 8 }, headerRight: { width: 40 }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 32 }, errorText: { fontSize: 16, color: colors.error, textAlign: 'center', marginTop: 16, marginBottom: 24 }, retryButton: { backgroundColor: colors.primary, paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8 }, retryButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' }, listContent: { flexGrow: 1 }, userItem: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12 }, userInfo: { flex: 1, marginLeft: 12, marginRight: 8 }, userName: { fontSize: 16, fontWeight: '600', color: colors.text }, userHandle: { fontSize: 14, color: colors.secondaryText, marginTop: 2 }, userBio: { fontSize: 14, color: colors.text, marginTop: 4, opacity: 0.8 }, followButtonWrapper: { marginLeft: 'auto' }, separator: { height: 1, backgroundColor: colors.border, marginLeft: 76 }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 32, paddingTop: 80 }, emptyTitle: { fontSize: 18, fontWeight: '600', color: colors.text, marginTop: 16, textAlign: 'center' }, emptySubtitle: { fontSize: 14, color: colors.secondaryText, marginTop: 8, textAlign: 'center' }, footerLoader: { paddingVertical: 16, alignItems: 'center' } }); export default UserListScreen; //# sourceMappingURL=UserListScreen.js.map