Handle go to location from CommandResponse (#4620)
* First draft to handle go to location on mobile * Fix lint * Fix test * Remove unnecessary change * Add not handled cases * Add i18n missing string * Fix typo * Extract handleGotoLocation into an action * Fix minor issues and extract showPermalinkView to an action * Fix minor issues and extract showPermalinkView to an action * Add missing change * Fix this reference * Remove unneeded event handlers, sort imports, early handle errors, make group channel visible, remove duplications and move functions to the right place * Fix tests * Handle error when opening permalink
This commit is contained in:
parent
8b8cd1ab4d
commit
7bb777f4b3
13 changed files with 153 additions and 29 deletions
|
|
@ -213,6 +213,8 @@ export function handleSelectChannel(channelId) {
|
|||
|
||||
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +260,7 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
|||
dispatch(handleSelectChannel(channel.id));
|
||||
}
|
||||
|
||||
return null;
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
|
|||
showModalOverCurrentContext(screen, passProps, options);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ import {DeepLinkTypes} from '@constants';
|
|||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import {getCurrentServerUrl} from '@init/credentials';
|
||||
import BottomSheet from '@utils/bottom_sheet';
|
||||
import {alertErrorWithFallback} from '@utils/general';
|
||||
import {t} from '@utils/i18n';
|
||||
import {errorBadChannel} from '@utils/draft';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {matchDeepLink, normalizeProtocol} from '@utils/url';
|
||||
|
||||
|
|
@ -59,7 +58,8 @@ export default class MarkdownLink extends PureComponent {
|
|||
|
||||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
|
||||
const {intl} = this.context;
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel(intl));
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
onPermalinkPress(match.postId, match.teamName);
|
||||
}
|
||||
|
|
@ -84,16 +84,6 @@ export default class MarkdownLink extends PureComponent {
|
|||
}
|
||||
});
|
||||
|
||||
errorBadChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
parseLinkLiteral = (literal) => {
|
||||
let nextLiteral = literal;
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export default class DraftInput extends PureComponent {
|
|||
getChannelTimezones: PropTypes.func.isRequired,
|
||||
handleClearFiles: PropTypes.func.isRequired,
|
||||
handleClearFailedFiles: PropTypes.func.isRequired,
|
||||
handleGotoLocation: PropTypes.func.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
isTimezoneEnabled: PropTypes.bool,
|
||||
maxMessageLength: PropTypes.number.isRequired,
|
||||
|
|
@ -299,7 +300,7 @@ export default class DraftInput extends PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const {error} = await executeCommand(msg, channelId, rootId);
|
||||
const {data, error} = await executeCommand(msg, channelId, rootId);
|
||||
this.setState({sendingMessage: false});
|
||||
|
||||
if (error) {
|
||||
|
|
@ -310,6 +311,10 @@ export default class DraftInput extends PureComponent {
|
|||
|
||||
this.setInputValue('');
|
||||
this.input.current.changeDraft('');
|
||||
|
||||
if (data.goto_location) {
|
||||
this.props.handleGotoLocation(data.goto_location, this.context.intl);
|
||||
}
|
||||
};
|
||||
|
||||
sendMessage = (value = '') => {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ describe('DraftInput', () => {
|
|||
},
|
||||
membersCount: 10,
|
||||
addRecentUsedEmojisInMessage: jest.fn(),
|
||||
handleGotoLocation: jest.fn(),
|
||||
};
|
||||
const ref = React.createRef();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {addReactionToLatestPost, addRecentUsedEmojisInMessage} from '@actions/vi
|
|||
import {handleClearFiles, handleClearFailedFiles} from '@actions/views/file_upload';
|
||||
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
|
||||
import {getChannelTimezones, getChannelMemberCountsByGroup} from '@mm-redux/actions/channels';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {createPost} from '@mm-redux/actions/posts';
|
||||
import {setStatus} from '@mm-redux/actions/users';
|
||||
import {General, Permissions} from '@mm-redux/constants';
|
||||
|
|
@ -99,6 +100,7 @@ const mapDispatchToProps = {
|
|||
getChannelTimezones,
|
||||
handleClearFiles,
|
||||
handleClearFailedFiles,
|
||||
handleGotoLocation,
|
||||
setStatus,
|
||||
getChannelMemberCountsByGroup,
|
||||
addRecentUsedEmojisInMessage,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {intlShape} from 'react-intl';
|
|||
import {Posts} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import * as PostListUtils from '@mm-redux/utils/post_list';
|
||||
import {errorBadChannel} from '@utils/draft';
|
||||
|
||||
import CombinedUserActivityPost from 'app/components/combined_user_activity_post';
|
||||
import Post from 'app/components/post';
|
||||
|
|
@ -17,8 +18,6 @@ import mattermostManaged from 'app/mattermost_managed';
|
|||
import {makeExtraData} from 'app/utils/list_view';
|
||||
import {matchDeepLink} from 'app/utils/url';
|
||||
import telemetry from 'app/telemetry';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import DateHeader from './date_header';
|
||||
import NewMessagesDivider from './new_messages_divider';
|
||||
|
|
@ -188,7 +187,8 @@ export default class PostList extends PureComponent {
|
|||
|
||||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.permalinkBadChannel);
|
||||
const {intl} = this.context;
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel(intl));
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
this.handlePermalinkPress(match.postId, match.teamName);
|
||||
}
|
||||
|
|
@ -277,16 +277,6 @@ export default class PostList extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
permalinkBadChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
renderItem = ({item, index}) => {
|
||||
const {
|
||||
testID,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ jest.useFakeTimers();
|
|||
jest.mock('react-intl');
|
||||
|
||||
describe('PostList', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const serverURL = 'https://server-url.fake';
|
||||
const deeplinkRoot = 'mattermost://server-url.fake';
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ describe('PostList', () => {
|
|||
test('setting channel deep link', () => {
|
||||
const wrapper = shallow(
|
||||
<PostList {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
wrapper.setProps({deepLinkURL: deepLinks.channel});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
export default {
|
||||
CHANNEL: 'channel',
|
||||
DMCHANNEL: 'dmchannel',
|
||||
GROUPCHANNEL: 'groupchannel',
|
||||
PERMALINK: 'permalink',
|
||||
OTHER: 'other',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@ import {Command, DialogSubmission, IncomingWebhook, OAuthApp, OutgoingWebhook} f
|
|||
|
||||
import {logError} from './errors';
|
||||
import {bindClientFunc, forceLogoutIfNecessary, requestSuccess, requestFailure} from './helpers';
|
||||
import {makeGroupMessageVisibleIfNecessary} from './preferences';
|
||||
import {handleSelectChannel, handleSelectChannelByName, loadChannelsByTeamName} from '@actions/views/channel';
|
||||
import {makeDirectChannel} from '@actions/views/more_dms';
|
||||
import {showPermalink} from '@actions/views/permalink';
|
||||
import {DeepLinkTypes} from '@constants';
|
||||
import {getUserByUsername} from '@mm-redux/actions/users';
|
||||
import {getConfig, getCurrentUrl} from '@mm-redux/selectors/entities/general';
|
||||
import * as DraftUtils from '@utils/draft';
|
||||
import {permalinkBadTeam} from '@utils/general';
|
||||
import {getURLAndMatch} from '@utils/url';
|
||||
|
||||
export function createIncomingHook(hook: IncomingWebhook): ActionFunc {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.createIncomingWebhook,
|
||||
|
|
@ -399,3 +410,52 @@ export function submitInteractiveDialog(submission: DialogSubmission): ActionFun
|
|||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleGotoLocation(href: string, intl: any): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
const config = getConfig(state);
|
||||
|
||||
const {url, match} = await getURLAndMatch(href, getCurrentUrl(state), config.SiteURL);
|
||||
|
||||
if (match) {
|
||||
switch (match.type) {
|
||||
case DeepLinkTypes.CHANNEL:
|
||||
dispatch(handleSelectChannelByName(match.channelName, match.teamName, () => DraftUtils.errorBadChannel(intl)));
|
||||
break;
|
||||
case DeepLinkTypes.PERMALINK: {
|
||||
const {error} = await dispatch(loadChannelsByTeamName(match.teamName, () => permalinkBadTeam(intl)));
|
||||
if (!error && match.postId) {
|
||||
dispatch(showPermalink(intl, match.teamName, match.postId));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DeepLinkTypes.DMCHANNEL: {
|
||||
if (!match.userName) {
|
||||
DraftUtils.errorBadUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
const {data} = await dispatch(getUserByUsername(match.userName));
|
||||
if (!data) {
|
||||
DraftUtils.errorBadUser(intl);
|
||||
return {data: false};
|
||||
}
|
||||
dispatch(makeDirectChannel(data.id));
|
||||
break;
|
||||
}
|
||||
case DeepLinkTypes.GROUPCHANNEL:
|
||||
if (!match.id) {
|
||||
DraftUtils.errorBadChannel(intl);
|
||||
return {data: false};
|
||||
}
|
||||
dispatch(makeGroupMessageVisibleIfNecessary(match.id));
|
||||
dispatch(handleSelectChannel(match.id));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
DraftUtils.tryOpenURL(url);
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert} from 'react-native';
|
||||
import {Alert, Linking} from 'react-native';
|
||||
|
||||
import {AT_MENTION_REGEX_GLOBAL, CODE_REGEX} from '@constants/autocomplete';
|
||||
import {NOTIFY_ALL_MEMBERS} from '@constants/view';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {alertErrorWithFallback} from '@utils/general';
|
||||
import {t} from '@utils/i18n';
|
||||
|
||||
export function alertAttachmentFail(formatMessage, accept, cancel) {
|
||||
Alert.alert(
|
||||
|
|
@ -79,6 +81,44 @@ export function alertSendToGroups(formatMessage, notifyAllMessage, accept, cance
|
|||
);
|
||||
}
|
||||
|
||||
export function tryOpenURL(url) {
|
||||
Linking.canOpenURL(url).then((supported) => {
|
||||
if (supported) {
|
||||
Linking.openURL(url);
|
||||
} else {
|
||||
const {formatMessage} = this.context.intl;
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.title',
|
||||
defaultMessage: 'Link Error',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.server_link.error.text',
|
||||
defaultMessage: 'The link could not be found on this server.',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function errorBadChannel(intl) {
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
}
|
||||
|
||||
export function errorBadUser(intl) {
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_user.error'),
|
||||
defaultMessage: 'This link belongs to a deleted user.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
}
|
||||
|
||||
export function alertSlashCommandFailed(formatMessage, error) {
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {latinise} from './latinise.js';
|
|||
import {escapeRegex} from './markdown';
|
||||
|
||||
import {Files} from '@mm-redux/constants';
|
||||
import {getCurrentServerUrl} from '@init/credentials';
|
||||
|
||||
import {DeepLinkTypes} from 'app/constants';
|
||||
|
||||
|
|
@ -128,6 +129,16 @@ export function matchDeepLink(url, serverURL, siteURL) {
|
|||
return {type: DeepLinkTypes.PERMALINK, teamName: match[1], postId: match[2]};
|
||||
}
|
||||
|
||||
match = new RegExp(linkRoot + '\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(urlToMatch);
|
||||
if (match) {
|
||||
return {type: DeepLinkTypes.DMCHANNEL, teamName: match[1], userName: match[2]};
|
||||
}
|
||||
|
||||
match = new RegExp(linkRoot + '\\/([^\\/]+)\\/messages\\/(\\S+)').exec(urlToMatch);
|
||||
if (match) {
|
||||
return {type: DeepLinkTypes.GROUPCHANNEL, teamName: match[1], id: match[2]};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -152,3 +163,20 @@ export function getYouTubeVideoId(link) {
|
|||
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function getURLAndMatch(href, serverURL, siteURL) {
|
||||
const url = normalizeProtocol(href);
|
||||
|
||||
if (!url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let serverUrl = serverURL;
|
||||
if (!serverUrl) {
|
||||
serverUrl = await getCurrentServerUrl();
|
||||
}
|
||||
|
||||
const match = matchDeepLink(url, serverURL, siteURL);
|
||||
|
||||
return {url, match};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -467,6 +467,7 @@
|
|||
"mobile.server_link.error.title": "Link Error",
|
||||
"mobile.server_link.unreachable_channel.error": "This link belongs to a deleted channel or to a channel to which you do not have access.",
|
||||
"mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.",
|
||||
"mobile.server_link.unreachable_user.error": "This link belongs to a deleted user.",
|
||||
"mobile.server_ssl.error.text": "The certificate from {host} is not trusted.\n\nPlease contact your System Administrator to resolve the certificate issues and allow connections to this server.",
|
||||
"mobile.server_ssl.error.title": "Untrusted Certificate",
|
||||
"mobile.server_upgrade.alert_description": "This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue