From 7b0587876fb5081993ac18500de0166983e70a0c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Nov 2021 10:37:40 +1100 Subject: [PATCH 01/13] opengroup messages from blocked user are dropped Fixes #2019 --- ts/opengroup/opengroupV2/ApiUtil.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/opengroup/opengroupV2/ApiUtil.ts b/ts/opengroup/opengroupV2/ApiUtil.ts index 56993469d..bb345ce1f 100644 --- a/ts/opengroup/opengroupV2/ApiUtil.ts +++ b/ts/opengroup/opengroupV2/ApiUtil.ts @@ -3,6 +3,7 @@ import { FileServerV2Request } from '../../fileserver/FileServerApiV2'; import { PubKey } from '../../session/types'; import { allowOnlyOneAtATime } from '../../session/utils/Promise'; import { updateDefaultRooms, updateDefaultRoomsInProgress } from '../../state/ducks/defaultRooms'; +import { BlockedNumberController } from '../../util'; import { getCompleteUrlFromRoom } from '../utils/OpenGroupUtils'; import { parseOpenGroupV2 } from './JoinOpenGroupV2'; import { getAllRoomInfos } from './OpenGroupAPIV2'; @@ -100,7 +101,11 @@ export const parseMessages = async ( } } - return _.compact(parsedMessages).sort((a, b) => (a.serverId || 0) - (b.serverId || 0)); + return _.compact( + parsedMessages.map(m => + m && m.sender && !BlockedNumberController.isBlocked(m.sender) ? m : null + ) + ).sort((a, b) => (a.serverId || 0) - (b.serverId || 0)); }; // tslint:disable: no-http-string const defaultServerUrl = 'http://116.203.70.33'; From c1471426acd45159f536f20ee4d157da41645229 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Nov 2021 16:01:33 +1100 Subject: [PATCH 02/13] dismiss a call when answered from another of our devices --- ts/receiver/callMessage.ts | 17 ++ ts/session/sending/MessageSentHandler.ts | 11 +- ts/session/snode_api/swarmPolling.ts | 2 - ts/session/utils/CallManager.ts | 195 ++++++++++++++++------- 4 files changed, 161 insertions(+), 64 deletions(-) diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index bec8f2f6b..d7ef83232 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -17,6 +17,23 @@ export async function handleCallMessage( const { type } = callMessage; + // we just allow self send of ANSWER message to remove the incoming call dialog when we accepted it from another device + if ( + sender === UserUtils.getOurPubKeyStrFromCache() && + callMessage.type !== SignalService.CallMessage.Type.ANSWER + ) { + window.log.info('Dropping incoming call from ourself'); + await removeFromCache(envelope); + return; + } + + if (CallManager.isCallRejected(callMessage.uuid)) { + await removeFromCache(envelope); + + window.log.info(`Dropping already rejected call ${callMessage.uuid}`); + return; + } + if (type === SignalService.CallMessage.Type.PROVISIONAL_ANSWER) { await removeFromCache(envelope); diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 666e66d57..9d78abd10 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -1,13 +1,11 @@ import _ from 'lodash'; import { getMessageById } from '../../data/data'; -import { MessageModel } from '../../models/message'; import { SignalService } from '../../protobuf'; import { PnServer } from '../../pushnotification'; import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { EncryptionType, RawMessage } from '../types'; import { UserUtils } from '../utils'; -// tslint:disable-next-line no-unnecessary-class export class MessageSentHandler { public static async handlePublicMessageSentSuccess( sentMessage: OpenGroupVisibleMessage, @@ -54,10 +52,8 @@ export class MessageSentHandler { let sentTo = fetchedMessage.get('sent_to') || []; - let isOurDevice = false; - if (sentMessage.device) { - isOurDevice = UserUtils.isUsFromCache(sentMessage.device); - } + const isOurDevice = UserUtils.isUsFromCache(sentMessage.device); + // FIXME this is not correct and will cause issues with syncing // At this point the only way to check for medium // group is by comparing the encryption type @@ -113,8 +109,9 @@ export class MessageSentHandler { window?.log?.warn( 'Got an error while trying to sendSyncMessage(): fetchedMessage is null' ); + return; } - fetchedMessage = tempFetchMessage as MessageModel; + fetchedMessage = tempFetchMessage; } catch (e) { window?.log?.warn('Got an error while trying to sendSyncMessage():', e); } diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index 437c32333..85aab1156 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -318,7 +318,6 @@ export class SwarmPolling { } private loadGroupIds() { - // Start polling for medium size groups as well (they might be in different swarms) const convos = getConversationController().getConversations(); const mediumGroupsOnly = convos.filter( @@ -328,7 +327,6 @@ export class SwarmPolling { mediumGroupsOnly.forEach((c: any) => { this.addGroupId(new PubKey(c.id)); - // TODO: unsubscribe if the group is deleted }); } diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 56f613ed6..d639358e3 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { MessageUtils, ToastUtils } from '.'; +import { MessageUtils, ToastUtils, UserUtils } from '.'; import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings'; import { getConversationById } from '../../data/data'; import { ConversationModel } from '../../models/conversation'; @@ -28,6 +28,8 @@ export type InputItem = { deviceId: string; label: string }; let currentCallUUID: string | undefined; +const rejectedCallUUIDS: Set = new Set(); + export type CallManagerOptionsType = { localStream: MediaStream | null; remoteStream: MediaStream | null; @@ -80,7 +82,7 @@ export function removeVideoEventsListener(uniqueId: string) { } /** - * This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per device cache. + * This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per call cache. */ const callCache = new Map>>(); @@ -203,7 +205,15 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { if (!peerConnection) { throw new Error('cannot selectCameraByDeviceId without a peer connection'); } - const sender = peerConnection.getSenders().find(s => { + let sender = peerConnection.getSenders().find(s => { + return s.track?.kind === videoTrack.kind; + }); + + // video might be completely off + if (!sender) { + peerConnection.addTrack(videoTrack); + } + sender = peerConnection.getSenders().find(s => { return s.track?.kind === videoTrack.kind; }); if (sender) { @@ -217,8 +227,6 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { sendVideoStatusViaDataChannel(); callVideoListeners(); - } else { - throw new Error('Failed to get sender for selectCameraByDeviceId '); } } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); @@ -313,7 +321,7 @@ async function handleNegotiationNeededEvent(recipient: string) { uuid: currentCallUUID, }); - window.log.info('sending OFFER MESSAGE'); + window.log.info(`sending OFFER MESSAGE with callUUID: ${currentCallUUID}`); const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( PubKey.cast(recipient), offerMessage @@ -340,10 +348,7 @@ function handleIceCandidates(event: RTCPeerConnectionIceEvent, pubkey: string) { async function openMediaDevicesAndAddTracks() { try { await updateConnectedDevices(); - if (!camerasList.length) { - ToastUtils.pushNoCameraFound(); - return; - } + if (!audioInputsList.length) { ToastUtils.pushNoAudioInputFound(); return; @@ -352,34 +357,32 @@ async function openMediaDevicesAndAddTracks() { selectedAudioInputId = audioInputsList[0].deviceId; selectedCameraId = DEVICE_DISABLED_DEVICE_ID; window.log.info( - `openMediaDevices videoDevice:${selectedCameraId}:${camerasList[0].label} audioDevice:${selectedAudioInputId}` + `openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}` ); const devicesConfig = { audio: { - deviceId: selectedAudioInputId, + deviceId: { exact: selectedAudioInputId }, echoCancellation: true, }, - video: { - deviceId: selectedCameraId, - // width: VIDEO_WIDTH, - // height: Math.floor(VIDEO_WIDTH * VIDEO_RATIO), - }, + // we don't need a video stream on start + video: false, }; mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig); mediaDevices.getTracks().map(track => { - if (track.kind === 'video') { - track.enabled = false; - } + // if (track.kind === 'video') { + // track.enabled = false; + // } if (mediaDevices) { peerConnection?.addTrack(track, mediaDevices); } }); } catch (err) { + window.log.warn('openMediaDevices: ', err); ToastUtils.pushVideoCallPermissionNeeded(); - closeVideoCall(); + await closeVideoCall(); } callVideoListeners(); } @@ -412,6 +415,9 @@ export async function USER_callRecipient(recipient: string) { }); window.log.info('Sending preOffer message to ', ed25519Str(recipient)); + + // we do it manually as the sendToPubkeyNonDurably rely on having a message saved to the db for MessageSentSuccess + // which is not the case for a pre offer message (the message only exists in memory) const rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg); const { wrappedEnvelope } = await MessageSender.send(rawMessage); void PnServer.notifyPnServer(wrappedEnvelope, recipient); @@ -455,7 +461,9 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => { uuid: currentCallUUID, }); - window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient); + window.log.info( + `sending ICE CANDIDATES MESSAGE to ${ed25519Str(recipient)} about call ${currentCallUUID}` + ); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates); }, 2000); @@ -479,7 +487,7 @@ const findLastMessageTypeFromSender = (sender: string, msgType: SignalService.Ca function handleSignalingStateChangeEvent() { if (peerConnection?.signalingState === 'closed') { - closeVideoCall(); + void closeVideoCall(); } } @@ -487,14 +495,14 @@ function handleConnectionStateChanged(pubkey: string) { window.log.info('handleConnectionStateChanged :', peerConnection?.connectionState); if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { - closeVideoCall(); + void closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { setIsRinging(false); window.inboxStore?.dispatch(callConnected({ pubkey })); } } -function closeVideoCall() { +async function closeVideoCall() { window.log.info('closingVideoCall '); setIsRinging(false); if (peerConnection) { @@ -533,6 +541,18 @@ function closeVideoCall() { currentCallUUID = undefined; window.inboxStore?.dispatch(setFullScreenCall(false)); + const convos = getConversationController().getConversations(); + const callingConvos = convos.filter(convo => convo.callState !== undefined); + if (callingConvos.length > 0) { + // reset all convos callState + await Promise.all( + callingConvos.map(async m => { + m.callState = undefined; + await m.commit(); + }) + ); + } + remoteVideoStreamIsMuted = true; makingOffer = false; @@ -712,7 +732,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } // tslint:disable-next-line: function-name -export async function USER_rejectIncomingCallRequest(fromSender: string) { +export async function USER_rejectIncomingCallRequest(fromSender: string, forcedUUID?: string) { setIsRinging(false); const lastOfferMessage = findLastMessageTypeFromSender( @@ -720,32 +740,36 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) { SignalService.CallMessage.Type.OFFER ); - const lastCallUUID = lastOfferMessage?.uuid; - window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${lastCallUUID}`); - if (lastCallUUID) { + const aboutCallUUID = forcedUUID || lastOfferMessage?.uuid; + window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${aboutCallUUID}`); + if (aboutCallUUID) { + rejectedCallUUIDS.add(aboutCallUUID); const endCallMessage = new CallMessage({ type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), - uuid: lastCallUUID, + uuid: aboutCallUUID, }); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); // delete all msg not from that uuid only but from that sender pubkey - clearCallCacheFromPubkeyAndUUID(fromSender, lastCallUUID); + clearCallCacheFromPubkeyAndUUID(fromSender, aboutCallUUID); } - window.inboxStore?.dispatch( - endCall({ - pubkey: fromSender, - }) - ); + // if we got a forceUUID, it means we just to deny another user's device incoming call we are already in a call with. + if (!forcedUUID) { + window.inboxStore?.dispatch( + endCall({ + pubkey: fromSender, + }) + ); - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // we just got a new offer from someone we are already in a call with - if (callingConvos.length === 1 && callingConvos[0].id === fromSender) { - closeVideoCall(); + const convos = getConversationController().getConversations(); + const callingConvos = convos.filter(convo => convo.callState !== undefined); + if (callingConvos.length > 0) { + // we just got a new offer from someone we are already in a call with + if (callingConvos.length === 1 && callingConvos[0].id === fromSender) { + await closeVideoCall(); + } } } } @@ -758,6 +782,7 @@ export async function USER_hangup(fromSender: string) { window.log.warn('should not be able to hangup without a currentCallUUID'); return; } else { + rejectedCallUUIDS.add(currentCallUUID); const endCallMessage = new CallMessage({ type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), @@ -773,17 +798,19 @@ export async function USER_hangup(fromSender: string) { clearCallCacheFromPubkeyAndUUID(fromSender, currentCallUUID); - closeVideoCall(); + await closeVideoCall(); } export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { window.log.info('handling callMessage END_CALL:', aboutCallUUID); if (aboutCallUUID) { + rejectedCallUUIDS.add(aboutCallUUID); + clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID); if (aboutCallUUID === currentCallUUID) { - closeVideoCall(); + void closeVideoCall(); window.inboxStore?.dispatch(endCall({ pubkey: sender })); } @@ -817,9 +844,17 @@ async function buildAnswerAndSendIt(sender: string) { window.log.info('sending ANSWER MESSAGE'); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage); + await getMessageQueue().sendToPubKeyNonDurably( + UserUtils.getOurPubKeyFromCache(), + callAnswerMessage + ); } } +export function isCallRejected(uuid: string) { + return rejectedCallUUIDS.has(uuid); +} + export async function handleCallTypeOffer( sender: string, callMessage: SignalService.CallMessage, @@ -832,20 +867,22 @@ export async function handleCallTypeOffer( } window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID); - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (!getCallMediaPermissionsSettings()) { await handleMissedCall(sender, incomingOfferTimestamp, true); return; } - if (callingConvos.length > 0) { - // we just got a new offer from someone we are NOT already in a call with - if (callingConvos.length !== 1 || callingConvos[0].id !== sender) { - await handleMissedCall(sender, incomingOfferTimestamp, false); + if (currentCallUUID && currentCallUUID !== remoteCallUUID) { + // we just got a new offer with a different callUUID. this is a missed call (from either the same sender or another one) + if (callCache.get(sender)?.has(currentCallUUID)) { + // this is a missed call from the same sender (another call from another device maybe?) + // just reject it. + await USER_rejectIncomingCallRequest(sender, remoteCallUUID); return; } + await handleMissedCall(sender, incomingOfferTimestamp, false); + + return; } const readyForOffer = @@ -859,7 +896,7 @@ export async function handleCallTypeOffer( return; } - if (callingConvos.length === 1 && callingConvos[0].id === sender) { + if (remoteCallUUID === currentCallUUID && currentCallUUID) { window.log.info('Got a new offer message from our ongoing call'); isSettingRemoteAnswerPending = false; const remoteDesc = new RTCSessionDescription({ @@ -938,7 +975,48 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe return; } - window.log.info('handling callMessage ANSWER'); + // this is an answer we sent to ourself, this must be about another of our device accepting an incoming call + // if we accepted that call already from the current device, currentCallUUID is set + if (sender === UserUtils.getOurPubKeyStrFromCache() && remoteCallUUID !== currentCallUUID) { + window.log.info(`handling callMessage ANSWER from ourself about call ${remoteCallUUID}`); + + let foundOwnerOfCallUUID: string | undefined; + for (const deviceKey of callCache.keys()) { + if (foundOwnerOfCallUUID) { + break; + } + for (const callUUIDEntry of callCache.get(deviceKey) as Map< + string, + Array + >) { + if (callUUIDEntry[0] === remoteCallUUID) { + foundOwnerOfCallUUID = deviceKey; + break; + } + } + } + + if (foundOwnerOfCallUUID) { + rejectedCallUUIDS.add(remoteCallUUID); + + const convos = getConversationController().getConversations(); + const callingConvos = convos.filter(convo => convo.callState !== undefined); + if (callingConvos.length > 0) { + // we just got a new offer from someone we are already in a call with + if (callingConvos.length === 1 && callingConvos[0].id === foundOwnerOfCallUUID) { + await closeVideoCall(); + } + } + window.inboxStore?.dispatch( + endCall({ + pubkey: foundOwnerOfCallUUID, + }) + ); + return; + } + } else { + window.log.info(`handling callMessage ANSWER from ${remoteCallUUID}`); + } pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); @@ -946,8 +1024,15 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe window.log.info('handleCallTypeAnswer without peer connection. Dropping'); return; } - window.inboxStore?.dispatch(answerCall({ pubkey: sender })); - const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] }); + window.inboxStore?.dispatch( + answerCall({ + pubkey: sender, + }) + ); + const remoteDesc = new RTCSessionDescription({ + type: 'answer', + sdp: callMessage.sdps[0], + }); // window.log?.info('Setting remote answer pending'); isSettingRemoteAnswerPending = true; From 4ce1b7813a719685f62c818a8f131a6d134ac43f Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 18 Nov 2021 11:36:14 +1100 Subject: [PATCH 03/13] add data-testid for leftpane sections and edit profile dialog --- ts/components/Avatar.tsx | 4 +- .../conversation/ConversationHeader.tsx | 4 +- .../media-gallery/MediaGallery.tsx | 2 +- ts/components/dialog/EditProfileDialog.tsx | 11 ++- .../dialog/OnionStatusPathDialog.tsx | 4 +- ts/components/session/ActionsPanel.tsx | 87 ++++++++++++------- .../session/LeftPaneSettingSection.tsx | 2 +- ts/components/session/SessionInput.tsx | 2 +- .../session/SessionMemberListItem.tsx | 2 +- ts/components/session/SessionSearchInput.tsx | 2 +- .../session/conversation/SessionRecording.tsx | 6 +- .../conversation/SessionRightPanel.tsx | 2 +- .../session/icon/SessionIconButton.tsx | 2 + .../session/settings/SessionSettings.tsx | 2 +- 14 files changed, 84 insertions(+), 48 deletions(-) diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index acc5ead9f..0c347cce8 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -23,6 +23,7 @@ type Props = { base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data memberAvatars?: Array; // this is added by usingClosedConversationDetails onAvatarClick?: () => void; + dataTestId?: string; }; const Identicon = (props: Props) => { @@ -92,7 +93,7 @@ const AvatarImage = (props: { }; const AvatarInner = (props: Props) => { - const { avatarPath, base64Data, size, memberAvatars, name } = props; + const { avatarPath, base64Data, size, memberAvatars, name, dataTestId } = props; const [imageBroken, setImageBroken] = useState(false); // contentType is not important const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', ''); @@ -122,6 +123,7 @@ const AvatarInner = (props: Props) => { props.onAvatarClick?.(); }} role="button" + data-testid={dataTestId} > {hasImage ? ( { return (
- +
@@ -145,7 +145,7 @@ const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) = }); }} > - +
); }; diff --git a/ts/components/conversation/media-gallery/MediaGallery.tsx b/ts/components/conversation/media-gallery/MediaGallery.tsx index efffb3caa..72a69aaa6 100644 --- a/ts/components/conversation/media-gallery/MediaGallery.tsx +++ b/ts/components/conversation/media-gallery/MediaGallery.tsx @@ -45,7 +45,7 @@ const Sections = (props: Props & { selectedTab: TabType }) => { const label = type === 'media' ? window.i18n('mediaEmptyState') : window.i18n('documentsEmptyState'); - return ; + return ; } return ( diff --git a/ts/components/dialog/EditProfileDialog.tsx b/ts/components/dialog/EditProfileDialog.tsx index dfd7d09f1..4c84a81ea 100644 --- a/ts/components/dialog/EditProfileDialog.tsx +++ b/ts/components/dialog/EditProfileDialog.tsx @@ -82,7 +82,7 @@ export class EditProfileDialog extends React.Component<{}, State> { : undefined; return ( -
+
{
-

+

{sessionID}

@@ -182,10 +185,10 @@ export class EditProfileDialog extends React.Component<{}, State> { {this.renderProfileHeader()}
-

{name}

+

{name}

{ this.setState({ mode: 'edit' }); }} diff --git a/ts/components/dialog/OnionStatusPathDialog.tsx b/ts/components/dialog/OnionStatusPathDialog.tsx index 00540b6fb..dee364720 100644 --- a/ts/components/dialog/OnionStatusPathDialog.tsx +++ b/ts/components/dialog/OnionStatusPathDialog.tsx @@ -150,8 +150,9 @@ export const ModalStatusLight = (props: StatusLightType) => { export const ActionPanelOnionStatusLight = (props: { isSelected: boolean; handleClick: () => void; + dataTestId?: string; }) => { - const { isSelected, handleClick } = props; + const { isSelected, handleClick, dataTestId } = props; const onionPathsCount = useSelector(getOnionPathsCount); const firstPathLength = useSelector(getFirstOnionPathLength); @@ -179,6 +180,7 @@ export const ActionPanelOnionStatusLight = (props: { glowStartDelay={0} noScale={true} isSelected={isSelected} + dataTestId={dataTestId} /> ); }; diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 4952fdffc..8e09ca7f9 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { SessionIconButton, SessionIconType } from './icon'; +import { SessionIconButton } from './icon'; import { Avatar, AvatarSize } from '../Avatar'; import { SessionToastContainer } from './SessionToastContainer'; import { getConversationController } from '../../session/conversations'; @@ -96,47 +96,71 @@ const Section = (props: { type: SectionType; avatarPath?: string | null }) => { onAvatarClick={handleClick} name={userName} pubkey={ourNumber} + dataTestId="leftpane-primary-avatar" /> ); } const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined; - let iconType: SessionIconType; switch (type) { case SectionType.Message: - iconType = 'chatBubble'; - break; - case SectionType.Contact: - iconType = 'users'; - break; - case SectionType.Settings: - iconType = 'gear'; - break; - case SectionType.Moon: - iconType = 'moon'; - break; - default: - iconType = 'moon'; - } - const iconColor = undefined; - - return ( - <> - {type === SectionType.PathIndicator ? ( - - ) : ( + return ( - )} - - ); + ); + case SectionType.Contact: + return ( + + ); + case SectionType.Settings: + return ( + + ); + case SectionType.PathIndicator: + return ( + + ); + default: + return ( + + ); + } }; const cleanUpMediasInterval = DURATION.MINUTES * 30; @@ -300,7 +324,10 @@ export const ActionsPanel = () => { -
+
diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx index 29e87d949..73ba038bf 100644 --- a/ts/components/session/LeftPaneSettingSection.tsx +++ b/ts/components/session/LeftPaneSettingSection.tsx @@ -58,7 +58,7 @@ const LeftPaneSettingsCategoryRow = (props: { item: any }) => {
{item.id === focusedSettingsSection && ( - + )}
diff --git a/ts/components/session/SessionInput.tsx b/ts/components/session/SessionInput.tsx index c870e0928..611ae6450 100644 --- a/ts/components/session/SessionInput.tsx +++ b/ts/components/session/SessionInput.tsx @@ -108,7 +108,7 @@ export class SessionInput extends React.PureComponent { return ( { this.setState({ forceShow: !this.state.forceShow, diff --git a/ts/components/session/SessionMemberListItem.tsx b/ts/components/session/SessionMemberListItem.tsx index 9557c4ebc..9cf9fd0b6 100644 --- a/ts/components/session/SessionMemberListItem.tsx +++ b/ts/components/session/SessionMemberListItem.tsx @@ -72,7 +72,7 @@ export const SessionMemberListItem = (props: Props) => { {name}
- +
); diff --git a/ts/components/session/SessionSearchInput.tsx b/ts/components/session/SessionSearchInput.tsx index 5f8512859..3c25f4301 100644 --- a/ts/components/session/SessionSearchInput.tsx +++ b/ts/components/session/SessionSearchInput.tsx @@ -21,7 +21,7 @@ export const SessionSearchInput = (props: Props) => { return (
- + onChange(e.target.value)} diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx index d8dbdf393..e98a333fd 100644 --- a/ts/components/session/conversation/SessionRecording.tsx +++ b/ts/components/session/conversation/SessionRecording.tsx @@ -125,16 +125,16 @@ export class SessionRecording extends React.Component { {isRecording && ( )} {actionPauseAudio && ( - + )} {hasRecordingAndPaused && ( - + )} {hasRecording && ( {
{ dispatch(closeRightPanel()); diff --git a/ts/components/session/icon/SessionIconButton.tsx b/ts/components/session/icon/SessionIconButton.tsx index 48271ec53..90ef8bdfb 100644 --- a/ts/components/session/icon/SessionIconButton.tsx +++ b/ts/components/session/icon/SessionIconButton.tsx @@ -10,6 +10,7 @@ interface SProps extends SessionIconProps { isSelected?: boolean; isHidden?: boolean; margin?: string; + dataTestId?: string; } const SessionIconButtonInner = React.forwardRef((props, ref) => { @@ -43,6 +44,7 @@ const SessionIconButtonInner = React.forwardRef((props, ref={ref} onClick={clickHandler} style={{ display: isHidden ? 'none' : 'flex', margin: margin ? margin : '' }} + data-testid={props.dataTestId} > {
v{window.versionInfo.version} - + {window.versionInfo.commitHash}
From 2f49228317e3bc96ddb12394f473da26c2d09809 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 09:27:52 +1100 Subject: [PATCH 04/13] update turn servers --- ts/session/utils/CallManager.ts | 34 +++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index d639358e3..6389c7197 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -105,8 +105,38 @@ const configuration: RTCConfiguration = { iceServers: [ { urls: 'turn:freyr.getsession.org', - username: 'session', - credential: 'session', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:fenrir.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:frigg.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:angus.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:hereford.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:holstein.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', + }, + { + urls: 'turn:brahman.getsession.org', + username: 'session202111', + credential: '053c268164bc7bd7', }, ], // iceTransportPolicy: 'relay', // for now, this cause the connection to break after 30-40 sec if we enable this From 80566fd60e98856fc5cd358363a5d19c1309151b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 10:46:45 +1100 Subject: [PATCH 05/13] cleanup sessionprotobuf --- protos/SubProtocol.proto | 8 -------- 1 file changed, 8 deletions(-) diff --git a/protos/SubProtocol.proto b/protos/SubProtocol.proto index c266e53f2..a788021e5 100644 --- a/protos/SubProtocol.proto +++ b/protos/SubProtocol.proto @@ -26,13 +26,6 @@ message WebSocketRequestMessage { optional uint64 id = 4; } -message WebSocketResponseMessage { - optional uint64 id = 1; - optional uint32 status = 2; - optional string message = 3; - repeated string headers = 5; - optional bytes body = 4; -} message WebSocketMessage { enum Type { @@ -43,5 +36,4 @@ message WebSocketMessage { optional Type type = 1; optional WebSocketRequestMessage request = 2; - optional WebSocketResponseMessage response = 3; } From 6f3625f99cc516bf3039e44c20d42f4f734bb9bf Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 10:49:05 +1100 Subject: [PATCH 06/13] move the state of calling to its own slice --- .../conversation/ConversationHeader.tsx | 3 +- ts/components/session/SessionInboxView.tsx | 2 + ts/components/session/calling/CallButtons.tsx | 4 +- .../calling/CallInFullScreenContainer.tsx | 4 +- .../calling/DraggableCallContainer.tsx | 7 +- .../calling/InConversationCallContainer.tsx | 2 +- .../session/calling/IncomingCallDialog.tsx | 2 +- ts/components/session/menu/Menu.tsx | 7 +- ts/hooks/useVideoEventListener.ts | 7 +- ts/interactions/conversationInteractions.ts | 2 - ts/models/conversation.ts | 9 - ts/receiver/cache.ts | 4 +- ts/receiver/callMessage.ts | 4 +- ts/receiver/contentMessage.ts | 2 +- ts/receiver/receiver.ts | 2 +- ts/session/sending/MessageSender.ts | 7 +- ts/session/utils/CallManager.ts | 238 ++++++++++-------- ts/state/ducks/call.tsx | 111 ++++++++ ts/state/ducks/conversations.ts | 108 -------- ts/state/reducer.ts | 3 + ts/state/selectors/call.ts | 103 ++++++++ ts/state/selectors/conversations.ts | 93 ------- ts/state/smart/SessionConversation.ts | 2 +- 23 files changed, 381 insertions(+), 345 deletions(-) create mode 100644 ts/state/ducks/call.tsx create mode 100644 ts/state/selectors/call.ts diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 697b2fead..122faeeeb 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -14,8 +14,6 @@ import { getConversationHeaderProps, getConversationHeaderTitleProps, getCurrentNotificationSettingText, - getHasIncomingCall, - getHasOngoingCall, getIsSelectedNoteToSelf, getIsSelectedPrivate, getSelectedConversation, @@ -40,6 +38,7 @@ import { resetSelectedMessageIds, } from '../../state/ducks/conversations'; import { callRecipient } from '../../interactions/conversationInteractions'; +import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call'; export interface TimerOption { name: string; diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index fdb34b04d..57fb092c9 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -26,6 +26,7 @@ import { PersistGate } from 'redux-persist/integration/react'; import { persistStore } from 'redux-persist'; import { TimerOptionsArray } from '../../state/ducks/timerOptions'; import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments'; +import { initialCallState } from '../../state/ducks/call'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 @@ -105,6 +106,7 @@ export class SessionInboxView extends React.Component { timerOptions, }, stagedAttachments: getEmptyStagedAttachmentsState(), + call: initialCallState, }; this.store = createStore(initialState); diff --git a/ts/components/session/calling/CallButtons.tsx b/ts/components/session/calling/CallButtons.tsx index 0469ee810..bfb2b583b 100644 --- a/ts/components/session/calling/CallButtons.tsx +++ b/ts/components/session/calling/CallButtons.tsx @@ -1,11 +1,11 @@ import { SessionIconButton } from '../icon'; import { animation, contextMenu, Item, Menu } from 'react-contexify'; import { InputItem } from '../../../session/utils/CallManager'; -import { setFullScreenCall } from '../../../state/ducks/conversations'; +import { setFullScreenCall } from '../../../state/ducks/call'; import { CallManager, ToastUtils } from '../../../session/utils'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { getHasOngoingCallWithPubkey } from '../../../state/selectors/conversations'; +import { getHasOngoingCallWithPubkey } from '../../../state/selectors/call'; import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton'; import styled from 'styled-components'; diff --git a/ts/components/session/calling/CallInFullScreenContainer.tsx b/ts/components/session/calling/CallInFullScreenContainer.tsx index 55d03fea6..15aa0f76a 100644 --- a/ts/components/session/calling/CallInFullScreenContainer.tsx +++ b/ts/components/session/calling/CallInFullScreenContainer.tsx @@ -4,11 +4,11 @@ import { useDispatch, useSelector } from 'react-redux'; import useKey from 'react-use/lib/useKey'; import styled from 'styled-components'; import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; -import { setFullScreenCall } from '../../../state/ducks/conversations'; +import { setFullScreenCall } from '../../../state/ducks/call'; import { getCallIsInFullScreen, getHasOngoingCallWithFocusedConvo, -} from '../../../state/selectors/conversations'; +} from '../../../state/selectors/call'; import { CallWindowControls } from './CallButtons'; import { StyledVideoElement } from './DraggableCallContainer'; diff --git a/ts/components/session/calling/DraggableCallContainer.tsx b/ts/components/session/calling/DraggableCallContainer.tsx index 1b543c5f4..7455da648 100644 --- a/ts/components/session/calling/DraggableCallContainer.tsx +++ b/ts/components/session/calling/DraggableCallContainer.tsx @@ -4,11 +4,8 @@ import Draggable, { DraggableData, DraggableEvent } from 'react-draggable'; import styled from 'styled-components'; import _ from 'underscore'; -import { - getHasOngoingCall, - getHasOngoingCallWith, - getSelectedConversationKey, -} from '../../../state/selectors/conversations'; +import { getSelectedConversationKey } from '../../../state/selectors/conversations'; +import { getHasOngoingCall, getHasOngoingCallWith } from '../../../state/selectors/call'; import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { Avatar, AvatarSize } from '../../Avatar'; import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index 2a2dc4686..d8c748ed0 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -10,7 +10,7 @@ import { getHasOngoingCallWithFocusedConvoIsOffering, getHasOngoingCallWithFocusedConvosIsConnecting, getHasOngoingCallWithPubkey, -} from '../../../state/selectors/conversations'; +} from '../../../state/selectors/call'; import { StyledVideoElement } from './DraggableCallContainer'; import { Avatar, AvatarSize } from '../../Avatar'; diff --git a/ts/components/session/calling/IncomingCallDialog.tsx b/ts/components/session/calling/IncomingCallDialog.tsx index 5e7db9e6f..d022e2c26 100644 --- a/ts/components/session/calling/IncomingCallDialog.tsx +++ b/ts/components/session/calling/IncomingCallDialog.tsx @@ -6,7 +6,7 @@ import _ from 'underscore'; import { useAvatarPath, useConversationUsername } from '../../../hooks/useParamSelector'; import { ed25519Str } from '../../../session/onions/onionPath'; import { CallManager } from '../../../session/utils'; -import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/conversations'; +import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/call'; import { Avatar, AvatarSize } from '../../Avatar'; import { SessionButton, SessionButtonColor } from '../SessionButton'; import { SessionWrapperModal } from '../SessionWrapperModal'; diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 5195b77cf..f325086d9 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -1,10 +1,7 @@ import React from 'react'; -import { - getHasIncomingCall, - getHasOngoingCall, - getNumberOfPinnedConversations, -} from '../../../state/selectors/conversations'; +import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call'; +import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; import { getFocusedSection } from '../../../state/selectors/section'; import { Item, Submenu } from 'react-contexify'; import { diff --git a/ts/hooks/useVideoEventListener.ts b/ts/hooks/useVideoEventListener.ts index 203614a8e..401420cde 100644 --- a/ts/hooks/useVideoEventListener.ts +++ b/ts/hooks/useVideoEventListener.ts @@ -8,11 +8,8 @@ import { DEVICE_DISABLED_DEVICE_ID, InputItem, } from '../session/utils/CallManager'; -import { - getCallIsInFullScreen, - getHasOngoingCallWithPubkey, - getSelectedConversationKey, -} from '../state/selectors/conversations'; +import { getSelectedConversationKey } from '../state/selectors/conversations'; +import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call'; export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { const selectedConversationKey = useSelector(getSelectedConversationKey); diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 16615e1dd..4851f6dc4 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -447,8 +447,6 @@ export async function callRecipient(pubkey: string, canCall: boolean) { } if (convo && convo.isPrivate() && !convo.isMe()) { - convo.callState = 'offering'; - await convo.commit(); await CallManager.USER_callRecipient(convo.id); } } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 6af47dc74..f70fe9f26 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -176,8 +176,6 @@ export const fillConvoAttributesWithDefaults = ( }); }; -export type CallState = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined; - export class ConversationModel extends Backbone.Model { public updateLastMessage: () => any; public throttledBumpTyping: () => void; @@ -185,8 +183,6 @@ export class ConversationModel extends Backbone.Model { public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise; public initialPromise: any; - public callState: CallState; - private typingRefreshTimer?: NodeJS.Timeout | null; private typingPauseTimer?: NodeJS.Timeout | null; private typingTimer?: NodeJS.Timeout | null; @@ -441,7 +437,6 @@ export class ConversationModel extends Backbone.Model { const left = !!this.get('left'); const expireTimer = this.get('expireTimer'); const currentNotificationSetting = this.get('triggerNotificationsFor'); - const callState = this.callState; // to reduce the redux store size, only set fields which cannot be undefined // for instance, a boolean can usually be not set if false, etc @@ -546,10 +541,6 @@ export class ConversationModel extends Backbone.Model { text: lastMessageText, }; } - - if (callState) { - toRet.callState = callState; - } return toRet; } diff --git a/ts/receiver/cache.ts b/ts/receiver/cache.ts index a98a79c5b..d6919273a 100644 --- a/ts/receiver/cache.ts +++ b/ts/receiver/cache.ts @@ -15,7 +15,7 @@ import { export async function removeFromCache(envelope: EnvelopePlus) { const { id } = envelope; - window?.log?.info(`removing from cache envelope: ${id}`); + // window?.log?.info(`removing from cache envelope: ${id}`); return removeUnprocessed(id); } @@ -25,7 +25,7 @@ export async function addToCache( messageHash: string ) { const { id } = envelope; - window?.log?.info(`adding to cache envelope: ${id}`); + // window?.log?.info(`adding to cache envelope: ${id}`); const encodedEnvelope = StringUtils.decode(plaintext, 'base64'); const data: UnprocessedParameter = { diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index d7ef83232..8b0d4122e 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -30,7 +30,7 @@ export async function handleCallMessage( if (CallManager.isCallRejected(callMessage.uuid)) { await removeFromCache(envelope); - window.log.info(`Dropping already rejected call ${callMessage.uuid}`); + window.log.info(`Dropping already rejected call from this device ${callMessage.uuid}`); return; } @@ -65,7 +65,7 @@ export async function handleCallMessage( if (type === SignalService.CallMessage.Type.END_CALL) { await removeFromCache(envelope); - CallManager.handleCallTypeEndCall(sender, callMessage.uuid); + await CallManager.handleCallTypeEndCall(sender, callMessage.uuid); return; } diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 7e160ca5e..09d4236fa 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -210,7 +210,7 @@ async function decryptUnidentifiedSender( envelope: EnvelopePlus, ciphertext: ArrayBuffer ): Promise { - window?.log?.info('received unidentified sender message'); + // window?.log?.info('received unidentified sender message'); try { const userX25519KeyPair = await UserUtils.getIdentityKeyPair(); diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index c3a8c0521..b35827ca5 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -72,7 +72,7 @@ const envelopeQueue = new EnvelopeQueue(); function queueEnvelope(envelope: EnvelopePlus, messageHash?: string) { const id = getEnvelopeId(envelope); - window?.log?.info('queueing envelope', id); + // window?.log?.info('queueing envelope', id); const task = handleEnvelope.bind(null, envelope, messageHash); const taskWithTimeout = createTaskWithTimeout(task, `queueEnvelope ${id}`); diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index dde8630de..4cb519130 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -20,6 +20,7 @@ import { MessageSender } from '.'; import { getMessageById } from '../../../ts/data/data'; import { SNodeAPI } from '../snode_api'; import { getConversationController } from '../conversations'; +import { ed25519Str } from '../onions/onionPath'; const DEFAULT_CONNECTIONS = 1; @@ -129,7 +130,7 @@ export async function TEST_sendMessageToSnode( const data64 = window.dcodeIO.ByteBuffer.wrap(data).toString('base64'); const swarm = await getSwarmFor(pubKey); - window?.log?.debug('Sending envelope with timestamp: ', timestamp, ' to ', pubKey); + window?.log?.debug('Sending envelope with timestamp: ', timestamp, ' to ', ed25519Str(pubKey)); // send parameters const params = { pubKey, @@ -190,7 +191,9 @@ export async function TEST_sendMessageToSnode( } window?.log?.info( - `loki_message:::sendMessage - Successfully stored message to ${pubKey} via ${snode.ip}:${snode.port}` + `loki_message:::sendMessage - Successfully stored message to ${ed25519Str(pubKey)} via ${ + snode.ip + }:${snode.port}` ); } diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 6389c7197..3ab4e7866 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -2,18 +2,18 @@ import _ from 'lodash'; import { MessageUtils, ToastUtils, UserUtils } from '.'; import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings'; import { getConversationById } from '../../data/data'; -import { ConversationModel } from '../../models/conversation'; import { MessageModelType } from '../../models/messageType'; import { SignalService } from '../../protobuf'; +import { openConversationWithMessages } from '../../state/ducks/conversations'; import { answerCall, callConnected, + CallStatusEnum, endCall, incomingCall, - openConversationWithMessages, setFullScreenCall, startingCallWith, -} from '../../state/ducks/conversations'; +} from '../../state/ducks/call'; import { getConversationController } from '../conversations'; import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; import { ed25519Str } from '../onions/onionPath'; @@ -26,6 +26,9 @@ import { setIsRinging } from './RingingManager'; export type InputItem = { deviceId: string; label: string }; +/** + * This uuid is set only once we accepted a call or started one. + */ let currentCallUUID: string | undefined; const rejectedCallUUIDS: Set = new Set(); @@ -571,17 +574,7 @@ async function closeVideoCall() { currentCallUUID = undefined; window.inboxStore?.dispatch(setFullScreenCall(false)); - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // reset all convos callState - await Promise.all( - callingConvos.map(async m => { - m.callState = undefined; - await m.commit(); - }) - ); - } + window.inboxStore?.dispatch(endCall()); remoteVideoStreamIsMuted = true; @@ -592,24 +585,26 @@ async function closeVideoCall() { callVideoListeners(); } +function getCallingStateOutsideOfRedux() { + const ongoingCallWith = window.inboxStore?.getState().call.ongoingWith as string | undefined; + const ongoingCallStatus = window.inboxStore?.getState().call.ongoingCallStatus as CallStatusEnum; + return { ongoingCallWith, ongoingCallStatus }; +} + function onDataChannelReceivedMessage(ev: MessageEvent) { try { const parsed = JSON.parse(ev.data); if (parsed.hangup !== undefined) { - const foundEntry = getConversationController() - .getConversations() - .find( - (convo: ConversationModel) => - convo.callState === 'connecting' || - convo.callState === 'offering' || - convo.callState === 'ongoing' - ); - - if (!foundEntry || !foundEntry.id) { - return; + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); + if ( + (ongoingCallStatus === 'connecting' || + ongoingCallStatus === 'offering' || + ongoingCallStatus === 'ongoing') && + ongoingCallWith + ) { + void handleCallTypeEndCall(ongoingCallWith, currentCallUUID); } - handleCallTypeEndCall(foundEntry.id, currentCallUUID); return; } @@ -761,8 +756,23 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { await buildAnswerAndSendIt(fromSender); } +export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUID: string) { + setIsRinging(false); + window.log.info(`rejectCallAlreadyAnotherCall ${ed25519Str(fromSender)}: ${forcedUUID}`); + rejectedCallUUIDS.add(forcedUUID); + const rejectCallMessage = new CallMessage({ + type: SignalService.CallMessage.Type.END_CALL, + timestamp: Date.now(), + uuid: forcedUUID, + }); + await sendCallMessageAndSync(rejectCallMessage, fromSender); + + // delete all msg not from that uuid only but from that sender pubkey + clearCallCacheFromPubkeyAndUUID(fromSender, forcedUUID); +} + // tslint:disable-next-line: function-name -export async function USER_rejectIncomingCallRequest(fromSender: string, forcedUUID?: string) { +export async function USER_rejectIncomingCallRequest(fromSender: string) { setIsRinging(false); const lastOfferMessage = findLastMessageTypeFromSender( @@ -770,7 +780,7 @@ export async function USER_rejectIncomingCallRequest(fromSender: string, forcedU SignalService.CallMessage.Type.OFFER ); - const aboutCallUUID = forcedUUID || lastOfferMessage?.uuid; + const aboutCallUUID = lastOfferMessage?.uuid; window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${aboutCallUUID}`); if (aboutCallUUID) { rejectedCallUUIDS.add(aboutCallUUID); @@ -779,29 +789,25 @@ export async function USER_rejectIncomingCallRequest(fromSender: string, forcedU timestamp: Date.now(), uuid: aboutCallUUID, }); - await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); - + // sync the reject event so our other devices remove the popup too + await sendCallMessageAndSync(endCallMessage, fromSender); // delete all msg not from that uuid only but from that sender pubkey clearCallCacheFromPubkeyAndUUID(fromSender, aboutCallUUID); } + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); - // if we got a forceUUID, it means we just to deny another user's device incoming call we are already in a call with. - if (!forcedUUID) { - window.inboxStore?.dispatch( - endCall({ - pubkey: fromSender, - }) - ); - - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // we just got a new offer from someone we are already in a call with - if (callingConvos.length === 1 && callingConvos[0].id === fromSender) { - await closeVideoCall(); - } - } + // clear the ongoing call if needed + if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) { + await closeVideoCall(); } + + // close the popup call + window.inboxStore?.dispatch(endCall()); +} + +async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { + await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage); + await getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage); } // tslint:disable-next-line: function-name @@ -821,7 +827,7 @@ export async function USER_hangup(fromSender: string) { void getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); } - window.inboxStore?.dispatch(endCall({ pubkey: fromSender })); + window.inboxStore?.dispatch(endCall()); window.log.info('sending hangup with an END_CALL MESSAGE'); sendHangupViaDataChannel(); @@ -831,7 +837,10 @@ export async function USER_hangup(fromSender: string) { await closeVideoCall(); } -export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { +/** + * This can actually be called from either the datachannel or from the receiver END_CALL event + */ +export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { window.log.info('handling callMessage END_CALL:', aboutCallUUID); if (aboutCallUUID) { @@ -839,10 +848,25 @@ export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) { clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID); - if (aboutCallUUID === currentCallUUID) { - void closeVideoCall(); + // this is a end call from ourself. We must remove the popup about the incoming call + // if it matches the owner of this callUUID + if (sender === UserUtils.getOurPubKeyStrFromCache()) { + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); + const ownerOfCall = getOwnerOfCallUUID(aboutCallUUID); - window.inboxStore?.dispatch(endCall({ pubkey: sender })); + if ( + (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') && + ongoingCallWith === ownerOfCall + ) { + await closeVideoCall(); + window.inboxStore?.dispatch(endCall()); + } + return; + } + + if (aboutCallUUID === currentCallUUID) { + await closeVideoCall(); + window.inboxStore?.dispatch(endCall()); } } } @@ -871,13 +895,8 @@ async function buildAnswerAndSendIt(sender: string) { uuid: currentCallUUID, }); - window.log.info('sending ANSWER MESSAGE'); - - await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage); - await getMessageQueue().sendToPubKeyNonDurably( - UserUtils.getOurPubKeyFromCache(), - callAnswerMessage - ); + window.log.info('sending ANSWER MESSAGE and sync'); + await sendCallMessageAndSync(callAnswerMessage, sender); } } @@ -905,12 +924,17 @@ export async function handleCallTypeOffer( if (currentCallUUID && currentCallUUID !== remoteCallUUID) { // we just got a new offer with a different callUUID. this is a missed call (from either the same sender or another one) if (callCache.get(sender)?.has(currentCallUUID)) { - // this is a missed call from the same sender (another call from another device maybe?) - // just reject it. - await USER_rejectIncomingCallRequest(sender, remoteCallUUID); + // this is a missed call from the same sender but with a different callID. + // another call from another device maybe? just reject it. + await rejectCallAlreadyAnotherCall(sender, remoteCallUUID); return; } + // add a message in the convo with this user about the missed call. await handleMissedCall(sender, incomingOfferTimestamp, false); + // Here, we are in a call, and we got an offer from someone we are in a call with, and not one of his other devices. + // Just hangup automatically the call on the calling side. + + await rejectCallAlreadyAnotherCall(sender, remoteCallUUID); return; } @@ -994,61 +1018,69 @@ export async function handleMissedCall( return; } +function getOwnerOfCallUUID(callUUID: string) { + for (const deviceKey of callCache.keys()) { + for (const callUUIDEntry of callCache.get(deviceKey) as Map< + string, + Array + >) { + if (callUUIDEntry[0] === callUUID) { + return deviceKey; + } + } + } + return null; +} + export async function handleCallTypeAnswer(sender: string, callMessage: SignalService.CallMessage) { if (!callMessage.sdps || callMessage.sdps.length === 0) { - window.log.warn('cannot handle answered message without signal description protols'); + window.log.warn('cannot handle answered message without signal description proto sdps'); return; } - const remoteCallUUID = callMessage.uuid; - if (!remoteCallUUID || remoteCallUUID.length === 0) { + const callMessageUUID = callMessage.uuid; + if (!callMessageUUID || callMessageUUID.length === 0) { window.log.warn('handleCallTypeAnswer has no valid uuid'); return; } - // this is an answer we sent to ourself, this must be about another of our device accepting an incoming call - // if we accepted that call already from the current device, currentCallUUID is set - if (sender === UserUtils.getOurPubKeyStrFromCache() && remoteCallUUID !== currentCallUUID) { - window.log.info(`handling callMessage ANSWER from ourself about call ${remoteCallUUID}`); + // this is an answer we sent to ourself, this must be about another of our device accepting an incoming call. + // if we accepted that call already from the current device, currentCallUUID would be set + if (sender === UserUtils.getOurPubKeyStrFromCache()) { + // when we answer a call, we get this message on all our devices, including the one we just accepted the call with. - let foundOwnerOfCallUUID: string | undefined; - for (const deviceKey of callCache.keys()) { - if (foundOwnerOfCallUUID) { - break; - } - for (const callUUIDEntry of callCache.get(deviceKey) as Map< - string, - Array - >) { - if (callUUIDEntry[0] === remoteCallUUID) { - foundOwnerOfCallUUID = deviceKey; - break; - } - } - } + const isDeviceWhichJustAcceptedCall = currentCallUUID === callMessageUUID; - if (foundOwnerOfCallUUID) { - rejectedCallUUIDS.add(remoteCallUUID); - - const convos = getConversationController().getConversations(); - const callingConvos = convos.filter(convo => convo.callState !== undefined); - if (callingConvos.length > 0) { - // we just got a new offer from someone we are already in a call with - if (callingConvos.length === 1 && callingConvos[0].id === foundOwnerOfCallUUID) { - await closeVideoCall(); - } - } - window.inboxStore?.dispatch( - endCall({ - pubkey: foundOwnerOfCallUUID, - }) + if (isDeviceWhichJustAcceptedCall) { + window.log.info( + `isDeviceWhichJustAcceptedCall: skipping message back ANSWER from ourself about call ${callMessageUUID}` ); + return; } + window.log.info(`handling callMessage ANSWER from ourself about call ${callMessageUUID}`); + + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); + const foundOwnerOfCallUUID = getOwnerOfCallUUID(callMessageUUID); + + if (callMessageUUID !== currentCallUUID) { + // this is an answer we sent from another of our devices + // automatically close that call + if (foundOwnerOfCallUUID) { + rejectedCallUUIDS.add(callMessageUUID); + // if this call is about the one being currently displayed, force close it + if (ongoingCallStatus && ongoingCallWith === foundOwnerOfCallUUID) { + await closeVideoCall(); + } + + window.inboxStore?.dispatch(endCall()); + } + } + return; } else { - window.log.info(`handling callMessage ANSWER from ${remoteCallUUID}`); + window.log.info(`handling callMessage ANSWER from ${callMessageUUID}`); } - pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); + pushCallMessageToCallCache(sender, callMessageUUID, callMessage); if (!peerConnection) { window.log.info('handleCallTypeAnswer without peer connection. Dropping'); @@ -1066,7 +1098,11 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe // window.log?.info('Setting remote answer pending'); isSettingRemoteAnswerPending = true; - await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed + try { + await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed + } catch (e) { + window.log.warn('setRemoteDescription failed:', e); + } isSettingRemoteAnswerPending = false; } diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx new file mode 100644 index 000000000..47e9e3bdb --- /dev/null +++ b/ts/state/ducks/call.tsx @@ -0,0 +1,111 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export type CallStatusEnum = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined; + +export type CallStateType = { + ongoingWith?: string; + ongoingCallStatus?: CallStatusEnum; + callIsInFullScreen: boolean; +}; + +export const initialCallState: CallStateType = { + ongoingWith: undefined, + ongoingCallStatus: undefined, + callIsInFullScreen: false, +}; + +/** + * This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server. + */ +const callSlice = createSlice({ + name: 'call', + initialState: initialCallState, + reducers: { + incomingCall(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + const callerPubkey = action.payload.pubkey; + if (state.ongoingWith && state.ongoingWith !== callerPubkey) { + window.log.warn( + `Got an incoming call action for ${callerPubkey} but we are already in a call.` + ); + return state; + } + state.ongoingWith = callerPubkey; + state.ongoingCallStatus = 'incoming'; + return state; + }, + endCall(state: CallStateType) { + state.ongoingCallStatus = undefined; + state.ongoingWith = undefined; + + return state; + }, + answerCall(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + const callerPubkey = action.payload.pubkey; + + // to answer a call we need an incoming call form that specific pubkey + + if (state.ongoingWith !== callerPubkey || state.ongoingCallStatus !== 'incoming') { + window.log.info('cannot answer a call we are not displaying a dialog with'); + return state; + } + state.ongoingCallStatus = 'connecting'; + state.callIsInFullScreen = false; + return state; + }, + callConnected(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + const callerPubkey = action.payload.pubkey; + if (callerPubkey !== state.ongoingWith) { + window.log.info('cannot answer a call we did not start or receive first'); + return state; + } + const existingCallState = state.ongoingCallStatus; + + if (existingCallState !== 'connecting' && existingCallState !== 'offering') { + window.log.info( + 'cannot answer a call we are not connecting (and so answered) to or offering a call' + ); + return state; + } + + state.ongoingCallStatus = 'ongoing'; + state.callIsInFullScreen = false; + return state; + }, + startingCallWith(state: CallStateType, action: PayloadAction<{ pubkey: string }>) { + if (state.ongoingWith) { + window.log.warn('cannot start a call with an ongoing call already: ongoingWith'); + return state; + } + if (state.ongoingCallStatus) { + window.log.warn('cannot start a call with an ongoing call already: ongoingCallStatus'); + return state; + } + + const callerPubkey = action.payload.pubkey; + state.ongoingWith = callerPubkey; + state.ongoingCallStatus = 'offering'; + state.callIsInFullScreen = false; + + return state; + }, + setFullScreenCall(state: CallStateType, action: PayloadAction) { + // only set in full screen if we have an ongoing call + if (state.ongoingWith && state.ongoingCallStatus === 'ongoing' && action.payload) { + state.callIsInFullScreen = true; + } + state.callIsInFullScreen = false; + return state; + }, + }, +}); + +const { actions, reducer } = callSlice; +export const { + incomingCall, + endCall, + answerCall, + callConnected, + startingCallWith, + setFullScreenCall, +} = actions; +export const callReducer = reducer; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index bc00e05fe..177ca87bc 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -3,7 +3,6 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { getConversationController } from '../../session/conversations'; import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data'; import { - CallState, ConversationNotificationSettingType, ConversationTypeEnum, } from '../../models/conversation'; @@ -253,7 +252,6 @@ export interface ReduxConversationType { currentNotificationSetting?: ConversationNotificationSettingType; isPinned?: boolean; - callState?: CallState; } export interface NotificationForConvoOption { @@ -277,7 +275,6 @@ export type ConversationsStateType = { quotedMessage?: ReplyingToMessageProps; areMoreMessagesBeingFetched: boolean; haveDoneFirstScroll: boolean; - callIsInFullScreen: boolean; showScrollButton: boolean; animateQuotedMessageId?: string; @@ -372,7 +369,6 @@ export function getEmptyConversationState(): ConversationsStateType { mentionMembers: [], firstUnreadMessageId: undefined, haveDoneFirstScroll: false, - callIsInFullScreen: false, }; } @@ -698,7 +694,6 @@ const conversationsSlice = createSlice({ return { conversationLookup: state.conversationLookup, - callIsInFullScreen: state.callIsInFullScreen, selectedConversation: action.payload.id, areMoreMessagesBeingFetched: false, @@ -762,102 +757,6 @@ const conversationsSlice = createSlice({ state.mentionMembers = action.payload; return state; }, - incomingCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (existingCallState !== undefined) { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = 'incoming'; - - void foundConvo.commit(); - return state; - }, - endCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (!existingCallState) { - return state; - } - - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = undefined; - - void foundConvo.commit(); - return state; - }, - answerCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (!existingCallState || existingCallState !== 'incoming') { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - - foundConvo.callState = 'connecting'; - void foundConvo.commit(); - return state; - }, - callConnected(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (!existingCallState || existingCallState === 'ongoing') { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = 'ongoing'; - void foundConvo.commit(); - return state; - }, - startingCallWith(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) { - const callerPubkey = action.payload.pubkey; - const existingCallState = state.conversationLookup[callerPubkey].callState; - if (existingCallState) { - return state; - } - const foundConvo = getConversationController().get(callerPubkey); - if (!foundConvo) { - return state; - } - // we have to update the model itself. - // not the db (as we dont want to store that field in it) - // and not the redux store directly as it gets overriden by the commit() of the conversationModel - foundConvo.callState = 'offering'; - void foundConvo.commit(); - return state; - }, - setFullScreenCall(state: ConversationsStateType, action: PayloadAction) { - state.callIsInFullScreen = action.payload; - return state; - }, }, extraReducers: (builder: any) => { // Add reducers for additional action types here, and handle loading state as needed @@ -917,13 +816,6 @@ export const { quotedMessageToAnimate, setNextMessageToPlayId, updateMentionsMembers, - // calls - incomingCall, - endCall, - answerCall, - callConnected, - startingCallWith, - setFullScreenCall, } = actions; export async function openConversationWithMessages(args: { diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 04b206e3b..4e81828d7 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -6,6 +6,7 @@ import { reducer as user, UserStateType } from './ducks/user'; import { reducer as theme, ThemeStateType } from './ducks/theme'; import { reducer as section, SectionStateType } from './ducks/section'; import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms'; +import { callReducer as call, CallStateType } from './ducks/call'; import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion'; import { modalReducer as modals, ModalState } from './ducks/modalDialog'; @@ -28,6 +29,7 @@ export type StateType = { userConfig: UserConfigState; timerOptions: TimerOptionsState; stagedAttachments: StagedAttachmentsStateType; + call: CallStateType; }; export const reducers = { @@ -42,6 +44,7 @@ export const reducers = { userConfig, timerOptions, stagedAttachments, + call, }; // Making this work would require that our reducer signature supported AnyAction, not diff --git a/ts/state/selectors/call.ts b/ts/state/selectors/call.ts new file mode 100644 index 000000000..819938245 --- /dev/null +++ b/ts/state/selectors/call.ts @@ -0,0 +1,103 @@ +import { createSelector } from 'reselect'; +import { CallStateType } from '../ducks/call'; +import { ConversationsStateType, ReduxConversationType } from '../ducks/conversations'; +import { StateType } from '../reducer'; +import { getConversations, getSelectedConversationKey } from './conversations'; + +export const getCallState = (state: StateType): CallStateType => state.call; + +// --- INCOMING CALLS +export const getHasIncomingCallFrom = createSelector(getCallState, (state: CallStateType): + | string + | undefined => { + return state.ongoingWith && state.ongoingCallStatus === 'incoming' + ? state.ongoingWith + : undefined; +}); + +export const getHasIncomingCall = createSelector( + getHasIncomingCallFrom, + (withConvo: string | undefined): boolean => !!withConvo +); + +// --- ONGOING CALLS +export const getHasOngoingCallWith = createSelector( + getConversations, + getCallState, + (convos: ConversationsStateType, callState: CallStateType): ReduxConversationType | undefined => { + if ( + callState.ongoingWith && + (callState.ongoingCallStatus === 'connecting' || + callState.ongoingCallStatus === 'offering' || + callState.ongoingCallStatus === 'ongoing') + ) { + return convos.conversationLookup[callState.ongoingWith] || undefined; + } + return undefined; + } +); + +export const getHasOngoingCall = createSelector( + getHasOngoingCallWith, + (withConvo: ReduxConversationType | undefined): boolean => !!withConvo +); + +export const getHasOngoingCallWithPubkey = createSelector( + getHasOngoingCallWith, + (withConvo: ReduxConversationType | undefined): string | undefined => withConvo?.id +); + +export const getHasOngoingCallWithFocusedConvo = createSelector( + getHasOngoingCallWithPubkey, + getSelectedConversationKey, + (withPubkey, selectedPubkey) => { + return withPubkey && withPubkey === selectedPubkey; + } +); + +export const getHasOngoingCallWithFocusedConvoIsOffering = createSelector( + getCallState, + getSelectedConversationKey, + (callState: CallStateType, selectedConvoPubkey?: string): boolean => { + if ( + !selectedConvoPubkey || + !callState.ongoingWith || + callState.ongoingCallStatus !== 'offering' || + selectedConvoPubkey !== callState.ongoingWith + ) { + return false; + } + + return true; + } +); + +export const getHasOngoingCallWithFocusedConvosIsConnecting = createSelector( + getCallState, + getSelectedConversationKey, + (callState: CallStateType, selectedConvoPubkey?: string): boolean => { + if ( + !selectedConvoPubkey || + !callState.ongoingWith || + callState.ongoingCallStatus !== 'connecting' || + selectedConvoPubkey !== callState.ongoingWith + ) { + return false; + } + + return true; + } +); + +export const getHasOngoingCallWithNonFocusedConvo = createSelector( + getHasOngoingCallWithPubkey, + getSelectedConversationKey, + (withPubkey, selectedPubkey) => { + return withPubkey && withPubkey !== selectedPubkey; + } +); + +export const getCallIsInFullScreen = createSelector( + getCallState, + (callState): boolean => callState.callIsInFullScreen +); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 301ae73d4..de9fc35a4 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -95,99 +95,6 @@ export const getConversationById = createSelector( } ); -export const getHasIncomingCallFrom = createSelector( - getConversations, - (state: ConversationsStateType): string | undefined => { - const foundEntry = Object.entries(state.conversationLookup).find( - ([_convoKey, convo]) => convo.callState === 'incoming' - ); - - if (!foundEntry) { - return undefined; - } - return foundEntry[1].id; - } -); - -export const getHasOngoingCallWith = createSelector( - getConversations, - (state: ConversationsStateType): ReduxConversationType | undefined => { - const foundEntry = Object.entries(state.conversationLookup).find( - ([_convoKey, convo]) => - convo.callState === 'connecting' || - convo.callState === 'offering' || - convo.callState === 'ongoing' - ); - - if (!foundEntry) { - return undefined; - } - return foundEntry[1]; - } -); - -export const getHasIncomingCall = createSelector( - getHasIncomingCallFrom, - (withConvo: string | undefined): boolean => !!withConvo -); - -export const getHasOngoingCall = createSelector( - getHasOngoingCallWith, - (withConvo: ReduxConversationType | undefined): boolean => !!withConvo -); - -export const getHasOngoingCallWithPubkey = createSelector( - getHasOngoingCallWith, - (withConvo: ReduxConversationType | undefined): string | undefined => withConvo?.id -); - -export const getHasOngoingCallWithFocusedConvo = createSelector( - getHasOngoingCallWithPubkey, - getSelectedConversationKey, - (withPubkey, selectedPubkey) => { - return withPubkey && withPubkey === selectedPubkey; - } -); - -export const getHasOngoingCallWithFocusedConvoIsOffering = createSelector( - getConversations, - getSelectedConversationKey, - (state: ConversationsStateType, selectedConvoPubkey?: string): boolean => { - if (!selectedConvoPubkey) { - return false; - } - const isOffering = state.conversationLookup[selectedConvoPubkey]?.callState === 'offering'; - - return Boolean(isOffering); - } -); - -export const getHasOngoingCallWithFocusedConvosIsConnecting = createSelector( - getConversations, - getSelectedConversationKey, - (state: ConversationsStateType, selectedConvoPubkey?: string): boolean => { - if (!selectedConvoPubkey) { - return false; - } - const isOffering = state.conversationLookup[selectedConvoPubkey]?.callState === 'connecting'; - - return Boolean(isOffering); - } -); - -export const getHasOngoingCallWithNonFocusedConvo = createSelector( - getHasOngoingCallWithPubkey, - getSelectedConversationKey, - (withPubkey, selectedPubkey) => { - return withPubkey && withPubkey !== selectedPubkey; - } -); - -export const getCallIsInFullScreen = createSelector( - getConversations, - (state: ConversationsStateType): boolean => state.callIsInFullScreen -); - export const getIsTypingEnabled = createSelector( getConversations, getSelectedConversationKey, diff --git a/ts/state/smart/SessionConversation.ts b/ts/state/smart/SessionConversation.ts index 03cc046ff..0cf347fa9 100644 --- a/ts/state/smart/SessionConversation.ts +++ b/ts/state/smart/SessionConversation.ts @@ -4,7 +4,6 @@ import { SessionConversation } from '../../components/session/conversation/Sessi import { StateType } from '../reducer'; import { getTheme } from '../selectors/theme'; import { - getHasOngoingCallWithFocusedConvo, getLightBoxOptions, getSelectedConversation, getSelectedConversationKey, @@ -15,6 +14,7 @@ import { } from '../selectors/conversations'; import { getOurNumber } from '../selectors/user'; import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments'; +import { getHasOngoingCallWithFocusedConvo } from '../selectors/call'; const mapStateToProps = (state: StateType) => { return { From 1dff310820971230a89376be4b08fe8b6a58b9a1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Nov 2021 12:08:51 +1100 Subject: [PATCH 07/13] no video track by default and will be turn ON if asked to --- ts/receiver/callMessage.ts | 3 +- ts/session/sending/MessageSentHandler.ts | 1 + ts/session/utils/CallManager.ts | 108 +++++++++++++---------- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts index 8b0d4122e..cbf0d4891 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -20,7 +20,8 @@ export async function handleCallMessage( // we just allow self send of ANSWER message to remove the incoming call dialog when we accepted it from another device if ( sender === UserUtils.getOurPubKeyStrFromCache() && - callMessage.type !== SignalService.CallMessage.Type.ANSWER + callMessage.type !== SignalService.CallMessage.Type.ANSWER && + callMessage.type !== SignalService.CallMessage.Type.END_CALL ) { window.log.info('Dropping incoming call from ourself'); await removeFromCache(envelope); diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 9d78abd10..a0c3dcc18 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -6,6 +6,7 @@ import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/Ope import { EncryptionType, RawMessage } from '../types'; import { UserUtils } from '../utils'; +// tslint:disable-next-line: no-unnecessary-class export class MessageSentHandler { public static async handlePublicMessageSentSuccess( sentMessage: OpenGroupVisibleMessage, diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 3ab4e7866..5d1171865 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -52,7 +52,7 @@ function callVideoListeners() { if (videoEventsListeners.length) { videoEventsListeners.forEach(item => { item.listener?.({ - localStream: mediaDevices, + localStream, remoteStream, camerasList, audioInputsList, @@ -92,7 +92,7 @@ const callCache = new Map>> let peerConnection: RTCPeerConnection | null; let dataChannel: RTCDataChannel | null; let remoteStream: MediaStream | null; -let mediaDevices: MediaStream | null; +let localStream: MediaStream | null; let remoteVideoStreamIsMuted = true; export const DEVICE_DISABLED_DEVICE_ID = 'off'; @@ -238,29 +238,32 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { if (!peerConnection) { throw new Error('cannot selectCameraByDeviceId without a peer connection'); } - let sender = peerConnection.getSenders().find(s => { + + // video might be completely off on start. adding a track like this triggers a negotationneeded event + window.log.info('adding/replacing video track'); + const sender = peerConnection.getSenders().find(s => { return s.track?.kind === videoTrack.kind; }); - // video might be completely off - if (!sender) { - peerConnection.addTrack(videoTrack); - } - sender = peerConnection.getSenders().find(s => { - return s.track?.kind === videoTrack.kind; - }); + videoTrack.enabled = true; if (sender) { + // this should not trigger a negotationneeded event + // and it is needed for when the video cam was never turn on await sender.replaceTrack(videoTrack); - videoTrack.enabled = true; - mediaDevices?.getVideoTracks().forEach(t => { - t.stop(); - mediaDevices?.removeTrack(t); - }); - mediaDevices?.addTrack(videoTrack); - - sendVideoStatusViaDataChannel(); - callVideoListeners(); + } else { + // this will trigger a negotiationeeded event + peerConnection.addTrack(videoTrack, newVideoStream); } + + // do the same changes locally + localStream?.getVideoTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); + localStream?.addTrack(videoTrack); + + sendVideoStatusViaDataChannel(); + callVideoListeners(); } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); callVideoListeners(); @@ -403,19 +406,16 @@ async function openMediaDevicesAndAddTracks() { video: false, }; - mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig); - mediaDevices.getTracks().map(track => { - // if (track.kind === 'video') { - // track.enabled = false; - // } - if (mediaDevices) { - peerConnection?.addTrack(track, mediaDevices); + localStream = await navigator.mediaDevices.getUserMedia(devicesConfig); + localStream.getTracks().map(track => { + if (localStream) { + peerConnection?.addTrack(track, localStream); } }); } catch (err) { window.log.warn('openMediaDevices: ', err); ToastUtils.pushVideoCallPermissionNeeded(); - await closeVideoCall(); + closeVideoCall(); } callVideoListeners(); } @@ -520,7 +520,7 @@ const findLastMessageTypeFromSender = (sender: string, msgType: SignalService.Ca function handleSignalingStateChangeEvent() { if (peerConnection?.signalingState === 'closed') { - void closeVideoCall(); + closeVideoCall(); } } @@ -528,14 +528,14 @@ function handleConnectionStateChanged(pubkey: string) { window.log.info('handleConnectionStateChanged :', peerConnection?.connectionState); if (peerConnection?.signalingState === 'closed' || peerConnection?.connectionState === 'failed') { - void closeVideoCall(); + closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { setIsRinging(false); window.inboxStore?.dispatch(callConnected({ pubkey })); } } -async function closeVideoCall() { +function closeVideoCall() { window.log.info('closingVideoCall '); setIsRinging(false); if (peerConnection) { @@ -551,14 +551,16 @@ async function closeVideoCall() { dataChannel.close(); dataChannel = null; } - if (mediaDevices) { - mediaDevices.getTracks().forEach(track => { + if (localStream) { + localStream.getTracks().forEach(track => { track.stop(); + localStream?.removeTrack(track); }); } if (remoteStream) { remoteStream.getTracks().forEach(track => { + track.stop(); remoteStream?.removeTrack(track); }); } @@ -567,7 +569,7 @@ async function closeVideoCall() { peerConnection = null; } - mediaDevices = null; + localStream = null; remoteStream = null; selectedCameraId = DEVICE_DISABLED_DEVICE_ID; selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; @@ -577,6 +579,7 @@ async function closeVideoCall() { window.inboxStore?.dispatch(endCall()); remoteVideoStreamIsMuted = true; + timestampAcceptedCall = undefined; makingOffer = false; ignoreOffer = false; @@ -638,11 +641,17 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) dataChannel.onmessage = onDataChannelReceivedMessage; dataChannel.onopen = onDataChannelOnOpen; - if (!isAcceptingCall) { - peerConnection.onnegotiationneeded = async () => { + peerConnection.onnegotiationneeded = async () => { + const shouldTriggerAnotherNeg = + isAcceptingCall && timestampAcceptedCall && Date.now() - timestampAcceptedCall > 1000; + if (!isAcceptingCall || shouldTriggerAnotherNeg) { await handleNegotiationNeededEvent(withPubkey); - }; - } + } else { + window.log.info( + 'should negotaite again but we accepted the call recently, so swallowing this one' + ); + } + }; peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; @@ -685,6 +694,8 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) return peerConnection; } +let timestampAcceptedCall: number | undefined; + // tslint:disable-next-line: function-name export async function USER_acceptIncomingCallRequest(fromSender: string) { window.log.info('USER_acceptIncomingCallRequest'); @@ -719,6 +730,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } currentCallUUID = lastOfferMessage.uuid; + timestampAcceptedCall = Date.now(); peerConnection = createOrGetPeerConnection(fromSender, true); await openMediaDevicesAndAddTracks(); @@ -774,7 +786,8 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI // tslint:disable-next-line: function-name export async function USER_rejectIncomingCallRequest(fromSender: string) { setIsRinging(false); - + // close the popup call + window.inboxStore?.dispatch(endCall()); const lastOfferMessage = findLastMessageTypeFromSender( fromSender, SignalService.CallMessage.Type.OFFER @@ -798,16 +811,15 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) { // clear the ongoing call if needed if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) { - await closeVideoCall(); + closeVideoCall(); } - - // close the popup call - window.inboxStore?.dispatch(endCall()); } async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { - await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage); - await getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage); + await Promise.all([ + getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage), + getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage), + ]); } // tslint:disable-next-line: function-name @@ -834,7 +846,7 @@ export async function USER_hangup(fromSender: string) { clearCallCacheFromPubkeyAndUUID(fromSender, currentCallUUID); - await closeVideoCall(); + closeVideoCall(); } /** @@ -858,14 +870,14 @@ export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: stri (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') && ongoingCallWith === ownerOfCall ) { - await closeVideoCall(); + closeVideoCall(); window.inboxStore?.dispatch(endCall()); } return; } if (aboutCallUUID === currentCallUUID) { - await closeVideoCall(); + closeVideoCall(); window.inboxStore?.dispatch(endCall()); } } @@ -1069,7 +1081,7 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe rejectedCallUUIDS.add(callMessageUUID); // if this call is about the one being currently displayed, force close it if (ongoingCallStatus && ongoingCallWith === foundOwnerOfCallUUID) { - await closeVideoCall(); + closeVideoCall(); } window.inboxStore?.dispatch(endCall()); From d5f6180ae64293211f46a003d7852005c038bc7d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 14:36:02 +1100 Subject: [PATCH 08/13] create offer and answer ourselves and do not use the negotiation needed event. this event is causing us to loop in negotiation needed when each side try to create one, gets the answer and so on... --- ts/session/utils/CallManager.ts | 167 ++++++++++++++++---------------- ts/state/ducks/call.tsx | 1 + 2 files changed, 87 insertions(+), 81 deletions(-) diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 5d1171865..2a2f3c1c0 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -25,7 +25,10 @@ import { PnServer } from '../../pushnotification'; import { setIsRinging } from './RingingManager'; export type InputItem = { deviceId: string; label: string }; +// tslint:disable: function-name +const maxWidth = 1920; +const maxHeight = 1080; /** * This uuid is set only once we accepted a call or started one. */ @@ -166,6 +169,28 @@ if (typeof navigator !== 'undefined') { }); } +const silence = () => { + const ctx = new AudioContext(); + const oscillator = ctx.createOscillator(); + const dst = oscillator.connect(ctx.createMediaStreamDestination()); + oscillator.start(); + return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false }); +}; + +const black = () => { + const canvas = Object.assign(document.createElement('canvas'), { + width: maxWidth, + height: maxHeight, + }); + canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight); + const stream = (canvas as any).captureStream(); + return Object.assign(stream.getVideoTracks()[0], { enabled: false }); +}; + +const getBlackSilenceMediaStream = () => { + return new MediaStream([black(), silence()]); +}; + async function updateConnectedDevices() { // Get the set of cameras connected const videoCameras = await getConnectedDevices('videoinput'); @@ -219,6 +244,14 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { if (sender?.track) { sender.track.enabled = false; } + + // do the same changes locally + localStream?.getVideoTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); + localStream?.addTrack(getBlackSilenceMediaStream().getVideoTracks()[0]); + sendVideoStatusViaDataChannel(); callVideoListeners(); return; @@ -235,24 +268,23 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { try { const newVideoStream = await navigator.mediaDevices.getUserMedia(devicesConfig); const videoTrack = newVideoStream.getVideoTracks()[0]; + if (!peerConnection) { throw new Error('cannot selectCameraByDeviceId without a peer connection'); } - // video might be completely off on start. adding a track like this triggers a negotationneeded event - window.log.info('adding/replacing video track'); - const sender = peerConnection.getSenders().find(s => { - return s.track?.kind === videoTrack.kind; - }); + window.log.info('replacing video track'); + const videoSender = peerConnection + .getTransceivers() + .find(t => t.sender.track?.kind === 'video')?.sender; videoTrack.enabled = true; - if (sender) { - // this should not trigger a negotationneeded event - // and it is needed for when the video cam was never turn on - await sender.replaceTrack(videoTrack); + if (videoSender) { + await videoSender.replaceTrack(videoTrack); } else { - // this will trigger a negotiationeeded event - peerConnection.addTrack(videoTrack, newVideoStream); + throw new Error( + 'We should always have a videoSender as we are using a black video when no camera are in use' + ); } // do the same changes locally @@ -266,6 +298,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { callVideoListeners(); } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); + ToastUtils.pushToastError('selectCamera', e.message); callVideoListeners(); } } @@ -281,6 +314,12 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { if (sender?.track) { sender.track.enabled = false; } + // do the same changes locally + localStream?.getAudioTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); + localStream?.addTrack(getBlackSilenceMediaStream().getAudioTracks()[0]); callVideoListeners(); return; } @@ -295,6 +334,7 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { try { const newAudioStream = await navigator.mediaDevices.getUserMedia(devicesConfig); + const audioTrack = newAudioStream.getAudioTracks()[0]; if (!peerConnection) { throw new Error('cannot selectAudioInputByDeviceId without a peer connection'); @@ -331,18 +371,15 @@ export async function selectAudioOutputByDeviceId(audioOutputDeviceId: string) { } } -async function handleNegotiationNeededEvent(recipient: string) { +async function createOfferAndSendIt(recipient: string) { try { makingOffer = true; - window.log.info('got handleNegotiationNeeded event. creating offer'); - const offer = await peerConnection?.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }); + window.log.info('got createOfferAndSendIt event. creating offer'); + await (peerConnection as any)?.setLocalDescription(); + const offer = peerConnection?.localDescription; if (!offer) { throw new Error('Could not create an offer'); } - await peerConnection?.setLocalDescription(offer); if (!currentCallUUID) { window.log.warn('cannot send offer without a currentCallUUID'); @@ -357,18 +394,18 @@ async function handleNegotiationNeededEvent(recipient: string) { uuid: currentCallUUID, }); - window.log.info(`sending OFFER MESSAGE with callUUID: ${currentCallUUID}`); - const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( + window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`); + const negotiationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably( PubKey.cast(recipient), offerMessage ); - if (typeof negotationOfferSendResult === 'number') { + if (typeof negotiationOfferSendResult === 'number') { // window.log?.warn('setting last sent timestamp'); - lastOutgoingOfferTimestamp = negotationOfferSendResult; + lastOutgoingOfferTimestamp = negotiationOfferSendResult; } } } catch (err) { - window.log?.error(`Error on handling negotiation needed ${err}`); + window.log?.error(`Error createOfferAndSendIt ${err}`); } finally { makingOffer = false; } @@ -390,23 +427,13 @@ async function openMediaDevicesAndAddTracks() { return; } - selectedAudioInputId = audioInputsList[0].deviceId; + selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; //audioInputsList[0].deviceId; selectedCameraId = DEVICE_DISABLED_DEVICE_ID; window.log.info( `openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}` ); - const devicesConfig = { - audio: { - deviceId: { exact: selectedAudioInputId }, - - echoCancellation: true, - }, - // we don't need a video stream on start - video: false, - }; - - localStream = await navigator.mediaDevices.getUserMedia(devicesConfig); + localStream = getBlackSilenceMediaStream(); localStream.getTracks().map(track => { if (localStream) { peerConnection?.addTrack(track, localStream); @@ -420,7 +447,6 @@ async function openMediaDevicesAndAddTracks() { callVideoListeners(); } -// tslint:disable-next-line: function-name export async function USER_callRecipient(recipient: string) { if (!getCallMediaPermissionsSettings()) { ToastUtils.pushVideoCallPermissionNeeded(); @@ -457,6 +483,7 @@ export async function USER_callRecipient(recipient: string) { await openMediaDevicesAndAddTracks(); setIsRinging(true); + await createOfferAndSendIt(recipient); } const iceCandidates: Array = new Array(); @@ -579,7 +606,6 @@ function closeVideoCall() { window.inboxStore?.dispatch(endCall()); remoteVideoStreamIsMuted = true; - timestampAcceptedCall = undefined; makingOffer = false; ignoreOffer = false; @@ -626,7 +652,7 @@ function onDataChannelOnOpen() { sendVideoStatusViaDataChannel(); } -function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) { +function createOrGetPeerConnection(withPubkey: string) { if (peerConnection) { return peerConnection; } @@ -640,21 +666,7 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) dataChannel.onmessage = onDataChannelReceivedMessage; dataChannel.onopen = onDataChannelOnOpen; - - peerConnection.onnegotiationneeded = async () => { - const shouldTriggerAnotherNeg = - isAcceptingCall && timestampAcceptedCall && Date.now() - timestampAcceptedCall > 1000; - if (!isAcceptingCall || shouldTriggerAnotherNeg) { - await handleNegotiationNeededEvent(withPubkey); - } else { - window.log.info( - 'should negotaite again but we accepted the call recently, so swallowing this one' - ); - } - }; - peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; - peerConnection.ontrack = event => { event.track.onunmute = () => { remoteStream?.addTrack(event.track); @@ -694,9 +706,6 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) return peerConnection; } -let timestampAcceptedCall: number | undefined; - -// tslint:disable-next-line: function-name export async function USER_acceptIncomingCallRequest(fromSender: string) { window.log.info('USER_acceptIncomingCallRequest'); setIsRinging(false); @@ -730,8 +739,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } currentCallUUID = lastOfferMessage.uuid; - timestampAcceptedCall = Date.now(); - peerConnection = createOrGetPeerConnection(fromSender, true); + peerConnection = createOrGetPeerConnection(fromSender); await openMediaDevicesAndAddTracks(); @@ -783,7 +791,6 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI clearCallCacheFromPubkeyAndUUID(fromSender, forcedUUID); } -// tslint:disable-next-line: function-name export async function USER_rejectIncomingCallRequest(fromSender: string) { setIsRinging(false); // close the popup call @@ -822,7 +829,6 @@ async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { ]); } -// tslint:disable-next-line: function-name export async function USER_hangup(fromSender: string) { window.log.info('USER_hangup'); @@ -889,16 +895,12 @@ async function buildAnswerAndSendIt(sender: string) { window.log.warn('cannot send answer without a currentCallUUID'); return; } - - const answer = await peerConnection.createAnswer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }); + await (peerConnection as any).setLocalDescription(); + const answer = peerConnection.localDescription; if (!answer?.sdp || answer.sdp.length === 0) { window.log.warn('failed to create answer'); return; } - await peerConnection.setLocalDescription(answer); const answerSdp = answer.sdp; const callAnswerMessage = new CallMessage({ timestamp: Date.now(), @@ -955,25 +957,26 @@ export async function handleCallTypeOffer( !makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending); const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp; const offerCollision = !readyForOffer; - ignoreOffer = !polite && offerCollision; + if (ignoreOffer) { window.log?.warn('Received offer when unready for offer; Ignoring offer.'); return; } - if (remoteCallUUID === currentCallUUID && currentCallUUID) { + if (peerConnection && remoteCallUUID === currentCallUUID && currentCallUUID) { window.log.info('Got a new offer message from our ongoing call'); - isSettingRemoteAnswerPending = false; - const remoteDesc = new RTCSessionDescription({ + + const remoteOfferDesc = new RTCSessionDescription({ type: 'offer', sdp: callMessage.sdps[0], }); isSettingRemoteAnswerPending = false; - if (peerConnection) { - await peerConnection.setRemoteDescription(remoteDesc); // SRD rolls back as needed - await buildAnswerAndSendIt(sender); - } + + await peerConnection.setRemoteDescription(remoteOfferDesc); // SRD rolls back as needed + isSettingRemoteAnswerPending = false; + + await buildAnswerAndSendIt(sender); } else { window.inboxStore?.dispatch(incomingCall({ pubkey: sender })); @@ -1103,19 +1106,21 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe pubkey: sender, }) ); - const remoteDesc = new RTCSessionDescription({ - type: 'answer', - sdp: callMessage.sdps[0], - }); - // window.log?.info('Setting remote answer pending'); - isSettingRemoteAnswerPending = true; try { + isSettingRemoteAnswerPending = true; + + const remoteDesc = new RTCSessionDescription({ + type: 'answer', + sdp: callMessage.sdps[0], + }); + await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed } catch (e) { - window.log.warn('setRemoteDescription failed:', e); + window.log.warn('setRemoteDescriptio failed:', e); + } finally { + isSettingRemoteAnswerPending = false; } - isSettingRemoteAnswerPending = false; } export async function handleCallTypeIceCandidates( diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx index 47e9e3bdb..7f0c55a27 100644 --- a/ts/state/ducks/call.tsx +++ b/ts/state/ducks/call.tsx @@ -92,6 +92,7 @@ const callSlice = createSlice({ // only set in full screen if we have an ongoing call if (state.ongoingWith && state.ongoingCallStatus === 'ongoing' && action.payload) { state.callIsInFullScreen = true; + return state; } state.callIsInFullScreen = false; return state; From 53289298a9eba01ffa27462ec8b51e15b340e0db Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 14:39:38 +1100 Subject: [PATCH 09/13] auto select the first audio input on connection success webrtc --- ts/session/utils/CallManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 2a2f3c1c0..d4d068950 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -558,6 +558,10 @@ function handleConnectionStateChanged(pubkey: string) { closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { setIsRinging(false); + const firstAudioInput = audioInputsList?.[0].deviceId || undefined; + if (firstAudioInput) { + void selectAudioInputByDeviceId(firstAudioInput); + } window.inboxStore?.dispatch(callConnected({ pubkey })); } } From a4daabfa752c3efe74dbb31779dde80c44510535 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 15:04:43 +1100 Subject: [PATCH 10/13] add a way to choose the audioouput/mute a webrtc call --- ts/components/session/calling/CallButtons.tsx | 68 +++++++++--------- .../calling/InConversationCallContainer.tsx | 8 +-- ts/hooks/useVideoEventListener.ts | 9 +-- ts/session/utils/{ => calling}/CallManager.ts | 69 ++++++++----------- ts/session/utils/calling/Silence.ts | 24 +++++++ ts/session/utils/index.ts | 2 +- 6 files changed, 95 insertions(+), 85 deletions(-) rename ts/session/utils/{ => calling}/CallManager.ts (95%) create mode 100644 ts/session/utils/calling/Silence.ts diff --git a/ts/components/session/calling/CallButtons.tsx b/ts/components/session/calling/CallButtons.tsx index bfb2b583b..8b14869dd 100644 --- a/ts/components/session/calling/CallButtons.tsx +++ b/ts/components/session/calling/CallButtons.tsx @@ -1,6 +1,6 @@ import { SessionIconButton } from '../icon'; import { animation, contextMenu, Item, Menu } from 'react-contexify'; -import { InputItem } from '../../../session/utils/CallManager'; +import { InputItem } from '../../../session/utils/calling/CallManager'; import { setFullScreenCall } from '../../../state/ducks/call'; import { CallManager, ToastUtils } from '../../../session/utils'; import React from 'react'; @@ -71,16 +71,16 @@ export const AudioInputButton = ({ export const AudioOutputButton = ({ currentConnectedAudioOutputs, -}: // isAudioOutputMuted, -// hideArrowIcon = false, -{ + isAudioOutputMuted, + hideArrowIcon = false, +}: { currentConnectedAudioOutputs: Array; isAudioOutputMuted: boolean; hideArrowIcon?: boolean; }) => { return ( <> - {/* { @@ -90,7 +90,7 @@ export const AudioOutputButton = ({ showAudioOutputMenu(currentConnectedAudioOutputs, e); }} hidePopoverArrow={hideArrowIcon} - /> */} + /> , -// e: React.MouseEvent -// ) => { -// if (currentConnectedAudioOutputs.length === 0) { -// ToastUtils.pushNoAudioOutputFound(); -// return; -// } -// contextMenu.show({ -// id: audioOutputTriggerId, -// event: e, -// }); -// }; +const showAudioOutputMenu = ( + currentConnectedAudioOutputs: Array, + e: React.MouseEvent +) => { + if (currentConnectedAudioOutputs.length === 0) { + ToastUtils.pushNoAudioOutputFound(); + return; + } + contextMenu.show({ + id: audioOutputTriggerId, + event: e, + }); +}; const showVideoInputMenu = ( currentConnectedCameras: Array, @@ -300,22 +300,22 @@ const handleMicrophoneToggle = async ( } }; -// const handleSpeakerToggle = async ( -// currentConnectedAudioOutputs: Array, -// isAudioOutputMuted: boolean -// ) => { -// if (!currentConnectedAudioOutputs.length) { -// ToastUtils.pushNoAudioInputFound(); +const handleSpeakerToggle = async ( + currentConnectedAudioOutputs: Array, + isAudioOutputMuted: boolean +) => { + if (!currentConnectedAudioOutputs.length) { + ToastUtils.pushNoAudioInputFound(); -// return; -// } -// if (isAudioOutputMuted) { -// // selects the first one -// await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId); -// } else { -// await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID); -// } -// }; + return; + } + if (isAudioOutputMuted) { + // selects the first one + await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId); + } else { + await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID); + } +}; const StyledCallWindowControls = styled.div` position: absolute; diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index d8c748ed0..f7d42944f 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -23,7 +23,7 @@ import { import { useModuloWithTripleDots } from '../../../hooks/useModuloWithTripleDots'; import { CallWindowControls } from './CallButtons'; import { SessionSpinner } from '../SessionSpinner'; -import { DEVICE_DISABLED_DEVICE_ID } from '../../../session/utils/CallManager'; +import { DEVICE_DISABLED_DEVICE_ID } from '../../../session/utils/calling/CallManager'; const VideoContainer = styled.div` height: 100%; @@ -156,10 +156,10 @@ export const InConversationCallContainer = () => { if (videoRefRemote.current) { if (currentSelectedAudioOutput === DEVICE_DISABLED_DEVICE_ID) { - videoRefLocal.current.muted = true; + videoRefRemote.current.muted = true; } else { - // void videoRefLocal.current.setSinkId(currentSelectedAudioOutput); - videoRefLocal.current.muted = false; + // void videoRefRemote.current.setSinkId(currentSelectedAudioOutput); + videoRefRemote.current.muted = false; } } } diff --git a/ts/hooks/useVideoEventListener.ts b/ts/hooks/useVideoEventListener.ts index 401420cde..515be62d7 100644 --- a/ts/hooks/useVideoEventListener.ts +++ b/ts/hooks/useVideoEventListener.ts @@ -2,12 +2,13 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; // tslint:disable-next-line: no-submodule-imports import useMountedState from 'react-use/lib/useMountedState'; -import { CallManager } from '../session/utils'; import { + addVideoEventsListener, CallManagerOptionsType, DEVICE_DISABLED_DEVICE_ID, InputItem, -} from '../session/utils/CallManager'; + removeVideoEventsListener, +} from '../session/utils/calling/CallManager'; import { getSelectedConversationKey } from '../state/selectors/conversations'; import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call'; @@ -40,7 +41,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { (onSame && ongoingCallPubkey === selectedConversationKey) || (!onSame && ongoingCallPubkey !== selectedConversationKey) ) { - CallManager.addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => { + addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => { const { audioInputsList, audioOutputsList, @@ -68,7 +69,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { } return () => { - CallManager.removeVideoEventsListener(uniqueId); + removeVideoEventsListener(uniqueId); }; }, [ongoingCallPubkey, selectedConversationKey, isFullScreen]); diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/calling/CallManager.ts similarity index 95% rename from ts/session/utils/CallManager.ts rename to ts/session/utils/calling/CallManager.ts index d4d068950..b84c0e776 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -1,10 +1,10 @@ import _ from 'lodash'; -import { MessageUtils, ToastUtils, UserUtils } from '.'; -import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings'; -import { getConversationById } from '../../data/data'; -import { MessageModelType } from '../../models/messageType'; -import { SignalService } from '../../protobuf'; -import { openConversationWithMessages } from '../../state/ducks/conversations'; +import { MessageUtils, ToastUtils, UserUtils } from '../'; +import { getCallMediaPermissionsSettings } from '../../../components/session/settings/SessionSettings'; +import { getConversationById } from '../../../data/data'; +import { MessageModelType } from '../../../models/messageType'; +import { SignalService } from '../../../protobuf'; +import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { answerCall, callConnected, @@ -13,22 +13,23 @@ import { incomingCall, setFullScreenCall, startingCallWith, -} from '../../state/ducks/call'; -import { getConversationController } from '../conversations'; -import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; -import { ed25519Str } from '../onions/onionPath'; -import { getMessageQueue, MessageSender } from '../sending'; -import { PubKey } from '../types'; +} from '../../../state/ducks/call'; +import { getConversationController } from '../../conversations'; +import { CallMessage } from '../../messages/outgoing/controlMessage/CallMessage'; +import { ed25519Str } from '../../onions/onionPath'; +import { PubKey } from '../../types'; import { v4 as uuidv4 } from 'uuid'; -import { PnServer } from '../../pushnotification'; -import { setIsRinging } from './RingingManager'; +import { PnServer } from '../../../pushnotification'; +import { setIsRinging } from '../RingingManager'; +import { getBlackSilenceMediaStream } from './Silence'; +import { getMessageQueue } from '../..'; +import { MessageSender } from '../../sending'; -export type InputItem = { deviceId: string; label: string }; // tslint:disable: function-name -const maxWidth = 1920; -const maxHeight = 1080; +export type InputItem = { deviceId: string; label: string }; + /** * This uuid is set only once we accepted a call or started one. */ @@ -169,28 +170,6 @@ if (typeof navigator !== 'undefined') { }); } -const silence = () => { - const ctx = new AudioContext(); - const oscillator = ctx.createOscillator(); - const dst = oscillator.connect(ctx.createMediaStreamDestination()); - oscillator.start(); - return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false }); -}; - -const black = () => { - const canvas = Object.assign(document.createElement('canvas'), { - width: maxWidth, - height: maxHeight, - }); - canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight); - const stream = (canvas as any).captureStream(); - return Object.assign(stream.getVideoTracks()[0], { enabled: false }); -}; - -const getBlackSilenceMediaStream = () => { - return new MediaStream([black(), silence()]); -}; - async function updateConnectedDevices() { // Get the set of cameras connected const videoCameras = await getConnectedDevices('videoinput'); @@ -339,12 +318,13 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { if (!peerConnection) { throw new Error('cannot selectAudioInputByDeviceId without a peer connection'); } - const sender = peerConnection.getSenders().find(s => { + const audioSender = peerConnection.getSenders().find(s => { return s.track?.kind === audioTrack.kind; }); + window.log.info('replacing audio track'); - if (sender) { - await sender.replaceTrack(audioTrack); + if (audioSender) { + await audioSender.replaceTrack(audioTrack); // we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves) } else { throw new Error('Failed to get sender for selectAudioInputByDeviceId '); @@ -562,6 +542,11 @@ function handleConnectionStateChanged(pubkey: string) { if (firstAudioInput) { void selectAudioInputByDeviceId(firstAudioInput); } + + const firstAudioOutput = audioOutputsList?.[0].deviceId || undefined; + if (firstAudioOutput) { + void selectAudioOutputByDeviceId(firstAudioOutput); + } window.inboxStore?.dispatch(callConnected({ pubkey })); } } diff --git a/ts/session/utils/calling/Silence.ts b/ts/session/utils/calling/Silence.ts new file mode 100644 index 000000000..9d86488de --- /dev/null +++ b/ts/session/utils/calling/Silence.ts @@ -0,0 +1,24 @@ +const maxWidth = 1920; +const maxHeight = 1080; + +const silence = () => { + const ctx = new AudioContext(); + const oscillator = ctx.createOscillator(); + const dst = oscillator.connect(ctx.createMediaStreamDestination()); + oscillator.start(); + return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false }); +}; + +const black = () => { + const canvas = Object.assign(document.createElement('canvas'), { + width: maxWidth, + height: maxHeight, + }); + canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight); + const stream = (canvas as any).captureStream(); + return Object.assign(stream.getVideoTracks()[0], { enabled: false }); +}; + +export const getBlackSilenceMediaStream = () => { + return new MediaStream([black(), silence()]); +}; diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index 6bc07f0dc..19ab86ab3 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -8,7 +8,7 @@ import * as UserUtils from './User'; import * as SyncUtils from './syncUtils'; import * as AttachmentsV2Utils from './AttachmentsV2'; import * as AttachmentDownloads from './AttachmentsDownload'; -import * as CallManager from './CallManager'; +import * as CallManager from './calling/CallManager'; export * from './Attachments'; export * from './TypedEmitter'; From e716f73d6cf68ff8a64696f0af46bf0bf2a8f567 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Nov 2021 15:28:27 +1100 Subject: [PATCH 11/13] mute audio from bg when video is in fullscreen this is to avoid having two times the remote sound playing one in the bg and one in the fullscreen --- .../session/calling/InConversationCallContainer.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index f7d42944f..85d0384f3 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import _ from 'underscore'; import { UserUtils } from '../../../session/utils'; import { + getCallIsInFullScreen, getHasOngoingCallWith, getHasOngoingCallWithFocusedConvo, getHasOngoingCallWithFocusedConvoIsOffering, @@ -119,6 +120,8 @@ export const VideoLoadingSpinner = (props: { fullWidth: boolean }) => { export const InConversationCallContainer = () => { const ongoingCallProps = useSelector(getHasOngoingCallWith); + const isInFullScreen = useSelector(getCallIsInFullScreen); + const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey); const ongoingCallWithFocused = useSelector(getHasOngoingCallWithFocusedConvo); const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name; @@ -158,12 +161,17 @@ export const InConversationCallContainer = () => { if (currentSelectedAudioOutput === DEVICE_DISABLED_DEVICE_ID) { videoRefRemote.current.muted = true; } else { - // void videoRefRemote.current.setSinkId(currentSelectedAudioOutput); + void (videoRefRemote.current as any)?.setSinkId(currentSelectedAudioOutput); videoRefRemote.current.muted = false; } } } + if (isInFullScreen && videoRefRemote.current) { + // disable this video element so the one in fullscreen is the only one playing audio + videoRefRemote.current.muted = true; + } + if (!ongoingCallWithFocused) { return null; } From 8fea533124a2a23a32ac1432dbfde8b683ccaf2a Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Nov 2021 09:50:17 +1100 Subject: [PATCH 12/13] darken a bit the green of sent message box in light theme --- ts/state/ducks/SessionTheme.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/state/ducks/SessionTheme.tsx b/ts/state/ducks/SessionTheme.tsx index df22031c7..1f73c9bdc 100644 --- a/ts/state/ducks/SessionTheme.tsx +++ b/ts/state/ducks/SessionTheme.tsx @@ -149,7 +149,7 @@ const lightColorTextSubtle = `${black}99`; const lightColorTextAccent = '#00c769'; const lightColorSessionShadow = `0 0 4px 0 ${black}5E`; const lightColorComposeViewBg = '#efefef'; -const lightColorSentMessageBg = accentLightTheme; +const lightColorSentMessageBg = 'hsl(152, 100%, 40%)'; const lightColorSentMessageText = white; const lightColorClickableHovered = '#dfdfdf'; const lightColorSessionBorderColor = borderLightThemeColor; From af75b6f0e293cf4cf1c7aef7e801ff93f442ae96 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Nov 2021 10:32:11 +1100 Subject: [PATCH 13/13] disable deduplication based serverId+sender only use the serverTimestamp+sender for searching because serverId+sender might have false positive --- app/sql.js | 16 ---------------- ts/data/data.ts | 19 ------------------- ts/receiver/dataMessage.ts | 18 +++--------------- 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/app/sql.js b/app/sql.js index 217f7ba56..a1d421450 100644 --- a/app/sql.js +++ b/app/sql.js @@ -60,7 +60,6 @@ module.exports = { getUnreadByConversation, getUnreadCountByConversation, getMessageBySender, - getMessageBySenderAndServerId, getMessageBySenderAndServerTimestamp, getMessageBySenderAndTimestamp, getMessageIdsFromServerIds, @@ -2058,21 +2057,6 @@ function getMessageBySender({ source, sourceDevice, sentAt }) { return map(rows, row => jsonToObject(row.json)); } -function getMessageBySenderAndServerId({ source, serverId }) { - const rows = globalInstance - .prepare( - `SELECT json FROM ${MESSAGES_TABLE} WHERE - source = $source AND - serverId = $serverId;` - ) - .all({ - source, - serverId, - }); - - return map(rows, row => jsonToObject(row.json)); -} - function getMessageBySenderAndTimestamp({ source, timestamp }) { const rows = globalInstance .prepare( diff --git a/ts/data/data.ts b/ts/data/data.ts index c8c912086..8665a9da0 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -110,7 +110,6 @@ const channelsToMake = { removeAllMessagesInConversation, getMessageBySender, - getMessageBySenderAndServerId, getMessageBySenderAndServerTimestamp, getMessageBySenderAndTimestamp, getMessageIdsFromServerIds, @@ -690,24 +689,6 @@ export async function getMessageBySender({ return new MessageModel(messages[0]); } -export async function getMessageBySenderAndServerId({ - source, - serverId, -}: { - source: string; - serverId: number; -}): Promise { - const messages = await channels.getMessageBySenderAndServerId({ - source, - serverId, - }); - if (!messages || !messages.length) { - return null; - } - - return new MessageModel(messages[0]); -} - export async function getMessageBySenderAndServerTimestamp({ source, serverTimestamp, diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index b41265c09..f0619d434 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -12,11 +12,7 @@ import { getConversationController } from '../session/conversations'; import { handleClosedGroupControlMessage } from './closedGroups'; import { MessageModel } from '../models/message'; import { MessageModelType } from '../models/messageType'; -import { - getMessageBySender, - getMessageBySenderAndServerId, - getMessageBySenderAndServerTimestamp, -} from '../../ts/data/data'; +import { getMessageBySender, getMessageBySenderAndServerTimestamp } from '../../ts/data/data'; import { ConversationModel, ConversationTypeEnum } from '../models/conversation'; import { allowOnlyOneAtATime } from '../session/utils/Promise'; import { toHex } from '../session/utils/String'; @@ -368,22 +364,14 @@ export async function isMessageDuplicate({ try { let result; if (serverId || serverTimestamp) { - // first try to find a duplicate serverId from this sender - if (serverId) { - result = await getMessageBySenderAndServerId({ - source, - serverId, - }); - } - // if no result, try to find a duplicate with the same serverTimestamp from this sender + // first try to find a duplicate with the same serverTimestamp from this sender if (!result && serverTimestamp) { result = await getMessageBySenderAndServerTimestamp({ source, serverTimestamp, }); } - // if we have a result, it means a specific user sent two messages either with the same - // serverId or the same serverTimestamp. + // if we have a result, it means a specific user sent two messages either with the same serverTimestamp. // no need to do anything else, those messages must be the same // Note: this test is not based on which conversation the user sent the message // but we consider that a user sending two messages with the same serverTimestamp is unlikely