@mereb/app-profile 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,3 +6,4 @@ export type ProfilePost = NonNullable<ProfileData>['posts']['edges'][number]['no
6
6
  export declare function parseProfileTimestamp(value?: string | null): Date | null;
7
7
  export declare function formatRelativeProfileTimestamp(value?: string | null, now?: number): string;
8
8
  export declare function selectProfilePosts(profile?: ProfileData | null): ProfilePost[];
9
+ export declare function normalizeUserSearchQuery(value?: string | null): string;
package/dist/headless.js CHANGED
@@ -43,3 +43,6 @@ export function selectProfilePosts(profile) {
43
43
  }
44
44
  return profile.posts.edges.map((edge) => edge.node);
45
45
  }
46
+ export function normalizeUserSearchQuery(value) {
47
+ return value?.trim().replace(/^@/, '') ?? '';
48
+ }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,25 @@ type AuthControls = {
6
6
  type ProfileScreenProps = {
7
7
  auth?: AuthControls;
8
8
  handle?: string;
9
+ onMessageUser?: (user: {
10
+ id: string;
11
+ handle: string;
12
+ displayName: string;
13
+ }) => void;
14
+ onSearchUsers?: () => void;
15
+ onSelectUser?: (handle: string) => void;
16
+ onOpenPrivacyPolicy?: () => void;
17
+ onOpenSupport?: () => void;
9
18
  };
10
- export declare function ProfileScreen({ auth, handle }: Readonly<ProfileScreenProps>): import("react/jsx-runtime.js").JSX.Element;
19
+ type PeopleScreenProps = {
20
+ onSelectUser?: (handle: string) => void;
21
+ onMessageUser?: (user: {
22
+ id: string;
23
+ handle: string;
24
+ displayName: string;
25
+ }) => void;
26
+ };
27
+ export declare function ProfileScreen({ auth, handle, onMessageUser, onSearchUsers, onSelectUser, onOpenPrivacyPolicy, onOpenSupport }: Readonly<ProfileScreenProps>): import("react/jsx-runtime").JSX.Element;
28
+ export declare function PeopleScreen({ onSelectUser, onMessageUser }: Readonly<PeopleScreenProps>): import("react/jsx-runtime").JSX.Element;
29
+ export declare function UserSearchScreen(props: Readonly<PeopleScreenProps>): import("react/jsx-runtime").JSX.Element;
11
30
  export {};
package/dist/index.js CHANGED
@@ -1,258 +1,394 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
3
- import { ActivityIndicator, Image, RefreshControl, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
4
- import { useFollowUserMutation, useMeProfileQuery, useProfileByHandleQuery, useUnfollowUserMutation, useUpdateProfileMutation } from '@mereb/shared-graphql';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react';
3
+ import { ActivityIndicator, Image, Pressable, RefreshControl, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
4
+ import * as ImagePicker from 'expo-image-picker';
5
+ import { MediaUploadKind, useCompleteMediaUploadMutation, useDiscoverUsersQuery, useFollowUserMutation, useMeProfileQuery, useProfileByHandleQuery, useRequestMediaUploadMutation, useSearchUsersQuery, useUnfollowUserMutation, useUpdateProfileMutation } from '@mereb/shared-graphql';
5
6
  import { tokens } from '@mereb/tokens/native';
6
- import { cardRecipes, placeholderAvatarRecipe } from '@mereb/ui-shared';
7
- import { formatRelativeProfileTimestamp, selectProfilePosts } from './headless.js';
8
- function extractGraphQLErrors(error) {
9
- if (error &&
10
- typeof error === 'object' &&
11
- 'graphQLErrors' in error &&
12
- Array.isArray(error.graphQLErrors)) {
13
- return error.graphQLErrors;
7
+ import { resolveMediaUploadErrorMessage } from '@mereb/ui-shared';
8
+ import { formatRelativeProfileTimestamp, normalizeUserSearchQuery, selectProfilePosts } from './headless';
9
+ const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
10
+ const ALLOWED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
11
+ const COMPLETE_UPLOAD_RETRY_DELAYS_MS = [300, 800, 1500];
12
+ const INLINE_SEARCH_RESULT_LIMIT = 6;
13
+ const DISCOVER_RESULT_LIMIT = 8;
14
+ const MIN_SEARCH_QUERY_LENGTH = 2;
15
+ function describeClientError(error, fallback) {
16
+ const message = error instanceof Error ? error.message.trim() : typeof error === 'string' ? error.trim() : '';
17
+ if (!message || message === 'Subgraph errors redacted') {
18
+ return fallback;
14
19
  }
15
- return undefined;
20
+ return __DEV__ ? `${fallback} (${message})` : fallback;
21
+ }
22
+ function sleep(ms) {
23
+ return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
24
+ }
25
+ async function completeUploadWithRetry(execute) {
26
+ let lastError;
27
+ for (let attempt = 0; attempt <= COMPLETE_UPLOAD_RETRY_DELAYS_MS.length; attempt += 1) {
28
+ try {
29
+ const completed = await execute();
30
+ if (completed) {
31
+ return completed;
32
+ }
33
+ lastError = new Error('MEDIA_UPLOAD_COMPLETE_FAILED');
34
+ }
35
+ catch (error) {
36
+ lastError = error;
37
+ }
38
+ if (attempt < COMPLETE_UPLOAD_RETRY_DELAYS_MS.length) {
39
+ await sleep(COMPLETE_UPLOAD_RETRY_DELAYS_MS[attempt]);
40
+ }
41
+ }
42
+ throw lastError ?? new Error('MEDIA_UPLOAD_COMPLETE_FAILED');
43
+ }
44
+ async function uploadAvatarImage(asset, requestUpload, completeUpload) {
45
+ const filename = asset.fileName?.trim() || `avatar-${Date.now()}.jpg`;
46
+ const mimeType = asset.mimeType?.trim() || 'image/jpeg';
47
+ if (!ALLOWED_IMAGE_TYPES.has(mimeType)) {
48
+ throw new Error('UNSUPPORTED_MEDIA_TYPE');
49
+ }
50
+ if (asset.fileSize && asset.fileSize > MAX_UPLOAD_BYTES) {
51
+ throw new Error('MEDIA_TOO_LARGE');
52
+ }
53
+ const requestResult = await requestUpload({
54
+ variables: {
55
+ filename,
56
+ contentType: mimeType,
57
+ kind: MediaUploadKind.Avatar
58
+ }
59
+ });
60
+ const uploadRequest = requestResult.data?.requestMediaUpload;
61
+ if (!uploadRequest?.assetId || !uploadRequest.putUrl) {
62
+ throw new Error('MEDIA_UPLOAD_REQUEST_FAILED');
63
+ }
64
+ const localResponse = await fetch(asset.uri);
65
+ const blob = await localResponse.blob();
66
+ const putResponse = await fetch(uploadRequest.putUrl, {
67
+ method: 'PUT',
68
+ headers: {
69
+ 'Content-Type': mimeType
70
+ },
71
+ body: blob
72
+ });
73
+ if (!putResponse.ok) {
74
+ throw new Error(`MEDIA_UPLOAD_TRANSFER_FAILED_${putResponse.status}`);
75
+ }
76
+ const completed = await completeUploadWithRetry(async () => {
77
+ const result = await completeUpload({
78
+ variables: {
79
+ assetId: uploadRequest.assetId
80
+ }
81
+ });
82
+ return result.data?.completeMediaUpload ?? null;
83
+ });
84
+ return {
85
+ id: completed.id,
86
+ url: completed.url
87
+ };
88
+ }
89
+ function EmptyState({ title, body }) {
90
+ return (_jsx(View, { style: styles.centeredContainer, children: _jsxs(View, { style: styles.placeholderCard, children: [_jsx(Text, { style: styles.placeholderTitle, children: title }), _jsx(Text, { style: styles.placeholderBody, children: body })] }) }));
16
91
  }
17
92
  function PlaceholderAvatar() {
18
93
  return (_jsxs(View, { style: styles.placeholderAvatar, accessible: false, children: [_jsx(View, { style: styles.placeholderHead }), _jsx(View, { style: styles.placeholderShoulders })] }));
19
94
  }
20
- export function ProfileScreen({ auth, handle }) {
95
+ function UserAvatar({ displayName, handle, avatarUrl }) {
96
+ const fallback = (displayName || handle).trim().charAt(0).toUpperCase() || '?';
97
+ return (_jsx(View, { style: styles.searchAvatarShell, children: avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.searchAvatarImage, resizeMode: "cover" })) : (_jsx(Text, { style: styles.searchAvatarFallback, children: fallback })) }));
98
+ }
99
+ function UserRow({ user, onSelect, actionLabel, onAction }) {
100
+ return (_jsxs(View, { style: styles.userRow, children: [_jsx(UserAvatar, { displayName: user.displayName, handle: user.handle, avatarUrl: user.avatarUrl }), _jsxs(Pressable, { accessibilityRole: "button", onPress: onSelect, style: styles.flexSpacer, children: [_jsx(Text, { style: styles.cardTitle, children: user.displayName }), _jsxs(Text, { style: styles.mutedText, children: ["@", user.handle] })] }), actionLabel && onAction ? (_jsx(Pressable, { accessibilityRole: "button", onPress: onAction, style: styles.secondaryButton, children: _jsx(Text, { style: styles.secondaryButtonText, children: actionLabel }) })) : null] }));
101
+ }
102
+ function SearchResultsSection({ query, canSearch, loading, error, results, onSelectUser, onMessageUser }) {
103
+ if (!query.length) {
104
+ return null;
105
+ }
106
+ return (_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Search results" }), !canSearch ? (_jsx(Text, { style: styles.mutedText, children: "Type at least two characters to search." })) : error ? (_jsx(Text, { style: styles.errorText, children: describeClientError(error, 'We could not search for users right now.') })) : loading ? (_jsx(View, { style: styles.inlineState, children: _jsx(ActivityIndicator, {}) })) : results.length === 0 ? (_jsx(Text, { style: styles.mutedText, children: "No matching users found." })) : (results.map((user) => (_jsx(UserRow, { user: user, onSelect: () => onSelectUser?.(user.handle), actionLabel: onMessageUser ? 'Message' : undefined, onAction: onMessageUser
107
+ ? () => onMessageUser({
108
+ id: user.id,
109
+ handle: user.handle,
110
+ displayName: user.displayName
111
+ })
112
+ : undefined }, user.id))))] }));
113
+ }
114
+ function DiscoverPeopleSection({ users, loading, error, loadingUserId, onSelectUser, onToggleFollow }) {
115
+ return (_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Discover people" }), _jsx(Text, { style: styles.sectionBody, children: "Follow active members to improve the relevance of your feed and uncover more conversations." }), error ? (_jsx(Text, { style: styles.errorText, children: describeClientError(error, 'We could not load suggested people right now.') })) : loading && users.length === 0 ? (_jsx(View, { style: styles.inlineState, children: _jsx(ActivityIndicator, {}) })) : users.length === 0 ? (_jsx(Text, { style: styles.mutedText, children: "No suggestions available yet." })) : (users.map((user) => (_jsx(UserRow, { user: user, onSelect: () => onSelectUser?.(user.handle), actionLabel: loadingUserId === user.id ? 'Saving…' : user.followedByMe ? 'Following' : 'Follow', onAction: () => void onToggleFollow(user) }, user.id))))] }));
116
+ }
117
+ export function ProfileScreen({ auth, handle, onMessageUser, onSearchUsers, onSelectUser, onOpenPrivacyPolicy, onOpenSupport }) {
21
118
  const token = auth?.token;
22
119
  const viewingOwnProfile = !handle;
23
120
  const trimmedHandle = handle?.replace(/^@/, '') ?? '';
24
- const { data: meData, loading: loadingMe, error: meError, refetch: refetchMe } = useMeProfileQuery({
25
- variables: { postsLimit: 10 },
121
+ const meQuery = useMeProfileQuery({
122
+ variables: { postsLimit: 10, followersLimit: 8, followingLimit: 8 },
26
123
  skip: !viewingOwnProfile || !token,
27
- notifyOnNetworkStatusChange: true
124
+ notifyOnNetworkStatusChange: true,
125
+ fetchPolicy: 'cache-and-network',
126
+ nextFetchPolicy: 'cache-first'
28
127
  });
29
- const { data: handleData, loading: loadingHandle, error: handleError, refetch: refetchHandle } = useProfileByHandleQuery({
30
- variables: { handle: trimmedHandle, postsLimit: 10 },
128
+ const publicQuery = useProfileByHandleQuery({
129
+ variables: { handle: trimmedHandle, postsLimit: 10, followersLimit: 8, followingLimit: 8 },
31
130
  skip: viewingOwnProfile || trimmedHandle.length === 0,
32
131
  notifyOnNetworkStatusChange: true
33
132
  });
34
- const activeError = viewingOwnProfile ? meError : handleError;
35
- const activeLoading = viewingOwnProfile ? loadingMe : loadingHandle;
36
- const activeRefetch = viewingOwnProfile ? refetchMe : refetchHandle;
37
- const profile = viewingOwnProfile ? meData?.me : handleData?.userByHandle;
38
- const followButtonLabel = profile?.followedByMe ? 'Following' : 'Follow';
39
- const activityIndicatorColor = profile?.followedByMe ? '#0f172a' : '#fff';
40
- const unauthenticatedError = viewingOwnProfile
41
- ? Boolean(extractGraphQLErrors(meError)?.some((graphError) => graphError.message?.toUpperCase().includes('UNAUTHENTICATED')))
42
- : false;
43
- const shouldPromptSignIn = viewingOwnProfile && (!token || unauthenticatedError);
44
- const [updateProfileMutation, { loading: saving }] = useUpdateProfileMutation();
45
- const [followUserMutation, { loading: followLoading }] = useFollowUserMutation();
46
- const [unfollowUserMutation, { loading: unfollowLoading }] = useUnfollowUserMutation();
133
+ const profile = viewingOwnProfile ? meQuery.data?.me : publicQuery.data?.userByHandle;
134
+ const loading = viewingOwnProfile ? meQuery.loading : publicQuery.loading;
135
+ const error = viewingOwnProfile ? meQuery.error : publicQuery.error;
136
+ const refetch = viewingOwnProfile ? meQuery.refetch : publicQuery.refetch;
137
+ const [updateProfile] = useUpdateProfileMutation();
138
+ const [followUser, followState] = useFollowUserMutation();
139
+ const [unfollowUser, unfollowState] = useUnfollowUserMutation();
140
+ const [requestUpload] = useRequestMediaUploadMutation();
141
+ const [completeUpload] = useCompleteMediaUploadMutation();
47
142
  const [displayName, setDisplayName] = useState('');
48
143
  const [bio, setBio] = useState('');
49
144
  const [formError, setFormError] = useState();
50
- const [successMessage, setSuccessMessage] = useState();
51
- const [followError, setFollowError] = useState();
52
- const followMutationInFlight = followLoading || unfollowLoading;
53
- const isAuthenticated = Boolean(token);
145
+ const [formSuccess, setFormSuccess] = useState();
146
+ const [avatarError, setAvatarError] = useState();
147
+ const [uploadingAvatar, setUploadingAvatar] = useState(false);
148
+ const [actionError, setActionError] = useState();
54
149
  useEffect(() => {
55
150
  if (viewingOwnProfile && profile) {
56
151
  setDisplayName(profile.displayName);
57
152
  setBio(profile.bio ?? '');
58
153
  }
59
- }, [viewingOwnProfile, profile?.id, profile?.displayName, profile?.bio]);
60
- useEffect(() => {
61
- if (!successMessage) {
62
- return;
63
- }
64
- const timeout = setTimeout(() => setSuccessMessage(undefined), 2500);
65
- return () => clearTimeout(timeout);
66
- }, [successMessage]);
67
- useEffect(() => {
68
- setFollowError(undefined);
69
- }, [profile?.id]);
154
+ }, [profile, viewingOwnProfile]);
70
155
  const posts = useMemo(() => selectProfilePosts(profile), [profile]);
71
- const handleRefresh = useCallback(() => {
72
- if (shouldPromptSignIn)
156
+ const followers = profile?.followers?.edges.map((edge) => edge.node) ?? [];
157
+ const following = profile?.following?.edges.map((edge) => edge.node) ?? [];
158
+ const followLoading = followState.loading || unfollowState.loading;
159
+ const refresh = useCallback(async () => {
160
+ if (viewingOwnProfile && !token) {
73
161
  return;
74
- activeRefetch?.();
75
- }, [shouldPromptSignIn, activeRefetch]);
162
+ }
163
+ try {
164
+ await refetch?.();
165
+ setActionError(undefined);
166
+ }
167
+ catch (error) {
168
+ setActionError(describeClientError(error, 'We could not refresh this profile right now.'));
169
+ }
170
+ }, [refetch, token, viewingOwnProfile]);
76
171
  const handleSave = useCallback(async () => {
77
- if (!viewingOwnProfile)
172
+ if (!viewingOwnProfile) {
78
173
  return;
174
+ }
79
175
  const trimmedName = displayName.trim();
80
- const trimmedBio = bio.trim();
81
176
  if (!trimmedName) {
82
177
  setFormError('Display name is required.');
83
178
  return;
84
179
  }
85
180
  setFormError(undefined);
86
- setSuccessMessage(undefined);
181
+ setFormSuccess(undefined);
87
182
  try {
88
- const result = await updateProfileMutation({
183
+ await updateProfile({
89
184
  variables: {
90
185
  displayName: trimmedName,
91
- bio: trimmedBio.length ? trimmedBio : null
186
+ bio: bio.trim() || null
92
187
  }
93
188
  });
94
- if (result.data?.updateProfile) {
95
- setSuccessMessage('Profile updated');
96
- await refetchMe();
97
- }
189
+ setFormSuccess('Profile updated.');
190
+ setActionError(undefined);
191
+ await meQuery.refetch();
98
192
  }
99
- catch (mutationError) {
100
- console.warn('Failed to update profile', mutationError);
101
- setFormError('We could not update your profile. Please try again.');
193
+ catch (nextError) {
194
+ console.warn('Failed to update profile', nextError);
195
+ setFormError(describeClientError(nextError, 'We could not update your profile. Please try again.'));
102
196
  }
103
- }, [viewingOwnProfile, displayName, bio, updateProfileMutation, refetchMe]);
104
- const handleLogin = useCallback(() => {
105
- auth?.login?.();
106
- }, [auth]);
107
- const handleLogout = useCallback(() => {
108
- auth?.logout?.();
109
- }, [auth]);
197
+ }, [bio, displayName, meQuery, updateProfile, viewingOwnProfile]);
110
198
  const handleToggleFollow = useCallback(async () => {
111
- if (viewingOwnProfile || !profile?.id || followMutationInFlight)
199
+ if (!profile?.id || viewingOwnProfile || followLoading) {
112
200
  return;
113
- if (!isAuthenticated) {
114
- auth?.login?.();
201
+ }
202
+ if (!token) {
203
+ await auth?.login?.();
115
204
  return;
116
205
  }
117
- setFollowError(undefined);
118
206
  try {
119
207
  if (profile.followedByMe) {
120
- await unfollowUserMutation({ variables: { userId: profile.id } });
208
+ await unfollowUser({ variables: { userId: profile.id } });
121
209
  }
122
210
  else {
123
- await followUserMutation({ variables: { userId: profile.id } });
211
+ await followUser({ variables: { userId: profile.id } });
124
212
  }
125
- await activeRefetch?.();
213
+ setActionError(undefined);
214
+ await refetch?.();
126
215
  }
127
- catch (mutationError) {
128
- console.warn('Failed to update follow status', mutationError);
129
- setFollowError('We could not update the follow status. Please try again.');
216
+ catch (error) {
217
+ setActionError(describeClientError(error, 'We could not update this follow relationship.'));
130
218
  }
131
- }, [
132
- viewingOwnProfile,
133
- profile?.id,
134
- profile?.followedByMe,
135
- followMutationInFlight,
136
- isAuthenticated,
137
- auth,
138
- unfollowUserMutation,
139
- followUserMutation,
140
- activeRefetch
141
- ]);
142
- // --- small renderer helpers to lower complexity ---
143
- const renderNoHandle = () => (_jsx(View, { style: styles.centeredContainer, children: _jsxs(View, { style: styles.placeholderCard, children: [_jsx(Text, { style: styles.placeholderTitle, children: "No handle provided" }), _jsx(Text, { style: styles.placeholderBody, children: "We could not determine which profile to show. Return to the feed and try again." })] }) }));
144
- const renderSignInPrompt = () => (_jsx(View, { style: styles.centeredContainer, children: _jsxs(View, { style: styles.placeholderCard, children: [_jsx(Text, { style: styles.placeholderTitle, children: "Sign in to continue" }), _jsx(Text, { style: styles.placeholderBody, children: "Log in to view and edit your profile, see your recent activity, and connect with teammates." }), _jsx(TouchableOpacity, { style: styles.primaryButton, onPress: handleLogin, accessibilityRole: "button", children: _jsx(Text, { style: styles.primaryButtonText, children: "Sign in" }) }), token ? (_jsx(TouchableOpacity, { style: styles.secondaryButton, onPress: handleLogout, accessibilityRole: "button", children: _jsx(Text, { style: styles.secondaryButtonText, children: "Log out" }) })) : null] }) }));
145
- const renderFollowAction = () => (_jsx(TouchableOpacity, { style: [
146
- styles.followActionButton,
147
- profile?.followedByMe ? styles.followingButton : styles.followButton,
148
- followMutationInFlight ? styles.buttonDisabled : null
149
- ], onPress: handleToggleFollow, disabled: followMutationInFlight, accessibilityRole: "button", children: followMutationInFlight ? (_jsx(ActivityIndicator, { color: activityIndicatorColor })) : (_jsx(Text, { style: profile?.followedByMe
150
- ? styles.followingButtonText
151
- : styles.followButtonText, children: followButtonLabel })) }));
152
- const renderProfileHeader = () => (_jsxs(View, { style: styles.card, children: [_jsx(View, { style: styles.avatarCircle, children: profile?.avatarUrl ? (_jsx(Image, { source: { uri: profile.avatarUrl }, style: styles.avatarImage, resizeMode: "cover" })) : (_jsx(PlaceholderAvatar, {})) }), _jsxs(View, { style: styles.headerText, children: [_jsx(Text, { style: styles.displayName, children: profile?.displayName ?? 'Set your display name' }), _jsx(Text, { style: styles.handleText, children: profile ? `@${profile.handle}` : 'No handle yet' }), _jsxs(View, { style: styles.metricsRow, children: [_jsxs(View, { style: styles.metricItem, children: [_jsx(Text, { style: styles.metricValue, children: profile?.followersCount ?? 0 }), _jsx(Text, { style: styles.metricLabel, children: "Followers" })] }), _jsxs(View, { style: styles.metricItem, children: [_jsx(Text, { style: styles.metricValue, children: profile?.followingCount ?? 0 }), _jsx(Text, { style: styles.metricLabel, children: "Following" })] })] })] }), viewingOwnProfile ? (_jsx(TouchableOpacity, { style: styles.logoutChip, onPress: handleLogout, accessibilityRole: "button", children: _jsx(Text, { style: styles.logoutChipText, children: "Log out" }) })) : null, viewingOwnProfile ? null : renderFollowAction()] }));
153
- const renderEditableProfile = () => (_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Profile details" }), _jsxs(View, { style: styles.formGroup, children: [_jsx(Text, { style: styles.label, children: "Display name" }), _jsx(TextInput, { value: displayName, onChangeText: setDisplayName, placeholder: "How should teammates see you?", style: styles.textInput, editable: !saving, autoCapitalize: "words" })] }), _jsxs(View, { style: styles.formGroup, children: [_jsx(Text, { style: styles.label, children: "Bio" }), _jsx(TextInput, { value: bio, onChangeText: setBio, placeholder: "Share a short introduction", style: [styles.textInput, styles.bioInput], multiline: true, numberOfLines: 4, textAlignVertical: "top", editable: !saving })] }), formError ? _jsx(Text, { style: styles.errorText, children: formError }) : null, successMessage ? _jsx(Text, { style: styles.successText, children: successMessage }) : null, _jsx(TouchableOpacity, { style: [styles.primaryButton, saving ? styles.buttonDisabled : null], onPress: handleSave, disabled: saving, accessibilityRole: "button", children: saving ? _jsx(ActivityIndicator, { color: "#fff" }) : _jsx(Text, { style: styles.primaryButtonText, children: "Save changes" }) })] }));
154
- const aboutSection = profile?.bio ? (_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "About" }), _jsx(Text, { style: styles.readOnlyBio, children: profile.bio })] })) : null;
155
- const renderRecentPosts = () => (_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Recent posts" }), activeLoading && !profile ? _jsx(ActivityIndicator, { style: styles.postsLoading }) : null, !activeLoading && posts.length === 0 ? (_jsx(Text, { style: styles.placeholderBody, children: viewingOwnProfile
156
- ? 'You have not shared anything yet. Post from the feed to see it listed here.'
157
- : 'No recent posts to show yet. Check back later!' })) : null, posts.map((post) => (_jsxs(View, { style: styles.postCard, children: [_jsx(Text, { style: styles.postTimestamp, children: formatRelativeProfileTimestamp(post.createdAt) }), _jsx(Text, { style: styles.postBody, children: post.body })] }, post.id)))] }));
158
- const renderActiveError = () => activeError && !unauthenticatedError ? (_jsxs(View, { style: styles.errorCard, children: [_jsx(Text, { style: styles.errorTitle, children: "We hit a snag" }), _jsx(Text, { style: styles.errorBody, children: "Something went wrong while loading your profile. Pull to refresh or try again." })] })) : null;
159
- const renderProfileNotFound = () => !activeLoading && !profile && !activeError ? (_jsxs(View, { style: styles.errorCard, children: [_jsx(Text, { style: styles.errorTitle, children: "Profile not found" }), _jsx(Text, { style: styles.errorBody, children: "This handle does not exist or is no longer available." })] })) : null;
160
- // --- top-level early returns to simplify main render ---
161
- if (!viewingOwnProfile && trimmedHandle.length === 0)
162
- return renderNoHandle();
163
- if (shouldPromptSignIn)
164
- return renderSignInPrompt();
165
- return (_jsxs(ScrollView, { style: styles.scroll, contentContainerStyle: styles.scrollContent, keyboardShouldPersistTaps: "handled", refreshControl: _jsx(RefreshControl, { refreshing: activeLoading, onRefresh: handleRefresh }), children: [renderProfileHeader(), !viewingOwnProfile && followError ? (_jsx(Text, { style: styles.errorText, children: followError })) : null, viewingOwnProfile ? renderEditableProfile() : aboutSection, renderRecentPosts(), renderActiveError(), renderProfileNotFound()] }));
219
+ }, [auth, followLoading, followUser, profile, refetch, token, unfollowUser, viewingOwnProfile]);
220
+ const handleUploadAvatar = useCallback(async () => {
221
+ const result = await ImagePicker.launchImageLibraryAsync({
222
+ allowsEditing: true,
223
+ quality: 0.85,
224
+ mediaTypes: ['images']
225
+ });
226
+ if (result.canceled || !result.assets[0]) {
227
+ return;
228
+ }
229
+ setUploadingAvatar(true);
230
+ setAvatarError(undefined);
231
+ try {
232
+ const avatar = await uploadAvatarImage(result.assets[0], requestUpload, completeUpload);
233
+ await updateProfile({
234
+ variables: {
235
+ displayName: displayName.trim() || profile?.displayName || '',
236
+ bio: bio.trim() || null,
237
+ avatarAssetId: avatar.id
238
+ }
239
+ });
240
+ setActionError(undefined);
241
+ await meQuery.refetch();
242
+ setFormSuccess('Avatar updated.');
243
+ }
244
+ catch (nextError) {
245
+ console.warn('Failed to upload avatar', nextError);
246
+ setAvatarError(resolveMediaUploadErrorMessage(nextError, describeClientError(nextError, 'We could not upload your avatar. Please try again.')));
247
+ }
248
+ finally {
249
+ setUploadingAvatar(false);
250
+ }
251
+ }, [bio, completeUpload, displayName, meQuery, profile?.displayName, requestUpload, updateProfile]);
252
+ if (viewingOwnProfile && !token) {
253
+ return (_jsx(EmptyState, { title: "Sign in to continue", body: "Log in to view and edit your profile, manage your avatar, and see your connections." }));
254
+ }
255
+ if (!viewingOwnProfile && !trimmedHandle) {
256
+ return _jsx(EmptyState, { title: "No profile selected", body: "Return to the feed and choose a user profile to inspect." });
257
+ }
258
+ if (error) {
259
+ return (_jsx(EmptyState, { title: "Profile unavailable", body: describeClientError(error, 'The requested profile could not be loaded.') }));
260
+ }
261
+ if (!profile && loading) {
262
+ return (_jsx(View, { style: styles.centeredContainer, children: _jsx(ActivityIndicator, {}) }));
263
+ }
264
+ if (!profile) {
265
+ return (_jsx(EmptyState, { title: viewingOwnProfile ? 'Profile unavailable' : 'User not found', body: viewingOwnProfile
266
+ ? 'Your session is active, but the API did not return your profile. Refresh the screen or sign out and log in again.'
267
+ : 'The requested profile could not be found.' }));
268
+ }
269
+ return (_jsxs(ScrollView, { contentContainerStyle: styles.screen, refreshControl: _jsx(RefreshControl, { refreshing: loading, onRefresh: () => void refresh() }), children: [_jsxs(View, { style: styles.card, children: [_jsxs(View, { style: styles.headerRow, children: [_jsx(View, { style: styles.avatarCircle, children: profile?.avatarUrl ? (_jsx(Image, { source: { uri: profile.avatarUrl }, style: styles.avatarImage, resizeMode: "cover" })) : (_jsx(PlaceholderAvatar, {})) }), _jsxs(View, { style: styles.flexSpacer, children: [_jsx(Text, { style: styles.displayName, children: profile?.displayName ?? 'Unknown user' }), _jsxs(Text, { style: styles.handleText, children: ["@", profile?.handle ?? 'unknown'] }), _jsxs(Text, { style: styles.metaCopy, children: ["Joined ", formatRelativeProfileTimestamp(profile?.createdAt)] })] })] }), _jsxs(View, { style: styles.metricsRow, children: [_jsxs(View, { style: styles.metricItem, children: [_jsx(Text, { style: styles.metricValue, children: profile?.followersCount ?? 0 }), _jsx(Text, { style: styles.metricLabel, children: "Followers" })] }), _jsxs(View, { style: styles.metricItem, children: [_jsx(Text, { style: styles.metricValue, children: profile?.followingCount ?? 0 }), _jsx(Text, { style: styles.metricLabel, children: "Following" })] })] }), profile?.bio ? _jsx(Text, { style: styles.bioText, children: profile.bio }) : null, _jsx(View, { style: styles.actionsRow, children: viewingOwnProfile ? (_jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityRole: "button", onPress: () => void handleUploadAvatar(), style: styles.secondaryButton, children: _jsx(Text, { style: styles.secondaryButtonText, children: uploadingAvatar ? 'Uploading…' : 'Update avatar' }) }), onSearchUsers ? (_jsx(Pressable, { accessibilityRole: "button", onPress: onSearchUsers, style: styles.secondaryButton, children: _jsx(Text, { style: styles.secondaryButtonText, children: "Find people" }) })) : null, _jsx(Pressable, { accessibilityRole: "button", onPress: () => void auth?.logout?.(), style: styles.secondaryButton, children: _jsx(Text, { style: styles.secondaryButtonText, children: "Log out" }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Pressable, { accessibilityRole: "button", disabled: followLoading, onPress: () => void handleToggleFollow(), style: styles.primaryButton, children: _jsx(Text, { style: styles.primaryButtonText, children: followLoading ? 'Saving…' : profile?.followedByMe ? 'Following' : 'Follow' }) }), onMessageUser && profile ? (_jsx(Pressable, { accessibilityRole: "button", onPress: () => onMessageUser({
270
+ id: profile.id,
271
+ handle: profile.handle,
272
+ displayName: profile.displayName
273
+ }), style: styles.secondaryButton, children: _jsx(Text, { style: styles.secondaryButtonText, children: "Message" }) })) : null] })) }), avatarError ? _jsx(Text, { style: styles.errorText, children: avatarError }) : null, actionError ? _jsx(Text, { style: styles.errorText, children: actionError }) : null] }), viewingOwnProfile ? (_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Account settings" }), _jsxs(View, { style: styles.formGroup, children: [_jsx(Text, { style: styles.label, children: "Display name" }), _jsx(TextInput, { value: displayName, onChangeText: setDisplayName, style: styles.input, autoCapitalize: "words" })] }), _jsxs(View, { style: styles.formGroup, children: [_jsx(Text, { style: styles.label, children: "Bio" }), _jsx(TextInput, { value: bio, onChangeText: setBio, style: [styles.input, styles.multilineInput], multiline: true })] }), formError ? _jsx(Text, { style: styles.errorText, children: formError }) : null, formSuccess ? _jsx(Text, { style: styles.successText, children: formSuccess }) : null, _jsxs(View, { style: styles.actionsRow, children: [_jsx(View, { style: styles.flexSpacer }), _jsx(Pressable, { accessibilityRole: "button", onPress: () => void handleSave(), style: styles.primaryButton, children: _jsx(Text, { style: styles.primaryButtonText, children: "Save changes" }) })] })] })) : null, viewingOwnProfile && (onOpenSupport || onOpenPrivacyPolicy) ? (_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Help and privacy" }), _jsx(Text, { style: styles.sectionBody, children: "Review support guidance and the latest privacy policy before sharing this beta build more broadly." }), _jsxs(View, { style: styles.actionsRow, children: [onOpenSupport ? (_jsx(Pressable, { accessibilityRole: "button", onPress: onOpenSupport, style: styles.secondaryButton, children: _jsx(Text, { style: styles.secondaryButtonText, children: "Support" }) })) : null, onOpenPrivacyPolicy ? (_jsx(Pressable, { accessibilityRole: "button", onPress: onOpenPrivacyPolicy, style: styles.secondaryButton, children: _jsx(Text, { style: styles.secondaryButtonText, children: "Privacy policy" }) })) : null] })] })) : null, _jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Followers" }), followers.length === 0 ? (_jsx(Text, { style: styles.mutedText, children: "No followers to show yet." })) : (followers.map((user) => (_jsx(UserRow, { user: user, onSelect: () => onSelectUser?.(user.handle) }, user.id))))] }), _jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Following" }), following.length === 0 ? (_jsx(Text, { style: styles.mutedText, children: "Not following anyone yet." })) : (following.map((user) => (_jsx(UserRow, { user: user, onSelect: () => onSelectUser?.(user.handle) }, user.id))))] }), _jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "Recent posts" }), posts.length === 0 ? (_jsx(Text, { style: styles.mutedText, children: "No recent posts yet." })) : (posts.map((post) => (_jsxs(View, { style: styles.postRow, children: [_jsx(Text, { style: styles.postBody, children: post.body }), _jsx(Text, { style: styles.mutedText, children: formatRelativeProfileTimestamp(post.createdAt) })] }, post.id))))] })] }));
274
+ }
275
+ export function PeopleScreen({ onSelectUser, onMessageUser }) {
276
+ const [query, setQuery] = useState('');
277
+ const deferredQuery = useDeferredValue(query);
278
+ const normalizedQuery = normalizeUserSearchQuery(deferredQuery);
279
+ const canSearch = normalizedQuery.length >= MIN_SEARCH_QUERY_LENGTH;
280
+ const discoverQuery = useDiscoverUsersQuery({
281
+ variables: {
282
+ limit: DISCOVER_RESULT_LIMIT
283
+ },
284
+ notifyOnNetworkStatusChange: true
285
+ });
286
+ const searchQuery = useSearchUsersQuery({
287
+ variables: {
288
+ query: normalizedQuery,
289
+ limit: INLINE_SEARCH_RESULT_LIMIT
290
+ },
291
+ skip: !canSearch,
292
+ notifyOnNetworkStatusChange: true
293
+ });
294
+ const [followUser] = useFollowUserMutation();
295
+ const [unfollowUser] = useUnfollowUserMutation();
296
+ const [discoverActionUserId, setDiscoverActionUserId] = useState();
297
+ const [actionError, setActionError] = useState();
298
+ const results = searchQuery.data?.searchUsers ?? [];
299
+ const discoverUsers = discoverQuery.data?.discoverUsers ?? [];
300
+ const refreshing = discoverQuery.loading || searchQuery.loading;
301
+ const refresh = useCallback(async () => {
302
+ try {
303
+ const tasks = [discoverQuery.refetch()];
304
+ if (canSearch) {
305
+ tasks.push(searchQuery.refetch());
306
+ }
307
+ await Promise.all(tasks);
308
+ setActionError(undefined);
309
+ }
310
+ catch (error) {
311
+ setActionError(describeClientError(error, 'We could not refresh people right now.'));
312
+ }
313
+ }, [canSearch, discoverQuery, searchQuery]);
314
+ const handleToggleFollow = useCallback(async (user) => {
315
+ setDiscoverActionUserId(user.id);
316
+ try {
317
+ if (user.followedByMe) {
318
+ await unfollowUser({ variables: { userId: user.id } });
319
+ }
320
+ else {
321
+ await followUser({ variables: { userId: user.id } });
322
+ }
323
+ setActionError(undefined);
324
+ await discoverQuery.refetch();
325
+ }
326
+ catch (error) {
327
+ setActionError(describeClientError(error, 'We could not update this follow relationship.'));
328
+ }
329
+ finally {
330
+ setDiscoverActionUserId(undefined);
331
+ }
332
+ }, [discoverQuery, followUser, unfollowUser]);
333
+ return (_jsxs(ScrollView, { contentContainerStyle: styles.screen, refreshControl: _jsx(RefreshControl, { refreshing: refreshing, onRefresh: () => void refresh() }), children: [_jsxs(View, { style: styles.card, children: [_jsx(Text, { style: styles.sectionTitle, children: "People" }), _jsx(Text, { style: styles.sectionBody, children: "Search users by name or handle, or browse suggested people to follow." }), _jsx(TextInput, { value: query, onChangeText: setQuery, placeholder: "Search users by name or handle", style: styles.input })] }), actionError ? _jsx(Text, { style: styles.errorText, children: actionError }) : null, _jsx(SearchResultsSection, { query: normalizedQuery, canSearch: canSearch, loading: searchQuery.loading, error: searchQuery.error, results: results, onSelectUser: onSelectUser, onMessageUser: onMessageUser }), normalizedQuery.length === 0 ? (_jsx(DiscoverPeopleSection, { users: discoverUsers, loading: discoverQuery.loading, error: discoverQuery.error, loadingUserId: discoverActionUserId, onSelectUser: onSelectUser, onToggleFollow: handleToggleFollow })) : null] }));
166
334
  }
167
- const { color, spacing, radius, shadow } = tokens;
168
- const profileCardRecipe = cardRecipes.surface;
169
- const postCardRecipe = cardRecipes.subdued;
170
- const placeholderPalette = placeholderAvatarRecipe;
335
+ export function UserSearchScreen(props) {
336
+ return _jsx(PeopleScreen, { ...props });
337
+ }
338
+ const { color, radius, shadow, spacing } = tokens;
171
339
  const styles = StyleSheet.create({
172
- scroll: {
173
- flex: 1,
174
- backgroundColor: color.surfaceMuted
175
- },
176
- scrollContent: {
340
+ screen: {
341
+ flexGrow: 1,
177
342
  padding: spacing.lg,
178
- gap: spacing.lg
343
+ gap: spacing.md,
344
+ backgroundColor: color.surfaceAlt
179
345
  },
180
346
  centeredContainer: {
181
347
  flex: 1,
182
348
  alignItems: 'center',
183
349
  justifyContent: 'center',
184
350
  padding: spacing.xxl,
185
- backgroundColor: color.surfaceMuted
351
+ backgroundColor: color.surfaceAlt
186
352
  },
187
353
  placeholderCard: {
188
354
  width: '100%',
189
- maxWidth: 360,
190
- backgroundColor: color[profileCardRecipe.backgroundColor],
191
- borderRadius: radius[profileCardRecipe.radius],
192
- padding: spacing.xxl,
193
- alignItems: 'center',
194
- gap: spacing.lg,
195
- ...shadow[profileCardRecipe.elevation]
355
+ borderRadius: radius.xl,
356
+ padding: spacing.xl,
357
+ backgroundColor: color.surface,
358
+ borderWidth: 1,
359
+ borderColor: color.border,
360
+ gap: spacing.sm,
361
+ ...shadow.sm
196
362
  },
197
363
  placeholderTitle: {
198
364
  fontSize: 20,
199
365
  fontWeight: '700',
200
- color: color.text,
201
- textAlign: 'center'
366
+ color: color.text
202
367
  },
203
368
  placeholderBody: {
204
- fontSize: 14,
205
369
  color: color.textMuted,
206
- textAlign: 'center',
207
370
  lineHeight: 20
208
371
  },
209
- primaryButton: {
210
- width: '100%',
211
- backgroundColor: color.primary,
212
- borderRadius: radius.pill,
213
- paddingVertical: spacing.mdPlus,
214
- paddingHorizontal: spacing.xxl,
215
- alignItems: 'center',
216
- justifyContent: 'center',
217
- ...shadow.md,
218
- shadowColor: color.primary
219
- },
220
- primaryButtonText: {
221
- fontSize: 16,
222
- fontWeight: '600',
223
- color: color.surface
224
- },
225
- secondaryButton: {
226
- width: '100%',
227
- borderRadius: radius.pill,
228
- paddingVertical: spacing.md,
229
- alignItems: 'center',
230
- justifyContent: 'center',
372
+ card: {
373
+ borderRadius: radius.xl,
374
+ padding: spacing.lg,
375
+ backgroundColor: color.surface,
231
376
  borderWidth: 1,
232
- borderColor: color.borderStrong
233
- },
234
- secondaryButtonText: {
235
- fontSize: 15,
236
- fontWeight: '600',
237
- color: color.text
238
- },
239
- buttonDisabled: {
240
- opacity: 0.65
377
+ borderColor: color.border,
378
+ gap: spacing.sm,
379
+ ...shadow.sm
241
380
  },
242
- card: {
243
- backgroundColor: color[profileCardRecipe.backgroundColor],
244
- borderRadius: radius[profileCardRecipe.radius],
245
- padding: spacing[profileCardRecipe.padding],
246
- gap: spacing.lg,
247
- ...shadow[profileCardRecipe.elevation]
381
+ headerRow: {
382
+ flexDirection: 'row',
383
+ alignItems: 'center',
384
+ gap: spacing.md
248
385
  },
249
386
  avatarCircle: {
250
- width: 96,
251
- height: 96,
252
- borderRadius: 48,
387
+ width: 84,
388
+ height: 84,
389
+ borderRadius: 42,
253
390
  overflow: 'hidden',
254
- backgroundColor: color[placeholderPalette.container],
255
- alignSelf: 'center',
391
+ backgroundColor: color.surfaceAlt,
256
392
  alignItems: 'center',
257
393
  justifyContent: 'center'
258
394
  },
@@ -261,44 +397,40 @@ const styles = StyleSheet.create({
261
397
  height: '100%'
262
398
  },
263
399
  placeholderAvatar: {
264
- width: '100%',
265
- height: '100%',
266
400
  alignItems: 'center',
267
- justifyContent: 'center'
401
+ gap: spacing.xs
268
402
  },
269
403
  placeholderHead: {
270
- width: 44,
271
- height: 44,
272
- borderRadius: 22,
273
- backgroundColor: color[placeholderPalette.head]
404
+ width: 28,
405
+ height: 28,
406
+ borderRadius: 14,
407
+ backgroundColor: color.borderStrong
274
408
  },
275
409
  placeholderShoulders: {
276
- width: 68,
277
- height: 36,
278
- borderRadius: 18,
279
- backgroundColor: color[placeholderPalette.shoulders],
280
- marginTop: spacing.sm
281
- },
282
- headerText: {
283
- alignItems: 'center',
284
- gap: spacing.xxs
410
+ width: 48,
411
+ height: 18,
412
+ borderRadius: 999,
413
+ backgroundColor: color.border
285
414
  },
286
415
  displayName: {
287
- fontSize: 22,
416
+ fontSize: 24,
288
417
  fontWeight: '700',
289
418
  color: color.text
290
419
  },
291
420
  handleText: {
292
- fontSize: 14,
293
- color: color.textSubdued
421
+ color: color.textMuted,
422
+ fontSize: 15
423
+ },
424
+ metaCopy: {
425
+ color: color.textSubdued,
426
+ fontSize: 12
294
427
  },
295
428
  metricsRow: {
296
429
  flexDirection: 'row',
297
- gap: spacing.xxl,
298
- marginTop: spacing.sm
430
+ gap: spacing.xl
299
431
  },
300
432
  metricItem: {
301
- alignItems: 'center'
433
+ gap: spacing.xxs
302
434
  },
303
435
  metricValue: {
304
436
  fontSize: 18,
@@ -306,127 +438,124 @@ const styles = StyleSheet.create({
306
438
  color: color.text
307
439
  },
308
440
  metricLabel: {
309
- fontSize: 12,
310
- color: color.textMuted,
311
- textTransform: 'uppercase',
312
- letterSpacing: 0.6
313
- },
314
- logoutChip: {
315
- alignSelf: 'center',
316
- borderRadius: radius.pill,
317
- borderWidth: 1,
318
- borderColor: color.borderStrong,
319
- paddingHorizontal: spacing.lg,
320
- paddingVertical: spacing.sm
441
+ color: color.textMuted
321
442
  },
322
- logoutChipText: {
323
- fontSize: 12,
324
- fontWeight: '600',
325
- color: color.text
443
+ bioText: {
444
+ color: color.text,
445
+ lineHeight: 22
326
446
  },
327
- followActionButton: {
328
- marginTop: spacing.md,
329
- alignSelf: 'center',
330
- borderRadius: radius.pill,
331
- paddingHorizontal: spacing.xxl,
332
- paddingVertical: spacing.smPlus,
333
- minWidth: 120,
447
+ actionsRow: {
448
+ flexDirection: 'row',
449
+ flexWrap: 'wrap',
450
+ gap: spacing.sm,
334
451
  alignItems: 'center'
335
452
  },
336
- followButton: {
337
- backgroundColor: color.primary
453
+ primaryButton: {
454
+ borderRadius: 999,
455
+ backgroundColor: color.primary,
456
+ paddingHorizontal: spacing.md,
457
+ paddingVertical: spacing.sm
338
458
  },
339
- followingButton: {
340
- backgroundColor: color.surfaceSubdued
459
+ primaryButtonText: {
460
+ color: '#ffffff',
461
+ fontWeight: '600'
341
462
  },
342
- followButtonText: {
343
- fontSize: 14,
344
- fontWeight: '600',
345
- color: color.surface
463
+ secondaryButton: {
464
+ borderRadius: 999,
465
+ borderWidth: 1,
466
+ borderColor: color.borderStrong,
467
+ paddingHorizontal: spacing.md,
468
+ paddingVertical: spacing.sm,
469
+ backgroundColor: color.surface
346
470
  },
347
- followingButtonText: {
348
- fontSize: 14,
349
- fontWeight: '600',
350
- color: color.text
471
+ secondaryButtonText: {
472
+ color: color.text,
473
+ fontWeight: '600'
351
474
  },
352
475
  sectionTitle: {
353
- fontSize: 16,
476
+ fontSize: 18,
354
477
  fontWeight: '700',
355
478
  color: color.text
356
479
  },
357
- readOnlyBio: {
358
- fontSize: 15,
359
- lineHeight: 20,
360
- color: color.text
480
+ sectionBody: {
481
+ color: color.textMuted,
482
+ lineHeight: 20
361
483
  },
362
484
  formGroup: {
363
- gap: spacing.sm
485
+ gap: spacing.xs
364
486
  },
365
487
  label: {
366
488
  fontSize: 13,
367
489
  fontWeight: '600',
368
490
  color: color.text
369
491
  },
370
- textInput: {
371
- borderRadius: radius.md,
492
+ input: {
372
493
  borderWidth: 1,
373
494
  borderColor: color.borderStrong,
374
- paddingHorizontal: spacing.mdPlus,
375
- paddingVertical: spacing.md,
376
- fontSize: 15,
377
- color: color.text,
378
- backgroundColor: color.surfaceAlt
379
- },
380
- bioInput: {
381
- minHeight: 120
495
+ borderRadius: radius.md,
496
+ paddingHorizontal: spacing.md,
497
+ paddingVertical: spacing.sm,
498
+ backgroundColor: color.surfaceAlt,
499
+ color: color.text
382
500
  },
383
- successText: {
384
- fontSize: 13,
385
- color: '#1d7e1f',
386
- fontWeight: '600'
501
+ multilineInput: {
502
+ minHeight: 88
387
503
  },
388
504
  errorText: {
389
- fontSize: 13,
390
- color: '#d22c2c',
391
- fontWeight: '600'
505
+ color: '#d22c2c'
392
506
  },
393
- postsLoading: {
394
- marginTop: spacing.md
395
- },
396
- postCard: {
397
- borderRadius: radius[postCardRecipe.radius],
398
- borderWidth: postCardRecipe.borderColor ? 1 : 0,
399
- borderColor: postCardRecipe.borderColor ? color[postCardRecipe.borderColor] : undefined,
400
- padding: spacing[postCardRecipe.padding],
401
- gap: spacing.sm,
402
- backgroundColor: color[postCardRecipe.backgroundColor],
403
- marginTop: spacing.md
507
+ successText: {
508
+ color: '#17663a'
404
509
  },
405
- postTimestamp: {
406
- fontSize: 12,
510
+ mutedText: {
407
511
  color: color.textMuted
408
512
  },
513
+ postRow: {
514
+ gap: spacing.xs,
515
+ borderTopWidth: 1,
516
+ borderTopColor: color.border,
517
+ paddingTop: spacing.sm
518
+ },
409
519
  postBody: {
410
- fontSize: 15,
411
520
  color: color.text,
412
521
  lineHeight: 20
413
522
  },
414
- errorCard: {
415
- backgroundColor: '#fff4f4',
416
- borderRadius: radius.lg,
417
- padding: spacing.lg,
418
- borderWidth: 1,
419
- borderColor: '#f6d5d5',
420
- gap: spacing.xs
523
+ userRow: {
524
+ flexDirection: 'row',
525
+ alignItems: 'center',
526
+ gap: spacing.md,
527
+ borderTopWidth: 1,
528
+ borderTopColor: color.border,
529
+ paddingTop: spacing.sm
530
+ },
531
+ searchAvatarShell: {
532
+ width: 44,
533
+ height: 44,
534
+ borderRadius: 22,
535
+ alignItems: 'center',
536
+ justifyContent: 'center',
537
+ overflow: 'hidden',
538
+ backgroundColor: color.primary
421
539
  },
422
- errorTitle: {
540
+ searchAvatarImage: {
541
+ width: '100%',
542
+ height: '100%'
543
+ },
544
+ searchAvatarFallback: {
545
+ color: '#ffffff',
546
+ fontSize: 16,
547
+ fontWeight: '700'
548
+ },
549
+ cardTitle: {
423
550
  fontSize: 15,
424
- fontWeight: '700',
425
- color: '#b11f1f'
551
+ fontWeight: '600',
552
+ color: color.text
426
553
  },
427
- errorBody: {
428
- fontSize: 13,
429
- color: '#7a2f2f',
430
- lineHeight: 18
554
+ inlineState: {
555
+ alignItems: 'center',
556
+ paddingVertical: spacing.lg
557
+ },
558
+ flexSpacer: {
559
+ flex: 1
431
560
  }
432
561
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mereb/app-profile",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Profile experience components and hooks for Mereb apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,12 +27,13 @@
27
27
  "package.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@mereb/shared-graphql": "^0.0.15",
31
- "@mereb/ui-shared": "^0.0.4",
32
- "@mereb/tokens": "^0.0.4"
30
+ "@mereb/shared-graphql": "^0.0.17",
31
+ "@mereb/ui-shared": "^0.0.7",
32
+ "@mereb/tokens": "^0.0.9"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@apollo/client": ">=4.0.0",
36
+ "expo-image-picker": ">=15.0.0",
36
37
  "expo-router": ">=3.0.0",
37
38
  "react": ">=18.2.0",
38
39
  "react-native": ">=0.72.0"