forked from Ivasoft/mattermost-mobile
Add DeepLink support (#6869)
This commit is contained in:
@@ -10,7 +10,7 @@ import {addChannelToDefaultCategory, storeCategories} from '@actions/local/categ
|
||||
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {Events, General, Preferences, Screens} from '@constants';
|
||||
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {privateChannelJoinPrompt} from '@helpers/api/channel';
|
||||
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
@@ -28,7 +28,6 @@ import {generateChannelNameFromDisplayName, getDirectChannelName, isDMorGM} from
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logError, logInfo} from '@utils/log';
|
||||
import {showMuteChannelSnackbar} from '@utils/snack_bar';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
import {fetchGroupsForChannelIfConstrained} from './groups';
|
||||
@@ -655,7 +654,7 @@ export async function switchToChannelByName(serverUrl: string, channelName: stri
|
||||
let joinedTeam = false;
|
||||
let teamId = '';
|
||||
try {
|
||||
if (teamName === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
if (teamName === DeepLink.Redirect) {
|
||||
teamId = await getCurrentTeamId(database);
|
||||
} else {
|
||||
const team = await getTeamByName(database, teamName);
|
||||
|
||||
@@ -5,26 +5,18 @@ import {IntlShape} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/remote/apps';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {Client} from '@client/rest';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {AppCallResponseTypes} from '@constants/apps';
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import IntegrationsManager from '@managers/integrations_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getConfig, getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getTeammateNameDisplay, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {showAppForm, showModal} from '@screens/navigation';
|
||||
import * as DraftUtils from '@utils/draft';
|
||||
import {matchDeepLink, tryOpenURL} from '@utils/url';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import {makeDirectChannel, switchToChannelById, switchToChannelByName} from './channel';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkDM, DeepLinkGM, DeepLinkPlugin} from '@typings/launch';
|
||||
import {showAppForm} from '@screens/navigation';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
export const executeCommand = async (serverUrl: string, intl: IntlShape, message: string, channelId: string, rootId?: string): Promise<{data?: CommandResponse; error?: string | {message: string}}> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -144,60 +136,9 @@ export const handleGotoLocation = async (serverUrl: string, intl: IntlShape, loc
|
||||
|
||||
const config = await getConfig(database);
|
||||
const match = matchDeepLink(location, serverUrl, config?.SiteURL);
|
||||
let linkServerUrl: string | undefined;
|
||||
if (match?.data?.serverUrl) {
|
||||
linkServerUrl = DatabaseManager.searchUrl(match.data.serverUrl);
|
||||
}
|
||||
|
||||
if (match && linkServerUrl) {
|
||||
switch (match.type) {
|
||||
case DeepLinkType.Channel: {
|
||||
const data = match.data as DeepLinkChannel;
|
||||
switchToChannelByName(linkServerUrl, data.channelName, data.teamName, DraftUtils.errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Permalink: {
|
||||
const data = match.data as DeepLinkPermalink;
|
||||
showPermalink(linkServerUrl, data.teamName, data.postId, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.DirectMessage: {
|
||||
const data = match.data as DeepLinkDM;
|
||||
if (!data.userName) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
if (data.serverUrl !== serverUrl) {
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
}
|
||||
const user = (await queryUsersByUsername(database, [data.userName]).fetch())[0];
|
||||
if (!user) {
|
||||
DraftUtils.errorUnkownUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
makeDirectChannel(linkServerUrl, user.id, displayUsername(user, intl.locale, await getTeammateNameDisplay(database)), true);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.GroupMessage: {
|
||||
const data = match.data as DeepLinkGM;
|
||||
if (!data.channelId) {
|
||||
DraftUtils.errorBadChannel(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
switchToChannelById(linkServerUrl, data.channelId);
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Plugin: {
|
||||
const data = match.data as DeepLinkPlugin;
|
||||
showModal('PluginInternal', data.id, {link: location});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
handleDeepLink(location, intl, location);
|
||||
} else {
|
||||
const {formatMessage} = intl;
|
||||
const onError = () => Alert.alert(
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeepLink} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentTeam} from '@queries/servers/team';
|
||||
import {displayPermalink} from '@utils/permalink';
|
||||
import {PERMALINK_GENERIC_TEAM_NAME_REDIRECT} from '@utils/url';
|
||||
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, intl: IntlShape, openAsPermalink = true) => {
|
||||
export const showPermalink = async (serverUrl: string, teamName: string, postId: string, openAsPermalink = true) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -18,7 +17,7 @@ export const showPermalink = async (serverUrl: string, teamName: string, postId:
|
||||
try {
|
||||
let name = teamName;
|
||||
let team: TeamModel | undefined;
|
||||
if (!name || name === PERMALINK_GENERIC_TEAM_NAME_REDIRECT) {
|
||||
if (!name || name === DeepLink.Redirect) {
|
||||
team = await getCurrentTeam(database);
|
||||
if (team) {
|
||||
name = team.name;
|
||||
|
||||
@@ -9,19 +9,14 @@ import {Alert, StyleSheet, Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {switchToChannelByName} from '@actions/remote/channel';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
|
||||
import {errorBadChannel} from '@utils/draft';
|
||||
import {handleDeepLink, matchDeepLink} from '@utils/deep_link';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkPermalink, DeepLinkWithData} from '@typings/launch';
|
||||
import {normalizeProtocol, tryOpenURL} from '@utils/url';
|
||||
|
||||
type MarkdownLinkProps = {
|
||||
children: ReactElement;
|
||||
@@ -65,28 +60,27 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
|
||||
return;
|
||||
}
|
||||
|
||||
const match: DeepLinkWithData | null = matchDeepLink(url, serverUrl, siteURL);
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (match && match.data?.teamName) {
|
||||
if (match.type === DeepLinkType.Channel) {
|
||||
await switchToChannelByName(serverUrl, (match?.data as DeepLinkChannel).channelName, match.data?.teamName, errorBadChannel, intl);
|
||||
} else if (match.type === DeepLinkType.Permalink) {
|
||||
showPermalink(serverUrl, match.data.teamName, (match.data as DeepLinkPermalink).postId, intl);
|
||||
const match = matchDeepLink(url, serverUrl, siteURL);
|
||||
|
||||
if (match) {
|
||||
const {error} = await handleDeepLink(url, intl);
|
||||
if (error) {
|
||||
tryOpenURL(url, onError);
|
||||
}
|
||||
} else {
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.title',
|
||||
defaultMessage: 'Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.link.error.text',
|
||||
defaultMessage: 'Unable to open the link.',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
tryOpenURL(url, onError);
|
||||
}
|
||||
}), [href, intl.locale, serverUrl, siteURL]);
|
||||
|
||||
@@ -139,7 +139,7 @@ const Post = ({
|
||||
|
||||
const handlePostPress = () => {
|
||||
if ([Screens.SAVED_MESSAGES, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) {
|
||||
showPermalink(serverUrl, '', post.id, intl);
|
||||
showPermalink(serverUrl, '', post.id);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const DeepLinkType = {
|
||||
Invalid: 'invalid',
|
||||
Permalink: 'permalink',
|
||||
Plugin: 'plugin',
|
||||
Redirect: '_redirect',
|
||||
} as const;
|
||||
|
||||
export default DeepLinkType;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
const LaunchType = {
|
||||
AddServer: 'add-server',
|
||||
AddServerFromDeepLink: 'add-server-deeplink',
|
||||
Normal: 'normal',
|
||||
DeepLink: 'deeplink',
|
||||
Notification: 'notification',
|
||||
|
||||
@@ -7,20 +7,20 @@ import {Notifications} from 'react-native-notifications';
|
||||
|
||||
import {appEntry, pushNotificationEntry, upgradeEntry} from '@actions/remote/entry';
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import {Screens, DeepLink, Events, Launch, PushNotification} from '@constants';
|
||||
import {DeepLink, Events, Launch, PushNotification} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {getOnboardingViewed} from '@queries/app/global';
|
||||
import {getThemeForCurrentTeam} from '@queries/servers/preference';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import {queryMyTeams} from '@queries/servers/team';
|
||||
import {goToScreen, resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation';
|
||||
import {resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {getLaunchPropsFromDeepLink} from '@utils/deep_link';
|
||||
import {logInfo} from '@utils/log';
|
||||
import {convertToNotificationData} from '@utils/notification';
|
||||
import {parseDeepLink} from '@utils/url';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||
import type {DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||
|
||||
const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, PushNotification.NOTIFICATION_TYPE.SESSION];
|
||||
|
||||
@@ -67,13 +67,18 @@ const launchAppFromNotification = async (notification: NotificationWithData, col
|
||||
|
||||
* @returns a redirection to a screen, either onboarding, add_server, login or home depending on the scenario
|
||||
*/
|
||||
const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
const launchApp = async (props: LaunchProps) => {
|
||||
let serverUrl: string | undefined;
|
||||
switch (props?.launchType) {
|
||||
case Launch.DeepLink:
|
||||
if (props.extra?.type !== DeepLink.Invalid) {
|
||||
const extra = props.extra as DeepLinkWithData;
|
||||
serverUrl = extra.data?.serverUrl;
|
||||
const existingServer = DatabaseManager.searchUrl(extra.data!.serverUrl);
|
||||
serverUrl = existingServer;
|
||||
props.serverUrl = serverUrl || extra.data?.serverUrl;
|
||||
if (!serverUrl) {
|
||||
props.launchError = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Launch.Notification: {
|
||||
@@ -142,17 +147,17 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
return resetToOnboarding(props);
|
||||
}
|
||||
|
||||
return launchToServer(props, resetNavigation);
|
||||
return resetToSelectServer(props);
|
||||
};
|
||||
|
||||
const launchToHome = async (props: LaunchProps) => {
|
||||
let openPushNotification = false;
|
||||
|
||||
switch (props.launchType) {
|
||||
case Launch.DeepLink:
|
||||
// TODO:
|
||||
// deepLinkEntry({props.serverUrl, props.extra});
|
||||
case Launch.DeepLink: {
|
||||
appEntry(props.serverUrl!);
|
||||
break;
|
||||
}
|
||||
case Launch.Notification: {
|
||||
const extra = props.extra as NotificationWithData;
|
||||
openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local);
|
||||
@@ -185,55 +190,8 @@ const launchToHome = async (props: LaunchProps) => {
|
||||
return resetToTeams();
|
||||
};
|
||||
|
||||
const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
|
||||
if (resetNavigation) {
|
||||
return resetToSelectServer(props);
|
||||
}
|
||||
|
||||
// This is being called for Deeplinks, but needs to be revisited when
|
||||
// the implementation of deep links is complete
|
||||
const title = '';
|
||||
return goToScreen(Screens.SERVER, title, {...props});
|
||||
};
|
||||
|
||||
export const relaunchApp = (props: LaunchProps, resetNavigation = false) => {
|
||||
return launchApp(props, resetNavigation);
|
||||
};
|
||||
|
||||
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => {
|
||||
const parsed = parseDeepLink(deepLinkUrl);
|
||||
const launchProps: LaunchProps = {
|
||||
launchType: Launch.DeepLink,
|
||||
coldStart,
|
||||
};
|
||||
|
||||
switch (parsed.type) {
|
||||
case DeepLink.Invalid:
|
||||
launchProps.launchError = true;
|
||||
break;
|
||||
case DeepLink.Channel: {
|
||||
const parsedData = parsed.data as DeepLinkChannel;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLink.DirectMessage: {
|
||||
const parsedData = parsed.data as DeepLinkDM;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLink.GroupMessage: {
|
||||
const parsedData = parsed.data as DeepLinkGM;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLink.Permalink: {
|
||||
const parsedData = parsed.data as DeepLinkPermalink;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return launchProps;
|
||||
export const relaunchApp = (props: LaunchProps) => {
|
||||
return launchApp(props);
|
||||
};
|
||||
|
||||
export const getLaunchPropsFromNotification = async (notification: NotificationWithData, coldStart = false): Promise<LaunchProps> => {
|
||||
|
||||
@@ -11,9 +11,9 @@ import {Events, Sso} from '@constants';
|
||||
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
|
||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
||||
import * as analytics from '@managers/analytics';
|
||||
import {getAllServers} from '@queries/app/servers';
|
||||
import {handleDeepLink} from '@utils/deep_link';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
|
||||
@@ -64,8 +64,7 @@ class GlobalEventHandler {
|
||||
}
|
||||
|
||||
if (event.url) {
|
||||
const props = getLaunchPropsFromDeepLink(event.url);
|
||||
relaunchApp(props);
|
||||
handleDeepLink(event.url);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ class SessionManager {
|
||||
await storeOnboardingViewedValue(false);
|
||||
}
|
||||
|
||||
relaunchApp({launchType, serverUrl, displayName}, true);
|
||||
relaunchApp({launchType, serverUrl, displayName});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -197,7 +197,7 @@ class SessionManager {
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const serverDisplayName = await getServerDisplayName(serverUrl);
|
||||
|
||||
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName}, true);
|
||||
await relaunchApp({launchType: Launch.Normal, serverUrl, displayName: serverDisplayName});
|
||||
if (activeServerUrl) {
|
||||
addNewServer(getThemeFromState(), serverUrl, serverDisplayName);
|
||||
} else {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {logout} from '@actions/remote/session';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerDisplayName, useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {alertServerLogout} from '@utils/server';
|
||||
@@ -30,6 +32,7 @@ const LogOut = () => {
|
||||
const serverDisplayName = useServerDisplayName();
|
||||
|
||||
const onLogout = useCallback(preventDoubleTap(() => {
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
|
||||
}), [serverDisplayName, serverUrl, intl]);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useIntl} from 'react-intl';
|
||||
import {Animated, DeviceEventEmitter, Platform, StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||
import {RectButton} from 'react-native-gesture-handler';
|
||||
import Swipeable from 'react-native-gesture-handler/Swipeable';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {storeMultiServerTutorial} from '@actions/app/global';
|
||||
import {appEntry} from '@actions/remote/entry';
|
||||
@@ -17,7 +18,7 @@ import Loading from '@components/loading';
|
||||
import ServerIcon from '@components/server_icon';
|
||||
import TutorialHighlight from '@components/tutorial_highlight';
|
||||
import TutorialSwipeLeft from '@components/tutorial_highlight/swipe_left';
|
||||
import {Events} from '@constants';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -178,6 +179,7 @@ const ServerItem = ({
|
||||
};
|
||||
|
||||
const logoutServer = async () => {
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
await logout(server.url);
|
||||
|
||||
if (isActive) {
|
||||
@@ -190,6 +192,7 @@ const ServerItem = ({
|
||||
const removeServer = async () => {
|
||||
const skipLogoutFromServer = server.lastActiveAt === 0;
|
||||
await dismissBottomSheet();
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
await logout(server.url, skipLogoutFromServer, true);
|
||||
};
|
||||
|
||||
@@ -286,6 +289,7 @@ const ServerItem = ({
|
||||
if (server.lastActiveAt) {
|
||||
setSwitching(true);
|
||||
await dismissBottomSheet();
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
DatabaseManager.setActiveServerDatabase(server.url);
|
||||
await appEntry(server.url, Date.now());
|
||||
return;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {Events, Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {findChannels, popToRoot} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {handleDeepLink} from '@utils/deep_link';
|
||||
import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
|
||||
import {notificationError} from '@utils/notification';
|
||||
|
||||
@@ -24,7 +25,7 @@ import SavedMessages from './saved_messages';
|
||||
import Search from './search';
|
||||
import TabBar from './tab_bar';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
import type {DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// We do this on iOS to avoid conflicts betwen ReactNavigation & Wix ReactNativeNavigation
|
||||
@@ -95,6 +96,15 @@ export default function HomeScreen(props: HomeProps) {
|
||||
};
|
||||
}, [intl.locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.launchType === 'deeplink') {
|
||||
const deepLink = props.extra as DeepLinkWithData;
|
||||
if (deepLink?.url) {
|
||||
handleDeepLink(deepLink.url);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationContainer
|
||||
@@ -111,7 +121,7 @@ export default function HomeScreen(props: HomeProps) {
|
||||
}}
|
||||
>
|
||||
<Tab.Navigator
|
||||
screenOptions={{headerShown: false, lazy: true, unmountOnBlur: false}}
|
||||
screenOptions={{headerShown: false, unmountOnBlur: false, lazy: true}}
|
||||
backBehavior='none'
|
||||
tabBar={(tabProps: BottomTabBarProps) => (
|
||||
<TabBar
|
||||
@@ -121,29 +131,29 @@ export default function HomeScreen(props: HomeProps) {
|
||||
>
|
||||
<Tab.Screen
|
||||
name={Screens.HOME}
|
||||
options={{title: 'Channel', unmountOnBlur: false, tabBarTestID: 'tab_bar.home.tab', freezeOnBlur: true}}
|
||||
options={{tabBarTestID: 'tab_bar.home.tab', unmountOnBlur: false, freezeOnBlur: true}}
|
||||
>
|
||||
{() => <ChannelList {...props}/>}
|
||||
</Tab.Screen>
|
||||
<Tab.Screen
|
||||
name={Screens.SEARCH}
|
||||
component={Search}
|
||||
options={{unmountOnBlur: false, lazy: true, tabBarTestID: 'tab_bar.search.tab', freezeOnBlur: true}}
|
||||
options={{tabBarTestID: 'tab_bar.search.tab', unmountOnBlur: false, freezeOnBlur: true, lazy: true}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={Screens.MENTIONS}
|
||||
component={RecentMentions}
|
||||
options={{tabBarTestID: 'tab_bar.mentions.tab', lazy: true, unmountOnBlur: false, freezeOnBlur: true}}
|
||||
options={{tabBarTestID: 'tab_bar.mentions.tab', freezeOnBlur: true, lazy: true}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={Screens.SAVED_MESSAGES}
|
||||
component={SavedMessages}
|
||||
options={{unmountOnBlur: false, lazy: true, tabBarTestID: 'tab_bar.saved_messages.tab', freezeOnBlur: true}}
|
||||
options={{tabBarTestID: 'tab_bar.saved_messages.tab', freezeOnBlur: true, lazy: true}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={Screens.ACCOUNT}
|
||||
component={Account}
|
||||
options={{tabBarTestID: 'tab_bar.account.tab', lazy: true, unmountOnBlur: false, freezeOnBlur: true}}
|
||||
options={{tabBarTestID: 'tab_bar.account.tab', freezeOnBlur: true, lazy: true}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</NavigationContainer>
|
||||
|
||||
@@ -42,7 +42,7 @@ const OptionMenus = ({
|
||||
|
||||
const handlePermalink = useCallback(() => {
|
||||
if (fileInfo.post_id) {
|
||||
showPermalink(serverUrl, '', fileInfo.post_id, intl);
|
||||
showPermalink(serverUrl, '', fileInfo.post_id);
|
||||
setAction('opening');
|
||||
}
|
||||
}, [intl, serverUrl, fileInfo.post_id, setAction]);
|
||||
|
||||
@@ -209,6 +209,7 @@ const LoginOptions = ({
|
||||
{hasLoginForm &&
|
||||
<Form
|
||||
config={config}
|
||||
extra={extra}
|
||||
keyboardAwareRef={keyboardAwareRef}
|
||||
license={license}
|
||||
launchError={launchError}
|
||||
|
||||
@@ -247,12 +247,15 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
|
||||
if (passProps.launchType === Launch.AddServer) {
|
||||
if (passProps.launchType === Launch.AddServer || passProps.launchType === Launch.AddServerFromDeepLink) {
|
||||
dismissModal({componentId: Screens.SERVER});
|
||||
dismissModal({componentId: Screens.LOGIN});
|
||||
dismissModal({componentId: Screens.SSO});
|
||||
dismissModal({componentId: Screens.BOTTOM_SHEET});
|
||||
DeviceEventEmitter.emit(Events.FETCHING_POSTS, false);
|
||||
if (passProps.launchType === Launch.AddServerFromDeepLink) {
|
||||
Navigation.updateProps(Screens.HOME, {launchType: Launch.DeepLink, extra: passProps.extra});
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView);
|
||||
|
||||
const Onboarding = ({
|
||||
theme,
|
||||
...props
|
||||
}: OnboardingProps) => {
|
||||
const {width} = useWindowDimensions();
|
||||
const {slidesData} = useSlidesData();
|
||||
@@ -73,7 +74,7 @@ const Onboarding = ({
|
||||
// mark the onboarding as already viewed
|
||||
storeOnboardingViewedValue();
|
||||
|
||||
goToScreen(Screens.SERVER, '', {animated: true, theme}, loginAnimationOptions());
|
||||
goToScreen(Screens.SERVER, '', {animated: true, theme, ...props}, loginAnimationOptions());
|
||||
}, []);
|
||||
|
||||
const nextSlide = useCallback(() => {
|
||||
|
||||
@@ -406,7 +406,8 @@ function Permalink({
|
||||
function processThreadPosts(posts: PostModel[], postId: string) {
|
||||
posts.sort((a, b) => b.createAt - a.createAt);
|
||||
const postIndex = posts.findIndex((p) => p.id === postId);
|
||||
return posts.slice(postIndex - POSTS_LIMIT, postIndex + POSTS_LIMIT + 1);
|
||||
const start = postIndex - POSTS_LIMIT;
|
||||
return posts.slice(start < 0 ? postIndex : start, postIndex + POSTS_LIMIT + 1);
|
||||
}
|
||||
|
||||
export default Permalink;
|
||||
|
||||
@@ -90,15 +90,16 @@ const Server = ({
|
||||
const styles = getStyleSheet(theme);
|
||||
const {formatMessage} = intl;
|
||||
const disableServerUrl = Boolean(managedConfig?.allowOtherServers === 'false' && managedConfig?.serverUrl);
|
||||
const additionalServer = launchType === Launch.AddServerFromDeepLink || launchType === Launch.AddServer;
|
||||
|
||||
useEffect(() => {
|
||||
let serverName: string | undefined = defaultDisplayName || managedConfig?.serverName || LocalConfig.DefaultServerName;
|
||||
let serverUrl: string | undefined = defaultServerUrl || managedConfig?.serverUrl || LocalConfig.DefaultServerUrl;
|
||||
let autoconnect = managedConfig?.allowOtherServers === 'false' || LocalConfig.AutoSelectServerUrl;
|
||||
|
||||
if (launchType === Launch.DeepLink) {
|
||||
if (launchType === Launch.DeepLink || launchType === Launch.AddServerFromDeepLink) {
|
||||
const deepLinkServerUrl = (extra as DeepLinkWithData).data?.serverUrl;
|
||||
if (managedConfig) {
|
||||
if (managedConfig.serverUrl) {
|
||||
autoconnect = (managedConfig.allowOtherServers === 'false' && managedConfig.serverUrl === deepLinkServerUrl);
|
||||
if (managedConfig.serverUrl !== deepLinkServerUrl || launchError) {
|
||||
Alert.alert('', intl.formatMessage({
|
||||
@@ -343,11 +344,11 @@ const Server = ({
|
||||
style={styles.flex}
|
||||
>
|
||||
<ServerHeader
|
||||
additionalServer={launchType === Launch.AddServer}
|
||||
additionalServer={additionalServer}
|
||||
theme={theme}
|
||||
/>
|
||||
<ServerForm
|
||||
autoFocus={launchType === Launch.AddServer}
|
||||
autoFocus={additionalServer}
|
||||
buttonDisabled={buttonDisabled}
|
||||
connecting={connecting}
|
||||
displayName={displayName}
|
||||
|
||||
@@ -20,7 +20,7 @@ const OpenInChannelOption = ({threadId}: Props) => {
|
||||
|
||||
const onHandlePress = useCallback(async () => {
|
||||
await dismissBottomSheet(Screens.THREAD_OPTIONS);
|
||||
showPermalink(serverUrl, '', threadId, intl);
|
||||
showPermalink(serverUrl, '', threadId);
|
||||
}, [intl, serverUrl, threadId]);
|
||||
|
||||
return (
|
||||
|
||||
183
app/utils/deep_link/index.ts
Normal file
183
app/utils/deep_link/index.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createIntl, IntlShape} from 'react-intl';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel';
|
||||
import {appEntry} from '@actions/remote/entry';
|
||||
import {showPermalink} from '@actions/remote/permalink';
|
||||
import {fetchUsersByUsernames} from '@actions/remote/user';
|
||||
import {DeepLink, Launch, Screens} from '@constants';
|
||||
import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||
import {getActiveServerUrl} from '@queries/app/servers';
|
||||
import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToRoot, showModal} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {errorBadChannel, errorUnkownUser} from '@utils/draft';
|
||||
import {logError} from '@utils/log';
|
||||
import {escapeRegex} from '@utils/markdown';
|
||||
import {addNewServer} from '@utils/server';
|
||||
import {removeProtocol} from '@utils/url';
|
||||
|
||||
import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkPlugin, DeepLinkWithData, LaunchProps} from '@typings/launch';
|
||||
|
||||
export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, location?: string) {
|
||||
try {
|
||||
const parsed = parseDeepLink(deepLinkUrl);
|
||||
if (parsed.type === DeepLink.Invalid || !parsed.data || !parsed.data.serverUrl) {
|
||||
return {error: true};
|
||||
}
|
||||
|
||||
const currentServerUrl = await getActiveServerUrl();
|
||||
const existingServerUrl = DatabaseManager.searchUrl(parsed.data.serverUrl);
|
||||
|
||||
// After checking the server for http & https then we add it
|
||||
if (!existingServerUrl) {
|
||||
const theme = EphemeralStore.theme || getDefaultThemeByAppearance();
|
||||
addNewServer(theme, parsed.data.serverUrl, undefined, parsed);
|
||||
return {error: false};
|
||||
}
|
||||
|
||||
if (existingServerUrl !== currentServerUrl && NavigationStore.getVisibleScreen()) {
|
||||
await dismissAllModalsAndPopToRoot();
|
||||
DatabaseManager.setActiveServerDatabase(existingServerUrl);
|
||||
appEntry(existingServerUrl, Date.now());
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
|
||||
}
|
||||
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(existingServerUrl);
|
||||
const currentUser = await getCurrentUser(database);
|
||||
const locale = currentUser?.locale || DEFAULT_LOCALE;
|
||||
const intl = intlShape || createIntl({
|
||||
locale,
|
||||
messages: getTranslations(locale),
|
||||
});
|
||||
|
||||
switch (parsed.type) {
|
||||
case DeepLink.Channel: {
|
||||
const deepLinkData = parsed.data as DeepLinkChannel;
|
||||
switchToChannelByName(existingServerUrl, deepLinkData.channelName, deepLinkData.teamName, errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLink.DirectMessage: {
|
||||
const deepLinkData = parsed.data as DeepLinkDM;
|
||||
const userIds = await queryUsersByUsername(database, [deepLinkData.userName]).fetchIds();
|
||||
let userId = userIds.length ? userIds[0] : undefined;
|
||||
if (!userId) {
|
||||
const {users} = await fetchUsersByUsernames(existingServerUrl, [deepLinkData.userName], false);
|
||||
if (users?.length) {
|
||||
userId = users[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
makeDirectChannel(existingServerUrl, userId, '', true);
|
||||
} else {
|
||||
errorUnkownUser(intl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DeepLink.GroupMessage: {
|
||||
const deepLinkData = parsed.data as DeepLinkGM;
|
||||
switchToChannelByName(existingServerUrl, deepLinkData.channelId, deepLinkData.teamName, errorBadChannel, intl);
|
||||
break;
|
||||
}
|
||||
case DeepLink.Permalink: {
|
||||
const deepLinkData = parsed.data as DeepLinkPermalink;
|
||||
if (NavigationStore.hasModalsOpened() || ![Screens.HOME, Screens.CHANNEL, Screens.GLOBAL_THREADS, Screens.THREAD].includes(NavigationStore.getVisibleScreen())) {
|
||||
await dismissAllModalsAndPopToRoot();
|
||||
}
|
||||
showPermalink(existingServerUrl, deepLinkData.teamName, deepLinkData.postId);
|
||||
break;
|
||||
}
|
||||
case DeepLink.Plugin: {
|
||||
const deepLinkData = parsed.data as DeepLinkPlugin;
|
||||
showModal('PluginInternal', deepLinkData.id, {link: location});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {error: false};
|
||||
} catch (error) {
|
||||
logError('Failed to open channel from deeplink', error);
|
||||
return {error: true};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData {
|
||||
const url = removeProtocol(deepLinkUrl);
|
||||
|
||||
let match = new RegExp('(.*)\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelName: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], postId: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], userName: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl: match[1], teamName: match[2], channelId: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/plugins\\/([^\\/]+)\\/(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.Plugin, url: deepLinkUrl, data: {serverUrl: match[1], id: match[2], teamName: ''}};
|
||||
}
|
||||
|
||||
return {type: DeepLink.Invalid, url: deepLinkUrl};
|
||||
}
|
||||
|
||||
export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) {
|
||||
if (!url || (!serverURL && !siteURL)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let urlToMatch = url;
|
||||
const urlBase = serverURL || siteURL || '';
|
||||
|
||||
if (!url.startsWith('mattermost://')) {
|
||||
// If url doesn't contain site or server URL, tack it on.
|
||||
// e.g. <jump to convo> URLs from autolink plugin.
|
||||
const match = new RegExp(escapeRegex(urlBase)).exec(url);
|
||||
if (!match) {
|
||||
urlToMatch = urlBase + url;
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParse(urlToMatch).hostname === urlParse(urlBase).hostname) {
|
||||
return urlToMatch;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string, coldStart = false): LaunchProps => {
|
||||
const parsed = parseDeepLink(deepLinkUrl);
|
||||
const launchProps: LaunchProps = {
|
||||
launchType: Launch.DeepLink,
|
||||
coldStart,
|
||||
};
|
||||
|
||||
switch (parsed.type) {
|
||||
case DeepLink.Invalid:
|
||||
launchProps.launchError = true;
|
||||
break;
|
||||
default: {
|
||||
launchProps.extra = parsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return launchProps;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {changeOpacity} from '@utils/theme';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
import type ServersModel from '@typings/database/models/app/servers';
|
||||
import type {DeepLinkWithData} from '@typings/launch';
|
||||
|
||||
export function isSupportedServer(currentVersion: string) {
|
||||
return isMinimumServerVersion(currentVersion, SupportedServer.MAJOR_VERSION, SupportedServer.MIN_VERSION, SupportedServer.PATCH_VERSION);
|
||||
@@ -39,15 +40,16 @@ export function semverFromServerVersion(value: string) {
|
||||
return `${major}.${minor}.${patch}`;
|
||||
}
|
||||
|
||||
export async function addNewServer(theme: Theme, serverUrl?: string, displayName?: string) {
|
||||
export async function addNewServer(theme: Theme, serverUrl?: string, displayName?: string, deepLinkProps?: DeepLinkWithData) {
|
||||
await dismissBottomSheet();
|
||||
const closeButtonId = 'close-server';
|
||||
const props = {
|
||||
closeButtonId,
|
||||
displayName,
|
||||
launchType: Launch.AddServer,
|
||||
launchType: deepLinkProps ? Launch.AddServerFromDeepLink : Launch.AddServer,
|
||||
serverUrl,
|
||||
theme,
|
||||
extra: deepLinkProps,
|
||||
};
|
||||
const options = buildServerModalOptions(theme, closeButtonId);
|
||||
|
||||
|
||||
@@ -5,14 +5,11 @@ import GenericClient from '@mattermost/react-native-network-client';
|
||||
import {Linking} from 'react-native';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {Files, DeepLink} from '@constants';
|
||||
import {Files} from '@constants';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import {escapeRegex} from '@utils/markdown';
|
||||
|
||||
import {latinise} from './latinise';
|
||||
|
||||
import type {DeepLinkWithData} from '@typings/launch';
|
||||
|
||||
const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
|
||||
|
||||
export function isValidUrl(url = '') {
|
||||
@@ -153,66 +150,6 @@ export function getScheme(url: string) {
|
||||
return match && match[1];
|
||||
}
|
||||
|
||||
export const PERMALINK_GENERIC_TEAM_NAME_REDIRECT = '_redirect';
|
||||
|
||||
export function parseDeepLink(deepLinkUrl: string): DeepLinkWithData {
|
||||
const url = removeProtocol(deepLinkUrl);
|
||||
|
||||
let match = new RegExp('(.*)\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.Channel, data: {serverUrl: match[1], teamName: match[2], channelName: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/([^\\/]+)\\/pl\\/(\\w+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.Permalink, data: {serverUrl: match[1], teamName: match[2], postId: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.DirectMessage, data: {serverUrl: match[1], teamName: match[2], userName: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/([^\\/]+)\\/messages\\/(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.GroupMessage, data: {serverUrl: match[1], teamName: match[2], channelId: match[3]}};
|
||||
}
|
||||
|
||||
match = new RegExp('(.*)\\/plugins\\/([^\\/]+)\\/(\\S+)').exec(url);
|
||||
if (match) {
|
||||
return {type: DeepLink.Plugin, data: {serverUrl: match[1], id: match[2], teamName: ''}};
|
||||
}
|
||||
|
||||
return {type: DeepLink.Invalid};
|
||||
}
|
||||
|
||||
export function matchDeepLink(url?: string, serverURL?: string, siteURL?: string) {
|
||||
if (!url || (!serverURL && !siteURL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let urlToMatch = url;
|
||||
const urlBase = serverURL || siteURL || '';
|
||||
|
||||
if (!url.startsWith('mattermost://')) {
|
||||
// If url doesn't contain site or server URL, tack it on.
|
||||
// e.g. <jump to convo> URLs from autolink plugin.
|
||||
const match = new RegExp(escapeRegex(urlBase)).exec(url);
|
||||
if (!match) {
|
||||
urlToMatch = urlBase + url;
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParse(urlToMatch).hostname === urlParse(urlBase).hostname) {
|
||||
const parsedDeepLink = parseDeepLink(urlToMatch);
|
||||
if (parsedDeepLink.type !== DeepLink.Invalid) {
|
||||
return parsedDeepLink;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getYouTubeVideoId(link: string) {
|
||||
// https://youtube.com/watch?v=<id>
|
||||
let match = (/youtube\.com\/watch\?\S*\bv=([a-zA-Z0-9_-]{6,11})/g).exec(link);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {Linking} from 'react-native';
|
||||
|
||||
import DeepLinkType from '@constants/deep_linking';
|
||||
import TestHelper from '@test/test_helper';
|
||||
import {matchDeepLink, parseDeepLink} from '@utils/deep_link';
|
||||
import * as UrlUtils from '@utils/url';
|
||||
|
||||
/* eslint-disable max-nested-callbacks */
|
||||
@@ -136,22 +137,22 @@ describe('UrlUtils', () => {
|
||||
{
|
||||
name: 'should return null if all inputs are empty',
|
||||
input: {url: '', serverURL: '', siteURL: ''},
|
||||
expected: null,
|
||||
expected: {type: 'invalid'},
|
||||
},
|
||||
{
|
||||
name: 'should return null if any of the input is null',
|
||||
input: {url: '', serverURL: '', siteURL: null},
|
||||
expected: null,
|
||||
expected: {type: 'invalid'},
|
||||
},
|
||||
{
|
||||
name: 'should return null if any of the input is null',
|
||||
input: {url: '', serverURL: null, siteURL: ''},
|
||||
expected: null,
|
||||
expected: {type: 'invalid'},
|
||||
},
|
||||
{
|
||||
name: 'should return null if any of the input is null',
|
||||
input: {url: null, serverURL: '', siteURL: ''},
|
||||
expected: null,
|
||||
expected: {type: 'invalid'},
|
||||
},
|
||||
{
|
||||
name: 'should return null for not supported link',
|
||||
@@ -160,12 +161,12 @@ describe('UrlUtils', () => {
|
||||
serverURL: SERVER_URL,
|
||||
siteURL: SITE_URL,
|
||||
},
|
||||
expected: null,
|
||||
expected: {type: 'invalid'},
|
||||
},
|
||||
{
|
||||
name: 'should return null despite url subset match',
|
||||
input: {url: 'http://myserver.com', serverURL: 'http://myserver.co'},
|
||||
expected: null,
|
||||
expected: {type: 'invalid'},
|
||||
},
|
||||
{
|
||||
name: 'should match despite no server URL in input link',
|
||||
@@ -253,7 +254,10 @@ describe('UrlUtils', () => {
|
||||
const {name, input, expected} = test;
|
||||
|
||||
it(name, () => {
|
||||
expect(UrlUtils.matchDeepLink(input.url!, input.serverURL!, input.siteURL!)).toEqual(expected);
|
||||
const match = matchDeepLink(input.url!, input.serverURL!, input.siteURL!);
|
||||
const parsed = parseDeepLink(match);
|
||||
Reflect.deleteProperty(parsed, 'url');
|
||||
expect(parsed).toEqual(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ export type DeepLinkType = typeof DeepLink[keyof typeof DeepLink];
|
||||
|
||||
export interface DeepLinkWithData {
|
||||
type: DeepLinkType;
|
||||
url: string;
|
||||
data?: DeepLinkChannel | DeepLinkDM | DeepLinkGM | DeepLinkPermalink | DeepLinkPlugin;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user