[RN] add support for inviting participants during a call on mobile

* Button conditionally shown based on if the feature is enabled and available
* Hooks for launching the invite UI (delegates to the native layer)
* Hooks for using the search and dial out checks from the native layer (calls back into JS)
* Hooks for handling sending invites and passing any failures back to the native layer
* Android and iOS handling for those hooks

Author: Ryan Peck <rpeck@atlassian.com>
Author: Eric Brynsvold <ebrynsvold@atlassian.com>
This commit is contained in:
Ryan Peck
2018-04-25 18:58:06 +02:00
committed by Saúl Ibarra Corretgé
parent 4e36127dc7
commit f64c13d4b7
24 changed files with 1559 additions and 185 deletions
+8
View File
@@ -29,6 +29,8 @@
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; };
75635B0A20751D6D00F29C9F /* joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0820751D6D00F29C9F /* joined.wav */; };
75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 412BF89C206AA66F0053B9E5 /* InviteSearch.m */; };
412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = 412BF89E206AA82F0053B9E5 /* InviteSearch.h */; settings = {ATTRIBUTES = (Public, ); }; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
@@ -62,6 +64,8 @@
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = "<group>"; };
412BF89C206AA66F0053B9E5 /* InviteSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InviteSearch.m; sourceTree = "<group>"; };
412BF89E206AA82F0053B9E5 /* InviteSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InviteSearch.h; sourceTree = "<group>"; };
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
@@ -125,6 +129,8 @@
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
412BF89E206AA82F0053B9E5 /* InviteSearch.h */,
412BF89C206AA66F0053B9E5 /* InviteSearch.m */,
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
0B7C2CFC200F51D60060D076 /* LaunchOptions.m */,
@@ -180,6 +186,7 @@
buildActionMask = 2147483647;
files = (
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */,
412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */,
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
@@ -347,6 +354,7 @@
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@class InviteSearchController;
@protocol InviteSearchControllerDelegate
/**
* Called when an InviteSearchController has results for a query that was previously provided.
*/
- (void)inviteSearchController:(InviteSearchController * _Nonnull)controller
didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
/**
* Called when all invitations were sent successfully.
*/
- (void)inviteDidSucceedForSearchController:(InviteSearchController * _Nonnull)searchController;
/**
* Called when one or more invitations fails to send successfully.
*/
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> * _Nonnull)items
fromSearchController:(InviteSearchController * _Nonnull)searchController;
@end
@interface InviteSearchController: NSObject
@property (nonatomic, nullable, weak) id<InviteSearchControllerDelegate> delegate;
- (void)performQuery:(NSString * _Nonnull)query;
- (void)cancelSearch;
- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids;
@end
+215
View File
@@ -0,0 +1,215 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <React/RCTBridge.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTUtils.h>
#import "JitsiMeetView+Private.h"
#import "InviteSearch.h"
// The events emitted/supported by InviteSearch:
static NSString * const InviteSearchPerformQueryAction = @"performQueryAction";
static NSString * const InviteSearchPerformSubmitInviteAction = @"performSubmitInviteAction";
@interface InviteSearch : RCTEventEmitter
@end
@interface InviteSearchController ()
@property (nonatomic, readonly) NSString* _Nonnull identifier;
@property (nonatomic, strong) NSMutableDictionary* _Nonnull items;
@property (nonatomic, nullable, weak) InviteSearch* module;
- (instancetype)initWithSearchModule:(InviteSearch *)module;
- (void)didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
- (void)inviteDidSucceed;
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items;
@end
@implementation InviteSearch
static NSMutableDictionary* searchControllers;
RCT_EXTERN void RCTRegisterModule(Class);
+ (void)load {
RCTRegisterModule(self);
searchControllers = [[NSMutableDictionary alloc] init];
}
+ (NSString *)moduleName {
return @"InviteSearch";
}
- (NSArray<NSString *> *)supportedEvents {
return @[
InviteSearchPerformQueryAction,
InviteSearchPerformSubmitInviteAction
];
}
/**
* Calls the corresponding JitsiMeetView's delegate to request that the native
* invite search be presented.
*
* @param scope
*/
RCT_EXPORT_METHOD(launchNativeInvite:(NSString *)scope) {
// The JavaScript App needs to provide uniquely identifying information to
// the native module so that the latter may match the former to the native
// JitsiMeetView which hosts it.
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope];
if (!view) {
return;
}
id<JitsiMeetViewDelegate> delegate = view.delegate;
if (!delegate) {
return;
}
if ([delegate respondsToSelector:@selector(launchNativeInviteForSearchController:)]) {
InviteSearchController* searchController = [searchControllers objectForKey:scope];
if (!searchController) {
searchController = [self makeInviteSearchController];
}
[delegate launchNativeInviteForSearchController:searchController];
}
}
RCT_EXPORT_METHOD(inviteSucceeded:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController inviteDidSucceed];
[searchControllers removeObjectForKey:inviteScope];
}
RCT_EXPORT_METHOD(inviteFailedForItems:(NSArray<NSDictionary *> *)items inviteScope:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController inviteDidFailForItems:items];
}
RCT_EXPORT_METHOD(receivedResults:(NSArray *)results forQuery:(NSString *)query inviteScope:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController didReceiveResults:results forQuery:query];
}
- (InviteSearchController *)makeInviteSearchController {
InviteSearchController* searchController = [[InviteSearchController alloc] initWithSearchModule:self];
[searchControllers setObject:searchController forKey:searchController.identifier];
return searchController;
}
- (void)performQuery:(NSString * _Nonnull)query inviteScope:(NSString * _Nonnull)inviteScope {
[self sendEventWithName:InviteSearchPerformQueryAction body:@{ @"query": query, @"inviteScope": inviteScope }];
}
- (void)cancelSearchForInviteScope:(NSString * _Nonnull)inviteScope {
[searchControllers removeObjectForKey:inviteScope];
}
- (void)submitSelectedItems:(NSArray<NSDictionary *> * _Nonnull)items inviteScope:(NSString * _Nonnull)inviteScope {
[self sendEventWithName:InviteSearchPerformSubmitInviteAction body:@{ @"selectedItems": items, @"inviteScope": inviteScope }];
}
@end
@implementation InviteSearchController
- (instancetype)initWithSearchModule:(InviteSearch *)module {
self = [super init];
if (self) {
_identifier = [[NSUUID UUID] UUIDString];
self.items = [[NSMutableDictionary alloc] init];
self.module = module;
}
return self;
}
- (void)performQuery:(NSString *)query {
[self.module performQuery:query inviteScope:self.identifier];
}
- (void)cancelSearch {
[self.module cancelSearchForInviteScope:self.identifier];
}
- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids {
NSMutableArray* items = [[NSMutableArray alloc] init];
for (NSString* itemId in ids) {
id item = [self.items objectForKey:itemId];
if (item) {
[items addObject:item];
}
}
[self.module submitSelectedItems:items inviteScope:self.identifier];
}
- (void)didReceiveResults:(NSArray<NSDictionary *> *)results forQuery:(NSString *)query {
for (NSDictionary* item in results) {
NSString* itemId = item[@"id"];
NSString* itemType = item[@"type"];
if (itemId) {
[self.items setObject:item forKey:itemId];
} else if (itemType != nil && [itemType isEqualToString: @"phone"]) {
NSString* number = item[@"number"];
if (number) {
[self.items setObject:item forKey:number];
}
}
}
[self.delegate inviteSearchController:self didReceiveResults:results forQuery:query];
}
- (void)inviteDidSucceed {
[self.delegate inviteDidSucceedForSearchController:self];
}
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items {
if (!items) {
items = @[];
}
[self.delegate inviteDidFailForItems:items fromSearchController:self];
}
@end
+1
View File
@@ -16,3 +16,4 @@
#import <JitsiMeet/JitsiMeetView.h>
#import <JitsiMeet/JitsiMeetViewDelegate.h>
#import <JitsiMeet/InviteSearch.h>
+5 -1
View File
@@ -21,10 +21,14 @@
@interface JitsiMeetView : UIView
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (nonatomic) BOOL addPeopleEnabled;
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (nonatomic) BOOL dialOutEnabled;
@property (nonatomic) BOOL pictureInPictureEnabled;
@property (nonatomic) BOOL welcomePageEnabled;
+2
View File
@@ -268,6 +268,8 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
props[@"defaultURL"] = [self.defaultURL absoluteString];
}
props[@"addPeopleEnabled"] = @(self.addPeopleEnabled);
props[@"dialOutEnabled"] = @(self.dialOutEnabled);
props[@"externalAPIScope"] = externalAPIScope;
props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
+11
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
@class InviteSearchController;
@protocol JitsiMeetViewDelegate <NSObject>
@optional
@@ -55,6 +57,15 @@
*/
- (void)conferenceWillLeave:(NSDictionary *)data;
/**
* Called when the invite button in the conference is tapped.
*
* The search controller provided can be used to query user search within the
* conference.
*/
- (void)launchNativeInviteForSearchController:(InviteSearchController *)searchController;
/**
* Called when entering Picture-in-Picture is requested by the user. The app
* should now activate its Picture-in-Picture implementation (and resize the