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:
Daniel Espino García 2021-01-04 23:06:37 +01:00 committed by GitHub
parent 8b8cd1ab4d
commit 7bb777f4b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 153 additions and 29 deletions

View file

@ -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};
};
}

View file

@ -40,6 +40,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
showModalOverCurrentContext(screen, passProps, options);
}
}
return {};
};
}

View file

@ -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;

View file

@ -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 = '') => {

View file

@ -80,6 +80,7 @@ describe('DraftInput', () => {
},
membersCount: 10,
addRecentUsedEmojisInMessage: jest.fn(),
handleGotoLocation: jest.fn(),
};
const ref = React.createRef();

View file

@ -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,

View file

@ -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,

View file

@ -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});

View file

@ -3,6 +3,8 @@
export default {
CHANNEL: 'channel',
DMCHANNEL: 'dmchannel',
GROUPCHANNEL: 'groupchannel',
PERMALINK: 'permalink',
OTHER: 'other',
};

View file

@ -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};
};
}

View file

@ -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({

View file

@ -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};
}

View file

@ -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.",