From fdc96044ad659c31c5eb0422a5af0f8fce789444 Mon Sep 17 00:00:00 2001
From: Lyubomir Marinov
Date: Tue, 31 Jan 2017 22:25:09 -0600
Subject: [PATCH] [RN] App-specific URL scheme
---
android/app/src/main/AndroidManifest.xml | 6 +
ios/app/Info.plist | 13 +
react/features/app/actions.js | 4 +-
react/features/app/functions.native.js | 237 +++++++++++++-----
react/features/app/functions.web.js | 2 +-
react/features/conference/route.js | 3 +-
.../components/UnsupportedMobileBrowser.js | 71 ++----
.../welcome/components/WelcomePage.web.js | 4 +-
8 files changed, 234 insertions(+), 106 deletions(-)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f46293cfd..39bb46658 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -43,6 +43,12 @@
+
+
+
+
+
+
diff --git a/ios/app/Info.plist b/ios/app/Info.plist
index b2f64a624..facf54cf7 100644
--- a/ios/app/Info.plist
+++ b/ios/app/Info.plist
@@ -20,6 +20,19 @@
1.2CFBundleSignature????
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Viewer
+ CFBundleURLName
+ org.jitsi.meet
+ CFBundleURLSchemes
+
+ org.jitsi.meet
+
+
+ CFBundleVersion1ITSAppUsesNonExemptEncryption
diff --git a/react/features/app/actions.js b/react/features/app/actions.js
index f6c5e6ad1..90cadb597 100644
--- a/react/features/app/actions.js
+++ b/react/features/app/actions.js
@@ -4,8 +4,8 @@ import { loadConfig, setConfig } from '../base/lib-jitsi-meet';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
import {
- _getRoomAndDomainFromUrlString,
_getRouteToRender,
+ _parseURIString,
init
} from './functions';
import './reducer';
@@ -34,7 +34,7 @@ export function appNavigate(urlOrRoom) {
const state = getState();
const oldDomain = getDomain(state);
- const { domain, room } = _getRoomAndDomainFromUrlString(urlOrRoom);
+ const { domain, room } = _parseURIString(urlOrRoom);
// TODO Kostiantyn Tsaregradskyi: We should probably detect if user is
// currently in a conference and ask her if she wants to close the
diff --git a/react/features/app/functions.native.js b/react/features/app/functions.native.js
index 86cf04d42..b58be8ad2 100644
--- a/react/features/app/functions.native.js
+++ b/react/features/app/functions.native.js
@@ -3,23 +3,134 @@ import { RouteRegistry } from '../base/react';
import { Conference } from '../conference';
import { WelcomePage } from '../welcome';
+/**
+ * The RegExp pattern of the authority of a URI.
+ *
+ * @private
+ * @type {string}
+ */
+const _URI_AUTHORITY_PATTERN = '(//[^/?#]+)';
+
+/**
+ * The RegExp pattern of the path of a URI.
+ *
+ * @private
+ * @type {string}
+ */
+const _URI_PATH_PATTERN = '([^?#]*)';
+
+/**
+ * The RegExp patther of the protocol of a URI.
+ *
+ * @private
+ * @type {string}
+ */
+const _URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)';
+
+/**
+ * Fixes the hier-part of a specific URI (string) so that the URI is well-known.
+ * For example, certain Jitsi Meet deployments are not conventional but it is
+ * possible to translate their URLs into conventional.
+ *
+ * @param {string} uri - The URI (string) to fix the hier-part of.
+ * @private
+ * @returns {string}
+ */
+function _fixURIStringHierPart(uri) {
+ // Rewrite the specified URL in order to handle special cases such as
+ // hipchat.com and enso.me which do not follow the common pattern of most
+ // Jitsi Meet deployments.
+
+ // hipchat.com
+ let regex
+ = new RegExp(
+ `^${_URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`,
+ 'gi');
+ let match = regex.exec(uri);
+
+ if (!match) {
+ // enso.me
+ regex
+ = new RegExp(
+ `^${_URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`,
+ 'gi');
+ match = regex.exec(uri);
+ }
+ if (match) {
+ /* eslint-disable no-param-reassign, prefer-template */
+
+ uri
+ = match[1] /* protocol */
+ + '//enso.hipchat.me/'
+ + uri.substring(regex.lastIndex); /* room (name) */
+
+ /* eslint-enable no-param-reassign, prefer-template */
+ }
+
+ return uri;
+}
+
+/**
+ * Fixes the scheme part of a specific URI (string) so that it contains a
+ * well-known scheme such as HTTP(S). For example, the mobile app implements an
+ * app-specific URI scheme in addition to Universal Links. The app-specific
+ * scheme may precede or replace the well-known scheme. In such a case, dealing
+ * with the app-specific scheme only complicates the logic and it is simpler to
+ * get rid of it (by translating the app-specific scheme into a well-known
+ * scheme).
+ *
+ * @param {string} uri - The URI (string) to fix the scheme of.
+ * @private
+ * @returns {string}
+ */
+function _fixURIStringScheme(uri) {
+ const regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}+`, 'gi');
+ const match = regex.exec(uri);
+
+ if (match) {
+ // As an implementation convenience, pick up the last scheme and make
+ // sure that it is a well-known one.
+ let protocol = match[match.length - 1].toLowerCase();
+
+ if (protocol !== 'http:' && protocol !== 'https:') {
+ protocol = 'https:';
+ }
+
+ /* eslint-disable no-param-reassign */
+
+ uri = uri.substring(regex.lastIndex);
+ if (uri.startsWith('//')) {
+ // The specified URL was not a room name only, it contained an
+ // authority.
+ uri = protocol + uri;
+ }
+
+ /* eslint-enable no-param-reassign */
+ }
+
+ return uri;
+}
+
/**
* Gets room name and domain from URL object.
*
* @param {URL} url - URL object.
* @private
* @returns {{
- * domain: (string|undefined),
- * room: (string|undefined)
- * }}
+ * domain: (string|undefined),
+ * room: (string|undefined)
+ * }}
*/
-function _getRoomAndDomainFromUrlObject(url) {
+function _getRoomAndDomainFromURLObject(url) {
let domain;
let room;
if (url) {
domain = url.hostname;
- room = url.pathname.substr(1);
+
+ // The room (name) is the last component of pathname.
+ room = url.pathname;
+ room = room.substring(room.lastIndexOf('/') + 1);
// Convert empty string to undefined to simplify checks.
if (room === '') {
@@ -36,44 +147,6 @@ function _getRoomAndDomainFromUrlObject(url) {
};
}
-/**
- * Gets conference room name and connection domain from URL.
- *
- * @param {(string|undefined)} url - URL.
- * @returns {{
- * domain: (string|undefined),
- * room: (string|undefined)
- * }}
- */
-export function _getRoomAndDomainFromUrlString(url) {
- // Rewrite the specified URL in order to handle special cases such as
- // hipchat.com and enso.me which do not follow the common pattern of most
- // Jitsi Meet deployments.
- if (typeof url === 'string') {
- // hipchat.com
- let regex = /^(https?):\/\/hipchat.com\/video\/call\//gi;
- let match = regex.exec(url);
-
- if (!match) {
- // enso.me
- regex = /^(https?):\/\/enso\.me\/(?:call|meeting)\//gi;
- match = regex.exec(url);
- }
- if (match && match.length > 1) {
- /* eslint-disable no-param-reassign, prefer-template */
-
- url
- = match[1] /* URL protocol */
- + '://enso.hipchat.me/'
- + url.substring(regex.lastIndex);
-
- /* eslint-enable no-param-reassign, prefer-template */
- }
- }
-
- return _getRoomAndDomainFromUrlObject(_urlStringToObject(url));
-}
-
/**
* Determines which route is to be rendered in order to depict a specific Redux
* store.
@@ -94,24 +167,74 @@ export function _getRouteToRender(stateOrGetState) {
}
/**
- * Parses a string into a URL (object).
+ * Parses a specific URI which (supposedly) references a Jitsi Meet resource
+ * (location).
*
- * @param {(string|undefined)} url - The URL to parse.
- * @private
- * @returns {URL}
+ * @param {(string|undefined)} uri - The URI to parse which (supposedly)
+ * references a Jitsi Meet resource (location).
+ * @returns {{
+ * domain: (string|undefined),
+ * room: (string|undefined)
+ * }}
*/
-function _urlStringToObject(url) {
- let urlObj;
+export function _parseURIString(uri) {
+ let obj;
- if (url) {
- try {
- urlObj = new URL(url);
- } catch (ex) {
- // The return value will signal the failure & the logged exception
- // will provide the details to the developers.
- console.log(`${url} seems to be not a valid URL, but it's OK`, ex);
+ if (typeof uri === 'string') {
+ let str = uri;
+
+ str = _fixURIStringScheme(str);
+ str = _fixURIStringHierPart(str);
+
+ obj = {};
+
+ let regex;
+ let match;
+
+ // protocol
+ regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}`, 'gi');
+ match = regex.exec(str);
+ if (match) {
+ obj.protocol = match[1].toLowerCase();
+ str = str.substring(regex.lastIndex);
+ }
+
+ // authority
+ regex = new RegExp(`^${_URI_AUTHORITY_PATTERN}`, 'gi');
+ match = regex.exec(str);
+ if (match) {
+ let authority = match[1];
+
+ str = str.substring(regex.lastIndex);
+
+ // userinfo
+ const userinfoEndIndex = authority.indexOf('@');
+
+ if (userinfoEndIndex !== -1) {
+ authority = authority.substring(userinfoEndIndex + 1);
+ }
+
+ obj.host = authority;
+
+ // port
+ const portBeginIndex = authority.lastIndexOf(':');
+
+ if (portBeginIndex !== -1) {
+ obj.port = authority.substring(portBeginIndex + 1);
+ authority = authority.substring(0, portBeginIndex);
+ }
+
+ obj.hostname = authority;
+ }
+
+ // pathname
+ regex = new RegExp(`^${_URI_PATH_PATTERN}`, 'gi');
+ match = regex.exec(str);
+ if (match) {
+ obj.pathname = match[1] || '/';
+ str = str.substring(regex.lastIndex);
}
}
- return urlObj;
+ return _getRoomAndDomainFromURLObject(obj);
}
diff --git a/react/features/app/functions.web.js b/react/features/app/functions.web.js
index a312d48f5..a83de8128 100644
--- a/react/features/app/functions.web.js
+++ b/react/features/app/functions.web.js
@@ -15,7 +15,7 @@ import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage';
const Logger = require('jitsi-meet-logger');
-export { _getRoomAndDomainFromUrlString } from './functions.native';
+export { _parseURIString } from './functions.native';
/**
* Determines which route is to be rendered in order to depict a specific Redux
diff --git a/react/features/conference/route.js b/react/features/conference/route.js
index 8c7309acd..7515f6b0b 100644
--- a/react/features/conference/route.js
+++ b/react/features/conference/route.js
@@ -40,7 +40,8 @@ function _initConference() {
* Promise wrapper on obtain config method. When HttpConfigFetch will be moved
* to React app it's better to use load config instead.
*
- * @param {string} location - URL of the domain.
+ * @param {string} location - URL of the domain from which the config is to be
+ * obtained.
* @param {string} room - Room name.
* @private
* @returns {Promise}
diff --git a/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js b/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js
index 979afda03..28f574999 100644
--- a/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js
+++ b/react/features/unsupported-browser/components/UnsupportedMobileBrowser.js
@@ -6,8 +6,10 @@ import { Platform } from '../../base/react';
/**
* The map of platforms to URLs at which the mobile app for the associated
* platform is available for download.
+ *
+ * @private
*/
-const URLS = {
+const _URLS = {
android: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
ios: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905'
};
@@ -19,7 +21,7 @@ const URLS = {
*/
class UnsupportedMobileBrowser extends Component {
/**
- * Mobile browser page component's property types.
+ * UnsupportedMobileBrowser component's property types.
*
* @static
*/
@@ -35,35 +37,32 @@ class UnsupportedMobileBrowser extends Component {
}
/**
- * Constructor of UnsupportedMobileBrowser component.
+ * Initializes the text and URL of the `Start a conference` / `Join the
+ * conversation` button which takes the user to the mobile app.
*
- * @param {Object} props - The read-only React Component props with which
- * the new instance is to be initialized.
- */
- constructor(props) {
- super(props);
-
- // Bind methods
- this._onJoinClick = this._onJoinClick.bind(this);
- }
-
- /**
- * React lifecycle method triggered before component will mount.
- *
- * @returns {void}
+ * @inheritdoc
*/
componentWillMount() {
- const joinButtonText
+ const joinText
= this.props._room ? 'Join the conversation' : 'Start a conference';
+ // If the user installed the app while this Component was displayed
+ // (e.g. the user clicked the Download the App button), then we would
+ // like to open the current URL in the mobile app. The only way to do it
+ // appears to be a link with an app-specific scheme, not a Universal
+ // Link.
+ const joinURL = `org.jitsi.meet:${window.location.href}`;
+
this.setState({
- joinButtonText
+ joinText,
+ joinURL
});
}
/**
- * Renders component.
+ * Implements React's {@link Component#render()}.
*
+ * @inheritdoc
* @returns {ReactElement}
*/
render() {
@@ -80,7 +79,7 @@ class UnsupportedMobileBrowser extends Component {
You need Jitsi Meet to join a
conversation on your mobile
-
+
@@ -90,33 +89,17 @@ class UnsupportedMobileBrowser extends Component {
then
-
+
+
+
);
}
-
- /**
- * Handles clicks on the button that joins the local participant in a
- * conference.
- *
- * @private
- * @returns {void}
- */
- _onJoinClick() {
- // If the user installed the app while this Component was displayed
- // (e.g. the user clicked the Download the App button), then we would
- // like to open the current URL in the mobile app.
-
- // TODO The only way to do it appears to be a link with an app-specific
- // scheme, not a Universal Link.
- }
}
/**
diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js
index 5b9c0eed4..9e03c6849 100644
--- a/react/features/welcome/components/WelcomePage.web.js
+++ b/react/features/welcome/components/WelcomePage.web.js
@@ -82,7 +82,9 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {string} Domain name.
*/
_getDomain() {
- return `${window.location.protocol}//${window.location.host}/`;
+ const windowLocation = window.location;
+
+ return `${windowLocation.protocol}//${windowLocation.host}/`;
}
/**