[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="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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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}/`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue