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 @@ -366,6 +366,54 @@ ); } +export type CreateFarcasterGroupInput = { + +participantFids: $ReadOnlyArray, + +name: string, + +description?: string, +}; + +type CreateFarcasterGroupResultData = { + +conversationId: string, + ... +}; +const createFarcasterGroupResultDataValidator: TInterface = + tShapeInexact({ + conversationId: t.String, + }); + +export type CreateFarcasterGroupResult = { + +result: CreateFarcasterGroupResultData, + ... +}; +const createFarcasterGroupResultValidator: TInterface = + tShapeInexact({ + result: createFarcasterGroupResultDataValidator, + }); + +function useCreateFarcasterGroup(): ( + input: CreateFarcasterGroupInput, +) => Promise { + const { sendFarcasterRequest } = useTunnelbroker(); + return React.useCallback( + async (input: CreateFarcasterGroupInput) => { + const response = await sendFarcasterRequest({ + apiVersion: 'v2', + endpoint: 'direct-cast-group', + method: { type: 'PUT' }, + payload: JSON.stringify(input), + }); + + const parsedResult = JSON.parse(response); + const result: CreateFarcasterGroupResult = assertWithValidator( + parsedResult, + createFarcasterGroupResultValidator, + ); + return result; + }, + [sendFarcasterRequest], + ); +} + export { useSendFarcasterTextMessage, useFetchFarcasterMessages, @@ -375,4 +423,5 @@ useUpdateFarcasterSubscription, useStreamFarcasterDirectCastRead, useMarkFarcasterDirectCastUnread, + useCreateFarcasterGroup, }; diff --git a/lib/shared/threads/protocols/farcaster-thread-protocol.js b/lib/shared/threads/protocols/farcaster-thread-protocol.js --- a/lib/shared/threads/protocols/farcaster-thread-protocol.js +++ b/lib/shared/threads/protocols/farcaster-thread-protocol.js @@ -48,12 +48,16 @@ import { farcasterMessageValidator } from '../../farcaster/farcaster-messages-types.js'; import { conversationIDFromFarcasterThreadID, + extractFIDFromUserID, + farcasterThreadIDFromConversationID, userIDFromFID, } from '../../id-utils.js'; import { messageNotifyTypes } from '../../messages/message-spec.js'; import { getContainingThreadID, getSingleOtherUser, + threadIsPending, + threadOtherMembers, } from '../../thread-utils.js'; import type { ChangeThreadSettingsUtils, @@ -70,6 +74,7 @@ ProtocolOnOpenThreadInput, UpdateSubscriptionUtils, ProtocolCreatePendingThreadInput, + CreateRealThreadParameters, } from '../thread-spec.js'; const farcasterThreadProtocol: ThreadProtocol = { @@ -424,13 +429,60 @@ pendingThreadType, - createRealThreadFromPendingThread: async (): Promise<{ + createRealThreadFromPendingThread: async ( + params: CreateRealThreadParameters, + ): Promise<{ +threadID: string, +threadType: ThreadType, }> => { - throw new Error( - 'createRealThreadFromPendingThread method is not yet implemented', - ); + const { + threadInfo, + farcasterFetchConversation, + createFarcasterGroup, + viewerID, + auxUserStore, + } = params; + if (!threadIsPending(threadInfo.id)) { + return { + threadID: threadInfo.id, + threadType: threadInfo.type, + }; + } + + const name = + threadInfo.name ?? + threadInfo.members.map(member => member.username).join(', '); + + const otherMembersFIDs = threadOtherMembers(threadInfo.members, viewerID) + .map(member => member.id) + .map( + otherMemberID => + auxUserStore.auxUserInfos[otherMemberID]?.fid ?? + extractFIDFromUserID(otherMemberID), + ) + .filter(Boolean) + .map(id => parseInt(id, 10)); + + let input = { + participantFids: otherMembersFIDs, + name: name.substring(0, 32), + }; + if (threadInfo.description) { + input = { + ...input, + description: threadInfo.description, + }; + } + const { + result: { conversationId }, + } = await createFarcasterGroup(input); + + await farcasterFetchConversation(conversationId); + + return { + threadID: farcasterThreadIDFromConversationID(conversationId), + threadType: threadInfo.type, + }; }, getRolePermissionBlobs: (threadType: ThreadType): RolePermissionBlobs => { diff --git a/lib/shared/threads/thread-spec.js b/lib/shared/threads/thread-spec.js --- a/lib/shared/threads/thread-spec.js +++ b/lib/shared/threads/thread-spec.js @@ -31,6 +31,7 @@ SetThreadUnreadStatusPayload, SetThreadUnreadStatusRequest, } from '../../types/activity-types.js'; +import type { AuxUserStore } from '../../types/aux-user-types.js'; import type { CalendarQuery, CreateEntryInfo, @@ -96,7 +97,10 @@ UpdateFarcasterSubscriptionInput, StreamFarcasterDirectCastReadInput, MarkFarcasterDirectCastUnreadInput, + CreateFarcasterGroupResult, + CreateFarcasterGroupInput, } from '../farcaster/farcaster-api.js'; +import type { FarcasterConversation } from '../farcaster/farcaster-conversation-types.js'; import type { FetchMessagesFromDBType } from '../message-utils.js'; import type { CreationSideEffectsFunc, @@ -285,6 +289,11 @@ +viewerID: ?string, +handleError?: () => mixed, +calendarQuery: CalendarQuery, + +createFarcasterGroup: CreateFarcasterGroupInput => Promise, + +farcasterFetchConversation: ( + conversationID: string, + ) => Promise, + +auxUserStore: AuxUserStore, }; export type ProtocolDeleteMessageInput = { diff --git a/native/input/input-state-container.react.js b/native/input/input-state-container.react.js --- a/native/input/input-state-container.react.js +++ b/native/input/input-state-container.react.js @@ -38,6 +38,13 @@ combineLoadingStatuses, createLoadingStatusSelector, } from 'lib/selectors/loading-selectors.js'; +import { + useCreateFarcasterGroup, + type CreateFarcasterGroupInput, + type CreateFarcasterGroupResult, +} from 'lib/shared/farcaster/farcaster-api.js'; +import type { FarcasterConversation } from 'lib/shared/farcaster/farcaster-conversation-types.js'; +import { useFetchConversation } from 'lib/shared/farcaster/farcaster-hooks.js'; import { getNextLocalID } from 'lib/shared/id-utils.js'; import { createMediaMessageInfo } from 'lib/shared/message-utils.js'; import { @@ -49,6 +56,7 @@ threadSpecs, threadTypeIsSidebar, } from 'lib/shared/threads/thread-specs.js'; +import type { AuxUserStore } from 'lib/types/aux-user-types.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { Media, @@ -152,6 +160,13 @@ request: ClientNewThinThreadRequest, ) => Promise, +newThickThread: (request: NewThickThreadRequest) => Promise, + +createFarcasterGroup: ( + input: CreateFarcasterGroupInput, + ) => Promise, + +fetchConversation: ( + conversationID: string, + ) => Promise, + +auxUserStore: AuxUserStore, +invalidTokenLogOut: (source: string) => Promise, }; type State = { @@ -569,6 +584,9 @@ sourceMessageID: threadInfo.sourceMessageID, viewerID: this.props.viewerID, calendarQuery, + createFarcasterGroup: this.props.createFarcasterGroup, + farcasterFetchConversation: this.props.fetchConversation, + auxUserStore: this.props.auxUserStore, }); this.pendingThreadCreations.set(threadInfo.id, threadCreationPromise); } @@ -1679,6 +1697,9 @@ const callSendTextMessage = useInputStateContainerSendTextMessage(); const callNewThinThread = useNewThinThread(); const callNewThickThread = useNewThickThread(); + const callCreateFarcasterGroup = useCreateFarcasterGroup(); + const callFetchConversation = useFetchConversation(); + const auxUserStore = useSelector(state => state.auxUserStore); const dispatchActionPromise = useDispatchActionPromise(); const dispatch = useDispatch(); const mediaReportsEnabled = useIsReportEnabled('mediaReports'); @@ -1699,6 +1720,9 @@ sendTextMessage={callSendTextMessage} newThinThread={callNewThinThread} newThickThread={callNewThickThread} + createFarcasterGroup={callCreateFarcasterGroup} + fetchConversation={callFetchConversation} + auxUserStore={auxUserStore} dispatchActionPromise={dispatchActionPromise} dispatch={dispatch} staffCanSee={staffCanSee} diff --git a/web/input/input-state-container.react.js b/web/input/input-state-container.react.js --- a/web/input/input-state-container.react.js +++ b/web/input/input-state-container.react.js @@ -37,6 +37,13 @@ } from 'lib/hooks/upload-hooks.js'; import { getNextLocalUploadID } from 'lib/media/media-utils.js'; import { pendingToRealizedThreadIDsSelector } from 'lib/selectors/thread-selectors.js'; +import { + useCreateFarcasterGroup, + type CreateFarcasterGroupInput, + type CreateFarcasterGroupResult, +} from 'lib/shared/farcaster/farcaster-api.js'; +import type { FarcasterConversation } from 'lib/shared/farcaster/farcaster-conversation-types.js'; +import { useFetchConversation } from 'lib/shared/farcaster/farcaster-hooks.js'; import { getNextLocalID, localIDPrefix } from 'lib/shared/id-utils.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import type { IdentityClientContextType } from 'lib/shared/identity-client-context.js'; @@ -51,6 +58,7 @@ threadSpecs, threadTypeIsSidebar, } from 'lib/shared/threads/thread-specs.js'; +import type { AuxUserStore } from 'lib/types/aux-user-types.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { MediaMission, @@ -147,6 +155,13 @@ request: ClientNewThinThreadRequest, ) => Promise, +newThickThread: (request: NewThickThreadRequest) => Promise, + +createFarcasterGroup: ( + input: CreateFarcasterGroupInput, + ) => Promise, + +fetchConversation: ( + conversationID: string, + ) => Promise, + +auxUserStore: AuxUserStore, +pushModal: PushModal, +sendCallbacks: $ReadOnlyArray<() => mixed>, +registerSendCallback: (() => mixed) => void, @@ -591,6 +606,9 @@ sourceMessageID: threadInfo.sourceMessageID, viewerID: this.props.viewerID, calendarQuery, + createFarcasterGroup: this.props.createFarcasterGroup, + farcasterFetchConversation: this.props.fetchConversation, + auxUserStore: this.props.auxUserStore, }); this.pendingThreadCreations.set(threadInfo.id, threadCreationPromise); } @@ -1671,6 +1689,9 @@ const callSendTextMessage = useInputStateContainerSendTextMessage(); const callNewThinThread = useNewThinThread(); const callNewThickThread = useNewThickThread(); + const callCreateFarcasterGroup = useCreateFarcasterGroup(); + const callFetchConversation = useFetchConversation(); + const auxUserStore = useSelector(state => state.auxUserStore); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); @@ -1707,6 +1728,9 @@ sendTextMessage={callSendTextMessage} newThinThread={callNewThinThread} newThickThread={callNewThickThread} + createFarcasterGroup={callCreateFarcasterGroup} + fetchConversation={callFetchConversation} + auxUserStore={auxUserStore} dispatch={dispatch} dispatchActionPromise={dispatchActionPromise} pushModal={modalContext.pushModal}