[IMPROVEMENT] Add deep link to Jitsi calls (#2223)

* [WIP] Jitsi Deep Links

* [WIP] Add app links

* save uniqueID servers database

* add serverInfoKey of uniqueID

* search server by call url

* open jitsi deeplink poc

* improve jitsi url

* fix

* improve comment

* add missing android scheme

* handle host not found

* Allow app links to be matched on parseDeepLinking

* Fix push notification of a call

* Minor fix

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-07-30 14:25:52 -03:00 committed by GitHub
parent 8cd38d5e19
commit cb5c914570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 86 additions and 36 deletions

View File

@ -37,8 +37,10 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="go.rocket.chat" /> <data android:scheme="https" android:host="go.rocket.chat" />
<data android:scheme="https" android:host="jitsi.rocket.chat" />
<data android:scheme="rocketchat" android:host="room" /> <data android:scheme="rocketchat" android:host="room" />
<data android:scheme="rocketchat" android:host="auth" /> <data android:scheme="rocketchat" android:host="auth" />
<data android:scheme="rocketchat" android:host="jitsi.rocket.chat" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

View File

@ -50,6 +50,13 @@ const parseDeepLinking = (url) => {
return parseQuery(url); return parseQuery(url);
} }
} }
const call = /^(https:\/\/)?jitsi.rocket.chat\//;
if (url.match(call)) {
url = url.replace(call, '').trim();
if (url) {
return { path: url, isCall: true };
}
}
} }
return null; return null;
}; };

View File

@ -25,4 +25,6 @@ export default class Server extends Model {
@field('auto_lock_time') autoLockTime; @field('auto_lock_time') autoLockTime;
@field('biometry') biometry; @field('biometry') biometry;
@field('unique_id') uniqueID;
} }

View File

@ -26,6 +26,17 @@ export default schemaMigrations({
] ]
}) })
] ]
},
{
toVersion: 5,
steps: [
addColumns({
table: 'servers',
columns: [
{ name: 'unique_id', type: 'string', isOptional: true }
]
})
]
} }
] ]
}); });

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 4, version: 5,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'users', name: 'users',
@ -28,7 +28,8 @@ export default appSchema({
{ name: 'last_local_authenticated_session', type: 'number', isOptional: true }, { name: 'last_local_authenticated_session', type: 'number', isOptional: true },
{ name: 'auto_lock', type: 'boolean', isOptional: true }, { name: 'auto_lock', type: 'boolean', isOptional: true },
{ name: 'auto_lock_time', type: 'number', isOptional: true }, { name: 'auto_lock_time', type: 'number', isOptional: true },
{ name: 'biometry', type: 'boolean', isOptional: true } { name: 'biometry', type: 'boolean', isOptional: true },
{ name: 'unique_id', type: 'string', isOptional: true }
] ]
}) })
] ]

View File

@ -1,41 +1,39 @@
import reduxStore from '../createStore'; import reduxStore from '../createStore';
import Navigation from '../Navigation'; import Navigation from '../Navigation';
const jitsiBaseUrl = ({ async function jitsiURL({ rid }) {
Jitsi_Enabled, Jitsi_SSL, Jitsi_Domain, Jitsi_URL_Room_Prefix, uniqueID const { settings } = reduxStore.getState();
}) => { const { Jitsi_Enabled } = settings;
if (!Jitsi_Enabled) { if (!Jitsi_Enabled) {
return ''; return '';
} }
const uniqueIdentifier = uniqueID || 'undefined';
const domain = Jitsi_Domain; const {
Jitsi_Domain, Jitsi_URL_Room_Prefix, Jitsi_SSL, Jitsi_Enabled_TokenAuth, uniqueID
} = settings;
const domain = `${ Jitsi_Domain }/`;
const prefix = Jitsi_URL_Room_Prefix; const prefix = Jitsi_URL_Room_Prefix;
const uniqueIdentifier = uniqueID || 'undefined';
const protocol = Jitsi_SSL ? 'https://' : 'http://';
const urlProtocol = Jitsi_SSL ? 'https://' : 'http://';
const urlDomain = `${ domain }/`;
return `${ urlProtocol }${ urlDomain }${ prefix }${ uniqueIdentifier }`;
};
async function callJitsi(rid, onlyAudio = false) {
let accessToken;
let queryString = ''; let queryString = '';
const { settings } = reduxStore.getState();
const { Jitsi_Enabled_TokenAuth } = settings;
if (Jitsi_Enabled_TokenAuth) { if (Jitsi_Enabled_TokenAuth) {
try { try {
accessToken = await this.methodCallWrapper('jitsi:generateAccessToken', rid); const accessToken = await this.methodCallWrapper('jitsi:generateAccessToken', rid);
} catch (e) { queryString = `?jwt=${ accessToken }`;
} catch {
// do nothing // do nothing
} }
} }
if (accessToken) { return `${ protocol }${ domain }${ prefix }${ uniqueIdentifier }${ rid }${ queryString }`;
queryString = `?jwt=${ accessToken }`; }
}
Navigation.navigate('JitsiMeetView', { url: `${ jitsiBaseUrl(settings) }${ rid }${ queryString }`, onlyAudio, rid }); async function callJitsi(rid, onlyAudio = false) {
const url = await jitsiURL.call(this, { rid });
Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid });
} }
export default callJitsi; export default callJitsi;

View File

@ -1,4 +1,5 @@
import database from '../database'; import database from '../database';
import store from '../createStore';
const restTypes = { const restTypes = {
channel: 'channels', direct: 'im', group: 'groups' channel: 'channels', direct: 'im', group: 'groups'
@ -53,11 +54,17 @@ async function open({ type, rid, name }) {
} }
} }
export default async function canOpenRoom({ rid, path }) { export default async function canOpenRoom({ rid, path, isCall }) {
try { try {
const db = database.active; const db = database.active;
const subsCollection = db.collections.get('subscriptions'); const subsCollection = db.collections.get('subscriptions');
const [type, name] = path.split('/');
if (isCall && !rid) {
// Extract rid from a Jitsi URL
// Eg.: [Jitsi_URL_Room_Prefix][uniqueID][rid][?jwt]
const { Jitsi_URL_Room_Prefix, uniqueID } = store.getState().settings;
rid = path.replace(`${ Jitsi_URL_Room_Prefix }${ uniqueID }`, '').replace(/\?(.*)/g, '');
}
if (rid) { if (rid) {
try { try {
@ -75,8 +82,10 @@ export default async function canOpenRoom({ rid, path }) {
} }
} }
const [type, name] = path.split('/');
try { try {
return await open.call(this, { type, rid, name }); const result = await open.call(this, { type, rid, name });
return result;
} catch (e) { } catch (e) {
return false; return false;
} }

View File

@ -11,7 +11,7 @@ import protectedFunction from './helpers/protectedFunction';
import fetch from '../../utils/fetch'; import fetch from '../../utils/fetch';
import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication'; import { DEFAULT_AUTO_LOCK } from '../../constants/localAuthentication';
const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After']; const serverInfoKeys = ['Site_Name', 'UI_Use_Real_Name', 'FileUpload_MediaTypeWhiteList', 'FileUpload_MaxFileSize', 'Force_Screen_Lock', 'Force_Screen_Lock_After', 'uniqueID'];
// these settings are used only on onboarding process // these settings are used only on onboarding process
const loginSettings = [ const loginSettings = [
@ -68,6 +68,9 @@ const serverInfoUpdate = async(serverInfo, iconSetting) => {
return { ...allSettings, autoLockTime: setting.valueAsNumber }; return { ...allSettings, autoLockTime: setting.valueAsNumber };
} }
} }
if (setting._id === 'uniqueID') {
return { ...allSettings, uniqueID: setting.valueAsString };
}
return allSettings; return allSettings;
}, {}); }, {});

View File

@ -10,7 +10,7 @@ export const onNotification = (notification) => {
if (data) { if (data) {
try { try {
const { const {
rid, name, sender, type, host rid, name, sender, type, host, messageType
} = EJSON.parse(data.ejson); } = EJSON.parse(data.ejson);
const types = { const types = {
@ -24,7 +24,8 @@ export const onNotification = (notification) => {
const params = { const params = {
host, host,
rid, rid,
path: `${ types[type] }/${ roomName }` path: `${ types[type] }/${ roomName }`,
isCall: messageType === 'jitsi_call_started'
}; };
store.dispatch(deepLinkingOpen(params)); store.dispatch(deepLinkingOpen(params));
} catch (e) { } catch (e) {

View File

@ -13,6 +13,7 @@ import EventEmitter from '../utils/events';
import { appStart, ROOT_INSIDE, ROOT_NEW_SERVER } from '../actions/app'; import { appStart, ROOT_INSIDE, ROOT_NEW_SERVER } from '../actions/app';
import { localAuthenticate } from '../utils/localAuthentication'; import { localAuthenticate } from '../utils/localAuthentication';
import { goRoom } from '../utils/goRoom'; import { goRoom } from '../utils/goRoom';
import callJitsi from '../lib/methods/callJitsi';
const roomTypes = { const roomTypes = {
channel: 'c', direct: 'd', group: 'p', channels: 'l' channel: 'c', direct: 'd', group: 'p', channels: 'l'
@ -48,7 +49,11 @@ const navigate = function* navigate({ params }) {
roomUserId: RocketChat.getUidDirectMessage(room), roomUserId: RocketChat.getUidDirectMessage(room),
...room ...room
}; };
goRoom({ item, isMasterDetail }); yield goRoom({ item, isMasterDetail });
if (params.isCall) {
callJitsi(item.rid);
}
} }
} else { } else {
yield handleInviteLink({ params }); yield handleInviteLink({ params });
@ -57,11 +62,23 @@ const navigate = function* navigate({ params }) {
}; };
const handleOpen = function* handleOpen({ params }) { const handleOpen = function* handleOpen({ params }) {
if (!params.host) { const serversDB = database.servers;
return; const serversCollection = serversDB.collections.get('servers');
}
let { host } = params; let { host } = params;
if (params.isCall && !host) {
const servers = yield serversCollection.query().fetch();
// search from which server is that call
servers.forEach(({ uniqueID, id }) => {
if (params.path.includes(uniqueID)) {
host = id;
}
});
}
if (!host) {
return;
}
if (!/^(http|https)/.test(host)) { if (!/^(http|https)/.test(host)) {
host = `https://${ params.host }`; host = `https://${ params.host }`;
} }
@ -87,8 +104,6 @@ const handleOpen = function* handleOpen({ params }) {
yield navigate({ params }); yield navigate({ params });
} else { } else {
// search if deep link's server already exists // search if deep link's server already exists
const serversDB = database.servers;
const serversCollection = serversDB.collections.get('servers');
try { try {
const servers = yield serversCollection.find(host); const servers = yield serversCollection.find(host);
if (servers && user) { if (servers && user) {

View File

@ -11,6 +11,7 @@
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array> <array>
<string>applinks:go.rocket.chat</string> <string>applinks:go.rocket.chat</string>
<string>applinks:jitsi.rocket.chat</string>
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>