UNPKG

@oxyhq/services

Version:

Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀

617 lines (610 loc) • 21.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("react"); var _reactNative = require("react-native"); var _OxyContext = require("../context/OxyContext"); var _styles = require("../styles"); var _Avatar = _interopRequireDefault(require("../components/Avatar")); var _components = require("../components"); var _useFollow = require("../hooks/useFollow"); var _vectorIcons = require("@expo/vector-icons"); var _useI18n = require("../hooks/useI18n"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const ProfileScreen = ({ userId, username, theme, goBack, navigate }) => { const { oxyServices, user: currentUser } = (0, _OxyContext.useOxy)(); const [profile, setProfile] = (0, _react.useState)(null); const [karmaTotal, setKarmaTotal] = (0, _react.useState)(null); const [postsCount, setPostsCount] = (0, _react.useState)(null); const [commentsCount, setCommentsCount] = (0, _react.useState)(null); const [isLoading, setIsLoading] = (0, _react.useState)(true); const [error, setError] = (0, _react.useState)(null); const [links, setLinks] = (0, _react.useState)([]); // Use the follow hook for real follower data const { followerCount, followingCount, isLoadingCounts } = (0, _useFollow.useFollow)(userId); const colors = (0, _styles.useThemeColors)(theme); const styles = createStyles(colors); const { t } = (0, _useI18n.useI18n)(); // Check if current user is viewing their own profile const isOwnProfile = currentUser && currentUser.id === userId; (0, _react.useEffect)(() => { if (!userId) { setError('No user ID provided'); setIsLoading(false); return; } setIsLoading(true); setError(null); // Load user profile and karma total Promise.all([oxyServices.getUserById(userId).catch(err => { // If this is the current user and the API call fails, use current user data as fallback if (currentUser && currentUser.id === userId) { return currentUser; } throw err; }), oxyServices.getUserKarmaTotal ? oxyServices.getUserKarmaTotal(userId).catch(() => { return { total: undefined }; }) : Promise.resolve({ total: undefined })]).then(([profileRes, karmaRes]) => { setProfile(profileRes); setKarmaTotal(typeof karmaRes.total === 'number' ? karmaRes.total : null); // Extract links from profile data if (profileRes.linksMetadata && Array.isArray(profileRes.linksMetadata)) { const linksWithIds = profileRes.linksMetadata.map((link, index) => ({ ...link, id: link.id || `existing-${index}` })); setLinks(linksWithIds); } else if (Array.isArray(profileRes.links)) { const simpleLinks = profileRes.links.map(l => typeof l === 'string' ? l : l.link).filter(Boolean); const linksWithMetadata = simpleLinks.map((url, index) => ({ url, title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''), description: `Link to ${url}`, image: undefined, id: `existing-${index}` })); setLinks(linksWithMetadata); } else if (profileRes.website) { setLinks([{ url: profileRes.website, title: profileRes.website.replace(/^https?:\/\//, '').replace(/\/$/, ''), description: `Link to ${profileRes.website}`, image: undefined, id: 'existing-0' }]); } else { setLinks([]); } // Follower/following counts are managed by the `useFollow` hook. // Mock data for other stats (these would come from separate API endpoints) setPostsCount(Math.floor(Math.random() * 50)); setCommentsCount(Math.floor(Math.random() * 100)); }).catch(err => { console.error('Profile loading error:', err); // Provide user-friendly error messages based on the error type let errorMessage = 'Failed to load profile'; if (err.status === 404 || err.message?.includes('not found') || err.message?.includes('Resource not found')) { if (currentUser && currentUser.id === userId) { errorMessage = 'Unable to load your profile from the server. This may be due to a temporary service issue.'; } else { errorMessage = 'This user profile could not be found or may have been removed.'; } } else if (err.status === 403) { errorMessage = 'You do not have permission to view this profile.'; } else if (err.status === 500) { errorMessage = 'Server error occurred while loading the profile. Please try again later.'; } else if (err.message) { errorMessage = err.message; } setError(errorMessage); }).finally(() => setIsLoading(false)); }, [userId]); if (isLoading) { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.container, { backgroundColor: colors.background, justifyContent: 'center' }], children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "large", color: colors.primary }) }); } if (error) { return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: [styles.container, { backgroundColor: colors.background }], children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.errorHeader, children: [goBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { onPress: goBack, style: styles.backButton, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "arrow-back", size: 24, color: colors.text }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.errorTitle, { color: colors.text }], children: "Profile Error" })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.errorContent, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "alert-circle", size: 48, color: colors.error, style: styles.errorIcon }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.errorText, { color: colors.error }], children: error }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.errorSubtext, { color: colors.secondaryText }], children: "This could happen if the user doesn't exist or the profile service is unavailable." })] })] }); } return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [styles.container, { backgroundColor: colors.background }], children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.ScrollView, { contentContainerStyle: styles.scrollContainer, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.bannerContainer, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.bannerImage }) }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.avatarRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.avatarWrapper, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Avatar.default, { uri: profile?.avatar ? oxyServices.getFileDownloadUrl(profile.avatar, 'thumb') : undefined, name: profile?.username || username, size: 96, theme: theme }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.actionButtonWrapper, children: isOwnProfile ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, { style: styles.actionButton, onPress: () => navigate?.('EditProfile'), children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: styles.actionButtonText, children: t('editProfile.title') || 'Edit Profile' }) }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.FollowButton, { userId: userId, theme: theme, onFollowChange: isFollowing => { // The follow button will automatically update counts via Zustand console.log(`Follow status changed: ${isFollowing}`); } }) })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.header, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.displayName, { color: colors.text }], children: profile?.displayName || profile?.username || username || profile?.id }), profile?.username && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, { style: [styles.subText, { color: colors.secondaryText }], children: ["@", profile.username] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.bio, { color: colors.text }], children: profile?.bio || t('profile.noBio') || 'This user has no bio yet.' }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGrid, children: [profile?.createdAt && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGridItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "calendar-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], children: t('profile.joinedOn', { date: new Date(profile.createdAt).toLocaleDateString() }) || `Joined ${new Date(profile.createdAt).toLocaleDateString()}` })] }), profile?.location && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGridItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "location-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], numberOfLines: 1, children: profile.location })] }), profile?.website && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGridItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "globe-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], numberOfLines: 1, children: profile.website })] }), profile?.company && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGridItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "business-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], numberOfLines: 1, children: profile.company })] }), profile?.jobTitle && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGridItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "briefcase-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], numberOfLines: 1, children: profile.jobTitle })] }), profile?.education && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGridItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "school-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], numberOfLines: 1, children: profile.education })] }), profile?.birthday && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.infoGridItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "gift-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], children: t('profile.bornOn', { date: new Date(profile.birthday).toLocaleDateString() }) || `Born ${new Date(profile.birthday).toLocaleDateString()}` })] }), links.length > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, { style: styles.infoGridItem, onPress: () => navigate?.('UserLinks', { userId, links }), children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_vectorIcons.Ionicons, { name: "link-outline", size: 16, color: colors.secondaryText, style: { marginRight: 6 } }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.infoGridText, { color: colors.secondaryText }], numberOfLines: 1, children: links[0].url }), links.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.linksMore, { color: colors.secondaryText }], children: t('profile.more', { count: links.length - 1 }) || `+ ${links.length - 1} more` })] })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: styles.divider }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.statsRow, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.statItem, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.karmaAmount, { color: colors.primary }], children: karmaTotal !== null && karmaTotal !== undefined ? karmaTotal : '--' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.karmaLabel, { color: colors.secondaryText }], children: t('profile.karma') || 'Karma' })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.statItem, children: [isLoadingCounts ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: colors.text }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.karmaAmount, { color: colors.text }], children: followerCount !== null ? followerCount : '--' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.karmaLabel, { color: colors.secondaryText }], children: t('profile.followers') || 'Followers' })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, { style: styles.statItem, children: [isLoadingCounts ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, { size: "small", color: colors.text }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.karmaAmount, { color: colors.text }], children: followingCount !== null ? followingCount : '--' }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, { style: [styles.karmaLabel, { color: colors.secondaryText }], children: t('profile.following') || 'Following' })] })] })] })] }) }); }; const createStyles = colors => _reactNative.StyleSheet.create({ container: { flex: 1 }, scrollContainer: { alignItems: 'stretch', paddingBottom: 40 }, bannerContainer: { height: 160, backgroundColor: colors.primary + '20', position: 'relative', overflow: 'hidden' }, bannerImage: { flex: 1, backgroundColor: colors.primary }, // Placeholder, replace with Image if available avatarRow: { flexDirection: 'row', alignItems: 'flex-end', marginTop: -56, paddingHorizontal: 20, justifyContent: 'space-between', zIndex: 2 }, avatarWrapper: { borderWidth: 5, borderColor: colors.background, borderRadius: 64, overflow: 'hidden', backgroundColor: colors.background }, actionButtonWrapper: { flex: 1, alignItems: 'flex-end', justifyContent: 'flex-end' }, actionButton: { backgroundColor: colors.background, borderWidth: 1, borderColor: colors.primary, borderRadius: 24, paddingVertical: 7, paddingHorizontal: 22, marginBottom: 8, elevation: 2, shadowColor: colors.primary, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, shadowRadius: 2 }, actionButtonText: { color: colors.primary, fontWeight: 'bold', fontSize: 16 }, header: { alignItems: 'flex-start', width: '100%', paddingHorizontal: 20 }, displayName: { fontSize: 24, fontWeight: 'bold', marginTop: 10, marginBottom: 2, letterSpacing: 0.1 }, subText: { fontSize: 16, marginBottom: 2, color: colors.secondaryText }, bio: { fontSize: 16, marginTop: 10, marginBottom: 10, color: colors.text, lineHeight: 22 }, infoGrid: { flexDirection: 'row', alignItems: 'center', marginBottom: 10, flexWrap: 'wrap' }, infoGridItem: { flexDirection: 'row', alignItems: 'center', marginRight: 24, marginBottom: 4 }, infoGridText: { fontSize: 15, color: colors.text }, divider: { height: 1, backgroundColor: colors.border, width: '100%', marginVertical: 14 }, linksMore: { fontSize: 15, marginLeft: 4 }, statsRow: { width: '100%', flex: 1, flexDirection: 'row', alignItems: 'center', marginTop: 6, marginBottom: 2, justifyContent: 'space-between' }, statItem: { flex: 1, alignItems: 'center', minWidth: 50, marginBottom: 12 }, karmaLabel: { fontSize: 14, marginBottom: 2, textAlign: 'center', color: colors.secondaryText }, karmaAmount: { fontSize: 24, fontWeight: 'bold', textAlign: 'center', letterSpacing: 0.2 }, // Error handling styles errorHeader: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#E0E0E0' }, backButton: { padding: 8, marginRight: 16 }, errorTitle: { fontSize: 20, fontWeight: 'bold' }, errorContent: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 32 }, errorIcon: { marginBottom: 16 }, errorText: { fontSize: 18, fontWeight: '600', textAlign: 'center', marginBottom: 8 }, errorSubtext: { fontSize: 14, textAlign: 'center', opacity: 0.7 } }); var _default = exports.default = ProfileScreen; //# sourceMappingURL=ProfileScreen.js.map