diff --git a/lib/contexts/protocol-selection-context.js b/lib/contexts/protocol-selection-context.js new file mode 100644 --- /dev/null +++ b/lib/contexts/protocol-selection-context.js @@ -0,0 +1,30 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import type { ProtocolName } from '../shared/threads/thread-spec.js'; +import type { AccountUserInfo } from '../types/user-types.js'; + +type ProtocolSelectionContextType = { + +selectedProtocol: ?ProtocolName, + +setSelectedProtocol: (?ProtocolName) => mixed, + +availableProtocols: $ReadOnlyArray, + +setUserInfoInput?: ($ReadOnlyArray) => mixed, + +setSearching?: boolean => mixed, +}; + +const ProtocolSelectionContext: React.Context = + React.createContext(null); + +function useProtocolSelection(): ProtocolSelectionContextType { + const context = React.useContext(ProtocolSelectionContext); + invariant( + context, + 'useProtocolSelection must be used within a ProtocolSelectionProvider', + ); + return context; +} + +export type { ProtocolSelectionContextType }; +export { ProtocolSelectionContext, useProtocolSelection }; diff --git a/lib/hooks/user-identities-hooks.js b/lib/hooks/user-identities-hooks.js --- a/lib/hooks/user-identities-hooks.js +++ b/lib/hooks/user-identities-hooks.js @@ -9,6 +9,7 @@ import type { FarcasterUser } from '../types/identity-service-types.js'; import type { AccountUserInfo } from '../types/user-types.js'; import { useSelector } from '../utils/redux-utils.js'; +import { useIsFarcasterDCsIntegrationEnabled } from '../utils/services-utils.js'; function useUsersSupportThickThreads(): ( userIDs: $ReadOnlyArray, @@ -54,11 +55,19 @@ ) => Promise<$ReadOnlyMap> { const findUserIdentities = useFindUserIdentities(); const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); + const supportsFarcasterDCs = useIsFarcasterDCsIntegrationEnabled(); return React.useCallback( async (userIDs: $ReadOnlyArray) => { const usersSupportingFCDCs = new Map(); + if (!supportsFarcasterDCs) { + for (const userID of userIDs) { + usersSupportingFCDCs.set(userID, false); + } + return usersSupportingFCDCs; + } + const usersNeedingFetch = []; for (const userID of userIDs) { const userIDIsFarcasterFID = !!extractFIDFromUserID(userID); @@ -91,7 +100,7 @@ } return usersSupportingFCDCs; }, - [auxUserInfos, findUserIdentities], + [auxUserInfos, findUserIdentities, supportsFarcasterDCs], ); } @@ -160,7 +169,8 @@ const unresolvedFIDs = new Set(farcasterIDs.map(fid => `${fid}`)); for (const [userID, auxUserInfo] of Object.entries(auxUserInfos)) { - const { fid, supportsFarcasterDCs } = auxUserInfo; + const { fid, supportsFarcasterDCs: userSupportsFarcasterDCs } = + auxUserInfo; const username = userInfos[userID]?.username; if (fid && username && unresolvedFIDs.delete(fid)) { @@ -168,7 +178,7 @@ userID, farcasterID: fid, username, - supportsFarcasterDCs: !!supportsFarcasterDCs, + supportsFarcasterDCs: !!userSupportsFarcasterDCs, }); } } diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js --- a/lib/selectors/thread-selectors.js +++ b/lib/selectors/thread-selectors.js @@ -487,19 +487,27 @@ ]; } - for (const type of possiblePendingThreadTypes) { - const pendingThreadID = getPendingThreadID( - type, - actualMemberIDs, - rawThreadInfo.sourceMessageID, - ); - const existingResult = result.get(pendingThreadID); - if ( - !existingResult || - rawThreadInfos[existingResult].creationTime > - rawThreadInfo.creationTime - ) { - result.set(pendingThreadID, threadID); + const possibleProtocols = [ + threadSpecs[rawThreadInfo.type].protocol(), + null, + ]; + + for (const protocol of possibleProtocols) { + for (const type of possiblePendingThreadTypes) { + const pendingThreadID = getPendingThreadID( + type, + actualMemberIDs, + rawThreadInfo.sourceMessageID, + protocol ? protocol.protocolName : null, + ); + const existingResult = result.get(pendingThreadID); + if ( + !existingResult || + rawThreadInfos[existingResult].creationTime > + rawThreadInfo.creationTime + ) { + result.set(pendingThreadID, threadID); + } } } } 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 @@ -4,12 +4,14 @@ import { useGetFarcasterDirectCastUsers } from './farcaster/farcaster-api.js'; import { messageID, userIDFromFID, extractFIDFromUserID } from './id-utils.js'; +import { protocolNames } from './protocol-names.js'; import SearchIndex from './search-index.js'; import { getContainingThreadID, userIsMember, userHasDeviceList, } from './thread-utils.js'; +import type { ProtocolName } from './threads/thread-spec.js'; import { threadSpecs, threadTypeIsSidebar } from './threads/thread-specs.js'; import { ensNameForFarcasterUsername } from './user-utils.js'; import { searchMessagesActionTypes } from '../actions/message-actions.js'; @@ -18,8 +20,13 @@ searchUsersActionTypes, } from '../actions/user-actions.js'; import { ENSCacheContext } from '../components/ens-cache-provider.react.js'; +import { useProtocolSelection } from '../contexts/protocol-selection-context.js'; import genesis from '../facts/genesis.js'; import { useSearchMessages as useSearchMessagesAction } from '../hooks/message-hooks.js'; +import { + useUsersSupportFarcasterDCs, + useUsersSupportThickThreads, +} from '../hooks/user-identities-hooks.js'; import { useIdentitySearch } from '../identity-search/identity-search-context.js'; import { useLegacyAshoatKeyserverCall } from '../keyserver-conn/legacy-keyserver-call.js'; import { decodeThreadRolePermissionsBitmaskArray } from '../permissions/minimally-encoded-thread-permissions.js'; @@ -126,6 +133,7 @@ }): UserListItem[] { const memoizedUserInfos = React.useMemo(() => values(userInfos), [userInfos]); const searchIndex: SearchIndex = useUserSearchIndex(memoizedUserInfos); + const { selectedProtocol } = useProtocolSelection(); const communityThreadInfo = React.useMemo( () => @@ -246,7 +254,10 @@ ]); const viewerID = useSelector(state => state.currentUserInfo?.id); - const sortedMembers = React.useMemo(() => { + const [sortedMembers, setSortedMembers] = React.useState< + Omit[], + >([]); + React.useEffect(() => { const nonFriends = []; const blockedUsers = []; const friends = []; @@ -277,7 +288,7 @@ .concat(nonFriends) .concat(blockedUsers); - return sortedResults.map( + const mappedResult = sortedResults.map( ({ isMemberOfContainingThread, isMemberOfParentThread, @@ -340,16 +351,61 @@ return result; }, ); + setSortedMembers(mappedResult); }, [ containingThreadInfo, filteredUserResults, parentThreadInfo, threadType, viewerID, + auxUserInfos, isFarcasterDCsIntegrationEnabled, ]); - return sortedMembers; + const usersSupportFarcasterDCs = useUsersSupportFarcasterDCs(); + const usersSupportThickThreads = useUsersSupportThickThreads(); + + const [potentialMembers, setPotentialMembers] = React.useState< + UserListItem[], + >([]); + React.useEffect(() => { + void (async () => { + const usersIDs = sortedMembers.map(user => user.id); + const [thickThreadUsers, farcasterUsers] = await Promise.all([ + usersSupportThickThreads(usersIDs), + usersSupportFarcasterDCs(usersIDs), + ]); + const usersWithProtocol = sortedMembers + .map(member => { + const supportedProtocols: Array = []; + if (thickThreadUsers.get(member.id)) { + supportedProtocols.push(protocolNames.COMM_DM); + } + if (farcasterUsers.get(member.id)) { + supportedProtocols.push(protocolNames.FARCASTER_DC); + } + if ( + selectedProtocol && + !supportedProtocols.includes(selectedProtocol) + ) { + return null; + } + return { + ...member, + supportedProtocols: [...supportedProtocols], + }; + }) + .filter(Boolean); + setPotentialMembers(usersWithProtocol); + })(); + }, [ + selectedProtocol, + sortedMembers, + usersSupportFarcasterDCs, + usersSupportThickThreads, + ]); + + return potentialMembers; } function useSearchMessages(): ( diff --git a/lib/shared/thread-utils.js b/lib/shared/thread-utils.js --- a/lib/shared/thread-utils.js +++ b/lib/shared/thread-utils.js @@ -13,10 +13,11 @@ import { relationshipBlockedInEitherDirection } from './relationship-utils.js'; import type { SidebarItem } from './sidebar-item-utils.js'; import { dmThreadProtocol } from './threads/protocols/dm-thread-protocol.js'; -import { farcasterThreadProtocol } from './threads/protocols/farcaster-thread-protocol.js'; -import { keyserverThreadProtocol } from './threads/protocols/keyserver-thread-protocol.js'; -import { protocols } from './threads/protocols/thread-protocols.js'; -import type { ThreadProtocol } from './threads/thread-spec.js'; +import { + getProtocolByName, + protocols, +} from './threads/protocols/thread-protocols.js'; +import type { ProtocolName } from './threads/thread-spec.js'; import { threadSpecs, threadTypeIsCommunityRoot, @@ -116,7 +117,6 @@ import { entries, values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; import { userSurfacedPermissionsFromRolePermissions } from '../utils/role-utils.js'; -import { useIsFarcasterDCsIntegrationEnabled } from '../utils/services-utils.js'; import { firstLine } from '../utils/string-utils.js'; import { pendingThreadIDRegex, @@ -393,6 +393,7 @@ threadType: ThreadType, memberIDs: $ReadOnlyArray, sourceMessageID: ?string, + protocol?: ?ProtocolName, ): string { let pendingThreadKey; if (sourceMessageID) { @@ -405,13 +406,15 @@ } const pendingThreadTypeString = sourceMessageID ? '' : `type${threadType}/`; - return `pending/${pendingThreadTypeString}${pendingThreadKey}`; + const pendingThreadProtocolString = protocol ? `/protocol_${protocol}` : ''; + return `pending/${pendingThreadTypeString}${pendingThreadKey}${pendingThreadProtocolString}`; } type PendingThreadIDContents = { +threadType: ThreadType, +memberIDs: $ReadOnlyArray, +sourceMessageID: ?string, + +protocol: ?string, }; function parsePendingThreadID( @@ -424,12 +427,20 @@ } const [threadTypeString, threadKey] = pendingThreadIDMatches[1].split('/'); + const protocolString = + pendingThreadIDMatches[pendingThreadIDMatches.length - 1]; + const threadType = protocols().find( p => p.sidebarConfig?.pendingSidebarURLPrefix === threadTypeString, )?.sidebarConfig?.sidebarThreadType ?? assertThreadType(Number(threadTypeString.replace('type', ''))); + let protocol = null; + if (protocolString && protocolString.startsWith('/protocol_')) { + protocol = protocolString.replace('/protocol_', ''); + } + const threadTypeStringIsSidebar = threadTypeIsSidebar(threadType); const memberIDs = threadTypeStringIsSidebar ? [] : threadKey.split('+'); const sourceMessageID = threadTypeStringIsSidebar ? threadKey : null; @@ -437,6 +448,7 @@ threadType, memberIDs, sourceMessageID, + protocol, }; } @@ -465,7 +477,12 @@ } const memberIDs = members.map(member => member.id); - const threadID = getPendingThreadID(threadType, memberIDs, sourceMessageID); + const threadID = getPendingThreadID( + threadType, + memberIDs, + sourceMessageID, + threadSpecs[threadType].protocol().protocolName, + ); const permissions: ThreadRolePermissionsBlob = { [threadPermissions.KNOW_OF]: true, @@ -1122,8 +1139,6 @@ type ExistingThreadInfoFinderParams = { +searching: boolean, +userInfoInputArray: $ReadOnlyArray, - +allUsersSupportThickThreads: boolean, - +allUsersSupportFarcasterThreads: boolean, }; type ExistingThreadInfoFinder = ( params: ExistingThreadInfoFinderParams, @@ -1131,9 +1146,8 @@ function useExistingThreadInfoFinder( baseThreadInfo: ?ThreadInfo, + selectedProtocol: ?ProtocolName, ): ExistingThreadInfoFinder { - const isFarcasterDCsIntegrationEnabled = - useIsFarcasterDCsIntegrationEnabled(); const threadInfos = useSelector(threadInfoSelector); const loggedInUserInfo = useLoggedInUserInfo(); @@ -1166,12 +1180,15 @@ const { sourceMessageID } = baseThreadInfo; + const protocol = getProtocolByName(selectedProtocol) ?? dmThreadProtocol; + let pendingThreadID; if (searching) { pendingThreadID = getPendingThreadID( - dmThreadProtocol.pendingThreadType(userInfoInputArray.length), + protocol.pendingThreadType(userInfoInputArray.length), [...userInfoInputArray.map(user => user.id), viewerID], sourceMessageID, + selectedProtocol, ); } else { pendingThreadID = getPendingThreadID( @@ -1189,16 +1206,6 @@ return baseThreadInfo; } - let protocol: ThreadProtocol = keyserverThreadProtocol; - if ( - params.allUsersSupportFarcasterThreads && - isFarcasterDCsIntegrationEnabled - ) { - protocol = farcasterThreadProtocol; - } else if (params.allUsersSupportThickThreads) { - protocol = dmThreadProtocol; - } - return createPendingThread({ viewerID, threadType: protocol.pendingThreadType(userInfoInputArray.length), @@ -1213,7 +1220,7 @@ threadInfos, loggedInUserInfo, pendingToRealizedThreadIDs, - isFarcasterDCsIntegrationEnabled, + selectedProtocol, ], ); } diff --git a/lib/shared/thread-utils.test.js b/lib/shared/thread-utils.test.js --- a/lib/shared/thread-utils.test.js +++ b/lib/shared/thread-utils.test.js @@ -17,6 +17,7 @@ threadType: threadTypes.SIDEBAR, memberIDs: [], sourceMessageID: '12345', + protocol: null, }; expect(parsePendingThreadID('pending/sidebar/12345')).toStrictEqual( sidebarResult, @@ -26,6 +27,7 @@ threadType: threadTypes.SIDEBAR, memberIDs: [], sourceMessageID: '789|12345', + protocol: null, }; expect(parsePendingThreadID('pending/sidebar/789|12345')).toStrictEqual( sidebarResultWithNewSchema, @@ -36,6 +38,7 @@ threadType: threadTypes.THICK_SIDEBAR, memberIDs: [], sourceMessageID: '12345', + protocol: null, }; expect(parsePendingThreadID('pending/dm_sidebar/12345')).toStrictEqual( thickSidebarResult, @@ -46,6 +49,7 @@ threadType: threadTypes.GENESIS_PERSONAL, memberIDs: ['83810', '86622'], sourceMessageID: null, + protocol: null, }; expect(parsePendingThreadID('pending/type6/83810+86622')).toStrictEqual( pendingPersonalResult, @@ -55,6 +59,7 @@ threadType: threadTypes.COMMUNITY_OPEN_SUBTHREAD, memberIDs: ['83810', '86622', '83889'], sourceMessageID: null, + protocol: null, }; expect( parsePendingThreadID('pending/type3/83810+86622+83889'), @@ -238,4 +243,37 @@ false, ); }); + + it('should parse pending thread IDs with protocol correctly', () => { + // Test parsing ID with protocol + const result = parsePendingThreadID( + 'pending/type6/83810+86622/protocol_dc', + ); + expect(result).toEqual({ + threadType: threadTypes.GENESIS_PERSONAL, + memberIDs: ['83810', '86622'], + sourceMessageID: null, + protocol: 'dc', + }); + + // Test parsing ID without protocol + const resultNoProtocol = parsePendingThreadID('pending/type6/83810+86622'); + expect(resultNoProtocol).toEqual({ + threadType: threadTypes.GENESIS_PERSONAL, + memberIDs: ['83810', '86622'], + sourceMessageID: null, + protocol: null, + }); + + // Test sidebar with protocol + const sidebarResult = parsePendingThreadID( + 'pending/sidebar/12345/protocol_dm', + ); + expect(sidebarResult).toEqual({ + threadType: threadTypes.SIDEBAR, + memberIDs: [], + sourceMessageID: '12345', + protocol: 'dm', + }); + }); }); diff --git a/lib/types/user-types.js b/lib/types/user-types.js --- a/lib/types/user-types.js +++ b/lib/types/user-types.js @@ -11,6 +11,7 @@ type UserRelationshipStatus, userRelationshipStatusValidator, } from './relationship-types.js'; +import type { ProtocolName } from '../shared/threads/thread-spec.js'; import { tBool, tShape, tUserID } from '../utils/validation-utils.js'; export type GlobalUserInfo = { @@ -124,4 +125,5 @@ +title: string, }, +avatar?: ?ClientAvatar, + +supportedProtocols: Array, }; diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -148,7 +148,7 @@ const pendingSidebarURLPrefix = 'sidebar'; const pendingThickSidebarURLPrefix = 'dm_sidebar'; -const pendingThreadIDRegex = `pending/(type[0-9]+/[0-9]+(\\+[0-9]+)*|(${pendingSidebarURLPrefix}|${pendingThickSidebarURLPrefix})/${idSchemaRegex})`; +const pendingThreadIDRegex = `pending/(type[0-9]+/[0-9]+(\\+[0-9]+)*|(${pendingSidebarURLPrefix}|${pendingThickSidebarURLPrefix})/${idSchemaRegex})(/protocol_[^/]+)?`; const chatNameMaxLength = 191; const chatNameMinLength = 0; diff --git a/native/chat/chat.react.js b/native/chat/chat.react.js --- a/native/chat/chat.react.js +++ b/native/chat/chat.react.js @@ -68,6 +68,7 @@ nuxTip, NUXTipsContext, } from '../components/nux-tips-context.react.js'; +import { ProtocolSelectionProvider } from '../components/protocol-selection-provider.react.js'; import { InputStateContext } from '../input/input-state.js'; import CommunityDrawerButton from '../navigation/community-drawer-button.react.js'; import HeaderBackButton from '../navigation/header-back-button.react.js'; @@ -458,68 +459,70 @@ const activeThreadID = activeThreadSelector(navContext); return ( - - - - - - - - - - - - - - - - - - {draftUpdater} - + + + + + + + + + + + + + + + + + + + {draftUpdater} + + ); } 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 @@ -7,12 +7,8 @@ import { Text, View } from 'react-native'; import { KeyboardAvoidingView } from 'react-native-keyboard-controller'; +import { useProtocolSelection } from 'lib/contexts/protocol-selection-context.js'; import genesis from 'lib/facts/genesis.js'; -import { - useUsersSupportFarcasterDCs, - useUsersSupportingProtocols, - useUsersSupportThickThreads, -} from 'lib/hooks/user-identities-hooks.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { useRefreshFarcasterConversation } from 'lib/shared/farcaster/farcaster-hooks.js'; @@ -270,6 +266,11 @@ const [userInfoInputArray, setUserInfoInputArray] = React.useState< $ReadOnlyArray, >([]); + const { setUserInfoInput, selectedProtocol, setSearching } = + useProtocolSelection(); + React.useEffect(() => { + setUserInfoInput?.(userInfoInputArray); + }, [setUserInfoInput, userInfoInputArray]); const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); const supportsFarcasterDCs = useIsFarcasterDCsIntegrationEnabled(); @@ -300,30 +301,26 @@ props.route.params.threadInfo, ); - const existingThreadInfoFinder = - useExistingThreadInfoFinder(baseThreadInfo); - - const checkUsersThickThreadSupport = useUsersSupportThickThreads(); - const checkUsersFarcasterDCsSupport = useUsersSupportFarcasterDCs(); - const { allUsersSupportThickThreads, allUsersSupportFarcasterThreads } = - useUsersSupportingProtocols(userInfoInputArray); + const existingThreadInfoFinder = useExistingThreadInfoFinder( + baseThreadInfo, + selectedProtocol, + ); const isSearching = !!props.route.params.searching; + React.useEffect(() => { + setSearching?.(isSearching); + return () => { + setSearching?.(false); + }; + }, [isSearching, setSearching]); + const threadInfo = React.useMemo( () => existingThreadInfoFinder({ searching: isSearching, userInfoInputArray, - allUsersSupportThickThreads, - allUsersSupportFarcasterThreads, }), - [ - allUsersSupportFarcasterThreads, - allUsersSupportThickThreads, - existingThreadInfoFinder, - isSearching, - userInfoInputArray, - ], + [existingThreadInfoFinder, isSearching, userInfoInputArray], ); invariant( threadInfo, @@ -386,24 +383,9 @@ const resolveToUser = React.useCallback( async (user: AccountUserInfo) => { const newUserInfoInputArray = user.id === viewerID ? [] : [user]; - const newUserIDs = newUserInfoInputArray.map(userInfo => userInfo.id); - const [usersSupportingThickThreads, usersSupportingFarcasterThreads] = - await Promise.all([ - checkUsersThickThreadSupport(newUserIDs), - checkUsersFarcasterDCsSupport(newUserIDs), - ]); - const resolvedThreadInfo = existingThreadInfoFinder({ searching: true, userInfoInputArray: newUserInfoInputArray, - allUsersSupportThickThreads: - user.id === viewerID - ? true - : !!usersSupportingThickThreads.get(user.id), - allUsersSupportFarcasterThreads: - user.id === viewerID - ? false - : !!usersSupportingFarcasterThreads.get(user.id), }); invariant( resolvedThreadInfo, @@ -413,14 +395,7 @@ setBaseThreadInfo(resolvedThreadInfo); setParams({ searching: false, threadInfo: resolvedThreadInfo }); }, - [ - viewerID, - checkUsersThickThreadSupport, - checkUsersFarcasterDCsSupport, - existingThreadInfoFinder, - editInputMessage, - setParams, - ], + [viewerID, existingThreadInfoFinder, editInputMessage, setParams], ); const messageListData = useNativeMessageListData({ diff --git a/native/components/protocol-selection-provider.react.js b/native/components/protocol-selection-provider.react.js new file mode 100644 --- /dev/null +++ b/native/components/protocol-selection-provider.react.js @@ -0,0 +1,106 @@ +// @flow + +import * as React from 'react'; + +import { ProtocolSelectionContext } from 'lib/contexts/protocol-selection-context.js'; +import { useUsersSupportingProtocols } from 'lib/hooks/user-identities-hooks.js'; +import { protocolNames } from 'lib/shared/protocol-names.js'; +import { threadIsPending } from 'lib/shared/thread-utils.js'; +import type { ProtocolName } from 'lib/shared/threads/thread-spec.js'; +import type { AccountUserInfo } from 'lib/types/user-types.js'; +import { useCurrentUserSupportsDCs } from 'lib/utils/farcaster-utils.js'; + +import { useActiveThread } from '../navigation/nav-selectors.js'; + +type ProtocolSelectionProviderProps = { + +children: React.Node, +}; + +function ProtocolSelectionProvider( + props: ProtocolSelectionProviderProps, +): React.Node { + const { children } = props; + + const [selectedProtocol, setSelectedProtocol] = + React.useState(null); + const [isSearching, setSearching] = React.useState(false); + const [userInfoInputArray, setUserInfoInputArray] = React.useState< + $ReadOnlyArray, + >([]); + + const activeThread = useActiveThread(); + const isThreadPending = React.useMemo( + () => activeThread && threadIsPending(activeThread), + [activeThread], + ); + + React.useEffect(() => { + if (!isSearching) { + setSelectedProtocol(null); + setUserInfoInputArray([]); + } + }, [isSearching, selectedProtocol]); + + const { allUsersSupportThickThreads, allUsersSupportFarcasterThreads } = + useUsersSupportingProtocols(userInfoInputArray); + const currentUserSupportsDCs = useCurrentUserSupportsDCs(); + + const availableProtocols = React.useMemo(() => { + const protocols: Array = []; + if ( + (userInfoInputArray.length === 0 || allUsersSupportFarcasterThreads) && + currentUserSupportsDCs + ) { + protocols.push(protocolNames.FARCASTER_DC); + } + if (userInfoInputArray.length === 0 || allUsersSupportThickThreads) { + protocols.push(protocolNames.COMM_DM); + } + return protocols.filter(protocol => protocol !== selectedProtocol); + }, [ + allUsersSupportFarcasterThreads, + allUsersSupportThickThreads, + currentUserSupportsDCs, + selectedProtocol, + userInfoInputArray.length, + ]); + + React.useEffect(() => { + if (userInfoInputArray.length === 0 || !isThreadPending) { + return; + } + if (allUsersSupportFarcasterThreads && !allUsersSupportThickThreads) { + setSelectedProtocol(protocolNames.FARCASTER_DC); + } else if ( + !allUsersSupportFarcasterThreads && + allUsersSupportThickThreads + ) { + setSelectedProtocol(protocolNames.COMM_DM); + } + }, [ + allUsersSupportFarcasterThreads, + allUsersSupportThickThreads, + isThreadPending, + selectedProtocol, + userInfoInputArray.length, + ]); + + const contextValue = React.useMemo( + () => ({ + selectedProtocol, + setSelectedProtocol, + availableProtocols, + setUserInfoInput: setUserInfoInputArray, + setSearching, + }), + [availableProtocols, selectedProtocol], + ); + + return ( + + {children} + + ); +} + +export { ProtocolSelectionProvider }; diff --git a/native/components/user-list-user.react.js b/native/components/user-list-user.react.js --- a/native/components/user-list-user.react.js +++ b/native/components/user-list-user.react.js @@ -78,7 +78,13 @@ onSelect = () => { const { userInfo } = this.props; if (!userInfo.alert) { - const { alert, notice, disabled, ...accountUserInfo } = userInfo; + const { + alert, + notice, + disabled, + supportedProtocols, + ...accountUserInfo + } = userInfo; this.props.onSelect(accountUserInfo); return; } diff --git a/web/app.react.js b/web/app.react.js --- a/web/app.react.js +++ b/web/app.react.js @@ -70,6 +70,7 @@ import LogOutIfMissingCSATHandler from './components/log-out-if-missing-csat-handler.react.js'; import NavigationArrows from './components/navigation-arrows.react.js'; import NonKeyserverActivityHandler from './components/non-keyserver-activity-handler.react.js'; +import { ProtocolSelectionProvider } from './components/protocol-selection-provider.react.js'; import MinVersionHandler from './components/version-handler.react.js'; import { olmAPI } from './crypto/olm-api.js'; import { sqliteAPI } from './database/sqlite-api.js'; @@ -400,10 +401,12 @@ css['main-content-container-column'], ); return ( -
- -
{mainContent}
-
+ +
+ +
{mainContent}
+
+
); } } diff --git a/web/chat/chat-message-list-container.react.js b/web/chat/chat-message-list-container.react.js --- a/web/chat/chat-message-list-container.react.js +++ b/web/chat/chat-message-list-container.react.js @@ -6,6 +6,7 @@ import { useDrop } from 'react-dnd'; import { NativeTypes } from 'react-dnd-html5-backend'; +import { useProtocolSelection } from 'lib/contexts/protocol-selection-context.js'; import { useRefreshFarcasterConversation } from 'lib/shared/farcaster/farcaster-hooks.js'; import { threadIsPending } from 'lib/shared/thread-utils.js'; import { threadSpecs } from 'lib/shared/threads/thread-specs.js'; @@ -32,7 +33,11 @@ const { activeChatThreadID } = props; const { isChatCreation, selectedUserInfos } = useInfosForPendingThread(); - const threadInfo = useThreadInfoForPossiblyPendingThread(activeChatThreadID); + const { selectedProtocol } = useProtocolSelection(); + const threadInfo = useThreadInfoForPossiblyPendingThread( + activeChatThreadID, + selectedProtocol, + ); invariant(threadInfo, 'ThreadInfo should be set'); const dispatch = useDispatch(); 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 @@ -7,12 +7,9 @@ import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; +import { useProtocolSelection } from 'lib/contexts/protocol-selection-context.js'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; import { useResolvableNames } from 'lib/hooks/names-cache.js'; -import { - useUsersSupportFarcasterDCs, - useUsersSupportThickThreads, -} from 'lib/hooks/user-identities-hooks.js'; import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { extractFIDFromUserID } from 'lib/shared/id-utils.js'; import { @@ -25,7 +22,8 @@ threadIsPending, useExistingThreadInfoFinder, } from 'lib/shared/thread-utils.js'; -import { threadTypes } from 'lib/types/thread-types-enum.js'; +import { dmThreadProtocol } from 'lib/shared/threads/protocols/dm-thread-protocol.js'; +import { getProtocolByName } from 'lib/shared/threads/protocols/thread-protocols.js'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { useIsFarcasterDCsIntegrationEnabled } from 'lib/utils/services-utils.js'; @@ -55,6 +53,7 @@ const [usernameInputText, setUsernameInputText] = React.useState(''); + const { selectedProtocol } = useProtocolSelection(); const dispatch = useDispatch(); const loggedInUserInfo = useLoggedInUserInfo(); @@ -91,20 +90,21 @@ const { pushModal } = useModalContext(); - const pendingPrivateThread = React.useRef( - createPendingThread({ + const pendingThread = React.useMemo(() => { + const protocol = getProtocolByName(selectedProtocol) ?? dmThreadProtocol; + + return createPendingThread({ viewerID, - threadType: threadTypes.PRIVATE, + threadType: protocol.pendingThreadType(userInfoInputArray.length), members: [loggedInUserInfo], - }), - ); + }); + }, [loggedInUserInfo, selectedProtocol, userInfoInputArray.length, viewerID]); + const existingThreadInfoFinderForCreatingThread = useExistingThreadInfoFinder( - pendingPrivateThread.current, + pendingThread, + selectedProtocol, ); - const checkUsersThickThreadSupport = useUsersSupportThickThreads(); - const checkUsersFarcasterDCsSupport = useUsersSupportFarcasterDCs(); - const onSelectUserFromSearch = React.useCallback( async (userListItem: UserListItem) => { const { alert, notice, disabled, ...user } = userListItem; @@ -121,25 +121,10 @@ username: userListItem.username, }; const newUserInfoInputArray = user.id === viewerID ? [] : [newUserInfo]; - const newUserIDs = newUserInfoInputArray.map(userInfo => userInfo.id); - - const [usersSupportingThickThreads, usersSupportingFarcasterThreads] = - await Promise.all([ - checkUsersThickThreadSupport(newUserIDs), - checkUsersFarcasterDCsSupport(newUserIDs), - ]); const threadInfo = existingThreadInfoFinderForCreatingThread({ searching: true, userInfoInputArray: newUserInfoInputArray, - allUsersSupportThickThreads: - user.id === viewerID - ? true - : !!usersSupportingThickThreads.get(user.id), - allUsersSupportFarcasterThreads: - user.id === viewerID - ? false - : !!usersSupportingFarcasterThreads.get(user.id), }); dispatch({ type: updateNavInfoActionType, @@ -163,8 +148,6 @@ [ viewerID, userInfoInputArray, - checkUsersThickThreadSupport, - checkUsersFarcasterDCsSupport, existingThreadInfoFinderForCreatingThread, dispatch, pushModal, diff --git a/web/components/protocol-selection-provider.react.js b/web/components/protocol-selection-provider.react.js new file mode 100644 --- /dev/null +++ b/web/components/protocol-selection-provider.react.js @@ -0,0 +1,111 @@ +// @flow + +import * as React from 'react'; + +import { ProtocolSelectionContext } from 'lib/contexts/protocol-selection-context.js'; +import { useUsersSupportingProtocols } from 'lib/hooks/user-identities-hooks.js'; +import { protocolNames } from 'lib/shared/protocol-names.js'; +import { threadIsPending } from 'lib/shared/thread-utils.js'; +import type { ProtocolName } from 'lib/shared/threads/thread-spec.js'; +import { useCurrentUserSupportsDCs } from 'lib/utils/farcaster-utils.js'; + +import { useSelector } from '../redux/redux-utils.js'; +import { + useInfosForPendingThread, + useThreadInfoForPossiblyPendingThread, +} from '../utils/thread-utils.js'; + +type ProtocolSelectionProviderProps = { + +children: React.Node, +}; + +function ProtocolSelectionProvider( + props: ProtocolSelectionProviderProps, +): React.Node { + const { children } = props; + + const [selectedProtocol, setSelectedProtocol] = + React.useState(null); + + const activeChatThreadID = useSelector( + state => state.navInfo.activeChatThreadID, + ); + + const threadInfo = useThreadInfoForPossiblyPendingThread( + activeChatThreadID, + selectedProtocol, + ); + const { isChatCreation, selectedUserInfos } = useInfosForPendingThread(); + + const isThreadPending = React.useMemo( + () => threadInfo && threadIsPending(threadInfo.id), + [threadInfo], + ); + + React.useEffect(() => { + if (!isChatCreation) { + setSelectedProtocol(null); + } + }, [isChatCreation]); + + const { allUsersSupportThickThreads, allUsersSupportFarcasterThreads } = + useUsersSupportingProtocols(selectedUserInfos); + const currentUserSupportsDCs = useCurrentUserSupportsDCs(); + + const availableProtocols = React.useMemo(() => { + const protocols: Array = []; + if ( + (selectedUserInfos.length === 0 || allUsersSupportFarcasterThreads) && + currentUserSupportsDCs + ) { + protocols.push(protocolNames.FARCASTER_DC); + } + if (selectedUserInfos.length === 0 || allUsersSupportThickThreads) { + protocols.push(protocolNames.COMM_DM); + } + return protocols.filter(protocol => protocol !== selectedProtocol); + }, [ + allUsersSupportFarcasterThreads, + allUsersSupportThickThreads, + currentUserSupportsDCs, + selectedProtocol, + selectedUserInfos.length, + ]); + + React.useEffect(() => { + if (selectedUserInfos.length === 0 || !isThreadPending) { + return; + } + if (allUsersSupportFarcasterThreads && !allUsersSupportThickThreads) { + setSelectedProtocol(protocolNames.FARCASTER_DC); + } else if ( + !allUsersSupportFarcasterThreads && + allUsersSupportThickThreads + ) { + setSelectedProtocol(protocolNames.COMM_DM); + } + }, [ + allUsersSupportFarcasterThreads, + allUsersSupportThickThreads, + isThreadPending, + selectedProtocol, + selectedUserInfos.length, + ]); + + const contextValue = React.useMemo( + () => ({ + selectedProtocol, + setSelectedProtocol, + availableProtocols, + }), + [availableProtocols, selectedProtocol], + ); + + return ( + + {children} + + ); +} + +export { ProtocolSelectionProvider }; diff --git a/web/navigation-panels/topbar.react.js b/web/navigation-panels/topbar.react.js --- a/web/navigation-panels/topbar.react.js +++ b/web/navigation-panels/topbar.react.js @@ -4,6 +4,7 @@ import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; +import { useProtocolSelection } from 'lib/contexts/protocol-selection-context.js'; import AppSwitcher from './app-switcher.react.js'; import NavStateInfoBar from './nav-state-info-bar.react.js'; @@ -31,7 +32,11 @@ ); const activeChatThreadID = useDrawerSelectedThreadID(); - const threadInfo = useThreadInfoForPossiblyPendingThread(activeChatThreadID); + const { selectedProtocol } = useProtocolSelection(); + const threadInfo = useThreadInfoForPossiblyPendingThread( + activeChatThreadID, + selectedProtocol, + ); return ( <> diff --git a/web/utils/thread-utils.js b/web/utils/thread-utils.js --- a/web/utils/thread-utils.js +++ b/web/utils/thread-utils.js @@ -4,14 +4,15 @@ import * as React from 'react'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; -import { useUsersSupportingProtocols } from 'lib/hooks/user-identities-hooks.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { createPendingThread, useExistingThreadInfoFinder, } from 'lib/shared/thread-utils.js'; +import { dmThreadProtocol } from 'lib/shared/threads/protocols/dm-thread-protocol.js'; +import { getProtocolByName } from 'lib/shared/threads/protocols/thread-protocols.js'; +import type { ProtocolName } from 'lib/shared/threads/thread-spec.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; -import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { AccountUserInfo } from 'lib/types/user-types.js'; import { useSelector } from '../redux/redux-utils.js'; @@ -36,35 +37,39 @@ function useThreadInfoForPossiblyPendingThread( activeChatThreadID: ?string, + selectedProtocol: ?ProtocolName, ): ?ThreadInfo { const { isChatCreation, selectedUserInfos } = useInfosForPendingThread(); const loggedInUserInfo = useLoggedInUserInfo(); invariant(loggedInUserInfo, 'loggedInUserInfo should be set'); - const pendingPrivateThread = React.useRef( - createPendingThread({ + const pendingThread = React.useMemo(() => { + const protocol = getProtocolByName(selectedProtocol) ?? dmThreadProtocol; + return createPendingThread({ viewerID: loggedInUserInfo.id, - threadType: threadTypes.PRIVATE, + threadType: protocol.pendingThreadType(1), members: [loggedInUserInfo], - }), - ); + }); + }, [loggedInUserInfo, selectedProtocol]); const newThreadID = 'pending/new_thread'; - const pendingNewThread = React.useMemo( - () => ({ + const pendingNewThread = React.useMemo(() => { + const protocol = getProtocolByName(selectedProtocol) ?? dmThreadProtocol; + return { ...createPendingThread({ viewerID: loggedInUserInfo.id, - threadType: threadTypes.PRIVATE, + threadType: protocol.pendingThreadType(1), members: [loggedInUserInfo], name: 'New thread', }), id: newThreadID, - }), - [loggedInUserInfo], - ); + }; + }, [loggedInUserInfo, selectedProtocol]); + const existingThreadInfoFinderForCreatingThread = useExistingThreadInfoFinder( - pendingPrivateThread.current, + pendingThread, + selectedProtocol, ); const baseThreadInfo = useSelector(state => { @@ -76,10 +81,10 @@ } return state.navInfo.pendingThread; }); - const existingThreadInfoFinder = useExistingThreadInfoFinder(baseThreadInfo); - - const { allUsersSupportThickThreads, allUsersSupportFarcasterThreads } = - useUsersSupportingProtocols(selectedUserInfos); + const existingThreadInfoFinder = useExistingThreadInfoFinder( + baseThreadInfo, + selectedProtocol, + ); const threadInfo = React.useMemo(() => { if (isChatCreation) { @@ -90,20 +95,14 @@ return existingThreadInfoFinderForCreatingThread({ searching: true, userInfoInputArray: selectedUserInfos, - allUsersSupportThickThreads, - allUsersSupportFarcasterThreads, }); } return existingThreadInfoFinder({ searching: false, userInfoInputArray: [], - allUsersSupportThickThreads: true, - allUsersSupportFarcasterThreads: true, }); }, [ - allUsersSupportFarcasterThreads, - allUsersSupportThickThreads, existingThreadInfoFinder, existingThreadInfoFinderForCreatingThread, isChatCreation,