@oxyhq/services
Version:
405 lines (404 loc) • 12 kB
JavaScript
"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