Selaa lähdekoodia

[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>
master
Ryan Peck 7 vuotta sitten
vanhempi
commit
f64c13d4b7

+ 7
- 0
android/app/src/main/java/org/jitsi/meet/MainActivity.java Näytä tiedosto

@@ -19,12 +19,14 @@ package org.jitsi.meet;
19 19
 import android.os.Bundle;
20 20
 import android.util.Log;
21 21
 
22
+import org.jitsi.meet.sdk.InviteSearchController;
22 23
 import org.jitsi.meet.sdk.JitsiMeetActivity;
23 24
 import org.jitsi.meet.sdk.JitsiMeetView;
24 25
 import org.jitsi.meet.sdk.JitsiMeetViewListener;
25 26
 
26 27
 import com.calendarevents.CalendarEventsPackage;
27 28
 
29
+import java.util.HashMap;
28 30
 import java.util.Map;
29 31
 
30 32
 /**
@@ -84,6 +86,11 @@ public class MainActivity extends JitsiMeetActivity {
84 86
                     on("CONFERENCE_WILL_LEAVE", data);
85 87
                 }
86 88
 
89
+                @Override
90
+                public void launchNativeInvite(InviteSearchController inviteSearchController) {
91
+                    on("LAUNCH_NATIVE_INVITE", new HashMap<String, Object>());
92
+                }
93
+
87 94
                 @Override
88 95
                 public void onLoadConfigError(Map<String, Object> data) {
89 96
                     on("LOAD_CONFIG_ERROR", data);

+ 184
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchController.java Näytä tiedosto

@@ -0,0 +1,184 @@
1
+package org.jitsi.meet.sdk;
2
+
3
+import android.util.Log;
4
+
5
+import com.facebook.react.bridge.ReadableArray;
6
+import com.facebook.react.bridge.ReadableMap;
7
+import com.facebook.react.bridge.WritableArray;
8
+import com.facebook.react.bridge.WritableNativeArray;
9
+import com.facebook.react.bridge.WritableNativeMap;
10
+
11
+import java.lang.ref.WeakReference;
12
+import java.util.ArrayList;
13
+import java.util.HashMap;
14
+import java.util.List;
15
+import java.util.Map;
16
+import java.util.UUID;
17
+
18
+/**
19
+ * Controller object used by native code to query and submit user selections for the user invitation flow.
20
+ */
21
+public class InviteSearchController {
22
+
23
+    /**
24
+     * The InviteSearchControllerDelegate for this controller, used to pass query
25
+     * results back to the native code that initiated the query.
26
+     */
27
+    private InviteSearchControllerDelegate searchControllerDelegate;
28
+
29
+    /**
30
+     * Local cache of search query results.  Used to re-hydrate the list
31
+     * of selected items based on their ids passed to submitSelectedItemIds
32
+     * in order to pass the full item maps back to the JitsiMeetView during submission.
33
+     */
34
+    private Map<String, ReadableMap> items = new HashMap<>();
35
+
36
+    /**
37
+     * Randomly generated UUID, used for identification in the InviteSearchModule
38
+     */
39
+    private String uuid = UUID.randomUUID().toString();
40
+
41
+    private WeakReference<InviteSearchModule> parentModuleRef;
42
+
43
+    public InviteSearchController(InviteSearchModule module) {
44
+        parentModuleRef = new WeakReference<>(module);
45
+    }
46
+
47
+    /**
48
+     * Start a search for entities to invite with the given query.
49
+     * Results will be returned through the associated InviteSearchControllerDelegate's
50
+     * onReceiveResults method.
51
+     *
52
+     * @param query
53
+     */
54
+    public void performQuery(String query) {
55
+        JitsiMeetView.onInviteQuery(query, uuid);
56
+    }
57
+
58
+    /**
59
+     * Send invites to selected users based on their item ids
60
+     *
61
+     * @param ids
62
+     */
63
+    public void submitSelectedItemIds(List<String> ids) {
64
+        WritableArray selectedItems = new WritableNativeArray();
65
+        for(int i=0; i<ids.size(); i++) {
66
+            if(items.containsKey(ids.get(i))) {
67
+                WritableNativeMap map = new WritableNativeMap();
68
+                map.merge(items.get(ids.get(i)));
69
+                selectedItems.pushMap(map);
70
+            } else {
71
+                // if the id doesn't exist in the map, we can't do anything, so just skip it
72
+            }
73
+        }
74
+
75
+        JitsiMeetView.submitSelectedItems(selectedItems, uuid);
76
+    }
77
+
78
+    /**
79
+     * Caches results received by the search into a local map for use
80
+     * later when the items are submitted.  Submission requires the full
81
+     * map of information, but only the IDs are returned back to the delegate.
82
+     * Using this map means we don't have to send the whole map back to the delegate.
83
+     *
84
+     * @param results
85
+     * @param query
86
+     */
87
+    void receivedResultsForQuery(ReadableArray results, String query) {
88
+
89
+        List<Map<String, Object>> jvmResults = new ArrayList<>();
90
+        // cache results for use in submission later
91
+        // convert to jvm array
92
+        for(int i=0; i<results.size(); i++) {
93
+            ReadableMap map = results.getMap(i);
94
+            if(map.hasKey("id")) {
95
+                items.put(map.getString("id"), map);
96
+            } else if(map.hasKey("type") && map.getString("type").equals("phone") && map.hasKey("number")) {
97
+                items.put(map.getString("number"), map);
98
+            } else {
99
+                Log.w("InviteSearchController", "Received result without id and that was not a phone number, so not adding it to suggestions: " + map);
100
+            }
101
+
102
+            jvmResults.add(map.toHashMap());
103
+        }
104
+
105
+
106
+        searchControllerDelegate.onReceiveResults(this, jvmResults, query);
107
+    }
108
+
109
+    /**
110
+     *
111
+     * @return the InviteSearchControllerDelegate for this controller, used to pass query
112
+     * results back to the native code that initiated the query.
113
+     */
114
+    public InviteSearchControllerDelegate getSearchControllerDelegate() {
115
+        return searchControllerDelegate;
116
+    }
117
+
118
+    /**
119
+     * Sets the InviteSearchControllerDelegate for this controller, used to pass query results
120
+     * back to the native code that initiated the query.
121
+     *
122
+     * @param searchControllerDelegate
123
+     */
124
+    public void setSearchControllerDelegate(InviteSearchControllerDelegate searchControllerDelegate) {
125
+        this.searchControllerDelegate = searchControllerDelegate;
126
+    }
127
+
128
+    /**
129
+     * Cancel the invitation flow and free memory allocated to the InviteSearchController.  After
130
+     * calling this method, this object is invalid - a new InviteSearchController will be passed
131
+     * to the caller through launchNativeInvite.
132
+     */
133
+    public void cancelSearch() {
134
+        InviteSearchModule parentModule = parentModuleRef.get();
135
+        if(parentModule != null) {
136
+            parentModule.removeSearchController(uuid);
137
+        }
138
+    }
139
+
140
+    /**
141
+     * @return the unique identifier for this InviteSearchController
142
+     */
143
+    public String getUuid() {
144
+        return uuid;
145
+    }
146
+
147
+    public interface InviteSearchControllerDelegate {
148
+        /**
149
+         * Called when results are received for a query called through InviteSearchController.query()
150
+         *
151
+         * @param searchController
152
+         * @param results a List of Map<String, Object> objects that represent items returned by the query.
153
+         *                The object at key "type" describes the type of item: "user", "videosipgw" (conference room), or "phone".
154
+         *                "user" types have properties at "id", "name", and "avatar"
155
+         *                "videosipgw" types have properties at "id" and "name"
156
+         *                "phone" types have properties at "number", "title", "and "subtitle"
157
+         * @param query the query that generated the given results
158
+         */
159
+        void onReceiveResults(InviteSearchController searchController, List<Map<String, Object>> results, String query);
160
+
161
+        /**
162
+         * Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes successfully
163
+         * and invitations are sent to all given IDs.
164
+         *
165
+         * @param searchController the active {@link InviteSearchController} for this invite flow.  This object will be
166
+         *                         cleaned up after the call to inviteSucceeded completes.
167
+         */
168
+        void inviteSucceeded(InviteSearchController searchController);
169
+
170
+        /**
171
+         * Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes, but the
172
+         * invitation fails for one or more of the selected items.
173
+         *
174
+         * @param searchController the active {@link InviteSearchController} for this invite flow.  This object
175
+         *                         should be cleaned up by calling {@link InviteSearchController#cancelSearch()} if
176
+         *                         the user exits the invite flow.  Otherwise, it can stay active if the user
177
+         *                         will attempt to invite
178
+         * @param failedInviteItems a {@code List} of {@code Map<String, Object>} dictionaries that represent the
179
+         *                          invitations that failed.  The data type of the objects is identical to the results
180
+         *                          returned in onReceiveResuls.
181
+         */
182
+        void inviteFailed(InviteSearchController searchController, List<Map<String, Object>> failedInviteItems);
183
+    }
184
+}

+ 126
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/InviteSearchModule.java Näytä tiedosto

@@ -0,0 +1,126 @@
1
+package org.jitsi.meet.sdk;
2
+
3
+import android.util.Log;
4
+
5
+import com.facebook.react.bridge.ReactApplicationContext;
6
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
7
+import com.facebook.react.bridge.ReactMethod;
8
+import com.facebook.react.bridge.ReadableArray;
9
+import com.facebook.react.bridge.ReadableMap;
10
+
11
+import java.util.ArrayList;
12
+import java.util.HashMap;
13
+import java.util.Map;
14
+
15
+/**
16
+ * Native module for Invite Search
17
+ */
18
+class InviteSearchModule extends ReactContextBaseJavaModule {
19
+
20
+    /**
21
+     * Map of InviteSearchController objects passed to connected JitsiMeetView.
22
+     * A call to launchNativeInvite will create a new InviteSearchController and pass
23
+     * it back to the caller.  On a successful invitation, the controller will be removed automatically.
24
+     * On a failed invitation, the caller has the option of calling InviteSearchController#cancelSearch()
25
+     * to remove the controller from this map.  The controller should also be removed if the user cancels
26
+     * the invitation flow.
27
+     */
28
+    private Map<String, InviteSearchController> searchControllers = new HashMap<>();
29
+
30
+    public InviteSearchModule(ReactApplicationContext reactContext) {
31
+        super(reactContext);
32
+    }
33
+
34
+    /**
35
+     * Launch the native user invite flow
36
+     *
37
+     * @param externalAPIScope a string that represents a connection to a specific JitsiMeetView
38
+     */
39
+    @ReactMethod
40
+    public void launchNativeInvite(String externalAPIScope) {
41
+        JitsiMeetView viewToLaunchInvite = JitsiMeetView.findViewByExternalAPIScope(externalAPIScope);
42
+
43
+        if(viewToLaunchInvite == null) {
44
+            return;
45
+        }
46
+
47
+        if(viewToLaunchInvite.getListener() == null) {
48
+            return;
49
+        }
50
+
51
+        InviteSearchController controller = createSearchController();
52
+        viewToLaunchInvite.getListener().launchNativeInvite(controller);
53
+    }
54
+
55
+    /**
56
+     * Callback for results received from the JavaScript invite search call
57
+     *
58
+     * @param results the results in a ReadableArray of ReadableMap objects
59
+     * @param query the query associated with the search
60
+     * @param inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController
61
+     */
62
+    @ReactMethod
63
+    public void receivedResults(ReadableArray results, String query, String inviteSearchControllerScope) {
64
+        InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
65
+
66
+        if(controller == null) {
67
+            Log.w("InviteSearchModule", "Received results, but unable to find active controller to send results back");
68
+            return;
69
+        }
70
+
71
+        controller.receivedResultsForQuery(results, query);
72
+
73
+    }
74
+
75
+    /**
76
+     * Callback for invitation failures
77
+     *
78
+     * @param items the items for which the invitation failed
79
+     * @param inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController
80
+     */
81
+    @ReactMethod
82
+    public void inviteFailedForItems(ReadableArray items, String inviteSearchControllerScope) {
83
+        InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
84
+
85
+        if(controller == null) {
86
+            Log.w("InviteSearchModule", "Invite failed, but unable to find active controller to notify");
87
+            return;
88
+        }
89
+
90
+        ArrayList<Map<String, Object>> jvmItems = new ArrayList<>();
91
+        for(int i=0; i<items.size(); i++) {
92
+            ReadableMap item = items.getMap(i);
93
+            jvmItems.add(item.toHashMap());
94
+        }
95
+
96
+        controller.getSearchControllerDelegate().inviteFailed(controller, jvmItems);
97
+    }
98
+
99
+    @ReactMethod
100
+    public void inviteSucceeded(String inviteSearchControllerScope) {
101
+        InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
102
+
103
+        if(controller == null) {
104
+            Log.w("InviteSearchModule", "Invite succeeded, but unable to find active controller to notify");
105
+            return;
106
+        }
107
+
108
+        controller.getSearchControllerDelegate().inviteSucceeded(controller);
109
+        searchControllers.remove(inviteSearchControllerScope);
110
+    }
111
+
112
+    void removeSearchController(String inviteSearchControllerUuid) {
113
+        searchControllers.remove(inviteSearchControllerUuid);
114
+    }
115
+
116
+    @Override
117
+    public String getName() {
118
+        return "InviteSearch";
119
+    }
120
+
121
+    private InviteSearchController createSearchController() {
122
+        InviteSearchController searchController = new InviteSearchController(this);
123
+        searchControllers.put(searchController.getUuid(), searchController);
124
+        return searchController;
125
+    }
126
+}

+ 72
- 3
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java Näytä tiedosto

@@ -32,7 +32,9 @@ import com.facebook.react.ReactRootView;
32 32
 import com.facebook.react.bridge.NativeModule;
33 33
 import com.facebook.react.bridge.ReactApplicationContext;
34 34
 import com.facebook.react.bridge.ReactContext;
35
+import com.facebook.react.bridge.WritableArray;
35 36
 import com.facebook.react.bridge.WritableMap;
37
+import com.facebook.react.bridge.WritableNativeMap;
36 38
 import com.facebook.react.common.LifecycleState;
37 39
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
38 40
 import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -42,6 +44,7 @@ import java.net.URL;
42 44
 import java.util.Arrays;
43 45
 import java.util.Collections;
44 46
 import java.util.List;
47
+import java.util.Map;
45 48
 import java.util.Set;
46 49
 import java.util.UUID;
47 50
 import java.util.WeakHashMap;
@@ -75,6 +78,7 @@ public class JitsiMeetView extends FrameLayout {
75 78
             new AppInfoModule(reactContext),
76 79
             new AudioModeModule(reactContext),
77 80
             new ExternalAPIModule(reactContext),
81
+            new InviteSearchModule(reactContext),
78 82
             new PictureInPictureModule(reactContext),
79 83
             new ProximityModule(reactContext),
80 84
             new WiFiStatsModule(reactContext),
@@ -268,15 +272,43 @@ public class JitsiMeetView extends FrameLayout {
268 272
         sendEvent("onUserLeaveHint", null);
269 273
     }
270 274
 
275
+    /**
276
+     * Starts a query for users to invite to the conference.  Results will be
277
+     * returned through the {@link InviteSearchController.InviteSearchControllerDelegate#onReceiveResults(InviteSearchController, List, String)}
278
+     * method.
279
+     *
280
+     * @param query {@code String} to use for the query
281
+     */
282
+    public static void onInviteQuery(String query, String inviteSearchControllerScope) {
283
+        WritableNativeMap params = new WritableNativeMap();
284
+        params.putString("query", query);
285
+        params.putString("inviteScope", inviteSearchControllerScope);
286
+        sendEvent("performQueryAction", params);
287
+    }
288
+
289
+    /**
290
+     * Sends JavaScript event to submit invitations to the given item ids
291
+     *
292
+     * @param selectedItems a WritableArray of WritableNativeMaps representing selected items.
293
+     *                  Each map representing a selected item should match the data passed
294
+     *                  back in the return from a query.
295
+     */
296
+    public static void submitSelectedItems(WritableArray selectedItems, String inviteSearchControllerScope) {
297
+        WritableNativeMap params = new WritableNativeMap();
298
+        params.putArray("selectedItems", selectedItems);
299
+        params.putString("inviteScope", inviteSearchControllerScope);
300
+        sendEvent("performSubmitInviteAction", params);
301
+    }
302
+
271 303
     /**
272 304
      * Helper function to send an event to JavaScript.
273 305
      *
274 306
      * @param eventName {@code String} containing the event name.
275
-     * @param params {@code WritableMap} optional ancillary data for the event.
307
+     * @param data {@code Object} optional ancillary data for the event.
276 308
      */
277 309
     private static void sendEvent(
278 310
             String eventName,
279
-            @Nullable WritableMap params) {
311
+            @Nullable Object data) {
280 312
         if (reactInstanceManager != null) {
281 313
             ReactContext reactContext
282 314
                 = reactInstanceManager.getCurrentReactContext();
@@ -284,11 +316,16 @@ public class JitsiMeetView extends FrameLayout {
284 316
                 reactContext
285 317
                     .getJSModule(
286 318
                         DeviceEventManagerModule.RCTDeviceEventEmitter.class)
287
-                    .emit(eventName, params);
319
+                    .emit(eventName, data);
288 320
             }
289 321
         }
290 322
     }
291 323
 
324
+    /**
325
+     * Whether user invitation is enabled.
326
+     */
327
+    private boolean addPeopleEnabled;
328
+
292 329
     /**
293 330
      * The default base {@code URL} used to join a conference when a partial URL
294 331
      * (e.g. a room name only) is specified to {@link #loadURLString(String)} or
@@ -296,6 +333,11 @@ public class JitsiMeetView extends FrameLayout {
296 333
      */
297 334
     private URL defaultURL;
298 335
 
336
+    /**
337
+     * Whether the ability to add users by phone number is enabled.
338
+     */
339
+    private boolean dialOutEnabled;
340
+
299 341
     /**
300 342
      * The unique identifier of this {@code JitsiMeetView} within the process
301 343
      * for the purposes of {@link ExternalAPI}. The name scope was inspired by
@@ -454,6 +496,9 @@ public class JitsiMeetView extends FrameLayout {
454 496
         // welcomePageEnabled
455 497
         props.putBoolean("welcomePageEnabled", welcomePageEnabled);
456 498
 
499
+        props.putBoolean("addPeopleEnabled", addPeopleEnabled);
500
+        props.putBoolean("dialOutEnabled", dialOutEnabled);
501
+
457 502
         // XXX The method loadURLObject: is supposed to be imperative i.e.
458 503
         // a second invocation with one and the same URL is expected to join
459 504
         // the respective conference again if the first invocation was followed
@@ -535,6 +580,18 @@ public class JitsiMeetView extends FrameLayout {
535 580
         }
536 581
     }
537 582
 
583
+    /**
584
+     * Sets whether the ability to add users to the call is enabled.
585
+     * If this is enabled, an add user button will appear on the {@link JitsiMeetView}.
586
+     * If enabled, and the user taps the add user button,
587
+     * {@link JitsiMeetViewListener#launchNativeInvite(Map)} will be called.
588
+     *
589
+     * @param addPeopleEnabled {@code true} to enable the add people button; otherwise, {@code false}
590
+     */
591
+    public void setAddPeopleEnabled(boolean addPeopleEnabled) {
592
+        this.addPeopleEnabled = addPeopleEnabled;
593
+    }
594
+
538 595
     /**
539 596
      * Sets the default base {@code URL} used to join a conference when a
540 597
      * partial URL (e.g. a room name only) is specified to
@@ -548,6 +605,18 @@ public class JitsiMeetView extends FrameLayout {
548 605
         this.defaultURL = defaultURL;
549 606
     }
550 607
 
608
+    /**
609
+     * Sets whether the ability to add phone numbers to the call is enabled.
610
+     * Must be enabled along with {@link #setAddPeopleEnabled(boolean)} to
611
+     * be effective.
612
+     *
613
+     * @param dialOutEnabled {@code true} to enable the ability to add
614
+     *                       phone numbers to the call; otherwise, {@code false}
615
+     */
616
+    public void setDialOutEnabled(boolean dialOutEnabled) {
617
+        this.dialOutEnabled = dialOutEnabled;
618
+    }
619
+
551 620
     /**
552 621
      * Sets a specific {@link JitsiMeetViewListener} on this
553 622
      * {@code JitsiMeetView}.

+ 4
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewAdapter.java Näytä tiedosto

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

+ 10
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetViewListener.java Näytä tiedosto

@@ -59,6 +59,16 @@ public interface JitsiMeetViewListener {
59 59
      */
60 60
     void onConferenceWillLeave(Map<String, Object> data);
61 61
 
62
+    /**
63
+     * Called when the add user button is tapped.
64
+     *
65
+     * @param inviteSearchController {@code InviteSearchController} scoped
66
+     * for this user invite flow. The {@code InviteSearchController} is used
67
+     * to start user queries and accepts an {@code InviteSearchControllerDelegate}
68
+     * for receiving user query responses.
69
+     */
70
+    void launchNativeInvite(InviteSearchController inviteSearchController);
71
+
62 72
     /**
63 73
      * Called when loading the main configuration file from the Jitsi Meet
64 74
      * deployment fails.

+ 8
- 0
ios/sdk/sdk.xcodeproj/project.pbxproj Näytä tiedosto

@@ -29,6 +29,8 @@
29 29
 		0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; };
30 30
 		75635B0A20751D6D00F29C9F /* joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0820751D6D00F29C9F /* joined.wav */; };
31 31
 		75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
32
+		412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 412BF89C206AA66F0053B9E5 /* InviteSearch.m */; };
33
+		412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = 412BF89E206AA82F0053B9E5 /* InviteSearch.h */; settings = {ATTRIBUTES = (Public, ); }; };
32 34
 		C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
33 35
 		C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
34 36
 		C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
@@ -62,6 +64,8 @@
62 64
 		0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
63 65
 		75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
64 66
 		75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = "<group>"; };
67
+		412BF89C206AA66F0053B9E5 /* InviteSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InviteSearch.m; sourceTree = "<group>"; };
68
+		412BF89E206AA82F0053B9E5 /* InviteSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InviteSearch.h; sourceTree = "<group>"; };
65 69
 		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>"; };
66 70
 		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>"; };
67 71
 		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 @@
125 129
 				0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
126 130
 				0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
127 131
 				0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
132
+				412BF89E206AA82F0053B9E5 /* InviteSearch.h */,
133
+				412BF89C206AA66F0053B9E5 /* InviteSearch.m */,
128 134
 				0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
129 135
 				0BD906E91EC0C00300C8C18E /* Info.plist */,
130 136
 				0B7C2CFC200F51D60060D076 /* LaunchOptions.m */,
@@ -180,6 +186,7 @@
180 186
 			buildActionMask = 2147483647;
181 187
 			files = (
182 188
 				C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */,
189
+				412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */,
183 190
 				0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
184 191
 				0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
185 192
 				0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
@@ -347,6 +354,7 @@
347 354
 				C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
348 355
 				0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
349 356
 				0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
357
+				412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */,
350 358
 				0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
351 359
 				C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
352 360
 				0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,

+ 49
- 0
ios/sdk/src/InviteSearch.h Näytä tiedosto

@@ -0,0 +1,49 @@
1
+/*
2
+ * Copyright @ 2018-present Atlassian Pty Ltd
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+@class InviteSearchController;
18
+
19
+@protocol InviteSearchControllerDelegate
20
+
21
+/**
22
+ * Called when an InviteSearchController has results for a query that was previously provided.
23
+ */
24
+- (void)inviteSearchController:(InviteSearchController * _Nonnull)controller
25
+             didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
26
+                      forQuery:(NSString * _Nonnull)query;
27
+
28
+/**
29
+ * Called when all invitations were sent successfully.
30
+ */
31
+- (void)inviteDidSucceedForSearchController:(InviteSearchController * _Nonnull)searchController;
32
+
33
+/**
34
+ * Called when one or more invitations fails to send successfully.
35
+ */
36
+- (void)inviteDidFailForItems:(NSArray<NSDictionary *> * _Nonnull)items
37
+         fromSearchController:(InviteSearchController * _Nonnull)searchController;
38
+
39
+@end
40
+
41
+@interface InviteSearchController: NSObject
42
+
43
+@property (nonatomic, nullable, weak) id<InviteSearchControllerDelegate> delegate;
44
+
45
+- (void)performQuery:(NSString * _Nonnull)query;
46
+- (void)cancelSearch;
47
+- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids;
48
+
49
+@end

+ 215
- 0
ios/sdk/src/InviteSearch.m Näytä tiedosto

@@ -0,0 +1,215 @@
1
+/*
2
+ * Copyright @ 2018-present Atlassian Pty Ltd
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+#import <React/RCTBridge.h>
18
+#import <React/RCTEventEmitter.h>
19
+#import <React/RCTUtils.h>
20
+
21
+#import "JitsiMeetView+Private.h"
22
+
23
+#import "InviteSearch.h"
24
+
25
+// The events emitted/supported by InviteSearch:
26
+static NSString * const InviteSearchPerformQueryAction = @"performQueryAction";
27
+static NSString * const InviteSearchPerformSubmitInviteAction = @"performSubmitInviteAction";
28
+
29
+
30
+@interface InviteSearch : RCTEventEmitter
31
+
32
+@end
33
+
34
+
35
+@interface InviteSearchController ()
36
+
37
+@property (nonatomic, readonly) NSString* _Nonnull identifier;
38
+@property (nonatomic, strong) NSMutableDictionary* _Nonnull items;
39
+@property (nonatomic, nullable, weak) InviteSearch* module;
40
+
41
+- (instancetype)initWithSearchModule:(InviteSearch *)module;
42
+
43
+- (void)didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
44
+                 forQuery:(NSString * _Nonnull)query;
45
+
46
+- (void)inviteDidSucceed;
47
+
48
+- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items;
49
+
50
+@end
51
+
52
+
53
+@implementation InviteSearch
54
+
55
+static NSMutableDictionary* searchControllers;
56
+
57
+RCT_EXTERN void RCTRegisterModule(Class);
58
+
59
++ (void)load {
60
+    RCTRegisterModule(self);
61
+
62
+    searchControllers = [[NSMutableDictionary alloc] init];
63
+}
64
+
65
++ (NSString *)moduleName {
66
+    return @"InviteSearch";
67
+}
68
+
69
+- (NSArray<NSString *> *)supportedEvents {
70
+    return @[
71
+        InviteSearchPerformQueryAction,
72
+        InviteSearchPerformSubmitInviteAction
73
+    ];
74
+}
75
+
76
+/**
77
+ * Calls the corresponding JitsiMeetView's delegate to request that the native
78
+ * invite search be presented.
79
+ *
80
+ * @param scope
81
+ */
82
+RCT_EXPORT_METHOD(launchNativeInvite:(NSString *)scope) {
83
+    // The JavaScript App needs to provide uniquely identifying information to
84
+    // the native module so that the latter may match the former to the native
85
+    // JitsiMeetView which hosts it.
86
+    JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope];
87
+
88
+    if (!view) {
89
+        return;
90
+    }
91
+
92
+    id<JitsiMeetViewDelegate> delegate = view.delegate;
93
+
94
+    if (!delegate) {
95
+        return;
96
+    }
97
+
98
+    if ([delegate respondsToSelector:@selector(launchNativeInviteForSearchController:)]) {
99
+        InviteSearchController* searchController = [searchControllers objectForKey:scope];
100
+        if (!searchController) {
101
+            searchController = [self makeInviteSearchController];
102
+        }
103
+
104
+        [delegate launchNativeInviteForSearchController:searchController];
105
+    }
106
+}
107
+
108
+RCT_EXPORT_METHOD(inviteSucceeded:(NSString *)inviteScope) {
109
+    InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
110
+
111
+    [searchController inviteDidSucceed];
112
+
113
+    [searchControllers removeObjectForKey:inviteScope];
114
+}
115
+
116
+RCT_EXPORT_METHOD(inviteFailedForItems:(NSArray<NSDictionary *> *)items inviteScope:(NSString *)inviteScope) {
117
+    InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
118
+
119
+    [searchController inviteDidFailForItems:items];
120
+}
121
+
122
+RCT_EXPORT_METHOD(receivedResults:(NSArray *)results forQuery:(NSString *)query inviteScope:(NSString *)inviteScope) {
123
+
124
+    InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
125
+
126
+    [searchController didReceiveResults:results forQuery:query];
127
+}
128
+
129
+- (InviteSearchController *)makeInviteSearchController {
130
+    InviteSearchController* searchController = [[InviteSearchController alloc] initWithSearchModule:self];
131
+
132
+    [searchControllers setObject:searchController forKey:searchController.identifier];
133
+
134
+    return searchController;
135
+}
136
+
137
+- (void)performQuery:(NSString * _Nonnull)query inviteScope:(NSString * _Nonnull)inviteScope  {
138
+    [self sendEventWithName:InviteSearchPerformQueryAction body:@{ @"query": query, @"inviteScope": inviteScope }];
139
+}
140
+
141
+- (void)cancelSearchForInviteScope:(NSString * _Nonnull)inviteScope {
142
+    [searchControllers removeObjectForKey:inviteScope];
143
+}
144
+
145
+- (void)submitSelectedItems:(NSArray<NSDictionary *> * _Nonnull)items inviteScope:(NSString * _Nonnull)inviteScope {
146
+    [self sendEventWithName:InviteSearchPerformSubmitInviteAction body:@{ @"selectedItems": items, @"inviteScope": inviteScope }];
147
+}
148
+
149
+@end
150
+
151
+
152
+@implementation InviteSearchController
153
+
154
+- (instancetype)initWithSearchModule:(InviteSearch *)module {
155
+    self = [super init];
156
+    if (self) {
157
+        _identifier = [[NSUUID UUID] UUIDString];
158
+
159
+        self.items = [[NSMutableDictionary alloc] init];
160
+        self.module = module;
161
+    }
162
+    return self;
163
+}
164
+
165
+- (void)performQuery:(NSString *)query {
166
+    [self.module performQuery:query inviteScope:self.identifier];
167
+}
168
+
169
+- (void)cancelSearch {
170
+    [self.module cancelSearchForInviteScope:self.identifier];
171
+}
172
+
173
+- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids {
174
+    NSMutableArray* items = [[NSMutableArray alloc] init];
175
+
176
+    for (NSString* itemId in ids) {
177
+        id item = [self.items objectForKey:itemId];
178
+
179
+        if (item) {
180
+            [items addObject:item];
181
+        }
182
+    }
183
+
184
+    [self.module submitSelectedItems:items inviteScope:self.identifier];
185
+}
186
+
187
+- (void)didReceiveResults:(NSArray<NSDictionary *> *)results forQuery:(NSString *)query {
188
+    for (NSDictionary* item in results) {
189
+        NSString* itemId = item[@"id"];
190
+        NSString* itemType = item[@"type"];
191
+        if (itemId) {
192
+            [self.items setObject:item forKey:itemId];
193
+        } else if (itemType != nil && [itemType isEqualToString: @"phone"]) {
194
+            NSString* number = item[@"number"];
195
+            if (number) {
196
+                [self.items setObject:item forKey:number];
197
+            }
198
+        }
199
+    }
200
+
201
+    [self.delegate inviteSearchController:self didReceiveResults:results forQuery:query];
202
+}
203
+
204
+- (void)inviteDidSucceed {
205
+    [self.delegate inviteDidSucceedForSearchController:self];
206
+}
207
+
208
+- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items {
209
+    if (!items) {
210
+        items = @[];
211
+    }
212
+    [self.delegate inviteDidFailForItems:items fromSearchController:self];
213
+}
214
+
215
+@end

+ 1
- 0
ios/sdk/src/JitsiMeet.h Näytä tiedosto

@@ -16,3 +16,4 @@
16 16
 
17 17
 #import <JitsiMeet/JitsiMeetView.h>
18 18
 #import <JitsiMeet/JitsiMeetViewDelegate.h>
19
+#import <JitsiMeet/InviteSearch.h>

+ 5
- 1
ios/sdk/src/JitsiMeetView.h Näytä tiedosto

@@ -21,10 +21,14 @@
21 21
 
22 22
 @interface JitsiMeetView : UIView
23 23
 
24
-@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
24
+@property (nonatomic) BOOL addPeopleEnabled;
25 25
 
26 26
 @property (copy, nonatomic, nullable) NSURL *defaultURL;
27 27
 
28
+@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
29
+
30
+@property (nonatomic) BOOL dialOutEnabled;
31
+
28 32
 @property (nonatomic) BOOL pictureInPictureEnabled;
29 33
 
30 34
 @property (nonatomic) BOOL welcomePageEnabled;

+ 2
- 0
ios/sdk/src/JitsiMeetView.m Näytä tiedosto

@@ -268,6 +268,8 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
268 268
         props[@"defaultURL"] = [self.defaultURL absoluteString];
269 269
     }
270 270
 
271
+    props[@"addPeopleEnabled"] = @(self.addPeopleEnabled);
272
+    props[@"dialOutEnabled"] = @(self.dialOutEnabled);
271 273
     props[@"externalAPIScope"] = externalAPIScope;
272 274
     props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
273 275
     props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);

+ 11
- 0
ios/sdk/src/JitsiMeetViewDelegate.h Näytä tiedosto

@@ -14,6 +14,8 @@
14 14
  * limitations under the License.
15 15
  */
16 16
 
17
+@class InviteSearchController;
18
+
17 19
 @protocol JitsiMeetViewDelegate <NSObject>
18 20
 
19 21
 @optional
@@ -55,6 +57,15 @@
55 57
  */
56 58
 - (void)conferenceWillLeave:(NSDictionary *)data;
57 59
 
60
+
61
+/**
62
+ * Called when the invite button in the conference is tapped.
63
+ *
64
+ * The search controller provided can be used to query user search within the
65
+ * conference.
66
+ */
67
+- (void)launchNativeInviteForSearchController:(InviteSearchController *)searchController;
68
+
58 69
 /**
59 70
  * Called when entering Picture-in-Picture is requested by the user. The app
60 71
  * should now activate its Picture-in-Picture implementation (and resize the

+ 4
- 0
react/features/app/components/App.native.js Näytä tiedosto

@@ -37,6 +37,10 @@ export class App extends AbstractApp {
37 37
     static propTypes = {
38 38
         ...AbstractApp.propTypes,
39 39
 
40
+        addPeopleEnabled: PropTypes.bool,
41
+
42
+        dialOutEnabled: PropTypes.bool,
43
+
40 44
         /**
41 45
          * Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
42 46
          * button is rendered in the {@link Conference} view to afford entering

+ 41
- 176
react/features/invite/components/AddPeopleDialog.web.js Näytä tiedosto

@@ -14,18 +14,14 @@ import { MultiSelectAutocomplete } from '../../base/react';
14 14
 import { inviteVideoRooms } from '../../videosipgw';
15 15
 
16 16
 import {
17
-    checkDialNumber,
18
-    invitePeopleAndChatRooms,
19
-    searchDirectory
17
+    sendInvitesForItems,
18
+    getInviteResultsForQuery
20 19
 } from '../functions';
21 20
 
22 21
 const logger = require('jitsi-meet-logger').getLogger(__filename);
23 22
 
24 23
 declare var interfaceConfig: Object;
25 24
 
26
-const isPhoneNumberRegex
27
-    = new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$');
28
-
29 25
 /**
30 26
  * The dialog that allows to invite people to the call.
31 27
  */
@@ -240,20 +236,6 @@ class AddPeopleDialog extends Component<*, *> {
240 236
         );
241 237
     }
242 238
 
243
-    _getDigitsOnly: (string) => string;
244
-
245
-    /**
246
-     * Removes all non-numeric characters from a string.
247
-     *
248
-     * @param {string} text - The string from which to remove all characters
249
-     * except numbers.
250
-     * @private
251
-     * @returns {string} A string with only numbers.
252
-     */
253
-    _getDigitsOnly(text = '') {
254
-        return text.replace(/\D/g, '');
255
-    }
256
-
257 239
     /**
258 240
      * Helper for determining how many of each type of user is being invited.
259 241
      * Used for logging and sending analytics related to invites.
@@ -294,27 +276,6 @@ class AddPeopleDialog extends Component<*, *> {
294 276
             || this.state.addToCallInProgress;
295 277
     }
296 278
 
297
-    _isMaybeAPhoneNumber: (string) => boolean;
298
-
299
-    /**
300
-     * Checks whether a string looks like it could be for a phone number.
301
-     *
302
-     * @param {string} text - The text to check whether or not it could be a
303
-     * phone number.
304
-     * @private
305
-     * @returns {boolean} True if the string looks like it could be a phone
306
-     * number.
307
-     */
308
-    _isMaybeAPhoneNumber(text) {
309
-        if (!isPhoneNumberRegex.test(text)) {
310
-            return false;
311
-        }
312
-
313
-        const digits = this._getDigitsOnly(text);
314
-
315
-        return Boolean(digits.length);
316
-    }
317
-
318 279
     _onItemSelected: (Object) => Object;
319 280
 
320 281
     /**
@@ -379,75 +340,26 @@ class AddPeopleDialog extends Component<*, *> {
379 340
             addToCallInProgress: true
380 341
         });
381 342
 
382
-        let allInvitePromises = [];
383
-        let invitesLeftToSend = [
384
-            ...this.state.inviteItems
385
-        ];
386
-
387
-        // First create all promises for dialing out.
388
-        if (this.props.enableDialOut && this.props._conference) {
389
-            const phoneNumbers = invitesLeftToSend.filter(
390
-                ({ item }) => item.type === 'phone');
391
-
392
-            // For each number, dial out. On success, remove the number from
393
-            // {@link invitesLeftToSend}.
394
-            const phoneInvitePromises = phoneNumbers.map(number => {
395
-                const numberToInvite = this._getDigitsOnly(number.item.number);
396
-
397
-                return this.props._conference.dial(numberToInvite)
398
-                        .then(() => {
399
-                            invitesLeftToSend
400
-                                = invitesLeftToSend.filter(invite =>
401
-                                    invite !== number);
402
-                        })
403
-                        .catch(error => logger.error(
404
-                            'Error inviting phone number:', error));
405
-
406
-            });
407
-
408
-            allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
409
-        }
410
-
411
-        if (this.props.enableAddPeople) {
412
-            const usersAndRooms = invitesLeftToSend.filter(i =>
413
-                i.item.type === 'user' || i.item.type === 'room')
414
-                .map(i => i.item);
415
-
416
-            if (usersAndRooms.length) {
417
-                // Send a request to invite all the rooms and users. On success,
418
-                // filter all rooms and users from {@link invitesLeftToSend}.
419
-                const peopleInvitePromise = invitePeopleAndChatRooms(
420
-                    this.props._inviteServiceUrl,
421
-                    this.props._inviteUrl,
422
-                    this.props._jwt,
423
-                    usersAndRooms)
424
-                    .then(() => {
425
-                        invitesLeftToSend = invitesLeftToSend.filter(i =>
426
-                            i.item.type !== 'user' && i.item.type !== 'room');
427
-                    })
428
-                    .catch(error => logger.error(
429
-                        'Error inviting people:', error));
430
-
431
-                allInvitePromises.push(peopleInvitePromise);
432
-            }
433
-
434
-            // Sipgw calls are fire and forget. Invite them to the conference
435
-            // then immediately remove them from {@link invitesLeftToSend}.
436
-            const vrooms = invitesLeftToSend.filter(i =>
437
-                i.item.type === 'videosipgw')
438
-                .map(i => i.item);
343
+        const {
344
+            _conference,
345
+            _inviteServiceUrl,
346
+            _inviteUrl,
347
+            _jwt
348
+        } = this.props;
439 349
 
440
-            this.props._conference
441
-                && vrooms.length > 0
442
-                && this.props.inviteVideoRooms(
443
-                    this.props._conference, vrooms);
350
+        const inviteItems = this.state.inviteItems;
351
+        const items = inviteItems.map(item => item.item);
444 352
 
445
-            invitesLeftToSend = invitesLeftToSend.filter(i =>
446
-                i.item.type !== 'videosipgw');
447
-        }
353
+        const options = {
354
+            conference: _conference,
355
+            inviteServiceUrl: _inviteServiceUrl,
356
+            inviteUrl: _inviteUrl,
357
+            inviteVideoRooms: this.props.inviteVideoRooms,
358
+            jwt: _jwt
359
+        };
448 360
 
449
-        Promise.all(allInvitePromises)
450
-            .then(() => {
361
+        sendInvitesForItems(items, options)
362
+            .then(invitesLeftToSend => {
451 363
                 // If any invites are left that means something failed to send
452 364
                 // so treat it as an error.
453 365
                 if (invitesLeftToSend.length) {
@@ -467,8 +379,18 @@ class AddPeopleDialog extends Component<*, *> {
467 379
                         addToCallError: true
468 380
                     });
469 381
 
382
+                    const unsentInviteIDs = invitesLeftToSend.map(invite =>
383
+                        invite.id || invite.number
384
+                    );
385
+
386
+                    const itemsToSelect = inviteItems.filter(invite =>
387
+                        unsentInviteIDs.includes(
388
+                            invite.item.id || invite.item.number
389
+                        )
390
+                    );
391
+
470 392
                     if (this._multiselect) {
471
-                        this._multiselect.setSelectedItems(invitesLeftToSend);
393
+                        this._multiselect.setSelectedItems(itemsToSelect);
472 394
                     }
473 395
 
474 396
                     return;
@@ -558,82 +480,25 @@ class AddPeopleDialog extends Component<*, *> {
558 480
      * @returns {Promise}
559 481
      */
560 482
     _query(query = '') {
561
-        const text = query.trim();
562 483
         const {
484
+            enableAddPeople,
485
+            enableDialOut,
563 486
             _dialOutAuthUrl,
564 487
             _jwt,
565 488
             _peopleSearchQueryTypes,
566 489
             _peopleSearchUrl
567 490
         } = this.props;
568 491
 
569
-        let peopleSearchPromise;
570
-
571
-        if (this.props.enableAddPeople && text) {
572
-            peopleSearchPromise = searchDirectory(
573
-                _peopleSearchUrl,
574
-                _jwt,
575
-                text,
576
-                _peopleSearchQueryTypes);
577
-        } else {
578
-            peopleSearchPromise = Promise.resolve([]);
579
-        }
580
-
581
-
582
-        const hasCountryCode = text.startsWith('+');
583
-        let phoneNumberPromise;
584
-
585
-        if (this.props.enableDialOut && this._isMaybeAPhoneNumber(text)) {
586
-            let numberToVerify = text;
587
-
588
-            // When the number to verify does not start with a +, we assume no
589
-            // proper country code has been entered. In such a case, prepend 1
590
-            // for the country code. The service currently takes care of
591
-            // prepending the +.
592
-            if (!hasCountryCode && !text.startsWith('1')) {
593
-                numberToVerify = `1${numberToVerify}`;
594
-            }
595
-
596
-            // The validation service works properly when the query is digits
597
-            // only so ensure only digits get sent.
598
-            numberToVerify = this._getDigitsOnly(numberToVerify);
599
-
600
-            phoneNumberPromise
601
-                = checkDialNumber(numberToVerify, _dialOutAuthUrl);
602
-        } else {
603
-            phoneNumberPromise = Promise.resolve({});
604
-        }
605
-
606
-        return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
607
-            .then(([ peopleResults, phoneResults ]) => {
608
-                const results = [
609
-                    ...peopleResults
610
-                ];
611
-
612
-                /**
613
-                 * This check for phone results is for the day the call to
614
-                 * searching people might return phone results as well. When
615
-                 * that day comes this check will make it so the server checks
616
-                 * are honored and the local appending of the number is not
617
-                 * done. The local appending of the phone number can then be
618
-                 * cleaned up when convenient.
619
-                 */
620
-                const hasPhoneResult = peopleResults.find(
621
-                    result => result.type === 'phone');
622
-
623
-                if (!hasPhoneResult
624
-                        && typeof phoneResults.allow === 'boolean') {
625
-                    results.push({
626
-                        allowed: phoneResults.allow,
627
-                        country: phoneResults.country,
628
-                        type: 'phone',
629
-                        number: phoneResults.phone,
630
-                        originalEntry: text,
631
-                        showCountryCodeReminder: !hasCountryCode
632
-                    });
633
-                }
492
+        const options = {
493
+            dialOutAuthUrl: _dialOutAuthUrl,
494
+            enableAddPeople,
495
+            enableDialOut,
496
+            jwt: _jwt,
497
+            peopleSearchQueryTypes: _peopleSearchQueryTypes,
498
+            peopleSearchUrl: _peopleSearchUrl
499
+        };
634 500
 
635
-                return results;
636
-            });
501
+        return getInviteResultsForQuery(query, options);
637 502
     }
638 503
 
639 504
     /**

+ 91
- 0
react/features/invite/components/InviteButton.native.js Näytä tiedosto

@@ -0,0 +1,91 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import { launchNativeInvite } from '../../mobile/invite-search';
7
+import { ToolbarButton } from '../../toolbox';
8
+
9
+/**
10
+ * The type of {@link EnterPictureInPictureToobarButton}'s React
11
+ * {@code Component} props.
12
+ */
13
+type Props = {
14
+
15
+    /**
16
+     * Indicates if the "Add to call" feature is available.
17
+     */
18
+    enableAddPeople: boolean,
19
+
20
+    /**
21
+     * Indicates if the "Dial out" feature is available.
22
+     */
23
+    enableDialOut: boolean,
24
+
25
+    /**
26
+     * Launches native invite dialog.
27
+     *
28
+     * @protected
29
+     */
30
+    onLaunchNativeInvite: Function,
31
+};
32
+
33
+/**
34
+ * Implements a {@link ToolbarButton} to enter Picture-in-Picture.
35
+ */
36
+class InviteButton extends Component<Props> {
37
+
38
+    /**
39
+     * Implements React's {@link Component#render()}.
40
+     *
41
+     * @inheritdoc
42
+     * @returns {ReactElement}
43
+     */
44
+    render() {
45
+        const {
46
+            enableAddPeople,
47
+            enableDialOut,
48
+            onLaunchNativeInvite,
49
+            ...props
50
+        } = this.props;
51
+
52
+        if (!enableAddPeople && !enableDialOut) {
53
+            return null;
54
+        }
55
+
56
+        return (
57
+            <ToolbarButton
58
+                iconName = { 'add' }
59
+                onClick = { onLaunchNativeInvite }
60
+                { ...props } />
61
+        );
62
+    }
63
+}
64
+
65
+/**
66
+ * Maps redux actions to {@link InviteButton}'s React
67
+ * {@code Component} props.
68
+ *
69
+ * @param {Function} dispatch - The redux action {@code dispatch} function.
70
+ * @returns {{
71
+*      onLaunchNativeInvite
72
+ * }}
73
+ * @private
74
+ */
75
+function _mapDispatchToProps(dispatch) {
76
+    return {
77
+
78
+        /**
79
+         * Launches native invite dialog.
80
+         *
81
+         * @private
82
+         * @returns {void}
83
+         * @type {Function}
84
+         */
85
+        onLaunchNativeInvite() {
86
+            dispatch(launchNativeInvite());
87
+        }
88
+    };
89
+}
90
+
91
+export default connect(undefined, _mapDispatchToProps)(InviteButton);

+ 332
- 3
react/features/invite/functions.js Näytä tiedosto

@@ -1,5 +1,6 @@
1 1
 // @flow
2 2
 
3
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
3 4
 import { doGetJSON } from '../base/util';
4 5
 
5 6
 declare var $: Function;
@@ -50,7 +51,7 @@ export function getDialInNumbers(url: string): Promise<*> {
50 51
  * type items to invite.
51 52
  * @returns {Promise} - The promise created by the request.
52 53
  */
53
-export function invitePeopleAndChatRooms( // eslint-disable-line max-params
54
+function invitePeopleAndChatRooms( // eslint-disable-line max-params
54 55
         inviteServiceUrl: string,
55 56
         inviteUrl: string,
56 57
         jwt: string,
@@ -88,9 +89,10 @@ export function searchDirectory( // eslint-disable-line max-params
88 89
         text: string,
89 90
         queryTypes: Array<string> = [ 'conferenceRooms', 'user', 'room' ]
90 91
 ): Promise<Array<Object>> {
91
-    const queryTypesString = JSON.stringify(queryTypes);
92
+    const query = encodeURIComponent(text);
93
+    const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
92 94
 
93
-    return fetch(`${serviceUrl}?query=${encodeURIComponent(text)}&queryTypes=${
95
+    return fetch(`${serviceUrl}?query=${query}&queryTypes=${
94 96
         queryTypesString}&jwt=${jwt}`)
95 97
             .then(response => {
96 98
                 const jsonify = response.json();
@@ -110,6 +112,21 @@ export function searchDirectory( // eslint-disable-line max-params
110 112
             });
111 113
 }
112 114
 
115
+/**
116
+ * RegExp to use to determine if some text might be a phone number.
117
+ *
118
+ * @returns {RegExp}
119
+ */
120
+function isPhoneNumberRegex(): RegExp {
121
+    let regexString = '^[0-9+()-\\s]*$';
122
+
123
+    if (typeof interfaceConfig !== 'undefined') {
124
+        regexString = interfaceConfig.PHONE_NUMBER_REGEX || regexString;
125
+    }
126
+
127
+    return new RegExp(regexString);
128
+}
129
+
113 130
 /**
114 131
  * Sends an ajax request to check if the phone number can be called.
115 132
  *
@@ -137,3 +154,315 @@ export function checkDialNumber(
137 154
             .catch(reject);
138 155
     });
139 156
 }
157
+
158
+/**
159
+ * Removes all non-numeric characters from a string.
160
+ *
161
+ * @param {string} text - The string from which to remove all characters
162
+ * except numbers.
163
+ * @private
164
+ * @returns {string} A string with only numbers.
165
+ */
166
+function getDigitsOnly(text: string = ''): string {
167
+    return text.replace(/\D/g, '');
168
+}
169
+
170
+/**
171
+ * Type of the options to use when sending a search query.
172
+ */
173
+export type GetInviteResultsOptions = {
174
+
175
+    /**
176
+     * The endpoint to use for checking phone number validity.
177
+     */
178
+    dialOutAuthUrl: string,
179
+
180
+    /**
181
+     * Whether or not to search for people.
182
+     */
183
+    enableAddPeople: boolean,
184
+
185
+    /**
186
+     * Whether or not to check phone numbers.
187
+     */
188
+    enableDialOut: boolean,
189
+
190
+    /**
191
+     * Array with the query types that will be executed -
192
+     * "conferenceRooms" | "user" | "room".
193
+     */
194
+    peopleSearchQueryTypes: Array<string>,
195
+
196
+    /**
197
+     * The url to query for people.
198
+     */
199
+    peopleSearchUrl: string,
200
+
201
+    /**
202
+     * The jwt token to pass to the search service.
203
+     */
204
+    jwt: string
205
+};
206
+
207
+/**
208
+ * Combines directory search with phone number validation to produce a single
209
+ * set of invite search results.
210
+ *
211
+ * @param  {string} query - Text to search.
212
+ * @param  {GetInviteResultsOptions} options - Options to use when searching.
213
+ * @returns {Promise<*>}
214
+ */
215
+export function getInviteResultsForQuery(
216
+        query: string,
217
+        options: GetInviteResultsOptions): Promise<*> {
218
+    const text = query.trim();
219
+
220
+    const {
221
+        dialOutAuthUrl,
222
+        enableAddPeople,
223
+        enableDialOut,
224
+        peopleSearchQueryTypes,
225
+        peopleSearchUrl,
226
+        jwt
227
+    } = options;
228
+
229
+    let peopleSearchPromise;
230
+
231
+    if (enableAddPeople && text) {
232
+        peopleSearchPromise = searchDirectory(
233
+            peopleSearchUrl,
234
+            jwt,
235
+            text,
236
+            peopleSearchQueryTypes);
237
+    } else {
238
+        peopleSearchPromise = Promise.resolve([]);
239
+    }
240
+
241
+
242
+    const hasCountryCode = text.startsWith('+');
243
+    let phoneNumberPromise;
244
+
245
+    if (enableDialOut && isMaybeAPhoneNumber(text)) {
246
+        let numberToVerify = text;
247
+
248
+        // When the number to verify does not start with a +, we assume no
249
+        // proper country code has been entered. In such a case, prepend 1
250
+        // for the country code. The service currently takes care of
251
+        // prepending the +.
252
+        if (!hasCountryCode && !text.startsWith('1')) {
253
+            numberToVerify = `1${numberToVerify}`;
254
+        }
255
+
256
+        // The validation service works properly when the query is digits
257
+        // only so ensure only digits get sent.
258
+        numberToVerify = getDigitsOnly(numberToVerify);
259
+
260
+        phoneNumberPromise
261
+            = checkDialNumber(numberToVerify, dialOutAuthUrl);
262
+    } else {
263
+        phoneNumberPromise = Promise.resolve({});
264
+    }
265
+
266
+    return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
267
+        .then(([ peopleResults, phoneResults ]) => {
268
+            const results = [
269
+                ...peopleResults
270
+            ];
271
+
272
+            /**
273
+             * This check for phone results is for the day the call to
274
+             * searching people might return phone results as well. When
275
+             * that day comes this check will make it so the server checks
276
+             * are honored and the local appending of the number is not
277
+             * done. The local appending of the phone number can then be
278
+             * cleaned up when convenient.
279
+             */
280
+            const hasPhoneResult = peopleResults.find(
281
+                result => result.type === 'phone');
282
+
283
+            if (!hasPhoneResult
284
+                    && typeof phoneResults.allow === 'boolean') {
285
+                results.push({
286
+                    allowed: phoneResults.allow,
287
+                    country: phoneResults.country,
288
+                    type: 'phone',
289
+                    number: phoneResults.phone,
290
+                    originalEntry: text,
291
+                    showCountryCodeReminder: !hasCountryCode
292
+                });
293
+            }
294
+
295
+            return results;
296
+        });
297
+}
298
+
299
+/**
300
+ * Checks whether a string looks like it could be for a phone number.
301
+ *
302
+ * @param {string} text - The text to check whether or not it could be a
303
+ * phone number.
304
+ * @private
305
+ * @returns {boolean} True if the string looks like it could be a phone
306
+ * number.
307
+ */
308
+function isMaybeAPhoneNumber(text: string): boolean {
309
+    if (!isPhoneNumberRegex().test(text)) {
310
+        return false;
311
+    }
312
+
313
+    const digits = getDigitsOnly(text);
314
+
315
+    return Boolean(digits.length);
316
+}
317
+
318
+/**
319
+ * Type of the options to use when sending invites.
320
+ */
321
+export type SendInvitesOptions = {
322
+
323
+    /**
324
+     * Conference object used to dial out.
325
+     */
326
+    conference: Object,
327
+
328
+    /**
329
+     * The URL to send invites through.
330
+     */
331
+    inviteServiceUrl: string,
332
+
333
+    /**
334
+     * The URL sent with each invite.
335
+     */
336
+    inviteUrl: string,
337
+
338
+    /**
339
+     * The function to use to invite video rooms.
340
+     *
341
+     * @param  {Object} The conference to which the video rooms should be
342
+     * invited.
343
+     * @param  {Array<Object>} The list of rooms that should be invited.
344
+     * @returns {void}
345
+     */
346
+    inviteVideoRooms: (Object, Array<Object>) => void,
347
+
348
+    /**
349
+     * The jwt token to pass to the invite service.
350
+     */
351
+    jwt: string
352
+};
353
+
354
+/**
355
+ * Send invites for a list of items (may be a combination of users, rooms, phone
356
+ * numbers, and video rooms).
357
+ *
358
+ * @param  {Array<Object>} invites - Items for which invites should be sent.
359
+ * @param  {SendInvitesOptions} options - Options to use when sending the
360
+ * provided invites.
361
+ * @returns {Promise} Promise containing the list of invites that were not sent.
362
+ */
363
+export function sendInvitesForItems(
364
+        invites: Array<Object>,
365
+        options: SendInvitesOptions
366
+): Promise<Array<Object>> {
367
+
368
+    const {
369
+        conference,
370
+        inviteServiceUrl,
371
+        inviteUrl,
372
+        inviteVideoRooms,
373
+        jwt
374
+    } = options;
375
+
376
+    let allInvitePromises = [];
377
+    let invitesLeftToSend = [ ...invites ];
378
+
379
+    // First create all promises for dialing out.
380
+    if (conference) {
381
+        const phoneNumbers = invitesLeftToSend.filter(
382
+            item => item.type === 'phone');
383
+
384
+        // For each number, dial out. On success, remove the number from
385
+        // {@link invitesLeftToSend}.
386
+        const phoneInvitePromises = phoneNumbers.map(item => {
387
+            const numberToInvite = getDigitsOnly(item.number);
388
+
389
+            return conference.dial(numberToInvite)
390
+                    .then(() => {
391
+                        invitesLeftToSend
392
+                            = invitesLeftToSend.filter(invite =>
393
+                                invite !== item);
394
+                    })
395
+                    .catch(error => logger.error(
396
+                        'Error inviting phone number:', error));
397
+
398
+        });
399
+
400
+        allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
401
+    }
402
+
403
+    const usersAndRooms = invitesLeftToSend.filter(item =>
404
+        item.type === 'user' || item.type === 'room');
405
+
406
+    if (usersAndRooms.length) {
407
+        // Send a request to invite all the rooms and users. On success,
408
+        // filter all rooms and users from {@link invitesLeftToSend}.
409
+        const peopleInvitePromise = invitePeopleAndChatRooms(
410
+            inviteServiceUrl,
411
+            inviteUrl,
412
+            jwt,
413
+            usersAndRooms)
414
+            .then(() => {
415
+                invitesLeftToSend = invitesLeftToSend.filter(item =>
416
+                    item.type !== 'user' && item.type !== 'room');
417
+            })
418
+            .catch(error => logger.error(
419
+                'Error inviting people:', error));
420
+
421
+        allInvitePromises.push(peopleInvitePromise);
422
+    }
423
+
424
+    // Sipgw calls are fire and forget. Invite them to the conference
425
+    // then immediately remove them from {@link invitesLeftToSend}.
426
+    const vrooms = invitesLeftToSend.filter(item =>
427
+        item.type === 'videosipgw');
428
+
429
+    conference
430
+        && vrooms.length > 0
431
+        && inviteVideoRooms(conference, vrooms);
432
+
433
+    invitesLeftToSend = invitesLeftToSend.filter(item =>
434
+        item.type !== 'videosipgw');
435
+
436
+    return Promise.all(allInvitePromises)
437
+        .then(() => invitesLeftToSend);
438
+}
439
+
440
+/**
441
+ * Determines if adding people is currently enabled.
442
+ *
443
+ * @param {boolean} state - Current state.
444
+ * @returns {boolean} Indication of whether adding people is currently enabled.
445
+ */
446
+export function isAddPeopleEnabled(state: Object): boolean {
447
+    const { app } = state['features/app'];
448
+    const { isGuest } = state['features/base/jwt'];
449
+
450
+    return !isGuest && Boolean(app && app.props.addPeopleEnabled);
451
+}
452
+
453
+/**
454
+ * Determines if dial out is currently enabled or not.
455
+ *
456
+ * @param {boolean} state - Current state.
457
+ * @returns {boolean} Indication of whether dial out is currently enabled.
458
+ */
459
+export function isDialOutEnabled(state: Object): boolean {
460
+    const { conference } = state['features/base/conference'];
461
+    const { isGuest } = state['features/base/jwt'];
462
+    const { enableUserRolesBasedOnToken } = state['features/base/config'];
463
+    const participant = getLocalParticipant(state);
464
+
465
+    return participant && participant.role === PARTICIPANT_ROLE.MODERATOR
466
+                && conference && conference.isSIPCallingSupported()
467
+                && (!enableUserRolesBasedOnToken || !isGuest);
468
+}

+ 1
- 0
react/features/invite/index.js Näytä tiedosto

@@ -1,5 +1,6 @@
1 1
 export * from './actions';
2 2
 export * from './components';
3
+export * from './functions';
3 4
 
4 5
 import './reducer';
5 6
 import './middleware';

+ 46
- 0
react/features/mobile/invite-search/actionTypes.js Näytä tiedosto

@@ -0,0 +1,46 @@
1
+/**
2
+ * The type of redux action to set InviteSearch's event subscriptions.
3
+ *
4
+ * {
5
+ *     type: _SET_INVITE_SEARCH_SUBSCRIPTIONS,
6
+ *     subscriptions: Array|undefined
7
+ * }
8
+ *
9
+ * @protected
10
+ */
11
+export const _SET_INVITE_SEARCH_SUBSCRIPTIONS
12
+  = Symbol('_SET_INVITE_SEARCH_SUBSCRIPTIONS');
13
+
14
+
15
+/**
16
+ * The type of the action which signals a request to launch the native invite
17
+ * dialog.
18
+ *
19
+ * {
20
+ *     type: LAUNCH_NATIVE_INVITE
21
+ * }
22
+ */
23
+export const LAUNCH_NATIVE_INVITE = Symbol('LAUNCH_NATIVE_INVITE');
24
+
25
+/**
26
+ * The type of the action which signals that native invites were sent
27
+ * successfully.
28
+ *
29
+ * {
30
+ *     type: SEND_INVITE_SUCCESS,
31
+ *     inviteScope: string
32
+ * }
33
+ */
34
+export const SEND_INVITE_SUCCESS = Symbol('SEND_INVITE_SUCCESS');
35
+
36
+/**
37
+ * The type of the action which signals that native invites failed to send
38
+ * successfully.
39
+ *
40
+ * {
41
+ *     type: SEND_INVITE_FAILURE,
42
+ *     items: Array<*>,
43
+ *     inviteScope: string
44
+ * }
45
+ */
46
+export const SEND_INVITE_FAILURE = Symbol('SEND_INVITE_FAILURE');

+ 50
- 0
react/features/mobile/invite-search/actions.js Näytä tiedosto

@@ -0,0 +1,50 @@
1
+// @flow
2
+
3
+import {
4
+    LAUNCH_NATIVE_INVITE,
5
+    SEND_INVITE_SUCCESS,
6
+    SEND_INVITE_FAILURE
7
+} from './actionTypes';
8
+
9
+/**
10
+ * Launches the native invite dialog.
11
+ *
12
+ * @returns {{
13
+ *     type: LAUNCH_NATIVE_INVITE
14
+ * }}
15
+ */
16
+export function launchNativeInvite() {
17
+    return {
18
+        type: LAUNCH_NATIVE_INVITE
19
+    };
20
+}
21
+
22
+/**
23
+ * Indicates that all native invites were sent successfully.
24
+ *
25
+ * @param  {string} inviteScope - Scope identifier for the invite success. This
26
+ * is used to look up relevant information on the native side.
27
+ * @returns {void}
28
+ */
29
+export function sendInviteSuccess(inviteScope: string) {
30
+    return {
31
+        type: SEND_INVITE_SUCCESS,
32
+        inviteScope
33
+    };
34
+}
35
+
36
+/**
37
+ * Indicates that some native invites failed to send successfully.
38
+ *
39
+ * @param  {Array<*>} items - Invite items that failed to send.
40
+ * @param  {string} inviteScope - Scope identifier for the invite failure. This
41
+ * is used to look up relevant information on the native side.
42
+ * @returns {void}
43
+ */
44
+export function sendInviteFailure(items: Array<*>, inviteScope: string) {
45
+    return {
46
+        type: SEND_INVITE_FAILURE,
47
+        items,
48
+        inviteScope
49
+    };
50
+}

+ 5
- 0
react/features/mobile/invite-search/index.js Näytä tiedosto

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

+ 233
- 0
react/features/mobile/invite-search/middleware.js Näytä tiedosto

@@ -0,0 +1,233 @@
1
+/* @flow */
2
+
3
+import i18next from 'i18next';
4
+import { NativeModules, NativeEventEmitter } from 'react-native';
5
+
6
+import { MiddlewareRegistry } from '../../base/redux';
7
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
8
+import { getInviteURL } from '../../base/connection';
9
+import {
10
+    getInviteResultsForQuery,
11
+    isAddPeopleEnabled,
12
+    isDialOutEnabled,
13
+    sendInvitesForItems
14
+} from '../../invite';
15
+import { inviteVideoRooms } from '../../videosipgw';
16
+
17
+import { sendInviteSuccess, sendInviteFailure } from './actions';
18
+import {
19
+    _SET_INVITE_SEARCH_SUBSCRIPTIONS,
20
+    LAUNCH_NATIVE_INVITE,
21
+    SEND_INVITE_SUCCESS,
22
+    SEND_INVITE_FAILURE
23
+} from './actionTypes';
24
+
25
+/**
26
+ * Middleware that captures Redux actions and uses the InviteSearch module to
27
+ * turn them into native events so the application knows about them.
28
+ *
29
+ * @param {Store} store - Redux store.
30
+ * @returns {Function}
31
+ */
32
+MiddlewareRegistry.register(store => next => action => {
33
+    const result = next(action);
34
+
35
+    switch (action.type) {
36
+
37
+    case APP_WILL_MOUNT:
38
+        return _appWillMount(store, next, action);
39
+
40
+    case APP_WILL_UNMOUNT:
41
+        store.dispatch({
42
+            type: _SET_INVITE_SEARCH_SUBSCRIPTIONS,
43
+            subscriptions: undefined
44
+        });
45
+        break;
46
+
47
+    case LAUNCH_NATIVE_INVITE:
48
+        launchNativeInvite(store);
49
+        break;
50
+
51
+    case SEND_INVITE_SUCCESS:
52
+        onSendInviteSuccess(action);
53
+        break;
54
+
55
+    case SEND_INVITE_FAILURE:
56
+        onSendInviteFailure(action);
57
+        break;
58
+    }
59
+
60
+    return result;
61
+});
62
+
63
+/**
64
+ * Notifies the feature jwt that the action {@link APP_WILL_MOUNT} is being
65
+ * dispatched within a specific redux {@code store}.
66
+ *
67
+ * @param {Store} store - The redux store in which the specified {@code action}
68
+ * is being dispatched.
69
+ * @param {Dispatch} next - The redux dispatch function to dispatch the
70
+ * specified {@code action} to the specified {@code store}.
71
+ * @param {Action} action - The redux action {@code APP_WILL_MOUNT} which is
72
+ * being dispatched in the specified {@code store}.
73
+ * @private
74
+ * @returns {*}
75
+ */
76
+function _appWillMount({ dispatch, getState }, next, action) {
77
+    const result = next(action);
78
+
79
+    const emitter = new NativeEventEmitter(NativeModules.InviteSearch);
80
+
81
+    const context = {
82
+        dispatch,
83
+        getState
84
+    };
85
+    const subscriptions = [
86
+        emitter.addListener(
87
+            'performQueryAction',
88
+            _onPerformQueryAction,
89
+            context),
90
+        emitter.addListener(
91
+            'performSubmitInviteAction',
92
+            _onPerformSubmitInviteAction,
93
+            context)
94
+    ];
95
+
96
+    dispatch({
97
+        type: _SET_INVITE_SEARCH_SUBSCRIPTIONS,
98
+        subscriptions
99
+    });
100
+
101
+    return result;
102
+}
103
+
104
+/**
105
+ * Sends a request to the native counterpart of InviteSearch to launch a native.
106
+ * invite search.
107
+ *
108
+ * @param {Object} store - The redux store.
109
+ * @private
110
+ * @returns {void}
111
+ */
112
+function launchNativeInvite(store: { getState: Function }) {
113
+    // The JavaScript App needs to provide uniquely identifying information
114
+    // to the native module so that the latter may match the former
115
+    // to the native JitsiMeetView which hosts it.
116
+    const { app } = store.getState()['features/app'];
117
+
118
+    if (app) {
119
+        const { externalAPIScope } = app.props;
120
+
121
+        if (externalAPIScope) {
122
+            NativeModules.InviteSearch.launchNativeInvite(externalAPIScope);
123
+        }
124
+    }
125
+}
126
+
127
+/**
128
+ * Sends a notification to the native counterpart of InviteSearch that all
129
+ * invites were sent successfully.
130
+ *
131
+ * @param  {Object} action - The redux action {@code SEND_INVITE_SUCCESS} which
132
+ * is being dispatched.
133
+ * @returns {void}
134
+ */
135
+function onSendInviteSuccess({ inviteScope }) {
136
+    NativeModules.InviteSearch.inviteSucceeded(inviteScope);
137
+}
138
+
139
+/**
140
+ * Sends a notification to the native counterpart of InviteSearch that some
141
+ * invite items failed to send successfully.
142
+ *
143
+ * @param  {Object} action - The redux action {@code SEND_INVITE_FAILURE} which
144
+ * is being dispatched.
145
+ * @returns {void}
146
+ */
147
+function onSendInviteFailure({ items, inviteScope }) {
148
+    NativeModules.InviteSearch.inviteFailedForItems(items, inviteScope);
149
+}
150
+
151
+/**
152
+ * Handles InviteSearch's event {@code performQueryAction}.
153
+ *
154
+ * @param {Object} event - The details of the InviteSearch event
155
+ * {@code performQueryAction}.
156
+ * @returns {void}
157
+ */
158
+function _onPerformQueryAction({ query, inviteScope }) {
159
+    const { getState } = this; // eslint-disable-line no-invalid-this
160
+
161
+    const state = getState();
162
+
163
+    const {
164
+        dialOutAuthUrl,
165
+        peopleSearchQueryTypes,
166
+        peopleSearchUrl
167
+    } = state['features/base/config'];
168
+
169
+    const options = {
170
+        dialOutAuthUrl,
171
+        enableAddPeople: isAddPeopleEnabled(state),
172
+        enableDialOut: isDialOutEnabled(state),
173
+        jwt: state['features/base/jwt'].jwt,
174
+        peopleSearchQueryTypes,
175
+        peopleSearchUrl
176
+    };
177
+
178
+    getInviteResultsForQuery(query, options)
179
+        .catch(() => [])
180
+        .then(results => {
181
+            const translatedResults = results.map(result => {
182
+                if (result.type === 'phone') {
183
+                    result.title = i18next.t('addPeople.telephone', {
184
+                        number: result.number
185
+                    });
186
+
187
+                    if (result.showCountryCodeReminder) {
188
+                        result.subtitle = i18next.t(
189
+                            'addPeople.countryReminder'
190
+                        );
191
+                    }
192
+                }
193
+
194
+                return result;
195
+            }).filter(result => result.type !== 'phone' || result.allowed);
196
+
197
+            NativeModules.InviteSearch.receivedResults(
198
+                translatedResults,
199
+                query,
200
+                inviteScope);
201
+        });
202
+}
203
+
204
+/**
205
+ * Handles InviteSearch's event {@code performSubmitInviteAction}.
206
+ *
207
+ * @param {Object} event - The details of the InviteSearch event.
208
+ * @returns {void}
209
+ */
210
+function _onPerformSubmitInviteAction({ selectedItems, inviteScope }) {
211
+    const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
212
+    const state = getState();
213
+    const { conference } = state['features/base/conference'];
214
+    const {
215
+        inviteServiceUrl
216
+    } = state['features/base/config'];
217
+    const options = {
218
+        conference,
219
+        inviteServiceUrl,
220
+        inviteUrl: getInviteURL(state),
221
+        inviteVideoRooms,
222
+        jwt: state['features/base/jwt'].jwt
223
+    };
224
+
225
+    sendInvitesForItems(selectedItems, options)
226
+        .then(invitesLeftToSend => {
227
+            if (invitesLeftToSend.length) {
228
+                dispatch(sendInviteFailure(invitesLeftToSend, inviteScope));
229
+            } else {
230
+                dispatch(sendInviteSuccess(inviteScope));
231
+            }
232
+        });
233
+}

+ 14
- 0
react/features/mobile/invite-search/reducer.js Näytä tiedosto

@@ -0,0 +1,14 @@
1
+import { assign, ReducerRegistry } from '../../base/redux';
2
+
3
+import { _SET_INVITE_SEARCH_SUBSCRIPTIONS } from './actionTypes';
4
+
5
+ReducerRegistry.register(
6
+    'features/invite-search',
7
+    (state = {}, action) => {
8
+        switch (action.type) {
9
+        case _SET_INVITE_SEARCH_SUBSCRIPTIONS:
10
+            return assign(state, 'subscriptions', action.subscriptions);
11
+        }
12
+
13
+        return state;
14
+    });

+ 48
- 2
react/features/toolbox/components/Toolbox.native.js Näytä tiedosto

@@ -14,6 +14,11 @@ import {
14 14
     isNarrowAspectRatio,
15 15
     makeAspectRatioAware
16 16
 } from '../../base/responsive-ui';
17
+import {
18
+    InviteButton,
19
+    isAddPeopleEnabled,
20
+    isDialOutEnabled
21
+} from '../../invite';
17 22
 import {
18 23
     EnterPictureInPictureToolbarButton
19 24
 } from '../../mobile/picture-in-picture';
@@ -39,7 +44,7 @@ import { AudioMuteButton, HangupButton, VideoMuteButton } from './buttons';
39 44
  * @private
40 45
  * @type {boolean}
41 46
  */
42
-const _SHARE_ROOM_TOOLBAR_BUTTON = true;
47
+const _SHARE_ROOM_TOOLBAR_BUTTON = false;
43 48
 
44 49
 /**
45 50
  * The type of {@link Toolbox}'s React {@code Component} props.
@@ -56,6 +61,18 @@ type Props = {
56 61
      */
57 62
     _audioOnly: boolean,
58 63
 
64
+    /**
65
+     * Whether or not the feature to directly invite people into the
66
+     * conference is available.
67
+     */
68
+    _enableAddPeople: boolean,
69
+
70
+    /**
71
+     * Whether or not the feature to dial out to number to join the
72
+     * conference is available.
73
+     */
74
+    _enableDialOut: boolean,
75
+
59 76
     /**
60 77
      * The indicator which determines whether the toolbox is enabled.
61 78
      */
@@ -212,9 +229,13 @@ class Toolbox extends Component<Props> {
212 229
         const underlayColor = 'transparent';
213 230
         const {
214 231
             _audioOnly: audioOnly,
232
+            _enableAddPeople: enableAddPeople,
233
+            _enableDialOut: enableDialOut,
215 234
             _videoMuted: videoMuted
216 235
         } = this.props;
217 236
 
237
+        const showInviteButton = enableAddPeople || enableDialOut;
238
+
218 239
         /* eslint-disable react/jsx-curly-spacing,react/jsx-handler-names */
219 240
 
220 241
         return (
@@ -252,7 +273,7 @@ class Toolbox extends Component<Props> {
252 273
                     style = { style }
253 274
                     underlayColor = { underlayColor } />
254 275
                 {
255
-                    _SHARE_ROOM_TOOLBAR_BUTTON
276
+                    _SHARE_ROOM_TOOLBAR_BUTTON && !showInviteButton
256 277
                         && <ToolbarButton
257 278
                             iconName = 'link'
258 279
                             iconStyle = { iconStyle }
@@ -260,6 +281,15 @@ class Toolbox extends Component<Props> {
260 281
                             style = { style }
261 282
                             underlayColor = { underlayColor } />
262 283
                 }
284
+                {
285
+                    showInviteButton
286
+                        && <InviteButton
287
+                            enableAddPeople = { enableAddPeople }
288
+                            enableDialOut = { enableDialOut }
289
+                            iconStyle = { iconStyle }
290
+                            style = { style }
291
+                            underlayColor = { underlayColor } />
292
+                }
263 293
                 <EnterPictureInPictureToolbarButton
264 294
                     iconStyle = { iconStyle }
265 295
                     style = { style }
@@ -388,6 +418,22 @@ function _mapStateToProps(state) {
388 418
          */
389 419
         _audioOnly: Boolean(conference.audioOnly),
390 420
 
421
+        /**
422
+         * Whether or not the feature to directly invite people into the
423
+         * conference is available.
424
+         *
425
+         * @type {boolean}
426
+         */
427
+        _enableAddPeople: isAddPeopleEnabled(state),
428
+
429
+        /**
430
+         * Whether or not the feature to dial out to number to join the
431
+         * conference is available.
432
+         *
433
+         * @type {boolean}
434
+         */
435
+        _enableDialOut: isDialOutEnabled(state),
436
+
391 437
         /**
392 438
          * The indicator which determines whether the toolbox is enabled.
393 439
          *

Loading…
Peruuta
Tallenna