diff --git a/ios/app/src/AppDelegate.m b/ios/app/src/AppDelegate.m index ee0b3c77c..b4d6a24ad 100644 --- a/ios/app/src/AppDelegate.m +++ b/ios/app/src/AppDelegate.m @@ -37,11 +37,18 @@ [Fabric with:@[[Crashlytics class]]]; } - [JitsiMeet sharedInstance].conferenceActivityType = JitsiMeetConferenceActivityType; - [JitsiMeet sharedInstance].customUrlScheme = @"org.jitsi.meet"; - [JitsiMeet sharedInstance].universalLinkDomains = @[@"meet.jit.si", @"beta.meet.jit.si"]; + JitsiMeet *jitsiMeet = [JitsiMeet sharedInstance]; - [[JitsiMeet sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; + jitsiMeet.conferenceActivityType = JitsiMeetConferenceActivityType; + jitsiMeet.customUrlScheme = @"org.jitsi.meet"; + jitsiMeet.universalLinkDomains = @[@"meet.jit.si", @"beta.meet.jit.si"]; + + jitsiMeet.defaultConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) { + builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"]; + builder.welcomePageEnabled = YES; + }]; + + [jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions]; return YES; } diff --git a/ios/app/src/ViewController.m b/ios/app/src/ViewController.m index 6fbd9dce9..9ccb38c5c 100644 --- a/ios/app/src/ViewController.m +++ b/ios/app/src/ViewController.m @@ -21,6 +21,8 @@ @import MobileCoreServices; @import Intents; // Needed for NSUserActivity suggestedInvocationPhrase +@import JitsiMeet; + #import "Types.h" #import "ViewController.h" @@ -33,13 +35,7 @@ JitsiMeetView *view = (JitsiMeetView *) self.view; view.delegate = self; - // As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do want - // the Welcome page to be enabled. It defaults to disabled in the SDK at the - // time of this writing but it is clearer to be explicit about what we want - // anyway. - view.welcomePageEnabled = YES; - - [view join:[[JitsiMeet sharedInstance] getInitialURL]]; + [view join:[[JitsiMeet sharedInstance] getInitialConferenceOptions]]; } // JitsiMeetViewDelegate diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 10a8e5060..6283bc917 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -42,7 +42,8 @@ C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitListener.swift */; }; C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; }; C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; }; - C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; }; + DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */; }; DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */; }; DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535321FB1BF800011A3A /* JitsiMeet.m */; }; DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535521FB2E8300011A3A /* ReactUtils.m */; }; @@ -92,6 +93,9 @@ C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = ""; }; C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = ""; }; C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = ""; }; + DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetConferenceOptions.h; sourceTree = ""; }; + DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetConferenceOptions.m; sourceTree = ""; }; + DEAD3228220C734300E93636 /* JitsiMeetConferenceOptions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetConferenceOptions+Private.h"; sourceTree = ""; }; DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocaleDetector.m; sourceTree = ""; }; DEFE535321FB1BF800011A3A /* JitsiMeet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeet.m; sourceTree = ""; }; DEFE535521FB2E8300011A3A /* ReactUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactUtils.m; sourceTree = ""; }; @@ -164,6 +168,9 @@ 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */, DEFE535821FB311F00011A3A /* JitsiMeet+Private.h */, DEFE535321FB1BF800011A3A /* JitsiMeet.m */, + DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */, + DEAD3228220C734300E93636 /* JitsiMeetConferenceOptions+Private.h */, + DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */, 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */, 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */, C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */, @@ -247,11 +254,11 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */, 0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */, 0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */, 0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */, 0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */, + DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -451,6 +458,7 @@ files = ( 0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */, 0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */, + DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */, 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */, 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */, 0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */, diff --git a/ios/sdk/src/JitsiMeet+Private.h b/ios/sdk/src/JitsiMeet+Private.h index 31dca550e..fbb58fe9e 100644 --- a/ios/sdk/src/JitsiMeet+Private.h +++ b/ios/sdk/src/JitsiMeet+Private.h @@ -18,6 +18,7 @@ @interface JitsiMeet () +- (NSDictionary *)getDefaultProps; - (RCTBridge *)getReactBridge; @end diff --git a/ios/sdk/src/JitsiMeet.h b/ios/sdk/src/JitsiMeet.h index 447d392d6..58eafaadf 100644 --- a/ios/sdk/src/JitsiMeet.h +++ b/ios/sdk/src/JitsiMeet.h @@ -17,6 +17,7 @@ #import #import +#import @interface JitsiMeet : NSObject @@ -25,6 +26,8 @@ @property (copy, nonatomic, nullable) NSString *customUrlScheme; @property (copy, nonatomic, nullable) NSArray *universalLinkDomains; +@property (nonatomic, nullable) JitsiMeetConferenceOptions *defaultConferenceOptions; + #pragma mak - This class is a singleton + (instancetype)sharedInstance; @@ -44,6 +47,6 @@ #pragma mark - Utility methods -- (NSDictionary *)getInitialURL; +- (JitsiMeetConferenceOptions *)getInitialConferenceOptions; @end diff --git a/ios/sdk/src/JitsiMeet.m b/ios/sdk/src/JitsiMeet.m index 034b00c2b..ce2a70506 100644 --- a/ios/sdk/src/JitsiMeet.m +++ b/ios/sdk/src/JitsiMeet.m @@ -18,6 +18,7 @@ #import "Dropbox.h" #import "JitsiMeet+Private.h" +#import "JitsiMeetConferenceOptions+Private.h" #import "JitsiMeetView+Private.h" #import "RCTBridgeWrapper.h" #import "ReactUtils.h" @@ -71,9 +72,9 @@ continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler { - id url = [self urlFromUserActivity:userActivity]; + JitsiMeetConferenceOptions *options = [self optionsFromUserActivity:userActivity]; - return url && [JitsiMeetView loadURLInViews:url]; + return options && [JitsiMeetView setPropsInViews:[options asProps]]; } - (BOOL)application:(UIApplication *)app @@ -95,36 +96,44 @@ return NO; } - return [JitsiMeetView loadURLInViews:@{ @"url" : url.absoluteString }]; + JitsiMeetConferenceOptions *conferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) { + builder.room = [url absoluteString]; + }]; + + return [JitsiMeetView setPropsInViews:[conferenceOptions asProps]]; } #pragma mark - Utility methods -- (NSDictionary *)getInitialURL { +- (JitsiMeetConferenceOptions *)getInitialConferenceOptions { if (_launchOptions[UIApplicationLaunchOptionsURLKey]) { NSURL *url = _launchOptions[UIApplicationLaunchOptionsURLKey]; - return @{ @"url" : url.absoluteString }; + return [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) { + builder.room = [url absoluteString]; + }]; } else { NSDictionary *userActivityDictionary = _launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; NSUserActivity *userActivity = [userActivityDictionary objectForKey:@"UIApplicationLaunchOptionsUserActivityKey"]; if (userActivity != nil) { - return [self urlFromUserActivity:userActivity]; + return [self optionsFromUserActivity:userActivity]; } } return nil; } -- (NSDictionary *)urlFromUserActivity:(NSUserActivity *)userActivity { +- (JitsiMeetConferenceOptions *)optionsFromUserActivity:(NSUserActivity *)userActivity { NSString *activityType = userActivity.activityType; if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { // App was started by opening a URL in the browser NSURL *url = userActivity.webpageURL; if ([_universalLinkDomains containsObject:url.host]) { - return @{ @"url" : url.absoluteString }; + return [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) { + builder.room = [url absoluteString]; + }]; } } else if ([activityType isEqualToString:@"INStartAudioCallIntent"] || [activityType isEqualToString:@"INStartVideoCallIntent"]) { @@ -132,27 +141,29 @@ INIntent *intent = userActivity.interaction.intent; NSArray *contacts; NSString *url; - BOOL startAudioOnly = NO; + BOOL audioOnly = NO; if ([intent isKindOfClass:[INStartAudioCallIntent class]]) { contacts = ((INStartAudioCallIntent *) intent).contacts; - startAudioOnly = YES; + audioOnly = YES; } else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) { contacts = ((INStartVideoCallIntent *) intent).contacts; } if (contacts && (url = contacts.firstObject.personHandle.value)) { - return @{ - @"config": @{@"startAudioOnly":@(startAudioOnly)}, - @"url": url - }; + return [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) { + builder.audioOnly = audioOnly; + builder.room = url; + }]; } } else if (self.conferenceActivityType && [activityType isEqualToString:self.conferenceActivityType]) { // App was started by continuing a registered NSUserActivity (SiriKit, Handoff, ...) NSString *url; if ((url = userActivity.userInfo[@"url"])) { - return @{ @"url" : url }; + return [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) { + builder.room = url; + }]; } } @@ -171,6 +182,10 @@ #pragma mark - Private API methods +- (NSDictionary *)getDefaultProps { + return _defaultConferenceOptions == nil ? @{} : [_defaultConferenceOptions asProps]; +} + - (RCTBridge *)getReactBridge { return _bridgeWrapper.bridge; } diff --git a/ios/sdk/src/JitsiMeetConferenceOptions+Private.h b/ios/sdk/src/JitsiMeetConferenceOptions+Private.h new file mode 100644 index 000000000..905569ae3 --- /dev/null +++ b/ios/sdk/src/JitsiMeetConferenceOptions+Private.h @@ -0,0 +1,23 @@ +/* + * Copyright @ 2019-present 8x8, Inc. + * + * 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 "JitsiMeetConferenceOptions.h" + +@interface JitsiMeetConferenceOptions () + +- (NSMutableDictionary *)asProps; + +@end diff --git a/ios/sdk/src/JitsiMeetConferenceOptions.h b/ios/sdk/src/JitsiMeetConferenceOptions.h new file mode 100644 index 000000000..bfd4e3466 --- /dev/null +++ b/ios/sdk/src/JitsiMeetConferenceOptions.h @@ -0,0 +1,52 @@ +/* + * Copyright @ 2019-present 8x8, Inc. + * + * 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 + +@interface JitsiMeetConferenceOptionsBuilder : NSObject + +@property (nonatomic, copy, nullable) NSURL *serverURL; +@property (nonatomic, copy, nullable) NSString *room; +@property (nonatomic, copy, nullable) NSString *token; + +@property (nonatomic, copy, nullable) NSDictionary *colorScheme; + +@property (nonatomic) BOOL audioOnly; +@property (nonatomic) BOOL audioMuted; +@property (nonatomic) BOOL videoMuted; + +@property (nonatomic) BOOL welcomePageEnabled; + +@end + +@interface JitsiMeetConferenceOptions : NSObject + +@property (nonatomic, copy, nullable, readonly) NSURL *serverURL; +@property (nonatomic, copy, nullable, readonly) NSString *room; +@property (nonatomic, copy, nullable, readonly) NSString *token; + +@property (nonatomic, copy, nullable) NSDictionary *colorScheme; + +@property (nonatomic, readonly) BOOL audioOnly; +@property (nonatomic, readonly) BOOL audioMuted; +@property (nonatomic, readonly) BOOL videoMuted; + +@property (nonatomic, readonly) BOOL welcomePageEnabled; + ++ (instancetype)fromBuilder:(void (^)(JitsiMeetConferenceOptionsBuilder *))initBlock; +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/ios/sdk/src/JitsiMeetConferenceOptions.m b/ios/sdk/src/JitsiMeetConferenceOptions.m new file mode 100644 index 000000000..ba07f6891 --- /dev/null +++ b/ios/sdk/src/JitsiMeetConferenceOptions.m @@ -0,0 +1,212 @@ +/* + * Copyright @ 2019-present 8x8, Inc. + * + * 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 + +#import "JitsiMeetConferenceOptions+Private.h" + +@implementation JitsiMeetConferenceOptionsBuilder { + NSNumber *_audioOnly; + NSNumber *_audioMuted; + NSNumber *_videoMuted; + NSNumber *_welcomePageEnabled; +} + +@dynamic audioOnly; +@dynamic audioMuted; +@dynamic videoMuted; +@dynamic welcomePageEnabled; + +- (instancetype)init { + if (self = [super init]) { + _serverURL = nil; + _room = nil; + _token = nil; + + _colorScheme = nil; + + _audioOnly = nil; + _audioMuted = nil; + _videoMuted = nil; + + _welcomePageEnabled = nil; + } + + return self; +} + +#pragma mark - Dynamic properties + +- (void)setAudioOnly:(BOOL)audioOnly { + _audioOnly = [NSNumber numberWithBool:audioOnly]; +} + +- (BOOL)audioOnly { + return _audioOnly && [_audioOnly boolValue]; +} + +- (void)setAudioMuted:(BOOL)audioMuted { + _audioMuted = [NSNumber numberWithBool:audioMuted]; +} + +- (BOOL)audioMuted { + return _audioMuted && [_audioMuted boolValue]; +} + +- (void)setVideoMuted:(BOOL)videoMuted { + _videoMuted = [NSNumber numberWithBool:videoMuted]; +} + +- (BOOL)videoMuted { + return _videoMuted && [_videoMuted boolValue]; +} + +- (void)setWelcomePageEnabled:(BOOL)welcomePageEnabled { + _welcomePageEnabled = [NSNumber numberWithBool:welcomePageEnabled]; +} + +- (BOOL)welcomePageEnabled { + return _welcomePageEnabled && [_welcomePageEnabled boolValue]; +} + +#pragma mark - Private API + +- (NSNumber *)getAudioOnly { + return _audioOnly; +} + +- (NSNumber *)getAudioMuted { + return _audioMuted; +} + +- (NSNumber *)getVideoMuted { + return _videoMuted; +} + +- (NSNumber *)getWelcomePageEnabled { + return _welcomePageEnabled; +} + +@end + +@implementation JitsiMeetConferenceOptions { + NSNumber *_audioOnly; + NSNumber *_audioMuted; + NSNumber *_videoMuted; + NSNumber *_welcomePageEnabled; +} + +@dynamic audioOnly; +@dynamic audioMuted; +@dynamic videoMuted; +@dynamic welcomePageEnabled; + +#pragma mark - Dynamic properties + +- (BOOL)audioOnly { + return _audioOnly && [_audioOnly boolValue]; +} + +- (BOOL)audioMuted { + return _audioMuted && [_audioMuted boolValue]; +} + +- (BOOL)videoMuted { + return _videoMuted && [_videoMuted boolValue]; +} + +- (BOOL)welcomePageEnabled { + return _welcomePageEnabled && [_welcomePageEnabled boolValue]; +} + +#pragma mark - Internal initializer + +- (instancetype)initWithBuilder:(JitsiMeetConferenceOptionsBuilder *)builder { + if (self = [super init]) { + _serverURL = builder.serverURL; + _room = builder.room; + _token = builder.token; + + _colorScheme = builder.colorScheme; + + _audioOnly = [builder getAudioOnly]; + _audioMuted = [builder getAudioMuted]; + _videoMuted = [builder getVideoMuted]; + + _welcomePageEnabled = [builder getWelcomePageEnabled]; + } + + return self; +} + +#pragma mark - API + ++ (instancetype)fromBuilder:(void (^)(JitsiMeetConferenceOptionsBuilder *))initBlock { + JitsiMeetConferenceOptionsBuilder *builder = [[JitsiMeetConferenceOptionsBuilder alloc] init]; + initBlock(builder); + return [[JitsiMeetConferenceOptions alloc] initWithBuilder:builder]; +} + +#pragma mark - Private API + +- (NSDictionary *)asProps { + NSMutableDictionary *props = [[NSMutableDictionary alloc] init]; + + if (_colorScheme != nil) { + props[@"colorScheme"] = self.colorScheme; + } + + if (_welcomePageEnabled != nil) { + props[@"welcomePageEnabled"] = @(self.welcomePageEnabled); + } + + NSMutableDictionary *config = [[NSMutableDictionary alloc] init]; + if (_audioOnly != nil) { + config[@"startAudioOnly"] = @(self.audioOnly); + } + if (_audioMuted != nil) { + config[@"startWithAudioMuted"] = @(self.audioMuted); + } + if (_videoMuted != nil) { + config[@"startWithVideoMuted"] = @(self.videoMuted); + } + + NSMutableDictionary *urlProps = [[NSMutableDictionary alloc] init]; + + // The room is fully qualified. + if (_room != nil && [_room containsString:@"://"]) { + urlProps[@"url"] = _room; + } else { + if (_serverURL != nil) { + urlProps[@"serverURL"] = [_serverURL absoluteString]; + } + + if (_room != nil) { + urlProps[@"room"] = _room; + } + } + + if (_token != nil) { + urlProps[@"jwt"] = _token; + } + + urlProps[@"config"] = config; + props[@"url"] = urlProps; + + return props; +} + +@end diff --git a/ios/sdk/src/JitsiMeetView+Private.h b/ios/sdk/src/JitsiMeetView+Private.h index 89f6cc0e2..bd468a325 100644 --- a/ios/sdk/src/JitsiMeetView+Private.h +++ b/ios/sdk/src/JitsiMeetView+Private.h @@ -20,6 +20,6 @@ @interface JitsiMeetView () + (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope; -+ (BOOL)loadURLInViews:(NSDictionary *)urlObject; ++ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps; @end diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h index 0c7cc0ec1..261dfd7d2 100644 --- a/ios/sdk/src/JitsiMeetView.h +++ b/ios/sdk/src/JitsiMeetView.h @@ -18,23 +18,14 @@ #import #import +#import "JitsiMeetConferenceOptions.h" #import "JitsiMeetViewDelegate.h" @interface JitsiMeetView : UIView -@property (class, copy, nonatomic, nullable) NSString *conferenceActivityType; - -@property (nonatomic) NSDictionary *colorScheme; - -@property (copy, nonatomic, nullable) NSURL *defaultURL; - @property (nonatomic, nullable, weak) id delegate; -@property (nonatomic) BOOL pictureInPictureEnabled; - -@property (nonatomic) BOOL welcomePageEnabled; - -- (void)join:(NSDictionary * _Nullable)url; +- (void)join:(JitsiMeetConferenceOptions *)options; - (void)leave; @end diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index 2d8866689..8ae4a7ffe 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -20,7 +20,9 @@ #import #import "JitsiMeet+Private.h" +#import "JitsiMeetConferenceOptions+Private.h" #import "JitsiMeetView+Private.h" +#import "ReactUtils.h" @implementation JitsiMeetView { @@ -32,12 +34,8 @@ NSString *externalAPIScope; RCTRootView *rootView; - - NSNumber *_pictureInPictureEnabled; } -@dynamic pictureInPictureEnabled; - /** * The `JitsiMeetView`s associated with their `ExternalAPI` scopes (i.e. unique * identifiers within the process). @@ -96,80 +94,46 @@ static void initializeViewsMap() { // parts of the application and causes less perceived visual flicker than // the default background color. self.backgroundColor - = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1]; + = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1]; } #pragma mark API -- (void)join:(NSDictionary *_Nullable)url { - [self loadURL:url]; +- (void)join:(JitsiMeetConferenceOptions *)options { + [self setProps:options == nil ? @{} : [options asProps]]; } - (void)leave { - [self loadURL:nil]; -} - -#pragma pictureInPictureEnabled getter / setter - -- (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled { - _pictureInPictureEnabled - = [NSNumber numberWithBool:pictureInPictureEnabled]; -} - -- (BOOL) pictureInPictureEnabled { - if (_pictureInPictureEnabled) { - return [_pictureInPictureEnabled boolValue]; - } - - // The SDK/JitsiMeetView client/consumer did not explicitly enable/disable - // Picture-in-Picture. However, we may automatically deduce their - // intentions: we need the support of the client in order to implement - // Picture-in-Picture on iOS (in contrast to Android) so if the client - // appears to have provided the support then we can assume that they did it - // with the intention to have Picture-in-Picture enabled. - return self.delegate - && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]; + [self setProps:@{}]; } #pragma mark Private methods /** - * Loads a specific URL which may identify a conference to join. The URL is - * specified in the form of an `NSDictionary` of properties which (1) - * internally are sufficient to construct a URL `NSString` while (2) abstracting - * the specifics of constructing the URL away from API clients/consumers. If the - * specified URL is `nil` and the Welcome page is enabled, the Welcome page is - * displayed instead. + * Passes the given props to the React Native application. The props which we pass + * are a combination of 3 different sources: * - * @param urlObject The URL to load which may identify a conference to join. + * - JitsiMeet.defaultConferenceOptions + * - This function's parameters + * - Some extras which are added by this function */ -- (void)loadURL:(NSDictionary *_Nullable)urlObject { - NSMutableDictionary *props = [[NSMutableDictionary alloc] init]; +- (void)setProps:(NSDictionary *_Nonnull)newProps { + NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps); - if (self.defaultURL) { - props[@"defaultURL"] = [self.defaultURL absoluteString]; - } - - props[@"colorScheme"] = self.colorScheme; props[@"externalAPIScope"] = externalAPIScope; - props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled); - props[@"welcomePageEnabled"] = @(self.welcomePageEnabled); - // XXX If urlObject is nil, then it must appear as undefined in the - // JavaScript source code so that we check the launchOptions there. - if (urlObject) { - props[@"url"] = urlObject; - } + // TODO: put this in some 'flags' field + props[@"pictureInPictureEnabled"] + = @(self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]); - // XXX The method loadURL: is supposed to be imperative i.e. a second + // This method is supposed to be imperative i.e. a second // invocation with one and the same URL is expected to join the respective // conference again if the first invocation was followed by leaving the // conference. However, React and, respectively, // appProperties/initialProperties are declarative expressions i.e. one and // the same URL will not trigger an automatic re-render in the JavaScript // source code. The workaround implemented bellow introduces imperativeness - // in React Component props by defining a unique value per loadURL: - // invocation. + // in React Component props by defining a unique value per invocation. props[@"timestamp"] = @(mach_absolute_time()); if (rootView) { @@ -192,7 +156,7 @@ static void initializeViewsMap() { } } -+ (BOOL)loadURLInViews:(NSDictionary *)urlObject { ++ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps { BOOL handled = NO; if (views) { @@ -201,7 +165,7 @@ static void initializeViewsMap() { = [self viewForExternalAPIScope:externalAPIScope]; if (view) { - [view loadURL:urlObject]; + [view setProps:newProps]; handled = YES; } } diff --git a/ios/sdk/src/ReactUtils.h b/ios/sdk/src/ReactUtils.h index 163f1ade7..f80028f82 100644 --- a/ios/sdk/src/ReactUtils.h +++ b/ios/sdk/src/ReactUtils.h @@ -14,9 +14,10 @@ * limitations under the License. */ -#ifndef ReactUtils_h -#define ReactUtils_h +#ifndef JM_REACTUTILS_H +#define JM_REACTUTILS_H +NSMutableDictionary* mergeProps(NSDictionary *a, NSDictionary *b); void registerReactFatalErrorHandler(void); -#endif /* ReactUtils_h */ +#endif /* JM_REACTUTILS_H */ diff --git a/ios/sdk/src/ReactUtils.m b/ios/sdk/src/ReactUtils.m index 52e3e3216..da1daa250 100644 --- a/ios/sdk/src/ReactUtils.m +++ b/ios/sdk/src/ReactUtils.m @@ -18,6 +18,46 @@ #import "ReactUtils.h" +#pragma mark - Utility functions + +/** + * Merges 2 sets of props into a single one. + */ +NSMutableDictionary* mergeProps(NSDictionary *a, NSDictionary *b) { + if (a == nil) { + return [NSMutableDictionary dictionaryWithDictionary:b == nil ? @{} : b]; + } + + if (b == nil) { + return [NSMutableDictionary dictionaryWithDictionary:a]; + } + + // Both have values, let's merge them, the strategy is to take the value from a first, + // then override it with the one from b. If the value is a dictionary, merge them + // recursively. Same goes for arrays. + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:a]; + + for (NSString *key in b) { + id value = b[key]; + id aValue = result[key]; + + if (aValue == nil) { + result[key] = value; + continue; + } + + if ([value isKindOfClass:NSArray.class]) { + result[key] = [aValue arrayByAddingObjectsFromArray:value]; + } else if ([value isKindOfClass:NSDictionary.class]) { + result[key] = mergeProps(aValue, value); + } else { + result[key] = value; + } + } + + return result; +} + /** * A `RCTFatalHandler` implementation which swallows JavaScript errors. In the * Release configuration, React Native will (intentionally) raise an unhandled