From 3b5ee2d4c6dfeba6c8e50f67ba14c62ac9792ed6 Mon Sep 17 00:00:00 2001 From: Lyubo Marinov Date: Thu, 28 Sep 2017 16:25:04 -0500 Subject: [PATCH] [iOS] Add initial CallKit support --- android/README.md | 14 +- ios/README.md | 6 +- ios/sdk/src/CallKit.m | 370 ++++++++------- ios/sdk/src/JitsiMeetView.h | 2 +- ios/sdk/src/JitsiMeetView.m | 87 ++-- react/features/base/conference/functions.js | 32 +- react/features/base/conference/reducer.js | 4 +- react/features/base/media/functions.js | 28 +- react/features/mobile/callkit/CallKit.js | 232 +--------- react/features/mobile/callkit/actionTypes.js | 8 +- react/features/mobile/callkit/middleware.js | 459 ++++++++++++------- react/features/mobile/callkit/reducer.js | 26 +- 12 files changed, 647 insertions(+), 621 deletions(-) diff --git a/android/README.md b/android/README.md index 5d607dfa58..0192750daf 100644 --- a/android/README.md +++ b/android/README.md @@ -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) diff --git a/ios/README.md b/ios/README.md index dd8cc64ab4..5bccb9b25a 100644 --- a/ios/README.md +++ b/ios/README.md @@ -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" }]; ``` diff --git a/ios/sdk/src/CallKit.m b/ios/sdk/src/CallKit.m index c302a7775e..63d700bbd6 100644 --- a/ios/sdk/src/CallKit.m +++ b/ios/sdk/src/CallKit.m @@ -22,36 +22,49 @@ #import #import -#import -#import -#import -#import -#import - -// Weakly load CallKit, because it's not available on iOS 9. @import CallKit; +#import +#import +#import +#import +#import -// 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 @end @implementation RNCallKit { - CXCallController *callKitCallController; - CXProvider *callKitProvider; + CXCallController *_callController; + CXProvider *_provider; } -RCT_EXPORT_MODULE() +RCT_EXTERN void RCTRegisterModule(Class); -- (NSArray *)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 *)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 diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h index 6af9e7723d..254600f1af 100644 --- a/ios/sdk/src/JitsiMeetView.h +++ b/ios/sdk/src/JitsiMeetView.h @@ -23,7 +23,7 @@ @property (nonatomic, nullable, weak) id delegate; -@property (copy, nonatomic) NSURL *defaultURL; +@property (copy, nonatomic, nullable) NSURL *defaultURL; @property (nonatomic) BOOL welcomePageEnabled; diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index fcda21b24f..317c124503 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -17,26 +17,15 @@ #import #include +@import Intents; + #import #import #import -#include -#import - #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 RCTFatalHandler implementation which swallows JavaScript errors. * In the Release configuration, React Native will (intentionally) raise an @@ -153,50 +142,45 @@ static NSMapTable *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 *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 *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 *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; } } diff --git a/react/features/base/conference/functions.js b/react/features/base/conference/functions.js index f354db8de9..234c67add3 100644 --- a/react/features/base/conference/functions.js +++ b/react/features/base/conference/functions.js @@ -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 diff --git a/react/features/base/conference/reducer.js b/react/features/base/conference/reducer.js index f1ba5d390f..41e8d85ce6 100644 --- a/react/features/base/conference/reducer.js +++ b/react/features/base/conference/reducer.js @@ -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'; /** diff --git a/react/features/base/media/functions.js b/react/features/base/media/functions.js index f254a74b95..bf8f1c580e 100644 --- a/react/features/base/media/functions.js +++ b/react/features/base/media/functions.js @@ -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 * VIDEO_MUTISM_AUTHORITY. * - * @param {Store} store - The redux store. + * @param {Function|Object} stateful - The redux store, state, or + * {@code getState} function. * @param {number} videoMutismAuthority - The VIDEO_MUTISM_AUTHORITY * which is to be checked whether it has muted video. * @returns {boolean} If video is currently muted by the specified * videoMutismAuthority, then true; otherwise, false. */ 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); } /** diff --git a/react/features/mobile/callkit/CallKit.js b/react/features/mobile/callkit/CallKit.js index b0c8b1bc8c..ed00c24eb2 100644 --- a/react/features/mobile/callkit/CallKit.js +++ b/react/features/mobile/callkit/CallKit.js @@ -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; diff --git a/react/features/mobile/callkit/actionTypes.js b/react/features/mobile/callkit/actionTypes.js index ce109d5046..d1109d77c4 100644 --- a/react/features/mobile/callkit/actionTypes.js +++ b/react/features/mobile/callkit/actionTypes.js @@ -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'); diff --git a/react/features/mobile/callkit/middleware.js b/react/features/mobile/callkit/middleware.js index 5bd033fa95..d1ee8621f9 100644 --- a/react/features/mobile/callkit/middleware.js +++ b/react/features/mobile/callkit/middleware.js @@ -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 store. * - * @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 action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action APP_WILL_MOUNT which is + * being dispatched in the specified store. + * @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 store. + * + * @param {Store} store - The redux store in which the specified action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action CONFERENCE_FAILED which is + * being dispatched in the specified store. + * @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 store. + * + * @param {Store} store - The redux store in which the specified action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action CONFERENCE_JOINED which is + * being dispatched in the specified store. + * @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 store. + * + * @param {Store} store - The redux store in which the specified action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action CONFERENCE_LEFT which is + * being dispatched in the specified store. + * @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 store. + * + * @param {Store} store - The redux store in which the specified action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action CONFERENCE_WILL_JOIN which + * is being dispatched in the specified store. + * @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 performEndCallAction. + * + * @param {Object} event - The details of the CallKit event + * performEndCallAction. + * @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 performSetMutedCallAction. + * + * @param {Object} event - The details of the CallKit event + * performSetMutedCallAction. + * @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 store. + * + * @param {Store} store - The redux store in which the specified action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action SET_AUDIO_MUTED which is + * being dispatched in the specified store. + * @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 store. + * + * @param {Store} store - The redux store in which the specified action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action _SET_CALLKIT_SUBSCRIPTIONS + * which is being dispatched in the specified store. + * @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 store. + * + * @param {Store} store - The redux store in which the specified action + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified action to the specified store. + * @param {Action} action - The redux action SET_VIDEO_MUTED which is + * being dispatched in the specified store. + * @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; } diff --git a/react/features/mobile/callkit/reducer.js b/react/features/mobile/callkit/reducer.js index ce7231c3f8..320685a1f4 100644 --- a/react/features/mobile/callkit/reducer.js +++ b/react/features/mobile/callkit/reducer.js @@ -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; + });