From b85425ff83701e3fc90894cc6dc7f8b4581faf27 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 5 Oct 2021 16:38:08 +1100 Subject: [PATCH] make a draggable or in conversation call container --- ts/components/Lightbox.tsx | 1 - ts/components/session/ActionsPanel.tsx | 7 +- .../session/calling/CallContainer.tsx | 244 ++++++++++-------- .../session/calling/IncomingCallDialog.tsx | 67 +++++ .../conversation/SessionConversation.tsx | 2 + .../SessionQuotedMessageComposition.tsx | 2 +- ts/components/session/menu/Menu.tsx | 4 +- ts/session/utils/AttachmentsDownload.ts | 17 +- ts/session/utils/CallManager.ts | 28 +- 9 files changed, 241 insertions(+), 131 deletions(-) create mode 100644 ts/components/session/calling/IncomingCallDialog.tsx diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 84d90e4d1..339953747 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -279,7 +279,6 @@ export const Lightbox = (props: Props) => { }; const onContainerClick = (event: React.MouseEvent) => { - debugger; if (renderedRef && event.target === renderedRef.current) { return; } diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 80b397f73..ef9a246d9 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -46,7 +46,9 @@ import { loadDefaultRooms } from '../../opengroup/opengroupV2/ApiUtil'; import { ActionPanelOnionStatusLight } from '../dialog/OnionStatusPathDialog'; import { switchHtmlToDarkTheme, switchHtmlToLightTheme } from '../../state/ducks/SessionTheme'; -import { CallContainer } from './calling/CallContainer'; +import { DraggableCallContainer } from './calling/CallContainer'; +import { IncomingCallDialog } from './calling/IncomingCallDialog'; + const Section = (props: { type: SectionType; avatarPath?: string | null }) => { const ourNumber = useSelector(getOurNumber); const unreadMessageCount = useSelector(getUnreadMessageCount); @@ -288,7 +290,8 @@ export const ActionsPanel = () => { <> - + +
diff --git a/ts/components/session/calling/CallContainer.tsx b/ts/components/session/calling/CallContainer.tsx index aff8c2174..8b746a8ba 100644 --- a/ts/components/session/calling/CallContainer.tsx +++ b/ts/components/session/calling/CallContainer.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import Draggable from 'react-draggable'; +import Draggable, { DraggableData, DraggableEvent } from 'react-draggable'; // tslint:disable-next-line: no-submodule-imports import useMountedState from 'react-use/lib/useMountedState'; @@ -8,66 +8,44 @@ import styled from 'styled-components'; import _ from 'underscore'; import { CallManager } from '../../../session/utils'; import { - getHasIncomingCall, - getHasIncomingCallFrom, getHasOngoingCall, getHasOngoingCallWith, + getSelectedConversationKey, } from '../../../state/selectors/conversations'; -import { SessionButton, SessionButtonColor } from '../SessionButton'; -import { SessionWrapperModal } from '../SessionWrapperModal'; +import { SessionButton } from '../SessionButton'; -export const CallWindow = styled.div` +export const DraggableCallWindow = styled.div` position: absolute; z-index: 9; - padding: 1rem; - top: 50vh; - left: 50vw; - transform: translate(-50%, -50%); + box-shadow: var(--color-session-shadow); + max-height: 300px; + width: 300px; display: flex; flex-direction: column; background-color: var(--color-modal-background); border: var(--session-border); `; -// similar styling to modal header -const CallWindowHeader = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - align-self: center; - - padding: $session-margin-lg; - - font-family: $session-font-default; - text-align: center; - line-height: 18px; - font-size: $session-font-md; - font-weight: 700; +const StyledVideoElement = styled.video` + padding: 0 1rem; + height: 100%; + width: 100%; `; -const VideoContainer = styled.div` - position: relative; - max-height: 60vh; +const StyledDraggableVideoElement = styled(StyledVideoElement)` + padding: 0 0; `; -const VideoContainerRemote = styled.video` - max-height: inherit; -`; -const VideoContainerLocal = styled.video` - max-height: 45%; - max-width: 45%; - position: absolute; - bottom: 0; - right: 0; +const CallWindowControls = styled.div` + padding: 5px; + flex-shrink: 0; `; -const CallWindowInner = styled.div` - text-align: center; - padding: 1rem; -`; +const DraggableCallWindowInner = styled.div``; -const CallWindowControls = styled.div` - padding: 5px; +const VideoContainer = styled.div` + height: 100%; + width: 50%; `; // TODO: @@ -75,94 +53,144 @@ const CallWindowControls = styled.div` * Add mute input, deafen, end call, possibly add person to call * duration - look at how duration calculated for recording. */ -export const CallContainer = () => { - const hasIncomingCall = useSelector(getHasIncomingCall); - const incomingCallProps = useSelector(getHasIncomingCallFrom); +export const DraggableCallContainer = () => { const ongoingCallProps = useSelector(getHasOngoingCallWith); + const selectedConversationKey = useSelector(getSelectedConversationKey); const hasOngoingCall = useSelector(getHasOngoingCall); - const ongoingOrIncomingPubkey = ongoingCallProps?.id || incomingCallProps?.id; - const videoRefRemote = useRef(); - const videoRefLocal = useRef(); + const [positionX, setPositionX] = useState(0); + const [positionY, setPositionY] = useState(0); + + const ongoingCallPubkey = ongoingCallProps?.id; + const videoRefRemote = useRef(undefined); const mountedState = useMountedState(); + function onWindowResize() { + if (positionY + 50 > window.innerHeight || positionX + 50 > window.innerWidth) { + setPositionX(window.innerWidth / 2); + setPositionY(window.innerHeight / 2); + } + } + + useEffect(() => { + window.addEventListener('resize', onWindowResize); + + return () => { + window.removeEventListener('resize', onWindowResize); + }; + }, [positionX, positionY]); + useEffect(() => { - CallManager.setVideoEventsListener( - (localStream: MediaStream | null, remoteStream: MediaStream | null) => { - if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) { - videoRefLocal.current.srcObject = localStream; - videoRefRemote.current.srcObject = remoteStream; + if (ongoingCallPubkey !== selectedConversationKey) { + CallManager.setVideoEventsListener( + (_localStream: MediaStream | null, remoteStream: MediaStream | null) => { + if (mountedState() && videoRefRemote?.current) { + videoRefRemote.current.srcObject = remoteStream; + } } - } - ); + ); + } return () => { CallManager.setVideoEventsListener(null); }; - }, []); + }, [ongoingCallPubkey, selectedConversationKey]); - //#region input handlers - const handleAcceptIncomingCall = async () => { - if (incomingCallProps?.id) { - await CallManager.USER_acceptIncomingCallRequest(incomingCallProps.id); + const handleEndCall = async () => { + // call method to end call connection + if (ongoingCallPubkey) { + await CallManager.USER_rejectIncomingCallRequest(ongoingCallPubkey); } }; - const handleDeclineIncomingCall = async () => { - // close the modal - if (incomingCallProps?.id) { - await CallManager.USER_rejectIncomingCallRequest(incomingCallProps.id); + if (!hasOngoingCall || !ongoingCallProps || ongoingCallPubkey === selectedConversationKey) { + return null; + } + + console.warn('rendering with pos', positionX, positionY); + + return ( + { + console.warn('setting position ', { x: data.x, y: data.y }); + setPositionX(data.x); + setPositionY(data.y); + }} + > + + + + + + + + + + ); +}; + +export const InConvoCallWindow = styled.div` + padding: 1rem; + display: flex; + height: 50%; + + /* background-color: var(--color-background-primary); */ + + background: radial-gradient(black, #505050); + + flex-shrink: 0; + min-height: 200px; + align-items: center; +`; + +export const InConversationCallContainer = () => { + const ongoingCallProps = useSelector(getHasOngoingCallWith); + const selectedConversationKey = useSelector(getSelectedConversationKey); + const hasOngoingCall = useSelector(getHasOngoingCall); + + const ongoingCallPubkey = ongoingCallProps?.id; + const videoRefRemote = useRef(); + const videoRefLocal = useRef(); + const mountedState = useMountedState(); + + useEffect(() => { + if (ongoingCallPubkey === selectedConversationKey) { + CallManager.setVideoEventsListener( + (localStream: MediaStream | null, remoteStream: MediaStream | null) => { + if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) { + videoRefLocal.current.srcObject = localStream; + videoRefRemote.current.srcObject = remoteStream; + } + } + ); } - }; + + return () => { + CallManager.setVideoEventsListener(null); + }; + }, [ongoingCallPubkey, selectedConversationKey]); const handleEndCall = async () => { // call method to end call connection - if (ongoingOrIncomingPubkey) { - await CallManager.USER_rejectIncomingCallRequest(ongoingOrIncomingPubkey); + if (ongoingCallPubkey) { + await CallManager.USER_rejectIncomingCallRequest(ongoingCallPubkey); } }; - //#endregion - - if (!hasOngoingCall && !hasIncomingCall) { + if (!hasOngoingCall || !ongoingCallProps || ongoingCallPubkey !== selectedConversationKey) { return null; } - if (hasOngoingCall && ongoingCallProps) { - return ( - - - Call with: {ongoingCallProps.name} - - -
{hasIncomingCall}
- - - - -
- - - -
-
- ); - } - - if (hasIncomingCall) { - return ( - -
- - -
-
- ); - } - // display spinner while connecting - return null; + return ( + + + + + + + + + ); }; diff --git a/ts/components/session/calling/IncomingCallDialog.tsx b/ts/components/session/calling/IncomingCallDialog.tsx new file mode 100644 index 000000000..365c8c9cd --- /dev/null +++ b/ts/components/session/calling/IncomingCallDialog.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import styled from 'styled-components'; +import _ from 'underscore'; +import { CallManager } from '../../../session/utils'; +import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/conversations'; +import { SessionButton, SessionButtonColor } from '../SessionButton'; +import { SessionWrapperModal } from '../SessionWrapperModal'; + +export const CallWindow = styled.div` + position: absolute; + z-index: 9; + padding: 1rem; + top: 50vh; + left: 50vw; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + background-color: var(--color-modal-background); + border: var(--session-border); +`; + +// TODO: +/** + * Add mute input, deafen, end call, possibly add person to call + * duration - look at how duration calculated for recording. + */ +export const IncomingCallDialog = () => { + const hasIncomingCall = useSelector(getHasIncomingCall); + const incomingCallProps = useSelector(getHasIncomingCallFrom); + + //#region input handlers + const handleAcceptIncomingCall = async () => { + if (incomingCallProps?.id) { + await CallManager.USER_acceptIncomingCallRequest(incomingCallProps.id); + } + }; + + const handleDeclineIncomingCall = async () => { + // close the modal + if (incomingCallProps?.id) { + await CallManager.USER_rejectIncomingCallRequest(incomingCallProps.id); + } + }; + + if (!hasIncomingCall) { + return null; + } + + if (hasIncomingCall) { + return ( + +
+ + +
+
+ ); + } + // display spinner while connecting + return null; +}; diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 9a000e415..34372c7fc 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -43,6 +43,7 @@ import { import { SessionButtonColor } from '../SessionButton'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { addStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments'; +import { InConversationCallContainer } from '../calling/CallContainer'; interface State { showRecordingView: boolean; @@ -263,6 +264,7 @@ export class SessionConversation extends React.Component { {lightBoxOptions?.media && this.renderLightBox(lightBoxOptions)}
+ diff --git a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx index c814ee9dd..591a55860 100644 --- a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx +++ b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx @@ -19,7 +19,7 @@ const QuotedMessageCompositionReply = styled.div` background: var(--color-quote-bottom-bar-background); border-radius: var(--margins-sm); padding: var(--margins-xs); - box-shadow: --color-session-shadow; + box-shadow: var(--color-session-shadow); margin: var(--margins-xs); `; diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 14ea628e4..4048c4bca 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -324,7 +324,9 @@ export function getMarkAllReadMenuItem(conversationId: string): JSX.Element | nu export function getStartCallMenuItem(conversationId: string): JSX.Element | null { if (window?.lokiFeatureFlags.useCallMessage) { - const canCall = !(useSelector(getHasIncomingCall) || useSelector(getHasOngoingCall)); + const hasIncomingCall = useSelector(getHasIncomingCall); + const hasOngoingCall = useSelector(getHasOngoingCall); + const canCall = !(hasIncomingCall || hasOngoingCall); return ( { diff --git a/ts/session/utils/AttachmentsDownload.ts b/ts/session/utils/AttachmentsDownload.ts index d99e0eaa6..37edd9e6e 100644 --- a/ts/session/utils/AttachmentsDownload.ts +++ b/ts/session/utils/AttachmentsDownload.ts @@ -1,7 +1,7 @@ -import { isNumber, omit } from 'lodash'; -import _ from 'lodash'; +import { filter, isNumber, omit } from 'lodash'; // tslint:disable-next-line: no-submodule-imports import { default as getGuid } from 'uuid/v4'; +import * as Constants from '../constants'; import { getMessageById, getNextAttachmentDownloadJobs, @@ -17,16 +17,13 @@ import { downloadAttachment, downloadAttachmentOpenGroupV2 } from '../../receive // this cause issues if we increment that value to > 1. const MAX_ATTACHMENT_JOB_PARALLELISM = 3; -const SECOND = 1000; -const MINUTE = SECOND * 60; -const HOUR = MINUTE * 60; -const TICK_INTERVAL = MINUTE; +const TICK_INTERVAL = Constants.DURATION.MINUTES; // tslint:disable: function-name const RETRY_BACKOFF = { - 1: SECOND * 30, - 2: MINUTE * 30, - 3: HOUR * 6, + 1: Constants.DURATION.SECONDS * 30, + 2: Constants.DURATION.MINUTES * 30, + 3: Constants.DURATION.HOURS * 6, }; let enabled = false; @@ -113,7 +110,7 @@ async function _maybeStartJob() { return; } - const nextJobsWithoutCurrentlyRunning = _.filter( + const nextJobsWithoutCurrentlyRunning = filter( nextJobs, j => _activeAttachmentDownloadJobs[j.id] === undefined ); diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 98491f8ae..f23fea93f 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -25,6 +25,9 @@ let videoEventsListener: CallManagerListener; export function setVideoEventsListener(listener: CallManagerListener) { videoEventsListener = listener; + if (videoEventsListener) { + videoEventsListener(mediaDevices, remoteStream); + } } /** @@ -33,6 +36,8 @@ export function setVideoEventsListener(listener: CallManagerListener) { const callCache = new Map>(); let peerConnection: RTCPeerConnection | null; +let remoteStream: MediaStream | null; +let mediaDevices: MediaStream | null; const ENABLE_VIDEO = true; @@ -66,12 +71,13 @@ export async function USER_callRecipient(recipient: string) { } peerConnection = new RTCPeerConnection(configuration); - let mediaDevices: any; try { mediaDevices = await openMediaDevices(); mediaDevices.getTracks().map((track: any) => { window.log.info('USER_callRecipient adding track: ', track); - peerConnection?.addTrack(track, mediaDevices); + if (mediaDevices) { + peerConnection?.addTrack(track, mediaDevices); + } }); } catch (err) { ToastUtils.pushMicAndCameraPermissionNeeded(() => { @@ -131,7 +137,7 @@ export async function USER_callRecipient(recipient: string) { } }; - const remoteStream = new MediaStream(); + remoteStream = new MediaStream(); if (videoEventsListener) { videoEventsListener(mediaDevices, remoteStream); @@ -141,7 +147,9 @@ export async function USER_callRecipient(recipient: string) { if (videoEventsListener) { videoEventsListener(mediaDevices, remoteStream); } - remoteStream.addTrack(event.track); + if (remoteStream) { + remoteStream.addTrack(event.track); + } }); const offerDescription = await peerConnection.createOffer({ @@ -254,12 +262,14 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { peerConnection = null; } peerConnection = new RTCPeerConnection(configuration); - const mediaDevices = await openMediaDevices(); + mediaDevices = await openMediaDevices(); mediaDevices.getTracks().map(track => { // window.log.info('USER_acceptIncomingCallRequest adding track ', track); - peerConnection?.addTrack(track, mediaDevices); + if (mediaDevices) { + peerConnection?.addTrack(track, mediaDevices); + } }); - const remoteStream = new MediaStream(); + remoteStream = new MediaStream(); peerConnection.addEventListener('icecandidate', event => { window.log?.warn('icecandidateerror:', event); @@ -279,7 +289,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { if (videoEventsListener) { videoEventsListener(mediaDevices, remoteStream); } - remoteStream.addTrack(event.track); + remoteStream?.addTrack(event.track); }); peerConnection.addEventListener('connectionstatechange', _event => { window.log.info( @@ -363,6 +373,8 @@ export function handleEndCallMessage(sender: string) { if (videoEventsListener) { videoEventsListener(null, null); } + mediaDevices = null; + remoteStream = null; // // FIXME audric trigger UI cleanup window.inboxStore?.dispatch(endCall({ pubkey: sender }));