diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -9,6 +9,7 @@ getRandomDefaultEmojiAvatar, } from '../shared/avatar-utils.js'; import { getSingleOtherUser } from '../shared/thread-utils.js'; +import { type P2PMessageRecipient } from '../tunnelbroker/peer-to-peer-context.js'; import { type AuxUserInfos, type AuxUserInfo, @@ -305,11 +306,9 @@ const getAllPeerUserIDAndDeviceIDs: ( state: BaseAppState<>, -) => $ReadOnlyArray<{ +userID: string, +deviceID: string }> = createSelector( +) => $ReadOnlyArray = createSelector( (state: BaseAppState<>) => state.auxUserStore.auxUserInfos, - ( - auxUserInfos: AuxUserInfos, - ): $ReadOnlyArray<{ +userID: string, +deviceID: string }> => + (auxUserInfos: AuxUserInfos): $ReadOnlyArray => entries(auxUserInfos).flatMap( ([userID, { deviceList }]: [string, AuxUserInfo]) => deviceList?.devices.map(deviceID => ({ 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 @@ -11,8 +11,10 @@ import { useFindUserIdentities } from '../../actions/user-actions.js'; import { useLoggedInUserInfo } from '../../hooks/account-hooks.js'; import { useGetLatestMessageEdit } from '../../hooks/latest-message-edit.js'; +import { useGetAndUpdateDeviceListsForUsers } from '../../hooks/peer-list-hooks.js'; import { mergeUpdatesWithMessageInfos } from '../../reducers/message-reducer.js'; import { getAllPeerUserIDAndDeviceIDs } from '../../selectors/user-selectors.js'; +import { type P2PMessageRecipient } from '../../tunnelbroker/peer-to-peer-context.js'; import type { CreateThickRawThreadInfoInput, DMAddMembersOperation, @@ -45,6 +47,7 @@ import { getContentSigningKey } from '../../utils/crypto-utils.js'; import { useSelector } from '../../utils/redux-utils.js'; import { messageSpecs } from '../messages/message-specs.js'; +import { userSupportsThickThreads } from '../thread-utils.js'; import { updateSpecs } from '../updates/update-specs.js'; function generateMessagesToPeers( @@ -124,6 +127,55 @@ ) => Promise<$ReadOnlyArray> { const allPeerUserIDAndDeviceIDs = useSelector(getAllPeerUserIDAndDeviceIDs); const utilities = useSendDMOperationUtils(); + const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); + const getAndUpdateDeviceListsForUsers = useGetAndUpdateDeviceListsForUsers(); + + const getUsersWithoutDeviceList = React.useCallback( + (userIDs: $ReadOnlyArray) => { + const missingDeviceListsUserIDs: Array = []; + for (const userID of userIDs) { + const supportsThickThreads = userSupportsThickThreads( + userID, + auxUserInfos, + ); + if (!supportsThickThreads) { + missingDeviceListsUserIDs.push(userID); + } + } + return missingDeviceListsUserIDs; + }, + [auxUserInfos], + ); + + const getMissingPeers = React.useCallback( + async ( + userIDs: $ReadOnlyArray, + ): Promise<$ReadOnlyArray> => { + const missingDeviceListsUserIDs = getUsersWithoutDeviceList(userIDs); + if (missingDeviceListsUserIDs.length === 0) { + return []; + } + + const deviceLists = await getAndUpdateDeviceListsForUsers( + missingDeviceListsUserIDs, + true, + ); + + const updatedPeers: Array = []; + for (const userID of missingDeviceListsUserIDs) { + if (deviceLists[userID] && deviceLists[userID].devices.length > 0) { + updatedPeers.push( + ...deviceLists[userID].devices.map(deviceID => ({ + deviceID, + userID, + })), + ); + } + } + return updatedPeers; + }, + [getAndUpdateDeviceListsForUsers, getUsersWithoutDeviceList], + ); return React.useCallback( async ( @@ -141,8 +193,11 @@ peer => peer.userID === viewerID, ); } else if (recipients.type === 'some_users') { + const missingPeers = await getMissingPeers(recipients.userIDs); + const updatedPeers = [...allPeerUserIDAndDeviceIDs, ...missingPeers]; + const userIDs = new Set(recipients.userIDs); - peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => + peerUserIDAndDeviceIDs = updatedPeers.filter(peer => userIDs.has(peer.userID), ); } else if (recipients.type === 'all_thread_members') { @@ -157,8 +212,11 @@ const members = threadInfos[recipients.threadID]?.members ?? []; const memberIDs = members.map(member => member.id); + const missingPeers = await getMissingPeers(memberIDs); + const updatedPeers = [...allPeerUserIDAndDeviceIDs, ...missingPeers]; + const userIDs = new Set(memberIDs); - peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer => + peerUserIDAndDeviceIDs = updatedPeers.filter(peer => userIDs.has(peer.userID), ); } else if (recipients.type === 'some_devices') { @@ -174,7 +232,7 @@ ); return generateMessagesToPeers(operation, targetPeers); }, - [allPeerUserIDAndDeviceIDs, utilities], + [allPeerUserIDAndDeviceIDs, getMissingPeers, utilities], ); } diff --git a/lib/tunnelbroker/peer-to-peer-context.js b/lib/tunnelbroker/peer-to-peer-context.js --- a/lib/tunnelbroker/peer-to-peer-context.js +++ b/lib/tunnelbroker/peer-to-peer-context.js @@ -29,6 +29,8 @@ } from '../utils/peer-to-peer-communication-utils.js'; import { useSelector } from '../utils/redux-utils.js'; +export type P2PMessageRecipient = { +userID: string, +deviceID: string }; + type PeerToPeerContextType = { +processOutboundMessages: ( outboundMessageIDs: ?$ReadOnlyArray, @@ -41,7 +43,7 @@ }, +broadcastEphemeralMessage: ( contentPayload: string, - recipients: $ReadOnlyArray<{ +userID: string, +deviceID: string }>, + recipients: $ReadOnlyArray, authMetadata: AuthMetadata, ) => Promise, }; @@ -354,7 +356,7 @@ const broadcastEphemeralMessage = React.useCallback( async ( contentPayload: string, - recipients: $ReadOnlyArray<{ +userID: string, +deviceID: string }>, + recipients: $ReadOnlyArray, authMetadata: AuthMetadata, ) => { const { userID: thisUserID, deviceID: thisDeviceID } = authMetadata; @@ -380,8 +382,7 @@ // 2. Analyze results to retrieve all devices that need session creation // and map by userID. const userDevicesWithoutSession: { [userID: string]: Array } = {}; - const recipientsToRetry: Array<{ +userID: string, +deviceID: string }> = - []; + const recipientsToRetry: Array = []; for (const result of messagesResults) { if (result.status === 'missing_session') { recipientsToRetry.push(result.recipient); diff --git a/lib/utils/peer-to-peer-communication-utils.js b/lib/utils/peer-to-peer-communication-utils.js --- a/lib/utils/peer-to-peer-communication-utils.js +++ b/lib/utils/peer-to-peer-communication-utils.js @@ -3,6 +3,7 @@ import { getConfig } from './config.js'; import { olmSessionErrors } from './olm-utils.js'; import { type AuthMetadata } from '../shared/identity-client-context.js'; +import type { P2PMessageRecipient } from '../tunnelbroker/peer-to-peer-context.js'; import type { TunnelbrokerClientMessageToDevice } from '../tunnelbroker/tunnelbroker-context.js'; import { outboundP2PMessageStatuses, @@ -139,11 +140,11 @@ export type EphemeralEncryptAndSendMessageToPeerResult = { +status: 'success' | 'failure' | 'missing_session', - +recipient: { +userID: string, +deviceID: string }, + +recipient: P2PMessageRecipient, }; async function ephemeralEncryptAndSendMessageToPeer( contentPayload: string, - recipient: { +userID: string, +deviceID: string }, + recipient: P2PMessageRecipient, authMetadata: ?AuthMetadata, sendMessage: ( message: TunnelbrokerClientMessageToDevice,