Add deep linking support (#1805)

This commit is contained in:
Chris Duarte 2018-07-31 05:43:54 -07:00 committed by Elias Nahum
parent d5c3137946
commit 26950abb61
12 changed files with 109 additions and 17 deletions

View file

@ -135,6 +135,13 @@ export function recordLoadTime(screenName, category) {
};
}
export function setDeepLinkURL(url) {
return {
type: ViewTypes.SET_DEEP_LINK_URL,
url,
};
}
export default {
loadConfigAndLicense,
loadFromPushNotification,

View file

@ -2,13 +2,14 @@
// See LICENSE.txt for license information.
/* eslint-disable global-require*/
import {AsyncStorage, NativeModules} from 'react-native';
import {AsyncStorage, Linking, NativeModules} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {setDeepLinkURL} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {getCurrentLocale} from 'app/selectors/i18n';
@ -50,6 +51,9 @@ export default class App {
this.token = null;
this.url = null;
// Usage deeplinking
Linking.addEventListener('url', this.handleDeepLink);
this.getStartupThemes();
this.getAppCredentials();
}
@ -223,6 +227,11 @@ export default class App {
]);
};
handleDeepLink = (event) => {
const {url} = event;
store.dispatch(setDeepLinkURL(url));
}
launchApp = async () => {
const shouldStart = await handleManagedConfig();
if (shouldStart) {
@ -237,6 +246,10 @@ export default class App {
const {dispatch} = store;
Linking.getInitialURL().then((url) => {
dispatch(setDeepLinkURL(url));
});
let screen = 'SelectServer';
if (this.token && this.url) {
screen = 'Channel';

View file

@ -12,9 +12,8 @@ import mattermostManaged from 'app/mattermost_managed';
import Config from 'assets/config';
import {escapeRegex} from 'app/utils/markdown';
import {preventDoubleTap} from 'app/utils/tap';
import {normalizeProtocol} from 'app/utils/url';
import {matchPermalink, normalizeProtocol} from 'app/utils/url';
export default class MarkdownLink extends PureComponent {
static propTypes = {
@ -43,7 +42,7 @@ export default class MarkdownLink extends PureComponent {
return;
}
const match = this.matchPermalink(url, serverURL) || this.matchPermalink(url, siteURL);
const match = matchPermalink(url, serverURL) || matchPermalink(url, siteURL);
if (match) {
const teamName = match[1];
@ -58,14 +57,6 @@ export default class MarkdownLink extends PureComponent {
}
});
matchPermalink = (link, rootURL) => {
if (!rootURL) {
return null;
}
return new RegExp('^' + escapeRegex(rootURL) + '\\/([^\\/]+)\\/pl\\/(\\w+)').exec(link);
}
parseLinkLiteral = (literal) => {
let nextLiteral = literal;

View file

@ -5,9 +5,11 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {selectFocusedPostId} from 'mattermost-redux/actions/posts';
import {getConfig, getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {loadChannelsByTeamName, refreshChannelWithRetry} from 'app/actions/views/channel';
import {setDeepLinkURL} from 'app/actions/views/root';
import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
import PostList from './post_list';
@ -21,9 +23,12 @@ function makeMapStateToProps() {
const {deviceHeight} = state.device.dimension;
return {
deepLinkURL: state.views.root.deepLinkURL,
deviceHeight,
measureCellLayout,
postIds,
serverURL: getCurrentUrl(state),
siteURL: getConfig(state).SiteURL,
theme: getTheme(state),
};
};
@ -35,6 +40,7 @@ function mapDispatchToProps(dispatch) {
loadChannelsByTeamName,
refreshChannelWithRetry,
selectFocusedPostId,
setDeepLinkURL,
}, dispatch),
};
}

View file

@ -17,6 +17,7 @@ import {START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
import mattermostManaged from 'app/mattermost_managed';
import {makeExtraData} from 'app/utils/list_view';
import {changeOpacity} from 'app/utils/theme';
import {matchPermalink} from 'app/utils/url';
import DateHeader from './date_header';
import {isDateLine} from './date_header/utils';
@ -35,9 +36,11 @@ export default class PostList extends PureComponent {
loadChannelsByTeamName: PropTypes.func.isRequired,
refreshChannelWithRetry: PropTypes.func.isRequired,
selectFocusedPostId: PropTypes.func.isRequired,
setDeepLinkURL: PropTypes.func.isRequired,
}).isRequired,
channelId: PropTypes.string,
currentUserId: PropTypes.string,
deepLinkURL: PropTypes.string,
deviceHeight: PropTypes.number.isRequired,
extraData: PropTypes.any,
highlightPostId: PropTypes.string,
@ -54,7 +57,9 @@ export default class PostList extends PureComponent {
postIds: PropTypes.array.isRequired,
renderFooter: PropTypes.func,
renderReplies: PropTypes.bool,
serverURL: PropTypes.string.isRequired,
shouldRenderReplyButton: PropTypes.bool,
siteURL: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
};
@ -99,6 +104,11 @@ export default class PostList extends PureComponent {
if ((this.props.measureCellLayout || this.props.isSearchResult) && this.state.scrollToMessage) {
this.scrollListToMessageOffset();
}
if (this.props.deepLinkURL) {
this.handleDeepLink(this.props.deepLinkURL);
this.props.actions.setDeepLinkURL('');
}
}
componentWillUnmount() {
@ -112,6 +122,18 @@ export default class PostList extends PureComponent {
this.showingPermalink = false;
};
handleDeepLink = (url) => {
const {serverURL, siteURL} = this.props;
const match = matchPermalink(url, serverURL) || matchPermalink(url, siteURL);
if (match) {
const teamName = match[1];
const postId = match[2];
this.handlePermalinkPress(postId, teamName);
}
};
handlePermalinkPress = (postId, teamName) => {
const {actions, onPermalinkPress} = this.props;
@ -179,7 +201,7 @@ export default class PostList extends PureComponent {
scrollListToMessageOffset = () => {
const index = this.moreNewMessages ? this.props.postIds.length - 1 : this.newMessagesIndex;
if (index !== -1) {
let offset = this.getMeasurementOffset(index) - (3 * this.itemMeasurements[index]);
let offset = this.getMeasurementOffset(index) + this.itemMeasurements[index];
const windowHeight = this.state.postListHeight;
if (offset < windowHeight) {

View file

@ -67,6 +67,8 @@ const ViewTypes = keyMirror({
LAUNCH_LOGIN: null,
LAUNCH_CHANNEL: null,
SET_DEEP_LINK_URL: null,
});
export default {

View file

@ -271,6 +271,7 @@ const state = {
password: '',
},
root: {
deepLinkURL: '',
hydrationComplete: false,
purge: false,
},

View file

@ -7,9 +7,9 @@ import {
AppState,
Dimensions,
InteractionManager,
Platform,
NativeModules,
Keyboard,
NativeModules,
Platform,
} from 'react-native';
const {StatusBarManager, MattermostShare, Initialization} = NativeModules;

View file

@ -5,6 +5,18 @@ import {combineReducers} from 'redux';
import {General} from 'mattermost-redux/constants';
import {ViewTypes} from 'app/constants';
function deepLinkURL(state = '', action) {
switch (action.type) {
case ViewTypes.SET_DEEP_LINK_URL: {
return action.url;
}
default:
return state;
}
}
function hydrationComplete(state = false, action) {
switch (action.type) {
case General.STORE_REHYDRATION_COMPLETE:
@ -24,6 +36,7 @@ function purge(state = false, action) {
}
export default combineReducers({
deepLinkURL,
hydrationComplete,
purge,
});

View file

@ -58,6 +58,15 @@ export default class ChannelPostList extends PureComponent {
this.contentHeight = 0;
}
componentDidMount() {
this.mounted = true;
InteractionManager.runAfterInteractions(() => {
if (this.mounted === true) {
this.setState({loading: false});
}
});
}
componentWillReceiveProps(nextProps) {
const {postIds: nextPostIds} = nextProps;
@ -84,8 +93,8 @@ export default class ChannelPostList extends PureComponent {
}
}
componentDidMount() {
InteractionManager.runAfterInteractions(() => this.setState({loading: false}));
componentWillUnmount() {
this.mounted = false;
}
getVisiblePostIds = (props) => {

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {latinise} from './latinise.js';
import {escapeRegex} from './markdown';
import {Files} from 'mattermost-redux/constants';
@ -95,6 +96,14 @@ export function getScheme(url) {
return match && match[1];
}
export function matchPermalink(link, rootURL) {
if (!rootURL) {
return null;
}
return new RegExp('^' + escapeRegex(rootURL) + '\\/([^\\/]+)\\/pl\\/(\\w+)').exec(link);
}
export function getYouTubeVideoId(link) {
// https://youtube.com/watch?v=<id>
let match = (/youtube\.com\/watch\?\S*\bv=([a-zA-Z0-9_-]{6,11})/g).exec(link);

View file

@ -11,6 +11,7 @@
#import <AVFoundation/AVFoundation.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#if __has_include(<React/RNSentry.h>)
#import <React/RNSentry.h> // This is used for versions of react >= 0.40
#else
@ -95,4 +96,22 @@
[[SessionManager sharedSession] createSessionForRequestRequest:identifier];
}
// Required for deeplinking
- (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