diff --git a/lib/hooks/thread-hooks.js b/lib/hooks/thread-hooks.js --- a/lib/hooks/thread-hooks.js +++ b/lib/hooks/thread-hooks.js @@ -20,7 +20,9 @@ useFetchFarcasterConversation, useFetchFarcasterMessages, useUpdateFarcasterGroupNameAndDescription, + useModifyFarcasterMembershipInput, } from '../shared/farcaster/farcaster-api.js'; +import { useFetchConversation } from '../shared/farcaster/farcaster-hooks.js'; import { threadSpecs } from '../shared/threads/thread-specs.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import type { @@ -349,6 +351,9 @@ ) => Promise { const keyserverCall = useKeyserverCall(changeThreadMemberRoles); const dispatchActionPromise = useDispatchActionPromise(); + const modifyFarcasterMembership = useModifyFarcasterMembershipInput(); + const fetchConversation = useFetchConversation(); + const auxUserStore = useSelector(state => state.auxUserStore); return React.useCallback( async (input: ChangeThreadMemberRolesInput) => { const { threadInfo, memberIDs, newRole } = input; @@ -367,10 +372,19 @@ { keyserverCall, dispatchActionPromise, + modifyFarcasterMembership, + fetchConversation, + auxUserStore, }, ); }, - [dispatchActionPromise, keyserverCall], + [ + dispatchActionPromise, + keyserverCall, + modifyFarcasterMembership, + fetchConversation, + auxUserStore, + ], ); } 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 @@ -492,6 +492,55 @@ ); } +export type ModifyFarcasterMembershipInput = + | { + +conversationId: string, + +action: 'demote', + +targetFid: number, + } + | { + +conversationId: string, + +action: 'promote', + +targetFid: number, + } + | { + +conversationId: string, + +action: 'remove', + +targetFid: number, + } + | { + +conversationId: string, + +action: 'add', + +inviteCode: string, + } + | { + +conversationId: string, + +action: 'add', + +targetFid: number, + } + | { + +conversationId: string, + +action: 'add', + +targetFids: $ReadOnlyArray, + }; + +function useModifyFarcasterMembershipInput(): ( + input: ModifyFarcasterMembershipInput, +) => Promise { + const { sendFarcasterRequest } = useTunnelbroker(); + return React.useCallback( + async (input: ModifyFarcasterMembershipInput) => { + await sendFarcasterRequest({ + apiVersion: 'v2', + endpoint: 'direct-cast-group-membership', + method: { type: 'POST' }, + payload: JSON.stringify(input), + }); + }, + [sendFarcasterRequest], + ); +} + export { useSendFarcasterTextMessage, useFetchFarcasterMessages, @@ -503,4 +552,5 @@ useMarkFarcasterDirectCastUnread, useCreateFarcasterGroup, useGetFarcasterDirectCastUsers, + useModifyFarcasterMembershipInput, }; 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 @@ -3,6 +3,7 @@ import invariant from 'invariant'; import { fetchMessagesBeforeCursorActionTypes } from '../../../actions/message-actions.js'; +import { changeThreadMemberRolesActionTypes } from '../../../actions/thread-action-types.js'; import { getFarcasterRolePermissionsBlobs } from '../../../permissions/farcaster-permissions.js'; import type { RolePermissionBlobs } from '../../../permissions/thread-permissions.js'; import type { SetThreadUnreadStatusPayload } from '../../../types/activity-types.js'; @@ -45,6 +46,7 @@ import { createFarcasterRawThreadInfo } from '../../../utils/create-farcaster-raw-thread-info.js'; import { farcasterThreadIDRegExp } from '../../../utils/validation-utils.js'; import { generatePendingThreadColor } from '../../color-utils.js'; +import { type ModifyFarcasterMembershipInput } from '../../farcaster/farcaster-api.js'; import { farcasterMessageValidator } from '../../farcaster/farcaster-messages-types.js'; import { conversationIDFromFarcasterThreadID, @@ -58,6 +60,7 @@ getSingleOtherUser, threadIsPending, threadOtherMembers, + roleIsAdminRole, } from '../../thread-utils.js'; import type { ChangeThreadSettingsUtils, @@ -75,6 +78,8 @@ UpdateSubscriptionUtils, ProtocolCreatePendingThreadInput, CreateRealThreadParameters, + ProtocolChangeThreadMemberRolesInput, + ChangeThreadMemberRolesUtils, } from '../thread-spec.js'; const farcasterThreadProtocol: ThreadProtocol = { @@ -277,6 +282,67 @@ throw new Error('addThreadMembers method is not yet implemented'); }, + changeThreadMemberRoles: async ( + input: ProtocolChangeThreadMemberRolesInput, + utils: ChangeThreadMemberRolesUtils, + ): Promise => { + const { threadInfo, memberIDs, newRole } = input; + const { + modifyFarcasterMembership, + fetchConversation, + dispatchActionPromise, + auxUserStore, + } = utils; + + const conversationId = conversationIDFromFarcasterThreadID(threadInfo.id); + + // Determine if we're promoting or demoting based on role + const isPromoting = roleIsAdminRole(threadInfo.roles[newRole]); + + const promise = (async () => { + const membershipPromises = memberIDs + .map(memberID => { + const targetFid = + auxUserStore.auxUserInfos[memberID]?.fid ?? + extractFIDFromUserID(memberID); + if (targetFid) { + let modifyFarcasterMembershipInput: ModifyFarcasterMembershipInput = + { + conversationId, + action: 'demote', + targetFid: parseInt(targetFid, 10), + }; + + if (isPromoting) { + modifyFarcasterMembershipInput = { + conversationId, + action: 'promote', + targetFid: parseInt(targetFid, 10), + }; + } + + return modifyFarcasterMembership(modifyFarcasterMembershipInput); + } + return null; + }) + .filter(Boolean); + + await Promise.all(membershipPromises); + + await fetchConversation(conversationId); + + return { + threadID: threadInfo.id, + updatesResult: { newUpdates: [] }, + newMessageInfos: [], + }; + })(); + + void dispatchActionPromise(changeThreadMemberRolesActionTypes, promise); + + return await promise; + }, + updateSubscription: async ( protocolInput: ProtocolUpdateSubscriptionInput, utils: UpdateSubscriptionUtils, 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 @@ -99,6 +99,7 @@ MarkFarcasterDirectCastUnreadInput, CreateFarcasterGroupResult, CreateFarcasterGroupInput, + ModifyFarcasterMembershipInput, } from '../farcaster/farcaster-api.js'; import type { FarcasterConversation } from '../farcaster/farcaster-conversation-types.js'; import type { FetchMessagesFromDBType } from '../message-utils.js'; @@ -246,6 +247,11 @@ export type ChangeThreadMemberRolesUtils = { +keyserverCall: ProtocolChangeThreadMemberRolesInput => Promise, +dispatchActionPromise: DispatchActionPromise, + +modifyFarcasterMembership: ( + input: ModifyFarcasterMembershipInput, + ) => Promise, + +fetchConversation: (conversationId: string) => Promise, + +auxUserStore: AuxUserStore, }; export type ProtocolUpdateSubscriptionInput = {