Procházet zdrojové kódy

[RN] App-specific URL scheme

j8
Lyubomir Marinov před 8 roky
rodič
revize
fdc96044ad

+ 6
- 0
android/app/src/main/AndroidManifest.xml Zobrazit soubor

@@ -43,6 +43,12 @@
43 43
             <data android:host="enso.me" android:scheme="https" />
44 44
             <data android:host="meet.jit.si" android:scheme="https" />
45 45
         </intent-filter>
46
+        <intent-filter>
47
+            <action android:name="android.intent.action.VIEW" />
48
+            <category android:name="android.intent.category.BROWSABLE" />
49
+            <category android:name="android.intent.category.DEFAULT" />
50
+            <data android:scheme="org.jitsi.meet" />
51
+        </intent-filter>
46 52
       </activity>
47 53
       <activity
48 54
         android:name="com.facebook.react.devsupport.DevSettingsActivity" />

+ 13
- 0
ios/app/Info.plist Zobrazit soubor

@@ -20,6 +20,19 @@
20 20
 	<string>1.2</string>
21 21
 	<key>CFBundleSignature</key>
22 22
 	<string>????</string>
23
+	<key>CFBundleURLTypes</key>
24
+	<array>
25
+		<dict>
26
+			<key>CFBundleTypeRole</key>
27
+			<string>Viewer</string>
28
+			<key>CFBundleURLName</key>
29
+			<string>org.jitsi.meet</string>
30
+			<key>CFBundleURLSchemes</key>
31
+			<array>
32
+				<string>org.jitsi.meet</string>
33
+			</array>
34
+		</dict>
35
+	</array>
23 36
 	<key>CFBundleVersion</key>
24 37
 	<string>1</string>
25 38
 	<key>ITSAppUsesNonExemptEncryption</key>

+ 2
- 2
react/features/app/actions.js Zobrazit soubor

@@ -4,8 +4,8 @@ import { loadConfig, setConfig } from '../base/lib-jitsi-meet';
4 4
 
5 5
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
6 6
 import {
7
-    _getRoomAndDomainFromUrlString,
8 7
     _getRouteToRender,
8
+    _parseURIString,
9 9
     init
10 10
 } from './functions';
11 11
 import './reducer';
@@ -34,7 +34,7 @@ export function appNavigate(urlOrRoom) {
34 34
         const state = getState();
35 35
         const oldDomain = getDomain(state);
36 36
 
37
-        const { domain, room } = _getRoomAndDomainFromUrlString(urlOrRoom);
37
+        const { domain, room } = _parseURIString(urlOrRoom);
38 38
 
39 39
         // TODO Kostiantyn Tsaregradskyi: We should probably detect if user is
40 40
         // currently in a conference and ask her if she wants to close the

+ 180
- 57
react/features/app/functions.native.js Zobrazit soubor

@@ -3,23 +3,134 @@ import { RouteRegistry } from '../base/react';
3 3
 import { Conference } from '../conference';
4 4
 import { WelcomePage } from '../welcome';
5 5
 
6
+/**
7
+ * The RegExp pattern of the authority of a URI.
8
+ *
9
+ * @private
10
+ * @type {string}
11
+ */
12
+const _URI_AUTHORITY_PATTERN = '(//[^/?#]+)';
13
+
14
+/**
15
+ * The RegExp pattern of the path of a URI.
16
+ *
17
+ * @private
18
+ * @type {string}
19
+ */
20
+const _URI_PATH_PATTERN = '([^?#]*)';
21
+
22
+/**
23
+ * The RegExp patther of the protocol of a URI.
24
+ *
25
+ * @private
26
+ * @type {string}
27
+ */
28
+const _URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)';
29
+
30
+/**
31
+ * Fixes the hier-part of a specific URI (string) so that the URI is well-known.
32
+ * For example, certain Jitsi Meet deployments are not conventional but it is
33
+ * possible to translate their URLs into conventional.
34
+ *
35
+ * @param {string} uri - The URI (string) to fix the hier-part of.
36
+ * @private
37
+ * @returns {string}
38
+ */
39
+function _fixURIStringHierPart(uri) {
40
+    // Rewrite the specified URL in order to handle special cases such as
41
+    // hipchat.com and enso.me which do not follow the common pattern of most
42
+    // Jitsi Meet deployments.
43
+
44
+    // hipchat.com
45
+    let regex
46
+        = new RegExp(
47
+                `^${_URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`,
48
+                'gi');
49
+    let match = regex.exec(uri);
50
+
51
+    if (!match) {
52
+        // enso.me
53
+        regex
54
+            = new RegExp(
55
+                    `^${_URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`,
56
+                    'gi');
57
+        match = regex.exec(uri);
58
+    }
59
+    if (match) {
60
+        /* eslint-disable no-param-reassign, prefer-template */
61
+
62
+        uri
63
+            = match[1] /* protocol */
64
+                + '//enso.hipchat.me/'
65
+                + uri.substring(regex.lastIndex); /* room (name) */
66
+
67
+        /* eslint-enable no-param-reassign, prefer-template */
68
+    }
69
+
70
+    return uri;
71
+}
72
+
73
+/**
74
+ * Fixes the scheme part of a specific URI (string) so that it contains a
75
+ * well-known scheme such as HTTP(S). For example, the mobile app implements an
76
+ * app-specific URI scheme in addition to Universal Links. The app-specific
77
+ * scheme may precede or replace the well-known scheme. In such a case, dealing
78
+ * with the app-specific scheme only complicates the logic and it is simpler to
79
+ * get rid of it (by translating the app-specific scheme into a well-known
80
+ * scheme).
81
+ *
82
+ * @param {string} uri - The URI (string) to fix the scheme of.
83
+ * @private
84
+ * @returns {string}
85
+ */
86
+function _fixURIStringScheme(uri) {
87
+    const regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}+`, 'gi');
88
+    const match = regex.exec(uri);
89
+
90
+    if (match) {
91
+        // As an implementation convenience, pick up the last scheme and make
92
+        // sure that it is a well-known one.
93
+        let protocol = match[match.length - 1].toLowerCase();
94
+
95
+        if (protocol !== 'http:' && protocol !== 'https:') {
96
+            protocol = 'https:';
97
+        }
98
+
99
+        /* eslint-disable no-param-reassign */
100
+
101
+        uri = uri.substring(regex.lastIndex);
102
+        if (uri.startsWith('//')) {
103
+            // The specified URL was not a room name only, it contained an
104
+            // authority.
105
+            uri = protocol + uri;
106
+        }
107
+
108
+        /* eslint-enable no-param-reassign */
109
+    }
110
+
111
+    return uri;
112
+}
113
+
6 114
 /**
7 115
  * Gets room name and domain from URL object.
8 116
  *
9 117
  * @param {URL} url - URL object.
10 118
  * @private
11 119
  * @returns {{
12
- *      domain: (string|undefined),
13
- *      room: (string|undefined)
14
- *  }}
120
+ *     domain: (string|undefined),
121
+ *     room: (string|undefined)
122
+ * }}
15 123
  */
16
-function _getRoomAndDomainFromUrlObject(url) {
124
+function _getRoomAndDomainFromURLObject(url) {
17 125
     let domain;
18 126
     let room;
19 127
 
20 128
     if (url) {
21 129
         domain = url.hostname;
22
-        room = url.pathname.substr(1);
130
+
131
+        // The room (name) is the last component of pathname.
132
+        room = url.pathname;
133
+        room = room.substring(room.lastIndexOf('/') + 1);
23 134
 
24 135
         // Convert empty string to undefined to simplify checks.
25 136
         if (room === '') {
@@ -36,44 +147,6 @@ function _getRoomAndDomainFromUrlObject(url) {
36 147
     };
37 148
 }
38 149
 
39
-/**
40
- * Gets conference room name and connection domain from URL.
41
- *
42
- * @param {(string|undefined)} url - URL.
43
- * @returns {{
44
- *      domain: (string|undefined),
45
- *      room: (string|undefined)
46
- *  }}
47
- */
48
-export function _getRoomAndDomainFromUrlString(url) {
49
-    // Rewrite the specified URL in order to handle special cases such as
50
-    // hipchat.com and enso.me which do not follow the common pattern of most
51
-    // Jitsi Meet deployments.
52
-    if (typeof url === 'string') {
53
-        // hipchat.com
54
-        let regex = /^(https?):\/\/hipchat.com\/video\/call\//gi;
55
-        let match = regex.exec(url);
56
-
57
-        if (!match) {
58
-            // enso.me
59
-            regex = /^(https?):\/\/enso\.me\/(?:call|meeting)\//gi;
60
-            match = regex.exec(url);
61
-        }
62
-        if (match && match.length > 1) {
63
-            /* eslint-disable no-param-reassign, prefer-template */
64
-
65
-            url
66
-                = match[1] /* URL protocol */
67
-                    + '://enso.hipchat.me/'
68
-                    + url.substring(regex.lastIndex);
69
-
70
-            /* eslint-enable no-param-reassign, prefer-template */
71
-        }
72
-    }
73
-
74
-    return _getRoomAndDomainFromUrlObject(_urlStringToObject(url));
75
-}
76
-
77 150
 /**
78 151
  * Determines which route is to be rendered in order to depict a specific Redux
79 152
  * store.
@@ -94,24 +167,74 @@ export function _getRouteToRender(stateOrGetState) {
94 167
 }
95 168
 
96 169
 /**
97
- * Parses a string into a URL (object).
170
+ * Parses a specific URI which (supposedly) references a Jitsi Meet resource
171
+ * (location).
98 172
  *
99
- * @param {(string|undefined)} url - The URL to parse.
100
- * @private
101
- * @returns {URL}
173
+ * @param {(string|undefined)} uri - The URI to parse which (supposedly)
174
+ * references a Jitsi Meet resource (location).
175
+ * @returns {{
176
+ *     domain: (string|undefined),
177
+ *     room: (string|undefined)
178
+ * }}
102 179
  */
103
-function _urlStringToObject(url) {
104
-    let urlObj;
180
+export function _parseURIString(uri) {
181
+    let obj;
105 182
 
106
-    if (url) {
107
-        try {
108
-            urlObj = new URL(url);
109
-        } catch (ex) {
110
-            // The return value will signal the failure & the logged exception
111
-            // will provide the details to the developers.
112
-            console.log(`${url} seems to be not a valid URL, but it's OK`, ex);
183
+    if (typeof uri === 'string') {
184
+        let str = uri;
185
+
186
+        str = _fixURIStringScheme(str);
187
+        str = _fixURIStringHierPart(str);
188
+
189
+        obj = {};
190
+
191
+        let regex;
192
+        let match;
193
+
194
+        // protocol
195
+        regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}`, 'gi');
196
+        match = regex.exec(str);
197
+        if (match) {
198
+            obj.protocol = match[1].toLowerCase();
199
+            str = str.substring(regex.lastIndex);
200
+        }
201
+
202
+        // authority
203
+        regex = new RegExp(`^${_URI_AUTHORITY_PATTERN}`, 'gi');
204
+        match = regex.exec(str);
205
+        if (match) {
206
+            let authority = match[1];
207
+
208
+            str = str.substring(regex.lastIndex);
209
+
210
+            // userinfo
211
+            const userinfoEndIndex = authority.indexOf('@');
212
+
213
+            if (userinfoEndIndex !== -1) {
214
+                authority = authority.substring(userinfoEndIndex + 1);
215
+            }
216
+
217
+            obj.host = authority;
218
+
219
+            // port
220
+            const portBeginIndex = authority.lastIndexOf(':');
221
+
222
+            if (portBeginIndex !== -1) {
223
+                obj.port = authority.substring(portBeginIndex + 1);
224
+                authority = authority.substring(0, portBeginIndex);
225
+            }
226
+
227
+            obj.hostname = authority;
228
+        }
229
+
230
+        // pathname
231
+        regex = new RegExp(`^${_URI_PATH_PATTERN}`, 'gi');
232
+        match = regex.exec(str);
233
+        if (match) {
234
+            obj.pathname = match[1] || '/';
235
+            str = str.substring(regex.lastIndex);
113 236
         }
114 237
     }
115 238
 
116
-    return urlObj;
239
+    return _getRoomAndDomainFromURLObject(obj);
117 240
 }

+ 1
- 1
react/features/app/functions.web.js Zobrazit soubor

@@ -15,7 +15,7 @@ import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage';
15 15
 
16 16
 const Logger = require('jitsi-meet-logger');
17 17
 
18
-export { _getRoomAndDomainFromUrlString } from './functions.native';
18
+export { _parseURIString } from './functions.native';
19 19
 
20 20
 /**
21 21
  * Determines which route is to be rendered in order to depict a specific Redux

+ 2
- 1
react/features/conference/route.js Zobrazit soubor

@@ -40,7 +40,8 @@ function _initConference() {
40 40
  * Promise wrapper on obtain config method. When HttpConfigFetch will be moved
41 41
  * to React app it's better to use load config instead.
42 42
  *
43
- * @param {string} location - URL of the domain.
43
+ * @param {string} location - URL of the domain from which the config is to be
44
+ * obtained.
44 45
  * @param {string} room - Room name.
45 46
  * @private
46 47
  * @returns {Promise}

+ 27
- 44
react/features/unsupported-browser/components/UnsupportedMobileBrowser.js Zobrazit soubor

@@ -6,8 +6,10 @@ import { Platform } from '../../base/react';
6 6
 /**
7 7
  * The map of platforms to URLs at which the mobile app for the associated
8 8
  * platform is available for download.
9
+ *
10
+ * @private
9 11
  */
10
-const URLS = {
12
+const _URLS = {
11 13
     android: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
12 14
     ios: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905'
13 15
 };
@@ -19,7 +21,7 @@ const URLS = {
19 21
  */
20 22
 class UnsupportedMobileBrowser extends Component {
21 23
     /**
22
-     * Mobile browser page component's property types.
24
+     * UnsupportedMobileBrowser component's property types.
23 25
      *
24 26
      * @static
25 27
      */
@@ -35,35 +37,32 @@ class UnsupportedMobileBrowser extends Component {
35 37
     }
36 38
 
37 39
     /**
38
-     * Constructor of UnsupportedMobileBrowser component.
40
+     * Initializes the text and URL of the `Start a conference` / `Join the
41
+     * conversation` button which takes the user to the mobile app.
39 42
      *
40
-     * @param {Object} props - The read-only React Component props with which
41
-     * the new instance is to be initialized.
42
-     */
43
-    constructor(props) {
44
-        super(props);
45
-
46
-        // Bind methods
47
-        this._onJoinClick = this._onJoinClick.bind(this);
48
-    }
49
-
50
-    /**
51
-     * React lifecycle method triggered before component will mount.
52
-     *
53
-     * @returns {void}
43
+     * @inheritdoc
54 44
      */
55 45
     componentWillMount() {
56
-        const joinButtonText
46
+        const joinText
57 47
             = this.props._room ? 'Join the conversation' : 'Start a conference';
58 48
 
49
+        // If the user installed the app while this Component was displayed
50
+        // (e.g. the user clicked the Download the App button), then we would
51
+        // like to open the current URL in the mobile app. The only way to do it
52
+        // appears to be a link with an app-specific scheme, not a Universal
53
+        // Link.
54
+        const joinURL = `org.jitsi.meet:${window.location.href}`;
55
+
59 56
         this.setState({
60
-            joinButtonText
57
+            joinText,
58
+            joinURL
61 59
         });
62 60
     }
63 61
 
64 62
     /**
65
-     * Renders component.
63
+     * Implements React's {@link Component#render()}.
66 64
      *
65
+     * @inheritdoc
67 66
      * @returns {ReactElement}
68 67
      */
69 68
     render() {
@@ -80,7 +79,7 @@ class UnsupportedMobileBrowser extends Component {
80 79
                         You need <strong>Jitsi Meet</strong> to join a
81 80
                         conversation on your mobile
82 81
                     </p>
83
-                    <a href = { URLS[Platform.OS] }>
82
+                    <a href = { _URLS[Platform.OS] }>
84 83
                         <button className = { downloadButtonClassName }>
85 84
                             Download the App
86 85
                         </button>
@@ -90,33 +89,17 @@ class UnsupportedMobileBrowser extends Component {
90 89
                         <br />
91 90
                         <strong>then</strong>
92 91
                     </p>
93
-                    <button
94
-                        className = { `${ns}__button` }
95
-                        onClick = { this._onJoinClick }>
96
-                        {
97
-                            this.state.joinButtonText
98
-                        }
99
-                    </button>
92
+                    <a href = { this.state.joinURL }>
93
+                        <button className = { `${ns}__button` }>
94
+                            {
95
+                                this.state.joinText
96
+                            }
97
+                        </button>
98
+                    </a>
100 99
                 </div>
101 100
             </div>
102 101
         );
103 102
     }
104
-
105
-    /**
106
-     * Handles clicks on the button that joins the local participant in a
107
-     * conference.
108
-     *
109
-     * @private
110
-     * @returns {void}
111
-     */
112
-    _onJoinClick() {
113
-        // If the user installed the app while this Component was displayed
114
-        // (e.g. the user clicked the Download the App button), then we would
115
-        // like to open the current URL in the mobile app.
116
-
117
-        // TODO The only way to do it appears to be a link with an app-specific
118
-        // scheme, not a Universal Link.
119
-    }
120 103
 }
121 104
 
122 105
 /**

+ 3
- 1
react/features/welcome/components/WelcomePage.web.js Zobrazit soubor

@@ -82,7 +82,9 @@ class WelcomePage extends AbstractWelcomePage {
82 82
      * @returns {string} Domain name.
83 83
      */
84 84
     _getDomain() {
85
-        return `${window.location.protocol}//${window.location.host}/`;
85
+        const windowLocation = window.location;
86
+
87
+        return `${windowLocation.protocol}//${windowLocation.host}/`;
86 88
     }
87 89
 
88 90
     /**

Načítá se…
Zrušit
Uložit