diff --git a/lib/shared/avatar-utils.js b/lib/shared/avatar-utils.js index 68eb3b4a7..8e9c4fd55 100644 --- a/lib/shared/avatar-utils.js +++ b/lib/shared/avatar-utils.js @@ -1,125 +1,166 @@ // @flow import invariant from 'invariant'; +import * as React from 'react'; import stringHash from 'string-hash'; +import { userIdentifiedByETHAddress } from './account-utils.js'; import { selectedThreadColors } from './color-utils.js'; import { threadOtherMembers } from './thread-utils.js'; import genesis from '../facts/genesis.js'; +import { useENSAvatar } from '../hooks/ens-cache.js'; import { threadInfoSelector } from '../selectors/thread-selectors.js'; -import type { ClientEmojiAvatar, ClientAvatar } from '../types/avatar-types.js'; +import type { + ClientEmojiAvatar, + ClientAvatar, + ResolvedClientAvatar, +} from '../types/avatar-types.js'; import { type RawThreadInfo, type ThreadInfo, threadTypes, } from '../types/thread-types.js'; -import type { UserInfos } from '../types/user-types.js'; +import type { UserInfos, UserInfo } from '../types/user-types.js'; import { useSelector } from '../utils/redux-utils.js'; const defaultAnonymousUserEmojiAvatar: ClientEmojiAvatar = { color: selectedThreadColors[4], emoji: '👤', type: 'emoji', }; const defaultEmojiAvatars: $ReadOnlyArray = [ { color: selectedThreadColors[7], emoji: '😀', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '😆', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🤩', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🏈', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '👻', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🧑‍🚀', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🎩', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🍦', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🚀', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🥰', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🐬', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🍀', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🍕', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '🙄', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🥳', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🥸', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '🦋', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🏆', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🎲', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🏀', type: 'emoji' }, ]; function getDefaultAvatar(hashKey: string, color?: string): ClientEmojiAvatar { const avatarIndex = stringHash(hashKey) % defaultEmojiAvatars.length; return { ...defaultEmojiAvatars[avatarIndex], color: color ? color : defaultEmojiAvatars[avatarIndex].color, }; } function getAvatarForUser( user: ?{ +avatar?: ?ClientAvatar, +username?: ?string, ... }, ): ClientAvatar { if (user?.avatar) { return user.avatar; } if (!user?.username) { return defaultAnonymousUserEmojiAvatar; } return getDefaultAvatar(user.username); } function getUserAvatarForThread( threadInfo: RawThreadInfo | ThreadInfo, viewerID: ?string, userInfos: UserInfos, ): ClientAvatar { if (threadInfo.type === threadTypes.PRIVATE) { invariant(viewerID, 'viewerID should be set for PRIVATE threads'); return getAvatarForUser(userInfos[viewerID]); } invariant( threadInfo.type === threadTypes.PERSONAL, 'threadInfo should be a PERSONAL type', ); const memberInfos = threadOtherMembers(threadInfo.members, viewerID) .map(member => userInfos[member.id] && userInfos[member.id]) .filter(Boolean); if (memberInfos.length === 0) { return defaultAnonymousUserEmojiAvatar; } return getAvatarForUser(memberInfos[0]); } function useGetAvatarForThread( thread: RawThreadInfo | ThreadInfo, ): ClientAvatar { const containingThreadID = thread.containingThreadID; const containingThreadInfo = useSelector(state => containingThreadID ? threadInfoSelector(state)[containingThreadID] : null, ); if (thread.avatar) { return thread.avatar; } if (thread.containingThreadID && thread.containingThreadID !== genesis.id) { invariant(containingThreadInfo, 'containingThreadInfo should be set'); return containingThreadInfo.avatar ? containingThreadInfo.avatar : getDefaultAvatar(containingThreadInfo.id, containingThreadInfo.color); } return getDefaultAvatar(thread.id, thread.color); } +function useENSResolvedAvatar( + avatarInfo: ClientAvatar, + userInfo: ?UserInfo, +): ResolvedClientAvatar { + const ethAddress = React.useMemo(() => { + let address = null; + if (userInfo && avatarInfo.type === 'ens') { + const { username } = userInfo; + address = + username && userIdentifiedByETHAddress(userInfo) ? username : null; + } + return address; + }, [avatarInfo.type, userInfo]); + + const ensAvatarURI = useENSAvatar(ethAddress); + + const resolvedAvatar = React.useMemo(() => { + if (avatarInfo.type !== 'ens') { + return avatarInfo; + } + + if (ensAvatarURI) { + return { + type: 'image', + uri: ensAvatarURI, + }; + } + + return defaultAnonymousUserEmojiAvatar; + }, [ensAvatarURI, avatarInfo]); + + return resolvedAvatar; +} + export { - defaultAnonymousUserEmojiAvatar, getAvatarForUser, getUserAvatarForThread, useGetAvatarForThread, + useENSResolvedAvatar, }; diff --git a/lib/types/avatar-types.js b/lib/types/avatar-types.js index e155bcc19..a406a73d8 100644 --- a/lib/types/avatar-types.js +++ b/lib/types/avatar-types.js @@ -1,39 +1,41 @@ // @flow export type EmojiAvatarDBContent = { +type: 'emoji', +emoji: string, +color: string, // hex, without "#" or "0x" }; export type ImageAvatarDBContent = { +type: 'image', +uploadID: string, }; export type ENSAvatarDBContent = { +type: 'ens', }; export type AvatarDBContent = | EmojiAvatarDBContent | ImageAvatarDBContent | ENSAvatarDBContent; export type UpdateUserAvatarRemoveRequest = { +type: 'remove' }; export type UpdateUserAvatarRequest = | AvatarDBContent | UpdateUserAvatarRemoveRequest; export type ClientEmojiAvatar = EmojiAvatarDBContent; export type ClientImageAvatar = { +type: 'image', +uri: string, }; export type ClientENSAvatar = ENSAvatarDBContent; export type ClientAvatar = | ClientEmojiAvatar | ClientImageAvatar | ClientENSAvatar; + +export type ResolvedClientAvatar = ClientEmojiAvatar | ClientImageAvatar; diff --git a/native/components/user-avatar.react.js b/native/components/user-avatar.react.js index 7561df9e9..94cfa9dc5 100644 --- a/native/components/user-avatar.react.js +++ b/native/components/user-avatar.react.js @@ -1,58 +1,31 @@ // @flow import * as React from 'react'; -import { useENSAvatar } from 'lib/hooks/ens-cache.js'; -import { userIdentifiedByETHAddress } from 'lib/shared/account-utils.js'; import { - defaultAnonymousUserEmojiAvatar, getAvatarForUser, + useENSResolvedAvatar, } from 'lib/shared/avatar-utils.js'; import Avatar from './avatar.react.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { +userID: ?string, +size: 'micro' | 'small' | 'large' | 'profile', }; function UserAvatar(props: Props): React.Node { const { userID, size } = props; const userInfo = useSelector(state => userID ? state.userStore.userInfos[userID] : null, ); const avatarInfo = getAvatarForUser(userInfo); - const ethAddress = React.useMemo(() => { - let address = null; - if (userInfo && avatarInfo.type === 'ens') { - const { username } = userInfo; - address = - username && userIdentifiedByETHAddress(userInfo) ? username : null; - } - return address; - }, [avatarInfo.type, userInfo]); - - const ensAvatarURI = useENSAvatar(ethAddress); - - const userAvatarInfo = React.useMemo(() => { - if (avatarInfo.type !== 'ens') { - return avatarInfo; - } - - if (ensAvatarURI) { - return { - type: 'image', - uri: ensAvatarURI, - }; - } - - return defaultAnonymousUserEmojiAvatar; - }, [ensAvatarURI, avatarInfo]); - - return ; + const resolvedUserAvatar = useENSResolvedAvatar(avatarInfo, userInfo); + + return ; } export default UserAvatar;