diff --git a/lib/handlers/initial-state-sharing-handler.react.js b/lib/handlers/initial-state-sharing-handler.react.js --- a/lib/handlers/initial-state-sharing-handler.react.js +++ b/lib/handlers/initial-state-sharing-handler.react.js @@ -6,9 +6,9 @@ import { useIsLoggedInToIdentityAndAuthoritativeKeyserver } from '../hooks/account-hooks.js'; import { getOwnPeerDevices } from '../selectors/user-selectors.js'; import { dmOperationSpecificationTypes } from '../shared/dm-ops/dm-op-types.js'; -import { getCreateThickRawThreadInfoInputFromThreadInfo } from '../shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; +import { getCreateThickRawThreadInfoInputFromThreadInfo } from '../shared/threads/protocols/dm-thread-protocol.js'; import { values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; 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 @@ -14,21 +14,20 @@ type OutboundDMOperationSpecification, dmOperationSpecificationTypes, } from '../shared/dm-ops/dm-op-types.js'; -import { getCreateThickRawThreadInfoInputFromThreadInfo } from '../shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { threadSpecs } from '../shared/threads/thread-specs.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import type { DMCreateSidebarOperation, DMCreateThreadOperation, - DMJoinThreadOperation, } from '../types/dm-ops'; +import type { CalendarQuery } from '../types/entry-types.js'; import type { RawThreadInfo, ResolvedThreadInfo, ThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; -import { thickThreadTypes, threadTypes } from '../types/thread-types-enum.js'; +import { threadTypes } from '../types/thread-types-enum.js'; import type { ChangeThreadSettingsPayload, ClientNewThinThreadRequest, @@ -376,16 +375,15 @@ keyserverID, }; }; -export type UseJoinThreadInput = $ReadOnly< - | { - +thick: false, - ...ClientThreadJoinRequest, - } - | { - +thick: true, - +rawThreadInfo: RawThreadInfo, - }, ->; + +function useJoinKeyserverThread(): ClientThreadJoinRequest => Promise { + return useKeyserverCall(joinThread); +} + +export type UseJoinThreadInput = { + +rawThreadInfo: RawThreadInfo, + +calendarQuery: () => CalendarQuery, +}; function useJoinThread(): ( input: UseJoinThreadInput, @@ -394,51 +392,20 @@ const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); - const keyserverCall = useKeyserverCall(joinThread); + const keyserverCall = useJoinKeyserverThread(); return React.useCallback( - async (input: UseJoinThreadInput) => { - if (!input.thick) { - const { thick, ...rest } = input; - return await keyserverCall({ ...rest }); - } - - const { rawThreadInfo } = input; - - invariant(viewerID, 'viewerID should be set'); - invariant(rawThreadInfo.thick, 'thread must be thick'); - - const existingThreadDetails = - getCreateThickRawThreadInfoInputFromThreadInfo(rawThreadInfo); - - const op: DMJoinThreadOperation = { - type: 'join_thread', - joinerID: viewerID, - time: Date.now(), - messageID: uuid.v4(), - existingThreadDetails, - }; - - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - rawThreadInfo.type === thickThreadTypes.THICK_SIDEBAR && - rawThreadInfo.parentThreadID - ? rawThreadInfo.parentThreadID - : rawThreadInfo.id, + async (input: UseJoinThreadInput) => + threadSpecs[input.rawThreadInfo.type].protocol.joinThread( + { + rawThreadInfo: input.rawThreadInfo, + viewerID, }, - }; - - await processAndSendDMOperation(opSpecification); - return ({ - updatesResult: { newUpdates: [] }, - rawMessageInfos: [], - truncationStatuses: {}, - userInfos: [], - }: ThreadJoinPayload); - }, + { + processAndSendDMOperation, + keyserverJoinThread: keyserverCall, + calendarQuery: input.calendarQuery, + }, + ), [keyserverCall, processAndSendDMOperation, viewerID], ); } @@ -586,4 +553,5 @@ useRemoveUsersFromThread, useChangeThreadSettings, useDeleteThread, + useJoinKeyserverThread, }; diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js --- a/lib/shared/community-utils.js +++ b/lib/shared/community-utils.js @@ -2,7 +2,10 @@ import * as React from 'react'; +import type { KeyserverOverride } from './invite-links.js'; +import { useIsKeyserverURLValid } from './keyserver-utils.js'; import { useThreadHasPermission } from './thread-utils.js'; +import { permissionsAndAuthRelatedRequestTimeout } from './timeouts.js'; import { createOrUpdateFarcasterChannelTagActionTypes, useCreateOrUpdateFarcasterChannelTag, @@ -12,13 +15,10 @@ import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { joinThreadActionTypes } from '../actions/thread-action-types.js'; import type { LinkStatus } from '../hooks/invite-links.js'; -import { useJoinThread } from '../hooks/thread-hooks.js'; +import { useJoinKeyserverThread } from '../hooks/thread-hooks.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; import { isLoggedInToKeyserver } from '../selectors/user-selectors.js'; -import type { KeyserverOverride } from '../shared/invite-links.js'; -import { useIsKeyserverURLValid } from '../shared/keyserver-utils.js'; -import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import type { JoinCommunityStep, OngoingJoinCommunityData, @@ -208,11 +208,8 @@ const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); - const rawThreadInfo = useSelector(state => - threadID ? state.threadStore.threadInfos[threadID] : null, - ); - const callJoinThread = useJoinThread(); + const joinThread = useJoinKeyserverThread(); let keyserverID = keyserverOverride?.keyserverID; if (!keyserverID && communityID) { @@ -340,9 +337,7 @@ } const communityThreadID = ongoingJoinData.communityID; const query = calendarQuery(); - const joinThreadPromise = callJoinThread({ - thick: false, - + const joinThreadPromise = joinThread({ threadID: communityThreadID, calendarQuery: { startDate: query.startDate, @@ -377,7 +372,7 @@ })(); }, [ calendarQuery, - callJoinThread, + joinThread, defaultSubscription, dispatchActionPromise, inviteSecret, @@ -401,29 +396,19 @@ } const query = calendarQuery(); - let joinThreadInput; - if (rawThreadInfo && rawThreadInfo.thick) { - joinThreadInput = { - thick: true, - rawThreadInfo, - }; - } else { - joinThreadInput = { - thick: false, - - threadID, - calendarQuery: { - startDate: query.startDate, - endDate: query.endDate, - filters: [ - ...query.filters, - { type: 'threads', threadIDs: [threadID] }, - ], - }, - }; - } + const request = { + threadID, + calendarQuery: { + startDate: query.startDate, + endDate: query.endDate, + filters: [ + ...query.filters, + { type: 'threads', threadIDs: [threadID] }, + ], + }, + }; - const joinThreadPromise = callJoinThread(joinThreadInput); + const joinThreadPromise = joinThread(request); void dispatchActionPromise(joinThreadActionTypes, joinThreadPromise); try { @@ -444,9 +429,8 @@ } })(); }, [ - rawThreadInfo, calendarQuery, - callJoinThread, + joinThread, dispatchActionPromise, inviteSecret, ongoingJoinData, diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js --- a/lib/shared/dm-ops/dm-op-utils.js +++ b/lib/shared/dm-ops/dm-op-utils.js @@ -26,24 +26,17 @@ import { getAllPeerUserIDAndDeviceIDs } from '../../selectors/user-selectors.js'; import { type P2PMessageRecipient } from '../../tunnelbroker/peer-to-peer-context.js'; import type { - CreateThickRawThreadInfoInput, DMAddMembersOperation, DMAddViewerToThreadMembersOperation, DMOperation, } from '../../types/dm-ops.js'; import type { RawMessageInfo } from '../../types/message-types.js'; -import type { - ThickRawThreadInfo, - ThreadInfo, -} from '../../types/minimally-encoded-thread-permissions-types.js'; +import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import { outboundP2PMessageStatuses, type OutboundP2PMessage, } from '../../types/sqlite-types.js'; -import { - assertThickThreadType, - thickThreadTypes, -} from '../../types/thread-types-enum.js'; +import { thickThreadTypes } from '../../types/thread-types-enum.js'; import type { RawThreadInfos } from '../../types/thread-types.js'; import { type DMOperationP2PMessage, @@ -66,6 +59,7 @@ userHasDeviceList, deviceListCanBeRequestedForUser, } from '../thread-utils.js'; +import { getCreateThickRawThreadInfoInputFromThreadInfo } from '../threads/protocols/dm-thread-protocol.js'; function generateMessagesToPeers( message: DMOperation, @@ -260,36 +254,6 @@ ); } -function getCreateThickRawThreadInfoInputFromThreadInfo( - threadInfo: ThickRawThreadInfo, -): CreateThickRawThreadInfoInput { - const roleID = Object.keys(threadInfo.roles).pop(); - const thickThreadType = assertThickThreadType(threadInfo.type); - return { - threadID: threadInfo.id, - threadType: thickThreadType, - creationTime: threadInfo.creationTime, - parentThreadID: threadInfo.parentThreadID, - allMemberIDsWithSubscriptions: threadInfo.members.map( - ({ id, subscription }) => ({ - id, - subscription, - }), - ), - roleID, - unread: !!threadInfo.currentUser.unread, - name: threadInfo.name, - avatar: threadInfo.avatar, - description: threadInfo.description, - color: threadInfo.color, - containingThreadID: threadInfo.containingThreadID, - sourceMessageID: threadInfo.sourceMessageID, - repliesCount: threadInfo.repliesCount, - pinnedCount: threadInfo.pinnedCount, - timestamps: threadInfo.timestamps, - }; -} - function useAddDMThreadMembers(): ( newMemberIDs: $ReadOnlyArray, threadInfo: ThreadInfo, @@ -487,7 +451,6 @@ export { useCreateMessagesToPeersFromDMOp, useAddDMThreadMembers, - getCreateThickRawThreadInfoInputFromThreadInfo, getThreadUpdatesForNewMessages, useSendDMOperationUtils, }; diff --git a/lib/shared/threads/protocols/dm-thread-protocol.js b/lib/shared/threads/protocols/dm-thread-protocol.js --- a/lib/shared/threads/protocols/dm-thread-protocol.js +++ b/lib/shared/threads/protocols/dm-thread-protocol.js @@ -7,12 +7,14 @@ import { getThickThreadRolePermissionsBlob } from '../../../permissions/dm-permissions.js'; import type { TunnelbrokerSocketState } from '../../../tunnelbroker/tunnelbroker-context.js'; import type { + CreateThickRawThreadInfoInput, DMChangeThreadReadStatusOperation, DMChangeThreadSettingsOperation, DMChangeThreadSubscriptionOperation, DMCreateEntryOperation, DMDeleteEntryOperation, DMEditEntryOperation, + DMJoinThreadOperation, DMLeaveThreadOperation, DMSendDeleteMessageOperation, DMSendEditMessageOperation, @@ -41,6 +43,7 @@ ChangeThreadSettingsPayload, ClientDBThreadInfo, ThickMemberInfo, + ThreadJoinPayload, } from '../../../types/thread-types.js'; import { threadTimestampsValidator } from '../../../types/thread-types.js'; import { dateString as stringFromDate } from '../../../utils/date-utils.js'; @@ -93,6 +96,8 @@ CreateRealThreadParameters, DeleteMessageUtils, ProtocolDeleteMessageInput, + JoinThreadUtils, + ProtocolJoinThreadInput, } from '../thread-spec.js'; const dmThreadProtocol: ThreadProtocol = @@ -834,6 +839,48 @@ await utils.processAndSendDMOperation(opSpecification); }, + joinThread: async ( + input: ProtocolJoinThreadInput, + utils: JoinThreadUtils, + ) => { + const { viewerID, rawThreadInfo } = input; + + invariant(viewerID, 'viewerID should be set'); + invariant(rawThreadInfo && rawThreadInfo.thick, 'thread must be thick'); + + const existingThreadDetails = + getCreateThickRawThreadInfoInputFromThreadInfo(rawThreadInfo); + + const op: DMJoinThreadOperation = { + type: 'join_thread', + joinerID: viewerID, + time: Date.now(), + messageID: uuid.v4(), + existingThreadDetails, + }; + + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + rawThreadInfo.type === thickThreadTypes.THICK_SIDEBAR && + rawThreadInfo.parentThreadID + ? rawThreadInfo.parentThreadID + : rawThreadInfo.id, + }, + }; + + await utils.processAndSendDMOperation(opSpecification); + return ({ + updatesResult: { newUpdates: [] }, + rawMessageInfos: [], + truncationStatuses: {}, + userInfos: [], + }: ThreadJoinPayload); + }, + allowsDeletingSidebarSource: false, presentationDetails: { @@ -874,4 +921,34 @@ } } -export { dmThreadProtocol }; +function getCreateThickRawThreadInfoInputFromThreadInfo( + threadInfo: ThickRawThreadInfo, +): CreateThickRawThreadInfoInput { + const roleID = Object.keys(threadInfo.roles).pop(); + const thickThreadType = assertThickThreadType(threadInfo.type); + return { + threadID: threadInfo.id, + threadType: thickThreadType, + creationTime: threadInfo.creationTime, + parentThreadID: threadInfo.parentThreadID, + allMemberIDsWithSubscriptions: threadInfo.members.map( + ({ id, subscription }) => ({ + id, + subscription, + }), + ), + roleID, + unread: !!threadInfo.currentUser.unread, + name: threadInfo.name, + avatar: threadInfo.avatar, + description: threadInfo.description, + color: threadInfo.color, + containingThreadID: threadInfo.containingThreadID, + sourceMessageID: threadInfo.sourceMessageID, + repliesCount: threadInfo.repliesCount, + pinnedCount: threadInfo.pinnedCount, + timestamps: threadInfo.timestamps, + }; +} + +export { dmThreadProtocol, getCreateThickRawThreadInfoInputFromThreadInfo }; diff --git a/lib/shared/threads/protocols/keyserver-thread-protocol.js b/lib/shared/threads/protocols/keyserver-thread-protocol.js --- a/lib/shared/threads/protocols/keyserver-thread-protocol.js +++ b/lib/shared/threads/protocols/keyserver-thread-protocol.js @@ -110,6 +110,8 @@ CreateRealThreadParameters, DeleteMessageUtils, ProtocolDeleteMessageInput, + JoinThreadUtils, + ProtocolJoinThreadInput, } from '../thread-spec.js'; import { threadTypeIsSidebar } from '../thread-specs.js'; @@ -611,6 +613,27 @@ await promise; }, + joinThread: async ( + input: ProtocolJoinThreadInput, + utils: JoinThreadUtils, + ) => { + const { rawThreadInfo } = input; + const query = utils.calendarQuery(); + const request = { + threadID: rawThreadInfo.id, + calendarQuery: { + startDate: query.startDate, + endDate: query.endDate, + filters: [ + ...query.filters, + { type: 'threads', threadIDs: [rawThreadInfo.id] }, + ], + }, + }; + invariant(request, 'request should be provided'); + return await utils.keyserverJoinThread(request); + }, + allowsDeletingSidebarSource: true, presentationDetails: { 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 @@ -72,9 +72,11 @@ ChangeThreadSettingsPayload, ClientDBThreadInfo, ClientNewThinThreadRequest, + ClientThreadJoinRequest, LeaveThreadPayload, NewThickThreadRequest, NewThreadResult, + ThreadJoinPayload, UpdateThreadRequest, } from '../../types/thread-types.js'; import type { DispatchActionPromise } from '../../utils/redux-promise-utils.js'; @@ -268,6 +270,16 @@ +dispatchActionPromise: DispatchActionPromise, }; +export type ProtocolJoinThreadInput = { + +rawThreadInfo: RawThreadInfo, + +viewerID: ?string, +}; +export type JoinThreadUtils = { + +processAndSendDMOperation: OutboundDMOperationSpecification => Promise, + +keyserverJoinThread: ClientThreadJoinRequest => Promise, + +calendarQuery: () => CalendarQuery, +}; + export type ThreadProtocol< RawThreadMemberType: | MemberInfoSansPermissions @@ -355,6 +367,10 @@ input: ProtocolDeleteMessageInput, utils: DeleteMessageUtils, ) => Promise, + +joinThread: ( + input: ProtocolJoinThreadInput, + utils: JoinThreadUtils, + ) => Promise, +allowsDeletingSidebarSource: boolean, +presentationDetails: { +membershipChangesShownInThreadPreview: boolean, diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -67,7 +67,6 @@ import type { MessageInfo } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; -import type { ThreadJoinPayload } from 'lib/types/thread-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; @@ -842,37 +841,17 @@ onNavigationFocus, ]); - const callJoinThread = useJoinThread(); - - const joinAction = React.useCallback(async (): Promise => { - let joinThreadInput; - if (rawThreadInfo.thick) { - joinThreadInput = { - thick: true, - rawThreadInfo: rawThreadInfo, - }; - } else { - const query = calendarQuery(); - joinThreadInput = { - thick: false, - threadID: threadInfo.id, - calendarQuery: { - startDate: query.startDate, - endDate: query.endDate, - filters: [ - ...query.filters, - { type: 'threads', threadIDs: [threadInfo.id] }, - ], - }, - }; - } - - return await callJoinThread(joinThreadInput); - }, [calendarQuery, callJoinThread, threadInfo.id, rawThreadInfo]); + const joinThread = useJoinThread(); const onPressJoin = React.useCallback(() => { - void dispatchActionPromise(joinThreadActionTypes, joinAction()); - }, [dispatchActionPromise, joinAction]); + void dispatchActionPromise( + joinThreadActionTypes, + joinThread({ + rawThreadInfo, + calendarQuery, + }), + ); + }, [calendarQuery, dispatchActionPromise, joinThread, rawThreadInfo]); const setIOSKeyboardHeight = React.useCallback(() => { if (Platform.OS !== 'ios') { diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -538,29 +538,10 @@ }; async joinAction(): Promise { - let joinThreadInput; - if (this.props.rawThreadInfo.thick) { - joinThreadInput = { - thick: true, - rawThreadInfo: this.props.rawThreadInfo, - }; - } else { - const query = this.props.calendarQuery(); - joinThreadInput = { - thick: false, - threadID: this.props.threadInfo.id, - calendarQuery: { - startDate: query.startDate, - endDate: query.endDate, - filters: [ - ...query.filters, - { type: 'threads', threadIDs: [this.props.threadInfo.id] }, - ], - }, - }; - } - - return await this.props.joinThread(joinThreadInput); + return await this.props.joinThread({ + rawThreadInfo: this.props.rawThreadInfo, + calendarQuery: this.props.calendarQuery, + }); } }