Page MenuHomePhorge

D13101.1767273529.diff
No OneTemporary

Size
14 KB
Referenced Files
None
Subscribers
None

D13101.1767273529.diff

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

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)

Event Timeline