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 @@ -10,6 +10,7 @@ import { useMediaMetadataReassignment } from './upload-hooks.js'; 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 { useMessageCreationSideEffectsFunc } from '../shared/message-utils.js'; import { threadSpecs } from '../shared/threads/thread-specs.js'; import { messageTypes } from '../types/message-types-enum.js'; @@ -20,6 +21,7 @@ } from '../types/message-types.js'; import type { RawTextMessageInfo } from '../types/messages/text.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import { useCurrentUserFID } from '../utils/farcaster-utils.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; function useInputStateContainerSendTextMessage(): ( @@ -30,8 +32,10 @@ ) => Promise { const sendKeyserverTextMessage = useSendTextMessage(); const sendComposableDMOperation = useSendComposableDMOperation(); + const sendFarcasterTextMessage = useSendFarcasterTextMessage(); const sideEffectsFunction = useMessageCreationSideEffectsFunc(messageTypes.TEXT); + const currentUserFID = useCurrentUserFID(); return React.useCallback( ( @@ -51,9 +55,17 @@ sendKeyserverTextMessage, sendComposableDMOperation, sideEffectsFunction, + sendFarcasterTextMessage, + currentUserFID, }, ), - [sendComposableDMOperation, sendKeyserverTextMessage, sideEffectsFunction], + [ + sendComposableDMOperation, + sendKeyserverTextMessage, + sideEffectsFunction, + sendFarcasterTextMessage, + currentUserFID, + ], ); } 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 @@ -36,7 +36,7 @@ messageId: t.String, }); -type SendFarcasterMessageResult = { +export type SendFarcasterMessageResult = { +result: SendFarcasterMessageResultData, ... }; diff --git a/lib/shared/id-utils.js b/lib/shared/id-utils.js --- a/lib/shared/id-utils.js +++ b/lib/shared/id-utils.js @@ -138,6 +138,16 @@ return localID.replace(localIDPrefix, ''); } +const farcasterIDPrefix = 'FARCASTER#'; + +function farcasterThreadIDFromConversationID(conversationID: string): string { + return `${farcasterIDPrefix}${conversationID}`; +} + +function conversationIDFromFarcasterThreadID(threadID: string): string { + return threadID.replace(farcasterIDPrefix, ''); +} + export { getNextLocalID, getOldestNonLocalMessageID, @@ -149,4 +159,7 @@ messageKey, localIDPrefix, getIDFromLocalID, + farcasterIDPrefix, + farcasterThreadIDFromConversationID, + conversationIDFromFarcasterThreadID, }; 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,5 +1,7 @@ // @flow +import invariant from 'invariant'; + import { getFarcasterRolePermissionsBlobs } from '../../../permissions/farcaster-permissions.js'; import type { RolePermissionBlobs } from '../../../permissions/thread-permissions.js'; import type { SetThreadUnreadStatusPayload } from '../../../types/activity-types.js'; @@ -32,12 +34,59 @@ ThreadJoinPayload, } from '../../../types/thread-types.js'; import { farcasterThreadIDRegExp } from '../../../utils/validation-utils.js'; +import { conversationIDFromFarcasterThreadID } from '../../id-utils.js'; import { messageNotifyTypes } from '../../messages/message-spec.js'; -import type { ThreadProtocol } from '../thread-spec.js'; +import { getSingleOtherUser } from '../../thread-utils.js'; +import type { + ProtocolSendTextMessageInput, + SendTextMessageUtils, + ThreadProtocol, +} from '../thread-spec.js'; const farcasterThreadProtocol: ThreadProtocol = { - sendTextMessage: async (): Promise => { - throw new Error('sendTextMessage method is not yet implemented'); + sendTextMessage: async ( + message: ProtocolSendTextMessageInput, + utils: SendTextMessageUtils, + ): Promise => { + const { sendFarcasterTextMessage, currentUserFID } = utils; + const { messageInfo, threadInfo } = message; + const { localID } = messageInfo; + invariant( + localID !== null && localID !== undefined, + 'localID should be set', + ); + + const time = Date.now(); + + 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: messageInfo.text, + }; + } else { + const conversationID = conversationIDFromFarcasterThreadID(threadInfo.id); + request = { + groupId: conversationID, + message: messageInfo.text, + }; + } + + const result = await sendFarcasterTextMessage(request); + + return { + localID, + serverID: result.result.messageId, + threadID: messageInfo.threadID, + time: time, + }; }, sendMultimediaMessage: async (): Promise => { 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 @@ -84,6 +84,10 @@ OutboundComposableDMOperationSpecification, OutboundDMOperationSpecification, } from '../dm-ops/dm-op-types.js'; +import type { + SendFarcasterMessageResult, + SendFarcasterTextMessageInput, +} from '../farcaster/farcaster-api.js'; import type { FetchThickMessagesType } from '../message-utils.js'; import type { CreationSideEffectsFunc, @@ -108,6 +112,8 @@ export type SendTextMessageUtils = { +sendKeyserverTextMessage: SendTextMessageInput => Promise, +sendComposableDMOperation: OutboundComposableDMOperationSpecification => Promise, + +sendFarcasterTextMessage: SendFarcasterTextMessageInput => Promise, + +currentUserFID: ?string, +sideEffectsFunction: CreationSideEffectsFunc, }; diff --git a/lib/utils/farcaster-utils.js b/lib/utils/farcaster-utils.js --- a/lib/utils/farcaster-utils.js +++ b/lib/utils/farcaster-utils.js @@ -7,7 +7,6 @@ import { getConfig } from './config.js'; import { getContentSigningKey } from './crypto-utils.js'; import { useSelector, useDispatch } from './redux-utils.js'; -import { farcasterIDPrefix } from './validation-utils.js'; import { setSyncedMetadataEntryActionType } from '../actions/synced-metadata-actions.js'; import { useUserIdentityCache } from '../components/user-identity-cache.react.js'; import { getFarcasterRolePermissionsBlobsFromConversation } from '../permissions/farcaster-permissions.js'; @@ -20,6 +19,7 @@ import { generatePendingThreadColor } from '../shared/color-utils.js'; import type { FarcasterConversation } from '../shared/farcaster/farcaster-conversation-types.js'; import type { FarcasterMessage } from '../shared/farcaster/farcaster-messages-types.js'; +import { farcasterThreadIDFromConversationID } from '../shared/id-utils.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { PeerToPeerContext } from '../tunnelbroker/peer-to-peer-context.js'; import type { ClientAvatar } from '../types/avatar-types.js'; @@ -269,10 +269,6 @@ ); } -function farcasterThreadIDFromConversationID(conversationID: string): string { - return `${farcasterIDPrefix}${conversationID}`; -} - function createPermissionsInfo( permissionsBlob: ThreadRolePermissionsBlob, threadID: string, @@ -329,14 +325,14 @@ .map(p => `${p.fid}`); const adminIDs = new Set(conversation.adminFids.map(fid => `${fid}`)); - const members = userIDs.map(id => ({ - id, + const members = userIDs.map(fid => ({ + id: fid, // This flag was introduced for sidebars to show who replied to a thread. // Now it doesn't seem to be used anywhere. Regardless, for Farcaster // threads its value doesn't matter. isSender: true, minimallyEncoded: true, - role: adminIDs.has(id) && adminsRole ? adminsRole.id : membersRole.id, + role: adminIDs.has(fid) && adminsRole ? adminsRole.id : membersRole.id, })); const currentUserRole = 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 @@ -17,6 +17,7 @@ oldValidUsernameRegex, validHexColorRegex, } from '../shared/account-utils.js'; +import { farcasterIDPrefix } from '../shared/id-utils.js'; import type { PlatformDetails } from '../types/device-types'; import type { MediaMessageServerDBContent, @@ -125,7 +126,6 @@ '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; const thickIDRegExp: RegExp = new RegExp(`^${uuidRegex}$`); -const farcasterIDPrefix = 'FARCASTER#'; const farcasterThreadIDRegex = `${farcasterIDPrefix}(?:(?:[0-9a-z]+)|(?:[0-9]+-[0-9]+))`; const farcasterThreadIDRegExp: RegExp = new RegExp( `^${farcasterThreadIDRegex}$`, @@ -189,5 +189,4 @@ idSchemaRegExp, farcasterThreadIDRegExp, isKeyserverThreadID, - farcasterIDPrefix, };