Deep linking (#291)

* deep linking

* Basic deep link working

* Deep link routing

* Multiple servers working

* Send user to the room
This commit is contained in:
Diego Mello 2018-05-07 17:43:26 -03:00 committed by Guilherme Gazzo
parent 33baf35de6
commit 69513a8327
21 changed files with 300 additions and 88 deletions

View File

@ -33,11 +33,19 @@
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </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>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

View File

@ -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 MENTIONED_MESSAGES = createRequestTypes('MENTIONED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_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 ROOM_FILES = createRequestTypes('ROOM_FILES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const INCREMENT = 'INCREMENT'; export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT'; export const DECREMENT = 'DECREMENT';

View File

@ -0,0 +1,8 @@
import * as types from './actionsTypes';
export function deepLinkingOpen(params) {
return {
type: types.DEEP_LINKING.OPEN,
params
};
}

View File

@ -1,13 +1,16 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Linking } from 'react-native';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SplashScreen from 'react-native-splash-screen'; 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 AuthRoutes from './routes/AuthRoutes';
import PublicRoutes from './routes/PublicRoutes'; import PublicRoutes from './routes/PublicRoutes';
import * as NavigationService from './routes/NavigationService'; import * as NavigationService from './routes/NavigationService';
import parseQuery from '../lib/methods/helpers/parseQuery';
@connect( @connect(
state => ({ state => ({
@ -16,7 +19,7 @@ import * as NavigationService from './routes/NavigationService';
background: state.app.background background: state.app.background
}), }),
dispatch => bindActionCreators({ dispatch => bindActionCreators({
appInit appInit, deepLinkingOpen
}, dispatch) }, dispatch)
) )
export default class Routes extends React.Component { export default class Routes extends React.Component {
@ -26,11 +29,22 @@ export default class Routes extends React.Component {
appInit: PropTypes.func.isRequired appInit: PropTypes.func.isRequired
} }
constructor(props) {
super(props);
this.handleOpenURL = this.handleOpenURL.bind(this);
}
componentDidMount() { componentDidMount() {
if (this.props.app.ready) { if (this.props.app.ready) {
return SplashScreen.hide(); return SplashScreen.hide();
} }
this.props.appInit(); this.props.appInit();
Linking
.getInitialURL()
.then(url => this.handleOpenURL({ url }))
.catch(console.error);
Linking.addEventListener('url', this.handleOpenURL);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -43,6 +57,18 @@ export default class Routes extends React.Component {
NavigationService.setNavigator(this.navigator); 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() { render() {
const { login } = this.props; const { login } = this.props;

View File

@ -34,7 +34,7 @@ export function goRoomsList() {
export function goRoom({ rid, name }, counter = 0) { 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 ... // 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; return;
} }
if (!config.navigator) { if (!config.navigator) {

View File

@ -224,6 +224,8 @@ export default class Socket extends EventEmitter {
} }
disconnect() { disconnect() {
this._close(); this._close();
this._login = null;
this.subscriptions = {};
} }
async reconnect() { async reconnect() {
if (this._timer) { if (this._timer) {

View File

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

View File

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

View File

@ -21,8 +21,10 @@ const stop = (ddp) => {
promises = false; promises = false;
} }
if (ddp) {
ddp.removeListener('logged', logged); ddp.removeListener('logged', logged);
ddp.removeListener('disconnected', disconnected); ddp.removeListener('disconnected', disconnected);
}
logged = false; logged = false;
disconnected = false; disconnected = false;
@ -50,18 +52,21 @@ export default async function subscribeRoom({ rid, t }) {
}, 5000); }, 5000);
}; };
if (!this.ddp || !this.ddp.status) {
loop();
} else {
logged = this.ddp.on('logged', () => { logged = this.ddp.on('logged', () => {
clearTimeout(timer); clearTimeout(timer);
timer = false; timer = false;
promises = subscribe(this.ddp, rid); promises = subscribe(this.ddp, rid);
}); });
disconnected = this.ddp.on('disconnected', () => { loop(); }); disconnected = this.ddp.on('disconnected', () => {
if (this._login) {
if (!this.ddp.status) {
loop(); loop();
} else { }
});
promises = subscribe(this.ddp, rid); promises = subscribe(this.ddp, rid);
} }

View File

@ -24,6 +24,7 @@ export default async function subscribeRooms(id) {
}, 5000); }, 5000);
}; };
if (this.ddp) {
this.ddp.on('logged', () => { this.ddp.on('logged', () => {
clearTimeout(timer); clearTimeout(timer);
timer = false; timer = false;
@ -34,7 +35,11 @@ export default async function subscribeRooms(id) {
timer = true; timer = true;
}); });
this.ddp.on('disconnected', () => { loop(); }); this.ddp.on('disconnected', () => {
if (this._login) {
loop();
}
});
this.ddp.on('stream-notify-user', (ddpMessage) => { this.ddp.on('stream-notify-user', (ddpMessage) => {
const [type, data] = ddpMessage.fields.args; const [type, data] = ddpMessage.fields.args;
@ -52,7 +57,8 @@ export default async function subscribeRooms(id) {
}); });
} }
}); });
}
await subscriptions; await subscriptions;
console.log(this.ddp.subscriptions); // console.log(this.ddp.subscriptions);
} }

View File

@ -35,7 +35,7 @@ import getSettings from './methods/getSettings';
import getRooms from './methods/getRooms'; import getRooms from './methods/getRooms';
import getPermissions from './methods/getPermissions'; import getPermissions from './methods/getPermissions';
import getCustomEmoji from './methods/getCustomEmojis'; import getCustomEmoji from './methods/getCustomEmojis';
import canOpenRoom from './methods/canOpenRoom';
import _buildMessage from './methods/helpers/buildMessage'; import _buildMessage from './methods/helpers/buildMessage';
import loadMessagesForRoom from './methods/loadMessagesForRoom'; import loadMessagesForRoom from './methods/loadMessagesForRoom';
@ -51,6 +51,7 @@ const RocketChat = {
TOKEN_KEY, TOKEN_KEY,
subscribeRooms, subscribeRooms,
subscribeRoom, subscribeRoom,
canOpenRoom,
createChannel({ name, users, type }) { createChannel({ name, users, type }) {
return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type); return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type);
}, },

View File

@ -17,7 +17,10 @@ const getToken = function* getToken() {
} }
return JSON.parse(user); return JSON.parse(user);
} }
return yield put(setToken());
yield AsyncStorage.removeItem(RocketChat.TOKEN_KEY);
yield put(setToken());
return null;
}; };

57
app/sagas/deepLinking.js Normal file
View File

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

View File

@ -12,6 +12,7 @@ import pinnedMessages from './pinnedMessages';
import mentionedMessages from './mentionedMessages'; import mentionedMessages from './mentionedMessages';
import snippetedMessages from './snippetedMessages'; import snippetedMessages from './snippetedMessages';
import roomFiles from './roomFiles'; import roomFiles from './roomFiles';
import deepLinking from './deepLinking';
const root = function* root() { const root = function* root() {
yield all([ yield all([
@ -27,7 +28,8 @@ const root = function* root() {
pinnedMessages(), pinnedMessages(),
mentionedMessages(), mentionedMessages(),
snippetedMessages(), snippetedMessages(),
roomFiles() roomFiles(),
deepLinking()
]); ]);
}; };

View File

@ -1,14 +1,14 @@
import { put, call, takeLatest, race, take } from 'redux-saga/effects'; import { put, call, takeLatest, take } from 'redux-saga/effects';
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { SERVER } from '../actions/actionsTypes'; import { SERVER, LOGIN } from '../actions/actionsTypes';
import * as actions from '../actions'; import * as actions from '../actions';
import { connectRequest } from '../actions/connect'; import { connectRequest } from '../actions/connect';
import { serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server'; import { serverSuccess, serverFailure, setServer } from '../actions/server';
import { setRoles } from '../actions/roles'; import { setRoles } from '../actions/roles';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import * as NavigationService from '../containers/routes/NavigationService'; import { navigate } from '../containers/routes/NavigationService';
const validate = function* validate(server) { const validate = function* validate(server) {
return yield RocketChat.testServer(server); return yield RocketChat.testServer(server);
@ -21,6 +21,7 @@ const selectServer = function* selectServer({ server }) {
// yield RocketChat.disconnect(); // yield RocketChat.disconnect();
yield call([AsyncStorage, 'setItem'], 'currentServer', server); yield call([AsyncStorage, 'setItem'], 'currentServer', server);
// yield AsyncStorage.removeItem(RocketChat.TOKEN_KEY);
const settings = database.objects('settings'); const settings = database.objects('settings');
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
const permissions = database.objects('permissions'); const permissions = database.objects('permissions');
@ -33,7 +34,7 @@ const selectServer = function* selectServer({ server }) {
return result; return result;
}, {}))); }, {})));
yield put(connectRequest(server)); yield put(connectRequest());
} catch (e) { } catch (e) {
console.warn('selectServer', e); console.warn('selectServer', e);
} }
@ -51,23 +52,17 @@ const validateServer = function* validateServer({ server }) {
}; };
const addServer = function* addServer({ server }) { const addServer = function* addServer({ server }) {
yield put(serverRequest(server));
const { error } = yield race({
error: take(SERVER.FAILURE),
success: take(SERVER.SUCCESS)
});
if (!error) {
database.databases.serversDB.write(() => { database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server, current: false }, true); database.databases.serversDB.create('servers', { id: server, current: false }, true);
}); });
yield put(setServer(server)); yield put(setServer(server));
} yield take(LOGIN.SET_TOKEN);
navigate('LoginSignup');
}; };
const handleGotoAddServer = function* handleGotoAddServer() { const handleGotoAddServer = function* handleGotoAddServer() {
yield call(AsyncStorage.removeItem, RocketChat.TOKEN_KEY); yield call(AsyncStorage.removeItem, RocketChat.TOKEN_KEY);
yield call(NavigationService.navigate, 'AddServer'); yield call(navigate, 'AddServer');
}; };
const root = function* root() { const root = function* root() {

View File

@ -49,7 +49,6 @@ export default class NewServerView extends LoggedView {
submit = () => { submit = () => {
this.props.addServer(this.completeUrl(this.state.text)); this.props.addServer(this.completeUrl(this.state.text));
this.props.navigation.navigate('LoginSignup');
} }
completeUrl = (url) => { completeUrl = (url) => {

View File

@ -54,16 +54,22 @@ export default class RoomHeaderView extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
room: realm.objects('subscriptions').filtered('rid = $0', this.rid)[0] || {}, room: props.navigation.state.params.room
roomName: props.navigation.state.params.room.name
}; };
this.rid = props.navigation.state.params.room.rid; this.room = realm.objects('subscriptions').filtered('rid = $0', this.state.room.rid);
this.room = realm.objects('subscriptions').filtered('rid = $0', this.rid);
this.room.addListener(this.updateState);
} }
componentDidMount() { componentDidMount() {
this.updateState(); this.updateState();
this.room.addListener(this.updateState);
}
componentWillReceiveProps(nextProps) {
if (nextProps.navigation.state.params.room !== this.props.navigation.state.params.room) {
this.room.removeAllListeners();
this.room = realm.objects('subscriptions').filtered('rid = $0', nextProps.navigation.state.params.room.rid);
this.room.addListener(this.updateState);
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -71,7 +77,7 @@ export default class RoomHeaderView extends React.PureComponent {
} }
getUserStatus() { getUserStatus() {
const userId = this.rid.replace(this.props.user.id, '').trim(); const userId = this.state.room.rid.replace(this.props.user.id, '').trim();
const userInfo = this.props.activeUsers[userId]; const userInfo = this.props.activeUsers[userId];
return (userInfo && userInfo.status) || 'offline'; return (userInfo && userInfo.status) || 'offline';
} }
@ -82,7 +88,9 @@ export default class RoomHeaderView extends React.PureComponent {
} }
updateState = () => { updateState = () => {
if (this.room.length > 0) {
this.setState({ room: this.room[0] }); this.setState({ room: this.room[0] });
}
}; };
isDirect = () => this.state.room && this.state.room.t === 'd'; isDirect = () => this.state.room && this.state.room.t === 'd';
@ -98,11 +106,11 @@ export default class RoomHeaderView extends React.PureComponent {
/>); />);
renderCenter() { renderCenter() {
if (!this.state.roomName) { if (!this.state.room.name) {
return null; return null;
} }
let accessibilityLabel = this.state.roomName; let accessibilityLabel = this.state.room.name;
if (this.isDirect()) { if (this.isDirect()) {
accessibilityLabel += `, ${ this.getUserStatusLabel() }`; accessibilityLabel += `, ${ this.getUserStatusLabel() }`;
@ -125,11 +133,11 @@ export default class RoomHeaderView extends React.PureComponent {
style={styles.titleContainer} style={styles.titleContainer}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
accessibilityTraits='header' accessibilityTraits='header'
onPress={() => this.props.navigation.navigate({ key: 'RoomInfo', routeName: 'RoomInfo', params: { rid: this.rid } })} onPress={() => this.props.navigation.navigate({ key: 'RoomInfo', routeName: 'RoomInfo', params: { rid: this.state.rid } })}
> >
<Avatar <Avatar
text={this.state.roomName} text={this.state.room.name}
size={24} size={24}
style={styles.avatar} style={styles.avatar}
type={this.state.room.t} type={this.state.room.t}
@ -140,7 +148,7 @@ export default class RoomHeaderView extends React.PureComponent {
} }
</Avatar> </Avatar>
<View style={styles.titleTextContainer}> <View style={styles.titleTextContainer}>
<Text style={styles.title} allowFontScaling={false}>{this.state.roomName}</Text> <Text style={styles.title} allowFontScaling={false}>{this.state.room.name}</Text>
{ t && <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text>} { t && <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text>}

View File

@ -65,22 +65,17 @@ export default class RoomView extends LoggedView {
this.rid = this.rid =
props.rid || props.rid ||
props.navigation.state.params.room.rid; props.navigation.state.params.room.rid;
this.name = props.name ||
props.navigation.state.params.name ||
props.navigation.state.params.room.name;
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = { this.state = {
loaded: true, loaded: true,
joined: typeof props.rid === 'undefined', joined: typeof props.rid === 'undefined',
room: JSON.parse(JSON.stringify(this.rooms[0])) room: {}
}; };
this.onReactionPress = this.onReactionPress.bind(this); this.onReactionPress = this.onReactionPress.bind(this);
} }
async componentDidMount() { async componentDidMount() {
this.props.navigation.setParams({ await this.updateRoom();
title: this.name
});
await this.props.openRoom({ await this.props.openRoom({
...this.state.room ...this.state.room
}); });
@ -89,7 +84,6 @@ export default class RoomView extends LoggedView {
} else { } else {
this.props.setLastOpen(null); this.props.setLastOpen(null);
} }
this.rooms.addListener(this.updateRoom); this.rooms.addListener(this.updateRoom);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
@ -129,8 +123,10 @@ export default class RoomView extends LoggedView {
RocketChat.setReaction(shortname, messageId); RocketChat.setReaction(shortname, messageId);
}; };
updateRoom = () => { updateRoom = async() => {
this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) }); if (this.rooms.length > 0) {
await this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
}
} }
sendMessage = (message) => { sendMessage = (message) => {

View File

@ -123,13 +123,17 @@ export default class RoomsListView extends LoggedView {
} }
_onPressItem = async(item = {}) => { _onPressItem = async(item = {}) => {
// if user is using the search we need first to join/create room
if (!item.search) { if (!item.search) {
return this.props.navigation.navigate({ key: `Room-${ item._id }`, routeName: 'Room', params: { room: item, ...item } }); return goRoom({ rid: item.rid });
} }
if (item.t === 'd') { if (item.t === 'd') {
const sub = await RocketChat.createDirectMessageAndWait(item.username); // if user is using the search we need first to join/create room
return goRoom({ room: sub, name: sub.name }); try {
const sub = await RocketChat.createDirectMessage(item.username);
return goRoom(sub);
} catch (error) {
console.warn('_onPressItem', error);
}
} }
return goRoom(item); return goRoom(item);
} }

View File

@ -15,6 +15,7 @@
#import "SplashScreen.h" #import "SplashScreen.h"
#import <Fabric/Fabric.h> #import <Fabric/Fabric.h>
#import <Crashlytics/Crashlytics.h> #import <Crashlytics/Crashlytics.h>
#import <React/RCTLinkingManager.h>
@implementation AppDelegate @implementation AppDelegate
@ -70,4 +71,20 @@
{ {
[RCTPushNotificationManager didReceiveLocalNotification:notification]; [RCTPushNotificationManager didReceiveLocalNotification:notification];
} }
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
return [RCTLinkingManager application:application openURL:url
sourceApplication:sourceApplication annotation:annotation];
}
// Only if your app is using [Universal Links](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html).
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler
{
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
@end @end

View File

@ -20,6 +20,20 @@
<string>1.0.0</string> <string>1.0.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>rocketchat</string>
<key>CFBundleURLSchemes</key>
<array>
<string>rocketchat</string>
<string>https://go.rocket.chat</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>100</string> <string>100</string>
<key>Fabric</key> <key>Fabric</key>