diff --git a/app/sql.js b/app/sql.js index f81655201..5125442f3 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/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; } 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 +144,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/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/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/calling/CallButtons.tsx b/ts/components/session/calling/CallButtons.tsx index 0469ee810..8b14869dd 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 { InputItem } from '../../../session/utils/calling/CallManager'; +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'; @@ -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/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..85d0384f3 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -5,12 +5,13 @@ import styled from 'styled-components'; import _ from 'underscore'; import { UserUtils } from '../../../session/utils'; import { + getCallIsInFullScreen, getHasOngoingCallWith, getHasOngoingCallWithFocusedConvo, getHasOngoingCallWithFocusedConvoIsOffering, getHasOngoingCallWithFocusedConvosIsConnecting, getHasOngoingCallWithPubkey, -} from '../../../state/selectors/conversations'; +} from '../../../state/selectors/call'; import { StyledVideoElement } from './DraggableCallContainer'; import { Avatar, AvatarSize } from '../../Avatar'; @@ -23,7 +24,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%; @@ -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; @@ -156,14 +159,19 @@ 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 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; } 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/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}
diff --git a/ts/data/data.ts b/ts/data/data.ts index 7a94130e7..6924c6355 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/hooks/useVideoEventListener.ts b/ts/hooks/useVideoEventListener.ts index 203614a8e..515be62d7 100644 --- a/ts/hooks/useVideoEventListener.ts +++ b/ts/hooks/useVideoEventListener.ts @@ -2,17 +2,15 @@ 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'; -import { - getCallIsInFullScreen, - getHasOngoingCallWithPubkey, - getSelectedConversationKey, -} from '../state/selectors/conversations'; + removeVideoEventsListener, +} from '../session/utils/calling/CallManager'; +import { getSelectedConversationKey } from '../state/selectors/conversations'; +import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call'; export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { const selectedConversationKey = useSelector(getSelectedConversationKey); @@ -43,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, @@ -71,7 +69,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) { } return () => { - CallManager.removeVideoEventsListener(uniqueId); + removeVideoEventsListener(uniqueId); }; }, [ongoingCallPubkey, selectedConversationKey, isFullScreen]); diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index f41b568a3..247cb3537 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -458,8 +458,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 7c1edb278..507b0f1d5 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -180,8 +180,6 @@ export const fillConvoAttributesWithDefaults = ( }); }; -export type CallState = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined; - export class ConversationModel extends Backbone.Model { public updateLastMessage: () => any; public throttledBumpTyping: () => void; @@ -189,8 +187,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; @@ -446,7 +442,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 @@ -554,10 +549,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 3670670fb..b1f387262 100644 --- a/ts/receiver/callMessage.ts +++ b/ts/receiver/callMessage.ts @@ -17,6 +17,24 @@ 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 && + callMessage.type !== SignalService.CallMessage.Type.END_CALL + ) { + 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 from this device ${callMessage.uuid}`); + return; + } + if (type === SignalService.CallMessage.Type.PROVISIONAL_ANSWER) { await removeFromCache(envelope); @@ -48,7 +66,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/dataMessage.ts b/ts/receiver/dataMessage.ts index c3da23b33..03a57c1f7 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'; @@ -371,22 +367,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 diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 0706d837b..eccf7d339 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/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 666e66d57..a0c3dcc18 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -1,13 +1,12 @@ 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 +// tslint:disable-next-line: no-unnecessary-class export class MessageSentHandler { public static async handlePublicMessageSentSuccess( sentMessage: OpenGroupVisibleMessage, @@ -54,10 +53,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 +110,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/calling/CallManager.ts similarity index 67% rename from ts/session/utils/CallManager.ts rename to ts/session/utils/calling/CallManager.ts index 56f613ed6..b84c0e776 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -1,33 +1,42 @@ import _ from 'lodash'; -import { MessageUtils, ToastUtils } 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 { 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, + CallStatusEnum, endCall, incomingCall, - openConversationWithMessages, setFullScreenCall, startingCallWith, -} from '../../state/ducks/conversations'; -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'; + +// tslint:disable: function-name 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(); + export type CallManagerOptionsType = { localStream: MediaStream | null; remoteStream: MediaStream | null; @@ -47,7 +56,7 @@ function callVideoListeners() { if (videoEventsListeners.length) { videoEventsListeners.forEach(item => { item.listener?.({ - localStream: mediaDevices, + localStream, remoteStream, camerasList, audioInputsList, @@ -80,14 +89,14 @@ 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>>(); 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'; @@ -103,8 +112,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 @@ -184,6 +223,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; @@ -200,28 +247,37 @@ 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'); } - const sender = peerConnection.getSenders().find(s => { - return s.track?.kind === videoTrack.kind; - }); - if (sender) { - await sender.replaceTrack(videoTrack); - videoTrack.enabled = true; - mediaDevices?.getVideoTracks().forEach(t => { - t.stop(); - mediaDevices?.removeTrack(t); - }); - mediaDevices?.addTrack(videoTrack); - sendVideoStatusViaDataChannel(); - callVideoListeners(); + window.log.info('replacing video track'); + const videoSender = peerConnection + .getTransceivers() + .find(t => t.sender.track?.kind === 'video')?.sender; + + videoTrack.enabled = true; + if (videoSender) { + await videoSender.replaceTrack(videoTrack); } else { - throw new Error('Failed to get sender for selectCameraByDeviceId '); + 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 + localStream?.getVideoTracks().forEach(t => { + t.stop(); + localStream?.removeTrack(t); + }); + localStream?.addTrack(videoTrack); + + sendVideoStatusViaDataChannel(); + callVideoListeners(); } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); + ToastUtils.pushToastError('selectCamera', e.message); callVideoListeners(); } } @@ -237,6 +293,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; } @@ -251,16 +313,18 @@ 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'); } - 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 '); @@ -287,18 +351,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'); @@ -313,18 +374,18 @@ async function handleNegotiationNeededEvent(recipient: string) { uuid: currentCallUUID, }); - window.log.info('sending OFFER MESSAGE'); - 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; } @@ -340,51 +401,32 @@ function handleIceCandidates(event: RTCPeerConnectionIceEvent, pubkey: string) { async function openMediaDevicesAndAddTracks() { try { await updateConnectedDevices(); - if (!camerasList.length) { - ToastUtils.pushNoCameraFound(); - return; - } + if (!audioInputsList.length) { ToastUtils.pushNoAudioInputFound(); return; } - selectedAudioInputId = audioInputsList[0].deviceId; + selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; //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, - - echoCancellation: true, - }, - video: { - deviceId: selectedCameraId, - // width: VIDEO_WIDTH, - // height: Math.floor(VIDEO_WIDTH * VIDEO_RATIO), - }, - }; - - mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig); - mediaDevices.getTracks().map(track => { - if (track.kind === 'video') { - track.enabled = false; - } - if (mediaDevices) { - peerConnection?.addTrack(track, mediaDevices); + localStream = getBlackSilenceMediaStream(); + localStream.getTracks().map(track => { + if (localStream) { + peerConnection?.addTrack(track, localStream); } }); } catch (err) { + window.log.warn('openMediaDevices: ', err); ToastUtils.pushVideoCallPermissionNeeded(); closeVideoCall(); } callVideoListeners(); } -// tslint:disable-next-line: function-name export async function USER_callRecipient(recipient: string) { if (!getCallMediaPermissionsSettings()) { ToastUtils.pushVideoCallPermissionNeeded(); @@ -412,12 +454,16 @@ 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); await openMediaDevicesAndAddTracks(); setIsRinging(true); + await createOfferAndSendIt(recipient); } const iceCandidates: Array = new Array(); @@ -455,7 +501,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); @@ -490,6 +538,15 @@ function handleConnectionStateChanged(pubkey: string) { closeVideoCall(); } else if (peerConnection?.connectionState === 'connected') { setIsRinging(false); + const firstAudioInput = audioInputsList?.[0].deviceId || undefined; + if (firstAudioInput) { + void selectAudioInputByDeviceId(firstAudioInput); + } + + const firstAudioOutput = audioOutputsList?.[0].deviceId || undefined; + if (firstAudioOutput) { + void selectAudioOutputByDeviceId(firstAudioOutput); + } window.inboxStore?.dispatch(callConnected({ pubkey })); } } @@ -510,14 +567,16 @@ 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); }); } @@ -526,13 +585,15 @@ function closeVideoCall() { peerConnection = null; } - mediaDevices = null; + localStream = null; remoteStream = null; selectedCameraId = DEVICE_DISABLED_DEVICE_ID; selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; currentCallUUID = undefined; window.inboxStore?.dispatch(setFullScreenCall(false)); + window.inboxStore?.dispatch(endCall()); + remoteVideoStreamIsMuted = true; makingOffer = false; @@ -542,24 +603,26 @@ 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; } @@ -578,7 +641,7 @@ function onDataChannelOnOpen() { sendVideoStatusViaDataChannel(); } -function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) { +function createOrGetPeerConnection(withPubkey: string) { if (peerConnection) { return peerConnection; } @@ -592,15 +655,7 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) dataChannel.onmessage = onDataChannelReceivedMessage; dataChannel.onopen = onDataChannelOnOpen; - - if (!isAcceptingCall) { - peerConnection.onnegotiationneeded = async () => { - await handleNegotiationNeededEvent(withPubkey); - }; - } - peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent; - peerConnection.ontrack = event => { event.track.onunmute = () => { remoteStream?.addTrack(event.track); @@ -640,7 +695,6 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) return peerConnection; } -// tslint:disable-next-line: function-name export async function USER_acceptIncomingCallRequest(fromSender: string) { window.log.info('USER_acceptIncomingCallRequest'); setIsRinging(false); @@ -674,7 +728,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { } currentCallUUID = lastOfferMessage.uuid; - peerConnection = createOrGetPeerConnection(fromSender, true); + peerConnection = createOrGetPeerConnection(fromSender); await openMediaDevicesAndAddTracks(); @@ -711,46 +765,59 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { await buildAnswerAndSendIt(fromSender); } -// tslint:disable-next-line: function-name +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); +} + 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 ); - const lastCallUUID = lastOfferMessage?.uuid; - window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${lastCallUUID}`); - if (lastCallUUID) { + const aboutCallUUID = 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); - + // 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, lastCallUUID); + clearCallCacheFromPubkeyAndUUID(fromSender, aboutCallUUID); } + const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux(); - 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(); - } + // clear the ongoing call if needed + if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) { + closeVideoCall(); } } -// tslint:disable-next-line: function-name +async function sendCallMessageAndSync(callmessage: CallMessage, user: string) { + await Promise.all([ + getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage), + getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage), + ]); +} + export async function USER_hangup(fromSender: string) { window.log.info('USER_hangup'); @@ -758,6 +825,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(), @@ -766,7 +834,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(); @@ -776,16 +844,36 @@ export async function USER_hangup(fromSender: string) { 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) { + rejectedCallUUIDS.add(aboutCallUUID); + clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID); + // 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); + + if ( + (ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') && + ongoingCallWith === ownerOfCall + ) { + closeVideoCall(); + window.inboxStore?.dispatch(endCall()); + } + return; + } + if (aboutCallUUID === currentCallUUID) { closeVideoCall(); - - window.inboxStore?.dispatch(endCall({ pubkey: sender })); + window.inboxStore?.dispatch(endCall()); } } } @@ -796,16 +884,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(), @@ -814,12 +898,15 @@ async function buildAnswerAndSendIt(sender: string) { uuid: currentCallUUID, }); - window.log.info('sending ANSWER MESSAGE'); - - await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage); + window.log.info('sending ANSWER MESSAGE and sync'); + await sendCallMessageAndSync(callAnswerMessage, sender); } } +export function isCallRejected(uuid: string) { + return rejectedCallUUIDS.has(uuid); +} + export async function handleCallTypeOffer( sender: string, callMessage: SignalService.CallMessage, @@ -832,45 +919,53 @@ 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 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; } const readyForOffer = !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 (callingConvos.length === 1 && callingConvos[0].id === sender) { + 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 })); @@ -927,32 +1022,94 @@ 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; } - 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 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. - pushCallMessageToCallCache(sender, remoteCallUUID, callMessage); + const isDeviceWhichJustAcceptedCall = currentCallUUID === callMessageUUID; + + 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) { + closeVideoCall(); + } + + window.inboxStore?.dispatch(endCall()); + } + } + return; + } else { + window.log.info(`handling callMessage ANSWER from ${callMessageUUID}`); + } + + pushCallMessageToCallCache(sender, callMessageUUID, callMessage); if (!peerConnection) { 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, + }) + ); - // window.log?.info('Setting remote answer pending'); - isSettingRemoteAnswerPending = true; - await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed - isSettingRemoteAnswerPending = false; + 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('setRemoteDescriptio failed:', e); + } finally { + isSettingRemoteAnswerPending = false; + } } export async function handleCallTypeIceCandidates( 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'; 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; diff --git a/ts/state/ducks/call.tsx b/ts/state/ducks/call.tsx new file mode 100644 index 000000000..7f0c55a27 --- /dev/null +++ b/ts/state/ducks/call.tsx @@ -0,0 +1,112 @@ +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; + return state; + } + 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 8ccf398ed..6124189f5 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'; @@ -254,7 +253,6 @@ export interface ReduxConversationType { isPinned?: boolean; isApproved?: boolean; - callState?: CallState; } export interface NotificationForConvoOption { @@ -278,7 +276,6 @@ export type ConversationsStateType = { quotedMessage?: ReplyingToMessageProps; areMoreMessagesBeingFetched: boolean; haveDoneFirstScroll: boolean; - callIsInFullScreen: boolean; showScrollButton: boolean; animateQuotedMessageId?: string; @@ -373,7 +370,6 @@ export function getEmptyConversationState(): ConversationsStateType { mentionMembers: [], firstUnreadMessageId: undefined, haveDoneFirstScroll: false, - callIsInFullScreen: false, }; } @@ -699,7 +695,6 @@ const conversationsSlice = createSlice({ return { conversationLookup: state.conversationLookup, - callIsInFullScreen: state.callIsInFullScreen, selectedConversation: action.payload.id, areMoreMessagesBeingFetched: false, @@ -763,102 +758,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 @@ -918,13 +817,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 44ebe7620..d508f1fbf 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -96,99 +96,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 {