diff --git a/lib/shared/farcaster/farcaster-api.js b/lib/shared/farcaster/farcaster-api.js --- a/lib/shared/farcaster/farcaster-api.js +++ b/lib/shared/farcaster/farcaster-api.js @@ -460,7 +460,6 @@ return React.useCallback( async (input: GetFarcasterDirectCastUsersInput) => { const { q, limit, cursor, excludeFIDs } = input; - // vNext doesn't work when cursor is set let query = { q }; if (limit) { query = { ...query, limit }; diff --git a/lib/shared/search-utils.js b/lib/shared/search-utils.js --- a/lib/shared/search-utils.js +++ b/lib/shared/search-utils.js @@ -2,7 +2,8 @@ import * as React from 'react'; -import { messageID, extractFIDFromUserID } from './id-utils.js'; +import { useGetFarcasterDirectCastUsers } from './farcaster/farcaster-api.js'; +import { messageID, userIDFromFID, extractFIDFromUserID } from './id-utils.js'; import SearchIndex from './search-index.js'; import { getContainingThreadID, @@ -10,6 +11,7 @@ userHasDeviceList, } from './thread-utils.js'; import { threadSpecs, threadTypeIsSidebar } from './threads/thread-specs.js'; +import { ensNameForFarcasterUsername } from './user-utils.js'; import { searchMessagesActionTypes } from '../actions/message-actions.js'; import { searchUsers, @@ -425,12 +427,15 @@ type UseSearchUsersOptions = { +includeViewer?: ?boolean, + +searchFarcaster?: ?boolean, }; function useSearchUsers( usernameInputText: string, options?: ?UseSearchUsersOptions, ): $ReadOnlyArray { const includeViewer = !!options?.includeViewer; + const searchFarcaster = !!options?.searchFarcaster; + const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); @@ -457,6 +462,8 @@ sendPrefixQuery: callIdentitySearchUsers, } = useIdentitySearch(); + const findFarcasterDCUsers = useGetFarcasterDirectCastUsers(); + const dispatchActionPromise = useDispatchActionPromise(); React.useEffect(() => { @@ -465,21 +472,56 @@ return; } const searchUsersPromise = (async () => { - if (identitySearchSocketConnected) { - try { - const identitySearchResult = await callIdentitySearchUsers( - forwardLookupSearchText, - ); - const userInfos = identitySearchResult.map(user => ({ - id: user.userID, - username: user.username, - avatar: null, - })); - setSearchResultsFromServer(userInfos); - return; - } catch (err) { - console.error(err); + const foundInfos: Array = []; + + const identityPromise = (async () => { + if (identitySearchSocketConnected) { + try { + const identitySearchResult = await callIdentitySearchUsers( + forwardLookupSearchText, + ); + const userInfos = identitySearchResult.map(user => ({ + id: user.userID, + username: user.username, + avatar: null, + })); + foundInfos.push(...userInfos); + } catch (err) { + console.error('identity search error', err); + } + } + })(); + + const farcasterPromise = (async () => { + if (searchFarcaster) { + try { + const fcSearchResult = await findFarcasterDCUsers({ + q: forwardLookupSearchText, + }); + const userInfos = fcSearchResult.result.users.map(fcUser => ({ + id: userIDFromFID(fcUser.fid.toString()), + username: fcUser.username + ? ensNameForFarcasterUsername(fcUser.username) + : 'anonymous', + avatar: fcUser.pfp + ? { + type: 'image', + uri: fcUser.pfp.url, + } + : null, + })); + foundInfos.push(...userInfos); + } catch (err) { + console.error('farcaster user search error', err); + } } + })(); + + await Promise.all([identityPromise, farcasterPromise]); + + if (foundInfos.length > 0) { + setSearchResultsFromServer(foundInfos); + return; } const { userInfos: keyserverSearchResult } = await callLegacyAshoatKeyserverSearchUsers(forwardLookupSearchText); @@ -493,6 +535,8 @@ identitySearchSocketConnected, dispatchActionPromise, forwardLookupSearchText, + findFarcasterDCUsers, + searchFarcaster, ]); return searchResults; } diff --git a/lib/shared/user-utils.js b/lib/shared/user-utils.js --- a/lib/shared/user-utils.js +++ b/lib/shared/user-utils.js @@ -11,6 +11,10 @@ import type { UserInfo } from '../types/user-types.js'; import { useSelector } from '../utils/redux-utils.js'; +function ensNameForFarcasterUsername(farcasterUsername: string): string { + return `${farcasterUsername}.fcast.id`; +} + function stringForUser( user: ?{ +username?: ?string, @@ -48,4 +52,9 @@ return adminUserInfoWithENSName; } -export { stringForUser, stringForUserExplicit, useKeyserverAdmin }; +export { + ensNameForFarcasterUsername, + stringForUser, + stringForUserExplicit, + useKeyserverAdmin, +}; diff --git a/native/chat/message-list-container.react.js b/native/chat/message-list-container.react.js --- a/native/chat/message-list-container.react.js +++ b/native/chat/message-list-container.react.js @@ -24,6 +24,7 @@ import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; import { pinnedMessageCountText } from 'lib/utils/message-pinning-utils.js'; +import { supportsFarcasterDCs } from 'lib/utils/services-utils.js'; import { type MessagesMeasurer, useHeightMeasurer } from './chat-context.js'; import { ChatInputBar } from './chat-input-bar.react.js'; @@ -260,7 +261,9 @@ const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); - const serverSearchResults = useSearchUsers(usernameInputText); + const serverSearchResults = useSearchUsers(usernameInputText, { + searchFarcaster: supportsFarcasterDCs, + }); const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); const viewerID = useSelector(state => state.currentUserInfo?.id); diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -69,7 +69,9 @@ [userInfoInputArray, viewerID], ); - const searchResults = useSearchUsers(usernameInputText); + const searchResults = useSearchUsers(usernameInputText, { + searchFarcaster: supportsFarcasterDCs, + }); const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers);