@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.
- package/dist/headless.d.ts +1 -0
- package/dist/headless.js +3 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.js +416 -287
- package/package.json +5 -4
package/dist/headless.d.ts
CHANGED
|
@@ -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
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
|
-
|
|
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,
|
|
4
|
-
import
|
|
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 {
|
|
7
|
-
import { formatRelativeProfileTimestamp, selectProfilePosts } from './headless
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
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 [
|
|
51
|
-
const [
|
|
52
|
-
const
|
|
53
|
-
const
|
|
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
|
-
}, [
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
181
|
+
setFormSuccess(undefined);
|
|
87
182
|
try {
|
|
88
|
-
|
|
183
|
+
await updateProfile({
|
|
89
184
|
variables: {
|
|
90
185
|
displayName: trimmedName,
|
|
91
|
-
bio:
|
|
186
|
+
bio: bio.trim() || null
|
|
92
187
|
}
|
|
93
188
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
189
|
+
setFormSuccess('Profile updated.');
|
|
190
|
+
setActionError(undefined);
|
|
191
|
+
await meQuery.refetch();
|
|
98
192
|
}
|
|
99
|
-
catch (
|
|
100
|
-
console.warn('Failed to update profile',
|
|
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
|
-
}, [
|
|
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 (
|
|
199
|
+
if (!profile?.id || viewingOwnProfile || followLoading) {
|
|
112
200
|
return;
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
208
|
+
await unfollowUser({ variables: { userId: profile.id } });
|
|
121
209
|
}
|
|
122
210
|
else {
|
|
123
|
-
await
|
|
211
|
+
await followUser({ variables: { userId: profile.id } });
|
|
124
212
|
}
|
|
125
|
-
|
|
213
|
+
setActionError(undefined);
|
|
214
|
+
await refetch?.();
|
|
126
215
|
}
|
|
127
|
-
catch (
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
backgroundColor: color.surfaceMuted
|
|
175
|
-
},
|
|
176
|
-
scrollContent: {
|
|
340
|
+
screen: {
|
|
341
|
+
flexGrow: 1,
|
|
177
342
|
padding: spacing.lg,
|
|
178
|
-
gap: spacing.
|
|
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.
|
|
351
|
+
backgroundColor: color.surfaceAlt
|
|
186
352
|
},
|
|
187
353
|
placeholderCard: {
|
|
188
354
|
width: '100%',
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
gap: spacing.
|
|
195
|
-
...shadow
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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.
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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:
|
|
251
|
-
height:
|
|
252
|
-
borderRadius:
|
|
387
|
+
width: 84,
|
|
388
|
+
height: 84,
|
|
389
|
+
borderRadius: 42,
|
|
253
390
|
overflow: 'hidden',
|
|
254
|
-
backgroundColor: color
|
|
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
|
-
|
|
401
|
+
gap: spacing.xs
|
|
268
402
|
},
|
|
269
403
|
placeholderHead: {
|
|
270
|
-
width:
|
|
271
|
-
height:
|
|
272
|
-
borderRadius:
|
|
273
|
-
backgroundColor: color
|
|
404
|
+
width: 28,
|
|
405
|
+
height: 28,
|
|
406
|
+
borderRadius: 14,
|
|
407
|
+
backgroundColor: color.borderStrong
|
|
274
408
|
},
|
|
275
409
|
placeholderShoulders: {
|
|
276
|
-
width:
|
|
277
|
-
height:
|
|
278
|
-
borderRadius:
|
|
279
|
-
backgroundColor: color
|
|
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:
|
|
416
|
+
fontSize: 24,
|
|
288
417
|
fontWeight: '700',
|
|
289
418
|
color: color.text
|
|
290
419
|
},
|
|
291
420
|
handleText: {
|
|
292
|
-
|
|
293
|
-
|
|
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.
|
|
298
|
-
marginTop: spacing.sm
|
|
430
|
+
gap: spacing.xl
|
|
299
431
|
},
|
|
300
432
|
metricItem: {
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
color: color.text
|
|
443
|
+
bioText: {
|
|
444
|
+
color: color.text,
|
|
445
|
+
lineHeight: 22
|
|
326
446
|
},
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
337
|
-
|
|
453
|
+
primaryButton: {
|
|
454
|
+
borderRadius: 999,
|
|
455
|
+
backgroundColor: color.primary,
|
|
456
|
+
paddingHorizontal: spacing.md,
|
|
457
|
+
paddingVertical: spacing.sm
|
|
338
458
|
},
|
|
339
|
-
|
|
340
|
-
|
|
459
|
+
primaryButtonText: {
|
|
460
|
+
color: '#ffffff',
|
|
461
|
+
fontWeight: '600'
|
|
341
462
|
},
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
fontWeight: '600'
|
|
350
|
-
color: color.text
|
|
471
|
+
secondaryButtonText: {
|
|
472
|
+
color: color.text,
|
|
473
|
+
fontWeight: '600'
|
|
351
474
|
},
|
|
352
475
|
sectionTitle: {
|
|
353
|
-
fontSize:
|
|
476
|
+
fontSize: 18,
|
|
354
477
|
fontWeight: '700',
|
|
355
478
|
color: color.text
|
|
356
479
|
},
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
lineHeight: 20
|
|
360
|
-
color: color.text
|
|
480
|
+
sectionBody: {
|
|
481
|
+
color: color.textMuted,
|
|
482
|
+
lineHeight: 20
|
|
361
483
|
},
|
|
362
484
|
formGroup: {
|
|
363
|
-
gap: spacing.
|
|
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
|
-
|
|
371
|
-
borderRadius: radius.md,
|
|
492
|
+
input: {
|
|
372
493
|
borderWidth: 1,
|
|
373
494
|
borderColor: color.borderStrong,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
color: '#1d7e1f',
|
|
386
|
-
fontWeight: '600'
|
|
501
|
+
multilineInput: {
|
|
502
|
+
minHeight: 88
|
|
387
503
|
},
|
|
388
504
|
errorText: {
|
|
389
|
-
|
|
390
|
-
color: '#d22c2c',
|
|
391
|
-
fontWeight: '600'
|
|
505
|
+
color: '#d22c2c'
|
|
392
506
|
},
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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: '
|
|
425
|
-
color:
|
|
551
|
+
fontWeight: '600',
|
|
552
|
+
color: color.text
|
|
426
553
|
},
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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.
|
|
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.
|
|
31
|
-
"@mereb/ui-shared": "^0.0.
|
|
32
|
-
"@mereb/tokens": "^0.0.
|
|
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"
|