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