Browse Source

[iOS] Add initial CallKit support

j8
Lyubo Marinov 7 years ago
parent
commit
3b5ee2d4c6

+ 7
- 7
android/README.md View File

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

+ 3
- 3
ios/README.md View File

@@ -76,11 +76,11 @@ instead.
76 76
 
77 77
 ```objc
78 78
 [jitsiMeetView loadURLObject:@{
79
-    @"url": @"https://meet.jit.si/test123",
80
-    @"configOverwrite": @{
79
+    @"config": @{
81 80
         @"startWithAudioMuted": @YES,
82 81
         @"startWithVideoMuted": @NO
83
-    }
82
+    },
83
+    @"url": @"https://meet.jit.si/test123"
84 84
 }];
85 85
 ```
86 86
 

+ 214
- 152
ios/sdk/src/CallKit.m View File

@@ -22,36 +22,49 @@
22 22
 #import <Foundation/Foundation.h>
23 23
 #import <UIKit/UIKit.h>
24 24
 
25
+@import CallKit;
26
+
25 27
 #import <React/RCTBridge.h>
26 28
 #import <React/RCTConvert.h>
27
-#import <React/RCTEventEmitter.h>
28 29
 #import <React/RCTEventDispatcher.h>
30
+#import <React/RCTEventEmitter.h>
29 31
 #import <React/RCTUtils.h>
30 32
 
31
-// Weakly load CallKit, because it's not available on iOS 9.
32
-@import CallKit;
33
-
34
-
35
-// Events we will emit.
36
-static NSString *const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction";
37
-static NSString *const RNCallKitPerformEndCallAction = @"performEndCallAction";
38
-static NSString *const RNCallKitPerformSetMutedCallAction = @"performSetMutedCallAction";
39
-static NSString *const RNCallKitProviderDidReset = @"providerDidReset";
40
-
33
+// The events emitted/supported by RNCallKit:
34
+static NSString * const RNCallKitPerformAnswerCallAction
35
+    = @"performAnswerCallAction";
36
+static NSString * const RNCallKitPerformEndCallAction
37
+    = @"performEndCallAction";
38
+static NSString * const RNCallKitPerformSetMutedCallAction
39
+    = @"performSetMutedCallAction";
40
+static NSString * const RNCallKitProviderDidReset
41
+    = @"providerDidReset";
41 42
 
42 43
 @interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
43 44
 @end
44 45
 
45 46
 @implementation RNCallKit
46 47
 {
47
-    CXCallController *callKitCallController;
48
-    CXProvider *callKitProvider;
48
+    CXCallController *_callController;
49
+    CXProvider *_provider;
49 50
 }
50 51
 
51
-RCT_EXPORT_MODULE()
52
+RCT_EXTERN void RCTRegisterModule(Class);
52 53
 
53
-- (NSArray<NSString *> *)supportedEvents
54
-{
54
++ (void)load {
55
+    // Make the react-native module RNCallKit available (to JS) only if CallKit
56
+    // is available on the executing operating sytem. For example, CallKit is
57
+    // not available on iOS 9.
58
+    if ([CXCallController class]) {
59
+        RCTRegisterModule(self);
60
+    }
61
+}
62
+
63
++ (NSString *)moduleName {
64
+    return @"RNCallKit";
65
+}
66
+
67
+- (NSArray<NSString *> *)supportedEvents {
55 68
     return @[
56 69
         RNCallKitPerformAnswerCallAction,
57 70
         RNCallKitPerformEndCallAction,
@@ -60,33 +73,17 @@ RCT_EXPORT_MODULE()
60 73
     ];
61 74
 }
62 75
 
63
-// Configure CallKit
64
-RCT_EXPORT_METHOD(setup:(NSDictionary *)options)
65
-{
66
-#ifdef DEBUG
67
-    NSLog(@"[RNCallKit][setup] options = %@", options);
68
-#endif
69
-    callKitCallController = [[CXCallController alloc] init];
70
-    if (callKitProvider) {
71
-        [callKitProvider invalidate];
72
-    }
73
-    callKitProvider = [[CXProvider alloc] initWithConfiguration:[self getProviderConfiguration: options]];
74
-    [callKitProvider setDelegate:self queue:nil];
75
-}
76
-
77
-#pragma mark - CXCallController call actions
78
-
79 76
 // Display the incoming call to the user
80
-RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
77
+RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callUUID
81 78
                                handle:(NSString *)handle
82 79
                              hasVideo:(BOOL)hasVideo
83 80
                               resolve:(RCTPromiseResolveBlock)resolve
84
-                               reject:(RCTPromiseRejectBlock)reject)
85
-{
81
+                               reject:(RCTPromiseRejectBlock)reject) {
86 82
 #ifdef DEBUG
87
-    NSLog(@"[RNCallKit][displayIncomingCall] uuidString = %@", uuidString);
83
+    NSLog(@"[RNCallKit][displayIncomingCall] callUUID = %@", callUUID);
88 84
 #endif
89
-    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
85
+
86
+    NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
90 87
     CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
91 88
     callUpdate.remoteHandle
92 89
         = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
@@ -95,149 +92,208 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
95 92
     callUpdate.supportsGrouping = NO;
96 93
     callUpdate.supportsUngrouping = NO;
97 94
     callUpdate.hasVideo = hasVideo;
98
-    
99
-    [callKitProvider reportNewIncomingCallWithUUID:uuid
100
-                                            update:callUpdate
101
-                                        completion:^(NSError * _Nullable error) {
102
-        if (error == nil) {
103
-            resolve(nil);
104
-        } else {
95
+
96
+    [self.provider reportNewIncomingCallWithUUID:callUUID_
97
+                                          update:callUpdate
98
+                                      completion:^(NSError * _Nullable error) {
99
+        if (error) {
105 100
             reject(nil, @"Error reporting new incoming call", error);
101
+        } else {
102
+            resolve(nil);
106 103
         }
107 104
     }];
108 105
 }
109 106
 
110 107
 // End call
111
-RCT_EXPORT_METHOD(endCall:(NSString *)uuidString
108
+RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
112 109
                   resolve:(RCTPromiseResolveBlock)resolve
113
-                  reject:(RCTPromiseRejectBlock)reject)
114
-{
110
+                   reject:(RCTPromiseRejectBlock)reject) {
115 111
 #ifdef DEBUG
116
-    NSLog(@"[RNCallKit][endCall] uuidString = %@", uuidString);
112
+    NSLog(@"[RNCallKit][endCall] callUUID = %@", callUUID);
117 113
 #endif
118
-    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
119
-    CXEndCallAction *action = [[CXEndCallAction alloc] initWithCallUUID:uuid];
120
-    CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
121
-    [self requestTransaction:transaction resolve:resolve reject:reject];
114
+
115
+    NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
116
+    CXEndCallAction *action
117
+        = [[CXEndCallAction alloc] initWithCallUUID:callUUID_];
118
+    [self requestTransaction:[[CXTransaction alloc] initWithAction:action]
119
+                     resolve:resolve
120
+                      reject:reject];
122 121
 }
123 122
 
124 123
 // Mute / unmute (audio)
125
-RCT_EXPORT_METHOD(setMuted:(NSString *)uuidString
126
-                  muted:(BOOL) muted
127
-                  resolve:(RCTPromiseResolveBlock)resolve
128
-                  reject:(RCTPromiseRejectBlock)reject)
129
-{
124
+RCT_EXPORT_METHOD(setMuted:(NSString *)callUUID
125
+                     muted:(BOOL)muted
126
+                   resolve:(RCTPromiseResolveBlock)resolve
127
+                    reject:(RCTPromiseRejectBlock)reject) {
130 128
 #ifdef DEBUG
131
-    NSLog(@"[RNCallKit][setMuted] uuidString = %@", uuidString);
129
+    NSLog(@"[RNCallKit][setMuted] callUUID = %@", callUUID);
132 130
 #endif
133
-    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
131
+
132
+    NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
134 133
     CXSetMutedCallAction *action
135
-    = [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:muted];
136
-    CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
137
-    [self requestTransaction:transaction resolve:resolve reject:reject];
134
+        = [[CXSetMutedCallAction alloc] initWithCallUUID:callUUID_ muted:muted];
135
+    [self requestTransaction:[[CXTransaction alloc] initWithAction:action]
136
+                     resolve:resolve
137
+                      reject:reject];
138
+}
139
+
140
+RCT_EXPORT_METHOD(setProviderConfiguration:(NSDictionary *)dictionary) {
141
+#ifdef DEBUG
142
+    NSLog(
143
+        @"[RNCallKit][setProviderConfiguration:] dictionary = %@",
144
+        dictionary);
145
+#endif
146
+
147
+    CXProviderConfiguration *configuration
148
+        = [self providerConfigurationFromDictionary:dictionary];
149
+    if (_provider) {
150
+        _provider.configuration = configuration;
151
+    } else {
152
+        _provider = [[CXProvider alloc] initWithConfiguration:configuration];
153
+        [_provider setDelegate:self queue:nil];
154
+    }
138 155
 }
139 156
 
140 157
 // Start outgoing call
141
-RCT_EXPORT_METHOD(startCall:(NSString *)uuidString
158
+RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
142 159
                      handle:(NSString *)handle
143 160
                       video:(BOOL)video
144 161
                     resolve:(RCTPromiseResolveBlock)resolve
145
-                     reject:(RCTPromiseRejectBlock)reject)
146
-{
162
+                     reject:(RCTPromiseRejectBlock)reject) {
147 163
 #ifdef DEBUG
148
-    NSLog(@"[RNCallKit][startCall] uuidString = %@", uuidString);
164
+    NSLog(@"[RNCallKit][startCall] callUUID = %@", callUUID);
149 165
 #endif
150
-    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
151
-    CXHandle *callHandle
166
+
167
+    NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
168
+    CXHandle *handle_
152 169
         = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
153 170
     CXStartCallAction *action
154
-        = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
171
+        = [[CXStartCallAction alloc] initWithCallUUID:callUUID_
172
+                                               handle:handle_];
155 173
     action.video = video;
156 174
     CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
157 175
     [self requestTransaction:transaction resolve:resolve reject:reject];
158 176
 }
159 177
 
160 178
 // Indicate call failed
161
-RCT_EXPORT_METHOD(reportCallFailed:(NSString *)uuidString
162
-                  resolve:(RCTPromiseResolveBlock)resolve
163
-                  reject:(RCTPromiseRejectBlock)reject)
164
-{
165
-    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
166
-    [callKitProvider reportCallWithUUID:uuid
167
-                            endedAtDate:[NSDate date]
168
-                                 reason:CXCallEndedReasonFailed];
179
+RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID
180
+                           resolve:(RCTPromiseResolveBlock)resolve
181
+                            reject:(RCTPromiseRejectBlock)reject) {
182
+    NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
183
+    [self.provider reportCallWithUUID:callUUID_
184
+                          endedAtDate:[NSDate date]
185
+                               reason:CXCallEndedReasonFailed];
169 186
     resolve(nil);
170 187
 }
171 188
 
172
-// Indicate outgoing call connected
173
-RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)uuidString
174
-                  resolve:(RCTPromiseResolveBlock)resolve
175
-                  reject:(RCTPromiseRejectBlock)reject)
176
-{
177
-    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
178
-    [callKitProvider reportOutgoingCallWithUUID:uuid
179
-                                connectedAtDate:[NSDate date]];
189
+// Indicate outgoing call connected.
190
+RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID
191
+                                      resolve:(RCTPromiseResolveBlock)resolve
192
+                                       reject:(RCTPromiseRejectBlock)reject) {
193
+    NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
194
+    [self.provider reportOutgoingCallWithUUID:callUUID_
195
+                              connectedAtDate:[NSDate date]];
180 196
     resolve(nil);
181 197
 }
182 198
 
183
-// Update call in case we have a display name or video capability changes
184
-RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
199
+// Update call in case we have a display name or video capability changes.
200
+RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
185 201
                      options:(NSDictionary *)options
186 202
                      resolve:(RCTPromiseResolveBlock)resolve
187
-                      reject:(RCTPromiseRejectBlock)reject)
188
-{
203
+                      reject:(RCTPromiseRejectBlock)reject) {
189 204
 #ifdef DEBUG
190
-    NSLog(@"[RNCallKit][updateCall] uuidString = %@ options = %@", uuidString, options);
205
+    NSLog(
206
+        @"[RNCallKit][updateCall] callUUID = %@ options = %@",
207
+        callUUID,
208
+        options);
191 209
 #endif
192
-    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
193
-    CXCallUpdate *update = [[CXCallUpdate alloc] init];
210
+
211
+    NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
212
+    CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
194 213
     if (options[@"displayName"]) {
195
-        update.localizedCallerName = options[@"displayName"];
214
+        callUpdate.localizedCallerName = options[@"displayName"];
196 215
     }
197 216
     if (options[@"hasVideo"]) {
198
-        update.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
217
+        callUpdate.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
199 218
     }
200
-    [callKitProvider reportCallWithUUID:uuid updated:update];
219
+    [self.provider reportCallWithUUID:callUUID_ updated:callUpdate];
201 220
     resolve(nil);
202 221
 }
203 222
 
204 223
 #pragma mark - Helper methods
205 224
 
206
-- (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary* )settings
207
-{
225
+- (CXCallController *)callController {
226
+    if (!_callController) {
227
+        _callController = [[CXCallController alloc] init];
228
+    }
229
+
230
+    return _callController;
231
+}
232
+
233
+- (CXProvider *)provider {
234
+    if (!_provider) {
235
+        [self setProviderConfiguration:nil];
236
+    }
237
+
238
+    return _provider;
239
+}
240
+
241
+- (CXProviderConfiguration *)providerConfigurationFromDictionary:(NSDictionary* )dictionary {
208 242
 #ifdef DEBUG
209
-    NSLog(@"[RNCallKit][getProviderConfiguration]");
243
+    NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]");
210 244
 #endif
245
+
246
+    if (!dictionary) {
247
+        dictionary = @{};
248
+    }
249
+
250
+    // localizedName
251
+    NSString *localizedName = dictionary[@"localizedName"];
252
+    if (!localizedName) {
253
+        localizedName
254
+            = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
255
+    }
256
+
211 257
     CXProviderConfiguration *providerConfiguration
212
-        = [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]];
213
-    providerConfiguration.supportsVideo = YES;
258
+        = [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName];
259
+
260
+    // iconTemplateImageData
261
+    NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"];
262
+    if (iconTemplateImageName) {
263
+        UIImage *iconTemplateImage = [UIImage imageNamed:iconTemplateImageName];
264
+        if (iconTemplateImage) {
265
+            providerConfiguration.iconTemplateImageData
266
+                = UIImagePNGRepresentation(iconTemplateImage);
267
+        }
268
+    }
269
+
214 270
     providerConfiguration.maximumCallGroups = 1;
215 271
     providerConfiguration.maximumCallsPerCallGroup = 1;
272
+    providerConfiguration.ringtoneSound = dictionary[@"ringtoneSound"];
216 273
     providerConfiguration.supportedHandleTypes
217
-        = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
218
-    if (settings[@"imageName"]) {
219
-        providerConfiguration.iconTemplateImageData
220
-            = UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]);
221
-    }
222
-    if (settings[@"ringtoneSound"]) {
223
-        providerConfiguration.ringtoneSound = settings[@"ringtoneSound"];
224
-    }
274
+        = [NSSet setWithObjects:@(CXHandleTypeGeneric), nil];
275
+    providerConfiguration.supportsVideo = YES;
276
+
225 277
     return providerConfiguration;
226 278
 }
227 279
 
228 280
 - (void)requestTransaction:(CXTransaction *)transaction
229 281
                    resolve:(RCTPromiseResolveBlock)resolve
230
-                    reject:(RCTPromiseRejectBlock)reject
231
-{
282
+                    reject:(RCTPromiseRejectBlock)reject {
232 283
 #ifdef DEBUG
233 284
     NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
234 285
 #endif
235
-    [callKitCallController requestTransaction:transaction completion:^(NSError * _Nullable error) {
236
-        if (error == nil) {
237
-            resolve(nil);
238
-        } else {
239
-            NSLog(@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error);
286
+
287
+    [self.callController requestTransaction:transaction
288
+                                 completion:^(NSError * _Nullable error) {
289
+        if (error) {
290
+            NSLog(
291
+                @"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)",
292
+                transaction.actions,
293
+                error);
240 294
             reject(nil, @"Error processing CallKit transaction", error);
295
+        } else {
296
+            resolve(nil);
241 297
         }
242 298
     }];
243 299
 }
@@ -247,53 +303,62 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
247 303
 // Called when the provider has been reset. We should terminate all calls.
248 304
 - (void)providerDidReset:(CXProvider *)provider {
249 305
 #ifdef DEBUG
250
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:providerDidReset]");
306
+    NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]");
251 307
 #endif
308
+
252 309
     [self sendEventWithName:RNCallKitProviderDidReset body:nil];
253 310
 }
254 311
 
255 312
 // Answering incoming call
256
-- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
257
-{
313
+- (void)         provider:(CXProvider *)provider
314
+  performAnswerCallAction:(CXAnswerCallAction *)action {
258 315
 #ifdef DEBUG
259
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction]");
316
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]");
260 317
 #endif
318
+
261 319
     [self sendEventWithName:RNCallKitPerformAnswerCallAction
262 320
                        body:@{ @"callUUID": action.callUUID.UUIDString }];
263 321
     [action fulfill];
264 322
 }
265 323
 
266 324
 // Call ended, user request
267
-- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
268
-{
325
+- (void)      provider:(CXProvider *)provider
326
+  performEndCallAction:(CXEndCallAction *)action {
269 327
 #ifdef DEBUG
270
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction]");
328
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]");
271 329
 #endif
330
+
272 331
     [self sendEventWithName:RNCallKitPerformEndCallAction
273 332
                        body:@{ @"callUUID": action.callUUID.UUIDString }];
274 333
     [action fulfill];
275 334
 }
276 335
 
277 336
 // Handle audio mute from CallKit view
278
-- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
337
+- (void)           provider:(CXProvider *)provider
338
+  performSetMutedCallAction:(CXSetMutedCallAction *)action {
279 339
 #ifdef DEBUG
280
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction]");
340
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]");
281 341
 #endif
342
+
282 343
     [self sendEventWithName:RNCallKitPerformSetMutedCallAction
283
-                       body:@{ @"callUUID": action.callUUID.UUIDString,
284
-                               @"muted": [NSNumber numberWithBool:action.muted]}];
344
+                       body:@{
345
+                           @"callUUID": action.callUUID.UUIDString,
346
+                           @"muted": @(action.muted)
347
+                       }];
285 348
     [action fulfill];
286 349
 }
287 350
 
288 351
 // Starting outgoing call
289
-- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
290
-{
352
+- (void)        provider:(CXProvider *)provider
353
+  performStartCallAction:(CXStartCallAction *)action {
291 354
 #ifdef DEBUG
292
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction]");
355
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]");
293 356
 #endif
357
+
294 358
     [action fulfill];
295
-    
296
-    // Update call info
359
+
360
+    // Update call info.
361
+    NSUUID *callUUID = action.callUUID;
297 362
     CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
298 363
     callUpdate.remoteHandle = action.handle;
299 364
     callUpdate.supportsDTMF = NO;
@@ -301,34 +366,31 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
301 366
     callUpdate.supportsGrouping = NO;
302 367
     callUpdate.supportsUngrouping = NO;
303 368
     callUpdate.hasVideo = action.isVideo;
304
-    [callKitProvider reportCallWithUUID:action.callUUID updated:callUpdate];
305
-    
306
-    // Notify the system about the outgoing call
307
-    [callKitProvider reportOutgoingCallWithUUID:action.callUUID
308
-                        startedConnectingAtDate:[NSDate date]];
309
-}
369
+    [provider reportCallWithUUID:callUUID updated:callUpdate];
310 370
 
311
-// These just help with debugging
371
+    // Notify the system about the outgoing call.
372
+    [provider reportOutgoingCallWithUUID:callUUID
373
+                 startedConnectingAtDate:[NSDate date]];
374
+}
312 375
 
313
-- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
314
-{
376
+// The following just help with debugging:
315 377
 #ifdef DEBUG
316
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession]");
317
-#endif
378
+
379
+- (void)         provider:(CXProvider *)provider
380
+  didActivateAudioSession:(AVAudioSession *)audioSession {
381
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]");
318 382
 }
319 383
 
320
-- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
321
-{
322
-#ifdef DEBUG
323
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession]");
324
-#endif
384
+- (void)           provider:(CXProvider *)provider
385
+  didDeactivateAudioSession:(AVAudioSession *)audioSession {
386
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
325 387
 }
326 388
 
327
-- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action
328
-{
329
-#ifdef DEBUG
330
-    NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction]");
331
-#endif
389
+- (void)          provider:(CXProvider *)provider
390
+  timedOutPerformingAction:(CXAction *)action {
391
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
332 392
 }
333 393
 
394
+#endif
395
+
334 396
 @end

+ 1
- 1
ios/sdk/src/JitsiMeetView.h View File

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

+ 41
- 52
ios/sdk/src/JitsiMeetView.m View File

@@ -17,26 +17,15 @@
17 17
 #import <CoreText/CoreText.h>
18 18
 #include <mach/mach_time.h>
19 19
 
20
+@import Intents;
21
+
20 22
 #import <React/RCTAssert.h>
21 23
 #import <React/RCTLinkingManager.h>
22 24
 #import <React/RCTRootView.h>
23 25
 
24
-#include <Availability.h>
25
-#import <Foundation/Foundation.h>
26
-
27 26
 #import "JitsiMeetView+Private.h"
28 27
 #import "RCTBridgeWrapper.h"
29 28
 
30
-// Weakly load the Intents framework since it's not available on iOS 9.
31
-@import Intents;
32
-
33
-// Constant describing iOS 10.0.0
34
-static const NSOperatingSystemVersion ios10 = {
35
-  .majorVersion = 10,
36
-  .minorVersion = 0,
37
-  .patchVersion = 0
38
-};
39
-
40 29
 /**
41 30
  * A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.
42 31
  * In the Release configuration, React Native will (intentionally) raise an
@@ -153,50 +142,45 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
153 142
   continueUserActivity:(NSUserActivity *)userActivity
154 143
     restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
155 144
 {
145
+    NSString *activityType = userActivity.activityType;
146
+
156 147
     // XXX At least twice we received bug reports about malfunctioning loadURL
157 148
     // in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as
158 149
     // expected in our testing. But that was to be expected because the app does
159 150
     // not exercise loadURL. In order to increase the test coverage of loadURL,
160 151
     // channel Universal linking through loadURL.
161
-    if ([userActivity.activityType
162
-                isEqualToString:NSUserActivityTypeBrowsingWeb]
163
-            && [JitsiMeetView loadURLInViews:userActivity.webpageURL]) {
152
+    if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]
153
+            && [self loadURLInViews:userActivity.webpageURL]) {
164 154
         return YES;
165 155
     }
166 156
 
167
-      // Check for CallKit intents only on iOS >= 10
168
-      if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) {
169
-          if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]
170
-              || [userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
171
-              INInteraction *interaction = [userActivity interaction];
172
-              INIntent *intent = interaction.intent;
173
-              NSString *handle;
174
-              BOOL isAudio = NO;
175
-
176
-              if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
177
-                  INStartAudioCallIntent *startCallIntent
178
-                      = (INStartAudioCallIntent *)intent;
179
-                  handle = startCallIntent.contacts.firstObject.personHandle.value;
180
-                  isAudio = YES;
181
-              } else {
182
-                  INStartVideoCallIntent *startCallIntent
183
-                      = (INStartVideoCallIntent *)intent;
184
-                  handle = startCallIntent.contacts.firstObject.personHandle.value;
185
-              }
186
-
187
-              if (handle) {
188
-              // Load the URL contained in the handle
189
-              [view loadURLObject:@{
190
-                                    @"url": handle,
191
-                                    @"configOverwrite": @{
192
-                                        @"startAudioOnly": @(isAudio)
193
-                                    }
194
-                                    }];
195
-
196
-              return YES;
197
-              }
198
-          }
199
-      }
157
+    // Check for a CallKit intent.
158
+    if ([activityType isEqualToString:@"INStartAudioCallIntent"]
159
+            || [activityType isEqualToString:@"INStartVideoCallIntent"]) {
160
+        INIntent *intent = userActivity.interaction.intent;
161
+        NSArray<INPerson *> *contacts;
162
+        NSString *url;
163
+        BOOL startAudioOnly = NO;
164
+
165
+        if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
166
+            contacts = ((INStartAudioCallIntent *) intent).contacts;
167
+            startAudioOnly = YES;
168
+        } else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
169
+            contacts = ((INStartVideoCallIntent *) intent).contacts;
170
+        }
171
+
172
+        if (contacts && (url = contacts.firstObject.personHandle.value)) {
173
+            // Load the URL contained in the handle.
174
+            [self loadURLObjectInViews:@{
175
+                @"config": @{
176
+                    @"startAudioOnly": @(startAudioOnly)
177
+                },
178
+                @"url": url
179
+            }];
180
+
181
+            return YES;
182
+        }
183
+    }
200 184
 
201 185
     return [RCTLinkingManager application:application
202 186
                      continueUserActivity:userActivity
@@ -212,7 +196,7 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
212 196
     // expected in our testing. But that was to be expected because the app does
213 197
     // not exercise loadURL. In order to increase the test coverage of loadURL,
214 198
     // channel Universal linking through loadURL.
215
-    if ([JitsiMeetView loadURLInViews:url]) {
199
+    if ([self loadURLInViews:url]) {
216 200
         return YES;
217 201
     }
218 202
 
@@ -341,15 +325,20 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
341 325
  * at least one {@code JitsiMeetView}; otherwise, {@code NO}.
342 326
  */
343 327
 + (BOOL)loadURLInViews:(NSURL *)url {
328
+    return
329
+        [self loadURLObjectInViews:url ? @{ @"url": url.absoluteString } : nil];
330
+}
331
+
332
++ (BOOL)loadURLObjectInViews:(NSDictionary *)urlObject {
344 333
     BOOL handled = NO;
345 334
 
346 335
     if (views) {
347 336
         for (NSString *externalAPIScope in views) {
348 337
             JitsiMeetView *view
349
-                = [JitsiMeetView viewForExternalAPIScope:externalAPIScope];
338
+                = [self viewForExternalAPIScope:externalAPIScope];
350 339
 
351 340
             if (view) {
352
-                [view loadURL:url];
341
+                [view loadURLObject:urlObject];
353 342
                 handled = YES;
354 343
             }
355 344
         }

+ 24
- 8
react/features/base/conference/functions.js View File

@@ -1,12 +1,12 @@
1 1
 import { JitsiTrackErrors } from '../lib-jitsi-meet';
2
+import { toState } from '../redux';
2 3
 
3 4
 /**
4 5
  * Attach a set of local tracks to a conference.
5 6
  *
6
- * NOTE The function is internal to this feature.
7
- *
8 7
  * @param {JitsiConference} conference - Conference instance.
9 8
  * @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
9
+ * @protected
10 10
  * @returns {Promise}
11 11
  */
12 12
 export function _addLocalTracksToConference(conference, localTracks) {
@@ -29,14 +29,33 @@ export function _addLocalTracksToConference(conference, localTracks) {
29 29
     return Promise.all(promises);
30 30
 }
31 31
 
32
+/**
33
+ * Returns the current {@code JitsiConference} which is joining or joined and is
34
+ * not leaving. Please note the contrast with merely reading the
35
+ * {@code conference} state of the feature base/conference which is not joining
36
+ * but may be leaving already.
37
+ *
38
+ * @param {Function|Object} stateful - The redux store, state, or
39
+ * {@code getState} function.
40
+ * @returns {JitsiConference|undefined}
41
+ */
42
+export function getCurrentConference(stateful) {
43
+    const { conference, joining, leaving }
44
+        = toState(stateful)['features/base/conference'];
45
+
46
+    return (
47
+        conference
48
+            ? conference === leaving ? undefined : conference
49
+            : joining);
50
+}
51
+
32 52
 /**
33 53
  * Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
34 54
  * manipulating a conference participant (e.g. pin or select participant).
35 55
  *
36
- * NOTE The function is internal to this feature.
37
- *
38 56
  * @param {Error} err - The Error which was thrown by the backend while
39 57
  * manipulating a conference participant and which is to be handled.
58
+ * @protected
40 59
  * @returns {void}
41 60
  */
42 61
 export function _handleParticipantError(err) {
@@ -65,10 +84,9 @@ export function isRoomValid(room) {
65 84
 /**
66 85
  * Remove a set of local tracks from a conference.
67 86
  *
68
- * NOTE The function is internal to this feature.
69
- *
70 87
  * @param {JitsiConference} conference - Conference instance.
71 88
  * @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
89
+ * @protected
72 90
  * @returns {Promise}
73 91
  */
74 92
 export function _removeLocalTracksFromConference(conference, localTracks) {
@@ -93,8 +111,6 @@ export function _removeLocalTracksFromConference(conference, localTracks) {
93 111
  * time of this writing, the intention of the function is to abstract the
94 112
  * reporting of errors and facilitate elaborating on it in the future.
95 113
  *
96
- * NOTE The function is internal to this feature.
97
- *
98 114
  * @param {string} msg - The error message to report.
99 115
  * @param {Error} err - The Error to report.
100 116
  * @private

+ 1
- 3
react/features/base/conference/reducer.js View File

@@ -16,9 +16,7 @@ import {
16 16
     SET_RECEIVE_VIDEO_QUALITY,
17 17
     SET_ROOM
18 18
 } from './actionTypes';
19
-import {
20
-    VIDEO_QUALITY_LEVELS
21
-} from './constants';
19
+import { VIDEO_QUALITY_LEVELS } from './constants';
22 20
 import { isRoomValid } from './functions';
23 21
 
24 22
 /**

+ 17
- 11
react/features/base/media/functions.js View File

@@ -1,44 +1,50 @@
1 1
 /* @flow */
2 2
 
3
+import { toState } from '../redux';
4
+
3 5
 import { VIDEO_MUTISM_AUTHORITY } from './constants';
4 6
 
5 7
 /**
6 8
  * Determines whether video is currently muted by the audio-only authority.
7 9
  *
8
- * @param {Store} store - The redux store.
10
+ * @param {Function|Object} stateful - The redux store, state, or
11
+ * {@code getState} function.
9 12
  * @returns {boolean}
10 13
  */
11
-export function isVideoMutedByAudioOnly(store: { getState: Function }) {
12
-    return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY);
14
+export function isVideoMutedByAudioOnly(stateful: Function | Object) {
15
+    return (
16
+        _isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
13 17
 }
14 18
 
15 19
 /**
16 20
  * Determines whether video is currently muted by a specific
17 21
  * <tt>VIDEO_MUTISM_AUTHORITY</tt>.
18 22
  *
19
- * @param {Store} store - The redux store.
23
+ * @param {Function|Object} stateful - The redux store, state, or
24
+ * {@code getState} function.
20 25
  * @param {number} videoMutismAuthority - The <tt>VIDEO_MUTISM_AUTHORITY</tt>
21 26
  * which is to be checked whether it has muted video.
22 27
  * @returns {boolean} If video is currently muted by the specified
23 28
  * <tt>videoMutismAuthority</tt>, then <tt>true</tt>; otherwise, <tt>false</tt>.
24 29
  */
25 30
 function _isVideoMutedByAuthority(
26
-        { getState }: { getState: Function },
31
+        stateful: Function | Object,
27 32
         videoMutismAuthority: number) {
28
-    return Boolean(
33
+    const { muted } = toState(stateful)['features/base/media'].video;
29 34
 
30
-        // eslint-disable-next-line no-bitwise
31
-        getState()['features/base/media'].video.muted & videoMutismAuthority);
35
+    // eslint-disable-next-line no-bitwise
36
+    return Boolean(muted & videoMutismAuthority);
32 37
 }
33 38
 
34 39
 /**
35 40
  * Determines whether video is currently muted by the user authority.
36 41
  *
37
- * @param {Store} store - The redux store.
42
+ * @param {Function|Object} stateful - The redux store, state, or
43
+ * {@code getState} function.
38 44
  * @returns {boolean}
39 45
  */
40
-export function isVideoMutedByUser(store: { getState: Function }) {
41
-    return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.USER);
46
+export function isVideoMutedByUser(stateful: Function | Object) {
47
+    return _isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.USER);
42 48
 }
43 49
 
44 50
 /**

+ 20
- 216
react/features/mobile/callkit/CallKit.js View File

@@ -1,10 +1,4 @@
1
-import {
2
-    NativeModules,
3
-    NativeEventEmitter,
4
-    Platform
5
-} from 'react-native';
6
-
7
-const RNCallKit = NativeModules.RNCallKit;
1
+import { NativeModules, NativeEventEmitter } from 'react-native';
8 2
 
9 3
 /**
10 4
  * Thin wrapper around Apple's CallKit functionality.
@@ -18,218 +12,28 @@ const RNCallKit = NativeModules.RNCallKit;
18 12
  * the "endCall" method in this class, for example.
19 13
  *
20 14
  * Emitted events:
21
- *  - performAnswerCallAction: The user pressed the answer button.
22
- *  - performEndCallAction: The call should be ended.
23
- *  - performSetMutedCallAction: The call muted state should change. The
24
- *    ancillary `data` object contains a `muted` attribute.
25
- *  - providerDidReset: The system has reset, all calls should be terminated.
26
- *    This event gets no associated data.
15
+ * - performAnswerCallAction: The user pressed the answer button.
16
+ * - performEndCallAction: The call should be ended.
17
+ * - performSetMutedCallAction: The call muted state should change. The
18
+ *   ancillary `data` object contains a `muted` attribute.
19
+ * - providerDidReset: The system has reset, all calls should be terminated.
20
+ *   This event gets no associated data.
27 21
  *
28 22
  * All events get a `data` object with a `callUUID` property, unless stated
29 23
  * otherwise.
30 24
  */
31
-class CallKit extends NativeEventEmitter {
32
-    /**
33
-     * Initializes a new {@code CallKit} instance.
34
-     */
35
-    constructor() {
36
-        super(RNCallKit);
37
-        this._setup = false;
38
-    }
39
-
40
-    /**
41
-     * Returns True if the current platform is supported, false otherwise. The
42
-     * supported platforms are: iOS >= 10.
43
-     *
44
-     * @private
45
-     * @returns {boolean}
46
-     */
47
-    static isSupported() {
48
-        return Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 10;
49
-    }
50
-
51
-    /**
52
-     * Checks if CallKit was setup, and throws an exception in that case.
53
-     *
54
-     * @private
55
-     * @returns {void}
56
-     */
57
-    _checkSetup() {
58
-        if (!this._setup) {
59
-            throw new Error('CallKit not initialized, call setup() first.');
60
-        }
61
-    }
62
-
63
-    /**
64
-     * Adds a listener for the given event.
65
-     *
66
-     * @param {string} event - Name of the event we are interested in.
67
-     * @param {Function} listener - Function which will be called when the
68
-     * desired event is emitted.
69
-     * @returns {void}
70
-     */
71
-    addEventListener(event, listener) {
72
-        this._checkSetup();
73
-        if (!CallKit.isSupported()) {
74
-            return;
75
-        }
76
-
77
-        this.addListener(event, listener);
78
-    }
79
-
80
-    /**
81
-     * Notifies CallKit about an incoming call. This will display the system
82
-     * incoming call view.
83
-     *
84
-     * @param {string} uuid - Unique identifier for the call.
85
-     * @param {string} handle - Call handle in CallKit's terms. The room URL.
86
-     * @param {boolean} hasVideo - True if it's a video call, false otherwise.
87
-     * @returns {Promise}
88
-     */
89
-    displayIncomingCall(uuid, handle, hasVideo = true) {
90
-        this._checkSetup();
91
-        if (!CallKit.isSupported()) {
92
-            return Promise.resolve();
93
-        }
94
-
95
-        return RNCallKit.displayIncomingCall(uuid, handle, hasVideo);
96
-    }
97
-
98
-    /**
99
-     * Request CallKit to end the call.
100
-     *
101
-     * @param {string} uuid - Unique identifier for the call.
102
-     * @returns {Promise}
103
-     */
104
-    endCall(uuid) {
105
-        this._checkSetup();
106
-        if (!CallKit.isSupported()) {
107
-            return Promise.resolve();
108
-        }
109
-
110
-        return RNCallKit.endCall(uuid);
111
-    }
112
-
113
-    /**
114
-     * Removes a listener for the given event.
115
-     *
116
-     * @param {string} event - Name of the event we are no longer interested in.
117
-     * @param {Function} listener - Function which used to be called when the
118
-     * desired event was emitted.
119
-     * @returns {void}
120
-     */
121
-    removeEventListener(event, listener) {
122
-        this._checkSetup();
123
-        if (!CallKit.isSupported()) {
124
-            return;
125
-        }
126
-
127
-        this.removeListener(event, listener);
128
-    }
129
-
130
-    /**
131
-     * Indicate CallKit that the outgoing call with the given UUID is now
132
-     * connected.
133
-     *
134
-     * @param {string} uuid - Unique identifier for the call.
135
-     * @returns {Promise}
136
-     */
137
-    reportConnectedOutgoingCall(uuid) {
138
-        this._checkSetup();
139
-        if (!CallKit.isSupported()) {
140
-            return Promise.resolve();
141
-        }
142
-
143
-        return RNCallKit.reportConnectedOutgoingCall(uuid);
144
-    }
145
-
146
-    /**
147
-     * Indicate CallKit that the call with the given UUID has failed.
148
-     *
149
-     * @param {string} uuid - Unique identifier for the call.
150
-     * @returns {Promise}
151
-     */
152
-    reportCallFailed(uuid) {
153
-        this._checkSetup();
154
-        if (!CallKit.isSupported()) {
155
-            return Promise.resolve();
156
-        }
157
-
158
-        return RNCallKit.reportCallFailed(uuid);
159
-    }
160
-
161
-    /**
162
-     * Tell CallKit about the audio muted state.
163
-     *
164
-     * @param {string} uuid - Unique identifier for the call.
165
-     * @param {boolean} muted - True if audio is muted, false otherwise.
166
-     * @returns {Promise}
167
-     */
168
-    setMuted(uuid, muted) {
169
-        this._checkSetup();
170
-        if (!CallKit.isSupported()) {
171
-            return Promise.resolve();
172
-        }
173
-
174
-        return RNCallKit.setMuted(uuid, muted);
175
-    }
176
-
177
-    /**
178
-     * Prepare / initialize CallKit. This method must be called before any
179
-     * other.
180
-     *
181
-     * @param {Object} options - Initialization options.
182
-     * @param {string} options.imageName - Image to be used in CallKit's
183
-     * application button..
184
-     * @param {string} options.ringtoneSound - Ringtone to be used for incoming
185
-     * calls.
186
-     * @returns {void}
187
-     */
188
-    setup(options = {}) {
189
-        if (CallKit.isSupported()) {
190
-            options.appName = NativeModules.AppInfo.name;
191
-            RNCallKit.setup(options);
192
-        }
193
-
194
-        this._setup = true;
195
-    }
196
-
197
-    /**
198
-     * Indicate CallKit about a new outgoing call.
199
-     *
200
-     * @param {string} uuid - Unique identifier for the call.
201
-     * @param {string} handle - Call handle in CallKit's terms. The room URL in
202
-     * our case.
203
-     * @param {boolean} hasVideo - True if it's a video call, false otherwise.
204
-     * @returns {Promise}
205
-     */
206
-    startCall(uuid, handle, hasVideo = true) {
207
-        this._checkSetup();
208
-        if (!CallKit.isSupported()) {
209
-            return Promise.resolve();
210
-        }
211
-
212
-        return RNCallKit.startCall(uuid, handle, hasVideo);
213
-    }
214
-
215
-    /**
216
-     * Updates an ongoing call's parameters.
217
-     *
218
-     * @param {string} uuid - Unique identifier for the call.
219
-     * @param {Object} options - Object with properties which should be updated.
220
-     * @param {string} options.displayName - Display name for the caller.
221
-     * @param {boolean} options.hasVideo - True if the call has video, false
222
-     * otherwise.
223
-     * @returns {Promise}
224
-     */
225
-    updateCall(uuid, options) {
226
-        this._checkSetup();
227
-        if (!CallKit.isSupported()) {
228
-            return Promise.resolve();
229
-        }
230
-
231
-        return RNCallKit.updateCall(uuid, options);
232
-    }
25
+let CallKit = NativeModules.RNCallKit;
26
+
27
+// XXX Rather than wrapping RNCallKit in a new class and forwarding the many
28
+// methods of the latter to the former, add the one additional method that we
29
+// need to RNCallKit.
30
+if (CallKit) {
31
+    const eventEmitter = new NativeEventEmitter(CallKit);
32
+
33
+    CallKit = {
34
+        ...CallKit,
35
+        addListener: eventEmitter.addListener.bind(eventEmitter)
36
+    };
233 37
 }
234 38
 
235
-export default new CallKit();
39
+export default CallKit;

+ 4
- 4
react/features/mobile/callkit/actionTypes.js View File

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

+ 285
- 132
react/features/mobile/callkit/middleware.js View File

@@ -1,27 +1,26 @@
1 1
 /* @flow */
2 2
 
3
+import { NativeModules } from 'react-native';
3 4
 import uuid from 'uuid';
4 5
 
5
-import {
6
-    APP_WILL_MOUNT,
7
-    APP_WILL_UNMOUNT,
8
-    appNavigate
9
-} from '../../app';
6
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, appNavigate } from '../../app';
10 7
 import {
11 8
     CONFERENCE_FAILED,
12 9
     CONFERENCE_LEFT,
13 10
     CONFERENCE_WILL_JOIN,
14
-    CONFERENCE_JOINED
11
+    CONFERENCE_JOINED,
12
+    getCurrentConference
15 13
 } from '../../base/conference';
16 14
 import { getInviteURL } from '../../base/connection';
17 15
 import {
16
+    isVideoMutedByAudioOnly,
18 17
     SET_AUDIO_MUTED,
19 18
     SET_VIDEO_MUTED,
20
-    isVideoMutedByAudioOnly,
21 19
     setAudioMuted
22 20
 } from '../../base/media';
23
-import { MiddlewareRegistry, toState } from '../../base/redux';
24
-import { _SET_CALLKIT_LISTENERS } from './actionTypes';
21
+import { MiddlewareRegistry } from '../../base/redux';
22
+
23
+import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
25 24
 import CallKit from './CallKit';
26 25
 
27 26
 /**
@@ -30,165 +29,319 @@ import CallKit from './CallKit';
30 29
  * @param {Store} store - The redux store.
31 30
  * @returns {Function}
32 31
  */
33
-MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
34
-    const result = next(action);
35
-
32
+CallKit && MiddlewareRegistry.register(store => next => action => {
36 33
     switch (action.type) {
37
-    case _SET_CALLKIT_LISTENERS: {
38
-        const { listeners } = getState()['features/callkit'];
39
-
40
-        if (listeners) {
41
-            for (const [ event, listener ] of listeners) {
42
-                CallKit.removeEventListener(event, listener);
43
-            }
44
-        }
45
-
46
-        if (action.listeners) {
47
-            for (const [ event, listener ] of action.listeners) {
48
-                CallKit.addEventListener(event, listener);
49
-            }
50
-        }
51
-
52
-        break;
53
-    }
34
+    case _SET_CALLKIT_SUBSCRIPTIONS:
35
+        return _setCallKitSubscriptions(store, next, action);
54 36
 
55
-    case APP_WILL_MOUNT: {
56
-        CallKit.setup();  // TODO: set app icon.
57
-        const listeners = new Map();
58
-        const callEndListener = data => {
59
-            const conference = getCurrentConference(getState);
60
-
61
-            if (conference && conference.callUUID === data.callUUID) {
62
-                // We arrive here when a call is ended by the system, for
63
-                // for example when another incoming call is received and the
64
-                // user selects "End & Accept".
65
-                delete conference.callUUID;
66
-                dispatch(appNavigate(undefined));
67
-            }
68
-        };
69
-
70
-        listeners.set('performEndCallAction', callEndListener);
71
-
72
-        // Set the same listener for providerDidReset. According to the docs,
73
-        // when the system resets we should terminate all calls.
74
-        listeners.set('providerDidReset', callEndListener);
75
-
76
-        const setMutedListener = data => {
77
-            const conference = getCurrentConference(getState);
78
-
79
-            if (conference && conference.callUUID === data.callUUID) {
80
-                // Break the loop. Audio can be muted both from the CallKit
81
-                // interface and from the Jitsi Meet interface. We must keep
82
-                // them in sync, but at some point the loop needs to be broken.
83
-                // We are doing it here, on the CallKit handler.
84
-                const { muted } = getState()['features/base/media'].audio;
85
-
86
-                if (muted !== data.muted) {
87
-                    dispatch(setAudioMuted(Boolean(data.muted)));
88
-                }
89
-
90
-            }
91
-        };
92
-
93
-        listeners.set('performSetMutedCallAction', setMutedListener);
94
-
95
-        dispatch({
96
-            type: _SET_CALLKIT_LISTENERS,
97
-            listeners
98
-        });
99
-        break;
100
-    }
37
+    case APP_WILL_MOUNT:
38
+        return _appWillMount(store, next, action);
101 39
 
102 40
     case APP_WILL_UNMOUNT:
103
-        dispatch({
104
-            type: _SET_CALLKIT_LISTENERS,
105
-            listeners: null
41
+        store.dispatch({
42
+            type: _SET_CALLKIT_SUBSCRIPTIONS,
43
+            subscriptions: undefined
106 44
         });
107 45
         break;
108 46
 
109
-    case CONFERENCE_FAILED: {
110
-        const { callUUID } = action.conference;
47
+    case CONFERENCE_FAILED:
48
+        return _conferenceFailed(store, next, action);
111 49
 
112
-        if (callUUID) {
113
-            CallKit.reportCallFailed(callUUID);
114
-        }
50
+    case CONFERENCE_JOINED:
51
+        return _conferenceJoined(store, next, action);
115 52
 
116
-        break;
117
-    }
53
+    case CONFERENCE_LEFT:
54
+        return _conferenceLeft(store, next, action);
118 55
 
119
-    case CONFERENCE_LEFT: {
120
-        const { callUUID } = action.conference;
56
+    case CONFERENCE_WILL_JOIN:
57
+        return _conferenceWillJoin(store, next, action);
121 58
 
122
-        if (callUUID) {
123
-            CallKit.endCall(callUUID);
124
-        }
59
+    case SET_AUDIO_MUTED:
60
+        return _setAudioMuted(store, next, action);
125 61
 
126
-        break;
62
+    case SET_VIDEO_MUTED:
63
+        return _setVideoMuted(store, next, action);
127 64
     }
128 65
 
129
-    case CONFERENCE_JOINED: {
130
-        const { callUUID } = action.conference;
66
+    return next(action);
67
+});
131 68
 
132
-        if (callUUID) {
133
-            CallKit.reportConnectedOutgoingCall(callUUID);
134
-        }
69
+/**
70
+ * Notifies the feature jwt that the action {@link APP_WILL_MOUNT} is being
71
+ * dispatched within a specific redux <tt>store</tt>.
72
+ *
73
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
74
+ * is being dispatched.
75
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
76
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
77
+ * @param {Action} action - The redux action <tt>APP_WILL_MOUNT</tt> which is
78
+ * being dispatched in the specified <tt>store</tt>.
79
+ * @private
80
+ * @returns {*}
81
+ */
82
+function _appWillMount({ dispatch, getState }, next, action) {
83
+    const result = next(action);
135 84
 
136
-        break;
85
+    CallKit.setProviderConfiguration({
86
+        iconTemplateImageName: 'AppIcon40x40',
87
+        localizedName: NativeModules.AppInfo.name
88
+    });
89
+
90
+    const context = {
91
+        dispatch,
92
+        getState
93
+    };
94
+    const subscriptions = [
95
+        CallKit.addListener(
96
+            'performEndCallAction',
97
+            _onPerformEndCallAction,
98
+            context),
99
+        CallKit.addListener(
100
+            'performSetMutedCallAction',
101
+            _onPerformSetMutedCallAction,
102
+            context),
103
+
104
+        // According to CallKit's documentation, when the system resets we
105
+        // should terminate all calls. Hence, providerDidReset is the same
106
+        // to us as performEndCallAction.
107
+        CallKit.addListener(
108
+            'providerDidReset',
109
+            _onPerformEndCallAction,
110
+            context)
111
+    ];
112
+
113
+    dispatch({
114
+        type: _SET_CALLKIT_SUBSCRIPTIONS,
115
+        subscriptions
116
+    });
117
+
118
+    return result;
119
+}
120
+
121
+/**
122
+ * Notifies the feature jwt that the action {@link CONFERENCE_FAILED} is being
123
+ * dispatched within a specific redux <tt>store</tt>.
124
+ *
125
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
126
+ * is being dispatched.
127
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
128
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
129
+ * @param {Action} action - The redux action <tt>CONFERENCE_FAILED</tt> which is
130
+ * being dispatched in the specified <tt>store</tt>.
131
+ * @private
132
+ * @returns {*}
133
+ */
134
+function _conferenceFailed(store, next, action) {
135
+    const result = next(action);
136
+
137
+    const { callUUID } = action.conference;
138
+
139
+    if (callUUID) {
140
+        CallKit.reportCallFailed(callUUID);
137 141
     }
138 142
 
139
-    case CONFERENCE_WILL_JOIN: {
140
-        const conference = action.conference;
141
-        const url = getInviteURL(getState);
142
-        const hasVideo = !isVideoMutedByAudioOnly({ getState });
143
+    return result;
144
+}
145
+
146
+/**
147
+ * Notifies the feature jwt that the action {@link CONFERENCE_JOINED} is being
148
+ * dispatched within a specific redux <tt>store</tt>.
149
+ *
150
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
151
+ * is being dispatched.
152
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
153
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
154
+ * @param {Action} action - The redux action <tt>CONFERENCE_JOINED</tt> which is
155
+ * being dispatched in the specified <tt>store</tt>.
156
+ * @private
157
+ * @returns {*}
158
+ */
159
+function _conferenceJoined(store, next, action) {
160
+    const result = next(action);
143 161
 
144
-        // When assigning the call UUID, do so in upper case, since iOS will
145
-        // return it upper cased.
146
-        conference.callUUID = uuid.v4().toUpperCase();
147
-        CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
148
-            .then(() => {
149
-                const { room } = getState()['features/base/conference'];
162
+    const { callUUID } = action.conference;
150 163
 
151
-                CallKit.updateCall(conference.callUUID, { displayName: room });
152
-            });
153
-        break;
164
+    if (callUUID) {
165
+        CallKit.reportConnectedOutgoingCall(callUUID);
154 166
     }
155 167
 
156
-    case SET_AUDIO_MUTED: {
157
-        const conference = getCurrentConference(getState);
168
+    return result;
169
+}
158 170
 
159
-        if (conference && conference.callUUID) {
160
-            CallKit.setMuted(conference.callUUID, action.muted);
161
-        }
171
+/**
172
+ * Notifies the feature jwt that the action {@link CONFERENCE_LEFT} is being
173
+ * dispatched within a specific redux <tt>store</tt>.
174
+ *
175
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
176
+ * is being dispatched.
177
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
178
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
179
+ * @param {Action} action - The redux action <tt>CONFERENCE_LEFT</tt> which is
180
+ * being dispatched in the specified <tt>store</tt>.
181
+ * @private
182
+ * @returns {*}
183
+ */
184
+function _conferenceLeft(store, next, action) {
185
+    const result = next(action);
162 186
 
163
-        break;
187
+    const { callUUID } = action.conference;
188
+
189
+    if (callUUID) {
190
+        CallKit.endCall(callUUID);
164 191
     }
165 192
 
166
-    case SET_VIDEO_MUTED: {
167
-        const conference = getCurrentConference(getState);
193
+    return result;
194
+}
168 195
 
169
-        if (conference && conference.callUUID) {
170
-            const hasVideo = !isVideoMutedByAudioOnly({ getState });
196
+/**
197
+ * Notifies the feature jwt that the action {@link CONFERENCE_WILL_JOIN} is
198
+ * being dispatched within a specific redux <tt>store</tt>.
199
+ *
200
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
201
+ * is being dispatched.
202
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
203
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
204
+ * @param {Action} action - The redux action <tt>CONFERENCE_WILL_JOIN</tt> which
205
+ * is being dispatched in the specified <tt>store</tt>.
206
+ * @private
207
+ * @returns {*}
208
+ */
209
+function _conferenceWillJoin({ getState }, next, action) {
210
+    const result = next(action);
171 211
 
172
-            CallKit.updateCall(conference.callUUID, { hasVideo });
173
-        }
212
+    const conference = action.conference;
213
+    const url = getInviteURL(getState);
214
+    const hasVideo = !isVideoMutedByAudioOnly(getState);
174 215
 
175
-        break;
216
+    // When assigning the call UUID, do so in upper case, since iOS will
217
+    // return it upper cased.
218
+    conference.callUUID = uuid.v4().toUpperCase();
219
+    CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
220
+        .then(() => {
221
+            const { room } = getState()['features/base/conference'];
222
+
223
+            CallKit.updateCall(conference.callUUID, { displayName: room });
224
+        });
225
+
226
+    return result;
227
+}
228
+
229
+/**
230
+ * Handles CallKit's event <tt>performEndCallAction</tt>.
231
+ *
232
+ * @param {Object} event - The details of the CallKit event
233
+ * <tt>performEndCallAction</tt>.
234
+ * @returns {void}
235
+ */
236
+function _onPerformEndCallAction({ callUUID }) {
237
+    const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
238
+    const conference = getCurrentConference(getState);
239
+
240
+    if (conference && conference.callUUID === callUUID) {
241
+        // We arrive here when a call is ended by the system, for
242
+        // example when another incoming call is received and the user
243
+        // selects "End & Accept".
244
+        delete conference.callUUID;
245
+        dispatch(appNavigate(undefined));
176 246
     }
247
+}
248
+
249
+/**
250
+ * Handles CallKit's event <tt>performSetMutedCallAction</tt>.
251
+ *
252
+ * @param {Object} event - The details of the CallKit event
253
+ * <tt>performSetMutedCallAction</tt>.
254
+ * @returns {void}
255
+ */
256
+function _onPerformSetMutedCallAction({ callUUID, muted: newValue }) {
257
+    const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
258
+    const conference = getCurrentConference(getState);
259
+
260
+    if (conference && conference.callUUID === callUUID) {
261
+        // Break the loop. Audio can be muted from both CallKit and Jitsi Meet.
262
+        // We must keep them in sync, but at some point the loop needs to be
263
+        // broken. We are doing it here, on the CallKit handler.
264
+        const { muted: oldValue } = getState()['features/base/media'].audio;
265
+
266
+        if (oldValue !== newValue) {
267
+            dispatch(setAudioMuted(Boolean(newValue)));
268
+        }
269
+    }
270
+}
271
+
272
+/**
273
+ * Notifies the feature jwt that the action {@link SET_AUDIO_MUTED} is being
274
+ * dispatched within a specific redux <tt>store</tt>.
275
+ *
276
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
277
+ * is being dispatched.
278
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
279
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
280
+ * @param {Action} action - The redux action <tt>SET_AUDIO_MUTED</tt> which is
281
+ * being dispatched in the specified <tt>store</tt>.
282
+ * @private
283
+ * @returns {*}
284
+ */
285
+function _setAudioMuted({ getState }, next, action) {
286
+    const result = next(action);
287
+
288
+    const conference = getCurrentConference(getState);
289
+
290
+    if (conference && conference.callUUID) {
291
+        CallKit.setMuted(conference.callUUID, action.muted);
177 292
     }
178 293
 
179 294
     return result;
180
-});
295
+}
181 296
 
182 297
 /**
183
- * Returns the currently active conference.
298
+ * Notifies the feature jwt that the action {@link _SET_CALLKIT_SUBSCRIPTIONS}
299
+ * is being dispatched within a specific redux <tt>store</tt>.
184 300
  *
185
- * @param {Function|Object} stateOrGetState - The redux state or redux's
186
- * {@code getState} function.
187
- * @returns {Conference|undefined}
301
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
302
+ * is being dispatched.
303
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
304
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
305
+ * @param {Action} action - The redux action <tt>_SET_CALLKIT_SUBSCRIPTIONS</tt>
306
+ * which is being dispatched in the specified <tt>store</tt>.
307
+ * @private
308
+ * @returns {*}
188 309
  */
189
-function getCurrentConference(stateOrGetState: Function | Object): ?Object {
190
-    const state = toState(stateOrGetState);
191
-    const { conference, joining } = state['features/base/conference'];
310
+function _setCallKitSubscriptions({ getState }, next, action) {
311
+    const { subscriptions } = getState()['features/callkit'];
192 312
 
193
-    return conference || joining;
313
+    if (subscriptions) {
314
+        for (const subscription of subscriptions) {
315
+            subscription.remove();
316
+        }
317
+    }
318
+
319
+    return next(action);
320
+}
321
+
322
+/**
323
+ * Notifies the feature jwt that the action {@link SET_VIDEO_MUTED} is being
324
+ * dispatched within a specific redux <tt>store</tt>.
325
+ *
326
+ * @param {Store} store - The redux store in which the specified <tt>action</tt>
327
+ * is being dispatched.
328
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
329
+ * specified <tt>action</tt> to the specified <tt>store</tt>.
330
+ * @param {Action} action - The redux action <tt>SET_VIDEO_MUTED</tt> which is
331
+ * being dispatched in the specified <tt>store</tt>.
332
+ * @private
333
+ * @returns {*}
334
+ */
335
+function _setVideoMuted({ getState }, next, action) {
336
+    const result = next(action);
337
+
338
+    const conference = getCurrentConference(getState);
339
+
340
+    if (conference && conference.callUUID) {
341
+        CallKit.updateCall(
342
+            conference.callUUID,
343
+            { hasVideo: !isVideoMutedByAudioOnly(getState) });
344
+    }
345
+
346
+    return result;
194 347
 }

+ 12
- 14
react/features/mobile/callkit/reducer.js View File

@@ -1,17 +1,15 @@
1
-import { ReducerRegistry } from '../../base/redux';
1
+import { assign, ReducerRegistry } from '../../base/redux';
2 2
 
3
-import {
4
-    _SET_CALLKIT_LISTENERS
5
-} from './actionTypes';
3
+import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
4
+import CallKit from './CallKit';
6 5
 
7
-ReducerRegistry.register('features/callkit', (state = {}, action) => {
8
-    switch (action.type) {
9
-    case _SET_CALLKIT_LISTENERS:
10
-        return {
11
-            ...state,
12
-            listeners: action.listeners
13
-        };
14
-    }
6
+CallKit && ReducerRegistry.register(
7
+    'features/callkit',
8
+    (state = {}, action) => {
9
+        switch (action.type) {
10
+        case _SET_CALLKIT_SUBSCRIPTIONS:
11
+            return assign(state, 'subscriptions', action.subscriptions);
12
+        }
15 13
 
16
-    return state;
17
-});
14
+        return state;
15
+    });

Loading…
Cancel
Save