added perfect negotiation

Adding toast for cam and audio permission when making a call.

adding missed call message and toast when a call is received while mid-call.

background call message work
This commit is contained in:
Warrick Corfe-Tan 2021-09-29 10:50:10 +10:00
parent 8985d1ff19
commit 6743201cc4
10 changed files with 112 additions and 62 deletions

View File

@ -447,5 +447,11 @@
"incomingCall": "Incoming call",
"accept": "Accept",
"decline": "Decline",
"endCall": "End call"
"endCall": "End call",
"micAndCameraPermissionNeededTitle": "Camera and Microphone access required",
"micAndCameraPermissionNeeded": "You can enable microphone and camera access under: Settings (Gear icon) => Privacy",
"unableToCall": "Cancel your exiting call session.",
"unableToCallTitle": "Cannot start new call",
"callMissed": "Missed call from $name$",
"callMissedTitle": "Call missed"
}

View File

@ -135,6 +135,7 @@ export const CallContainer = () => {
<CallWindowHeader>Call with: {ongoingCallProps.name}</CallWindowHeader>
<CallWindowInner>
<div>{hasIncomingCall}</div>
<VideoContainer>
<VideoContainerRemote ref={videoRefRemote} autoPlay={true} />
<VideoContainerLocal ref={videoRefLocal} autoPlay={true} />

View File

@ -1,6 +1,10 @@
import React from 'react';
import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations';
import {
getHasIncomingCall,
getHasOngoingCall,
getNumberOfPinnedConversations,
} from '../../../state/selectors/conversations';
import { getFocusedSection } from '../../../state/selectors/section';
import { Item, Submenu } from 'react-contexify';
import {
@ -319,32 +323,27 @@ export function getMarkAllReadMenuItem(conversationId: string): JSX.Element | nu
}
export function getStartCallMenuItem(conversationId: string): JSX.Element | null {
// TODO: possibly conditionally show options?
// const callOptions = [
// {
// name: 'Video call',
// value: 'video-call',
// },
// // {
// // name: 'Audio call',
// // value: 'audio-call',
// // },
// ];
const canCall = !(useSelector(getHasIncomingCall) || useSelector(getHasOngoingCall));
return (
<Item
onClick={async () => {
// TODO: either pass param to callRecipient or call different call methods based on item selected.
// TODO: one time redux-persisted permission modal?
const convo = getConversationController().get(conversationId);
if (!canCall) {
ToastUtils.pushUnableToCall();
return;
}
if (convo) {
convo.callState = 'connecting';
await convo.commit();
await CallManager.USER_callRecipient(convo.id);
}
}}
>
{'video call'}
{'Video Call'}
</Item>
);
}

View File

@ -177,6 +177,7 @@ export interface MessageAttributesOptionals {
direction?: any;
messageHash?: string;
isDeleted?: boolean;
isCall?: boolean;
}
/**

View File

@ -46,7 +46,7 @@ export async function handleCallMessage(
}
await removeFromCache(envelope);
CallManager.handleOfferCallMessage(sender, callMessage);
CallManager.handleOfferCallMessage(sender, callMessage, sentTimestamp);
return;
}

View File

@ -977,7 +977,7 @@ async function sendToGroupMembers(
window?.log?.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`);
// evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog
const inviteResults = await Promise.all(promises);
const allInvitesSent = _.every(inviteResults, Boolean);
const allInvitesSent = _.every(inviteResults, inviteResult => inviteResult !== false);
if (allInvitesSent) {
// if (true) {

View File

@ -401,7 +401,7 @@ export async function innerHandleContentMessage(
await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend);
}
if (content.callMessage && window.lokiFeatureFlags?.useCallMessage) {
await handleCallMessage(envelope, content.callMessage as SignalService.CallMessage);
await handleCallMessage(envelope, content.callMessage as SignalService.CallMessage, messageHash);
}
} catch (e) {
window?.log?.warn(e);
@ -569,6 +569,7 @@ export async function handleDataExtractionNotification(
},
unread: 1, // 1 means unread
expireTimer: 0,
isCall: true,
});
convo.updateLastMessage();
}

View File

@ -131,7 +131,7 @@ export class MessageQueue {
public async sendToPubKeyNonDurably(
user: PubKey,
message: ClosedGroupNewMessage | CallMessage
): Promise<boolean> {
): Promise<boolean | number> {
let rawMessage;
try {
rawMessage = await MessageUtils.toRawMessage(user, message);
@ -141,7 +141,7 @@ export class MessageQueue {
effectiveTimestamp,
wrappedEnvelope
);
return !!wrappedEnvelope;
return effectiveTimestamp;
} catch (error) {
if (rawMessage) {
await MessageSentHandler.handleMessageSentFailure(rawMessage, error);

View File

@ -1,4 +1,8 @@
import _ from 'lodash';
import { ToastUtils } from '.';
import { SessionSettingCategory } from '../../components/session/settings/SessionSettings';
import { getConversationById } from '../../data/data';
import { MessageModelType } from '../../models/messageType';
import { SignalService } from '../../protobuf';
import {
answerCall,
@ -7,6 +11,8 @@ import {
incomingCall,
startingCallWith,
} from '../../state/ducks/conversations';
import { SectionType, showLeftPaneSection, showSettingsSection } from '../../state/ducks/section';
import { getConversationController } from '../conversations';
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { ed25519Str } from '../onions/onionPath';
import { getMessageQueue } from '../sending';
@ -33,6 +39,7 @@ const ENABLE_VIDEO = true;
let makingOffer = false;
let ignoreOffer = false;
let isSettingRemoteAnswerPending = false;
let lastOutgoingOfferTimestamp = -Infinity;
const configuration = {
configuration: {
@ -67,8 +74,10 @@ export async function USER_callRecipient(recipient: string) {
peerConnection?.addTrack(track, mediaDevices);
});
} catch (err) {
console.error('Failed to open media devices. Check camera and mic app permissions');
// TODO: implement toast popup
ToastUtils.pushMicAndCameraPermissionNeeded(() => {
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
});
}
peerConnection.addEventListener('connectionstatechange', _event => {
window.log.info('peerConnection?.connectionState caller :', peerConnection?.connectionState);
@ -92,21 +101,6 @@ export async function USER_callRecipient(recipient: string) {
console.warn('negotiationneeded:', event);
try {
makingOffer = true;
// const offerDescription = await peerConnection?.createOffer({
// offerToReceiveAudio: true,
// offerToReceiveVideo: true,
// });
// if (!offerDescription) {
// console.error('Failed to create offer for negotiation');
// return;
// }
// await peerConnection?.setLocalDescription(offerDescription);
// if (!offerDescription || !offerDescription.sdp || !offerDescription.sdp.length) {
// // window.log.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`);
// console.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`);
// return;
// }
// @ts-ignore
await peerConnection?.setLocalDescription();
let offer = await peerConnection?.createOffer();
@ -116,12 +110,20 @@ export async function USER_callRecipient(recipient: string) {
const callOfferMessage = new CallMessage({
timestamp: Date.now(),
type: SignalService.CallMessage.Type.OFFER,
// sdps: [offerDescription.sdp],
sdps: [offer.sdp],
});
window.log.info('sending OFFER MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callOfferMessage);
const sendResult = await getMessageQueue().sendToPubKeyNonDurably(
PubKey.cast(recipient),
callOfferMessage
);
if (typeof sendResult === 'number') {
console.warn('setting last sent timestamp');
lastOutgoingOfferTimestamp = sendResult;
}
await new Promise(r => setTimeout(r, 10000));
}
} catch (err) {
console.error(err);
@ -161,7 +163,14 @@ export async function USER_callRecipient(recipient: string) {
});
window.log.info('sending OFFER MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callOfferMessage);
let sendResult = await getMessageQueue().sendToPubKeyNonDurably(
PubKey.cast(recipient),
callOfferMessage
);
if (typeof sendResult === 'number') {
console.warn('setting timestamp');
lastOutgoingOfferTimestamp = sendResult;
}
// FIXME audric dispatch UI update to show the calling UI
}
@ -363,37 +372,30 @@ export function handleEndCallMessage(sender: string) {
export async function handleOfferCallMessage(
sender: string,
callMessage: SignalService.CallMessage
callMessage: SignalService.CallMessage,
incomingOfferTimestamp: number
) {
try {
console.warn({ callMessage });
const convos = getConversationController().getConversations();
if (convos.some(convo => convo.callState !== undefined)) {
return await handleMissedCall(sender, incomingOfferTimestamp);
}
const readyForOffer =
!makingOffer && (peerConnection?.signalingState == 'stable' || isSettingRemoteAnswerPending);
// TODO: How should politeness be decided between client / recipient?
ignoreOffer = !true && !readyForOffer;
// TODO: however sent offer last is the impolite user
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
console.warn({ polite });
ignoreOffer = !polite && !readyForOffer;
console.warn({ ignoreOffer });
if (ignoreOffer) {
// window.log?.warn('Received offer when unready for offer; Ignoring offer.');
console.warn('Received offer when unready for offer; Ignoring offer.');
return;
}
// const description = await peerConnection?.createOffer({
// const description = await peerConnection?.createOffer({
// offerToReceiveVideo: true,
// offerToReceiveAudio: true,
// })
// @ts-ignore
await peerConnection?.setLocalDescription();
console.warn(peerConnection?.localDescription);
const message = new CallMessage({
type: SignalService.CallMessage.Type.ANSWER,
timestamp: Date.now(),
});
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), message);
// TODO: send via our signalling with the sdp of our pc.localDescription
// don't need to do the sending here as we dispatch an answer in a
} catch (err) {
window.log?.error(`Error handling offer message ${err}`);
}
@ -405,6 +407,25 @@ export async function handleOfferCallMessage(
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
}
async function handleMissedCall(sender: string, incomingOfferTimestamp: number) {
const incomingCallConversation = await getConversationById(sender);
ToastUtils.pushedMissedCall(incomingCallConversation?.getNickname() || 'Unknown');
await incomingCallConversation?.addSingleMessage({
conversationId: incomingCallConversation.id,
source: sender,
type: 'incoming' as MessageModelType,
sent_at: incomingOfferTimestamp,
received_at: Date.now(),
expireTimer: 0,
body: 'Missed call',
unread: 1,
isCall: false,
});
incomingCallConversation?.updateLastMessage();
return;
}
export async function handleCallAnsweredMessage(
sender: string,
callMessage: SignalService.CallMessage

View File

@ -134,6 +134,27 @@ export function pushMessageDeleteForbidden() {
pushToastError('messageDeletionForbidden', window.i18n('messageDeletionForbidden'));
}
export function pushUnableToCall() {
pushToastError('unableToCall', window.i18n('unableToCallTitle'), window.i18n('unableToCall'));
}
export function pushedMissedCall(conversationName: string) {
pushToastInfo(
'missedCall',
window.i18n('callMissedTitle'),
window.i18n('callMissedTitle', conversationName)
);
}
export function pushMicAndCameraPermissionNeeded(onClicked: () => void) {
pushToastInfo(
'micAndCameraPermissionNeeded',
window.i18n('micAndCameraPermissionNeededTitle'),
window.i18n('micAndCameraPermissionNeeded'),
onClicked
);
}
export function pushAudioPermissionNeeded(onClicked: () => void) {
pushToastInfo(
'audioPermissionNeeded',