diff --git a/lib/hooks/input-state-container-hooks.js b/lib/hooks/input-state-container-hooks.js --- a/lib/hooks/input-state-container-hooks.js +++ b/lib/hooks/input-state-container-hooks.js @@ -11,6 +11,7 @@ import { useProcessBlobHolders } from '../actions/holder-actions.js'; import { useSendComposableDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { useSendFarcasterTextMessage } from '../shared/farcaster/farcaster-api.js'; +import { useFetchConversation } from '../shared/farcaster/farcaster-hooks.js'; import { useMessageCreationSideEffectsFunc } from '../shared/message-utils.js'; import { threadSpecs } from '../shared/threads/thread-specs.js'; import { messageTypes } from '../types/message-types-enum.js'; @@ -35,8 +36,9 @@ const sendFarcasterTextMessage = useSendFarcasterTextMessage(); const sideEffectsFunction = useMessageCreationSideEffectsFunc(messageTypes.TEXT); - const currentUserFID = useCurrentUserFID(); - + const auxUserStore = useSelector(state => state.auxUserStore); + const viewerID = useSelector(state => state.currentUserInfo?.id); + const farcasterFetchConversation = useFetchConversation(); return React.useCallback( ( messageInfo: RawTextMessageInfo, @@ -56,15 +58,19 @@ sendComposableDMOperation, sideEffectsFunction, sendFarcasterTextMessage, - currentUserFID, + viewerID, + auxUserStore, + farcasterFetchConversation, }, ), [ - sendComposableDMOperation, sendKeyserverTextMessage, + sendComposableDMOperation, sideEffectsFunction, sendFarcasterTextMessage, - currentUserFID, + viewerID, + auxUserStore, + farcasterFetchConversation, ], ); } 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 @@ -29,7 +29,7 @@ +groupId: string, +message: string, } - | { +recipientFid: string, +message: string }; + | { +recipientFid: number, +message: string }; type SendFarcasterMessageResultData = { +messageId: string, 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 @@ -1,6 +1,7 @@ // @flow import invariant from 'invariant'; +import uuid from 'uuid'; import { fetchMessagesBeforeCursorActionTypes } from '../../../actions/message-actions.js'; import { @@ -20,6 +21,7 @@ } from '../../../types/entry-types.js'; import { defaultNumberPerThread, + messageTruncationStatus, type SendMessagePayload, type SendMultimediaMessagePayload, } from '../../../types/message-types.js'; @@ -46,8 +48,10 @@ ThreadJoinPayload, } from '../../../types/thread-types.js'; import { updateTypes } from '../../../types/update-types-enum.js'; +import { createFarcasterRawThreadInfoPersonal } from '../../../utils/create-farcaster-raw-thread-info.js'; import { farcasterThreadIDRegExp } from '../../../utils/validation-utils.js'; import { generatePendingThreadColor } from '../../color-utils.js'; +import { processFarcasterOpsActionType } from '../../farcaster/farcaster-actions.js'; import { type ModifyFarcasterMembershipInput } from '../../farcaster/farcaster-api.js'; import { conversationIDFromFarcasterThreadID, @@ -90,13 +94,19 @@ ProtocolSendMultimediaMessageInput, SendMultimediaMessageUtils, } from '../thread-spec.js'; +import { threadTypeIsPersonal } from '../thread-specs.js'; const farcasterThreadProtocol: ThreadProtocol = { sendTextMessage: async ( message: ProtocolSendTextMessageInput, utils: SendTextMessageUtils, ): Promise => { - const { sendFarcasterTextMessage, currentUserFID } = utils; + const { + sendFarcasterTextMessage, + viewerID, + auxUserStore, + farcasterFetchConversation, + } = utils; const { messageInfo, threadInfo } = message; const { localID } = messageInfo; invariant( @@ -108,15 +118,24 @@ let request; if (threadInfo.type === farcasterThreadTypes.FARCASTER_PERSONAL) { - const otherUser = getSingleOtherUser(threadInfo, currentUserFID); + const otherUser = getSingleOtherUser(threadInfo, viewerID); invariant( otherUser, `Farcaster 1:1 conversation should have one more member except ${ - currentUserFID ?? 'null' + viewerID ?? 'null' }`, ); + const targetFid = + auxUserStore.auxUserInfos[otherUser]?.fid ?? + extractFIDFromUserID(otherUser); + + if (!targetFid) { + throw new Error('Missing target fid'); + } + + const recipientFid = parseInt(targetFid, 10); request = { - recipientFid: otherUser, + recipientFid, message: messageInfo.text, }; } else { @@ -129,10 +148,18 @@ const result = await sendFarcasterTextMessage(request); + //TODO: add check if this was first ever message to avoid fetching + // conversation on each message + if (threadInfo.type === farcasterThreadTypes.FARCASTER_PERSONAL) { + await farcasterFetchConversation( + conversationIDFromFarcasterThreadID(threadInfo.id), + ); + } + return { localID, serverID: result.result.messageId, - threadID: messageInfo.threadID, + threadID: threadInfo.id, time: time, }; }, @@ -163,17 +190,7 @@ let request; if (threadInfo.type === farcasterThreadTypes.FARCASTER_PERSONAL) { - const otherUser = getSingleOtherUser(threadInfo, currentUserFID); - invariant( - otherUser, - `Farcaster 1:1 conversation should have one more member except ${ - currentUserFID ?? 'null' - }`, - ); - request = { - recipientFid: otherUser, - message: messageText, - }; + //TODO: re-use code from sendTextMessage } else { const conversationID = conversationIDFromFarcasterThreadID(threadInfo.id); request = { @@ -690,6 +707,7 @@ createFarcasterGroup, viewerID, auxUserStore, + dispatch, } = params; if (!threadIsPending(threadInfo.id)) { return { @@ -712,6 +730,46 @@ .filter(Boolean) .map(id => parseInt(id, 10)); + if (threadTypeIsPersonal(threadInfo.type)) { + const viewerFid = + auxUserStore.auxUserInfos[viewerID]?.fid ?? + extractFIDFromUserID(viewerID); + + const otherUserFid = otherMembersFIDs[0]; + + // We can deterministically generate a future conversation ID for 1:1 + const conversationID = [viewerFid, otherUserFid].sort().join('-'); + + // We construct thread here to make it work with ours design, + // when sending message thread is going to be fetched, + // so Farcaster remains as source of truth + dispatch({ + type: processFarcasterOpsActionType, + payload: { + rawMessageInfos: [], + updateInfos: [ + { + type: updateTypes.JOIN_THREAD, + id: uuid.v4(), + time: Date.now(), + threadInfo: createFarcasterRawThreadInfoPersonal({ + ...threadInfo, + id: farcasterThreadIDFromConversationID(conversationID), + }), + rawMessageInfos: [], + truncationStatus: messageTruncationStatus.UNCHANGED, + rawEntryInfos: [], + }, + ], + }, + }); + + return { + threadID: farcasterThreadIDFromConversationID(conversationID), + threadType: threadInfo.type, + }; + } + let input = { participantFids: otherMembersFIDs, name: name.substring(0, 32), 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 @@ -123,8 +123,12 @@ +sendKeyserverTextMessage: SendTextMessageInput => Promise, +sendComposableDMOperation: OutboundComposableDMOperationSpecification => Promise, +sendFarcasterTextMessage: SendFarcasterTextMessageInput => Promise, - +currentUserFID: ?string, + +viewerID: ?string, +sideEffectsFunction: CreationSideEffectsFunc, + +auxUserStore: AuxUserStore, + +farcasterFetchConversation: ( + conversationID: string, + ) => Promise, }; export type ProtocolSendMultimediaMessageInput = { @@ -333,6 +337,7 @@ conversationID: string, ) => Promise, +auxUserStore: AuxUserStore, + +dispatch: Dispatch, }; export type ProtocolDeleteMessageInput = { diff --git a/lib/utils/create-farcaster-raw-thread-info.js b/lib/utils/create-farcaster-raw-thread-info.js --- a/lib/utils/create-farcaster-raw-thread-info.js +++ b/lib/utils/create-farcaster-raw-thread-info.js @@ -1,6 +1,9 @@ // @flow -import { getFarcasterRolePermissionsBlobsFromConversation } from '../permissions/farcaster-permissions.js'; +import { + getFarcasterRolePermissionsBlobs, + getFarcasterRolePermissionsBlobsFromConversation, +} from '../permissions/farcaster-permissions.js'; import { specialRoles } from '../permissions/special-roles.js'; import { getAllThreadPermissions, @@ -14,6 +17,7 @@ FarcasterRawThreadInfo, RoleInfo, ThreadCurrentUserInfo, + ThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { minimallyEncodeRoleInfo, @@ -155,4 +159,95 @@ }; } -export { createFarcasterRawThreadInfo }; +//TODO: this can be merged with createFarcasterRawThreadInfo +function createFarcasterRawThreadInfoPersonal( + threadInfo: ThreadInfo, +): FarcasterRawThreadInfo { + const threadID = threadInfo.id; + const threadType = farcasterThreadTypes.FARCASTER_PERSONAL; + const permissionBlobs = getFarcasterRolePermissionsBlobs( + threadType, + false, + false, + ); + + const membersRole: RoleInfo = { + ...minimallyEncodeRoleInfo({ + id: `${threadID}/member/role`, + name: 'Members', + permissions: permissionBlobs.Members, + isDefault: true, + }), + specialRole: specialRoles.DEFAULT_ROLE, + }; + const adminsRole: ?RoleInfo = permissionBlobs.Admins + ? { + ...minimallyEncodeRoleInfo({ + id: `${threadID}/admin/role`, + name: 'Admins', + permissions: permissionBlobs.Admins, + isDefault: false, + }), + specialRole: specialRoles.ADMIN_ROLE, + } + : null; + const roles: { [id: string]: RoleInfo } = { + [membersRole.id]: membersRole, + }; + if (adminsRole) { + roles[adminsRole.id] = adminsRole; + } + + const userIDs = threadInfo.members.map(member => member.id); + + const members = userIDs.map(fid => ({ + id: fid, + isSender: true, + minimallyEncoded: true, + role: membersRole.id, + })); + + const currentUser: ThreadCurrentUserInfo = + minimallyEncodeThreadCurrentUserInfo({ + role: membersRole.id, + permissions: createPermissionsInfo( + permissionBlobs.Members, + threadID, + threadType, + ), + subscription: { + home: true, + pushNotifs: true, + }, + unread: false, + }); + + const avatar: ?ClientAvatar = null; + + //TODO: user proper name and description + //NOTE: this doesn't really matter, right after creating thread is + // going to be fetched and updated + const name = '1:1'; + const description = 'TBD'; + + return { + farcaster: true, + id: threadID, + type: threadType, + name, + avatar, + description, + color: generatePendingThreadColor(userIDs), + parentThreadID: null, + community: null, + creationTime: threadInfo.creationTime, + repliesCount: 0, + pinnedCount: threadInfo.creationTime, + minimallyEncoded: true, + members, + roles, + currentUser, + }; +} + +export { createFarcasterRawThreadInfo, createFarcasterRawThreadInfoPersonal }; 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 @@ -595,6 +595,7 @@ createFarcasterGroup: this.props.createFarcasterGroup, farcasterFetchConversation: this.props.fetchConversation, auxUserStore: this.props.auxUserStore, + dispatch: this.props.dispatch, }); this.pendingThreadCreations.set(threadInfo.id, threadCreationPromise); } 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 @@ -612,6 +612,7 @@ createFarcasterGroup: this.props.createFarcasterGroup, farcasterFetchConversation: this.props.fetchConversation, auxUserStore: this.props.auxUserStore, + dispatch: this.props.dispatch, }); this.pendingThreadCreations.set(threadInfo.id, threadCreationPromise); }