From effd3728b67aea1f34f60eda1c25c541231fa68c Mon Sep 17 00:00:00 2001 From: Lyubo Marinov Date: Mon, 30 Apr 2018 23:43:47 -0500 Subject: [PATCH] [RN] add support for inviting participants during a call on mobile (2) --- .../java/org/jitsi/meet/MainActivity.java | 21 +- .../meet/sdk/InviteSearchController.java | 184 ---------- .../jitsi/meet/sdk/InviteSearchModule.java | 126 ------- .../org/jitsi/meet/sdk/JitsiMeetActivity.java | 3 +- .../org/jitsi/meet/sdk/JitsiMeetView.java | 221 +++--------- .../jitsi/meet/sdk/JitsiMeetViewAdapter.java | 4 - .../jitsi/meet/sdk/JitsiMeetViewListener.java | 10 - .../org/jitsi/meet/sdk/ReactContextUtils.java | 43 +++ .../meet/sdk/ReactInstanceManagerHolder.java | 125 +++++++ .../meet/sdk/invite/AddPeopleController.java | 204 +++++++++++ .../invite/AddPeopleControllerListener.java | 49 +++ .../meet/sdk/invite/InviteController.java | 265 ++++++++++++++ .../sdk/invite/InviteControllerListener.java | 29 ++ .../jitsi/meet/sdk/invite/InviteModule.java | 119 +++++++ ios/app/src/ViewController.h | 2 +- ios/app/src/ViewController.m | 26 +- ios/sdk/sdk.xcodeproj/project.pbxproj | 50 ++- ios/sdk/src/InviteSearch.h | 49 --- ios/sdk/src/InviteSearch.m | 215 ----------- ios/sdk/src/JitsiMeet.h | 8 +- ios/sdk/src/JitsiMeetView.h | 5 +- ios/sdk/src/JitsiMeetView.m | 18 +- ios/sdk/src/JitsiMeetViewDelegate.h | 11 - .../src/invite/AddPeopleController+Private.h | 33 ++ ios/sdk/src/invite/AddPeopleController.h | 31 ++ ios/sdk/src/invite/AddPeopleController.m | 79 +++++ .../src/invite/AddPeopleControllerDelegate.h | 38 ++ ios/sdk/src/invite/Invite+Private.h | 30 ++ ios/sdk/src/invite/Invite.m | 90 +++++ ios/sdk/src/invite/InviteController+Private.h | 50 +++ ios/sdk/src/invite/InviteController.h | 32 ++ ios/sdk/src/invite/InviteController.m | 118 ++++++ ios/sdk/src/invite/InviteControllerDelegate.h | 29 ++ react/features/invite/actionTypes.js | 24 ++ react/features/invite/actions.js | 16 + .../components/AddPeopleDialog.native.js | 3 - .../invite/components/AddPeopleDialog.web.js | 32 +- .../invite/components/InviteButton.native.js | 114 ++++-- react/features/invite/functions.js | 335 ++++++++++-------- .../{middleware.js => middleware.any.js} | 8 +- react/features/invite/middleware.native.js | 208 +++++++++++ react/features/invite/middleware.web.js | 50 +++ react/features/invite/reducer.js | 9 +- .../mobile/external-api/middleware.js | 6 +- .../mobile/invite-search/actionTypes.js | 46 --- .../features/mobile/invite-search/actions.js | 50 --- react/features/mobile/invite-search/index.js | 5 - .../mobile/invite-search/middleware.js | 233 ------------ .../features/mobile/invite-search/reducer.js | 14 - .../toolbox/components/Toolbox.native.js | 78 +--- .../toolbox/components/Toolbox.web.js | 54 +-- 51 files changed, 2143 insertions(+), 1459 deletions(-) delete mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchController.java delete mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchModule.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/ReactContextUtils.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleController.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleControllerListener.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteController.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteControllerListener.java create mode 100644 android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java delete mode 100644 ios/sdk/src/InviteSearch.h delete mode 100644 ios/sdk/src/InviteSearch.m create mode 100644 ios/sdk/src/invite/AddPeopleController+Private.h create mode 100644 ios/sdk/src/invite/AddPeopleController.h create mode 100644 ios/sdk/src/invite/AddPeopleController.m create mode 100644 ios/sdk/src/invite/AddPeopleControllerDelegate.h create mode 100644 ios/sdk/src/invite/Invite+Private.h create mode 100644 ios/sdk/src/invite/Invite.m create mode 100644 ios/sdk/src/invite/InviteController+Private.h create mode 100644 ios/sdk/src/invite/InviteController.h create mode 100644 ios/sdk/src/invite/InviteController.m create mode 100644 ios/sdk/src/invite/InviteControllerDelegate.h rename react/features/invite/{middleware.js => middleware.any.js} (80%) create mode 100644 react/features/invite/middleware.native.js create mode 100644 react/features/invite/middleware.web.js delete mode 100644 react/features/mobile/invite-search/actionTypes.js delete mode 100644 react/features/mobile/invite-search/actions.js delete mode 100644 react/features/mobile/invite-search/index.js delete mode 100644 react/features/mobile/invite-search/middleware.js delete mode 100644 react/features/mobile/invite-search/reducer.js diff --git a/android/app/src/main/java/org/jitsi/meet/MainActivity.java b/android/app/src/main/java/org/jitsi/meet/MainActivity.java index c84dc660c..a154c9601 100644 --- a/android/app/src/main/java/org/jitsi/meet/MainActivity.java +++ b/android/app/src/main/java/org/jitsi/meet/MainActivity.java @@ -19,10 +19,11 @@ package org.jitsi.meet; import android.os.Bundle; import android.util.Log; -import org.jitsi.meet.sdk.InviteSearchController; import org.jitsi.meet.sdk.JitsiMeetActivity; import org.jitsi.meet.sdk.JitsiMeetView; import org.jitsi.meet.sdk.JitsiMeetViewListener; +import org.jitsi.meet.sdk.invite.AddPeopleController; +import org.jitsi.meet.sdk.invite.InviteControllerListener; import com.calendarevents.CalendarEventsPackage; @@ -86,16 +87,24 @@ public class MainActivity extends JitsiMeetActivity { on("CONFERENCE_WILL_LEAVE", data); } - @Override - public void launchNativeInvite(InviteSearchController inviteSearchController) { - on("LAUNCH_NATIVE_INVITE", new HashMap()); - } - @Override public void onLoadConfigError(Map data) { on("LOAD_CONFIG_ERROR", data); } }); + + view.getInviteController().setListener( + new InviteControllerListener() { + public void beginAddPeople( + AddPeopleController addPeopleController) { + // Log with the tag "ReactNative" in order to have the + // log visible in react-native log-android as well. + Log.d( + "ReactNative", + InviteControllerListener.class.getSimpleName() + + ".beginAddPeople"); + } + }); } return view; diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchController.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchController.java deleted file mode 100644 index 969b016fb..000000000 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchController.java +++ /dev/null @@ -1,184 +0,0 @@ -package org.jitsi.meet.sdk; - -import android.util.Log; - -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.bridge.WritableNativeArray; -import com.facebook.react.bridge.WritableNativeMap; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** - * Controller object used by native code to query and submit user selections for the user invitation flow. - */ -public class InviteSearchController { - - /** - * The InviteSearchControllerDelegate for this controller, used to pass query - * results back to the native code that initiated the query. - */ - private InviteSearchControllerDelegate searchControllerDelegate; - - /** - * Local cache of search query results. Used to re-hydrate the list - * of selected items based on their ids passed to submitSelectedItemIds - * in order to pass the full item maps back to the JitsiMeetView during submission. - */ - private Map items = new HashMap<>(); - - /** - * Randomly generated UUID, used for identification in the InviteSearchModule - */ - private String uuid = UUID.randomUUID().toString(); - - private WeakReference parentModuleRef; - - public InviteSearchController(InviteSearchModule module) { - parentModuleRef = new WeakReference<>(module); - } - - /** - * Start a search for entities to invite with the given query. - * Results will be returned through the associated InviteSearchControllerDelegate's - * onReceiveResults method. - * - * @param query - */ - public void performQuery(String query) { - JitsiMeetView.onInviteQuery(query, uuid); - } - - /** - * Send invites to selected users based on their item ids - * - * @param ids - */ - public void submitSelectedItemIds(List ids) { - WritableArray selectedItems = new WritableNativeArray(); - for(int i=0; i> jvmResults = new ArrayList<>(); - // cache results for use in submission later - // convert to jvm array - for(int i=0; i objects that represent items returned by the query. - * The object at key "type" describes the type of item: "user", "videosipgw" (conference room), or "phone". - * "user" types have properties at "id", "name", and "avatar" - * "videosipgw" types have properties at "id" and "name" - * "phone" types have properties at "number", "title", "and "subtitle" - * @param query the query that generated the given results - */ - void onReceiveResults(InviteSearchController searchController, List> results, String query); - - /** - * Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes successfully - * and invitations are sent to all given IDs. - * - * @param searchController the active {@link InviteSearchController} for this invite flow. This object will be - * cleaned up after the call to inviteSucceeded completes. - */ - void inviteSucceeded(InviteSearchController searchController); - - /** - * Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes, but the - * invitation fails for one or more of the selected items. - * - * @param searchController the active {@link InviteSearchController} for this invite flow. This object - * should be cleaned up by calling {@link InviteSearchController#cancelSearch()} if - * the user exits the invite flow. Otherwise, it can stay active if the user - * will attempt to invite - * @param failedInviteItems a {@code List} of {@code Map} dictionaries that represent the - * invitations that failed. The data type of the objects is identical to the results - * returned in onReceiveResuls. - */ - void inviteFailed(InviteSearchController searchController, List> failedInviteItems); - } -} \ No newline at end of file diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchModule.java deleted file mode 100644 index 4cfe2d28e..000000000 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchModule.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.jitsi.meet.sdk; - -import android.util.Log; - -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -/** - * Native module for Invite Search - */ -class InviteSearchModule extends ReactContextBaseJavaModule { - - /** - * Map of InviteSearchController objects passed to connected JitsiMeetView. - * A call to launchNativeInvite will create a new InviteSearchController and pass - * it back to the caller. On a successful invitation, the controller will be removed automatically. - * On a failed invitation, the caller has the option of calling InviteSearchController#cancelSearch() - * to remove the controller from this map. The controller should also be removed if the user cancels - * the invitation flow. - */ - private Map searchControllers = new HashMap<>(); - - public InviteSearchModule(ReactApplicationContext reactContext) { - super(reactContext); - } - - /** - * Launch the native user invite flow - * - * @param externalAPIScope a string that represents a connection to a specific JitsiMeetView - */ - @ReactMethod - public void launchNativeInvite(String externalAPIScope) { - JitsiMeetView viewToLaunchInvite = JitsiMeetView.findViewByExternalAPIScope(externalAPIScope); - - if(viewToLaunchInvite == null) { - return; - } - - if(viewToLaunchInvite.getListener() == null) { - return; - } - - InviteSearchController controller = createSearchController(); - viewToLaunchInvite.getListener().launchNativeInvite(controller); - } - - /** - * Callback for results received from the JavaScript invite search call - * - * @param results the results in a ReadableArray of ReadableMap objects - * @param query the query associated with the search - * @param inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController - */ - @ReactMethod - public void receivedResults(ReadableArray results, String query, String inviteSearchControllerScope) { - InviteSearchController controller = searchControllers.get(inviteSearchControllerScope); - - if(controller == null) { - Log.w("InviteSearchModule", "Received results, but unable to find active controller to send results back"); - return; - } - - controller.receivedResultsForQuery(results, query); - - } - - /** - * Callback for invitation failures - * - * @param items the items for which the invitation failed - * @param inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController - */ - @ReactMethod - public void inviteFailedForItems(ReadableArray items, String inviteSearchControllerScope) { - InviteSearchController controller = searchControllers.get(inviteSearchControllerScope); - - if(controller == null) { - Log.w("InviteSearchModule", "Invite failed, but unable to find active controller to notify"); - return; - } - - ArrayList> jvmItems = new ArrayList<>(); - for(int i=0; i views = Collections.newSetFromMap(new WeakHashMap()); - private static List createNativeModules( - ReactApplicationContext reactContext) { - return Arrays.asList( - new AndroidSettingsModule(reactContext), - new AppInfoModule(reactContext), - new AudioModeModule(reactContext), - new ExternalAPIModule(reactContext), - new InviteSearchModule(reactContext), - new PictureInPictureModule(reactContext), - new ProximityModule(reactContext), - new WiFiStatsModule(reactContext), - new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext) - ); - } - public static JitsiMeetView findViewByExternalAPIScope( String externalAPIScope) { synchronized (views) { @@ -99,47 +68,6 @@ public class JitsiMeetView extends FrameLayout { return null; } - // XXX Strictly internal use only (at the time of this writing)! - static ReactInstanceManager getReactInstanceManager() { - return reactInstanceManager; - } - - /** - * 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 {@code ReactRootView} instances will be tied to the one and - * only {@code ReactInstanceManager}. - * - * @param application {@code Application} instance which is running. - */ - private static void initReactInstanceManager(Application application) { - reactInstanceManager - = ReactInstanceManager.builder() - .setApplication(application) - .setBundleAssetName("index.android.bundle") - .setJSMainModulePath("index.android") - .addPackage(new com.calendarevents.CalendarEventsPackage()) - .addPackage(new com.corbt.keepawake.KCKeepAwakePackage()) - .addPackage(new com.facebook.react.shell.MainReactPackage()) - .addPackage(new com.i18n.reactnativei18n.ReactNativeI18n()) - .addPackage(new com.oblador.vectoricons.VectorIconsPackage()) - .addPackage(new com.ocetnik.timer.BackgroundTimerPackage()) - .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage()) - .addPackage(new com.RNFetchBlob.RNFetchBlobPackage()) - .addPackage(new com.rnimmersive.RNImmersivePackage()) - .addPackage(new com.zmxv.RNSound.RNSoundPackage()) - .addPackage(new ReactPackageAdapter() { - @Override - public List createNativeModules( - ReactApplicationContext reactContext) { - return JitsiMeetView.createNativeModules(reactContext); - } - }) - .setUseDeveloperSupport(BuildConfig.DEBUG) - .setInitialLifecycleState(LifecycleState.RESUMED) - .build(); - } - /** * Loads a specific URL {@code String} in all existing * {@code JitsiMeetView}s. @@ -174,6 +102,9 @@ public class JitsiMeetView extends FrameLayout { * implementation. */ public static boolean onBackPressed() { + ReactInstanceManager reactInstanceManager + = ReactInstanceManagerHolder.getReactInstanceManager(); + if (reactInstanceManager == null) { return false; } else { @@ -190,6 +121,9 @@ public class JitsiMeetView extends FrameLayout { * @param activity {@code Activity} being destroyed. */ public static void onHostDestroy(Activity activity) { + ReactInstanceManager reactInstanceManager + = ReactInstanceManagerHolder.getReactInstanceManager(); + if (reactInstanceManager != null) { reactInstanceManager.onHostDestroy(activity); } @@ -202,6 +136,9 @@ public class JitsiMeetView extends FrameLayout { * @param activity {@code Activity} being paused. */ public static void onHostPause(Activity activity) { + ReactInstanceManager reactInstanceManager + = ReactInstanceManagerHolder.getReactInstanceManager(); + if (reactInstanceManager != null) { reactInstanceManager.onHostPause(activity); } @@ -228,6 +165,9 @@ public class JitsiMeetView extends FrameLayout { public static void onHostResume( Activity activity, DefaultHardwareBackBtnHandler defaultBackButtonImpl) { + ReactInstanceManager reactInstanceManager + = ReactInstanceManagerHolder.getReactInstanceManager(); + if (reactInstanceManager != null) { reactInstanceManager.onHostResume(activity, defaultBackButtonImpl); } @@ -256,6 +196,9 @@ public class JitsiMeetView extends FrameLayout { return; } + ReactInstanceManager reactInstanceManager + = ReactInstanceManagerHolder.getReactInstanceManager(); + if (reactInstanceManager != null) { reactInstanceManager.onNewIntent(intent); } @@ -269,63 +212,9 @@ public class JitsiMeetView extends FrameLayout { * This is currently not mandatory. */ public static void onUserLeaveHint() { - sendEvent("onUserLeaveHint", null); + ReactInstanceManagerHolder.emitEvent("onUserLeaveHint", null); } - /** - * Starts a query for users to invite to the conference. Results will be - * returned through the {@link InviteSearchController.InviteSearchControllerDelegate#onReceiveResults(InviteSearchController, List, String)} - * method. - * - * @param query {@code String} to use for the query - */ - public static void onInviteQuery(String query, String inviteSearchControllerScope) { - WritableNativeMap params = new WritableNativeMap(); - params.putString("query", query); - params.putString("inviteScope", inviteSearchControllerScope); - sendEvent("performQueryAction", params); - } - - /** - * Sends JavaScript event to submit invitations to the given item ids - * - * @param selectedItems a WritableArray of WritableNativeMaps representing selected items. - * Each map representing a selected item should match the data passed - * back in the return from a query. - */ - public static void submitSelectedItems(WritableArray selectedItems, String inviteSearchControllerScope) { - WritableNativeMap params = new WritableNativeMap(); - params.putArray("selectedItems", selectedItems); - params.putString("inviteScope", inviteSearchControllerScope); - sendEvent("performSubmitInviteAction", params); - } - - /** - * Helper function to send an event to JavaScript. - * - * @param eventName {@code String} containing the event name. - * @param data {@code Object} optional ancillary data for the event. - */ - private static void sendEvent( - String eventName, - @Nullable Object data) { - if (reactInstanceManager != null) { - ReactContext reactContext - = reactInstanceManager.getCurrentReactContext(); - if (reactContext != null) { - reactContext - .getJSModule( - DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(eventName, data); - } - } - } - - /** - * Whether user invitation is enabled. - */ - private boolean addPeopleEnabled; - /** * The default base {@code URL} used to join a conference when a partial URL * (e.g. a room name only) is specified to {@link #loadURLString(String)} or @@ -333,11 +222,6 @@ public class JitsiMeetView extends FrameLayout { */ private URL defaultURL; - /** - * Whether the ability to add users by phone number is enabled. - */ - private boolean dialOutEnabled; - /** * The unique identifier of this {@code JitsiMeetView} within the process * for the purposes of {@link ExternalAPI}. The name scope was inspired by @@ -346,6 +230,12 @@ public class JitsiMeetView extends FrameLayout { */ private final String externalAPIScope; + /** + * The entry point into the invite feature of Jitsi Meet. The Java + * counterpart of the JavaScript {@code InviteButton}. + */ + private final InviteController inviteController; + /** * {@link JitsiMeetViewListener} instance for reporting events occurring in * Jitsi Meet. @@ -374,15 +264,18 @@ public class JitsiMeetView extends FrameLayout { setBackgroundColor(BACKGROUND_COLOR); - if (reactInstanceManager == null) { - initReactInstanceManager(((Activity) context).getApplication()); - } + ReactInstanceManagerHolder.initReactInstanceManager( + ((Activity) context).getApplication()); // Hook this JitsiMeetView into ExternalAPI. externalAPIScope = UUID.randomUUID().toString(); synchronized (views) { views.add(this); } + + // The entry point into the invite feature of Jitsi Meet. The Java + // counterpart of the JavaScript InviteButton. + inviteController = new InviteController(externalAPIScope); } /** @@ -413,6 +306,19 @@ public class JitsiMeetView extends FrameLayout { return defaultURL; } + /** + * Gets the {@link InviteController} which represents the entry point into + * the invite feature of Jitsi Meet and is the Java counterpart of the + * JavaScript {@code InviteButton}. + * + * @return the {@link InviteController} which represents the entry point + * into the invite feature of Jitsi Meet and is the Java counterpart of the + * JavaScript {@code InviteButton} + */ + public InviteController getInviteController() { + return inviteController; + } + /** * Gets the {@link JitsiMeetViewListener} set on this {@code JitsiMeetView}. * @@ -483,6 +389,18 @@ public class JitsiMeetView extends FrameLayout { // externalAPIScope props.putString("externalAPIScope", externalAPIScope); + // inviteController + InviteController inviteController = getInviteController(); + + if (inviteController != null) { + props.putBoolean( + "addPeopleEnabled", + inviteController.isAddPeopleEnabled()); + props.putBoolean( + "dialOutEnabled", + inviteController.isDialOutEnabled()); + } + // pictureInPictureEnabled props.putBoolean( "pictureInPictureEnabled", @@ -496,9 +414,6 @@ public class JitsiMeetView extends FrameLayout { // welcomePageEnabled props.putBoolean("welcomePageEnabled", welcomePageEnabled); - props.putBoolean("addPeopleEnabled", addPeopleEnabled); - props.putBoolean("dialOutEnabled", dialOutEnabled); - // XXX The method loadURLObject: 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 @@ -513,7 +428,7 @@ public class JitsiMeetView extends FrameLayout { if (reactRootView == null) { reactRootView = new ReactRootView(getContext()); reactRootView.startReactApplication( - reactInstanceManager, + ReactInstanceManagerHolder.getReactInstanceManager(), "App", props); reactRootView.setBackgroundColor(BACKGROUND_COLOR); @@ -580,18 +495,6 @@ public class JitsiMeetView extends FrameLayout { } } - /** - * Sets whether the ability to add users to the call is enabled. - * If this is enabled, an add user button will appear on the {@link JitsiMeetView}. - * If enabled, and the user taps the add user button, - * {@link JitsiMeetViewListener#launchNativeInvite(Map)} will be called. - * - * @param addPeopleEnabled {@code true} to enable the add people button; otherwise, {@code false} - */ - public void setAddPeopleEnabled(boolean addPeopleEnabled) { - this.addPeopleEnabled = addPeopleEnabled; - } - /** * Sets the default base {@code URL} used to join a conference when a * partial URL (e.g. a room name only) is specified to @@ -605,18 +508,6 @@ public class JitsiMeetView extends FrameLayout { this.defaultURL = defaultURL; } - /** - * Sets whether the ability to add phone numbers to the call is enabled. - * Must be enabled along with {@link #setAddPeopleEnabled(boolean)} to - * be effective. - * - * @param dialOutEnabled {@code true} to enable the ability to add - * phone numbers to the call; otherwise, {@code false} - */ - public void setDialOutEnabled(boolean dialOutEnabled) { - this.dialOutEnabled = dialOutEnabled; - } - /** * Sets a specific {@link JitsiMeetViewListener} on this * {@code JitsiMeetView}. diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewAdapter.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewAdapter.java index 7ae1175c7..bde479354 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewAdapter.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewAdapter.java @@ -46,8 +46,4 @@ public abstract class JitsiMeetViewAdapter implements JitsiMeetViewListener { @Override public void onLoadConfigError(Map data) { } - - @Override - public void launchNativeInvite(InviteSearchController inviteSearchController) { - } } diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewListener.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewListener.java index 380ab75ed..52e48037e 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewListener.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewListener.java @@ -59,16 +59,6 @@ public interface JitsiMeetViewListener { */ void onConferenceWillLeave(Map data); - /** - * Called when the add user button is tapped. - * - * @param inviteSearchController {@code InviteSearchController} scoped - * for this user invite flow. The {@code InviteSearchController} is used - * to start user queries and accepts an {@code InviteSearchControllerDelegate} - * for receiving user query responses. - */ - void launchNativeInvite(InviteSearchController inviteSearchController); - /** * Called when loading the main configuration file from the Jitsi Meet * deployment fails. diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactContextUtils.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactContextUtils.java new file mode 100644 index 000000000..da4fc35e8 --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactContextUtils.java @@ -0,0 +1,43 @@ +/* + * 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; + +import android.support.annotation.Nullable; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +public class ReactContextUtils { + public static boolean emitEvent( + ReactContext reactContext, + String eventName, + @Nullable Object data) { + if (reactContext == null) { + // XXX If no ReactContext is specified, emit through the + // ReactContext of ReactInstanceManager. ReactInstanceManager + // cooperates with ReactContextUtils i.e. ReactInstanceManager will + // not invoke ReactContextUtils without a ReactContext. + return ReactInstanceManagerHolder.emitEvent(eventName, data); + } + + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, data); + + return true; + } +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java new file mode 100644 index 000000000..2c88be6aa --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java @@ -0,0 +1,125 @@ +/* + * 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; + +import android.app.Application; +import android.support.annotation.Nullable; + +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.common.LifecycleState; + +import java.util.Arrays; +import java.util.List; + +public class ReactInstanceManagerHolder { + /** + * React Native bridge. The instance manager allows embedding applications + * to create multiple root views off the same JavaScript bundle. + */ + private static ReactInstanceManager reactInstanceManager; + + private static List createNativeModules( + ReactApplicationContext reactContext) { + return Arrays.asList( + new AndroidSettingsModule(reactContext), + new AppInfoModule(reactContext), + new AudioModeModule(reactContext), + new ExternalAPIModule(reactContext), + new PictureInPictureModule(reactContext), + new ProximityModule(reactContext), + new WiFiStatsModule(reactContext), + new org.jitsi.meet.sdk.invite.InviteModule(reactContext), + new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext) + ); + } + + /** + * Helper function to send an event to JavaScript. + * + * @param eventName {@code String} containing the event name. + * @param data {@code Object} optional ancillary data for the event. + */ + public static boolean emitEvent( + String eventName, + @Nullable Object data) { + ReactInstanceManager reactInstanceManager + = ReactInstanceManagerHolder.getReactInstanceManager(); + + if (reactInstanceManager != null) { + ReactContext reactContext + = reactInstanceManager.getCurrentReactContext(); + + return + reactContext != null + && ReactContextUtils.emitEvent( + reactContext, + eventName, + data); + } + + return false; + } + + static ReactInstanceManager getReactInstanceManager() { + return reactInstanceManager; + } + + /** + * 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 {@code ReactRootView} instances will be tied to the one and + * only {@code ReactInstanceManager}. + * + * @param application {@code Application} instance which is running. + */ + static void initReactInstanceManager(Application application) { + if (reactInstanceManager != null) { + return; + } + + reactInstanceManager + = ReactInstanceManager.builder() + .setApplication(application) + .setBundleAssetName("index.android.bundle") + .setJSMainModulePath("index.android") + .addPackage(new com.calendarevents.CalendarEventsPackage()) + .addPackage(new com.corbt.keepawake.KCKeepAwakePackage()) + .addPackage(new com.facebook.react.shell.MainReactPackage()) + .addPackage(new com.i18n.reactnativei18n.ReactNativeI18n()) + .addPackage(new com.oblador.vectoricons.VectorIconsPackage()) + .addPackage(new com.ocetnik.timer.BackgroundTimerPackage()) + .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage()) + .addPackage(new com.RNFetchBlob.RNFetchBlobPackage()) + .addPackage(new com.rnimmersive.RNImmersivePackage()) + .addPackage(new com.zmxv.RNSound.RNSoundPackage()) + .addPackage(new ReactPackageAdapter() { + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + return + ReactInstanceManagerHolder.createNativeModules( + reactContext); + } + }) + .setUseDeveloperSupport(BuildConfig.DEBUG) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + } +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleController.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleController.java new file mode 100644 index 000000000..8ef5be823 --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleController.java @@ -0,0 +1,204 @@ +/* + * 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.invite; + +import android.util.Log; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Controller object used by native code to query and submit user selections for the user invitation flow. + */ +public class AddPeopleController { + + /** + * The AddPeopleControllerListener for this controller, used to pass query + * results back to the native code that initiated the query. + */ + private AddPeopleControllerListener listener; + + /** + * Local cache of search query results. Used to re-hydrate the list + * of selected items based on their ids passed to inviteById + * in order to pass the full item maps back to the JitsiMeetView during submission. + */ + private final Map items = new HashMap<>(); + + private final WeakReference owner; + + private final WeakReference reactContext; + /** + * Randomly generated UUID, used for identification in the InviteModule + */ + private final String uuid = UUID.randomUUID().toString(); + + public AddPeopleController( + InviteController owner, + ReactApplicationContext reactContext) { + this.owner = new WeakReference<>(owner); + this.reactContext = new WeakReference<>(reactContext); + } + + /** + * Cancel the invitation flow and free memory allocated to the + * AddPeopleController. After calling this method, this object is invalid - + * a new AddPeopleController will be passed to the caller through + * beginAddPeople. + */ + public void endAddPeople() { + InviteController owner = this.owner.get(); + + if (owner != null) { + owner.endAddPeople(this); + } + } + + /** + * + * @return the AddPeopleControllerListener for this controller, used to pass + * query results back to the native code that initiated the query. + */ + public AddPeopleControllerListener getListener() { + return listener; + } + + final ReactApplicationContext getReactApplicationContext() { + return reactContext.get(); + } + + /** + * + * @return the unique identifier for this AddPeopleController + */ + public String getUuid() { + return uuid; + } + + /** + * Send invites to selected users based on their item ids + * + * @param ids + */ + public void inviteById(List ids) { + InviteController owner = this.owner.get(); + + if (owner != null) { + WritableArray invitees = new WritableNativeArray(); + + for(int i = 0, size = ids.size(); i < size; i++) { + String id = ids.get(i); + + if(items.containsKey(id)) { + WritableNativeMap map = new WritableNativeMap(); + map.merge(items.get(ids)); + invitees.pushMap(map); + } else { + // If the id doesn't exist in the map, we can't do anything, + // so just skip it. + } + } + + owner.invite(this, invitees); + } + } + + void inviteSettled(ReadableArray failedInvitees) { + AddPeopleControllerListener listener = getListener(); + + if (listener != null) { + ArrayList> jFailedInvitees = new ArrayList<>(); + + for (int i = 0, size = failedInvitees.size(); i < size; ++i) { + jFailedInvitees.add(failedInvitees.getMap(i).toHashMap()); + } + + listener.inviteSettled(this, jFailedInvitees); + } + } + + /** + * Start a search for entities to invite with the given query. Results will + * be returned through the associated AddPeopleControllerListener's + * onReceiveResults method. + * + * @param query + */ + public void performQuery(String query) { + InviteController owner = this.owner.get(); + + if (owner != null) { + owner.performQuery(this, query); + } + } + + /** + * Caches results received by the search into a local map for use + * later when the items are submitted. Submission requires the full + * map of information, but only the IDs are returned back to the delegate. + * Using this map means we don't have to send the whole map back to the delegate. + * + * @param results + * @param query + */ + void receivedResultsForQuery(ReadableArray results, String query) { + AddPeopleControllerListener listener = getListener(); + + if (listener != null) { + List> jvmResults = new ArrayList<>(); + + // cache results for use in submission later + // convert to jvm array + for(int i = 0; i < results.size(); i++) { + ReadableMap map = results.getMap(i); + + if(map.hasKey("id")) { + items.put(map.getString("id"), map); + } else if(map.hasKey("type") && map.getString("type").equals("phone") && map.hasKey("number")) { + items.put(map.getString("number"), map); + } else { + Log.w("AddPeopleController", "Received result without id and that was not a phone number, so not adding it to suggestions: " + map); + } + + jvmResults.add(map.toHashMap()); + } + + listener.onReceiveResults(this, jvmResults, query); + } + } + + /** + * Sets the AddPeopleControllerListener for this controller, used to pass + * query results back to the native code that initiated the query. + * + * @param listener + */ + public void setListener(AddPeopleControllerListener listener) { + this.listener = listener; + } +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleControllerListener.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleControllerListener.java new file mode 100644 index 000000000..8d72c0cc8 --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/AddPeopleControllerListener.java @@ -0,0 +1,49 @@ +/* + * 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.invite; + +import java.util.List; +import java.util.Map; + +public interface AddPeopleControllerListener { + /** + * Called when results are received for a query called through AddPeopleController.query() + * + * @param addPeopleController + * @param results a List of Map objects that represent items returned by the query. + * The object at key "type" describes the type of item: "user", "videosipgw" (conference room), or "phone". + * "user" types have properties at "id", "name", and "avatar" + * "videosipgw" types have properties at "id" and "name" + * "phone" types have properties at "number", "title", "and "subtitle" + * @param query the query that generated the given results + */ + void onReceiveResults(AddPeopleController addPeopleController, List> results, String query); + + /** + * Called when the call to {@link AddPeopleController#inviteById(List)} completes, but the + * invitation fails for one or more of the selected items. + * + * @param addPeopleController the active {@link AddPeopleController} for this invite flow. This object + * should be cleaned up by calling {@link AddPeopleController#endAddPeople()} if + * the user exits the invite flow. Otherwise, it can stay active if the user + * will attempt to invite + * @param failedInvitees a {@code List} of {@code Map} dictionaries that represent the + * invitations that failed. The data type of the objects is identical to the results + * returned in onReceiveResuls. + */ + void inviteSettled(AddPeopleController addPeopleController, List> failedInvitees); +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteController.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteController.java new file mode 100644 index 000000000..0d74f9795 --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteController.java @@ -0,0 +1,265 @@ +/* + * 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.invite; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableNativeMap; + +import org.jitsi.meet.sdk.ReactContextUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +/** + * Represents the entry point into the invite feature of Jitsi Meet and is the + * Java counterpart of the JavaScript {@code InviteButton}. + */ +public class InviteController { + private AddPeopleController addPeopleController; + + /** + * Whether adding/inviting people by name (as opposed to phone number) is + * enabled. + */ + private Boolean addPeopleEnabled; + + /** + * Whether adding/inviting people by phone number (as opposed to name) is + * enabled. + */ + private Boolean dialOutEnabled; + + private final String externalAPIScope; + + private InviteControllerListener listener; + + public InviteController(String externalAPIScope) { + this.externalAPIScope = externalAPIScope; + } + + public InviteControllerListener getListener() { + return listener; + } + + void beginAddPeople(ReactApplicationContext reactContext) { + InviteControllerListener listener = getListener(); + + if (listener != null) { + // XXX For the sake of simplicity and in order to reduce the risk of + // memory leaks, allow a single AddPeopleController at a time. + AddPeopleController addPeopleController = this.addPeopleController; + + if (addPeopleController != null) { + return; + } + + // Initialize a new AddPeopleController to represent the click/tap + // on the InviteButton and notify the InviteControllerListener + // about the event. + addPeopleController = new AddPeopleController(this, reactContext); + + boolean success = false; + + this.addPeopleController = addPeopleController; + try { + listener.beginAddPeople(addPeopleController); + success = true; + } finally { + if (!success) { + endAddPeople(addPeopleController); + } + } + } + } + + void endAddPeople(AddPeopleController addPeopleController) { + if (this.addPeopleController == addPeopleController) { + this.addPeopleController = null; + } + } + + /** + * Sends JavaScript event to submit invitations to the given item ids + * + * @param invitees a WritableArray of WritableNativeMaps representing + * selected items. Each map representing a selected item should match the + * data passed back in the return from a query. + */ + boolean invite( + AddPeopleController addPeopleController, + WritableArray invitees) { + return + invite( + addPeopleController.getUuid(), + addPeopleController.getReactApplicationContext(), + invitees); + } + + public Future>> invite( + final List> invitees) { + final boolean inviteBegan + = invite( + UUID.randomUUID().toString(), + /* reactContext */ null, + Arguments.makeNativeArray(invitees)); + FutureTask futureTask + = new FutureTask(new Callable() { + @Override + public List> call() { + if (inviteBegan) { + // TODO Complete the returned Future when the invite + // settles. + return Collections.emptyList(); + } else { + // The invite failed to even begin so report that all + // invitees failed. + return invitees; + } + } + }); + + // If the invite failed to even begin, complete the returned Future + // already and the Future implementation will report that all invitees + // failed. + if (!inviteBegan) { + futureTask.run(); + } + + return futureTask; + } + + private boolean invite( + String addPeopleControllerScope, + ReactContext reactContext, + WritableArray invitees) { + WritableNativeMap data = new WritableNativeMap(); + + data.putString("addPeopleControllerScope", addPeopleControllerScope); + data.putString("externalAPIScope", externalAPIScope); + data.putArray("invitees", invitees); + + return + ReactContextUtils.emitEvent( + reactContext, + "org.jitsi.meet:features/invite#invite", + data); + } + + void inviteSettled( + String addPeopleControllerScope, + ReadableArray failedInvitees) { + AddPeopleController addPeopleController = this.addPeopleController; + + if (addPeopleController != null + && addPeopleController.getUuid().equals( + addPeopleControllerScope)) { + try { + addPeopleController.inviteSettled(failedInvitees); + } finally { + if (failedInvitees.size() == 0) { + endAddPeople(addPeopleController); + } + } + } + } + + public boolean isAddPeopleEnabled() { + Boolean b = this.addPeopleEnabled; + + return + (b == null || b.booleanValue()) ? (getListener() != null) : false; + } + + public boolean isDialOutEnabled() { + Boolean b = this.dialOutEnabled; + + return + (b == null || b.booleanValue()) ? (getListener() != null) : false; + } + + /** + * Starts a query for users to invite to the conference. Results will be + * returned through the {@link AddPeopleControllerListener#onReceiveResults(AddPeopleController, List, String)} + * method. + * + * @param query {@code String} to use for the query + */ + void performQuery(AddPeopleController addPeopleController, String query) { + WritableNativeMap params = new WritableNativeMap(); + + params.putString("externalAPIScope", externalAPIScope); + params.putString("addPeopleControllerScope", addPeopleController.getUuid()); + params.putString("query", query); + ReactContextUtils.emitEvent( + addPeopleController.getReactApplicationContext(), + "org.jitsi.meet:features/invite#performQuery", + params); + } + + void receivedResultsForQuery( + String addPeopleControllerScope, + String query, + ReadableArray results) { + AddPeopleController addPeopleController = this.addPeopleController; + + if (addPeopleController != null + && addPeopleController.getUuid().equals( + addPeopleControllerScope)) { + addPeopleController.receivedResultsForQuery(results, query); + } + } + + /** + * Sets whether the ability to add users to the call is enabled. If this is + * enabled, an add user button will appear on the {@link JitsiMeetView}. If + * enabled, and the user taps the add user button, + * {@link InviteControllerListener#beginAddPeople(AddPeopleController)} + * will be called. + * + * @param addPeopleEnabled {@code true} to enable the add people button; + * otherwise, {@code false} + */ + public void setAddPeopleEnabled(boolean addPeopleEnabled) { + this.addPeopleEnabled = Boolean.valueOf(addPeopleEnabled); + } + + /** + * Sets whether the ability to add phone numbers to the call is enabled. + * Must be enabled along with {@link #setAddPeopleEnabled(boolean)} to be + * effective. + * + * @param dialOutEnabled {@code true} to enable the ability to add phone + * numbers to the call; otherwise, {@code false} + */ + public void setDialOutEnabled(boolean dialOutEnabled) { + this.dialOutEnabled = Boolean.valueOf(dialOutEnabled); + } + + public void setListener(InviteControllerListener listener) { + this.listener = listener; + } +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteControllerListener.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteControllerListener.java new file mode 100644 index 000000000..a3b866ae5 --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteControllerListener.java @@ -0,0 +1,29 @@ +/* + * 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.invite; + +public interface InviteControllerListener { + /** + * Called when the add user button is tapped. + * + * @param addPeopleController {@code AddPeopleController} scoped + * for this user invite flow. The {@code AddPeopleController} is used + * to start user queries and accepts an {@code AddPeopleControllerListener} + * for receiving user query responses. + */ + void beginAddPeople(AddPeopleController addPeopleController); +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java new file mode 100644 index 000000000..0a9957bcc --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/invite/InviteModule.java @@ -0,0 +1,119 @@ +/* + * 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.invite; + +import android.util.Log; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; + +import org.jitsi.meet.sdk.JitsiMeetView; + +/** + * Implements the react-native module of the feature invite. + */ +public class InviteModule extends ReactContextBaseJavaModule { + public InviteModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + /** + * Signals that a click/tap has been performed on {@code InviteButton} and + * that the execution flow for adding/inviting people to the current + * conference/meeting is to begin + * + * @param externalAPIScope the unique identifier of the + * {@code JitsiMeetView} whose {@code InviteButton} was clicked/tapped. + */ + @ReactMethod + public void beginAddPeople(String externalAPIScope) { + InviteController inviteController + = findInviteControllerByExternalAPIScope(externalAPIScope); + + if (inviteController != null) { + inviteController.beginAddPeople(getReactApplicationContext()); + } + } + + private InviteController findInviteControllerByExternalAPIScope( + String externalAPIScope) { + JitsiMeetView view + = JitsiMeetView.findViewByExternalAPIScope(externalAPIScope); + + return view == null ? null : view.getInviteController(); + } + + @Override + public String getName() { + return "Invite"; + } + + /** + * Callback for invitation failures + * + * @param failedInvitees the items for which the invitation failed + * @param addPeopleControllerScope a string that represents a connection to a specific AddPeopleController + */ + @ReactMethod + public void inviteSettled( + String externalAPIScope, + String addPeopleControllerScope, + ReadableArray failedInvitees) { + InviteController inviteController + = findInviteControllerByExternalAPIScope(externalAPIScope); + + if (inviteController == null) { + Log.w( + "InviteModule", + "Invite settled, but failed to find active controller to notify"); + } else { + inviteController.inviteSettled( + addPeopleControllerScope, + failedInvitees); + } + } + + /** + * Callback for results received from the JavaScript invite search call + * + * @param results the results in a ReadableArray of ReadableMap objects + * @param query the query associated with the search + * @param addPeopleControllerScope a string that represents a connection to a specific AddPeopleController + */ + @ReactMethod + public void receivedResults( + String externalAPIScope, + String addPeopleControllerScope, + String query, + ReadableArray results) { + InviteController inviteController + = findInviteControllerByExternalAPIScope(externalAPIScope); + + if (inviteController == null) { + Log.w( + "InviteModule", + "Received results, but failed to find active controller to send results back"); + } else { + inviteController.receivedResultsForQuery( + addPeopleControllerScope, + query, + results); + } + } +} diff --git a/ios/app/src/ViewController.h b/ios/app/src/ViewController.h index 048427e56..20ea683bb 100644 --- a/ios/app/src/ViewController.h +++ b/ios/app/src/ViewController.h @@ -18,6 +18,6 @@ #import -@interface ViewController : UIViewController +@interface ViewController : UIViewController @end diff --git a/ios/app/src/ViewController.m b/ios/app/src/ViewController.m index 0beb292d2..587028166 100644 --- a/ios/app/src/ViewController.m +++ b/ios/app/src/ViewController.m @@ -27,12 +27,25 @@ JitsiMeetView *view = (JitsiMeetView *) self.view; +#ifdef DEBUG + view.delegate = self; + + // inviteController + InviteController *inviteController = view.inviteController; + + //inviteController.addPeopleEnabled = TRUE; + //inviteController.dialOutEnabled = TRUE; + inviteController.delegate = self; + +#endif // #ifdef DEBUG + // 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 loadURL:nil]; } @@ -68,6 +81,17 @@ void _onJitsiMeetViewDelegateEvent(NSString *name, NSDictionary *data) { _onJitsiMeetViewDelegateEvent(@"LOAD_CONFIG_ERROR", data); } -#endif +- (void)beginAddPeople:(AddPeopleController *)addPeopleController { + NSLog( + @"[%s:%d] InviteControllerDelegate %s", + __FILE__, __LINE__, __FUNCTION__); + + // XXX Explicitly invoke endAddPeople on addPeopleController; otherwise, it + // is going to be memory-leaked in the associated InviteController and no + // subsequent InviteButton clicks/taps will be delivered. + [addPeopleController endAddPeople]; +} + +#endif // #ifdef DEBUG @end diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 5e0005fc1..db1d2006f 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -29,8 +29,13 @@ 0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; }; 75635B0A20751D6D00F29C9F /* joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0820751D6D00F29C9F /* joined.wav */; }; 75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; }; - 412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 412BF89C206AA66F0053B9E5 /* InviteSearch.m */; }; - 412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = 412BF89E206AA82F0053B9E5 /* InviteSearch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B386B85720981A75000DEF7A /* InviteController.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85020981A74000DEF7A /* InviteController.m */; }; + B386B85820981A75000DEF7A /* AddPeopleController.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85120981A74000DEF7A /* AddPeopleController.m */; }; + B386B85920981A75000DEF7A /* AddPeopleControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85220981A74000DEF7A /* AddPeopleControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B386B85A20981A75000DEF7A /* AddPeopleController.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85320981A74000DEF7A /* AddPeopleController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B386B85B20981A75000DEF7A /* InviteControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85420981A74000DEF7A /* InviteControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B386B85C20981A75000DEF7A /* InviteController.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85520981A75000DEF7A /* InviteController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B386B85D20981A75000DEF7A /* Invite.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85620981A75000DEF7A /* Invite.m */; }; C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; }; C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; }; C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; }; @@ -44,6 +49,9 @@ 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetView.m; sourceTree = ""; }; 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewDelegate.h; sourceTree = ""; }; 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = ""; }; + 0B6F414F20987DE600FF6789 /* Invite+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Invite+Private.h"; sourceTree = ""; }; + 0B6F41502098840600FF6789 /* InviteController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "InviteController+Private.h"; sourceTree = ""; }; + 0B6F4151209884E500FF6789 /* AddPeopleController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AddPeopleController+Private.h"; sourceTree = ""; }; 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchOptions.m; sourceTree = ""; }; 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 = ""; }; @@ -64,10 +72,15 @@ 0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = ""; }; 75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = ""; }; - 412BF89C206AA66F0053B9E5 /* InviteSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InviteSearch.m; sourceTree = ""; }; - 412BF89E206AA82F0053B9E5 /* InviteSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InviteSearch.h; sourceTree = ""; }; 98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = ""; }; 9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = ""; }; + B386B85020981A74000DEF7A /* InviteController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InviteController.m; sourceTree = ""; }; + B386B85120981A74000DEF7A /* AddPeopleController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AddPeopleController.m; sourceTree = ""; }; + B386B85220981A74000DEF7A /* AddPeopleControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddPeopleControllerDelegate.h; sourceTree = ""; }; + B386B85320981A74000DEF7A /* AddPeopleController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddPeopleController.h; sourceTree = ""; }; + B386B85420981A74000DEF7A /* InviteControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InviteControllerDelegate.h; sourceTree = ""; }; + B386B85520981A75000DEF7A /* InviteController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InviteController.h; sourceTree = ""; }; + B386B85620981A75000DEF7A /* Invite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Invite.m; sourceTree = ""; }; C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = ""; }; C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = ""; }; C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = ""; }; @@ -125,12 +138,11 @@ 0BD906E71EC0C00300C8C18E /* src */ = { isa = PBXGroup; children = ( + B386B84F20981A11000DEF7A /* invite */, C6A3426B204F127900E062DD /* picture-in-picture */, 0BCA495C1EC4B6C600B793EE /* AudioMode.m */, 0BB9AD7C1F60356D001C08DB /* AppInfo.m */, 0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */, - 412BF89E206AA82F0053B9E5 /* InviteSearch.h */, - 412BF89C206AA66F0053B9E5 /* InviteSearch.m */, 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */, 0BD906E91EC0C00300C8C18E /* Info.plist */, 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */, @@ -160,6 +172,23 @@ name = Frameworks; sourceTree = ""; }; + B386B84F20981A11000DEF7A /* invite */ = { + isa = PBXGroup; + children = ( + B386B85320981A74000DEF7A /* AddPeopleController.h */, + B386B85120981A74000DEF7A /* AddPeopleController.m */, + 0B6F4151209884E500FF6789 /* AddPeopleController+Private.h */, + B386B85220981A74000DEF7A /* AddPeopleControllerDelegate.h */, + B386B85620981A75000DEF7A /* Invite.m */, + 0B6F414F20987DE600FF6789 /* Invite+Private.h */, + B386B85520981A75000DEF7A /* InviteController.h */, + B386B85020981A74000DEF7A /* InviteController.m */, + 0B6F41502098840600FF6789 /* InviteController+Private.h */, + B386B85420981A74000DEF7A /* InviteControllerDelegate.h */, + ); + path = invite; + sourceTree = ""; + }; C5E72ADFC30ED96F9B35F076 /* Pods */ = { isa = PBXGroup; children = ( @@ -185,11 +214,14 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + B386B85C20981A75000DEF7A /* InviteController.h in Headers */, + B386B85B20981A75000DEF7A /* InviteControllerDelegate.h in Headers */, C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */, - 412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */, 0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */, + B386B85920981A75000DEF7A /* AddPeopleControllerDelegate.h in Headers */, 0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */, 0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */, + B386B85A20981A75000DEF7A /* AddPeopleController.h in Headers */, 0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -350,11 +382,13 @@ 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */, 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */, 0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */, + B386B85D20981A75000DEF7A /* Invite.m in Sources */, 0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */, C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */, + B386B85720981A75000DEF7A /* InviteController.m in Sources */, + B386B85820981A75000DEF7A /* AddPeopleController.m in Sources */, 0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */, 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */, - 412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */, 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */, 0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */, diff --git a/ios/sdk/src/InviteSearch.h b/ios/sdk/src/InviteSearch.h deleted file mode 100644 index 95f29a8d8..000000000 --- a/ios/sdk/src/InviteSearch.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright @ 2018-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. - */ - -@class InviteSearchController; - -@protocol InviteSearchControllerDelegate - -/** - * Called when an InviteSearchController has results for a query that was previously provided. - */ -- (void)inviteSearchController:(InviteSearchController * _Nonnull)controller - didReceiveResults:(NSArray * _Nonnull)results - forQuery:(NSString * _Nonnull)query; - -/** - * Called when all invitations were sent successfully. - */ -- (void)inviteDidSucceedForSearchController:(InviteSearchController * _Nonnull)searchController; - -/** - * Called when one or more invitations fails to send successfully. - */ -- (void)inviteDidFailForItems:(NSArray * _Nonnull)items - fromSearchController:(InviteSearchController * _Nonnull)searchController; - -@end - -@interface InviteSearchController: NSObject - -@property (nonatomic, nullable, weak) id delegate; - -- (void)performQuery:(NSString * _Nonnull)query; -- (void)cancelSearch; -- (void)submitSelectedItemIds:(NSArray * _Nonnull)ids; - -@end diff --git a/ios/sdk/src/InviteSearch.m b/ios/sdk/src/InviteSearch.m deleted file mode 100644 index 74e768aca..000000000 --- a/ios/sdk/src/InviteSearch.m +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright @ 2018-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 -#import -#import - -#import "JitsiMeetView+Private.h" - -#import "InviteSearch.h" - -// The events emitted/supported by InviteSearch: -static NSString * const InviteSearchPerformQueryAction = @"performQueryAction"; -static NSString * const InviteSearchPerformSubmitInviteAction = @"performSubmitInviteAction"; - - -@interface InviteSearch : RCTEventEmitter - -@end - - -@interface InviteSearchController () - -@property (nonatomic, readonly) NSString* _Nonnull identifier; -@property (nonatomic, strong) NSMutableDictionary* _Nonnull items; -@property (nonatomic, nullable, weak) InviteSearch* module; - -- (instancetype)initWithSearchModule:(InviteSearch *)module; - -- (void)didReceiveResults:(NSArray * _Nonnull)results - forQuery:(NSString * _Nonnull)query; - -- (void)inviteDidSucceed; - -- (void)inviteDidFailForItems:(NSArray *)items; - -@end - - -@implementation InviteSearch - -static NSMutableDictionary* searchControllers; - -RCT_EXTERN void RCTRegisterModule(Class); - -+ (void)load { - RCTRegisterModule(self); - - searchControllers = [[NSMutableDictionary alloc] init]; -} - -+ (NSString *)moduleName { - return @"InviteSearch"; -} - -- (NSArray *)supportedEvents { - return @[ - InviteSearchPerformQueryAction, - InviteSearchPerformSubmitInviteAction - ]; -} - -/** - * Calls the corresponding JitsiMeetView's delegate to request that the native - * invite search be presented. - * - * @param scope - */ -RCT_EXPORT_METHOD(launchNativeInvite:(NSString *)scope) { - // The JavaScript App needs to provide uniquely identifying information to - // the native module so that the latter may match the former to the native - // JitsiMeetView which hosts it. - JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope]; - - if (!view) { - return; - } - - id delegate = view.delegate; - - if (!delegate) { - return; - } - - if ([delegate respondsToSelector:@selector(launchNativeInviteForSearchController:)]) { - InviteSearchController* searchController = [searchControllers objectForKey:scope]; - if (!searchController) { - searchController = [self makeInviteSearchController]; - } - - [delegate launchNativeInviteForSearchController:searchController]; - } -} - -RCT_EXPORT_METHOD(inviteSucceeded:(NSString *)inviteScope) { - InviteSearchController* searchController = [searchControllers objectForKey:inviteScope]; - - [searchController inviteDidSucceed]; - - [searchControllers removeObjectForKey:inviteScope]; -} - -RCT_EXPORT_METHOD(inviteFailedForItems:(NSArray *)items inviteScope:(NSString *)inviteScope) { - InviteSearchController* searchController = [searchControllers objectForKey:inviteScope]; - - [searchController inviteDidFailForItems:items]; -} - -RCT_EXPORT_METHOD(receivedResults:(NSArray *)results forQuery:(NSString *)query inviteScope:(NSString *)inviteScope) { - - InviteSearchController* searchController = [searchControllers objectForKey:inviteScope]; - - [searchController didReceiveResults:results forQuery:query]; -} - -- (InviteSearchController *)makeInviteSearchController { - InviteSearchController* searchController = [[InviteSearchController alloc] initWithSearchModule:self]; - - [searchControllers setObject:searchController forKey:searchController.identifier]; - - return searchController; -} - -- (void)performQuery:(NSString * _Nonnull)query inviteScope:(NSString * _Nonnull)inviteScope { - [self sendEventWithName:InviteSearchPerformQueryAction body:@{ @"query": query, @"inviteScope": inviteScope }]; -} - -- (void)cancelSearchForInviteScope:(NSString * _Nonnull)inviteScope { - [searchControllers removeObjectForKey:inviteScope]; -} - -- (void)submitSelectedItems:(NSArray * _Nonnull)items inviteScope:(NSString * _Nonnull)inviteScope { - [self sendEventWithName:InviteSearchPerformSubmitInviteAction body:@{ @"selectedItems": items, @"inviteScope": inviteScope }]; -} - -@end - - -@implementation InviteSearchController - -- (instancetype)initWithSearchModule:(InviteSearch *)module { - self = [super init]; - if (self) { - _identifier = [[NSUUID UUID] UUIDString]; - - self.items = [[NSMutableDictionary alloc] init]; - self.module = module; - } - return self; -} - -- (void)performQuery:(NSString *)query { - [self.module performQuery:query inviteScope:self.identifier]; -} - -- (void)cancelSearch { - [self.module cancelSearchForInviteScope:self.identifier]; -} - -- (void)submitSelectedItemIds:(NSArray * _Nonnull)ids { - NSMutableArray* items = [[NSMutableArray alloc] init]; - - for (NSString* itemId in ids) { - id item = [self.items objectForKey:itemId]; - - if (item) { - [items addObject:item]; - } - } - - [self.module submitSelectedItems:items inviteScope:self.identifier]; -} - -- (void)didReceiveResults:(NSArray *)results forQuery:(NSString *)query { - for (NSDictionary* item in results) { - NSString* itemId = item[@"id"]; - NSString* itemType = item[@"type"]; - if (itemId) { - [self.items setObject:item forKey:itemId]; - } else if (itemType != nil && [itemType isEqualToString: @"phone"]) { - NSString* number = item[@"number"]; - if (number) { - [self.items setObject:item forKey:number]; - } - } - } - - [self.delegate inviteSearchController:self didReceiveResults:results forQuery:query]; -} - -- (void)inviteDidSucceed { - [self.delegate inviteDidSucceedForSearchController:self]; -} - -- (void)inviteDidFailForItems:(NSArray *)items { - if (!items) { - items = @[]; - } - [self.delegate inviteDidFailForItems:items fromSearchController:self]; -} - -@end diff --git a/ios/sdk/src/JitsiMeet.h b/ios/sdk/src/JitsiMeet.h index 53829e7b2..7223fb4bc 100644 --- a/ios/sdk/src/JitsiMeet.h +++ b/ios/sdk/src/JitsiMeet.h @@ -14,6 +14,12 @@ * limitations under the License. */ +// JitsiMeetView #import #import -#import + +// invite/ +#import +#import +#import +#import diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h index 043ea00a8..708f4b498 100644 --- a/ios/sdk/src/JitsiMeetView.h +++ b/ios/sdk/src/JitsiMeetView.h @@ -17,17 +17,16 @@ #import #import +#import "InviteController.h" #import "JitsiMeetViewDelegate.h" @interface JitsiMeetView : UIView -@property (nonatomic) BOOL addPeopleEnabled; - @property (copy, nonatomic, nullable) NSURL *defaultURL; @property (nonatomic, nullable, weak) id delegate; -@property (nonatomic) BOOL dialOutEnabled; +@property (nonatomic, readonly) InviteController *inviteController; @property (nonatomic) BOOL pictureInPictureEnabled; diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index a026821e4..53a38fba6 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -23,6 +23,8 @@ #import #import +#import "Invite+Private.h" +#import "InviteController+Private.h" #import "JitsiMeetView+Private.h" #import "RCTBridgeWrapper.h" @@ -268,12 +270,13 @@ static NSMapTable *views; props[@"defaultURL"] = [self.defaultURL absoluteString]; } - props[@"addPeopleEnabled"] = @(self.addPeopleEnabled); - props[@"dialOutEnabled"] = @(self.dialOutEnabled); props[@"externalAPIScope"] = externalAPIScope; props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled); props[@"welcomePageEnabled"] = @(self.welcomePageEnabled); + props[@"addPeopleEnabled"] = @(_inviteController.addPeopleEnabled); + props[@"dialOutEnabled"] = @(_inviteController.dialOutEnabled); + // 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) { @@ -405,10 +408,13 @@ static NSMapTable *views; }); // Hook this JitsiMeetView into ExternalAPI. - if (!externalAPIScope) { - externalAPIScope = [NSUUID UUID].UUIDString; - [views setObject:self forKey:externalAPIScope]; - } + externalAPIScope = [NSUUID UUID].UUIDString; + [views setObject:self forKey:externalAPIScope]; + + Invite *inviteModule = [bridgeWrapper.bridge moduleForName:@"Invite"]; + _inviteController + = [[InviteController alloc] initWithExternalAPIScope:externalAPIScope + andInviteModule:inviteModule]; // Set a background color which is in accord with the JavaScript and Android // parts of the application and causes less perceived visual flicker than diff --git a/ios/sdk/src/JitsiMeetViewDelegate.h b/ios/sdk/src/JitsiMeetViewDelegate.h index 1b13a9119..f13529e95 100644 --- a/ios/sdk/src/JitsiMeetViewDelegate.h +++ b/ios/sdk/src/JitsiMeetViewDelegate.h @@ -14,8 +14,6 @@ * limitations under the License. */ -@class InviteSearchController; - @protocol JitsiMeetViewDelegate @optional @@ -57,15 +55,6 @@ */ - (void)conferenceWillLeave:(NSDictionary *)data; - -/** - * Called when the invite button in the conference is tapped. - * - * The search controller provided can be used to query user search within the - * conference. - */ -- (void)launchNativeInviteForSearchController:(InviteSearchController *)searchController; - /** * Called when entering Picture-in-Picture is requested by the user. The app * should now activate its Picture-in-Picture implementation (and resize the diff --git a/ios/sdk/src/invite/AddPeopleController+Private.h b/ios/sdk/src/invite/AddPeopleController+Private.h new file mode 100644 index 000000000..04ee93da4 --- /dev/null +++ b/ios/sdk/src/invite/AddPeopleController+Private.h @@ -0,0 +1,33 @@ +/* + * Copyright @ 2018-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 "AddPeopleController.h" +#import "InviteController.h" + +@interface AddPeopleController () + +@property (nonatomic, strong) NSMutableDictionary* _Nonnull items; +@property (nonatomic, weak) InviteController *owner; +@property (nonatomic, readonly) NSString* _Nonnull uuid; + +- (instancetype)initWithOwner:(InviteController *)owner; + +- (void)inviteSettled:(NSArray *)failedInvitees; + +- (void)receivedResults:(NSArray * _Nonnull)results + forQuery:(NSString * _Nonnull)query; + +@end diff --git a/ios/sdk/src/invite/AddPeopleController.h b/ios/sdk/src/invite/AddPeopleController.h new file mode 100644 index 000000000..9ce8933d0 --- /dev/null +++ b/ios/sdk/src/invite/AddPeopleController.h @@ -0,0 +1,31 @@ +/* + * Copyright @ 2018-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 + +#import "AddPeopleControllerDelegate.h" + +@interface AddPeopleController: NSObject + +@property (nonatomic, nullable, weak) id delegate; + +- (void)endAddPeople; + +- (void)inviteById:(NSArray * _Nonnull)ids; + +- (void)performQuery:(NSString * _Nonnull)query; + +@end diff --git a/ios/sdk/src/invite/AddPeopleController.m b/ios/sdk/src/invite/AddPeopleController.m new file mode 100644 index 000000000..b8daad2b7 --- /dev/null +++ b/ios/sdk/src/invite/AddPeopleController.m @@ -0,0 +1,79 @@ +/* + * Copyright @ 2018-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 "AddPeopleController+Private.h" +#import "InviteController+Private.h" + +@implementation AddPeopleController + +- (instancetype)initWithOwner:(InviteController *)owner { + self = [super init]; + if (self) { + _uuid = [[NSUUID UUID] UUIDString]; + _items = [[NSMutableDictionary alloc] init]; + _owner = owner; + } + return self; +} + +#pragma mark API + +- (void)endAddPeople { + [self.owner endAddPeopleForController:self]; +} + +- (void)inviteById:(NSArray * _Nonnull)ids { + NSMutableArray* invitees = [[NSMutableArray alloc] init]; + + for (NSString* itemId in ids) { + id invitee = [self.items objectForKey:itemId]; + + if (invitee) { + [invitees addObject:invitee]; + } + } + + [self.owner invite:invitees forController:self]; +} + +- (void)performQuery:(NSString *)query { + [self.owner performQuery:query forController:self]; +} + +#pragma mark Internal API, used to call the delegate and report to the user + +- (void)receivedResults:(NSArray *)results forQuery:(NSString *)query { + for (NSDictionary* item in results) { + NSString* itemId = item[@"id"]; + NSString* itemType = item[@"type"]; + if (itemId) { + [self.items setObject:item forKey:itemId]; + } else if (itemType != nil && [itemType isEqualToString: @"phone"]) { + NSString* number = item[@"number"]; + if (number) { + [self.items setObject:item forKey:number]; + } + } + } + + [self.delegate addPeopleController:self didReceiveResults:results forQuery:query]; +} + +- (void)inviteSettled:(NSArray *)failedInvitees { + [self.delegate inviteSettled:failedInvitees fromSearchController:self]; +} + +@end diff --git a/ios/sdk/src/invite/AddPeopleControllerDelegate.h b/ios/sdk/src/invite/AddPeopleControllerDelegate.h new file mode 100644 index 000000000..684c82f8a --- /dev/null +++ b/ios/sdk/src/invite/AddPeopleControllerDelegate.h @@ -0,0 +1,38 @@ +/* + * Copyright @ 2018-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 + +#import "AddPeopleController.h" + +@class AddPeopleController; + +@protocol AddPeopleControllerDelegate + +/** + * Called when an AddPeopleController has results for a query that was previously provided. + */ +- (void)addPeopleController:(AddPeopleController * _Nonnull)controller + didReceiveResults:(NSArray * _Nonnull)results + forQuery:(NSString * _Nonnull)query; + +/** + * TODO. + */ +- (void)inviteSettled:(NSArray * _Nonnull)failedInvitees + fromSearchController:(AddPeopleController * _Nonnull)addPeopleController; + +@end diff --git a/ios/sdk/src/invite/Invite+Private.h b/ios/sdk/src/invite/Invite+Private.h new file mode 100644 index 000000000..aa982e743 --- /dev/null +++ b/ios/sdk/src/invite/Invite+Private.h @@ -0,0 +1,30 @@ +/* + * Copyright @ 2018-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 +#import + +@interface Invite : RCTEventEmitter + +- (void) invite:(NSArray * _Nonnull)invitees + externalAPIScope:(NSString * _Nonnull)externalAPIScope + addPeopleControllerScope:(NSString * _Nonnull)addPeopleControllerScope; + +- (void) performQuery:(NSString * _Nonnull)query + externalAPIScope:(NSString * _Nonnull)externalAPIScope + addPeopleControllerScope:(NSString * _Nonnull)addPeopleControllerScope; + +@end diff --git a/ios/sdk/src/invite/Invite.m b/ios/sdk/src/invite/Invite.m new file mode 100644 index 000000000..50622d2a4 --- /dev/null +++ b/ios/sdk/src/invite/Invite.m @@ -0,0 +1,90 @@ +/* + * Copyright @ 2018-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 "Invite+Private.h" +#import "InviteController+Private.h" +#import "JitsiMeetView+Private.h" + +// The events emitted/supported by the Invite react-native module: +// +// XXX The event names are ridiculous on purpose. Even though iOS makes it look +// like it emits within the bounderies of a react-native module ony, it actually +// also emits through DeviceEventEmitter. (Of course, Android emits only through +// DeviceEventEmitter.) +static NSString * const InvitePerformQueryAction + = @"org.jitsi.meet:features/invite#performQuery"; +static NSString * const InvitePerformSubmitInviteAction + = @"org.jitsi.meet:features/invite#invite"; + +@implementation Invite + +RCT_EXPORT_MODULE(); + +- (NSArray *)supportedEvents { + return @[ + InvitePerformQueryAction, + InvitePerformSubmitInviteAction + ]; +} + +/** + * Calls the corresponding JitsiMeetView's delegate to request that the native + * invite search be presented. + * + * @param scope + */ +RCT_EXPORT_METHOD(beginAddPeople:(NSString *)externalAPIScope) { + JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:externalAPIScope]; + InviteController *controller = view.inviteController; + [controller beginAddPeople]; +} + +RCT_EXPORT_METHOD(inviteSettled:(NSString *)externalAPIScope + addPeopleControllerScope:(NSString *)addPeopleControllerScope + failedInvitees:(NSArray *)failedInvitees) { + JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:externalAPIScope]; + InviteController *controller = view.inviteController; + [controller inviteSettled:addPeopleControllerScope failedInvitees:failedInvitees]; +} + +RCT_EXPORT_METHOD(receivedResults:(NSString *)externalAPIScope + addPeopleControllerScope:(NSString *)addPeopleControllerScope + query:(NSString *)query + results:(NSArray *)results) { + JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:externalAPIScope]; + InviteController *controller = view.inviteController; + [controller receivedResults:addPeopleControllerScope query:query results:results]; +} + +- (void) invite:(NSArray * _Nonnull)invitees + externalAPIScope:(NSString * _Nonnull)externalAPIScope + addPeopleControllerScope:(NSString * _Nonnull) addPeopleControllerScope { + [self sendEventWithName:InvitePerformSubmitInviteAction + body:@{ @"addPeopleControllerScope": addPeopleControllerScope, + @"externalAPIScope": externalAPIScope, + @"invitees": invitees }]; +} + +- (void) performQuery:(NSString * _Nonnull)query + externalAPIScope:(NSString * _Nonnull)externalAPIScope + addPeopleControllerScope:(NSString * _Nonnull) addPeopleControllerScope { + [self sendEventWithName:InvitePerformQueryAction + body:@{ @"addPeopleControllerScope": addPeopleControllerScope, + @"externalAPIScope": externalAPIScope, + @"query": query }]; +} + +@end diff --git a/ios/sdk/src/invite/InviteController+Private.h b/ios/sdk/src/invite/InviteController+Private.h new file mode 100644 index 000000000..b77ecb3e6 --- /dev/null +++ b/ios/sdk/src/invite/InviteController+Private.h @@ -0,0 +1,50 @@ +/* + * Copyright @ 2018-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 "InviteController.h" + +#import "AddPeopleController.h" +#import "Invite+Private.h" + +@interface InviteController () + +@property (nonatomic, nullable) AddPeopleController *addPeopleController; + +@property (nonatomic) NSString *externalAPIScope; + +@property (nonatomic, nullable, weak) Invite *inviteModule; + +- (instancetype)initWithExternalAPIScope:(NSString * _Nonnull)externalAPIScope + andInviteModule:(Invite * _Nonnull)inviteModule; + +- (void)beginAddPeople; + +- (void)endAddPeopleForController:(AddPeopleController *)controller; + +- (void) invite:(NSArray *)invitees + forController:(AddPeopleController * _Nonnull)controller; + +- (void)inviteSettled:(NSString * _Nonnull)addPeopleControllerScope + failedInvitees:(NSArray *)failedInvitees; + +- (void)performQuery:(NSString * _Nonnull)query + forController:(AddPeopleController * _Nonnull)controller; + +- (void)receivedResults:(NSString * _Nonnull)addPeopleControllerScope + query:(NSString * _Nonnull)query + results:(NSArray *)results; + +@end diff --git a/ios/sdk/src/invite/InviteController.h b/ios/sdk/src/invite/InviteController.h new file mode 100644 index 000000000..68dbb9acd --- /dev/null +++ b/ios/sdk/src/invite/InviteController.h @@ -0,0 +1,32 @@ +/* + * 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 + +#import "InviteControllerDelegate.h" + +@interface InviteController : NSObject + +@property (nonatomic) BOOL addPeopleEnabled; + +@property (nonatomic) BOOL dialOutEnabled; + +@property (nonatomic, nullable, weak) id delegate; + +- (void) invite:(NSArray *)invitees + withCompletion:(void (^)(NSArray *failedInvitees))completion; + +@end diff --git a/ios/sdk/src/invite/InviteController.m b/ios/sdk/src/invite/InviteController.m new file mode 100644 index 000000000..499538d60 --- /dev/null +++ b/ios/sdk/src/invite/InviteController.m @@ -0,0 +1,118 @@ +/* + * 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 "InviteController+Private.h" +#import "AddPeopleController+Private.h" + +@implementation InviteController + +-(instancetype)initWithExternalAPIScope:(NSString * _Nonnull)externalAPIScope + andInviteModule:(Invite * _Nonnull)inviteModule { + self = [super init]; + if (self) { + self.externalAPIScope = externalAPIScope; + self.inviteModule = inviteModule; + } + + return self; +} + +-(void)beginAddPeople { + if (_delegate == nil) { + return; + } + + if (_addPeopleController != nil) { + return; + } + + _addPeopleController = [[AddPeopleController alloc] initWithOwner:self]; + + @try { + if (self.delegate + && [self.delegate respondsToSelector:@selector(beginAddPeople:)]) { + [self.delegate beginAddPeople:_addPeopleController]; + } + } @catch (NSException *e) { + [self endAddPeopleForController:_addPeopleController]; + } +} + +-(void)endAddPeopleForController:(AddPeopleController *)controller { + if (self.addPeopleController == controller) { + self.addPeopleController = nil; + } +} + +#pragma mark Result handling + +- (void)inviteSettled:(NSString *)addPeopleControllerScope + failedInvitees:(NSArray *)failedInvitees { + AddPeopleController *controller = self.addPeopleController; + + if (controller != nil + && [controller.uuid isEqualToString:addPeopleControllerScope]) { + @try { + [controller inviteSettled:failedInvitees]; + } @finally { + if ([failedInvitees count] == 0) { + [self endAddPeopleForController:controller]; + } + } + } +} + +- (void)receivedResults:(NSString *)addPeopleControllerScope + query:(NSString *)query + results:(NSArray *)results { + AddPeopleController *controller = self.addPeopleController; + + if (controller != nil + && [controller.uuid isEqualToString:addPeopleControllerScope]) { + [controller receivedResults:results forQuery:query]; + } +} + +#pragma mark Use the Invite react-native module to emit the search / submission events + +- (void) invite:(NSArray *)invitees + forController:(AddPeopleController * _Nonnull)controller { + [self invite:invitees + forControllerScope:controller.uuid]; +} + +- (void) invite:(NSArray *)invitees + forControllerScope:(NSString * _Nonnull)controllerScope { + [self.inviteModule invite:invitees + externalAPIScope:self.externalAPIScope + addPeopleControllerScope:controllerScope]; +} + +- (void) invite:(NSArray *)invitees + withCompletion:(void (^)(NSArray *failedInvitees))completion { + // TODO Execute the specified completion block when the invite settles. + [self invite:invitees + forControllerScope:[[NSUUID UUID] UUIDString]]; +} + +- (void)performQuery:(NSString * _Nonnull)query + forController:(AddPeopleController * _Nonnull)controller { + [self.inviteModule performQuery:query + externalAPIScope:self.externalAPIScope + addPeopleControllerScope:controller.uuid]; +} + +@end diff --git a/ios/sdk/src/invite/InviteControllerDelegate.h b/ios/sdk/src/invite/InviteControllerDelegate.h new file mode 100644 index 000000000..1a1f643b4 --- /dev/null +++ b/ios/sdk/src/invite/InviteControllerDelegate.h @@ -0,0 +1,29 @@ +/* + * 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 "AddPeopleController.h" + +@protocol InviteControllerDelegate + +/** + * Called when the invite button in the conference is tapped. + * + * The search controller provided can be used to query user search within the + * conference. + */ +- (void)beginAddPeople:(AddPeopleController *)addPeopleController; + +@end diff --git a/react/features/invite/actionTypes.js b/react/features/invite/actionTypes.js index c57de29bb..df58e868d 100644 --- a/react/features/invite/actionTypes.js +++ b/react/features/invite/actionTypes.js @@ -1,3 +1,27 @@ +/** + * The type of the (redux) action which signals that a click/tap has been + * performed on {@link InviteButton} and that the execution flow for + * adding/inviting people to the current conference/meeting is to begin. + * + * { + * type: BEGIN_ADD_PEOPLE + * } + */ +export const BEGIN_ADD_PEOPLE = Symbol('BEGIN_ADD_PEOPLE'); + +/** + * The type of redux action to set the {@code EventEmitter} subscriptions + * utilized by the feature invite. + * + * { + * type: _SET_EMITTER_SUBSCRIPTIONS, + * emitterSubscriptions: Array|undefined + * } + * + * @protected + */ +export const _SET_EMITTER_SUBSCRIPTIONS = Symbol('_SET_EMITTER_SUBSCRIPTIONS'); + /** * The type of the action which signals an error occurred while requesting dial- * in numbers. diff --git a/react/features/invite/actions.js b/react/features/invite/actions.js index a4fb3d1f0..6db681510 100644 --- a/react/features/invite/actions.js +++ b/react/features/invite/actions.js @@ -1,11 +1,27 @@ // @flow import { + BEGIN_ADD_PEOPLE, UPDATE_DIAL_IN_NUMBERS_FAILED, UPDATE_DIAL_IN_NUMBERS_SUCCESS } from './actionTypes'; import { getDialInConferenceID, getDialInNumbers } from './functions'; +/** + * Creates a (redux) action to signal that a click/tap has been performed on + * {@link InviteButton} and that the execution flow for adding/inviting people + * to the current conference/meeting is to begin. + * + * @returns {{ + * type: BEGIN_ADD_PEOPLE + * }} + */ +export function beginAddPeople() { + return { + type: BEGIN_ADD_PEOPLE + }; +} + /** * Sends AJAX requests for dial-in numbers and conference ID. * diff --git a/react/features/invite/components/AddPeopleDialog.native.js b/react/features/invite/components/AddPeopleDialog.native.js index 14532cc1c..e69de29bb 100644 --- a/react/features/invite/components/AddPeopleDialog.native.js +++ b/react/features/invite/components/AddPeopleDialog.native.js @@ -1,3 +0,0 @@ -/** - * Created by ystamcheva on 8/6/17. - */ diff --git a/react/features/invite/components/AddPeopleDialog.web.js b/react/features/invite/components/AddPeopleDialog.web.js index ad2910bfd..d0d495e8c 100644 --- a/react/features/invite/components/AddPeopleDialog.web.js +++ b/react/features/invite/components/AddPeopleDialog.web.js @@ -71,12 +71,12 @@ class AddPeopleDialog extends Component<*, *> { /** * Whether or not to show Add People functionality. */ - enableAddPeople: PropTypes.bool, + addPeopleEnabled: PropTypes.bool, /** * Whether or not to show Dial Out functionality. */ - enableDialOut: PropTypes.bool, + dialOutEnabled: PropTypes.bool, /** * The function closing the dialog. @@ -187,21 +187,21 @@ class AddPeopleDialog extends Component<*, *> { * @returns {ReactElement} */ render() { - const { enableAddPeople, enableDialOut, t } = this.props; + const { addPeopleEnabled, dialOutEnabled, t } = this.props; let isMultiSelectDisabled = this.state.addToCallInProgress || false; let placeholder; let loadingMessage; let noMatches; - if (enableAddPeople && enableDialOut) { + if (addPeopleEnabled && dialOutEnabled) { loadingMessage = 'addPeople.loading'; noMatches = 'addPeople.noResults'; placeholder = 'addPeople.searchPeopleAndNumbers'; - } else if (enableAddPeople) { + } else if (addPeopleEnabled) { loadingMessage = 'addPeople.loadingPeople'; noMatches = 'addPeople.noResults'; placeholder = 'addPeople.searchPeople'; - } else if (enableDialOut) { + } else if (dialOutEnabled) { loadingMessage = 'addPeople.loadingNumber'; noMatches = 'addPeople.noValidNumbers'; placeholder = 'addPeople.searchNumbers'; @@ -481,8 +481,8 @@ class AddPeopleDialog extends Component<*, *> { */ _query(query = '') { const { - enableAddPeople, - enableDialOut, + addPeopleEnabled, + dialOutEnabled, _dialOutAuthUrl, _jwt, _peopleSearchQueryTypes, @@ -491,8 +491,8 @@ class AddPeopleDialog extends Component<*, *> { const options = { dialOutAuthUrl: _dialOutAuthUrl, - enableAddPeople, - enableDialOut, + addPeopleEnabled, + dialOutEnabled, jwt: _jwt, peopleSearchQueryTypes: _peopleSearchQueryTypes, peopleSearchUrl: _peopleSearchUrl @@ -609,7 +609,11 @@ function _mapStateToProps(state) { }; } -export default translate(connect(_mapStateToProps, { - hideDialog, - inviteVideoRooms })( - AddPeopleDialog)); +export default translate( + connect( + _mapStateToProps, + /* mapDispatchToProps */ { + hideDialog, + inviteVideoRooms + })( + AddPeopleDialog)); diff --git a/react/features/invite/components/InviteButton.native.js b/react/features/invite/components/InviteButton.native.js index 25a1f0a81..e8983b168 100644 --- a/react/features/invite/components/InviteButton.native.js +++ b/react/features/invite/components/InviteButton.native.js @@ -3,9 +3,22 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { launchNativeInvite } from '../../mobile/invite-search'; +import { beginShareRoom } from '../../share-room'; import { ToolbarButton } from '../../toolbox'; +import { beginAddPeople } from '../actions'; +import { isAddPeopleEnabled, isDialOutEnabled } from '../functions'; + +/** + * The indicator which determines (at bundle time) whether there should be a + * {@code ToolbarButton} in {@code Toolbox} to expose the functionality of the + * feature share-room in the user interface of the app. + * + * @private + * @type {boolean} + */ +const _SHARE_ROOM_TOOLBAR_BUTTON = true; + /** * The type of {@link EnterPictureInPictureToobarButton}'s React * {@code Component} props. @@ -13,21 +26,28 @@ import { ToolbarButton } from '../../toolbox'; type Props = { /** - * Indicates if the "Add to call" feature is available. + * Whether or not the feature to directly invite people into the + * conference is available. */ - enableAddPeople: boolean, + _addPeopleEnabled: boolean, /** - * Indicates if the "Dial out" feature is available. + * Whether or not the feature to dial out to number to join the + * conference is available. */ - enableDialOut: boolean, + _dialOutEnabled: boolean, /** * Launches native invite dialog. * * @protected */ - onLaunchNativeInvite: Function, + _onAddPeople: Function, + + /** + * Begins the UI procedure to share the conference/room URL. + */ + _onShareRoom: Function }; /** @@ -43,22 +63,32 @@ class InviteButton extends Component { */ render() { const { - enableAddPeople, - enableDialOut, - onLaunchNativeInvite, + _addPeopleEnabled, + _dialOutEnabled, + _onAddPeople, + _onShareRoom, ...props } = this.props; - if (!enableAddPeople && !enableDialOut) { - return null; + if (_SHARE_ROOM_TOOLBAR_BUTTON) { + return ( + + ); } - return ( - - ); + if (_addPeopleEnabled || _dialOutEnabled) { + return ( + + ); + } + + return null; } } @@ -68,13 +98,13 @@ class InviteButton extends Component { * * @param {Function} dispatch - The redux action {@code dispatch} function. * @returns {{ -* onLaunchNativeInvite + * _onAddPeople, + * _onShareRoom * }} * @private */ function _mapDispatchToProps(dispatch) { return { - /** * Launches native invite dialog. * @@ -82,10 +112,50 @@ function _mapDispatchToProps(dispatch) { * @returns {void} * @type {Function} */ - onLaunchNativeInvite() { - dispatch(launchNativeInvite()); + _onAddPeople() { + dispatch(beginAddPeople()); + }, + + /** + * Begins the UI procedure to share the conference/room URL. + * + * @private + * @returns {void} + * @type {Function} + */ + _onShareRoom() { + dispatch(beginShareRoom()); } }; } -export default connect(undefined, _mapDispatchToProps)(InviteButton); +/** + * Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component} + * props. + * + * @param {Object} state - The redux store/state. + * @private + * @returns {{ + * }} + */ +function _mapStateToProps(state) { + return { + /** + * Whether or not the feature to directly invite people into the + * conference is available. + * + * @type {boolean} + */ + _addPeopleEnabled: isAddPeopleEnabled(state), + + /** + * Whether or not the feature to dial out to number to join the + * conference is available. + * + * @type {boolean} + */ + _dialOutEnabled: isDialOutEnabled(state) + }; +} + +export default connect(_mapStateToProps, _mapDispatchToProps)(InviteButton); diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index d52bce50d..1531ad133 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -8,125 +8,6 @@ declare var interfaceConfig: Object; const logger = require('jitsi-meet-logger').getLogger(__filename); -/** - * Sends a GET request to obtain the conference ID necessary for identifying - * which conference to join after diaing the dial-in service. - * - * @param {string} baseUrl - The url for obtaining the conference ID (pin) for - * dialing into a conference. - * @param {string} roomName - The conference name to find the associated - * conference ID. - * @param {string} mucURL - In which MUC the conference exists. - * @returns {Promise} - The promise created by the request. - */ -export function getDialInConferenceID( - baseUrl: string, - roomName: string, - mucURL: string): Promise { - const conferenceIDURL = `${baseUrl}?conference=${roomName}@${mucURL}`; - - return doGetJSON(conferenceIDURL); -} - -/** - * Sends a GET request for phone numbers used to dial into a conference. - * - * @param {string} url - The service that returns confernce dial-in numbers. - * @returns {Promise} - The promise created by the request. The returned numbers - * may be an array of numbers or an object with countries as keys and arrays of - * phone number strings. - */ -export function getDialInNumbers(url: string): Promise<*> { - return doGetJSON(url); -} - -/** - * Sends a post request to an invite service. - * - * @param {string} inviteServiceUrl - The invite service that generates the - * invitation. - * @param {string} inviteUrl - The url to the conference. - * @param {string} jwt - The jwt token to pass to the search service. - * @param {Immutable.List} inviteItems - The list of the "user" or "room" - * type items to invite. - * @returns {Promise} - The promise created by the request. - */ -function invitePeopleAndChatRooms( // eslint-disable-line max-params - inviteServiceUrl: string, - inviteUrl: string, - jwt: string, - inviteItems: Array): Promise { - if (!inviteItems || inviteItems.length === 0) { - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - $.post( - `${inviteServiceUrl}?token=${jwt}`, - JSON.stringify({ - 'invited': inviteItems, - 'url': inviteUrl - }), - resolve, - 'json') - .fail((jqxhr, textStatus, error) => reject(error)); - }); -} - -/** - * Sends an ajax request to a directory service. - * - * @param {string} serviceUrl - The service to query. - * @param {string} jwt - The jwt token to pass to the search service. - * @param {string} text - Text to search. - * @param {Array} queryTypes - Array with the query types that will be - * executed - "conferenceRooms" | "user" | "room". - * @returns {Promise} - The promise created by the request. - */ -export function searchDirectory( // eslint-disable-line max-params - serviceUrl: string, - jwt: string, - text: string, - queryTypes: Array = [ 'conferenceRooms', 'user', 'room' ] -): Promise> { - const query = encodeURIComponent(text); - const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes)); - - return fetch(`${serviceUrl}?query=${query}&queryTypes=${ - queryTypesString}&jwt=${jwt}`) - .then(response => { - const jsonify = response.json(); - - if (response.ok) { - return jsonify; - } - - return jsonify - .then(result => Promise.reject(result)); - }) - .catch(error => { - logger.error( - 'Error searching directory:', error); - - return Promise.reject(error); - }); -} - -/** - * RegExp to use to determine if some text might be a phone number. - * - * @returns {RegExp} - */ -function isPhoneNumberRegex(): RegExp { - let regexString = '^[0-9+()-\\s]*$'; - - if (typeof interfaceConfig !== 'undefined') { - regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString; - } - - return new RegExp(regexString); -} - /** * Sends an ajax request to check if the phone number can be called. * @@ -135,7 +16,10 @@ function isPhoneNumberRegex(): RegExp { * @returns {Promise} - The promise created by the request. */ export function checkDialNumber( - dialNumber: string, dialOutAuthUrl: string): Promise { + dialNumber: string, + dialOutAuthUrl: string +): Promise { + if (!dialOutAuthUrl) { // no auth url, let's say it is valid const response = { @@ -155,6 +39,40 @@ export function checkDialNumber( }); } +/** + * Sends a GET request to obtain the conference ID necessary for identifying + * which conference to join after diaing the dial-in service. + * + * @param {string} baseUrl - The url for obtaining the conference ID (pin) for + * dialing into a conference. + * @param {string} roomName - The conference name to find the associated + * conference ID. + * @param {string} mucURL - In which MUC the conference exists. + * @returns {Promise} - The promise created by the request. + */ +export function getDialInConferenceID( + baseUrl: string, + roomName: string, + mucURL: string +): Promise { + + const conferenceIDURL = `${baseUrl}?conference=${roomName}@${mucURL}`; + + return doGetJSON(conferenceIDURL); +} + +/** + * Sends a GET request for phone numbers used to dial into a conference. + * + * @param {string} url - The service that returns confernce dial-in numbers. + * @returns {Promise} - The promise created by the request. The returned numbers + * may be an array of numbers or an object with countries as keys and arrays of + * phone number strings. + */ +export function getDialInNumbers(url: string): Promise<*> { + return doGetJSON(url); +} + /** * Removes all non-numeric characters from a string. * @@ -180,12 +98,12 @@ export type GetInviteResultsOptions = { /** * Whether or not to search for people. */ - enableAddPeople: boolean, + addPeopleEnabled: boolean, /** * Whether or not to check phone numbers. */ - enableDialOut: boolean, + dialOutEnabled: boolean, /** * Array with the query types that will be executed - @@ -214,13 +132,15 @@ export type GetInviteResultsOptions = { */ export function getInviteResultsForQuery( query: string, - options: GetInviteResultsOptions): Promise<*> { + options: GetInviteResultsOptions +): Promise<*> { + const text = query.trim(); const { dialOutAuthUrl, - enableAddPeople, - enableDialOut, + addPeopleEnabled, + dialOutEnabled, peopleSearchQueryTypes, peopleSearchUrl, jwt @@ -228,7 +148,7 @@ export function getInviteResultsForQuery( let peopleSearchPromise; - if (enableAddPeople && text) { + if (addPeopleEnabled && text) { peopleSearchPromise = searchDirectory( peopleSearchUrl, jwt, @@ -242,7 +162,7 @@ export function getInviteResultsForQuery( const hasCountryCode = text.startsWith('+'); let phoneNumberPromise; - if (enableDialOut && isMaybeAPhoneNumber(text)) { + if (dialOutEnabled && isMaybeAPhoneNumber(text)) { let numberToVerify = text; // When the number to verify does not start with a +, we assume no @@ -296,6 +216,82 @@ export function getInviteResultsForQuery( }); } +/** + * Sends a post request to an invite service. + * + * @param {string} inviteServiceUrl - The invite service that generates the + * invitation. + * @param {string} inviteUrl - The url to the conference. + * @param {string} jwt - The jwt token to pass to the search service. + * @param {Immutable.List} inviteItems - The list of the "user" or "room" + * type items to invite. + * @returns {Promise} - The promise created by the request. + */ +function invitePeopleAndChatRooms( // eslint-disable-line max-params + inviteServiceUrl: string, + inviteUrl: string, + jwt: string, + inviteItems: Array +): Promise { + + if (!inviteItems || inviteItems.length === 0) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + $.post( + `${inviteServiceUrl}?token=${jwt}`, + JSON.stringify({ + 'invited': inviteItems, + 'url': inviteUrl + }), + resolve, + 'json') + .fail((jqxhr, textStatus, error) => reject(error)); + }); +} + +/** + * Determines if adding people is currently enabled. + * + * @param {boolean} state - Current state. + * @returns {boolean} Indication of whether adding people is currently enabled. + */ +export function isAddPeopleEnabled(state: Object): boolean { + const { isGuest } = state['features/base/jwt']; + + if (!isGuest) { + // XXX The mobile/react-native app is capable of disabling the + // adding/inviting of people in the current conference. Anyway, the + // Web/React app does not have that capability so default appropriately. + const { app } = state['features/app']; + const addPeopleEnabled = app && app.props.addPeopleEnabled; + + return ( + (typeof addPeopleEnabled === 'undefined') + || Boolean(addPeopleEnabled)); + } + + return false; +} + +/** + * Determines if dial out is currently enabled or not. + * + * @param {boolean} state - Current state. + * @returns {boolean} Indication of whether dial out is currently enabled. + */ +export function isDialOutEnabled(state: Object): boolean { + const { conference } = state['features/base/conference']; + const { isGuest } = state['features/base/jwt']; + const { enableUserRolesBasedOnToken } = state['features/base/config']; + const participant = getLocalParticipant(state); + + return participant && participant.role === PARTICIPANT_ROLE.MODERATOR + && conference && conference.isSIPCallingSupported() + && (!enableUserRolesBasedOnToken || !isGuest); +} + /** * Checks whether a string looks like it could be for a phone number. * @@ -315,6 +311,61 @@ function isMaybeAPhoneNumber(text: string): boolean { return Boolean(digits.length); } +/** + * RegExp to use to determine if some text might be a phone number. + * + * @returns {RegExp} + */ +function isPhoneNumberRegex(): RegExp { + let regexString = '^[0-9+()-\\s]*$'; + + if (typeof interfaceConfig !== 'undefined') { + regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString; + } + + return new RegExp(regexString); +} + +/** + * Sends an ajax request to a directory service. + * + * @param {string} serviceUrl - The service to query. + * @param {string} jwt - The jwt token to pass to the search service. + * @param {string} text - Text to search. + * @param {Array} queryTypes - Array with the query types that will be + * executed - "conferenceRooms" | "user" | "room". + * @returns {Promise} - The promise created by the request. + */ +export function searchDirectory( // eslint-disable-line max-params + serviceUrl: string, + jwt: string, + text: string, + queryTypes: Array = [ 'conferenceRooms', 'user', 'room' ] +): Promise> { + + const query = encodeURIComponent(text); + const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes)); + + return fetch(`${serviceUrl}?query=${query}&queryTypes=${ + queryTypesString}&jwt=${jwt}`) + .then(response => { + const jsonify = response.json(); + + if (response.ok) { + return jsonify; + } + + return jsonify + .then(result => Promise.reject(result)); + }) + .catch(error => { + logger.error( + 'Error searching directory:', error); + + return Promise.reject(error); + }); +} + /** * Type of the options to use when sending invites. */ @@ -436,33 +487,3 @@ export function sendInvitesForItems( return Promise.all(allInvitePromises) .then(() => invitesLeftToSend); } - -/** - * Determines if adding people is currently enabled. - * - * @param {boolean} state - Current state. - * @returns {boolean} Indication of whether adding people is currently enabled. - */ -export function isAddPeopleEnabled(state: Object): boolean { - const { app } = state['features/app']; - const { isGuest } = state['features/base/jwt']; - - return !isGuest && Boolean(app && app.props.addPeopleEnabled); -} - -/** - * Determines if dial out is currently enabled or not. - * - * @param {boolean} state - Current state. - * @returns {boolean} Indication of whether dial out is currently enabled. - */ -export function isDialOutEnabled(state: Object): boolean { - const { conference } = state['features/base/conference']; - const { isGuest } = state['features/base/jwt']; - const { enableUserRolesBasedOnToken } = state['features/base/config']; - const participant = getLocalParticipant(state); - - return participant && participant.role === PARTICIPANT_ROLE.MODERATOR - && conference && conference.isSIPCallingSupported() - && (!enableUserRolesBasedOnToken || !isGuest); -} diff --git a/react/features/invite/middleware.js b/react/features/invite/middleware.any.js similarity index 80% rename from react/features/invite/middleware.js rename to react/features/invite/middleware.any.js index b84ac31c2..c2642313c 100644 --- a/react/features/invite/middleware.js +++ b/react/features/invite/middleware.any.js @@ -1,3 +1,5 @@ +// @flow + import { MiddlewareRegistry } from '../base/redux'; import { UPDATE_DIAL_IN_NUMBERS_FAILED } from './actionTypes'; @@ -5,9 +7,10 @@ import { UPDATE_DIAL_IN_NUMBERS_FAILED } from './actionTypes'; const logger = require('jitsi-meet-logger').getLogger(__filename); /** - * Middleware that catches actions fetching dial-in numbers. + * The middleware of the feature invite common to mobile/react-native and + * Web/React. * - * @param {Store} store - Redux store. + * @param {Store} store - The redux store. * @returns {Function} */ // eslint-disable-next-line no-unused-vars @@ -15,7 +18,6 @@ MiddlewareRegistry.register(store => next => action => { const result = next(action); switch (action.type) { - case UPDATE_DIAL_IN_NUMBERS_FAILED: logger.error( 'Error encountered while fetching dial-in numbers:', diff --git a/react/features/invite/middleware.native.js b/react/features/invite/middleware.native.js new file mode 100644 index 000000000..663493ca1 --- /dev/null +++ b/react/features/invite/middleware.native.js @@ -0,0 +1,208 @@ +// @flow + +import i18next from 'i18next'; +import { NativeEventEmitter, NativeModules } from 'react-native'; + +import { MiddlewareRegistry } from '../base/redux'; +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { getInviteURL } from '../base/connection'; +import { inviteVideoRooms } from '../videosipgw'; + +import { + BEGIN_ADD_PEOPLE, + _SET_EMITTER_SUBSCRIPTIONS +} from './actionTypes'; +import { + getInviteResultsForQuery, + isAddPeopleEnabled, + isDialOutEnabled, + sendInvitesForItems +} from './functions'; +import './middleware.any'; + +/** + * The react-native module of the feature invite. + */ +const { Invite } = NativeModules; + +/** + * The middleware of the feature invite specific to mobile/react-native. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +Invite && MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case APP_WILL_MOUNT: + return _appWillMount(store, next, action); + + case APP_WILL_UNMOUNT: { + const result = next(action); + + store.dispatch({ + type: _SET_EMITTER_SUBSCRIPTIONS, + emitterSubscriptions: undefined + }); + + return result; + } + + case BEGIN_ADD_PEOPLE: + return _beginAddPeople(store, next, action); + } + + return next(action); +}); + +/** + * Notifies the feature jwt that the action {@link APP_WILL_MOUNT} is being + * dispatched within a specific redux {@code store}. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux dispatch function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {*} The value returned by {@code next(action)}. + */ +function _appWillMount({ dispatch, getState }, next, action) { + const result = next(action); + + const emitter = new NativeEventEmitter(Invite); + const context = { + dispatch, + getState + }; + + dispatch({ + type: _SET_EMITTER_SUBSCRIPTIONS, + emitterSubscriptions: [ + emitter.addListener( + 'org.jitsi.meet:features/invite#performQuery', + _onPerformQuery, + context), + emitter.addListener( + 'org.jitsi.meet:features/invite#invite', + _onInvite, + context) + ] + }); + + return result; +} + +/** + * Notifies the feature invite that the action {@link BEGIN_ADD_PEOPLE} is being + * dispatched within a specific redux {@code store}. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code BEGIN_ADD_PEOPLE} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {*} The value returned by {@code next(action)}. + */ +function _beginAddPeople({ getState }, next, action) { + const result = next(action); + + // The JavaScript App needs to provide uniquely identifying information to + // the native Invite module so that the latter may match the former to the + // native JitsiMeetView which hosts it. + const { app } = getState()['features/app']; + + if (app) { + const { externalAPIScope } = app.props; + + if (externalAPIScope) { + Invite.beginAddPeople(externalAPIScope); + } + } + + return result; +} + +/** + * Handles the {@code invite} event of the feature invite i.e. invites specific + * invitees to the current, ongoing conference. + * + * @param {Object} event - The details of the event. + * @returns {void} + */ +function _onInvite( + { addPeopleControllerScope, externalAPIScope, invitees }) { + const { getState } = this; // eslint-disable-line no-invalid-this + const state = getState(); + const { conference } = state['features/base/conference']; + const { inviteServiceUrl } = state['features/base/config']; + const options = { + conference, + inviteServiceUrl, + inviteUrl: getInviteURL(state), + inviteVideoRooms, + jwt: state['features/base/jwt'].jwt + }; + + sendInvitesForItems(invitees, options) + .then(failedInvitees => + Invite.inviteSettled( + externalAPIScope, + addPeopleControllerScope, + failedInvitees)); +} + +/** + * Handles the {@code performQuery} event of the feature invite i.e. queries for + * invitees who may subsequently be invited to the current, ongoing conference. + * + * @param {Object} event - The details of the event. + * @returns {void} + */ +function _onPerformQuery( + { addPeopleControllerScope, externalAPIScope, query }) { + const { getState } = this; // eslint-disable-line no-invalid-this + + const state = getState(); + const { + dialOutAuthUrl, + peopleSearchQueryTypes, + peopleSearchUrl + } = state['features/base/config']; + const options = { + dialOutAuthUrl, + addPeopleEnabled: isAddPeopleEnabled(state), + dialOutEnabled: isDialOutEnabled(state), + jwt: state['features/base/jwt'].jwt, + peopleSearchQueryTypes, + peopleSearchUrl + }; + + getInviteResultsForQuery(query, options) + .catch(() => []) + .then(results => { + const translatedResults = results.map(result => { + if (result.type === 'phone') { + result.title = i18next.t('addPeople.telephone', { + number: result.number + }); + + if (result.showCountryCodeReminder) { + result.subtitle = i18next.t( + 'addPeople.countryReminder' + ); + } + } + + return result; + }).filter(result => result.type !== 'phone' || result.allowed); + + Invite.receivedResults( + externalAPIScope, + addPeopleControllerScope, + query, + translatedResults); + }); +} diff --git a/react/features/invite/middleware.web.js b/react/features/invite/middleware.web.js new file mode 100644 index 000000000..5684ead49 --- /dev/null +++ b/react/features/invite/middleware.web.js @@ -0,0 +1,50 @@ +// @flow + +import { openDialog } from '../base/dialog'; +import { MiddlewareRegistry } from '../base/redux'; + +import { BEGIN_ADD_PEOPLE } from './actionTypes'; +import { AddPeopleDialog } from './components'; +import { isAddPeopleEnabled, isDialOutEnabled } from './functions'; +import './middleware.any'; + +/** + * The middleware of the feature invite specific to Web/React. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case BEGIN_ADD_PEOPLE: + return _beginAddPeople(store, next, action); + } + + return next(action); +}); + +/** + * Notifies the feature invite that the action {@link BEGIN_ADD_PEOPLE} is being + * dispatched within a specific redux {@code store}. + * + * @param {Store} store - The redux store in which the specified {@code action} + * is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the + * specified {@code action} to the specified {@code store}. + * @param {Action} action - The redux action {@code BEGIN_ADD_PEOPLE} which is + * being dispatched in the specified {@code store}. + * @private + * @returns {*} The value returned by {@code next(action)}. + */ +function _beginAddPeople({ dispatch, getState }, next, action) { + const result = next(action); + + const state = getState(); + + dispatch(openDialog(AddPeopleDialog, { + addPeopleEnabled: isAddPeopleEnabled(state), + dialOutEnabled: isDialOutEnabled(state) + })); + + return result; +} diff --git a/react/features/invite/reducer.js b/react/features/invite/reducer.js index 48a73cb4e..9f04007c2 100644 --- a/react/features/invite/reducer.js +++ b/react/features/invite/reducer.js @@ -1,6 +1,9 @@ -import { ReducerRegistry } from '../base/redux'; +// @flow + +import { assign, ReducerRegistry } from '../base/redux'; import { + _SET_EMITTER_SUBSCRIPTIONS, UPDATE_DIAL_IN_NUMBERS_FAILED, UPDATE_DIAL_IN_NUMBERS_SUCCESS } from './actionTypes'; @@ -11,6 +14,10 @@ const DEFAULT_STATE = { ReducerRegistry.register('features/invite', (state = DEFAULT_STATE, action) => { switch (action.type) { + case _SET_EMITTER_SUBSCRIPTIONS: + return ( + assign(state, 'emitterSubscriptions', action.emitterSubscriptions)); + case UPDATE_DIAL_IN_NUMBERS_FAILED: return { ...state, diff --git a/react/features/mobile/external-api/middleware.js b/react/features/mobile/external-api/middleware.js index 59d5dc0ad..f45fd2caf 100644 --- a/react/features/mobile/external-api/middleware.js +++ b/react/features/mobile/external-api/middleware.js @@ -159,9 +159,9 @@ function _sendEvent( { getState }: { getState: Function }, 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. + // 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 { app } = getState()['features/app']; if (app) { diff --git a/react/features/mobile/invite-search/actionTypes.js b/react/features/mobile/invite-search/actionTypes.js deleted file mode 100644 index 236fc6b00..000000000 --- a/react/features/mobile/invite-search/actionTypes.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * The type of redux action to set InviteSearch's event subscriptions. - * - * { - * type: _SET_INVITE_SEARCH_SUBSCRIPTIONS, - * subscriptions: Array|undefined - * } - * - * @protected - */ -export const _SET_INVITE_SEARCH_SUBSCRIPTIONS - = Symbol('_SET_INVITE_SEARCH_SUBSCRIPTIONS'); - - -/** - * The type of the action which signals a request to launch the native invite - * dialog. - * - * { - * type: LAUNCH_NATIVE_INVITE - * } - */ -export const LAUNCH_NATIVE_INVITE = Symbol('LAUNCH_NATIVE_INVITE'); - -/** - * The type of the action which signals that native invites were sent - * successfully. - * - * { - * type: SEND_INVITE_SUCCESS, - * inviteScope: string - * } - */ -export const SEND_INVITE_SUCCESS = Symbol('SEND_INVITE_SUCCESS'); - -/** - * The type of the action which signals that native invites failed to send - * successfully. - * - * { - * type: SEND_INVITE_FAILURE, - * items: Array<*>, - * inviteScope: string - * } - */ -export const SEND_INVITE_FAILURE = Symbol('SEND_INVITE_FAILURE'); diff --git a/react/features/mobile/invite-search/actions.js b/react/features/mobile/invite-search/actions.js deleted file mode 100644 index f01daabdc..000000000 --- a/react/features/mobile/invite-search/actions.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow - -import { - LAUNCH_NATIVE_INVITE, - SEND_INVITE_SUCCESS, - SEND_INVITE_FAILURE -} from './actionTypes'; - -/** - * Launches the native invite dialog. - * - * @returns {{ - * type: LAUNCH_NATIVE_INVITE - * }} - */ -export function launchNativeInvite() { - return { - type: LAUNCH_NATIVE_INVITE - }; -} - -/** - * Indicates that all native invites were sent successfully. - * - * @param {string} inviteScope - Scope identifier for the invite success. This - * is used to look up relevant information on the native side. - * @returns {void} - */ -export function sendInviteSuccess(inviteScope: string) { - return { - type: SEND_INVITE_SUCCESS, - inviteScope - }; -} - -/** - * Indicates that some native invites failed to send successfully. - * - * @param {Array<*>} items - Invite items that failed to send. - * @param {string} inviteScope - Scope identifier for the invite failure. This - * is used to look up relevant information on the native side. - * @returns {void} - */ -export function sendInviteFailure(items: Array<*>, inviteScope: string) { - return { - type: SEND_INVITE_FAILURE, - items, - inviteScope - }; -} diff --git a/react/features/mobile/invite-search/index.js b/react/features/mobile/invite-search/index.js deleted file mode 100644 index 496fe99f8..000000000 --- a/react/features/mobile/invite-search/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export * from './actions'; -export * from './actionTypes'; - -import './reducer'; -import './middleware'; diff --git a/react/features/mobile/invite-search/middleware.js b/react/features/mobile/invite-search/middleware.js deleted file mode 100644 index 2cb6b31e6..000000000 --- a/react/features/mobile/invite-search/middleware.js +++ /dev/null @@ -1,233 +0,0 @@ -/* @flow */ - -import i18next from 'i18next'; -import { NativeModules, NativeEventEmitter } from 'react-native'; - -import { MiddlewareRegistry } from '../../base/redux'; -import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app'; -import { getInviteURL } from '../../base/connection'; -import { - getInviteResultsForQuery, - isAddPeopleEnabled, - isDialOutEnabled, - sendInvitesForItems -} from '../../invite'; -import { inviteVideoRooms } from '../../videosipgw'; - -import { sendInviteSuccess, sendInviteFailure } from './actions'; -import { - _SET_INVITE_SEARCH_SUBSCRIPTIONS, - LAUNCH_NATIVE_INVITE, - SEND_INVITE_SUCCESS, - SEND_INVITE_FAILURE -} from './actionTypes'; - -/** - * Middleware that captures Redux actions and uses the InviteSearch 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 result = next(action); - - switch (action.type) { - - case APP_WILL_MOUNT: - return _appWillMount(store, next, action); - - case APP_WILL_UNMOUNT: - store.dispatch({ - type: _SET_INVITE_SEARCH_SUBSCRIPTIONS, - subscriptions: undefined - }); - break; - - case LAUNCH_NATIVE_INVITE: - launchNativeInvite(store); - break; - - case SEND_INVITE_SUCCESS: - onSendInviteSuccess(action); - break; - - case SEND_INVITE_FAILURE: - onSendInviteFailure(action); - break; - } - - return result; -}); - -/** - * Notifies the feature jwt that the action {@link APP_WILL_MOUNT} is being - * dispatched within a specific redux {@code store}. - * - * @param {Store} store - The redux store in which the specified {@code action} - * is being dispatched. - * @param {Dispatch} next - The redux dispatch function to dispatch the - * specified {@code action} to the specified {@code store}. - * @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is - * being dispatched in the specified {@code store}. - * @private - * @returns {*} - */ -function _appWillMount({ dispatch, getState }, next, action) { - const result = next(action); - - const emitter = new NativeEventEmitter(NativeModules.InviteSearch); - - const context = { - dispatch, - getState - }; - const subscriptions = [ - emitter.addListener( - 'performQueryAction', - _onPerformQueryAction, - context), - emitter.addListener( - 'performSubmitInviteAction', - _onPerformSubmitInviteAction, - context) - ]; - - dispatch({ - type: _SET_INVITE_SEARCH_SUBSCRIPTIONS, - subscriptions - }); - - return result; -} - -/** - * Sends a request to the native counterpart of InviteSearch to launch a native. - * invite search. - * - * @param {Object} store - The redux store. - * @private - * @returns {void} - */ -function launchNativeInvite(store: { getState: Function }) { - // The JavaScript App needs to provide uniquely identifying information - // to the native module so that the latter may match the former - // to the native JitsiMeetView which hosts it. - const { app } = store.getState()['features/app']; - - if (app) { - const { externalAPIScope } = app.props; - - if (externalAPIScope) { - NativeModules.InviteSearch.launchNativeInvite(externalAPIScope); - } - } -} - -/** - * Sends a notification to the native counterpart of InviteSearch that all - * invites were sent successfully. - * - * @param {Object} action - The redux action {@code SEND_INVITE_SUCCESS} which - * is being dispatched. - * @returns {void} - */ -function onSendInviteSuccess({ inviteScope }) { - NativeModules.InviteSearch.inviteSucceeded(inviteScope); -} - -/** - * Sends a notification to the native counterpart of InviteSearch that some - * invite items failed to send successfully. - * - * @param {Object} action - The redux action {@code SEND_INVITE_FAILURE} which - * is being dispatched. - * @returns {void} - */ -function onSendInviteFailure({ items, inviteScope }) { - NativeModules.InviteSearch.inviteFailedForItems(items, inviteScope); -} - -/** - * Handles InviteSearch's event {@code performQueryAction}. - * - * @param {Object} event - The details of the InviteSearch event - * {@code performQueryAction}. - * @returns {void} - */ -function _onPerformQueryAction({ query, inviteScope }) { - const { getState } = this; // eslint-disable-line no-invalid-this - - const state = getState(); - - const { - dialOutAuthUrl, - peopleSearchQueryTypes, - peopleSearchUrl - } = state['features/base/config']; - - const options = { - dialOutAuthUrl, - enableAddPeople: isAddPeopleEnabled(state), - enableDialOut: isDialOutEnabled(state), - jwt: state['features/base/jwt'].jwt, - peopleSearchQueryTypes, - peopleSearchUrl - }; - - getInviteResultsForQuery(query, options) - .catch(() => []) - .then(results => { - const translatedResults = results.map(result => { - if (result.type === 'phone') { - result.title = i18next.t('addPeople.telephone', { - number: result.number - }); - - if (result.showCountryCodeReminder) { - result.subtitle = i18next.t( - 'addPeople.countryReminder' - ); - } - } - - return result; - }).filter(result => result.type !== 'phone' || result.allowed); - - NativeModules.InviteSearch.receivedResults( - translatedResults, - query, - inviteScope); - }); -} - -/** - * Handles InviteSearch's event {@code performSubmitInviteAction}. - * - * @param {Object} event - The details of the InviteSearch event. - * @returns {void} - */ -function _onPerformSubmitInviteAction({ selectedItems, inviteScope }) { - const { dispatch, getState } = this; // eslint-disable-line no-invalid-this - const state = getState(); - const { conference } = state['features/base/conference']; - const { - inviteServiceUrl - } = state['features/base/config']; - const options = { - conference, - inviteServiceUrl, - inviteUrl: getInviteURL(state), - inviteVideoRooms, - jwt: state['features/base/jwt'].jwt - }; - - sendInvitesForItems(selectedItems, options) - .then(invitesLeftToSend => { - if (invitesLeftToSend.length) { - dispatch(sendInviteFailure(invitesLeftToSend, inviteScope)); - } else { - dispatch(sendInviteSuccess(inviteScope)); - } - }); -} diff --git a/react/features/mobile/invite-search/reducer.js b/react/features/mobile/invite-search/reducer.js deleted file mode 100644 index 370ae3be5..000000000 --- a/react/features/mobile/invite-search/reducer.js +++ /dev/null @@ -1,14 +0,0 @@ -import { assign, ReducerRegistry } from '../../base/redux'; - -import { _SET_INVITE_SEARCH_SUBSCRIPTIONS } from './actionTypes'; - -ReducerRegistry.register( - 'features/invite-search', - (state = {}, action) => { - switch (action.type) { - case _SET_INVITE_SEARCH_SUBSCRIPTIONS: - return assign(state, 'subscriptions', action.subscriptions); - } - - return state; - }); diff --git a/react/features/toolbox/components/Toolbox.native.js b/react/features/toolbox/components/Toolbox.native.js index d759d1010..0950018f3 100644 --- a/react/features/toolbox/components/Toolbox.native.js +++ b/react/features/toolbox/components/Toolbox.native.js @@ -14,16 +14,11 @@ import { isNarrowAspectRatio, makeAspectRatioAware } from '../../base/responsive-ui'; -import { - InviteButton, - isAddPeopleEnabled, - isDialOutEnabled -} from '../../invite'; +import { InviteButton } from '../../invite'; import { EnterPictureInPictureToolbarButton } from '../../mobile/picture-in-picture'; import { beginRoomLockRequest } from '../../room-lock'; -import { beginShareRoom } from '../../share-room'; import { abstractMapDispatchToProps, @@ -51,18 +46,6 @@ type Props = { */ _audioOnly: boolean, - /** - * Whether or not the feature to directly invite people into the - * conference is available. - */ - _enableAddPeople: boolean, - - /** - * Whether or not the feature to dial out to number to join the - * conference is available. - */ - _enableDialOut: boolean, - /** * The indicator which determines whether the toolbox is enabled. */ @@ -83,11 +66,6 @@ type Props = { */ _onRoomLock: Function, - /** - * Begins the UI procedure to share the conference/room URL. - */ - _onShareRoom: Function, - /** * Toggles the audio-only flag of the conference. */ @@ -112,7 +90,6 @@ type Props = { dispatch: Function }; - /** * Implements the conference toolbox on React Native. */ @@ -219,13 +196,9 @@ class Toolbox extends Component { const underlayColor = 'transparent'; const { _audioOnly: audioOnly, - _enableAddPeople: enableAddPeople, - _enableDialOut: enableDialOut, _videoMuted: videoMuted } = this.props; - const showInviteButton = enableAddPeople || enableDialOut; - /* eslint-disable react/jsx-curly-spacing,react/jsx-handler-names */ return ( @@ -262,24 +235,10 @@ class Toolbox extends Component { onClick = { this.props._onRoomLock } style = { style } underlayColor = { underlayColor } /> - { - !showInviteButton - && - } - { - showInviteButton - && - } + { this.props.dispatch(openFeedbackDialog(_conference)); } - /** - * Opens the dialog for inviting people directly into the conference. - * - * @private - * @returns {void} - */ - _doOpenInvite() { - const { _addPeopleAvailable, _dialOutAvailable, dispatch } = this.props; - - if (_addPeopleAvailable || _dialOutAvailable) { - dispatch(openDialog(AddPeopleDialog, { - enableAddPeople: _addPeopleAvailable, - enableDialOut: _dialOutAvailable - })); - } - } - /** * Dispatches an action to display {@code KeyboardShortcuts}. * @@ -692,8 +668,7 @@ class Toolbox extends Component { */ _onToolbarOpenInvite() { sendAnalytics(createToolbarEvent('invite')); - - this._doOpenInvite(); + this.props.dispatch(beginAddPeople()); } _onToolbarOpenKeyboardShortcuts: () => void; @@ -1118,10 +1093,8 @@ function _mapStateToProps(state) { callStatsID, disableDesktopSharing, enableRecording, - enableUserRolesBasedOnToken, iAmRecorder } = state['features/base/config']; - const { isGuest } = state['features/base/jwt']; const { isRecording, recordingType } = state['features/recording']; const sharedVideoStatus = state['features/shared-video'].status; const { current } = state['features/side-panel']; @@ -1134,25 +1107,20 @@ function _mapStateToProps(state) { const localParticipant = getLocalParticipant(state); const localVideo = getLocalVideoTrack(state['features/base/tracks']); const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR; - const isAddPeopleAvailable = !isGuest; - const isDialOutAvailable - = isModerator - && conference && conference.isSIPCallingSupported() - && (!enableUserRolesBasedOnToken || !isGuest); + const addPeopleEnabled = isAddPeopleEnabled(state); + const dialOutEnabled = isDialOutEnabled(state); return { - _addPeopleAvailable: isAddPeopleAvailable, _chatOpen: current === 'chat_container', _conference: conference, _desktopSharingEnabled: desktopSharingEnabled, _desktopSharingDisabledByConfig: disableDesktopSharing, - _dialOutAvailable: isDialOutAvailable, _dialog: Boolean(state['features/base/dialog'].component), _editingDocument: Boolean(state['features/etherpad'].editing), _etherpadInitialized: Boolean(state['features/etherpad'].initialized), _feedbackConfigured: Boolean(callStatsID), - _hideInviteButton: iAmRecorder - || (!isAddPeopleAvailable && !isDialOutAvailable), + _hideInviteButton: + iAmRecorder || (!addPeopleEnabled && !dialOutEnabled), _isRecording: isRecording, _fullScreen: fullScreen, _localParticipantID: localParticipant.id,