[iOS] Add initial CallKit support

This commit is contained in:
Lyubo Marinov 2017-09-28 16:25:04 -05:00
parent 8d11b3024e
commit 3b5ee2d4c6
12 changed files with 647 additions and 621 deletions

View File

@ -174,13 +174,13 @@ null and the Welcome page is enabled, the Welcome page is displayed instead.
Example:
```java
Bundle configOverwrite = new Bundle();
configOverwrite.putBoolean("startWithAudioMuted", true);
configOverwrite.putBoolean("startWithVideoMuted", false);
Bundle urlBundle = new Bundle();
urlBundle.putBundle("configOverwrite", configOverwrite);
urlBundle.putString("url", "https://meet.jit.si/Test123");
view.loadURLObject(urlBundle);
Bundle config = new Bundle();
config.putBoolean("startWithAudioMuted", true);
config.putBoolean("startWithVideoMuted", false);
Bundle urlObject = new Bundle();
urlObject.putBundle("config", config);
urlObject.putString("url", "https://meet.jit.si/Test123");
view.loadURLObject(urlObject);
```
#### setDefaultURL(URL)

View File

@ -76,11 +76,11 @@ instead.
```objc
[jitsiMeetView loadURLObject:@{
@"url": @"https://meet.jit.si/test123",
@"configOverwrite": @{
@"config": @{
@"startWithAudioMuted": @YES,
@"startWithVideoMuted": @NO
}
},
@"url": @"https://meet.jit.si/test123"
}];
```

View File

@ -22,36 +22,49 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUtils.h>
// Weakly load CallKit, because it's not available on iOS 9.
@import CallKit;
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTUtils.h>
// Events we will emit.
static NSString *const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction";
static NSString *const RNCallKitPerformEndCallAction = @"performEndCallAction";
static NSString *const RNCallKitPerformSetMutedCallAction = @"performSetMutedCallAction";
static NSString *const RNCallKitProviderDidReset = @"providerDidReset";
// The events emitted/supported by RNCallKit:
static NSString * const RNCallKitPerformAnswerCallAction
= @"performAnswerCallAction";
static NSString * const RNCallKitPerformEndCallAction
= @"performEndCallAction";
static NSString * const RNCallKitPerformSetMutedCallAction
= @"performSetMutedCallAction";
static NSString * const RNCallKitProviderDidReset
= @"providerDidReset";
@interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
@end
@implementation RNCallKit
{
CXCallController *callKitCallController;
CXProvider *callKitProvider;
CXCallController *_callController;
CXProvider *_provider;
}
RCT_EXPORT_MODULE()
RCT_EXTERN void RCTRegisterModule(Class);
- (NSArray<NSString *> *)supportedEvents
{
+ (void)load {
// Make the react-native module RNCallKit available (to JS) only if CallKit
// is available on the executing operating sytem. For example, CallKit is
// not available on iOS 9.
if ([CXCallController class]) {
RCTRegisterModule(self);
}
}
+ (NSString *)moduleName {
return @"RNCallKit";
}
- (NSArray<NSString *> *)supportedEvents {
return @[
RNCallKitPerformAnswerCallAction,
RNCallKitPerformEndCallAction,
@ -60,33 +73,17 @@ RCT_EXPORT_MODULE()
];
}
// Configure CallKit
RCT_EXPORT_METHOD(setup:(NSDictionary *)options)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][setup] options = %@", options);
#endif
callKitCallController = [[CXCallController alloc] init];
if (callKitProvider) {
[callKitProvider invalidate];
}
callKitProvider = [[CXProvider alloc] initWithConfiguration:[self getProviderConfiguration: options]];
[callKitProvider setDelegate:self queue:nil];
}
#pragma mark - CXCallController call actions
// Display the incoming call to the user
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callUUID
handle:(NSString *)handle
hasVideo:(BOOL)hasVideo
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
reject:(RCTPromiseRejectBlock)reject) {
#ifdef DEBUG
NSLog(@"[RNCallKit][displayIncomingCall] uuidString = %@", uuidString);
NSLog(@"[RNCallKit][displayIncomingCall] callUUID = %@", callUUID);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
@ -95,149 +92,208 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = hasVideo;
[callKitProvider reportNewIncomingCallWithUUID:uuid
update:callUpdate
completion:^(NSError * _Nullable error) {
if (error == nil) {
resolve(nil);
} else {
[self.provider reportNewIncomingCallWithUUID:callUUID_
update:callUpdate
completion:^(NSError * _Nullable error) {
if (error) {
reject(nil, @"Error reporting new incoming call", error);
} else {
resolve(nil);
}
}];
}
// End call
RCT_EXPORT_METHOD(endCall:(NSString *)uuidString
RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
reject:(RCTPromiseRejectBlock)reject) {
#ifdef DEBUG
NSLog(@"[RNCallKit][endCall] uuidString = %@", uuidString);
NSLog(@"[RNCallKit][endCall] callUUID = %@", callUUID);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXEndCallAction *action = [[CXEndCallAction alloc] initWithCallUUID:uuid];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXEndCallAction *action
= [[CXEndCallAction alloc] initWithCallUUID:callUUID_];
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
resolve:resolve
reject:reject];
}
// Mute / unmute (audio)
RCT_EXPORT_METHOD(setMuted:(NSString *)uuidString
muted:(BOOL) muted
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
RCT_EXPORT_METHOD(setMuted:(NSString *)callUUID
muted:(BOOL)muted
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
#ifdef DEBUG
NSLog(@"[RNCallKit][setMuted] uuidString = %@", uuidString);
NSLog(@"[RNCallKit][setMuted] callUUID = %@", callUUID);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXSetMutedCallAction *action
= [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:muted];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
= [[CXSetMutedCallAction alloc] initWithCallUUID:callUUID_ muted:muted];
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
resolve:resolve
reject:reject];
}
RCT_EXPORT_METHOD(setProviderConfiguration:(NSDictionary *)dictionary) {
#ifdef DEBUG
NSLog(
@"[RNCallKit][setProviderConfiguration:] dictionary = %@",
dictionary);
#endif
CXProviderConfiguration *configuration
= [self providerConfigurationFromDictionary:dictionary];
if (_provider) {
_provider.configuration = configuration;
} else {
_provider = [[CXProvider alloc] initWithConfiguration:configuration];
[_provider setDelegate:self queue:nil];
}
}
// Start outgoing call
RCT_EXPORT_METHOD(startCall:(NSString *)uuidString
RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
handle:(NSString *)handle
video:(BOOL)video
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
reject:(RCTPromiseRejectBlock)reject) {
#ifdef DEBUG
NSLog(@"[RNCallKit][startCall] uuidString = %@", uuidString);
NSLog(@"[RNCallKit][startCall] callUUID = %@", callUUID);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXHandle *callHandle
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXHandle *handle_
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
CXStartCallAction *action
= [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
= [[CXStartCallAction alloc] initWithCallUUID:callUUID_
handle:handle_];
action.video = video;
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Indicate call failed
RCT_EXPORT_METHOD(reportCallFailed:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
[callKitProvider reportCallWithUUID:uuid
endedAtDate:[NSDate date]
reason:CXCallEndedReasonFailed];
RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
[self.provider reportCallWithUUID:callUUID_
endedAtDate:[NSDate date]
reason:CXCallEndedReasonFailed];
resolve(nil);
}
// Indicate outgoing call connected
RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
[callKitProvider reportOutgoingCallWithUUID:uuid
connectedAtDate:[NSDate date]];
// Indicate outgoing call connected.
RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
[self.provider reportOutgoingCallWithUUID:callUUID_
connectedAtDate:[NSDate date]];
resolve(nil);
}
// Update call in case we have a display name or video capability changes
RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
// Update call in case we have a display name or video capability changes.
RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
options:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
reject:(RCTPromiseRejectBlock)reject) {
#ifdef DEBUG
NSLog(@"[RNCallKit][updateCall] uuidString = %@ options = %@", uuidString, options);
NSLog(
@"[RNCallKit][updateCall] callUUID = %@ options = %@",
callUUID,
options);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXCallUpdate *update = [[CXCallUpdate alloc] init];
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
if (options[@"displayName"]) {
update.localizedCallerName = options[@"displayName"];
callUpdate.localizedCallerName = options[@"displayName"];
}
if (options[@"hasVideo"]) {
update.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
callUpdate.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
}
[callKitProvider reportCallWithUUID:uuid updated:update];
[self.provider reportCallWithUUID:callUUID_ updated:callUpdate];
resolve(nil);
}
#pragma mark - Helper methods
- (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary* )settings
{
- (CXCallController *)callController {
if (!_callController) {
_callController = [[CXCallController alloc] init];
}
return _callController;
}
- (CXProvider *)provider {
if (!_provider) {
[self setProviderConfiguration:nil];
}
return _provider;
}
- (CXProviderConfiguration *)providerConfigurationFromDictionary:(NSDictionary* )dictionary {
#ifdef DEBUG
NSLog(@"[RNCallKit][getProviderConfiguration]");
NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]");
#endif
if (!dictionary) {
dictionary = @{};
}
// localizedName
NSString *localizedName = dictionary[@"localizedName"];
if (!localizedName) {
localizedName
= [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
}
CXProviderConfiguration *providerConfiguration
= [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]];
providerConfiguration.supportsVideo = YES;
= [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName];
// iconTemplateImageData
NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"];
if (iconTemplateImageName) {
UIImage *iconTemplateImage = [UIImage imageNamed:iconTemplateImageName];
if (iconTemplateImage) {
providerConfiguration.iconTemplateImageData
= UIImagePNGRepresentation(iconTemplateImage);
}
}
providerConfiguration.maximumCallGroups = 1;
providerConfiguration.maximumCallsPerCallGroup = 1;
providerConfiguration.ringtoneSound = dictionary[@"ringtoneSound"];
providerConfiguration.supportedHandleTypes
= [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
if (settings[@"imageName"]) {
providerConfiguration.iconTemplateImageData
= UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]);
}
if (settings[@"ringtoneSound"]) {
providerConfiguration.ringtoneSound = settings[@"ringtoneSound"];
}
= [NSSet setWithObjects:@(CXHandleTypeGeneric), nil];
providerConfiguration.supportsVideo = YES;
return providerConfiguration;
}
- (void)requestTransaction:(CXTransaction *)transaction
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
reject:(RCTPromiseRejectBlock)reject {
#ifdef DEBUG
NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
#endif
[callKitCallController requestTransaction:transaction completion:^(NSError * _Nullable error) {
if (error == nil) {
resolve(nil);
} else {
NSLog(@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error);
[self.callController requestTransaction:transaction
completion:^(NSError * _Nullable error) {
if (error) {
NSLog(
@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)",
transaction.actions,
error);
reject(nil, @"Error processing CallKit transaction", error);
} else {
resolve(nil);
}
}];
}
@ -247,53 +303,62 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
// Called when the provider has been reset. We should terminate all calls.
- (void)providerDidReset:(CXProvider *)provider {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:providerDidReset]");
NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]");
#endif
[self sendEventWithName:RNCallKitProviderDidReset body:nil];
}
// Answering incoming call
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
{
- (void) provider:(CXProvider *)provider
performAnswerCallAction:(CXAnswerCallAction *)action {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction]");
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]");
#endif
[self sendEventWithName:RNCallKitPerformAnswerCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill];
}
// Call ended, user request
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
{
- (void) provider:(CXProvider *)provider
performEndCallAction:(CXEndCallAction *)action {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction]");
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]");
#endif
[self sendEventWithName:RNCallKitPerformEndCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill];
}
// Handle audio mute from CallKit view
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
- (void) provider:(CXProvider *)provider
performSetMutedCallAction:(CXSetMutedCallAction *)action {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction]");
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]");
#endif
[self sendEventWithName:RNCallKitPerformSetMutedCallAction
body:@{ @"callUUID": action.callUUID.UUIDString,
@"muted": [NSNumber numberWithBool:action.muted]}];
body:@{
@"callUUID": action.callUUID.UUIDString,
@"muted": @(action.muted)
}];
[action fulfill];
}
// Starting outgoing call
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
{
- (void) provider:(CXProvider *)provider
performStartCallAction:(CXStartCallAction *)action {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction]");
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]");
#endif
[action fulfill];
// Update call info
// Update call info.
NSUUID *callUUID = action.callUUID;
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle = action.handle;
callUpdate.supportsDTMF = NO;
@ -301,34 +366,31 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = action.isVideo;
[callKitProvider reportCallWithUUID:action.callUUID updated:callUpdate];
// Notify the system about the outgoing call
[callKitProvider reportOutgoingCallWithUUID:action.callUUID
startedConnectingAtDate:[NSDate date]];
[provider reportCallWithUUID:callUUID updated:callUpdate];
// Notify the system about the outgoing call.
[provider reportOutgoingCallWithUUID:callUUID
startedConnectingAtDate:[NSDate date]];
}
// These just help with debugging
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
{
// The following just help with debugging:
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession]");
#endif
- (void) provider:(CXProvider *)provider
didActivateAudioSession:(AVAudioSession *)audioSession {
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]");
}
- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession]");
#endif
- (void) provider:(CXProvider *)provider
didDeactivateAudioSession:(AVAudioSession *)audioSession {
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
}
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction]");
#endif
- (void) provider:(CXProvider *)provider
timedOutPerformingAction:(CXAction *)action {
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
}
#endif
@end

View File

@ -23,7 +23,7 @@
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (copy, nonatomic) NSURL *defaultURL;
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic) BOOL welcomePageEnabled;

View File

@ -17,26 +17,15 @@
#import <CoreText/CoreText.h>
#include <mach/mach_time.h>
@import Intents;
#import <React/RCTAssert.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTRootView.h>
#include <Availability.h>
#import <Foundation/Foundation.h>
#import "JitsiMeetView+Private.h"
#import "RCTBridgeWrapper.h"
// Weakly load the Intents framework since it's not available on iOS 9.
@import Intents;
// Constant describing iOS 10.0.0
static const NSOperatingSystemVersion ios10 = {
.majorVersion = 10,
.minorVersion = 0,
.patchVersion = 0
};
/**
* A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.
* In the Release configuration, React Native will (intentionally) raise an
@ -153,50 +142,45 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
{
NSString *activityType = userActivity.activityType;
// XXX At least twice we received bug reports about malfunctioning loadURL
// in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as
// expected in our testing. But that was to be expected because the app does
// not exercise loadURL. In order to increase the test coverage of loadURL,
// channel Universal linking through loadURL.
if ([userActivity.activityType
isEqualToString:NSUserActivityTypeBrowsingWeb]
&& [JitsiMeetView loadURLInViews:userActivity.webpageURL]) {
if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]
&& [self loadURLInViews:userActivity.webpageURL]) {
return YES;
}
// Check for CallKit intents only on iOS >= 10
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) {
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]
|| [userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
INInteraction *interaction = [userActivity interaction];
INIntent *intent = interaction.intent;
NSString *handle;
BOOL isAudio = NO;
// Check for a CallKit intent.
if ([activityType isEqualToString:@"INStartAudioCallIntent"]
|| [activityType isEqualToString:@"INStartVideoCallIntent"]) {
INIntent *intent = userActivity.interaction.intent;
NSArray<INPerson *> *contacts;
NSString *url;
BOOL startAudioOnly = NO;
if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
INStartAudioCallIntent *startCallIntent
= (INStartAudioCallIntent *)intent;
handle = startCallIntent.contacts.firstObject.personHandle.value;
isAudio = YES;
} else {
INStartVideoCallIntent *startCallIntent
= (INStartVideoCallIntent *)intent;
handle = startCallIntent.contacts.firstObject.personHandle.value;
}
if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
contacts = ((INStartAudioCallIntent *) intent).contacts;
startAudioOnly = YES;
} else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
contacts = ((INStartVideoCallIntent *) intent).contacts;
}
if (handle) {
// Load the URL contained in the handle
[view loadURLObject:@{
@"url": handle,
@"configOverwrite": @{
@"startAudioOnly": @(isAudio)
}
}];
if (contacts && (url = contacts.firstObject.personHandle.value)) {
// Load the URL contained in the handle.
[self loadURLObjectInViews:@{
@"config": @{
@"startAudioOnly": @(startAudioOnly)
},
@"url": url
}];
return YES;
}
}
}
return YES;
}
}
return [RCTLinkingManager application:application
continueUserActivity:userActivity
@ -212,7 +196,7 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
// expected in our testing. But that was to be expected because the app does
// not exercise loadURL. In order to increase the test coverage of loadURL,
// channel Universal linking through loadURL.
if ([JitsiMeetView loadURLInViews:url]) {
if ([self loadURLInViews:url]) {
return YES;
}
@ -341,15 +325,20 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
* at least one {@code JitsiMeetView}; otherwise, {@code NO}.
*/
+ (BOOL)loadURLInViews:(NSURL *)url {
return
[self loadURLObjectInViews:url ? @{ @"url": url.absoluteString } : nil];
}
+ (BOOL)loadURLObjectInViews:(NSDictionary *)urlObject {
BOOL handled = NO;
if (views) {
for (NSString *externalAPIScope in views) {
JitsiMeetView *view
= [JitsiMeetView viewForExternalAPIScope:externalAPIScope];
= [self viewForExternalAPIScope:externalAPIScope];
if (view) {
[view loadURL:url];
[view loadURLObject:urlObject];
handled = YES;
}
}

View File

@ -1,12 +1,12 @@
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import { toState } from '../redux';
/**
* Attach a set of local tracks to a conference.
*
* NOTE The function is internal to this feature.
*
* @param {JitsiConference} conference - Conference instance.
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
* @protected
* @returns {Promise}
*/
export function _addLocalTracksToConference(conference, localTracks) {
@ -29,14 +29,33 @@ export function _addLocalTracksToConference(conference, localTracks) {
return Promise.all(promises);
}
/**
* Returns the current {@code JitsiConference} which is joining or joined and is
* not leaving. Please note the contrast with merely reading the
* {@code conference} state of the feature base/conference which is not joining
* but may be leaving already.
*
* @param {Function|Object} stateful - The redux store, state, or
* {@code getState} function.
* @returns {JitsiConference|undefined}
*/
export function getCurrentConference(stateful) {
const { conference, joining, leaving }
= toState(stateful)['features/base/conference'];
return (
conference
? conference === leaving ? undefined : conference
: joining);
}
/**
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
* manipulating a conference participant (e.g. pin or select participant).
*
* NOTE The function is internal to this feature.
*
* @param {Error} err - The Error which was thrown by the backend while
* manipulating a conference participant and which is to be handled.
* @protected
* @returns {void}
*/
export function _handleParticipantError(err) {
@ -65,10 +84,9 @@ export function isRoomValid(room) {
/**
* Remove a set of local tracks from a conference.
*
* NOTE The function is internal to this feature.
*
* @param {JitsiConference} conference - Conference instance.
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
* @protected
* @returns {Promise}
*/
export function _removeLocalTracksFromConference(conference, localTracks) {
@ -93,8 +111,6 @@ export function _removeLocalTracksFromConference(conference, localTracks) {
* time of this writing, the intention of the function is to abstract the
* reporting of errors and facilitate elaborating on it in the future.
*
* NOTE The function is internal to this feature.
*
* @param {string} msg - The error message to report.
* @param {Error} err - The Error to report.
* @private

View File

@ -16,9 +16,7 @@ import {
SET_RECEIVE_VIDEO_QUALITY,
SET_ROOM
} from './actionTypes';
import {
VIDEO_QUALITY_LEVELS
} from './constants';
import { VIDEO_QUALITY_LEVELS } from './constants';
import { isRoomValid } from './functions';
/**

View File

@ -1,44 +1,50 @@
/* @flow */
import { toState } from '../redux';
import { VIDEO_MUTISM_AUTHORITY } from './constants';
/**
* Determines whether video is currently muted by the audio-only authority.
*
* @param {Store} store - The redux store.
* @param {Function|Object} stateful - The redux store, state, or
* {@code getState} function.
* @returns {boolean}
*/
export function isVideoMutedByAudioOnly(store: { getState: Function }) {
return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY);
export function isVideoMutedByAudioOnly(stateful: Function | Object) {
return (
_isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
}
/**
* Determines whether video is currently muted by a specific
* <tt>VIDEO_MUTISM_AUTHORITY</tt>.
*
* @param {Store} store - The redux store.
* @param {Function|Object} stateful - The redux store, state, or
* {@code getState} function.
* @param {number} videoMutismAuthority - The <tt>VIDEO_MUTISM_AUTHORITY</tt>
* which is to be checked whether it has muted video.
* @returns {boolean} If video is currently muted by the specified
* <tt>videoMutismAuthority</tt>, then <tt>true</tt>; otherwise, <tt>false</tt>.
*/
function _isVideoMutedByAuthority(
{ getState }: { getState: Function },
stateful: Function | Object,
videoMutismAuthority: number) {
return Boolean(
const { muted } = toState(stateful)['features/base/media'].video;
// eslint-disable-next-line no-bitwise
getState()['features/base/media'].video.muted & videoMutismAuthority);
// eslint-disable-next-line no-bitwise
return Boolean(muted & videoMutismAuthority);
}
/**
* Determines whether video is currently muted by the user authority.
*
* @param {Store} store - The redux store.
* @param {Function|Object} stateful - The redux store, state, or
* {@code getState} function.
* @returns {boolean}
*/
export function isVideoMutedByUser(store: { getState: Function }) {
return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.USER);
export function isVideoMutedByUser(stateful: Function | Object) {
return _isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.USER);
}
/**

View File

@ -1,10 +1,4 @@
import {
NativeModules,
NativeEventEmitter,
Platform
} from 'react-native';
const RNCallKit = NativeModules.RNCallKit;
import { NativeModules, NativeEventEmitter } from 'react-native';
/**
* Thin wrapper around Apple's CallKit functionality.
@ -18,218 +12,28 @@ const RNCallKit = NativeModules.RNCallKit;
* the "endCall" method in this class, for example.
*
* Emitted events:
* - performAnswerCallAction: The user pressed the answer button.
* - performEndCallAction: The call should be ended.
* - performSetMutedCallAction: The call muted state should change. The
* ancillary `data` object contains a `muted` attribute.
* - providerDidReset: The system has reset, all calls should be terminated.
* This event gets no associated data.
* - performAnswerCallAction: The user pressed the answer button.
* - performEndCallAction: The call should be ended.
* - performSetMutedCallAction: The call muted state should change. The
* ancillary `data` object contains a `muted` attribute.
* - providerDidReset: The system has reset, all calls should be terminated.
* This event gets no associated data.
*
* All events get a `data` object with a `callUUID` property, unless stated
* otherwise.
*/
class CallKit extends NativeEventEmitter {
/**
* Initializes a new {@code CallKit} instance.
*/
constructor() {
super(RNCallKit);
this._setup = false;
}
let CallKit = NativeModules.RNCallKit;
/**
* Returns True if the current platform is supported, false otherwise. The
* supported platforms are: iOS >= 10.
*
* @private
* @returns {boolean}
*/
static isSupported() {
return Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 10;
}
// XXX Rather than wrapping RNCallKit in a new class and forwarding the many
// methods of the latter to the former, add the one additional method that we
// need to RNCallKit.
if (CallKit) {
const eventEmitter = new NativeEventEmitter(CallKit);
/**
* Checks if CallKit was setup, and throws an exception in that case.
*
* @private
* @returns {void}
*/
_checkSetup() {
if (!this._setup) {
throw new Error('CallKit not initialized, call setup() first.');
}
}
/**
* Adds a listener for the given event.
*
* @param {string} event - Name of the event we are interested in.
* @param {Function} listener - Function which will be called when the
* desired event is emitted.
* @returns {void}
*/
addEventListener(event, listener) {
this._checkSetup();
if (!CallKit.isSupported()) {
return;
}
this.addListener(event, listener);
}
/**
* Notifies CallKit about an incoming call. This will display the system
* incoming call view.
*
* @param {string} uuid - Unique identifier for the call.
* @param {string} handle - Call handle in CallKit's terms. The room URL.
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
* @returns {Promise}
*/
displayIncomingCall(uuid, handle, hasVideo = true) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.displayIncomingCall(uuid, handle, hasVideo);
}
/**
* Request CallKit to end the call.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
endCall(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.endCall(uuid);
}
/**
* Removes a listener for the given event.
*
* @param {string} event - Name of the event we are no longer interested in.
* @param {Function} listener - Function which used to be called when the
* desired event was emitted.
* @returns {void}
*/
removeEventListener(event, listener) {
this._checkSetup();
if (!CallKit.isSupported()) {
return;
}
this.removeListener(event, listener);
}
/**
* Indicate CallKit that the outgoing call with the given UUID is now
* connected.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
reportConnectedOutgoingCall(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.reportConnectedOutgoingCall(uuid);
}
/**
* Indicate CallKit that the call with the given UUID has failed.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
reportCallFailed(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.reportCallFailed(uuid);
}
/**
* Tell CallKit about the audio muted state.
*
* @param {string} uuid - Unique identifier for the call.
* @param {boolean} muted - True if audio is muted, false otherwise.
* @returns {Promise}
*/
setMuted(uuid, muted) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.setMuted(uuid, muted);
}
/**
* Prepare / initialize CallKit. This method must be called before any
* other.
*
* @param {Object} options - Initialization options.
* @param {string} options.imageName - Image to be used in CallKit's
* application button..
* @param {string} options.ringtoneSound - Ringtone to be used for incoming
* calls.
* @returns {void}
*/
setup(options = {}) {
if (CallKit.isSupported()) {
options.appName = NativeModules.AppInfo.name;
RNCallKit.setup(options);
}
this._setup = true;
}
/**
* Indicate CallKit about a new outgoing call.
*
* @param {string} uuid - Unique identifier for the call.
* @param {string} handle - Call handle in CallKit's terms. The room URL in
* our case.
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
* @returns {Promise}
*/
startCall(uuid, handle, hasVideo = true) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.startCall(uuid, handle, hasVideo);
}
/**
* Updates an ongoing call's parameters.
*
* @param {string} uuid - Unique identifier for the call.
* @param {Object} options - Object with properties which should be updated.
* @param {string} options.displayName - Display name for the caller.
* @param {boolean} options.hasVideo - True if the call has video, false
* otherwise.
* @returns {Promise}
*/
updateCall(uuid, options) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.updateCall(uuid, options);
}
CallKit = {
...CallKit,
addListener: eventEmitter.addListener.bind(eventEmitter)
};
}
export default new CallKit();
export default CallKit;

View File

@ -1,11 +1,11 @@
/**
* The type of redux action to set the CallKit event listeners.
* The type of redux action to set CallKit's event subscriptions.
*
* {
* type: _SET_CALLKIT_LISTENERS,
* listeners: Map|null
* type: _SET_CALLKIT_SUBSCRIPTIONS,
* subscriptions: Array|undefined
* }
*
* @protected
*/
export const _SET_CALLKIT_LISTENERS = Symbol('_SET_CALLKIT_LISTENERS');
export const _SET_CALLKIT_SUBSCRIPTIONS = Symbol('_SET_CALLKIT_SUBSCRIPTIONS');

View File

@ -1,27 +1,26 @@
/* @flow */
import { NativeModules } from 'react-native';
import uuid from 'uuid';
import {
APP_WILL_MOUNT,
APP_WILL_UNMOUNT,
appNavigate
} from '../../app';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, appNavigate } from '../../app';
import {
CONFERENCE_FAILED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_JOINED
CONFERENCE_JOINED,
getCurrentConference
} from '../../base/conference';
import { getInviteURL } from '../../base/connection';
import {
isVideoMutedByAudioOnly,
SET_AUDIO_MUTED,
SET_VIDEO_MUTED,
isVideoMutedByAudioOnly,
setAudioMuted
} from '../../base/media';
import { MiddlewareRegistry, toState } from '../../base/redux';
import { _SET_CALLKIT_LISTENERS } from './actionTypes';
import { MiddlewareRegistry } from '../../base/redux';
import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
import CallKit from './CallKit';
/**
@ -30,165 +29,319 @@ import CallKit from './CallKit';
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const result = next(action);
CallKit && MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case _SET_CALLKIT_LISTENERS: {
const { listeners } = getState()['features/callkit'];
case _SET_CALLKIT_SUBSCRIPTIONS:
return _setCallKitSubscriptions(store, next, action);
if (listeners) {
for (const [ event, listener ] of listeners) {
CallKit.removeEventListener(event, listener);
}
}
if (action.listeners) {
for (const [ event, listener ] of action.listeners) {
CallKit.addEventListener(event, listener);
}
}
break;
}
case APP_WILL_MOUNT: {
CallKit.setup(); // TODO: set app icon.
const listeners = new Map();
const callEndListener = data => {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === data.callUUID) {
// We arrive here when a call is ended by the system, for
// for example when another incoming call is received and the
// user selects "End & Accept".
delete conference.callUUID;
dispatch(appNavigate(undefined));
}
};
listeners.set('performEndCallAction', callEndListener);
// Set the same listener for providerDidReset. According to the docs,
// when the system resets we should terminate all calls.
listeners.set('providerDidReset', callEndListener);
const setMutedListener = data => {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === data.callUUID) {
// Break the loop. Audio can be muted both from the CallKit
// interface and from the Jitsi Meet interface. We must keep
// them in sync, but at some point the loop needs to be broken.
// We are doing it here, on the CallKit handler.
const { muted } = getState()['features/base/media'].audio;
if (muted !== data.muted) {
dispatch(setAudioMuted(Boolean(data.muted)));
}
}
};
listeners.set('performSetMutedCallAction', setMutedListener);
dispatch({
type: _SET_CALLKIT_LISTENERS,
listeners
});
break;
}
case APP_WILL_MOUNT:
return _appWillMount(store, next, action);
case APP_WILL_UNMOUNT:
dispatch({
type: _SET_CALLKIT_LISTENERS,
listeners: null
store.dispatch({
type: _SET_CALLKIT_SUBSCRIPTIONS,
subscriptions: undefined
});
break;
case CONFERENCE_FAILED: {
const { callUUID } = action.conference;
case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action);
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
break;
case CONFERENCE_LEFT:
return _conferenceLeft(store, next, action);
case CONFERENCE_WILL_JOIN:
return _conferenceWillJoin(store, next, action);
case SET_AUDIO_MUTED:
return _setAudioMuted(store, next, action);
case SET_VIDEO_MUTED:
return _setVideoMuted(store, next, action);
}
case CONFERENCE_LEFT: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.endCall(callUUID);
}
break;
}
case CONFERENCE_JOINED: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportConnectedOutgoingCall(callUUID);
}
break;
}
case CONFERENCE_WILL_JOIN: {
const conference = action.conference;
const url = getInviteURL(getState);
const hasVideo = !isVideoMutedByAudioOnly({ getState });
// When assigning the call UUID, do so in upper case, since iOS will
// return it upper cased.
conference.callUUID = uuid.v4().toUpperCase();
CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
.then(() => {
const { room } = getState()['features/base/conference'];
CallKit.updateCall(conference.callUUID, { displayName: room });
});
break;
}
case SET_AUDIO_MUTED: {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
CallKit.setMuted(conference.callUUID, action.muted);
}
break;
}
case SET_VIDEO_MUTED: {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
const hasVideo = !isVideoMutedByAudioOnly({ getState });
CallKit.updateCall(conference.callUUID, { hasVideo });
}
break;
}
}
return result;
return next(action);
});
/**
* Returns the currently active conference.
* Notifies the feature jwt that the action {@link APP_WILL_MOUNT} is being
* dispatched within a specific redux <tt>store</tt>.
*
* @param {Function|Object} stateOrGetState - The redux state or redux's
* {@code getState} function.
* @returns {Conference|undefined}
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>APP_WILL_MOUNT</tt> which is
* being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function getCurrentConference(stateOrGetState: Function | Object): ?Object {
const state = toState(stateOrGetState);
const { conference, joining } = state['features/base/conference'];
function _appWillMount({ dispatch, getState }, next, action) {
const result = next(action);
return conference || joining;
CallKit.setProviderConfiguration({
iconTemplateImageName: 'AppIcon40x40',
localizedName: NativeModules.AppInfo.name
});
const context = {
dispatch,
getState
};
const subscriptions = [
CallKit.addListener(
'performEndCallAction',
_onPerformEndCallAction,
context),
CallKit.addListener(
'performSetMutedCallAction',
_onPerformSetMutedCallAction,
context),
// According to CallKit's documentation, when the system resets we
// should terminate all calls. Hence, providerDidReset is the same
// to us as performEndCallAction.
CallKit.addListener(
'providerDidReset',
_onPerformEndCallAction,
context)
];
dispatch({
type: _SET_CALLKIT_SUBSCRIPTIONS,
subscriptions
});
return result;
}
/**
* Notifies the feature jwt that the action {@link CONFERENCE_FAILED} is being
* dispatched within a specific redux <tt>store</tt>.
*
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>CONFERENCE_FAILED</tt> which is
* being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function _conferenceFailed(store, next, action) {
const result = next(action);
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
return result;
}
/**
* Notifies the feature jwt that the action {@link CONFERENCE_JOINED} is being
* dispatched within a specific redux <tt>store</tt>.
*
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>CONFERENCE_JOINED</tt> which is
* being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function _conferenceJoined(store, next, action) {
const result = next(action);
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportConnectedOutgoingCall(callUUID);
}
return result;
}
/**
* Notifies the feature jwt that the action {@link CONFERENCE_LEFT} is being
* dispatched within a specific redux <tt>store</tt>.
*
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>CONFERENCE_LEFT</tt> which is
* being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function _conferenceLeft(store, next, action) {
const result = next(action);
const { callUUID } = action.conference;
if (callUUID) {
CallKit.endCall(callUUID);
}
return result;
}
/**
* Notifies the feature jwt that the action {@link CONFERENCE_WILL_JOIN} is
* being dispatched within a specific redux <tt>store</tt>.
*
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>CONFERENCE_WILL_JOIN</tt> which
* is being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function _conferenceWillJoin({ getState }, next, action) {
const result = next(action);
const conference = action.conference;
const url = getInviteURL(getState);
const hasVideo = !isVideoMutedByAudioOnly(getState);
// When assigning the call UUID, do so in upper case, since iOS will
// return it upper cased.
conference.callUUID = uuid.v4().toUpperCase();
CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
.then(() => {
const { room } = getState()['features/base/conference'];
CallKit.updateCall(conference.callUUID, { displayName: room });
});
return result;
}
/**
* Handles CallKit's event <tt>performEndCallAction</tt>.
*
* @param {Object} event - The details of the CallKit event
* <tt>performEndCallAction</tt>.
* @returns {void}
*/
function _onPerformEndCallAction({ callUUID }) {
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === callUUID) {
// We arrive here when a call is ended by the system, for
// example when another incoming call is received and the user
// selects "End & Accept".
delete conference.callUUID;
dispatch(appNavigate(undefined));
}
}
/**
* Handles CallKit's event <tt>performSetMutedCallAction</tt>.
*
* @param {Object} event - The details of the CallKit event
* <tt>performSetMutedCallAction</tt>.
* @returns {void}
*/
function _onPerformSetMutedCallAction({ callUUID, muted: newValue }) {
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === callUUID) {
// Break the loop. Audio can be muted from both CallKit and Jitsi Meet.
// We must keep them in sync, but at some point the loop needs to be
// broken. We are doing it here, on the CallKit handler.
const { muted: oldValue } = getState()['features/base/media'].audio;
if (oldValue !== newValue) {
dispatch(setAudioMuted(Boolean(newValue)));
}
}
}
/**
* Notifies the feature jwt that the action {@link SET_AUDIO_MUTED} is being
* dispatched within a specific redux <tt>store</tt>.
*
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>SET_AUDIO_MUTED</tt> which is
* being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function _setAudioMuted({ getState }, next, action) {
const result = next(action);
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
CallKit.setMuted(conference.callUUID, action.muted);
}
return result;
}
/**
* Notifies the feature jwt that the action {@link _SET_CALLKIT_SUBSCRIPTIONS}
* is being dispatched within a specific redux <tt>store</tt>.
*
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>_SET_CALLKIT_SUBSCRIPTIONS</tt>
* which is being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function _setCallKitSubscriptions({ getState }, next, action) {
const { subscriptions } = getState()['features/callkit'];
if (subscriptions) {
for (const subscription of subscriptions) {
subscription.remove();
}
}
return next(action);
}
/**
* Notifies the feature jwt that the action {@link SET_VIDEO_MUTED} is being
* dispatched within a specific redux <tt>store</tt>.
*
* @param {Store} store - The redux store in which the specified <tt>action</tt>
* is being dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified <tt>action</tt> to the specified <tt>store</tt>.
* @param {Action} action - The redux action <tt>SET_VIDEO_MUTED</tt> which is
* being dispatched in the specified <tt>store</tt>.
* @private
* @returns {*}
*/
function _setVideoMuted({ getState }, next, action) {
const result = next(action);
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
CallKit.updateCall(
conference.callUUID,
{ hasVideo: !isVideoMutedByAudioOnly(getState) });
}
return result;
}

View File

@ -1,17 +1,15 @@
import { ReducerRegistry } from '../../base/redux';
import { assign, ReducerRegistry } from '../../base/redux';
import {
_SET_CALLKIT_LISTENERS
} from './actionTypes';
import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
import CallKit from './CallKit';
ReducerRegistry.register('features/callkit', (state = {}, action) => {
switch (action.type) {
case _SET_CALLKIT_LISTENERS:
return {
...state,
listeners: action.listeners
};
}
CallKit && ReducerRegistry.register(
'features/callkit',
(state = {}, action) => {
switch (action.type) {
case _SET_CALLKIT_SUBSCRIPTIONS:
return assign(state, 'subscriptions', action.subscriptions);
}
return state;
});
return state;
});