[RN] add support for inviting participants during a call on mobile
* Button conditionally shown based on if the feature is enabled and available * Hooks for launching the invite UI (delegates to the native layer) * Hooks for using the search and dial out checks from the native layer (calls back into JS) * Hooks for handling sending invites and passing any failures back to the native layer * Android and iOS handling for those hooks Author: Ryan Peck <rpeck@atlassian.com> Author: Eric Brynsvold <ebrynsvold@atlassian.com>
This commit is contained in:
parent
4e36127dc7
commit
f64c13d4b7
|
@ -19,12 +19,14 @@ 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 com.calendarevents.CalendarEventsPackage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
@ -84,6 +86,11 @@ 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);
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -32,7 +32,9 @@ 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;
|
||||
|
@ -42,6 +44,7 @@ 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;
|
||||
|
@ -75,6 +78,7 @@ public class JitsiMeetView extends FrameLayout {
|
|||
new AppInfoModule(reactContext),
|
||||
new AudioModeModule(reactContext),
|
||||
new ExternalAPIModule(reactContext),
|
||||
new InviteSearchModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
new ProximityModule(reactContext),
|
||||
new WiFiStatsModule(reactContext),
|
||||
|
@ -268,15 +272,43 @@ public class JitsiMeetView extends FrameLayout {
|
|||
sendEvent("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 params {@code WritableMap} optional ancillary data for the event.
|
||||
* @param data {@code Object} optional ancillary data for the event.
|
||||
*/
|
||||
private static void sendEvent(
|
||||
String eventName,
|
||||
@Nullable WritableMap params) {
|
||||
@Nullable Object data) {
|
||||
if (reactInstanceManager != null) {
|
||||
ReactContext reactContext
|
||||
= reactInstanceManager.getCurrentReactContext();
|
||||
|
@ -284,11 +316,16 @@ public class JitsiMeetView extends FrameLayout {
|
|||
reactContext
|
||||
.getJSModule(
|
||||
DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
.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
|
||||
|
@ -296,6 +333,11 @@ 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
|
||||
|
@ -454,6 +496,9 @@ 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
|
||||
|
@ -535,6 +580,18 @@ 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
|
||||
|
@ -548,6 +605,18 @@ 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}.
|
||||
|
|
|
@ -46,4 +46,8 @@ public abstract class JitsiMeetViewAdapter implements JitsiMeetViewListener {
|
|||
@Override
|
||||
public void onLoadConfigError(Map<String, Object> data) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchNativeInvite(InviteSearchController inviteSearchController) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,6 +59,16 @@ 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.
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
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, ); }; };
|
||||
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 */; };
|
||||
|
@ -62,6 +64,8 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
|
@ -125,6 +129,8 @@
|
|||
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
|
||||
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
|
||||
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
|
||||
412BF89E206AA82F0053B9E5 /* InviteSearch.h */,
|
||||
412BF89C206AA66F0053B9E5 /* InviteSearch.m */,
|
||||
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
|
||||
0BD906E91EC0C00300C8C18E /* Info.plist */,
|
||||
0B7C2CFC200F51D60060D076 /* LaunchOptions.m */,
|
||||
|
@ -180,6 +186,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */,
|
||||
412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */,
|
||||
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
|
||||
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
|
||||
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
|
||||
|
@ -347,6 +354,7 @@
|
|||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift 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 */,
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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
|
|
@ -16,3 +16,4 @@
|
|||
|
||||
#import <JitsiMeet/JitsiMeetView.h>
|
||||
#import <JitsiMeet/JitsiMeetViewDelegate.h>
|
||||
#import <JitsiMeet/InviteSearch.h>
|
||||
|
|
|
@ -21,10 +21,14 @@
|
|||
|
||||
@interface JitsiMeetView : UIView
|
||||
|
||||
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
|
||||
@property (nonatomic) BOOL addPeopleEnabled;
|
||||
|
||||
@property (copy, nonatomic, nullable) NSURL *defaultURL;
|
||||
|
||||
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic) BOOL dialOutEnabled;
|
||||
|
||||
@property (nonatomic) BOOL pictureInPictureEnabled;
|
||||
|
||||
@property (nonatomic) BOOL welcomePageEnabled;
|
||||
|
|
|
@ -268,6 +268,8 @@ 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);
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@class InviteSearchController;
|
||||
|
||||
@protocol JitsiMeetViewDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
@ -55,6 +57,15 @@
|
|||
*/
|
||||
- (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
|
||||
|
|
|
@ -37,6 +37,10 @@ export class App extends AbstractApp {
|
|||
static propTypes = {
|
||||
...AbstractApp.propTypes,
|
||||
|
||||
addPeopleEnabled: PropTypes.bool,
|
||||
|
||||
dialOutEnabled: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
|
||||
* button is rendered in the {@link Conference} view to afford entering
|
||||
|
|
|
@ -14,18 +14,14 @@ import { MultiSelectAutocomplete } from '../../base/react';
|
|||
import { inviteVideoRooms } from '../../videosipgw';
|
||||
|
||||
import {
|
||||
checkDialNumber,
|
||||
invitePeopleAndChatRooms,
|
||||
searchDirectory
|
||||
sendInvitesForItems,
|
||||
getInviteResultsForQuery
|
||||
} from '../functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
const isPhoneNumberRegex
|
||||
= new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$');
|
||||
|
||||
/**
|
||||
* The dialog that allows to invite people to the call.
|
||||
*/
|
||||
|
@ -240,20 +236,6 @@ class AddPeopleDialog extends Component<*, *> {
|
|||
);
|
||||
}
|
||||
|
||||
_getDigitsOnly: (string) => string;
|
||||
|
||||
/**
|
||||
* Removes all non-numeric characters from a string.
|
||||
*
|
||||
* @param {string} text - The string from which to remove all characters
|
||||
* except numbers.
|
||||
* @private
|
||||
* @returns {string} A string with only numbers.
|
||||
*/
|
||||
_getDigitsOnly(text = '') {
|
||||
return text.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for determining how many of each type of user is being invited.
|
||||
* Used for logging and sending analytics related to invites.
|
||||
|
@ -294,27 +276,6 @@ class AddPeopleDialog extends Component<*, *> {
|
|||
|| this.state.addToCallInProgress;
|
||||
}
|
||||
|
||||
_isMaybeAPhoneNumber: (string) => boolean;
|
||||
|
||||
/**
|
||||
* Checks whether a string looks like it could be for a phone number.
|
||||
*
|
||||
* @param {string} text - The text to check whether or not it could be a
|
||||
* phone number.
|
||||
* @private
|
||||
* @returns {boolean} True if the string looks like it could be a phone
|
||||
* number.
|
||||
*/
|
||||
_isMaybeAPhoneNumber(text) {
|
||||
if (!isPhoneNumberRegex.test(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const digits = this._getDigitsOnly(text);
|
||||
|
||||
return Boolean(digits.length);
|
||||
}
|
||||
|
||||
_onItemSelected: (Object) => Object;
|
||||
|
||||
/**
|
||||
|
@ -379,75 +340,26 @@ class AddPeopleDialog extends Component<*, *> {
|
|||
addToCallInProgress: true
|
||||
});
|
||||
|
||||
let allInvitePromises = [];
|
||||
let invitesLeftToSend = [
|
||||
...this.state.inviteItems
|
||||
];
|
||||
const {
|
||||
_conference,
|
||||
_inviteServiceUrl,
|
||||
_inviteUrl,
|
||||
_jwt
|
||||
} = this.props;
|
||||
|
||||
// First create all promises for dialing out.
|
||||
if (this.props.enableDialOut && this.props._conference) {
|
||||
const phoneNumbers = invitesLeftToSend.filter(
|
||||
({ item }) => item.type === 'phone');
|
||||
const inviteItems = this.state.inviteItems;
|
||||
const items = inviteItems.map(item => item.item);
|
||||
|
||||
// For each number, dial out. On success, remove the number from
|
||||
// {@link invitesLeftToSend}.
|
||||
const phoneInvitePromises = phoneNumbers.map(number => {
|
||||
const numberToInvite = this._getDigitsOnly(number.item.number);
|
||||
const options = {
|
||||
conference: _conference,
|
||||
inviteServiceUrl: _inviteServiceUrl,
|
||||
inviteUrl: _inviteUrl,
|
||||
inviteVideoRooms: this.props.inviteVideoRooms,
|
||||
jwt: _jwt
|
||||
};
|
||||
|
||||
return this.props._conference.dial(numberToInvite)
|
||||
.then(() => {
|
||||
invitesLeftToSend
|
||||
= invitesLeftToSend.filter(invite =>
|
||||
invite !== number);
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting phone number:', error));
|
||||
|
||||
});
|
||||
|
||||
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
|
||||
}
|
||||
|
||||
if (this.props.enableAddPeople) {
|
||||
const usersAndRooms = invitesLeftToSend.filter(i =>
|
||||
i.item.type === 'user' || i.item.type === 'room')
|
||||
.map(i => i.item);
|
||||
|
||||
if (usersAndRooms.length) {
|
||||
// Send a request to invite all the rooms and users. On success,
|
||||
// filter all rooms and users from {@link invitesLeftToSend}.
|
||||
const peopleInvitePromise = invitePeopleAndChatRooms(
|
||||
this.props._inviteServiceUrl,
|
||||
this.props._inviteUrl,
|
||||
this.props._jwt,
|
||||
usersAndRooms)
|
||||
.then(() => {
|
||||
invitesLeftToSend = invitesLeftToSend.filter(i =>
|
||||
i.item.type !== 'user' && i.item.type !== 'room');
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting people:', error));
|
||||
|
||||
allInvitePromises.push(peopleInvitePromise);
|
||||
}
|
||||
|
||||
// Sipgw calls are fire and forget. Invite them to the conference
|
||||
// then immediately remove them from {@link invitesLeftToSend}.
|
||||
const vrooms = invitesLeftToSend.filter(i =>
|
||||
i.item.type === 'videosipgw')
|
||||
.map(i => i.item);
|
||||
|
||||
this.props._conference
|
||||
&& vrooms.length > 0
|
||||
&& this.props.inviteVideoRooms(
|
||||
this.props._conference, vrooms);
|
||||
|
||||
invitesLeftToSend = invitesLeftToSend.filter(i =>
|
||||
i.item.type !== 'videosipgw');
|
||||
}
|
||||
|
||||
Promise.all(allInvitePromises)
|
||||
.then(() => {
|
||||
sendInvitesForItems(items, options)
|
||||
.then(invitesLeftToSend => {
|
||||
// If any invites are left that means something failed to send
|
||||
// so treat it as an error.
|
||||
if (invitesLeftToSend.length) {
|
||||
|
@ -467,8 +379,18 @@ class AddPeopleDialog extends Component<*, *> {
|
|||
addToCallError: true
|
||||
});
|
||||
|
||||
const unsentInviteIDs = invitesLeftToSend.map(invite =>
|
||||
invite.id || invite.number
|
||||
);
|
||||
|
||||
const itemsToSelect = inviteItems.filter(invite =>
|
||||
unsentInviteIDs.includes(
|
||||
invite.item.id || invite.item.number
|
||||
)
|
||||
);
|
||||
|
||||
if (this._multiselect) {
|
||||
this._multiselect.setSelectedItems(invitesLeftToSend);
|
||||
this._multiselect.setSelectedItems(itemsToSelect);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -558,82 +480,25 @@ class AddPeopleDialog extends Component<*, *> {
|
|||
* @returns {Promise}
|
||||
*/
|
||||
_query(query = '') {
|
||||
const text = query.trim();
|
||||
const {
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
_dialOutAuthUrl,
|
||||
_jwt,
|
||||
_peopleSearchQueryTypes,
|
||||
_peopleSearchUrl
|
||||
} = this.props;
|
||||
|
||||
let peopleSearchPromise;
|
||||
const options = {
|
||||
dialOutAuthUrl: _dialOutAuthUrl,
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
jwt: _jwt,
|
||||
peopleSearchQueryTypes: _peopleSearchQueryTypes,
|
||||
peopleSearchUrl: _peopleSearchUrl
|
||||
};
|
||||
|
||||
if (this.props.enableAddPeople && text) {
|
||||
peopleSearchPromise = searchDirectory(
|
||||
_peopleSearchUrl,
|
||||
_jwt,
|
||||
text,
|
||||
_peopleSearchQueryTypes);
|
||||
} else {
|
||||
peopleSearchPromise = Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
||||
const hasCountryCode = text.startsWith('+');
|
||||
let phoneNumberPromise;
|
||||
|
||||
if (this.props.enableDialOut && this._isMaybeAPhoneNumber(text)) {
|
||||
let numberToVerify = text;
|
||||
|
||||
// When the number to verify does not start with a +, we assume no
|
||||
// proper country code has been entered. In such a case, prepend 1
|
||||
// for the country code. The service currently takes care of
|
||||
// prepending the +.
|
||||
if (!hasCountryCode && !text.startsWith('1')) {
|
||||
numberToVerify = `1${numberToVerify}`;
|
||||
}
|
||||
|
||||
// The validation service works properly when the query is digits
|
||||
// only so ensure only digits get sent.
|
||||
numberToVerify = this._getDigitsOnly(numberToVerify);
|
||||
|
||||
phoneNumberPromise
|
||||
= checkDialNumber(numberToVerify, _dialOutAuthUrl);
|
||||
} else {
|
||||
phoneNumberPromise = Promise.resolve({});
|
||||
}
|
||||
|
||||
return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
|
||||
.then(([ peopleResults, phoneResults ]) => {
|
||||
const results = [
|
||||
...peopleResults
|
||||
];
|
||||
|
||||
/**
|
||||
* This check for phone results is for the day the call to
|
||||
* searching people might return phone results as well. When
|
||||
* that day comes this check will make it so the server checks
|
||||
* are honored and the local appending of the number is not
|
||||
* done. The local appending of the phone number can then be
|
||||
* cleaned up when convenient.
|
||||
*/
|
||||
const hasPhoneResult = peopleResults.find(
|
||||
result => result.type === 'phone');
|
||||
|
||||
if (!hasPhoneResult
|
||||
&& typeof phoneResults.allow === 'boolean') {
|
||||
results.push({
|
||||
allowed: phoneResults.allow,
|
||||
country: phoneResults.country,
|
||||
type: 'phone',
|
||||
number: phoneResults.phone,
|
||||
originalEntry: text,
|
||||
showCountryCodeReminder: !hasCountryCode
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
return getInviteResultsForQuery(query, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { launchNativeInvite } from '../../mobile/invite-search';
|
||||
import { ToolbarButton } from '../../toolbox';
|
||||
|
||||
/**
|
||||
* The type of {@link EnterPictureInPictureToobarButton}'s React
|
||||
* {@code Component} props.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the "Add to call" feature is available.
|
||||
*/
|
||||
enableAddPeople: boolean,
|
||||
|
||||
/**
|
||||
* Indicates if the "Dial out" feature is available.
|
||||
*/
|
||||
enableDialOut: boolean,
|
||||
|
||||
/**
|
||||
* Launches native invite dialog.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
onLaunchNativeInvite: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a {@link ToolbarButton} to enter Picture-in-Picture.
|
||||
*/
|
||||
class InviteButton extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
onLaunchNativeInvite,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
if (!enableAddPeople && !enableDialOut) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
iconName = { 'add' }
|
||||
onClick = { onLaunchNativeInvite }
|
||||
{ ...props } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux actions to {@link InviteButton}'s React
|
||||
* {@code Component} props.
|
||||
*
|
||||
* @param {Function} dispatch - The redux action {@code dispatch} function.
|
||||
* @returns {{
|
||||
* onLaunchNativeInvite
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
|
||||
/**
|
||||
* Launches native invite dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
* @type {Function}
|
||||
*/
|
||||
onLaunchNativeInvite() {
|
||||
dispatch(launchNativeInvite());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(undefined, _mapDispatchToProps)(InviteButton);
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
|
||||
import { doGetJSON } from '../base/util';
|
||||
|
||||
declare var $: Function;
|
||||
|
@ -50,7 +51,7 @@ export function getDialInNumbers(url: string): Promise<*> {
|
|||
* type items to invite.
|
||||
* @returns {Promise} - The promise created by the request.
|
||||
*/
|
||||
export function invitePeopleAndChatRooms( // eslint-disable-line max-params
|
||||
function invitePeopleAndChatRooms( // eslint-disable-line max-params
|
||||
inviteServiceUrl: string,
|
||||
inviteUrl: string,
|
||||
jwt: string,
|
||||
|
@ -88,9 +89,10 @@ export function searchDirectory( // eslint-disable-line max-params
|
|||
text: string,
|
||||
queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
|
||||
): Promise<Array<Object>> {
|
||||
const queryTypesString = JSON.stringify(queryTypes);
|
||||
const query = encodeURIComponent(text);
|
||||
const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
|
||||
|
||||
return fetch(`${serviceUrl}?query=${encodeURIComponent(text)}&queryTypes=${
|
||||
return fetch(`${serviceUrl}?query=${query}&queryTypes=${
|
||||
queryTypesString}&jwt=${jwt}`)
|
||||
.then(response => {
|
||||
const jsonify = response.json();
|
||||
|
@ -110,6 +112,21 @@ export function searchDirectory( // eslint-disable-line max-params
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -137,3 +154,315 @@ export function checkDialNumber(
|
|||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all non-numeric characters from a string.
|
||||
*
|
||||
* @param {string} text - The string from which to remove all characters
|
||||
* except numbers.
|
||||
* @private
|
||||
* @returns {string} A string with only numbers.
|
||||
*/
|
||||
function getDigitsOnly(text: string = ''): string {
|
||||
return text.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the options to use when sending a search query.
|
||||
*/
|
||||
export type GetInviteResultsOptions = {
|
||||
|
||||
/**
|
||||
* The endpoint to use for checking phone number validity.
|
||||
*/
|
||||
dialOutAuthUrl: string,
|
||||
|
||||
/**
|
||||
* Whether or not to search for people.
|
||||
*/
|
||||
enableAddPeople: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not to check phone numbers.
|
||||
*/
|
||||
enableDialOut: boolean,
|
||||
|
||||
/**
|
||||
* Array with the query types that will be executed -
|
||||
* "conferenceRooms" | "user" | "room".
|
||||
*/
|
||||
peopleSearchQueryTypes: Array<string>,
|
||||
|
||||
/**
|
||||
* The url to query for people.
|
||||
*/
|
||||
peopleSearchUrl: string,
|
||||
|
||||
/**
|
||||
* The jwt token to pass to the search service.
|
||||
*/
|
||||
jwt: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines directory search with phone number validation to produce a single
|
||||
* set of invite search results.
|
||||
*
|
||||
* @param {string} query - Text to search.
|
||||
* @param {GetInviteResultsOptions} options - Options to use when searching.
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export function getInviteResultsForQuery(
|
||||
query: string,
|
||||
options: GetInviteResultsOptions): Promise<*> {
|
||||
const text = query.trim();
|
||||
|
||||
const {
|
||||
dialOutAuthUrl,
|
||||
enableAddPeople,
|
||||
enableDialOut,
|
||||
peopleSearchQueryTypes,
|
||||
peopleSearchUrl,
|
||||
jwt
|
||||
} = options;
|
||||
|
||||
let peopleSearchPromise;
|
||||
|
||||
if (enableAddPeople && text) {
|
||||
peopleSearchPromise = searchDirectory(
|
||||
peopleSearchUrl,
|
||||
jwt,
|
||||
text,
|
||||
peopleSearchQueryTypes);
|
||||
} else {
|
||||
peopleSearchPromise = Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
||||
const hasCountryCode = text.startsWith('+');
|
||||
let phoneNumberPromise;
|
||||
|
||||
if (enableDialOut && isMaybeAPhoneNumber(text)) {
|
||||
let numberToVerify = text;
|
||||
|
||||
// When the number to verify does not start with a +, we assume no
|
||||
// proper country code has been entered. In such a case, prepend 1
|
||||
// for the country code. The service currently takes care of
|
||||
// prepending the +.
|
||||
if (!hasCountryCode && !text.startsWith('1')) {
|
||||
numberToVerify = `1${numberToVerify}`;
|
||||
}
|
||||
|
||||
// The validation service works properly when the query is digits
|
||||
// only so ensure only digits get sent.
|
||||
numberToVerify = getDigitsOnly(numberToVerify);
|
||||
|
||||
phoneNumberPromise
|
||||
= checkDialNumber(numberToVerify, dialOutAuthUrl);
|
||||
} else {
|
||||
phoneNumberPromise = Promise.resolve({});
|
||||
}
|
||||
|
||||
return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
|
||||
.then(([ peopleResults, phoneResults ]) => {
|
||||
const results = [
|
||||
...peopleResults
|
||||
];
|
||||
|
||||
/**
|
||||
* This check for phone results is for the day the call to
|
||||
* searching people might return phone results as well. When
|
||||
* that day comes this check will make it so the server checks
|
||||
* are honored and the local appending of the number is not
|
||||
* done. The local appending of the phone number can then be
|
||||
* cleaned up when convenient.
|
||||
*/
|
||||
const hasPhoneResult = peopleResults.find(
|
||||
result => result.type === 'phone');
|
||||
|
||||
if (!hasPhoneResult
|
||||
&& typeof phoneResults.allow === 'boolean') {
|
||||
results.push({
|
||||
allowed: phoneResults.allow,
|
||||
country: phoneResults.country,
|
||||
type: 'phone',
|
||||
number: phoneResults.phone,
|
||||
originalEntry: text,
|
||||
showCountryCodeReminder: !hasCountryCode
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string looks like it could be for a phone number.
|
||||
*
|
||||
* @param {string} text - The text to check whether or not it could be a
|
||||
* phone number.
|
||||
* @private
|
||||
* @returns {boolean} True if the string looks like it could be a phone
|
||||
* number.
|
||||
*/
|
||||
function isMaybeAPhoneNumber(text: string): boolean {
|
||||
if (!isPhoneNumberRegex().test(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const digits = getDigitsOnly(text);
|
||||
|
||||
return Boolean(digits.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the options to use when sending invites.
|
||||
*/
|
||||
export type SendInvitesOptions = {
|
||||
|
||||
/**
|
||||
* Conference object used to dial out.
|
||||
*/
|
||||
conference: Object,
|
||||
|
||||
/**
|
||||
* The URL to send invites through.
|
||||
*/
|
||||
inviteServiceUrl: string,
|
||||
|
||||
/**
|
||||
* The URL sent with each invite.
|
||||
*/
|
||||
inviteUrl: string,
|
||||
|
||||
/**
|
||||
* The function to use to invite video rooms.
|
||||
*
|
||||
* @param {Object} The conference to which the video rooms should be
|
||||
* invited.
|
||||
* @param {Array<Object>} The list of rooms that should be invited.
|
||||
* @returns {void}
|
||||
*/
|
||||
inviteVideoRooms: (Object, Array<Object>) => void,
|
||||
|
||||
/**
|
||||
* The jwt token to pass to the invite service.
|
||||
*/
|
||||
jwt: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Send invites for a list of items (may be a combination of users, rooms, phone
|
||||
* numbers, and video rooms).
|
||||
*
|
||||
* @param {Array<Object>} invites - Items for which invites should be sent.
|
||||
* @param {SendInvitesOptions} options - Options to use when sending the
|
||||
* provided invites.
|
||||
* @returns {Promise} Promise containing the list of invites that were not sent.
|
||||
*/
|
||||
export function sendInvitesForItems(
|
||||
invites: Array<Object>,
|
||||
options: SendInvitesOptions
|
||||
): Promise<Array<Object>> {
|
||||
|
||||
const {
|
||||
conference,
|
||||
inviteServiceUrl,
|
||||
inviteUrl,
|
||||
inviteVideoRooms,
|
||||
jwt
|
||||
} = options;
|
||||
|
||||
let allInvitePromises = [];
|
||||
let invitesLeftToSend = [ ...invites ];
|
||||
|
||||
// First create all promises for dialing out.
|
||||
if (conference) {
|
||||
const phoneNumbers = invitesLeftToSend.filter(
|
||||
item => item.type === 'phone');
|
||||
|
||||
// For each number, dial out. On success, remove the number from
|
||||
// {@link invitesLeftToSend}.
|
||||
const phoneInvitePromises = phoneNumbers.map(item => {
|
||||
const numberToInvite = getDigitsOnly(item.number);
|
||||
|
||||
return conference.dial(numberToInvite)
|
||||
.then(() => {
|
||||
invitesLeftToSend
|
||||
= invitesLeftToSend.filter(invite =>
|
||||
invite !== item);
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting phone number:', error));
|
||||
|
||||
});
|
||||
|
||||
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
|
||||
}
|
||||
|
||||
const usersAndRooms = invitesLeftToSend.filter(item =>
|
||||
item.type === 'user' || item.type === 'room');
|
||||
|
||||
if (usersAndRooms.length) {
|
||||
// Send a request to invite all the rooms and users. On success,
|
||||
// filter all rooms and users from {@link invitesLeftToSend}.
|
||||
const peopleInvitePromise = invitePeopleAndChatRooms(
|
||||
inviteServiceUrl,
|
||||
inviteUrl,
|
||||
jwt,
|
||||
usersAndRooms)
|
||||
.then(() => {
|
||||
invitesLeftToSend = invitesLeftToSend.filter(item =>
|
||||
item.type !== 'user' && item.type !== 'room');
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting people:', error));
|
||||
|
||||
allInvitePromises.push(peopleInvitePromise);
|
||||
}
|
||||
|
||||
// Sipgw calls are fire and forget. Invite them to the conference
|
||||
// then immediately remove them from {@link invitesLeftToSend}.
|
||||
const vrooms = invitesLeftToSend.filter(item =>
|
||||
item.type === 'videosipgw');
|
||||
|
||||
conference
|
||||
&& vrooms.length > 0
|
||||
&& inviteVideoRooms(conference, vrooms);
|
||||
|
||||
invitesLeftToSend = invitesLeftToSend.filter(item =>
|
||||
item.type !== 'videosipgw');
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export * from './actions';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
import './reducer';
|
||||
import './middleware';
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* 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');
|
|
@ -0,0 +1,50 @@
|
|||
// @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
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
|
||||
import './reducer';
|
||||
import './middleware';
|
|
@ -0,0 +1,233 @@
|
|||
/* @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));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
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;
|
||||
});
|
|
@ -14,6 +14,11 @@ import {
|
|||
isNarrowAspectRatio,
|
||||
makeAspectRatioAware
|
||||
} from '../../base/responsive-ui';
|
||||
import {
|
||||
InviteButton,
|
||||
isAddPeopleEnabled,
|
||||
isDialOutEnabled
|
||||
} from '../../invite';
|
||||
import {
|
||||
EnterPictureInPictureToolbarButton
|
||||
} from '../../mobile/picture-in-picture';
|
||||
|
@ -39,7 +44,7 @@ import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons';
|
|||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
const _SHARE_ROOM_TOOLBAR_BUTTON = true;
|
||||
const _SHARE_ROOM_TOOLBAR_BUTTON = false;
|
||||
|
||||
/**
|
||||
* The type of {@link Toolbox}'s React {@code Component} props.
|
||||
|
@ -56,6 +61,18 @@ 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.
|
||||
*/
|
||||
|
@ -212,9 +229,13 @@ 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 (
|
||||
|
@ -252,7 +273,7 @@ class Toolbox extends Component<Props> {
|
|||
style = { style }
|
||||
underlayColor = { underlayColor } />
|
||||
{
|
||||
_SHARE_ROOM_TOOLBAR_BUTTON
|
||||
_SHARE_ROOM_TOOLBAR_BUTTON && !showInviteButton
|
||||
&& <ToolbarButton
|
||||
iconName = 'link'
|
||||
iconStyle = { iconStyle }
|
||||
|
@ -260,6 +281,15 @@ class Toolbox extends Component<Props> {
|
|||
style = { style }
|
||||
underlayColor = { underlayColor } />
|
||||
}
|
||||
{
|
||||
showInviteButton
|
||||
&& <InviteButton
|
||||
enableAddPeople = { enableAddPeople }
|
||||
enableDialOut = { enableDialOut }
|
||||
iconStyle = { iconStyle }
|
||||
style = { style }
|
||||
underlayColor = { underlayColor } />
|
||||
}
|
||||
<EnterPictureInPictureToolbarButton
|
||||
iconStyle = { iconStyle }
|
||||
style = { style }
|
||||
|
@ -388,6 +418,22 @@ 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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue