diff --git a/android/README.md b/android/README.md
index 1f1f42b43..c2d2c4809 100644
--- a/android/README.md
+++ b/android/README.md
@@ -93,11 +93,20 @@ See JitsiMeetView.loadURL.
The `JitsiMeetView` class is the core of Jitsi Meet SDK. It's designed to
display a Jitsi Meet conference view (or a welcome page).
+#### getListener()
+
+Returns the `JitsiMeetView.Listener` instance attached to the view.
+
#### loadURL(url)
Loads the given URL and joins the conference which is being pointed to. If null,
it will load the welcome page.
+#### setListener(listener)
+
+Sets the given listener (class implementing the `JitsiMeetView.Listener`
+interface) on the view.
+
#### onBackPressed()
Helper method which should be called from the activity's `onBackPressed` method.
@@ -132,3 +141,41 @@ application's activity is launched in "singleTask" mode this method should
be called from the activity's `onNewIntent` method.
This is a static method.
+
+#### Listener
+
+JitsiMeetView.Listener provides an interface applications can implement in order
+to get notified about the state of the Jitsi Meet conference.
+
+##### onConferenceFailed
+
+Called when a joining a conference was unsuccessful or when there was an error
+while in a conference.
+
+The `data` HashMap contains an "error" key describing the error and a "url"
+key with the conference URL.
+
+#### onConferenceJoined
+
+Called when a conference was joined.
+
+The `data` HashMap contains a "url" key with the conference URL.
+
+#### onConferenceLeft
+
+Called when a conference was left.
+
+The `data` HashMap contains a "url" key with the conference URL.
+
+#### onConferenceWillJoin
+
+Called before a conference is joined.
+
+The `data` HashMap contains a "url" key with the conference URL.
+
+#### onConferenceWillLeave
+
+Called before a conference is left.
+
+The `data` HashMap contains a "url" key with the conference URL.
+
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetBaseActivity.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetBaseActivity.java
index 92e0154a2..de5074645 100644
--- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetBaseActivity.java
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetBaseActivity.java
@@ -38,7 +38,15 @@ import java.net.URL;
* the JKConferenceView static methods.
*/
public class JitsiMeetBaseActivity extends AppCompatActivity {
+ /**
+ * Instance of the {@JitsiMeetView} which this activity will display.
+ */
private JitsiMeetView jitsiMeetView;
+
+ /**
+ * Code for identifying the permission to draw on top of other apps. The number is chosen
+ * arbitrarily and used to correlate the intent with its result.
+ */
public static final int OVERLAY_PERMISSION_REQ_CODE = 4242;
/**
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java
index 3bce82c60..e7cd956f2 100644
--- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java
@@ -30,13 +30,27 @@ import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState;
import java.net.URL;
+import java.util.HashMap;
public class JitsiMeetView extends FrameLayout {
/**
- * React Native root view.
+ * Background color used by this view and the React Native root view.
*/
- private ReactRootView mReactRootView;
+ private static final int BACKGROUND_COLOR = 0xFF111111;
+
+ /**
+ * {@JitsiMeetView.Listener} instance for reporting events occurring in Jitsi Meet.
+ */
+ private JitsiMeetView.Listener listener;
+
+ /**
+ * Reference to the single instance of this class we currently allow. It's currently used for
+ * fetching the instance from the listener's callbacks.
+ *
+ * TODO: lift this limitation.
+ */
+ private static JitsiMeetView mInstance;
/**
* React Native bridge. The instance manager allows embedding applications to create multiple
@@ -45,13 +59,25 @@ public class JitsiMeetView extends FrameLayout {
private static ReactInstanceManager mReactInstanceManager;
/**
- * Background color used by this view and the React Native root view.
+ * React Native root view.
*/
- private static final int BACKGROUND_COLOR = 0xFF111111;
+ private ReactRootView mReactRootView;
public JitsiMeetView(@NonNull Context context) {
super(context);
+ if (mInstance != null) {
+ throw new RuntimeException("Only a single instance is currently allowed");
+ }
+
+ /*
+ * 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.
+ */
+ mInstance = this;
+
setBackgroundColor(BACKGROUND_COLOR);
if (mReactInstanceManager == null) {
@@ -59,6 +85,50 @@ public class JitsiMeetView extends FrameLayout {
}
}
+ /**
+ * Returns the only instance of this class we currently allow creating.
+ *
+ * @returns The {@JitsiMeetView} instance.
+ */
+ public static JitsiMeetView getInstance() {
+ return mInstance;
+ }
+
+ /**
+ * Getter for the {@JitsiMeetView.Listener} set on this view.
+ *
+ * @returns The {@JitsiMeetView.Listener} instance.
+ */
+ public Listener getListener() {
+ return listener;
+ }
+
+ /**
+ * Internal method to initialize the React Native instance manager. We create a single instance
+ * in order to load the JavaScript bundle a single time. All ReactRootView instances
+ * will be tied to the one and only ReactInstanceManager.
+ *
+ * @param application - Application instance which is running.
+ */
+ private static void initReactInstanceManager(Application application) {
+ mReactInstanceManager = ReactInstanceManager.builder()
+ .setApplication(application)
+ .setBundleAssetName("index.android.bundle")
+ .setJSMainModuleName("index.android")
+ .addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
+ .addPackage(new com.facebook.react.shell.MainReactPackage())
+ .addPackage(new com.oblador.vectoricons.VectorIconsPackage())
+ .addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
+ .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
+ .addPackage(new com.rnimmersive.RNImmersivePackage())
+ .addPackage(new org.jitsi.meet.sdk.audiomode.AudioModePackage())
+ .addPackage(new org.jitsi.meet.sdk.externalapi.ExternalAPIPackage())
+ .addPackage(new org.jitsi.meet.sdk.proximity.ProximityPackage())
+ .setUseDeveloperSupport(BuildConfig.DEBUG)
+ .setInitialLifecycleState(LifecycleState.RESUMED)
+ .build();
+ }
+
/**
* Loads the given URL and displays the conference. If the specified URL is null, the welcome
* page is displayed instead.
@@ -84,28 +154,12 @@ public class JitsiMeetView extends FrameLayout {
}
/**
- * Internal method to initialize the React Native instance manager. We create a single instance
- * in order to load the JavaScript bundle a single time. All ReactRootView instances
- * will be tied to the one and only ReactInstanceManager.
+ * Setter for the {@JitsiMeetView.Listener} set on this view.
*
- * @param application - Application instance which is running.
+ * @param listener - Listener for this view.
*/
- private static void initReactInstanceManager(Application application) {
- mReactInstanceManager = ReactInstanceManager.builder()
- .setApplication(application)
- .setBundleAssetName("index.android.bundle")
- .setJSMainModuleName("index.android")
- .addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
- .addPackage(new com.facebook.react.shell.MainReactPackage())
- .addPackage(new com.oblador.vectoricons.VectorIconsPackage())
- .addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
- .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
- .addPackage(new com.rnimmersive.RNImmersivePackage())
- .addPackage(new org.jitsi.meet.sdk.audiomode.AudioModePackage())
- .addPackage(new org.jitsi.meet.sdk.proximity.ProximityPackage())
- .setUseDeveloperSupport(BuildConfig.DEBUG)
- .setInitialLifecycleState(LifecycleState.RESUMED)
- .build();
+ public void setListener(Listener listener) {
+ this.listener = listener;
}
/**
@@ -173,4 +227,41 @@ public class JitsiMeetView extends FrameLayout {
mReactInstanceManager.onNewIntent(intent);
}
}
+
+ /**
+ * Interface for listening to events coming from Jitsi Meet.
+ */
+ public interface Listener {
+ /**
+ * Called when joining a conference fails or an ongoing conference is interrupted due to a
+ * failure.
+ * @param data - HashMap with an "error" key describing the problem, and a "url" key with
+ * the conference URL.
+ */
+ void onConferenceFailed(HashMap data);
+
+ /**
+ * Called when a conference was joined.
+ * @param data - HashMap with a "url" key with the conference URL.
+ */
+ void onConferenceJoined(HashMap data);
+
+ /**
+ * Called when the conference was left, typically after hanging up.
+ * @param data - HashMap with a "url" key with the conference URL.
+ */
+ void onConferenceLeft(HashMap data);
+
+ /**
+ * Called before the conference is joined.
+ * @param data - HashMap with a "url" key with the conference URL.
+ */
+ void onConferenceWillJoin(HashMap data);
+
+ /**
+ * Called before the conference is left.
+ * @param data - HashMap with a "url" key with the conference URL.
+ */
+ void onConferenceWillLeave(HashMap data);
+ }
}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/externalapi/ExternalAPIModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/externalapi/ExternalAPIModule.java
new file mode 100644
index 000000000..fa0063cf4
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/externalapi/ExternalAPIModule.java
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+package org.jitsi.meet.sdk.externalapi;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+
+import org.jitsi.meet.sdk.JitsiMeetView;
+
+import java.util.HashMap;
+
+
+/**
+ * Module implementing a simple API to enable a proximity sensor-controlled
+ * wake lock. When the lock is held, if the proximity sensor detects a nearby
+ * object it will dim the screen and disable touch controls. The functionality
+ * is used with the conference audio-only mode.
+ */
+public class ExternalAPIModule extends ReactContextBaseJavaModule {
+ /**
+ * React Native module name.
+ */
+ private static final String MODULE_NAME = "ExternalAPI";
+
+ /**
+ * Initializes a new module instance. There shall be a single instance of
+ * this module throughout the lifetime of the application.
+ *
+ * @param reactContext the {@link ReactApplicationContext} where this module
+ * is created.
+ */
+ public ExternalAPIModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ /**
+ * Gets the name of this module to be used in the React Native bridge.
+ *
+ * @return The name of this module to be used in the React Native bridge.
+ */
+ @Override
+ public String getName() {
+ return MODULE_NAME;
+ }
+
+ /**
+ * Dispatches an event that occurred on JavaScript to the view's listener.
+ * @param name - Event name.
+ * @param data - Ancillary data for the event.
+ */
+ @ReactMethod
+ public void sendEvent(final String name, ReadableMap data) {
+ JitsiMeetView view = JitsiMeetView.getInstance();
+ JitsiMeetView.Listener listener = view != null ? view.getListener() : null;
+
+ if (listener == null) {
+ return;
+ }
+
+ // TODO: Sigh, converting a ReadableMap to a HashMap is not supported until React Native
+ // 0.46.
+ final HashMap dataMap = new HashMap<>();
+
+ try {
+ switch (name) {
+ case "CONFERENCE_FAILED":
+ dataMap.put("error", data.getString("error"));
+ dataMap.put("url", data.getString("url"));
+ listener.onConferenceFailed(dataMap);
+ break;
+ case "CONFERENCE_JOINED":
+ dataMap.put("url", data.getString("url"));
+ listener.onConferenceJoined(dataMap);
+ break;
+ case "CONFERENCE_LEFT":
+ dataMap.put("url", data.getString("url"));
+ listener.onConferenceLeft(dataMap);
+ break;
+ case "CONFERENCE_WILL_JOIN":
+ dataMap.put("url", data.getString("url"));
+ listener.onConferenceWillJoin(dataMap);
+ break;
+ case "CONFERENCE_WILL_LEAVE":
+ dataMap.put("url", data.getString("url"));
+ listener.onConferenceWillLeave(dataMap);
+ break;
+ }
+ } catch (UnsupportedOperationException e) {
+ // Allow partial interface implementations.
+ }
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/externalapi/ExternalAPIPackage.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/externalapi/ExternalAPIPackage.java
new file mode 100644
index 000000000..7f86ae93c
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/externalapi/ExternalAPIPackage.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+package org.jitsi.meet.sdk.externalapi;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implements {@link ReactPackage} for {@link ExternalAPIModule}.
+ */
+public class ExternalAPIPackage implements ReactPackage {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List> createJSModules() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return List of native modules to be exposed by React Native.
+ */
+ @Override
+ public List createNativeModules(
+ ReactApplicationContext reactContext) {
+ List modules = new ArrayList<>();
+
+ modules.add(new ExternalAPIModule(reactContext));
+
+ return modules;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List createViewManagers(
+ ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+}
diff --git a/ios/README.md b/ios/README.md
index 3afce365f..4010c565b 100644
--- a/ios/README.md
+++ b/ios/README.md
@@ -69,4 +69,43 @@ continueUserActivity:(NSUserActivity *)userActivity
### JitsiMeetViewDelegate
-TODO.
+This delegate is optional, and can be set on the `JitsiMeetView` instance using
+the `delegate` property.
+
+It provides information about the conference state: was it joined, left, did it
+fail?
+
+All methods in this delegate are optional.
+
+##### conferenceFailed
+
+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.
+
+#### conferenceJoined
+
+Called when a conference was joined.
+
+The `data` dictionary contains a "url" key with the conference URL.
+
+#### conferenceLeft
+
+Called when a conference was left.
+
+The `data` dictionary contains a "url" key with the conference URL.
+
+#### conferenceWillJoin
+
+Called before a conference is joined.
+
+The `data` dictionary contains a "url" key with the conference URL.
+
+#### conferenceWillLeave
+
+Called before a conference is left.
+
+The `data` dictionary contains a "url" key with the conference URL.
+
diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj
index 5640fdf24..7cba8c32e 100644
--- a/ios/sdk/sdk.xcodeproj/project.pbxproj
+++ b/ios/sdk/sdk.xcodeproj/project.pbxproj
@@ -13,6 +13,7 @@
0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B93EF7A1EC608550030D24D /* CoreText.framework */; };
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; };
+ 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */; };
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495C1EC4B6C600B793EE /* AudioMode.m */; };
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495D1EC4B6C600B793EE /* POSIX.m */; };
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495E1EC4B6C600B793EE /* Proximity.m */; };
@@ -44,6 +45,7 @@
0B93EF7A1EC608550030D24D /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; };
0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridgeWrapper.h; sourceTree = ""; };
0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeWrapper.m; sourceTree = ""; };
+ 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExternalAPI.m; sourceTree = ""; };
0BCA495C1EC4B6C600B793EE /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioMode.m; sourceTree = ""; };
0BCA495D1EC4B6C600B793EE /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = POSIX.m; sourceTree = ""; };
0BCA495E1EC4B6C600B793EE /* Proximity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Proximity.m; sourceTree = ""; };
@@ -100,6 +102,7 @@
isa = PBXGroup;
children = (
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
+ 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
@@ -267,6 +270,7 @@
buildActionMask = 2147483647;
files = (
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
+ 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
diff --git a/ios/sdk/src/ExternalAPI.m b/ios/sdk/src/ExternalAPI.m
new file mode 100644
index 000000000..e3e8fa41b
--- /dev/null
+++ b/ios/sdk/src/ExternalAPI.m
@@ -0,0 +1,66 @@
+/*
+ * 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 "RCTBridgeModule.h"
+
+#import "JitsiMeetView.h"
+
+
+@interface ExternalAPI : NSObject
+@end
+
+@implementation ExternalAPI
+
+RCT_EXPORT_MODULE();
+
+/**
+ * Dispatches an event that occurred on JavaScript to the view's delegate.
+ *
+ * - 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;
+
+ if (delegate == nil) {
+ 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];
+ }
+}
+
+@end
diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h
index b7c5a35a0..fb3fe2d33 100644
--- a/ios/sdk/src/JitsiMeetView.h
+++ b/ios/sdk/src/JitsiMeetView.h
@@ -32,6 +32,8 @@
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 6129193f4..310834f75 100644
--- a/ios/sdk/src/JitsiMeetView.m
+++ b/ios/sdk/src/JitsiMeetView.m
@@ -55,6 +55,7 @@ RCTFatalHandler _RCTFatal = ^(NSError *error) {
@implementation JitsiMeetView
static RCTBridgeWrapper *bridgeWrapper;
+static JitsiMeetView *instance;
#pragma mark Linking delegate helpers
// https://facebook.github.io/react-native/docs/linking.html
@@ -125,6 +126,10 @@ static RCTBridgeWrapper *bridgeWrapper;
#pragma mark private methods
++ (instancetype)getInstance {
+ return instance;
+}
+
/*
* Internal initialization:
*
@@ -136,6 +141,20 @@ static RCTBridgeWrapper *bridgeWrapper;
- (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
diff --git a/ios/sdk/src/JitsiMeetViewDelegate.h b/ios/sdk/src/JitsiMeetViewDelegate.h
index 90a460c70..c57a79dd6 100644
--- a/ios/sdk/src/JitsiMeetViewDelegate.h
+++ b/ios/sdk/src/JitsiMeetViewDelegate.h
@@ -16,6 +16,43 @@
@protocol JitsiMeetViewDelegate
-// TODO: add delegate methods
+@optional
+
+/*
+ * 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.
+ */
+- (void) conferenceFailed:(NSDictionary *) data;
+
+/*
+ * Called when a conference was joined.
+ *
+ * The `data` dictionary contains a "url" key with the conference URL.
+ */
+- (void) conferenceJoined:(NSDictionary *) data;
+
+/*
+ * Called when a conference was left.
+ *
+ * The `data` dictionary contains a "url" key with the conference URL.
+ */
+- (void) conferenceLeft:(NSDictionary *) data;
+
+/*
+ * Called before a conference is joined.
+ *
+ * The `data` dictionary contains a "url" key with the conference URL.
+ */
+- (void) conferenceWillJoin:(NSDictionary *) data;
+
+/*
+ * Called before a conference is left.
+ *
+ * The `data` dictionary contains a "url" key with the conference URL.
+ */
+- (void) conferenceWillLeave:(NSDictionary *) data;
@end
diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js
index b5c6f6e39..da54edcf5 100644
--- a/react/features/app/components/App.native.js
+++ b/react/features/app/components/App.native.js
@@ -5,6 +5,7 @@ import { Linking } from 'react-native';
import { Platform } from '../../base/react';
import '../../mobile/audio-mode';
import '../../mobile/background';
+import '../../mobile/external-api';
import '../../mobile/full-screen';
import '../../mobile/proximity';
import '../../mobile/wake-lock';
@@ -22,7 +23,7 @@ export class App extends AbstractApp {
*
* @static
*/
- static propTypes = AbstractApp.propTypes
+ static propTypes = AbstractApp.propTypes;
/**
* Initializes a new App instance.
diff --git a/react/features/mobile/external-api/index.js b/react/features/mobile/external-api/index.js
new file mode 100644
index 000000000..d43689289
--- /dev/null
+++ b/react/features/mobile/external-api/index.js
@@ -0,0 +1 @@
+import './middleware';
diff --git a/react/features/mobile/external-api/middleware.js b/react/features/mobile/external-api/middleware.js
new file mode 100644
index 000000000..5ed52496b
--- /dev/null
+++ b/react/features/mobile/external-api/middleware.js
@@ -0,0 +1,75 @@
+/* @flow */
+
+import { NativeModules } from 'react-native';
+
+import { getInviteURL } from '../../base/connection';
+import {
+ CONFERENCE_FAILED,
+ CONFERENCE_JOINED,
+ CONFERENCE_LEFT,
+ CONFERENCE_WILL_JOIN,
+ CONFERENCE_WILL_LEAVE
+} from '../../base/conference';
+import { MiddlewareRegistry } from '../../base/redux';
+
+
+/**
+ * Middleware that captures Redux actions and uses the ExternalAPI module to
+ * turn them into native events so the application knows about them.
+ *
+ * @param {Store} store - Redux store.
+ * @returns {Function}
+ */
+MiddlewareRegistry.register(store => next => action => {
+ const eventData = {};
+
+ switch (action.type) {
+ case CONFERENCE_FAILED: {
+ eventData.error = action.error;
+ eventData.url = getInviteURL(store.getState());
+ _sendEvent('CONFERENCE_FAILED', eventData);
+ break;
+ }
+
+ case CONFERENCE_JOINED: {
+ eventData.url = getInviteURL(store.getState());
+ _sendEvent('CONFERENCE_JOINED', eventData);
+ break;
+ }
+
+ case CONFERENCE_LEFT: {
+ eventData.url = getInviteURL(store.getState());
+ _sendEvent('CONFERENCE_LEFT', eventData);
+ break;
+ }
+
+ case CONFERENCE_WILL_JOIN: {
+ eventData.url = getInviteURL(store.getState());
+ _sendEvent('CONFERENCE_WILL_JOIN', eventData);
+ break;
+ }
+
+ case CONFERENCE_WILL_LEAVE: {
+ eventData.url = getInviteURL(store.getState());
+ _sendEvent('CONFERENCE_WILL_LEAVE', eventData);
+ break;
+ }
+
+ }
+
+ return next(action);
+});
+
+/**
+ * Sends the given event to the native side of the application. Applications can
+ * then listen to the events using the mechanisms provided by the Jitsi Meet
+ * SDK.
+ *
+ * @param {string} name - Event name.
+ * @param {Object} data - Ancillary data for the event.
+ * @private
+ * @returns {void}
+ */
+function _sendEvent(name: string, data: Object) {
+ NativeModules.ExternalAPI.sendEvent(name, data);
+}