[RN] add support for inviting participants during a call on mobile (2)

This commit is contained in:
Lyubo Marinov 2018-04-30 23:43:47 -05:00 committed by Saúl Ibarra Corretgé
parent f450756337
commit effd3728b6
51 changed files with 2143 additions and 1459 deletions

View File

@ -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<String, Object>());
}
@Override
public void onLoadConfigError(Map<String, Object> 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;

View File

@ -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<String, ReadableMap> items = new HashMap<>();
/**
* Randomly generated UUID, used for identification in the InviteSearchModule
*/
private String uuid = UUID.randomUUID().toString();
private WeakReference<InviteSearchModule> 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<String> ids) {
WritableArray selectedItems = new WritableNativeArray();
for(int i=0; i<ids.size(); i++) {
if(items.containsKey(ids.get(i))) {
WritableNativeMap map = new WritableNativeMap();
map.merge(items.get(ids.get(i)));
selectedItems.pushMap(map);
} else {
// if the id doesn't exist in the map, we can't do anything, so just skip it
}
}
JitsiMeetView.submitSelectedItems(selectedItems, uuid);
}
/**
* 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) {
List<Map<String, Object>> 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("InviteSearchController", "Received result without id and that was not a phone number, so not adding it to suggestions: " + map);
}
jvmResults.add(map.toHashMap());
}
searchControllerDelegate.onReceiveResults(this, jvmResults, query);
}
/**
*
* @return the InviteSearchControllerDelegate for this controller, used to pass query
* results back to the native code that initiated the query.
*/
public InviteSearchControllerDelegate getSearchControllerDelegate() {
return searchControllerDelegate;
}
/**
* Sets the InviteSearchControllerDelegate for this controller, used to pass query results
* back to the native code that initiated the query.
*
* @param searchControllerDelegate
*/
public void setSearchControllerDelegate(InviteSearchControllerDelegate searchControllerDelegate) {
this.searchControllerDelegate = searchControllerDelegate;
}
/**
* Cancel the invitation flow and free memory allocated to the InviteSearchController. After
* calling this method, this object is invalid - a new InviteSearchController will be passed
* to the caller through launchNativeInvite.
*/
public void cancelSearch() {
InviteSearchModule parentModule = parentModuleRef.get();
if(parentModule != null) {
parentModule.removeSearchController(uuid);
}
}
/**
* @return the unique identifier for this InviteSearchController
*/
public String getUuid() {
return uuid;
}
public interface InviteSearchControllerDelegate {
/**
* Called when results are received for a query called through InviteSearchController.query()
*
* @param searchController
* @param results a List of Map<String, Object> 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<Map<String, Object>> 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<String, Object>} 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<Map<String, Object>> failedInviteItems);
}
}

View File

@ -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<String, InviteSearchController> 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<Map<String, Object>> jvmItems = new ArrayList<>();
for(int i=0; i<items.size(); i++) {
ReadableMap item = items.getMap(i);
jvmItems.add(item.toHashMap());
}
controller.getSearchControllerDelegate().inviteFailed(controller, jvmItems);
}
@ReactMethod
public void inviteSucceeded(String inviteSearchControllerScope) {
InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
if(controller == null) {
Log.w("InviteSearchModule", "Invite succeeded, but unable to find active controller to notify");
return;
}
controller.getSearchControllerDelegate().inviteSucceeded(controller);
searchControllers.remove(inviteSearchControllerScope);
}
void removeSearchController(String inviteSearchControllerUuid) {
searchControllers.remove(inviteSearchControllerUuid);
}
@Override
public String getName() {
return "InviteSearch";
}
private InviteSearchController createSearchController() {
InviteSearchController searchController = new InviteSearchController(this);
searchControllers.put(searchController.getUuid(), searchController);
return searchController;
}
}

View File

@ -17,7 +17,6 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -229,7 +228,7 @@ public class JitsiMeetActivity extends AppCompatActivity {
if (!super.onKeyUp(keyCode, event)
&& BuildConfig.DEBUG
&& (reactInstanceManager
= JitsiMeetView.getReactInstanceManager())
= ReactInstanceManagerHolder.getReactInstanceManager())
!= null
&& keyCode == KeyEvent.KEYCODE_MENU) {
reactInstanceManager.showDevOptionsDialog();

View File

@ -17,7 +17,6 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@ -29,22 +28,13 @@ import android.widget.FrameLayout;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.rnimmersive.RNImmersiveModule;
import org.jitsi.meet.sdk.invite.InviteController;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
@ -62,30 +52,9 @@ public class JitsiMeetView extends FrameLayout {
*/
private final static String TAG = JitsiMeetView.class.getSimpleName();
/**
* 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 final Set<JitsiMeetView> views
= Collections.newSetFromMap(new WeakHashMap<JitsiMeetView, Boolean>());
private static List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
return Arrays.<NativeModule>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<NativeModule> 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}.

View File

@ -46,8 +46,4 @@ public abstract class JitsiMeetViewAdapter implements JitsiMeetViewListener {
@Override
public void onLoadConfigError(Map<String, Object> data) {
}
@Override
public void launchNativeInvite(InviteSearchController inviteSearchController) {
}
}

View File

@ -59,16 +59,6 @@ public interface JitsiMeetViewListener {
*/
void onConferenceWillLeave(Map<String, Object> 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.

View File

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

View File

@ -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<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
return Arrays.<NativeModule>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<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
return
ReactInstanceManagerHolder.createNativeModules(
reactContext);
}
})
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
}
}

View File

@ -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<String, ReadableMap> items = new HashMap<>();
private final WeakReference<InviteController> owner;
private final WeakReference<ReactApplicationContext> 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<String> 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<Map<String, Object>> 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<Map<String, Object>> 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;
}
}

View File

@ -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<String, Object> 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<Map<String, Object>> 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<String, Object>} 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<Map<String, Object>> failedInvitees);
}

View File

@ -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<List<Map<String, Object>>> invite(
final List<Map<String, Object>> invitees) {
final boolean inviteBegan
= invite(
UUID.randomUUID().toString(),
/* reactContext */ null,
Arguments.makeNativeArray(invitees));
FutureTask futureTask
= new FutureTask(new Callable() {
@Override
public List<Map<String, Object>> 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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -18,6 +18,6 @@
#import <JitsiMeet/JitsiMeet.h>
@interface ViewController : UIViewController<JitsiMeetViewDelegate>
@interface ViewController : UIViewController<JitsiMeetViewDelegate, InviteControllerDelegate>
@end

View File

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

View File

@ -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 = "<group>"; };
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewDelegate.h; sourceTree = "<group>"; };
0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = "<group>"; };
0B6F414F20987DE600FF6789 /* Invite+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Invite+Private.h"; sourceTree = "<group>"; };
0B6F41502098840600FF6789 /* InviteController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "InviteController+Private.h"; sourceTree = "<group>"; };
0B6F4151209884E500FF6789 /* AddPeopleController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AddPeopleController+Private.h"; sourceTree = "<group>"; };
0B7C2CFC200F51D60060D076 /* LaunchOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchOptions.m; sourceTree = "<group>"; };
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 = "<group>"; };
@ -64,10 +72,15 @@
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = "<group>"; };
412BF89C206AA66F0053B9E5 /* InviteSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InviteSearch.m; sourceTree = "<group>"; };
412BF89E206AA82F0053B9E5 /* InviteSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InviteSearch.h; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
B386B85020981A74000DEF7A /* InviteController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InviteController.m; sourceTree = "<group>"; };
B386B85120981A74000DEF7A /* AddPeopleController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AddPeopleController.m; sourceTree = "<group>"; };
B386B85220981A74000DEF7A /* AddPeopleControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddPeopleControllerDelegate.h; sourceTree = "<group>"; };
B386B85320981A74000DEF7A /* AddPeopleController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddPeopleController.h; sourceTree = "<group>"; };
B386B85420981A74000DEF7A /* InviteControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InviteControllerDelegate.h; sourceTree = "<group>"; };
B386B85520981A75000DEF7A /* InviteController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InviteController.h; sourceTree = "<group>"; };
B386B85620981A75000DEF7A /* Invite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Invite.m; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 = "<group>";
};
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 */,

View File

@ -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<NSDictionary*> * _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<NSDictionary *> * _Nonnull)items
fromSearchController:(InviteSearchController * _Nonnull)searchController;
@end
@interface InviteSearchController: NSObject
@property (nonatomic, nullable, weak) id<InviteSearchControllerDelegate> delegate;
- (void)performQuery:(NSString * _Nonnull)query;
- (void)cancelSearch;
- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids;
@end

View File

@ -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 <React/RCTBridge.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTUtils.h>
#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<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
- (void)inviteDidSucceed;
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)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<NSString *> *)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<JitsiMeetViewDelegate> 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<NSDictionary *> *)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<NSDictionary *> * _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<NSString *> * _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<NSDictionary *> *)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<NSDictionary *> *)items {
if (!items) {
items = @[];
}
[self.delegate inviteDidFailForItems:items fromSearchController:self];
}
@end

View File

@ -14,6 +14,12 @@
* limitations under the License.
*/
// JitsiMeetView
#import <JitsiMeet/JitsiMeetView.h>
#import <JitsiMeet/JitsiMeetViewDelegate.h>
#import <JitsiMeet/InviteSearch.h>
// invite/
#import <JitsiMeet/AddPeopleController.h>
#import <JitsiMeet/AddPeopleControllerDelegate.h>
#import <JitsiMeet/InviteController.h>
#import <JitsiMeet/InviteControllerDelegate.h>

View File

@ -17,17 +17,16 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "InviteController.h"
#import "JitsiMeetViewDelegate.h"
@interface JitsiMeetView : UIView
@property (nonatomic) BOOL addPeopleEnabled;
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (nonatomic) BOOL dialOutEnabled;
@property (nonatomic, readonly) InviteController *inviteController;
@property (nonatomic) BOOL pictureInPictureEnabled;

View File

@ -23,6 +23,8 @@
#import <React/RCTLinkingManager.h>
#import <React/RCTRootView.h>
#import "Invite+Private.h"
#import "InviteController+Private.h"
#import "JitsiMeetView+Private.h"
#import "RCTBridgeWrapper.h"
@ -268,12 +270,13 @@ static NSMapTable<NSString *, JitsiMeetView *> *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<NSString *, JitsiMeetView *> *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

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
@class InviteSearchController;
@protocol JitsiMeetViewDelegate <NSObject>
@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

View File

@ -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<NSDictionary *> *)failedInvitees;
- (void)receivedResults:(NSArray<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
@end

View File

@ -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 <Foundation/Foundation.h>
#import "AddPeopleControllerDelegate.h"
@interface AddPeopleController: NSObject
@property (nonatomic, nullable, weak) id<AddPeopleControllerDelegate> delegate;
- (void)endAddPeople;
- (void)inviteById:(NSArray<NSString *> * _Nonnull)ids;
- (void)performQuery:(NSString * _Nonnull)query;
@end

View File

@ -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<NSString *> * _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<NSDictionary *> *)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<NSDictionary *> *)failedInvitees {
[self.delegate inviteSettled:failedInvitees fromSearchController:self];
}
@end

View File

@ -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 <Foundation/Foundation.h>
#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<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
/**
* TODO.
*/
- (void)inviteSettled:(NSArray<NSDictionary *> * _Nonnull)failedInvitees
fromSearchController:(AddPeopleController * _Nonnull)addPeopleController;
@end

View File

@ -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 <React/RCTBridge.h>
#import <React/RCTEventEmitter.h>
@interface Invite : RCTEventEmitter <RCTBridgeModule>
- (void) invite:(NSArray<NSDictionary *> * _Nonnull)invitees
externalAPIScope:(NSString * _Nonnull)externalAPIScope
addPeopleControllerScope:(NSString * _Nonnull)addPeopleControllerScope;
- (void) performQuery:(NSString * _Nonnull)query
externalAPIScope:(NSString * _Nonnull)externalAPIScope
addPeopleControllerScope:(NSString * _Nonnull)addPeopleControllerScope;
@end

View File

@ -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<NSString *> *)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<NSDictionary *> * _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

View File

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

View File

@ -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 <Foundation/Foundation.h>
#import "InviteControllerDelegate.h"
@interface InviteController : NSObject
@property (nonatomic) BOOL addPeopleEnabled;
@property (nonatomic) BOOL dialOutEnabled;
@property (nonatomic, nullable, weak) id<InviteControllerDelegate> delegate;
- (void) invite:(NSArray *)invitees
withCompletion:(void (^)(NSArray<NSDictionary *> *failedInvitees))completion;
@end

View File

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

View File

@ -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 <NSObject>
/**
* 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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
/**
* Created by ystamcheva on 8/6/17.
*/

View File

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

View File

@ -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<Props> {
*/
render() {
const {
enableAddPeople,
enableDialOut,
onLaunchNativeInvite,
_addPeopleEnabled,
_dialOutEnabled,
_onAddPeople,
_onShareRoom,
...props
} = this.props;
if (!enableAddPeople && !enableDialOut) {
return null;
if (_SHARE_ROOM_TOOLBAR_BUTTON) {
return (
<ToolbarButton
iconName = 'link'
onClick = { _onShareRoom }
{ ...props } />
);
}
return (
<ToolbarButton
iconName = { 'add' }
onClick = { onLaunchNativeInvite }
{ ...props } />
);
if (_addPeopleEnabled || _dialOutEnabled) {
return (
<ToolbarButton
iconName = { 'link' }
onClick = { _onAddPeople }
{ ...props } />
);
}
return null;
}
}
@ -68,13 +98,13 @@ class InviteButton extends Component<Props> {
*
* @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);

View File

@ -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<Object> {
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<Object>): Promise<void> {
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<string>} 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<string> = [ 'conferenceRooms', 'user', 'room' ]
): Promise<Array<Object>> {
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<Object> {
dialNumber: string,
dialOutAuthUrl: string
): Promise<Object> {
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<Object> {
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<Object>
): Promise<void> {
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<string>} 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<string> = [ 'conferenceRooms', 'user', 'room' ]
): Promise<Array<Object>> {
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);
}

View File

@ -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:',

View File

@ -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);
});
}

View File

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

View File

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

View File

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

View File

@ -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');

View File

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

View File

@ -1,5 +0,0 @@
export * from './actions';
export * from './actionTypes';
import './reducer';
import './middleware';

View File

@ -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));
}
});
}

View File

@ -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;
});

View File

@ -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<Props> {
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<Props> {
onClick = { this.props._onRoomLock }
style = { style }
underlayColor = { underlayColor } />
{
!showInviteButton
&& <ToolbarButton
iconName = 'link'
iconStyle = { iconStyle }
onClick = { this.props._onShareRoom }
style = { style }
underlayColor = { underlayColor } />
}
{
showInviteButton
&& <InviteButton
enableAddPeople = { enableAddPeople }
enableDialOut = { enableDialOut }
iconStyle = { iconStyle }
style = { style }
underlayColor = { underlayColor } />
}
<InviteButton
iconStyle = { iconStyle }
style = { style }
underlayColor = { underlayColor } />
<EnterPictureInPictureToolbarButton
iconStyle = { iconStyle }
style = { style }
@ -344,17 +303,6 @@ function _mapDispatchToProps(dispatch) {
dispatch(beginRoomLockRequest());
},
/**
* Begins the UI procedure to share the conference/room URL.
*
* @private
* @returns {void}
* @type {Function}
*/
_onShareRoom() {
dispatch(beginShareRoom());
},
/**
* Toggles the audio-only flag of the conference.
*
@ -408,22 +356,6 @@ function _mapStateToProps(state) {
*/
_audioOnly: Boolean(conference.audioOnly),
/**
* Whether or not the feature to directly invite people into the
* conference is available.
*
* @type {boolean}
*/
_enableAddPeople: isAddPeopleEnabled(state),
/**
* Whether or not the feature to dial out to number to join the
* conference is available.
*
* @type {boolean}
*/
_enableDialOut: isDialOutEnabled(state),
/**
* The indicator which determines whether the toolbox is enabled.
*

View File

@ -21,7 +21,12 @@ import { ChatCounter } from '../../chat';
import { openDeviceSelectionDialog } from '../../device-selection';
import { toggleDocument } from '../../etherpad';
import { openFeedbackDialog } from '../../feedback';
import { AddPeopleDialog, InfoDialogButton } from '../../invite';
import {
beginAddPeople,
InfoDialogButton,
isAddPeopleEnabled,
isDialOutEnabled
} from '../../invite';
import { openKeyboardShortcutsDialog } from '../../keyboard-shortcuts';
import { RECORDING_TYPES, toggleRecording } from '../../recording';
import { toggleSharedVideo } from '../../shared-video';
@ -43,12 +48,6 @@ import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons';
type Props = {
/**
* Whether or not the feature for adding people directly into the call
* is enabled.
*/
_addPeopleAvailable: boolean,
/**
* Whether or not the chat feature is currently displayed.
*/
@ -69,12 +68,6 @@ type Props = {
*/
_desktopSharingEnabled: boolean,
/**
* Whether or not the feature for telephony to dial out to a number is
* enabled.
*/
_dialOutAvailable: boolean,
/**
* Whether or not a dialog is displayed.
*/
@ -399,23 +392,6 @@ class Toolbox extends Component<Props, State> {
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<Props, State> {
*/
_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,