[RN] App-specific URL scheme

This commit is contained in:
Lyubomir Marinov 2017-01-31 22:25:09 -06:00 committed by Любомир Маринов
parent 91487ffc94
commit fdc96044ad
8 changed files with 234 additions and 106 deletions

View File

@ -43,6 +43,12 @@
<data android:host="enso.me" android:scheme="https" />
<data android:host="meet.jit.si" android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="org.jitsi.meet" />
</intent-filter>
</activity>
<activity
android:name="com.facebook.react.devsupport.DevSettingsActivity" />

View File

@ -20,6 +20,19 @@
<string>1.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>org.jitsi.meet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>org.jitsi.meet</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>ITSAppUsesNonExemptEncryption</key>

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <strong>Jitsi Meet</strong> to join a
conversation on your mobile
</p>
<a href = { URLS[Platform.OS] }>
<a href = { _URLS[Platform.OS] }>
<button className = { downloadButtonClassName }>
Download the App
</button>
@ -90,33 +89,17 @@ class UnsupportedMobileBrowser extends Component {
<br />
<strong>then</strong>
</p>
<button
className = { `${ns}__button` }
onClick = { this._onJoinClick }>
{
this.state.joinButtonText
}
</button>
<a href = { this.state.joinURL }>
<button className = { `${ns}__button` }>
{
this.state.joinText
}
</button>
</a>
</div>
</div>
);
}
/**
* 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.
}
}
/**

View File

@ -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}/`;
}
/**