[iOS] Allow multiple JitsiMeetViews

This commit is contained in:
Lyubo Marinov 2017-06-09 19:17:01 -05:00
parent 5f64ccb97d
commit 10e5e0fdf5
8 changed files with 247 additions and 174 deletions

View File

@ -19,10 +19,10 @@ package org.jitsi.meet.sdk;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import java.net.URL; import java.net.URL;

View File

@ -16,9 +16,10 @@
#import "RCTBridgeModule.h" #import "RCTBridgeModule.h"
#import "JitsiMeetView.h" #import "JitsiMeetView+Private.h"
@interface ExternalAPI : NSObject<RCTBridgeModule> @interface ExternalAPI : NSObject<RCTBridgeModule>
@end @end
@implementation ExternalAPI @implementation ExternalAPI
@ -31,26 +32,40 @@ RCT_EXPORT_MODULE();
* - name: name of the event. * - name: name of the event.
* - data: dictionary (JSON object in JS) with data associated with the event. * - data: dictionary (JSON object in JS) with data associated with the event.
*/ */
RCT_EXPORT_METHOD(sendEvent:(NSString*)name data:(NSDictionary *) data) { RCT_EXPORT_METHOD(sendEvent:(NSString *)name
JitsiMeetView *view = [JitsiMeetView getInstance]; data:(NSDictionary *)data
id delegate = view != nil ? view.delegate : nil; 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; return;
} }
if ([name isEqualToString:@"CONFERENCE_FAILED"] if ([name isEqualToString:@"CONFERENCE_FAILED"]
&& [delegate respondsToSelector:@selector(conferenceFailed:)]) { && [delegate respondsToSelector:@selector(conferenceFailed:)]) {
[delegate conferenceFailed:data]; [delegate conferenceFailed:data];
} else if ([name isEqualToString:@"CONFERENCE_JOINED"] } else if ([name isEqualToString:@"CONFERENCE_JOINED"]
&& [delegate respondsToSelector:@selector(conferenceJoined:)]) { && [delegate respondsToSelector:@selector(conferenceJoined:)]) {
[delegate conferenceJoined:data]; [delegate conferenceJoined:data];
} else if ([name isEqualToString:@"CONFERENCE_LEFT"] } else if ([name isEqualToString:@"CONFERENCE_LEFT"]
&& [delegate respondsToSelector:@selector(conferenceLeft:)]) { && [delegate respondsToSelector:@selector(conferenceLeft:)]) {
[delegate conferenceLeft:data]; [delegate conferenceLeft:data];
} else if ([name isEqualToString:@"CONFERENCE_WILL_JOIN"] } else if ([name isEqualToString:@"CONFERENCE_WILL_JOIN"]
&& [delegate respondsToSelector:@selector(conferenceWillJoin:)]) { && [delegate respondsToSelector:@selector(conferenceWillJoin:)]) {
[delegate conferenceWillJoin:data]; [delegate conferenceWillJoin:data];
} else if ([name isEqualToString:@"CONFERENCE_WILL_LEAVE"] } else if ([name isEqualToString:@"CONFERENCE_WILL_LEAVE"]
&& [delegate respondsToSelector:@selector(conferenceWillLeave:)]) { && [delegate respondsToSelector:@selector(conferenceWillLeave:)]) {
[delegate conferenceWillLeave:data]; [delegate conferenceWillLeave:data];

View File

@ -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

View File

@ -34,8 +34,6 @@
sourceApplication:(NSString *)sourceApplication sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation; annotation:(id)annotation;
+ (instancetype)getInstance;
- (void)loadURL:(nullable NSURL *)url; - (void)loadURL:(nullable NSURL *)url;
@end @end

View File

@ -20,7 +20,7 @@
#import <React/RCTLinkingManager.h> #import <React/RCTLinkingManager.h>
#import <React/RCTRootView.h> #import <React/RCTRootView.h>
#import "JitsiMeetView.h" #import "JitsiMeetView+Private.h"
#import "RCTBridgeWrapper.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 * 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. * plist file doesn't work for frameworks, so fonts have to be manually loaded.
*/ */
- (void)loadCustomFonts { void loadCustomFonts(Class clazz) {
NSBundle *bundle = [NSBundle bundleForClass:self.class]; NSBundle *bundle = [NSBundle bundleForClass:clazz];
NSArray *fonts = [bundle objectForInfoDictionaryKey:@"JitsiMeetFonts"]; NSArray *fonts = [bundle objectForInfoDictionaryKey:@"JitsiMeetFonts"];
for (NSString *item in fonts) { for (NSString *item in fonts) {
@ -209,12 +75,12 @@ static JitsiMeetView *instance;
} }
} }
/* /**
* Helper function to register a fatal error handler for React. Our handler * 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 * won't kill the process, it will swallow JS errors and print stack traces
* instead. * instead.
*/ */
- (void)registerFatalErrorHandler { void registerFatalErrorHandler() {
#if !DEBUG #if !DEBUG
// In the Release configuration, React Native will (intentionally) raise // In the Release configuration, React Native will (intentionally) raise
// an unhandled NSException for an unhandled JavaScript error. This will // an unhandled NSException for an unhandled JavaScript error. This will
@ -226,4 +92,144 @@ static JitsiMeetView *instance;
#endif #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<NSString *, JitsiMeetView *> *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 @end

View File

@ -18,41 +18,45 @@
@optional @optional
/* /**
* Called when a joining a conference was unsuccessful or when there was an error * Called when a joining a conference was unsuccessful or when there was an
* while in a conference. * error while in a conference.
* *
* The `data` dictionary contains an "error" key describing the error and a "url" * The {@code data} dictionary contains an "error" key describing the error and
* key with the conference URL. * a {@code url} key with the conference URL.
*/ */
- (void) conferenceFailed:(NSDictionary *) data; - (void) conferenceFailed:(NSDictionary *)data;
/* /**
* Called when a conference was joined. * 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. * 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. * 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. * 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 @end

View File

@ -16,7 +16,7 @@
#include "RCTBridgeWrapper.h" #include "RCTBridgeWrapper.h"
/* /**
* Wrapper around RCTBridge which also implements the RCTBridgeDelegate methods, * Wrapper around RCTBridge which also implements the RCTBridgeDelegate methods,
* allowing us to specify where the bundles are loaded from. * allowing us to specify where the bundles are loaded from.
*/ */
@ -56,9 +56,9 @@ static NSURL *serverRootWithHost(NSString *host) {
- (NSString *)guessPackagerHost { - (NSString *)guessPackagerHost {
static NSString *ipGuess; static NSString *ipGuess;
static dispatch_once_t onceToken; static dispatch_once_t dispatchOncePredicate;
dispatch_once(&onceToken, ^{ dispatch_once(&dispatchOncePredicate, ^{
NSString *ipPath NSString *ipPath
= [[NSBundle bundleForClass:self.class] pathForResource:@"ip" = [[NSBundle bundleForClass:self.class] pathForResource:@"ip"
ofType:@"txt"]; ofType:@"txt"];

View File

@ -2,6 +2,7 @@
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import { Platform } from '../../base/react';
import { import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_JOINED, CONFERENCE_JOINED,
@ -41,8 +42,8 @@ MiddlewareRegistry.register(store => next => action => {
// of locationURL at the time of CONFERENCE_WILL_LEAVE and // of locationURL at the time of CONFERENCE_WILL_LEAVE and
// CONFERENCE_LEFT will not be the value with which the // CONFERENCE_LEFT will not be the value with which the
// JitsiConference instance being left.) // JitsiConference instance being left.)
const { locationURL } const state = store.getState();
= store.getState()['features/base/connection']; const { locationURL } = state['features/base/connection'];
if (!locationURL) { if (!locationURL) {
// The (redux) action cannot be fully converted to an (external // The (redux) action cannot be fully converted to an (external
@ -63,7 +64,14 @@ MiddlewareRegistry.register(store => next => action => {
name = name.slice(7, -1); 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; break;
} }
} }
@ -76,12 +84,31 @@ MiddlewareRegistry.register(store => next => action => {
* apps may listen to such events via the mechanisms provided by the (native) * apps may listen to such events via the mechanisms provided by the (native)
* mobile Jitsi Meet SDK. * 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 {string} name - The name of the event to send.
* @param {Object} data - The details/specifics of the event to send determined * @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code name}. * by/associated with the specified {@code name}.
* @private * @private
* @returns {void} * @returns {void}
*/ */
function _sendEvent(name: string, data: Object) { 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); NativeModules.ExternalAPI.sendEvent(name, data);
console.warn(name);
}
}
} }