Add deep linking support (#1805)
This commit is contained in:
parent
d5c3137946
commit
26950abb61
12 changed files with 109 additions and 17 deletions
|
|
@ -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,
|
||||
|
|
|
|||
15
app/app.js
15
app/app.js
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ const ViewTypes = keyMirror({
|
|||
|
||||
LAUNCH_LOGIN: null,
|
||||
LAUNCH_CHANNEL: null,
|
||||
|
||||
SET_DEEP_LINK_URL: null,
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -271,6 +271,7 @@ const state = {
|
|||
password: '',
|
||||
},
|
||||
root: {
|
||||
deepLinkURL: '',
|
||||
hydrationComplete: false,
|
||||
purge: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import {
|
|||
AppState,
|
||||
Dimensions,
|
||||
InteractionManager,
|
||||
Platform,
|
||||
NativeModules,
|
||||
Keyboard,
|
||||
NativeModules,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
const {StatusBarManager, MattermostShare, Initialization} = NativeModules;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue