@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
JavaScript
"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