Skip to content

Commit

Permalink
Deep linking (#291)
Browse files Browse the repository at this point in the history
* deep linking

* Basic deep link working

* Deep link routing

* Multiple servers working

* Send user to the room
  • Loading branch information
diegolmello authored and ggazzo committed May 7, 2018
1 parent 33baf35 commit 69513a8
Show file tree
Hide file tree
Showing 21 changed files with 300 additions and 88 deletions.
8 changes: 8 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,19 @@
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="RocketChatRN">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="go.rocket.chat" />
<data android:scheme="rocketchat" android:host="*" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

Expand Down
1 change: 1 addition & 0 deletions app/actions/actionsTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export const PINNED_MESSAGES = createRequestTypes('PINNED_MESSAGES', ['OPEN', 'R
export const MENTIONED_MESSAGES = createRequestTypes('MENTIONED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const ROOM_FILES = createRequestTypes('ROOM_FILES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
Expand Down
8 changes: 8 additions & 0 deletions app/actions/deepLinking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as types from './actionsTypes';

export function deepLinkingOpen(params) {
return {
type: types.DEEP_LINKING.OPEN,
params
};
}
30 changes: 28 additions & 2 deletions app/containers/Routes.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Linking } from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import SplashScreen from 'react-native-splash-screen';
import { appInit } from '../actions';

import { appInit } from '../actions';
import { deepLinkingOpen } from '../actions/deepLinking';
import AuthRoutes from './routes/AuthRoutes';
import PublicRoutes from './routes/PublicRoutes';
import * as NavigationService from './routes/NavigationService';
import parseQuery from '../lib/methods/helpers/parseQuery';

@connect(
state => ({
Expand All @@ -16,7 +19,7 @@ import * as NavigationService from './routes/NavigationService';
background: state.app.background
}),
dispatch => bindActionCreators({
appInit
appInit, deepLinkingOpen
}, dispatch)
)
export default class Routes extends React.Component {
Expand All @@ -26,11 +29,22 @@ export default class Routes extends React.Component {
appInit: PropTypes.func.isRequired
}

constructor(props) {
super(props);
this.handleOpenURL = this.handleOpenURL.bind(this);
}

componentDidMount() {
if (this.props.app.ready) {
return SplashScreen.hide();
}
this.props.appInit();

Linking
.getInitialURL()
.then(url => this.handleOpenURL({ url }))
.catch(console.error);
Linking.addEventListener('url', this.handleOpenURL);
}

componentWillReceiveProps(nextProps) {
Expand All @@ -43,6 +57,18 @@ export default class Routes extends React.Component {
NavigationService.setNavigator(this.navigator);
}

handleOpenURL({ url }) {
if (url) {
url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
const regex = /^(room|auth)\?/;
if (url.match(regex)) {
url = url.replace(regex, '');
const params = parseQuery(url);
this.props.deepLinkingOpen(params);
}
}
}

render() {
const { login } = this.props;

Expand Down
2 changes: 1 addition & 1 deletion app/containers/routes/NavigationService.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function goRoomsList() {

export function goRoom({ rid, name }, counter = 0) {
// about counter: we can call this method before navigator be set. so we have to wait, if we tried a lot, we give up ...
if (!rid || !name || counter > 10) {
if (!rid || counter > 10) {
return;
}
if (!config.navigator) {
Expand Down
2 changes: 2 additions & 0 deletions app/lib/ddp.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ export default class Socket extends EventEmitter {
}
disconnect() {
this._close();
this._login = null;
this.subscriptions = {};
}
async reconnect() {
if (this._timer) {
Expand Down
51 changes: 51 additions & 0 deletions app/lib/methods/canOpenRoom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { post } from './helpers/rest';
import database from '../realm';

// TODO: api fix
const ddpTypes = {
channel: 'c', direct: 'd', group: 'p'
};
const restTypes = {
channel: 'channels', direct: 'im', group: 'groups'
};

async function canOpenRoomREST({ type, rid }) {
try {
const { token, id } = this.ddp._login;
const server = this.ddp.url.replace('ws', 'http');
await post({ token, id, server }, `${ restTypes[type] }.open`, { roomId: rid });
return true;
} catch (error) {
// TODO: workround for 'already open for the sender' error
if (!error.errorType) {
return true;
}
return false;
}
}

async function canOpenRoomDDP(...args) {
try {
const [{ type, name }] = args;
await this.ddp.call('getRoomByTypeAndName', ddpTypes[type], name);
return true;
} catch (error) {
if (error.isClientSafe) {
return false;
}
return canOpenRoomREST.call(this, ...args);
}
}

export default async function canOpenRoom({ rid, path }) {
const { database: db } = database;
const room = db.objects('subscriptions').filtered('rid == $0', rid);
if (room.length) {
return true;
}

const [type, name] = path.split('/');
// eslint-disable-next-line
const data = await (this.ddp && this.ddp.status ? canOpenRoomDDP.call(this, { rid, type, name }) : canOpenRoomREST.call(this, { type, rid }));
return data;
}
9 changes: 9 additions & 0 deletions app/lib/methods/helpers/parseQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function(query) {
return (/^[?#]/.test(query) ? query.slice(1) : query)
.split('&')
.reduce((params, param) => {
const [key, value] = param.split('=');
params[key] = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
return params;
}, { });
}
29 changes: 17 additions & 12 deletions app/lib/methods/subscriptions/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ const stop = (ddp) => {
promises = false;
}

ddp.removeListener('logged', logged);
ddp.removeListener('disconnected', disconnected);
if (ddp) {
ddp.removeListener('logged', logged);
ddp.removeListener('disconnected', disconnected);
}

logged = false;
disconnected = false;
Expand Down Expand Up @@ -50,18 +52,21 @@ export default async function subscribeRoom({ rid, t }) {
}, 5000);
};


logged = this.ddp.on('logged', () => {
clearTimeout(timer);
timer = false;
promises = subscribe(this.ddp, rid);
});

disconnected = this.ddp.on('disconnected', () => { loop(); });

if (!this.ddp.status) {
if (!this.ddp || !this.ddp.status) {
loop();
} else {
logged = this.ddp.on('logged', () => {
clearTimeout(timer);
timer = false;
promises = subscribe(this.ddp, rid);
});

disconnected = this.ddp.on('disconnected', () => {
if (this._login) {
loop();
}
});

promises = subscribe(this.ddp, rid);
}

Expand Down
58 changes: 32 additions & 26 deletions app/lib/methods/subscriptions/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,41 @@ export default async function subscribeRooms(id) {
}, 5000);
};

this.ddp.on('logged', () => {
clearTimeout(timer);
timer = false;
});
if (this.ddp) {
this.ddp.on('logged', () => {
clearTimeout(timer);
timer = false;
});

this.ddp.on('logout', () => {
clearTimeout(timer);
timer = true;
});
this.ddp.on('logout', () => {
clearTimeout(timer);
timer = true;
});

this.ddp.on('disconnected', () => { loop(); });
this.ddp.on('disconnected', () => {
if (this._login) {
loop();
}
});

this.ddp.on('stream-notify-user', (ddpMessage) => {
const [type, data] = ddpMessage.fields.args;
const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) {
const tpm = merge(data);
return database.write(() => {
database.create('subscriptions', tpm, true);
});
}
if (/rooms/.test(ev) && type === 'updated') {
const [sub] = database.objects('subscriptions').filtered('rid == $0', data._id);
database.write(() => {
merge(sub, data);
});
}
});
this.ddp.on('stream-notify-user', (ddpMessage) => {
const [type, data] = ddpMessage.fields.args;
const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) {
const tpm = merge(data);
return database.write(() => {
database.create('subscriptions', tpm, true);
});
}
if (/rooms/.test(ev) && type === 'updated') {
const [sub] = database.objects('subscriptions').filtered('rid == $0', data._id);
database.write(() => {
merge(sub, data);
});
}
});
}

await subscriptions;
console.log(this.ddp.subscriptions);
// console.log(this.ddp.subscriptions);
}
3 changes: 2 additions & 1 deletion app/lib/rocketchat.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import getSettings from './methods/getSettings';
import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions';
import getCustomEmoji from './methods/getCustomEmojis';

import canOpenRoom from './methods/canOpenRoom';

import _buildMessage from './methods/helpers/buildMessage';
import loadMessagesForRoom from './methods/loadMessagesForRoom';
Expand All @@ -51,6 +51,7 @@ const RocketChat = {
TOKEN_KEY,
subscribeRooms,
subscribeRoom,
canOpenRoom,
createChannel({ name, users, type }) {
return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type);
},
Expand Down
5 changes: 4 additions & 1 deletion app/sagas/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ const getToken = function* getToken() {
}
return JSON.parse(user);
}
return yield put(setToken());

yield AsyncStorage.removeItem(RocketChat.TOKEN_KEY);
yield put(setToken());
return null;
};


Expand Down
57 changes: 57 additions & 0 deletions app/sagas/deepLinking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AsyncStorage } from 'react-native';
import { delay } from 'redux-saga';
import { takeLatest, take, select, call, put } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes';
import { setServer, addServer } from '../actions/server';
import * as NavigationService from '../containers/routes/NavigationService';
import database from '../lib/realm';
import RocketChat from '../lib/rocketchat';

const navigate = function* go({ server, params, sameServer = true }) {
const user = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
if (user) {
const { rid, path } = params;
if (rid) {
const canOpenRoom = yield RocketChat.canOpenRoom({ rid, path });
if (canOpenRoom) {
return yield call(NavigationService.goRoom, { rid: params.rid });
}
}
if (!sameServer) {
yield call(NavigationService.goRoomsList);
}
}
};

const handleOpen = function* handleOpen({ params }) {
const isReady = yield select(state => state.app.ready);
const server = yield select(state => state.server.server);

if (!isReady) {
yield take(types.APP.READY);
}

const host = `https://${ params.host }`;

// TODO: needs better test
// if deep link is from same server
if (server === host) {
yield navigate({ server, params });
} else { // if deep link is from a different server
// search if deep link's server already exists
const servers = yield database.databases.serversDB.objects('servers').filtered('id = $0', host); // TODO: need better test
if (servers.length) {
// if server exists, select it
yield put(setServer(servers[0].id));
yield delay(2000);
yield navigate({ server: servers[0].id, params, sameServer: false });
} else {
yield put(addServer(host));
}
}
};

const root = function* root() {
yield takeLatest(types.DEEP_LINKING.OPEN, handleOpen);
};
export default root;
Loading

0 comments on commit 69513a8

Please sign in to comment.