diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index f80e651b9..f69e75b90 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -148,8 +148,8 @@
"linkPreviewsTitle": "Send Link Previews",
"linkPreviewDescription": "Previews are supported for most urls",
"linkPreviewsConfirmMessage": "You will not have full metadata protection when sending link previews.",
- "mediaPermissionsTitle": "Microphone and Camera",
- "mediaPermissionsDescription": "Allow access to camera and microphone",
+ "mediaPermissionsTitle": "Microphone",
+ "mediaPermissionsDescription": "Allow access to microphone",
"spellCheckTitle": "Spell Check",
"spellCheckDescription": "Enable spell check of text entered in message composition box",
"spellCheckDirty": "You must restart Session to apply your new settings",
@@ -439,8 +439,8 @@
"accept": "Accept",
"decline": "Decline",
"endCall": "End call",
- "micAndCameraPermissionNeededTitle": "Camera and Microphone access required",
- "micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy",
+ "cameraPermissionNeededTitle": "Voice/Video Call permissions required",
+ "cameraPermissionNeeded": "You can enable the 'Voice and video calls' permission in the Privacy Settings.",
"unableToCall": "cancel your ongoing call first",
"unableToCallTitle": "Cannot start new call",
"callMissed": "Missed call from $name$",
@@ -449,6 +449,7 @@
"noCameraFound": "No camera found",
"noAudioInputFound": "No audio input found",
"callMediaPermissionsTitle": "Voice and video calls",
+ "callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.",
"callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users",
"callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user."
}
diff --git a/ts/components/session/SessionToastContainer.tsx b/ts/components/session/SessionToastContainer.tsx
index d73415ba9..7788a1e0b 100644
--- a/ts/components/session/SessionToastContainer.tsx
+++ b/ts/components/session/SessionToastContainer.tsx
@@ -13,7 +13,7 @@ const SessionToastContainerPrivate = () => {
rtl={false}
pauseOnFocusLoss={false}
draggable={false}
- pauseOnHover={false}
+ pauseOnHover={true}
transition={Slide}
limit={5}
/>
diff --git a/ts/components/session/calling/CallContainer.tsx b/ts/components/session/calling/CallContainer.tsx
index 261fbf1fe..f31ef3a4b 100644
--- a/ts/components/session/calling/CallContainer.tsx
+++ b/ts/components/session/calling/CallContainer.tsx
@@ -15,6 +15,7 @@ import {
import { openConversationWithMessages } from '../../../state/ducks/conversations';
import { Avatar, AvatarSize } from '../../Avatar';
import { getConversationController } from '../../../session/conversations';
+import { CallManagerOptionsType } from '../../../session/utils/CallManager';
export const DraggableCallWindow = styled.div`
position: absolute;
@@ -28,11 +29,11 @@ export const DraggableCallWindow = styled.div`
border: var(--session-border);
`;
-export const StyledVideoElement = styled.video<{ isRemoteVideoMuted: boolean }>`
+export const StyledVideoElement = styled.video<{ isVideoMuted: boolean }>`
padding: 0 1rem;
height: 100%;
width: 100%;
- opacity: ${props => (props.isRemoteVideoMuted ? 0 : 1)};
+ opacity: ${props => (props.isVideoMuted ? 0 : 1)};
`;
const StyledDraggableVideoElement = styled(StyledVideoElement)`
@@ -97,16 +98,10 @@ export const DraggableCallContainer = () => {
useEffect(() => {
if (ongoingCallPubkey !== selectedConversationKey) {
CallManager.setVideoEventsListener(
- (
- _localStream: MediaStream | null,
- remoteStream: MediaStream | null,
- _camerasList: any,
- _audioList: any,
- remoteVideoIsMuted: boolean
- ) => {
+ ({ isRemoteVideoStreamMuted, remoteStream }: CallManagerOptionsType) => {
if (mountedState() && videoRefRemote?.current) {
videoRefRemote.current.srcObject = remoteStream;
- setIsRemoteVideoMuted(remoteVideoIsMuted);
+ setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
}
}
);
@@ -157,7 +152,7 @@ export const DraggableCallContainer = () => {
{isRemoteVideoMuted && (
diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx
index 7210ac299..7c6a41914 100644
--- a/ts/components/session/calling/InConversationCallContainer.tsx
+++ b/ts/components/session/calling/InConversationCallContainer.tsx
@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import useMountedState from 'react-use/lib/useMountedState';
import styled from 'styled-components';
import _ from 'underscore';
-import { CallManager, ToastUtils } from '../../../session/utils';
+import { CallManager, ToastUtils, UserUtils } from '../../../session/utils';
import {
getHasOngoingCall,
getHasOngoingCallWith,
@@ -13,7 +13,7 @@ import {
} from '../../../state/selectors/conversations';
import { SessionIconButton } from '../icon';
import { animation, contextMenu, Item, Menu } from 'react-contexify';
-import { InputItem } from '../../../session/utils/CallManager';
+import { CallManagerOptionsType, InputItem } from '../../../session/utils/CallManager';
import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton';
import { StyledVideoElement } from './CallContainer';
import { Avatar, AvatarSize } from '../../Avatar';
@@ -124,8 +124,9 @@ const AudioInputMenu = ({
};
const CenteredAvatarInConversation = styled.div`
- position: absolute;
- top: 0;
+ top: -50%;
+ transform: translateY(-50%);
+ position: relative;
bottom: 0;
left: 0;
right: 50%;
@@ -151,39 +152,50 @@ export const InConversationCallContainer = () => {
const videoRefLocal = useRef();
const mountedState = useMountedState();
- const [isVideoMuted, setVideoMuted] = useState(true);
+ const [isLocalVideoMuted, setLocalVideoMuted] = useState(true);
const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(true);
+
const [isAudioMuted, setAudioMuted] = useState(false);
const videoTriggerId = 'video-menu-trigger-id';
const audioTriggerId = 'audio-menu-trigger-id';
- const avatarPath = ongoingCallPubkey
+ const remoteAvatarPath = ongoingCallPubkey
? getConversationController()
.get(ongoingCallPubkey)
.getAvatarPath()
: undefined;
+ const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
+ const ourUsername = getConversationController()
+ .get(ourPubkey)
+ .getProfileName();
+
+ const ourAvatarPath = getConversationController()
+ .get(ourPubkey)
+ .getAvatarPath();
+
useEffect(() => {
if (ongoingCallPubkey === selectedConversationKey) {
- CallManager.setVideoEventsListener(
- (
- localStream: MediaStream | null,
- remoteStream: MediaStream | null,
- camerasList: Array,
- audioInputList: Array,
- isRemoteVideoStreamMuted: boolean
- ) => {
- if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
- videoRefLocal.current.srcObject = localStream;
- setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
- videoRefRemote.current.srcObject = remoteStream;
-
- setCurrentConnectedCameras(camerasList);
- setCurrentConnectedAudioInputs(audioInputList);
- }
+ CallManager.setVideoEventsListener((options: CallManagerOptionsType) => {
+ const {
+ audioInputsList,
+ camerasList,
+ isLocalVideoStreamMuted,
+ isRemoteVideoStreamMuted,
+ localStream,
+ remoteStream,
+ } = options;
+ if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) {
+ videoRefLocal.current.srcObject = localStream;
+ setIsRemoteVideoMuted(isRemoteVideoStreamMuted);
+ setLocalVideoMuted(isLocalVideoStreamMuted);
+ videoRefRemote.current.srcObject = remoteStream;
+
+ setCurrentConnectedCameras(camerasList);
+ setCurrentConnectedAudioInputs(audioInputsList);
}
- );
+ });
}
return () => {
@@ -204,14 +216,14 @@ export const InConversationCallContainer = () => {
return;
}
- if (isVideoMuted) {
+ if (isLocalVideoMuted) {
// select the first one
await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId);
} else {
await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID);
}
- setVideoMuted(!isVideoMuted);
+ setLocalVideoMuted(!isLocalVideoMuted);
};
const handleMicrophoneToggle = async () => {
@@ -263,13 +275,13 @@ export const InConversationCallContainer = () => {
{isRemoteVideoMuted && (
@@ -281,8 +293,18 @@ export const InConversationCallContainer = () => {
ref={videoRefLocal}
autoPlay={true}
muted={true}
- isRemoteVideoMuted={false}
+ isVideoMuted={isLocalVideoMuted}
/>
+ {isLocalVideoMuted && (
+
+
+
+ )}
@@ -298,7 +320,7 @@ export const InConversationCallContainer = () => {
/>
@@ -312,7 +334,7 @@ export const InConversationCallContainer = () => {
{
- setVideoMuted(false);
+ setLocalVideoMuted(false);
}}
camerasList={currentConnectedCameras}
/>
diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx
index 7a31dd885..651de525e 100644
--- a/ts/components/session/menu/Menu.tsx
+++ b/ts/components/session/menu/Menu.tsx
@@ -373,7 +373,7 @@ export function getStartCallMenuItem(conversationId: string): JSX.Element | null
}
if (!getCallMediaPermissionsSettings()) {
- ToastUtils.pushMicAndCameraPermissionNeeded();
+ ToastUtils.pushVideoCallPermissionNeeded();
return;
}
diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts
index 2a13e5ea4..fec70d7ae 100644
--- a/ts/session/utils/CallManager.ts
+++ b/ts/session/utils/CallManager.ts
@@ -22,26 +22,28 @@ export type InputItem = { deviceId: string; label: string };
// const VIDEO_WIDTH = 640;
// const VIDEO_RATIO = 16 / 9;
-type CallManagerListener =
- | ((
- localStream: MediaStream | null,
- remoteStream: MediaStream | null,
- camerasList: Array,
- audioInputsList: Array,
- isRemoteVideoStreamMuted: boolean
- ) => void)
- | null;
+export type CallManagerOptionsType = {
+ localStream: MediaStream | null;
+ remoteStream: MediaStream | null;
+ camerasList: Array;
+ audioInputsList: Array;
+ isLocalVideoStreamMuted: boolean;
+ isRemoteVideoStreamMuted: boolean;
+};
+
+export type CallManagerListener = ((options: CallManagerOptionsType) => void) | null;
let videoEventsListener: CallManagerListener;
function callVideoListener() {
if (videoEventsListener) {
- videoEventsListener(
- mediaDevices,
+ videoEventsListener({
+ localStream: mediaDevices,
remoteStream,
camerasList,
audioInputsList,
- remoteVideoStreamIsMuted
- );
+ isRemoteVideoStreamMuted: remoteVideoStreamIsMuted,
+ isLocalVideoStreamMuted: selectedCameraId === INPUT_DISABLED_DEVICE_ID,
+ });
}
}
@@ -79,7 +81,7 @@ const configuration: RTCConfiguration = {
iceTransportPolicy: 'relay',
};
-let selectedCameraId: string | undefined;
+let selectedCameraId: string = INPUT_DISABLED_DEVICE_ID;
let selectedAudioInputId: string | undefined;
let camerasList: Array = [];
let audioInputsList: Array = [];
@@ -115,8 +117,7 @@ async function updateInputLists() {
}
function sendVideoStatusViaDataChannel() {
- const videoEnabledLocally =
- selectedCameraId !== undefined && selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
+ const videoEnabledLocally = selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
const stringToSend = JSON.stringify({
video: videoEnabledLocally,
});
@@ -127,7 +128,7 @@ function sendVideoStatusViaDataChannel() {
export async function selectCameraByDeviceId(cameraDeviceId: string) {
if (cameraDeviceId === INPUT_DISABLED_DEVICE_ID) {
- selectedCameraId = cameraDeviceId;
+ selectedCameraId = INPUT_DISABLED_DEVICE_ID;
const sender = peerConnection?.getSenders().find(s => {
return s.track?.kind === 'video';
@@ -136,6 +137,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
sender.track.enabled = false;
}
sendVideoStatusViaDataChannel();
+ callVideoListener();
return;
}
if (camerasList.some(m => m.deviceId === cameraDeviceId)) {
@@ -164,12 +166,15 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
mediaDevices?.removeTrack(t);
});
mediaDevices?.addTrack(videoTrack);
+
sendVideoStatusViaDataChannel();
+ callVideoListener();
} else {
throw new Error('Failed to get sender for selectCameraByDeviceId ');
}
} catch (e) {
window.log.warn('selectCameraByDeviceId failed with', e.message);
+ callVideoListener();
}
}
}
@@ -301,7 +306,7 @@ async function openMediaDevicesAndAddTracks() {
}
});
} catch (err) {
- ToastUtils.pushMicAndCameraPermissionNeeded();
+ ToastUtils.pushVideoCallPermissionNeeded();
closeVideoCall();
}
callVideoListener();
@@ -310,7 +315,7 @@ async function openMediaDevicesAndAddTracks() {
// tslint:disable-next-line: function-name
export async function USER_callRecipient(recipient: string) {
if (!getCallMediaPermissionsSettings()) {
- ToastUtils.pushMicAndCameraPermissionNeeded();
+ ToastUtils.pushVideoCallPermissionNeeded();
return;
}
await updateInputLists();
@@ -420,8 +425,16 @@ function closeVideoCall() {
mediaDevices = null;
remoteStream = null;
+ selectedCameraId = INPUT_DISABLED_DEVICE_ID;
if (videoEventsListener) {
- videoEventsListener(null, null, [], [], true);
+ videoEventsListener({
+ audioInputsList: [],
+ camerasList: [],
+ isLocalVideoStreamMuted: true,
+ isRemoteVideoStreamMuted: true,
+ localStream: null,
+ remoteStream: null,
+ });
}
}
@@ -592,7 +605,14 @@ export function handleCallTypeEndCall(sender: string) {
if (callingConvos.length === 1 && callingConvos[0].id === sender) {
closeVideoCall();
if (videoEventsListener) {
- videoEventsListener(null, null, [], [], true);
+ videoEventsListener({
+ audioInputsList: [],
+ camerasList: [],
+ isLocalVideoStreamMuted: true,
+ isRemoteVideoStreamMuted: true,
+ localStream: null,
+ remoteStream: null,
+ });
}
window.inboxStore?.dispatch(endCall({ pubkey: sender }));
}
@@ -633,21 +653,20 @@ export async function handleCallTypeOffer(
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);
+ await handleMissedCall(sender, incomingOfferTimestamp, false);
return;
}
}
- if (!getCallMediaPermissionsSettings()) {
- await handleMissedCall(sender, incomingOfferTimestamp);
- // TODO audric show where to turn it on
- throw new Error('TODO AUDRIC');
- return;
- }
-
const readyForOffer =
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
@@ -672,6 +691,7 @@ export async function handleCallTypeOffer(
await buildAnswerAndSendIt(sender);
}
}
+ window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
// don't need to do the sending here as we dispatch an answer in a
} catch (err) {
@@ -682,16 +702,28 @@ export async function handleCallTypeOffer(
callCache.set(sender, new Array());
}
callCache.get(sender)?.push(callMessage);
- window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
}
-async function handleMissedCall(sender: string, incomingOfferTimestamp: number) {
+async function handleMissedCall(
+ sender: string,
+ incomingOfferTimestamp: number,
+ isBecauseOfCallPermission: boolean
+) {
const incomingCallConversation = await getConversationById(sender);
- ToastUtils.pushedMissedCall(
- incomingCallConversation?.getNickname() ||
- incomingCallConversation?.getProfileName() ||
- 'Unknown'
- );
+
+ if (!isBecauseOfCallPermission) {
+ ToastUtils.pushedMissedCall(
+ incomingCallConversation?.getNickname() ||
+ incomingCallConversation?.getProfileName() ||
+ 'Unknown'
+ );
+ } else {
+ ToastUtils.pushedMissedCallCauseOfPermission(
+ incomingCallConversation?.getNickname() ||
+ incomingCallConversation?.getProfileName() ||
+ 'Unknown'
+ );
+ }
await incomingCallConversation?.addSingleMessage({
conversationId: incomingCallConversation.id,
diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx
index a90146fbb..ce2fa7191 100644
--- a/ts/session/utils/Toast.tsx
+++ b/ts/session/utils/Toast.tsx
@@ -148,15 +148,30 @@ export function pushedMissedCall(conversationName: string) {
);
}
-export function pushMicAndCameraPermissionNeeded() {
+const openPrivacySettings = () => {
+ window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
+ window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
+};
+
+export function pushedMissedCallCauseOfPermission(conversationName: string) {
+ const id = 'missedCallPermission';
+ toast.info(
+ ,
+ { toastId: id, updateId: id, autoClose: 10000 }
+ );
+}
+
+export function pushVideoCallPermissionNeeded() {
pushToastInfo(
- 'micAndCameraPermissionNeeded',
- window.i18n('micAndCameraPermissionNeededTitle'),
- window.i18n('micAndCameraPermissionNeeded'),
- () => {
- window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
- window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
- }
+ 'videoCallPermissionNeeded',
+ window.i18n('cameraPermissionNeededTitle'),
+ window.i18n('cameraPermissionNeeded'),
+ openPrivacySettings
);
}