[iOS] Allow multiple JitsiMeetViews
This commit is contained in:
parent
5f64ccb97d
commit
10e5e0fdf5
|
@ -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;
|
||||
|
||||
|
|
|
@ -16,9 +16,10 @@
|
|||
|
||||
#import "RCTBridgeModule.h"
|
||||
|
||||
#import "JitsiMeetView.h"
|
||||
#import "JitsiMeetView+Private.h"
|
||||
|
||||
@interface ExternalAPI : NSObject<RCTBridgeModule>
|
||||
|
||||
@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];
|
||||
|
|
|
@ -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
|
|
@ -34,8 +34,6 @@
|
|||
sourceApplication:(NSString *)sourceApplication
|
||||
annotation:(id)annotation;
|
||||
|
||||
+ (instancetype)getInstance;
|
||||
|
||||
- (void)loadURL:(nullable NSURL *)url;
|
||||
|
||||
@end
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
#import <React/RCTLinkingManager.h>
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
#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<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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue