[RN] App-specific URL scheme
This commit is contained in:
parent
91487ffc94
commit
fdc96044ad
|
@ -43,6 +43,12 @@
|
||||||
<data android:host="enso.me" android:scheme="https" />
|
<data android:host="enso.me" android:scheme="https" />
|
||||||
<data android:host="meet.jit.si" android:scheme="https" />
|
<data android:host="meet.jit.si" android:scheme="https" />
|
||||||
</intent-filter>
|
</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>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||||
|
|
|
@ -20,6 +20,19 @@
|
||||||
<string>1.2</string>
|
<string>1.2</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<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>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { loadConfig, setConfig } from '../base/lib-jitsi-meet';
|
||||||
|
|
||||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
|
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
|
||||||
import {
|
import {
|
||||||
_getRoomAndDomainFromUrlString,
|
|
||||||
_getRouteToRender,
|
_getRouteToRender,
|
||||||
|
_parseURIString,
|
||||||
init
|
init
|
||||||
} from './functions';
|
} from './functions';
|
||||||
import './reducer';
|
import './reducer';
|
||||||
|
@ -34,7 +34,7 @@ export function appNavigate(urlOrRoom) {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const oldDomain = getDomain(state);
|
const oldDomain = getDomain(state);
|
||||||
|
|
||||||
const { domain, room } = _getRoomAndDomainFromUrlString(urlOrRoom);
|
const { domain, room } = _parseURIString(urlOrRoom);
|
||||||
|
|
||||||
// TODO Kostiantyn Tsaregradskyi: We should probably detect if user is
|
// TODO Kostiantyn Tsaregradskyi: We should probably detect if user is
|
||||||
// currently in a conference and ask her if she wants to close the
|
// currently in a conference and ask her if she wants to close the
|
||||||
|
|
|
@ -3,6 +3,114 @@ import { RouteRegistry } from '../base/react';
|
||||||
import { Conference } from '../conference';
|
import { Conference } from '../conference';
|
||||||
import { WelcomePage } from '../welcome';
|
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.
|
* Gets room name and domain from URL object.
|
||||||
*
|
*
|
||||||
|
@ -13,13 +121,16 @@ import { WelcomePage } from '../welcome';
|
||||||
* room: (string|undefined)
|
* room: (string|undefined)
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
function _getRoomAndDomainFromUrlObject(url) {
|
function _getRoomAndDomainFromURLObject(url) {
|
||||||
let domain;
|
let domain;
|
||||||
let room;
|
let room;
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
domain = url.hostname;
|
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.
|
// Convert empty string to undefined to simplify checks.
|
||||||
if (room === '') {
|
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
|
* Determines which route is to be rendered in order to depict a specific Redux
|
||||||
* store.
|
* 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.
|
* @param {(string|undefined)} uri - The URI to parse which (supposedly)
|
||||||
* @private
|
* references a Jitsi Meet resource (location).
|
||||||
* @returns {URL}
|
* @returns {{
|
||||||
|
* domain: (string|undefined),
|
||||||
|
* room: (string|undefined)
|
||||||
|
* }}
|
||||||
*/
|
*/
|
||||||
function _urlStringToObject(url) {
|
export function _parseURIString(uri) {
|
||||||
let urlObj;
|
let obj;
|
||||||
|
|
||||||
if (url) {
|
if (typeof uri === 'string') {
|
||||||
try {
|
let str = uri;
|
||||||
urlObj = new URL(url);
|
|
||||||
} catch (ex) {
|
str = _fixURIStringScheme(str);
|
||||||
// The return value will signal the failure & the logged exception
|
str = _fixURIStringHierPart(str);
|
||||||
// will provide the details to the developers.
|
|
||||||
console.log(`${url} seems to be not a valid URL, but it's OK`, ex);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage';
|
||||||
|
|
||||||
const Logger = require('jitsi-meet-logger');
|
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
|
* Determines which route is to be rendered in order to depict a specific Redux
|
||||||
|
|
|
@ -40,7 +40,8 @@ function _initConference() {
|
||||||
* Promise wrapper on obtain config method. When HttpConfigFetch will be moved
|
* Promise wrapper on obtain config method. When HttpConfigFetch will be moved
|
||||||
* to React app it's better to use load config instead.
|
* 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.
|
* @param {string} room - Room name.
|
||||||
* @private
|
* @private
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
|
|
|
@ -6,8 +6,10 @@ import { Platform } from '../../base/react';
|
||||||
/**
|
/**
|
||||||
* The map of platforms to URLs at which the mobile app for the associated
|
* The map of platforms to URLs at which the mobile app for the associated
|
||||||
* platform is available for download.
|
* platform is available for download.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
const URLS = {
|
const _URLS = {
|
||||||
android: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
|
android: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
|
||||||
ios: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905'
|
ios: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905'
|
||||||
};
|
};
|
||||||
|
@ -19,7 +21,7 @@ const URLS = {
|
||||||
*/
|
*/
|
||||||
class UnsupportedMobileBrowser extends Component {
|
class UnsupportedMobileBrowser extends Component {
|
||||||
/**
|
/**
|
||||||
* Mobile browser page component's property types.
|
* UnsupportedMobileBrowser component's property types.
|
||||||
*
|
*
|
||||||
* @static
|
* @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
|
* @inheritdoc
|
||||||
* 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}
|
|
||||||
*/
|
*/
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const joinButtonText
|
const joinText
|
||||||
= this.props._room ? 'Join the conversation' : 'Start a conference';
|
= 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({
|
this.setState({
|
||||||
joinButtonText
|
joinText,
|
||||||
|
joinURL
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders component.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
* @inheritdoc
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
|
@ -80,7 +79,7 @@ class UnsupportedMobileBrowser extends Component {
|
||||||
You need <strong>Jitsi Meet</strong> to join a
|
You need <strong>Jitsi Meet</strong> to join a
|
||||||
conversation on your mobile
|
conversation on your mobile
|
||||||
</p>
|
</p>
|
||||||
<a href = { URLS[Platform.OS] }>
|
<a href = { _URLS[Platform.OS] }>
|
||||||
<button className = { downloadButtonClassName }>
|
<button className = { downloadButtonClassName }>
|
||||||
Download the App
|
Download the App
|
||||||
</button>
|
</button>
|
||||||
|
@ -90,33 +89,17 @@ class UnsupportedMobileBrowser extends Component {
|
||||||
<br />
|
<br />
|
||||||
<strong>then</strong>
|
<strong>then</strong>
|
||||||
</p>
|
</p>
|
||||||
<button
|
<a href = { this.state.joinURL }>
|
||||||
className = { `${ns}__button` }
|
<button className = { `${ns}__button` }>
|
||||||
onClick = { this._onJoinClick }>
|
|
||||||
{
|
{
|
||||||
this.state.joinButtonText
|
this.state.joinText
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -82,7 +82,9 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
* @returns {string} Domain name.
|
* @returns {string} Domain name.
|
||||||
*/
|
*/
|
||||||
_getDomain() {
|
_getDomain() {
|
||||||
return `${window.location.protocol}//${window.location.host}/`;
|
const windowLocation = window.location;
|
||||||
|
|
||||||
|
return `${windowLocation.protocol}//${windowLocation.host}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue