Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32558925
D13101.1767273529.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
14 KB
Referenced Files
None
Subscribers
None
D13101.1767273529.diff
View Options
diff --git a/lib/hooks/fc-cache.js b/lib/hooks/fc-cache.js
--- a/lib/hooks/fc-cache.js
+++ b/lib/hooks/fc-cache.js
@@ -141,4 +141,44 @@
);
}
-export { useFCNames };
+function useFarcasterAvatarURL(fid: ?string): ?string {
+ const neynarClientContext = React.useContext(NeynarClientContext);
+ const fcCache = neynarClientContext?.fcCache;
+
+ const cachedAvatarURL = React.useMemo(() => {
+ if (!fid || !fcCache) {
+ return null;
+ }
+ const cachedUser = fcCache.getCachedFarcasterUserForFID(fid);
+ return cachedUser?.pfpURL ?? null;
+ }, [fcCache, fid]);
+
+ const [farcasterAvatarURL, setFarcasterAvatarURL] =
+ React.useState<?string>(null);
+
+ React.useEffect(() => {
+ if (!fcCache || !fid || cachedAvatarURL) {
+ return;
+ }
+ void (async () => {
+ const [fetchedUser] = await fcCache.getFarcasterUsersForFIDs([fid]);
+ const avatarURL = fetchedUser?.pfpURL;
+ if (!avatarURL) {
+ return;
+ }
+ setFarcasterAvatarURL(avatarURL);
+ })();
+ }, [fcCache, cachedAvatarURL, fid]);
+
+ return React.useMemo(() => {
+ if (!fid) {
+ return null;
+ } else if (cachedAvatarURL) {
+ return cachedAvatarURL;
+ } else {
+ return farcasterAvatarURL;
+ }
+ }, [fid, cachedAvatarURL, farcasterAvatarURL]);
+}
+
+export { useFCNames, useFarcasterAvatarURL };
diff --git a/lib/shared/avatar-utils.js b/lib/shared/avatar-utils.js
--- a/lib/shared/avatar-utils.js
+++ b/lib/shared/avatar-utils.js
@@ -8,11 +8,11 @@
import { threadOtherMembers } from './thread-utils.js';
import genesis from '../facts/genesis.js';
import { useENSAvatar } from '../hooks/ens-cache.js';
+import { useFarcasterAvatarURL } from '../hooks/fc-cache.js';
import { getETHAddressForUserInfo } from '../shared/account-utils.js';
import type {
ClientAvatar,
ClientEmojiAvatar,
- GenericUserInfoWithAvatar,
ResolvedClientAvatar,
} from '../types/avatar-types.js';
import type {
@@ -318,17 +318,20 @@
function useResolvedAvatar(
avatarInfo: ClientAvatar,
- userInfo: ?GenericUserInfoWithAvatar,
+ usernameAndFID: ?{ +username?: ?string, +farcasterID?: ?string, ... },
): ResolvedClientAvatar {
const ethAddress = React.useMemo(
- () => getETHAddressForUserInfo(userInfo),
- [userInfo],
+ () => getETHAddressForUserInfo(usernameAndFID),
+ [usernameAndFID],
);
const ensAvatarURI = useENSAvatar(ethAddress);
+ const fid = usernameAndFID?.farcasterID;
+ const farcasterAvatarURL = useFarcasterAvatarURL(fid);
+
const resolvedAvatar = React.useMemo(() => {
- if (avatarInfo.type !== 'ens') {
+ if (avatarInfo.type !== 'ens' && avatarInfo.type !== 'farcaster') {
return avatarInfo;
}
@@ -337,10 +340,15 @@
type: 'image',
uri: ensAvatarURI,
};
+ } else if (farcasterAvatarURL) {
+ return {
+ type: 'image',
+ uri: farcasterAvatarURL,
+ };
}
return defaultAnonymousUserEmojiAvatar;
- }, [ensAvatarURI, avatarInfo]);
+ }, [avatarInfo, ensAvatarURI, farcasterAvatarURL]);
return resolvedAvatar;
}
diff --git a/lib/types/avatar-types.js b/lib/types/avatar-types.js
--- a/lib/types/avatar-types.js
+++ b/lib/types/avatar-types.js
@@ -35,11 +35,18 @@
export const ensAvatarDBContentValidator: TInterface<ENSAvatarDBContent> =
tShape({ type: tString('ens') });
+export type FarcasterAvatarDBContent = {
+ +type: 'farcaster',
+};
+export const farcasterAvatarDBContentValidator: TInterface<FarcasterAvatarDBContent> =
+ tShape({ type: tString('farcaster') });
+
export type AvatarDBContent =
| EmojiAvatarDBContent
| ImageAvatarDBContent
| EncryptedImageAvatarDBContent
- | ENSAvatarDBContent;
+ | ENSAvatarDBContent
+ | FarcasterAvatarDBContent;
export type UpdateUserAvatarRemoveRequest = { +type: 'remove' };
@@ -83,16 +90,21 @@
export type ClientENSAvatar = ENSAvatarDBContent;
const clientENSAvatarValidator = ensAvatarDBContentValidator;
+export type ClientFarcasterAvatar = FarcasterAvatarDBContent;
+const clientFarcasterAvatarValidator = farcasterAvatarDBContentValidator;
+
export type ClientAvatar =
| ClientEmojiAvatar
| ClientImageAvatar
| ClientEncryptedImageAvatar
- | ClientENSAvatar;
+ | ClientENSAvatar
+ | ClientFarcasterAvatar;
export const clientAvatarValidator: TUnion<ClientAvatar> = t.union([
clientEmojiAvatarValidator,
clientImageAvatarValidator,
clientENSAvatarValidator,
clientEncryptedImageAvatarValidator,
+ clientFarcasterAvatarValidator,
]);
export type ResolvedClientAvatar =
diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js
--- a/native/account/registration/avatar-selection.react.js
+++ b/native/account/registration/avatar-selection.react.js
@@ -19,6 +19,7 @@
type AccountSelection,
type AvatarData,
ensAvatarSelection,
+ farcasterAvatarSelection,
} from './registration-types.js';
import EditUserAvatar from '../../avatars/edit-user-avatar.react.js';
import PrimaryButton from '../../components/primary-button.react.js';
@@ -49,7 +50,7 @@
};
function AvatarSelection(props: Props): React.Node {
const { userSelections } = props.route.params;
- const { accountSelection } = userSelections;
+ const { accountSelection, farcasterAvatarURL, farcasterID } = userSelections;
const usernameOrETHAddress =
accountSelection.accountType === 'username'
? accountSelection.username
@@ -63,14 +64,16 @@
invariant(editUserAvatarContext, 'editUserAvatarContext should be set');
const { setRegistrationMode } = editUserAvatarContext;
- const prefetchedAvatarURI =
+ const prefetchedENSAvatarURI =
accountSelection.accountType === 'ethereum'
? accountSelection.avatarURI
: undefined;
let initialAvatarData = cachedSelections.avatarData;
- if (!initialAvatarData && prefetchedAvatarURI) {
+ if (!initialAvatarData && prefetchedENSAvatarURI) {
initialAvatarData = ensAvatarSelection;
+ } else if (!initialAvatarData && farcasterAvatarURL) {
+ initialAvatarData = farcasterAvatarSelection;
}
const [avatarData, setAvatarData] =
@@ -178,7 +181,9 @@
<View style={styles.editUserAvatar}>
<EditUserAvatar
userInfo={userInfoOverride}
- prefetchedAvatarURI={prefetchedAvatarURI}
+ prefetchedENSAvatarURI={prefetchedENSAvatarURI}
+ prefetchedFarcasterAvatarURL={farcasterAvatarURL}
+ fid={farcasterID}
/>
</View>
</View>
diff --git a/native/account/registration/registration-types.js b/native/account/registration/registration-types.js
--- a/native/account/registration/registration-types.js
+++ b/native/account/registration/registration-types.js
@@ -67,3 +67,9 @@
updateUserAvatarRequest: { type: 'ens' },
clientAvatar: { type: 'ens' },
};
+
+export const farcasterAvatarSelection: AvatarData = {
+ needsUpload: false,
+ updateUserAvatarRequest: { type: 'farcaster' },
+ clientAvatar: { type: 'farcaster' },
+};
diff --git a/native/avatars/avatar-hooks.js b/native/avatars/avatar-hooks.js
--- a/native/avatars/avatar-hooks.js
+++ b/native/avatars/avatar-hooks.js
@@ -469,7 +469,7 @@
}
type ShowAvatarActionSheetOptions = {
- +id: 'emoji' | 'image' | 'camera' | 'ens' | 'cancel' | 'remove',
+ +id: 'emoji' | 'image' | 'camera' | 'ens' | 'farcaster' | 'cancel' | 'remove',
+onPress?: () => mixed,
};
function useShowAvatarActionSheet(
@@ -491,6 +491,8 @@
return 'Open camera';
} else if (option.id === 'ens') {
return 'Use ENS avatar';
+ } else if (option.id === 'farcaster') {
+ return 'Use Farcaster avatar';
} else if (option.id === 'remove') {
return 'Reset to default';
} else {
diff --git a/native/avatars/edit-user-avatar.react.js b/native/avatars/edit-user-avatar.react.js
--- a/native/avatars/edit-user-avatar.react.js
+++ b/native/avatars/edit-user-avatar.react.js
@@ -7,6 +7,7 @@
import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js';
import { useENSAvatar } from 'lib/hooks/ens-cache.js';
+import { useFarcasterAvatarURL } from 'lib/hooks/fc-cache.js';
import { getETHAddressForUserInfo } from 'lib/shared/account-utils.js';
import type { GenericUserInfoWithAvatar } from 'lib/types/avatar-types.js';
@@ -27,11 +28,13 @@
import { useStyles } from '../themes/colors.js';
type Props =
- | { +userID: ?string, +disabled?: boolean }
+ | { +userID: ?string, +disabled?: boolean, +fid: ?string }
| {
+userInfo: ?GenericUserInfoWithAvatar,
+disabled?: boolean,
- +prefetchedAvatarURI: ?string,
+ +prefetchedENSAvatarURI: ?string,
+ +prefetchedFarcasterAvatarURL: ?string,
+ +fid: ?string,
};
function EditUserAvatar(props: Props): React.Node {
const editUserAvatarContext = React.useContext(EditUserAvatarContext);
@@ -53,7 +56,12 @@
[userInfo],
);
const fetchedENSAvatarURI = useENSAvatar(ethAddress);
- const ensAvatarURI = fetchedENSAvatarURI ?? props.prefetchedAvatarURI;
+ const ensAvatarURI = fetchedENSAvatarURI ?? props.prefetchedENSAvatarURI;
+
+ const fid = props.fid;
+ const fetchedFarcasterAvatarURL = useFarcasterAvatarURL(fid);
+ const farcasterAvatarURL =
+ fetchedFarcasterAvatarURL ?? props.prefetchedFarcasterAvatarURL;
const { navigate } = useNavigation();
@@ -82,6 +90,11 @@
[nativeSetUserAvatar],
);
+ const setFarcasterUserAvatar = React.useCallback(
+ () => nativeSetUserAvatar({ type: 'farcaster' }),
+ [nativeSetUserAvatar],
+ );
+
const removeUserAvatar = React.useCallback(
() => nativeSetUserAvatar({ type: 'remove' }),
[nativeSetUserAvatar],
@@ -99,19 +112,25 @@
configOptions.push({ id: 'ens', onPress: setENSUserAvatar });
}
+ if (farcasterAvatarURL) {
+ configOptions.push({ id: 'farcaster', onPress: setFarcasterUserAvatar });
+ }
+
if (hasCurrentAvatar) {
configOptions.push({ id: 'remove', onPress: removeUserAvatar });
}
return configOptions;
}, [
- hasCurrentAvatar,
- ensAvatarURI,
- navigateToCamera,
navigateToEmojiSelection,
- removeUserAvatar,
- setENSUserAvatar,
selectFromGalleryAndUpdateUserAvatar,
+ navigateToCamera,
+ ensAvatarURI,
+ farcasterAvatarURL,
+ hasCurrentAvatar,
+ setENSUserAvatar,
+ setFarcasterUserAvatar,
+ removeUserAvatar,
]);
const showAvatarActionSheet = useShowAvatarActionSheet(actionSheetConfig);
@@ -131,7 +150,7 @@
const userAvatar = userID ? (
<UserAvatar userID={userID} size="XL" />
) : (
- <UserAvatar userInfo={userInfo} size="XL" />
+ <UserAvatar userInfo={userInfo} fid={fid} size="XL" />
);
const { disabled } = props;
diff --git a/native/avatars/user-avatar.react.js b/native/avatars/user-avatar.react.js
--- a/native/avatars/user-avatar.react.js
+++ b/native/avatars/user-avatar.react.js
@@ -10,29 +10,44 @@
GenericUserInfoWithAvatar,
AvatarSize,
} from 'lib/types/avatar-types.js';
+import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js';
import Avatar from './avatar.react.js';
import { useSelector } from '../redux/redux-utils.js';
+// We have two variants for Props here because we want to be able to display a
+// user avatar during the registration workflow, at which point the user will
+// not yet have a user ID. In this case, we must pass the relevant avatar info
+// into the component.
type Props =
| { +userID: ?string, +size: AvatarSize }
- | { +userInfo: ?GenericUserInfoWithAvatar, +size: AvatarSize };
+ | { +userInfo: ?GenericUserInfoWithAvatar, +size: AvatarSize, +fid: ?string };
function UserAvatar(props: Props): React.Node {
- const { userID, userInfo: userInfoProp, size } = props;
+ const { userID, userInfo: userInfoProp, size, fid } = props;
- const userInfo = useSelector(state => {
+ const currentUserFID = useCurrentUserFID();
+ const userAvatarInfo = useSelector(state => {
if (!userID) {
- return userInfoProp;
+ return {
+ ...userInfoProp,
+ farcasterID: fid,
+ };
} else if (userID === state.currentUserInfo?.id) {
- return state.currentUserInfo;
+ return {
+ ...state.currentUserInfo,
+ farcasterID: currentUserFID,
+ };
} else {
- return state.userStore.userInfos[userID];
+ return {
+ ...state.userStore.userInfos[userID],
+ farcasterID: state.auxUserStore.auxUserInfos[userID]?.fid,
+ };
}
});
- const avatarInfo = getAvatarForUser(userInfo);
+ const avatar = getAvatarForUser(userAvatarInfo);
- const resolvedUserAvatar = useResolvedAvatar(avatarInfo, userInfo);
+ const resolvedUserAvatar = useResolvedAvatar(avatar, userAvatarInfo);
return <Avatar size={size} avatarInfo={resolvedUserAvatar} />;
}
diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js
--- a/native/profile/profile-screen.react.js
+++ b/native/profile/profile-screen.react.js
@@ -25,6 +25,7 @@
import { thickThreadTypes } from 'lib/types/thread-types-enum.js';
import { type CurrentUserInfo } from 'lib/types/user-types.js';
import { getContentSigningKey } from 'lib/utils/crypto-utils.js';
+import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js';
import {
useDispatchActionPromise,
type DispatchActionPromise,
@@ -178,6 +179,7 @@
+stringForUser: ?string,
+isAccountWithPassword: boolean,
+onCreateDMThread: () => Promise<void>,
+ +currentUserFID: ?string,
};
class ProfileScreen extends React.PureComponent<Props> {
@@ -301,7 +303,10 @@
<View
style={[this.props.styles.section, this.props.styles.avatarSection]}
>
- <EditUserAvatar userID={this.props.currentUserInfo?.id} />
+ <EditUserAvatar
+ userID={this.props.currentUserInfo?.id}
+ fid={this.props.currentUserFID}
+ />
</View>
<Text style={this.props.styles.header}>ACCOUNT</Text>
<View style={this.props.styles.section}>
@@ -568,6 +573,7 @@
const isAccountWithPassword = useSelector(state =>
accountHasPassword(state.currentUserInfo),
);
+ const currentUserID = useCurrentUserFID();
const userID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
@@ -612,6 +618,7 @@
stringForUser={stringForUser}
isAccountWithPassword={isAccountWithPassword}
onCreateDMThread={onCreateDMThread}
+ currentUserFID={currentUserID}
/>
);
});
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Jan 1, 1:18 PM (12 h, 19 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5876314
Default Alt Text
D13101.1767273529.diff (14 KB)
Attached To
Mode
D13101: [native] let user choose farcaster avatar
Attached
Detach File
Event Timeline
Log In to Comment