diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java index ccac970f6..8d4e1e057 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java @@ -19,10 +19,10 @@ package org.jitsi.meet.sdk; import android.content.Intent; import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.provider.Settings; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; import java.net.URL; diff --git a/ios/sdk/src/ExternalAPI.m b/ios/sdk/src/ExternalAPI.m index 008564c6b..dbf00c70a 100644 --- a/ios/sdk/src/ExternalAPI.m +++ b/ios/sdk/src/ExternalAPI.m @@ -16,9 +16,10 @@ #import "RCTBridgeModule.h" -#import "JitsiMeetView.h" +#import "JitsiMeetView+Private.h" @interface ExternalAPI : NSObject + @end @implementation ExternalAPI @@ -31,26 +32,40 @@ RCT_EXPORT_MODULE(); * - name: name of the event. * - data: dictionary (JSON object in JS) with data associated with the event. */ -RCT_EXPORT_METHOD(sendEvent:(NSString*)name data:(NSDictionary *) data) { - JitsiMeetView *view = [JitsiMeetView getInstance]; - id delegate = view != nil ? view.delegate : nil; +RCT_EXPORT_METHOD(sendEvent:(NSString *)name + data:(NSDictionary *)data + scope:(NSString *)scope) { + // The JavaScript App needs to provide uniquely identifying information to + // the native ExternalAPI module so that the latter may match the former + // to the native JitsiMeetView which hosts it. + JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope]; - if (delegate == nil) { + if (!view) { + return; + } + + id delegate = view.delegate; + + if (!delegate) { return; } if ([name isEqualToString:@"CONFERENCE_FAILED"] && [delegate respondsToSelector:@selector(conferenceFailed:)]) { [delegate conferenceFailed:data]; + } else if ([name isEqualToString:@"CONFERENCE_JOINED"] && [delegate respondsToSelector:@selector(conferenceJoined:)]) { [delegate conferenceJoined:data]; + } else if ([name isEqualToString:@"CONFERENCE_LEFT"] && [delegate respondsToSelector:@selector(conferenceLeft:)]) { [delegate conferenceLeft:data]; + } else if ([name isEqualToString:@"CONFERENCE_WILL_JOIN"] && [delegate respondsToSelector:@selector(conferenceWillJoin:)]) { [delegate conferenceWillJoin:data]; + } else if ([name isEqualToString:@"CONFERENCE_WILL_LEAVE"] && [delegate respondsToSelector:@selector(conferenceWillLeave:)]) { [delegate conferenceWillLeave:data]; diff --git a/ios/sdk/src/JitsiMeetView+Private.h b/ios/sdk/src/JitsiMeetView+Private.h new file mode 100644 index 000000000..cc167c8a0 --- /dev/null +++ b/ios/sdk/src/JitsiMeetView+Private.h @@ -0,0 +1,23 @@ +/* + * Copyright @ 2017-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 "JitsiMeetView.h" + +@interface JitsiMeetView () + ++ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope; + +@end diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h index ba6e642b5..cedef022f 100644 --- a/ios/sdk/src/JitsiMeetView.h +++ b/ios/sdk/src/JitsiMeetView.h @@ -34,8 +34,6 @@ sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; -+ (instancetype)getInstance; - - (void)loadURL:(nullable NSURL *)url; @end diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index 9b9cad54c..eed5dd8bf 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -20,7 +20,7 @@ #import #import -#import "JitsiMeetView.h" +#import "JitsiMeetView+Private.h" #import "RCTBridgeWrapper.h" /** @@ -46,146 +46,12 @@ RCTFatalHandler _RCTFatal = ^(NSError *error) { } }; -@interface JitsiMeetView() { - RCTRootView *rootView; -} - -@end - -@implementation JitsiMeetView - -static RCTBridgeWrapper *bridgeWrapper; -static JitsiMeetView *instance; - -#pragma mark Linking delegate helpers -// https://facebook.github.io/react-native/docs/linking.html - -+ (BOOL)application:(UIApplication *)application - continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler -{ - return [RCTLinkingManager application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; -} - -+ (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication - annotation:(id)annotation { - return [RCTLinkingManager application:application - openURL:url - sourceApplication:sourceApplication - annotation:annotation]; -} - -#pragma mark initializers - -- (instancetype)initWithCoder:(NSCoder *)coder { - self = [super initWithCoder:coder]; - if (self) { - [self initialize]; - } - - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self initialize]; - } - - return self; -} - -#pragma mark API - -/* - * Loads the given URL and joins the specified conference. If the specified URL - * is null, the welcome page is shown. - */ -- (void)loadURL:(NSURL *)url { - NSMutableDictionary *props = [[NSMutableDictionary alloc] init]; - - // url - if (url) { - [props setObject:url.absoluteString forKey:@"url"]; - } - // welcomePageEnabled - [props setObject:@(self.welcomePageEnabled) forKey:@"welcomePageEnabled"]; - - if (rootView == nil) { - rootView - = [[RCTRootView alloc] initWithBridge:bridgeWrapper.bridge - moduleName:@"App" - initialProperties:props]; - rootView.backgroundColor = self.backgroundColor; - - // Add React's root view as a subview which completely covers this one. - [rootView setFrame:[self bounds]]; - [self addSubview:rootView]; - } else { - // Update props with the new URL. - rootView.appProperties = props; - } -} - -#pragma mark private methods - -+ (instancetype)getInstance { - return instance; -} - -/* - * Internal initialization: - * - * - sets the backgroudn color - * - creates the React bridge - * - loads the necessary custom fonts - * - registers a custom fatal error error handler for React - */ -- (void)initialize { - static dispatch_once_t onceToken; - - /* - * TODO: Only allow a single instance for now. All React Native modules are - * kinda singletons so global state would be broken since we have a single - * bridge. Once we have that sorted out multiple instances of JitsiMeetView - * will be allowed. - */ - if (instance != nil) { - @throw [NSException - exceptionWithName:@"RuntimeError" - reason:@"Only a single instance is currently allowed" - userInfo:nil]; - } - instance = self; - - dispatch_once(&onceToken, ^{ - // Set a background color which is in accord with the JavaScript and - // Android 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]; - - // Initialize the React bridge. - bridgeWrapper = [[RCTBridgeWrapper alloc] init]; - - // Dynamically load custom bundled fonts. - [self loadCustomFonts]; - - // Register a fatal error handler for React. - [self registerFatalErrorHandler]; - }); -} - -/* +/** * Helper function to dynamically load custom fonts. The UIAppFonts key in the * plist file doesn't work for frameworks, so fonts have to be manually loaded. */ -- (void)loadCustomFonts { - NSBundle *bundle = [NSBundle bundleForClass:self.class]; +void loadCustomFonts(Class clazz) { + NSBundle *bundle = [NSBundle bundleForClass:clazz]; NSArray *fonts = [bundle objectForInfoDictionaryKey:@"JitsiMeetFonts"]; for (NSString *item in fonts) { @@ -209,12 +75,12 @@ static JitsiMeetView *instance; } } -/* +/** * Helper function to register a fatal error handler for React. Our handler * won't kill the process, it will swallow JS errors and print stack traces * instead. */ -- (void)registerFatalErrorHandler { +void registerFatalErrorHandler() { #if !DEBUG // In the Release configuration, React Native will (intentionally) raise // an unhandled NSException for an unhandled JavaScript error. This will @@ -226,4 +92,144 @@ static JitsiMeetView *instance; #endif } +@interface JitsiMeetView() { + /** + * The unique identifier of this {@code JitsiMeetView} within the process + * for the purposes of {@link ExternalAPI}. The name scope was inspired by + * postis which we use on Web for the similar purposes of the iframe-based + * external API. + */ + NSString *externalAPIScope; + + RCTRootView *rootView; +} + +@end + +@implementation JitsiMeetView + +static RCTBridgeWrapper *bridgeWrapper; + +/** + * The {@code JitsiMeetView}s associated with their {@code ExternalAPI} scopes + * (i.e. unique identifiers within the process). + */ +static NSMapTable *views; + +#pragma mark Linking delegate helpers +// https://facebook.github.io/react-native/docs/linking.html + ++ (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler +{ + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + ++ (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + return [RCTLinkingManager application:application + openURL:url + sourceApplication:sourceApplication + annotation:annotation]; +} + +#pragma mark Initializers + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + [self initWithXXX]; + } + + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self initWithXXX]; + } + + return self; +} + +#pragma mark API + +/** + * Loads the given URL and joins the specified conference. If the specified URL + * is null, the welcome page is shown. + */ +- (void)loadURL:(NSURL *)url { + NSMutableDictionary *props = [[NSMutableDictionary alloc] init]; + + // externalAPIScope + [props setObject:externalAPIScope forKey:@"externalAPIScope"]; + // url + if (url) { + [props setObject:url.absoluteString forKey:@"url"]; + } + // welcomePageEnabled + [props setObject:@(self.welcomePageEnabled) forKey:@"welcomePageEnabled"]; + + if (rootView == nil) { + rootView + = [[RCTRootView alloc] initWithBridge:bridgeWrapper.bridge + moduleName:@"App" + initialProperties:props]; + rootView.backgroundColor = self.backgroundColor; + + // Add React's root view as a subview which completely covers this one. + [rootView setFrame:[self bounds]]; + [self addSubview:rootView]; + } else { + // Update props with the new URL. + rootView.appProperties = props; + } +} + +#pragma mark Private methods + ++ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope { + return [views objectForKey:externalAPIScope]; +} + +/** + * Internal initialization: + * + * - sets the backgroudn color + * - creates the React bridge + * - loads the necessary custom fonts + * - registers a custom fatal error error handler for React + */ +- (void)initWithXXX { + static dispatch_once_t dispatchOncePredicate; + + dispatch_once(&dispatchOncePredicate, ^{ + // Initialize the static state of JitsiMeetView. + bridgeWrapper = [[RCTBridgeWrapper alloc] init]; + views = [NSMapTable strongToWeakObjectsMapTable]; + + // Dynamically load custom bundled fonts. + loadCustomFonts(self.class); + + // Register a fatal error handler for React. + registerFatalErrorHandler(); + }); + + // Hook this JitsiMeetView into ExternalAPI. + externalAPIScope = [NSUUID UUID].UUIDString; + [views setObject:self forKey:externalAPIScope]; + + // Set a background color which is in accord with the JavaScript and + // Android 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]; +} + @end diff --git a/ios/sdk/src/JitsiMeetViewDelegate.h b/ios/sdk/src/JitsiMeetViewDelegate.h index c57a79dd6..66748e57b 100644 --- a/ios/sdk/src/JitsiMeetViewDelegate.h +++ b/ios/sdk/src/JitsiMeetViewDelegate.h @@ -18,41 +18,45 @@ @optional -/* - * Called when a joining a conference was unsuccessful or when there was an error - * while in a conference. +/** + * Called when a joining a conference was unsuccessful or when there was an + * error while in a conference. * - * The `data` dictionary contains an "error" key describing the error and a "url" - * key with the conference URL. + * The {@code data} dictionary contains an "error" key describing the error and + * a {@code url} key with the conference URL. */ -- (void) conferenceFailed:(NSDictionary *) data; +- (void) conferenceFailed:(NSDictionary *)data; -/* +/** * Called when a conference was joined. * - * The `data` dictionary contains a "url" key with the conference URL. + * The {@code data} dictionary contains a {@code url} key with the conference + * URL. */ -- (void) conferenceJoined:(NSDictionary *) data; +- (void) conferenceJoined:(NSDictionary *)data; -/* +/** * Called when a conference was left. * - * The `data` dictionary contains a "url" key with the conference URL. + * The {@code data} dictionary contains a {@code url} key with the conference + * URL. */ -- (void) conferenceLeft:(NSDictionary *) data; +- (void) conferenceLeft:(NSDictionary *)data; -/* +/** * Called before a conference is joined. * - * The `data` dictionary contains a "url" key with the conference URL. + * The {@code data} dictionary contains a {@code url} key with the conference + * URL. */ -- (void) conferenceWillJoin:(NSDictionary *) data; +- (void) conferenceWillJoin:(NSDictionary *)data; -/* +/** * Called before a conference is left. * - * The `data` dictionary contains a "url" key with the conference URL. + * The {@code data} dictionary contains a {@code url} key with the conference + * URL. */ -- (void) conferenceWillLeave:(NSDictionary *) data; +- (void) conferenceWillLeave:(NSDictionary *)data; @end diff --git a/ios/sdk/src/RCTBridgeWrapper.m b/ios/sdk/src/RCTBridgeWrapper.m index 3177a2baa..052bb8110 100644 --- a/ios/sdk/src/RCTBridgeWrapper.m +++ b/ios/sdk/src/RCTBridgeWrapper.m @@ -16,7 +16,7 @@ #include "RCTBridgeWrapper.h" -/* +/** * Wrapper around RCTBridge which also implements the RCTBridgeDelegate methods, * allowing us to specify where the bundles are loaded from. */ @@ -56,9 +56,9 @@ static NSURL *serverRootWithHost(NSString *host) { - (NSString *)guessPackagerHost { static NSString *ipGuess; - static dispatch_once_t onceToken; + static dispatch_once_t dispatchOncePredicate; - dispatch_once(&onceToken, ^{ + dispatch_once(&dispatchOncePredicate, ^{ NSString *ipPath = [[NSBundle bundleForClass:self.class] pathForResource:@"ip" ofType:@"txt"]; diff --git a/react/features/mobile/external-api/middleware.js b/react/features/mobile/external-api/middleware.js index 8451c0f94..2d1389353 100644 --- a/react/features/mobile/external-api/middleware.js +++ b/react/features/mobile/external-api/middleware.js @@ -2,6 +2,7 @@ import { NativeModules } from 'react-native'; +import { Platform } from '../../base/react'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, @@ -41,8 +42,8 @@ MiddlewareRegistry.register(store => next => action => { // of locationURL at the time of CONFERENCE_WILL_LEAVE and // CONFERENCE_LEFT will not be the value with which the // JitsiConference instance being left.) - const { locationURL } - = store.getState()['features/base/connection']; + const state = store.getState(); + const { locationURL } = state['features/base/connection']; if (!locationURL) { // The (redux) action cannot be fully converted to an (external @@ -63,7 +64,14 @@ MiddlewareRegistry.register(store => next => action => { name = name.slice(7, -1); } - _sendEvent(name, data); + // The polyfill es6-symbol that we use does not appear to comply with + // the Symbol standard and, merely, adds @@ at the beginning of the + // description. + if (name.startsWith('@@')) { + name = name.slice(2); + } + + _sendEvent(store, name, data); break; } } @@ -76,12 +84,31 @@ MiddlewareRegistry.register(store => next => action => { * apps may listen to such events via the mechanisms provided by the (native) * mobile Jitsi Meet SDK. * + * @param {Object} store - The redux store associated with the need to send the + * specified event. * @param {string} name - The name of the event to send. * @param {Object} data - The details/specifics of the event to send determined * by/associated with the specified {@code name}. * @private * @returns {void} */ -function _sendEvent(name: string, data: Object) { - NativeModules.ExternalAPI.sendEvent(name, data); +function _sendEvent(store: Object, name: string, data: Object) { + // The JavaScript App needs to provide uniquely identifying information + // to the native ExternalAPI module so that the latter may match the former + // to the native JitsiMeetView which hosts it. + const state = store.getState(); + const { app } = state['features/app']; + + if (app) { + const { externalAPIScope } = app.props; + + // TODO Lift the restriction on the JitsiMeetView instance count on + // Android as well. + if (externalAPIScope) { + NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope); + } else if (Platform.OS === 'android') { + NativeModules.ExternalAPI.sendEvent(name, data); + console.warn(name); + } + } }