Unverified Commit 63ec20a8 authored by Harrison Healey's avatar Harrison Healey Committed by GitHub
Browse files

MM-9547 Added CustomUrlSchemes setting (#1270)

* MM-9547 Added CustomUrlSchemes setting

* MM-9547 Added system console option for CustomUrlSchemes

* Added updated mattermost-redux and marked

* Fixed unit tests

* Added additional unit tests

* Moved default url schemes into mattermost-redux

* Updated help text

* Switched mattermost-redux back to master
parent 144d980d
......@@ -472,11 +472,11 @@ export default class AdminConsole extends React.Component {
extraProps={extraProps}
/>
<SCRoute
path={`${props.match.url}/link_previews`}
path={`${props.match.url}/posts`}
component={SchemaAdminSettings}
extraProps={{
...extraProps,
schema: AdminDefinition.settings.customization.link_previews.schema,
schema: AdminDefinition.settings.customization.posts.schema,
}}
/>
<SCRoute
......
......@@ -9,13 +9,14 @@ import {ldapTest, invalidateAllCaches, reloadConfig, testS3Connection} from 'act
import SystemAnalytics from 'components/analytics/system_analytics';
import TeamAnalytics from 'components/analytics/team_analytics';
import SystemUsers from './system_users';
import ServerLogs from './server_logs';
import Audits from './audits';
import CustomUrlSchemesSetting from './custom_url_schemes_setting.jsx';
import LicenseSettings from './license_settings';
import PermissionSchemesSettings from './permission_schemes_settings';
import PermissionSystemSchemeSettings from './permission_schemes_settings/permission_system_scheme_settings';
import PermissionTeamSchemeSettings from './permission_schemes_settings/permission_team_scheme_settings';
import SystemUsers from './system_users';
import ServerLogs from './server_logs';
import * as DefinitionConstants from './admin_definition_constants';
......@@ -1489,11 +1490,11 @@ export default {
],
},
},
link_previews: {
posts: {
schema: {
id: 'LinkPreviewsSettings',
name: 'admin.customization.linkPreviews',
name_default: 'Link Previews',
id: 'PostSettings',
name: 'admin.customization.posts',
name_default: 'Posts',
settings: [
{
type: Constants.SettingsTypes.TYPE_BOOL,
......@@ -1503,6 +1504,11 @@ export default {
help_text: 'admin.customization.enableLinkPreviewsDesc',
help_text_default: 'Display a preview of website content below messages, when available. Users can disable these previews from Account Settings > Display > Website Link Previews.',
},
{
type: Constants.SettingsTypes.TYPE_CUSTOM,
component: CustomUrlSchemesSetting,
key: 'DisplaySettings.CustomUrlSchemes',
},
],
},
},
......
......@@ -732,11 +732,11 @@ export default class AdminSidebar extends React.Component {
}
/>
<AdminSidebarSection
name='link_previews'
name='posts'
title={
<FormattedMessage
id='admin.sidebar.linkPreviews'
defaultMessage='Link Previews'
id='admin.sidebar.posts'
defaultMessage='Posts'
/>
}
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import * as Utils from 'utils/utils';
import Setting from './setting';
export default class CustomUrlSchemesSetting extends React.Component {
static get propTypes() {
return {
id: PropTypes.string.isRequired,
value: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
setByEnv: PropTypes.bool.isRequired,
};
}
constructor(props) {
super(props);
this.state = {
value: this.arrayToString(props.value),
};
}
stringToArray = (str) => {
return str.split(',').map((s) => s.trim()).filter(Boolean);
};
arrayToString = (arr) => {
return arr.join(',');
};
handleChange = (e) => {
const valueAsArray = this.stringToArray(e.target.value);
this.props.onChange(this.props.id, valueAsArray);
this.setState({
value: e.target.value,
});
};
render() {
const label = Utils.localizeMessage('admin.customization.customUrlSchemes', 'Custom URL Schemes:');
const helpText = Utils.localizeMessage(
'admin.customization.customUrlSchemesDesc',
'Allows message text to link if it begins with any of the comma-separated URL schemes listed. By default, the following schemes will create links: "http", "https", "ftp", "tel", and "mailto".'
);
const placeholder = Utils.localizeMessage('admin.customization.customUrlSchemesPlaceholder', 'E.g.: "git,smtp"');
return (
<Setting
label={label}
helpText={helpText}
inputId={this.props.id}
setByEnv={this.props.setByEnv}
>
<input
id={this.props.id}
className='form-control'
type='text'
placeholder={placeholder}
value={this.state.value}
onChange={this.handleChange}
disabled={this.props.disabled || this.props.setByEnv}
/>
</Setting>
);
}
}
......@@ -461,6 +461,11 @@ export default class SchemaAdminSettings extends AdminSettings {
return (
<CustomComponent
key={this.props.schema.id + '_userautocomplete_' + setting.key}
id={setting.key}
value={this.state[setting.key] || ''}
disabled={this.isDisabled(setting)}
setByEnv={this.isSetByEnv(setting.key)}
onChange={this.handleChange}
/>
);
}
......
......@@ -15,13 +15,9 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import * as WebrtcActions from 'actions/webrtc_actions.jsx';
import WebrtcStore from 'stores/webrtc_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import MessageWrapper from 'components/message_wrapper.jsx';
import Markdown from 'components/markdown';
import {Constants, NotificationLevels, RHSStates, UserStatuses, ModalIdentifiers} from 'utils/constants.jsx';
import messageHtmlToComponent from 'utils/message_html_to_component';
import * as TextFormatting from 'utils/text_formatting.jsx';
import {getSiteURL} from 'utils/url.jsx';
import * as Utils from 'utils/utils.jsx';
import ChannelInfoModal from 'components/channel_info_modal';
import ChannelInviteModal from 'components/channel_invite_modal';
......@@ -48,6 +44,8 @@ import Pluggable from 'plugins/pluggable';
import HeaderIconWrapper from './components/header_icon_wrapper';
const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true};
const SEARCH_BAR_MINIMUM_WINDOW_SIZE = 1140;
export default class ChannelHeader extends React.Component {
......@@ -73,7 +71,6 @@ export default class ChannelHeader extends React.Component {
dmUser: PropTypes.object,
dmUserStatus: PropTypes.object,
dmUserIsInCall: PropTypes.bool,
enableFormatting: PropTypes.bool.isRequired,
isReadOnly: PropTypes.bool,
rhsState: PropTypes.oneOf(
Object.values(RHSStates)
......@@ -308,7 +305,6 @@ export default class ChannelHeader extends React.Component {
const channel = this.props.channel;
const textFormattingOptions = {singleline: true, mentionHighlight: false, siteURL: getSiteURL(), channelNamesMap: ChannelStore.getChannelNamesMap(), team: TeamStore.getCurrent(), atMentions: true};
const popoverContent = (
<Popover
id='header-popover'
......@@ -319,9 +315,9 @@ export default class ChannelHeader extends React.Component {
onMouseOver={this.handleOnMouseOver}
onMouseOut={this.handleOnMouseOut}
>
<MessageWrapper
<Markdown
message={channel.header}
options={textFormattingOptions}
options={headerMarkdownOptions}
/>
</Popover>
);
......@@ -840,10 +836,14 @@ export default class ChannelHeader extends React.Component {
let headerTextContainer;
if (channel.header) {
let headerTextElement;
const formattedText = TextFormatting.formatText(channel.header, textFormattingOptions);
if (this.props.enableFormatting) {
headerTextElement = (
headerTextContainer = (
<OverlayTrigger
trigger={'click'}
placement='bottom'
rootClose={true}
overlay={popoverContent}
ref='headerOverlay'
>
<div
id='channelHeaderDescription'
className='channel-header__description'
......@@ -851,33 +851,12 @@ export default class ChannelHeader extends React.Component {
{dmHeaderIconStatus}
{dmHeaderTextStatus}
<span onClick={Utils.handleFormattedTextClick}>
{messageHtmlToComponent(formattedText, false, {mentions: false})}
<Markdown
message={channel.header}
options={headerMarkdownOptions}
/>
</span>
</div>
);
} else {
headerTextElement = (
<div
id='channelHeaderDescription'
onClick={Utils.handleFormattedTextClick}
className='channel-header__description light'
>
{dmHeaderIconStatus}
{dmHeaderTextStatus}
{channel.header}
</div>
);
}
headerTextContainer = (
<OverlayTrigger
trigger={'click'}
placement='bottom'
rootClose={true}
overlay={popoverContent}
ref='headerOverlay'
>
{headerTextElement}
</OverlayTrigger>
);
} else {
......
......@@ -5,9 +5,8 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {favoriteChannel, leaveChannel, unfavoriteChannel, updateChannelNotifyProps} from 'mattermost-redux/actions/channels';
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
import {General, Preferences} from 'mattermost-redux/constants';
import {General} from 'mattermost-redux/constants';
import {getChannel, getMyChannelMember, isCurrentChannelReadOnly} from 'mattermost-redux/selectors/entities/channels';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {getUserIdFromChannelName, isDefault, isFavoriteChannel} from 'mattermost-redux/utils/channel_utils';
......@@ -52,7 +51,6 @@ function mapStateToProps(state, ownProps) {
currentUser: user,
dmUser,
dmUserStatus,
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
rhsState: getRhsState(state),
isLicensed: license.IsLicensed === 'true',
enableWebrtc: config.EnableWebrtc === 'true',
......
......@@ -6,13 +6,16 @@ import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import Markdown from 'components/markdown';
import GlobeIcon from 'components/svg/globe_icon';
import LockIcon from 'components/svg/lock_icon';
import Constants from 'utils/constants.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import {getSiteURL} from 'utils/url.jsx';
import * as Utils from 'utils/utils.jsx';
const headerMarkdownOptions = {singleline: false, mentionHighlight: false};
export default class ChannelInfoModal extends React.PureComponent {
static propTypes = {
......@@ -109,10 +112,12 @@ export default class ChannelInfoModal extends React.PureComponent {
defaultMessage='Header:'
/>
</div>
<div
className='info__value'
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: false, mentionHighlight: false})}}
/>
<div className='info__value'>
<Markdown
message={channel.header}
options={headerMarkdownOptions}
/>
</div>
</div>
);
}
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {Preferences} from 'mattermost-redux/constants';
import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {getAutolinkedUrlSchemes, getConfig} from 'mattermost-redux/selectors/entities/general';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
import {getSiteURL} from 'utils/url.jsx';
import Markdown from './markdown';
function makeGetChannelNamesMap() {
return createSelector(
getChannelsNameMapInCurrentTeam,
(state, props) => props && props.channel_mentions,
(channelNamesMap, channelMentions) => {
if (channelMentions) {
return Object.assign({}, channelMentions, channelNamesMap);
}
return channelNamesMap;
}
);
}
function makeMapStateToProps() {
const getChannelNamesMap = makeGetChannelNamesMap();
return function mapStateToProps(state, ownProps) {
const config = getConfig(state);
return {
autolinkedUrlSchemes: getAutolinkedUrlSchemes(state),
channelNamesMap: getChannelNamesMap(state, ownProps),
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
mentionKeys: getCurrentUserMentionKeys(state),
siteURL: getSiteURL(),
team: getCurrentTeam(state),
hasImageProxy: config.HasImageProxy === 'true',
};
};
}
export default connect(makeMapStateToProps)(Markdown);
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import messageHtmlToComponent from 'utils/message_html_to_component';
import * as TextFormatting from 'utils/text_formatting.jsx';
export default class Markdown extends React.PureComponent {
static propTypes = {
/*
* An object mapping channel names to channels for the current team
*/
channelNamesMap: PropTypes.object.isRequired,
/*
* An array of URL schemes that should be turned into links. Anything that looks
* like a link will be turned into a link if this is not provided.
*/
autolinkedUrlSchemes: PropTypes.array,
/*
* Whether or not to do Markdown rendering
*/
enableFormatting: PropTypes.bool.isRequired,
/*
* Whether or not this text is part of the RHS
*/
isRHS: PropTypes.bool,
/*
* An array of words that can be used to mention a user
*/
mentionKeys: PropTypes.arrayOf(PropTypes.object).isRequired,
/*
* The text to be rendered
*/
message: PropTypes.string.isRequired,
/*
* Any additional text formatting options to be used
*/
options: PropTypes.object,
/*
* The root Site URL for the page
*/
siteURL: PropTypes.string.isRequired,
/*
* The current team
*/
team: PropTypes.object.isRequired,
/**
* If an image proxy is enabled.
*/
hasImageProxy: PropTypes.bool.isRequired,
/**
* Whether or not to proxy image URLs
*/
proxyImages: PropTypes.bool,
};
static defaultProps = {
options: {},
isRHS: false,
proxyImages: true,
};
render() {
if (!this.props.enableFormatting) {
return <span>{this.props.message}</span>;
}
const options = Object.assign({
autolinkedUrlSchemes: this.props.autolinkedUrlSchemes,
siteURL: this.props.siteURL,
mentionKeys: this.props.mentionKeys,
atMentions: true,
channelNamesMap: this.props.channelNamesMap,
proxyImages: this.props.hasImageProxy && this.props.proxyImages,
team: this.props.team,
}, this.props.options);
const htmlFormattedText = TextFormatting.formatText(this.props.message, options);
return messageHtmlToComponent(htmlFormattedText, this.props.isRHS);
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import messageHtmlToComponent from 'utils/message_html_to_component';
import * as TextFormatting from 'utils/text_formatting.jsx';
import {getSiteURL} from 'utils/url.jsx';
import * as Utils from 'utils/utils.jsx';
export default class MessageWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
if (this.props.message) {
const options = Object.assign({}, this.props.options, {
siteURL: getSiteURL(),
});
const formattedText = TextFormatting.formatText(this.props.message, options);
return (
<div onClick={Utils.handleFormattedTextClick}>
{messageHtmlToComponent(formattedText, false, {mentions: false})}
</div>
);
}
return <div/>;
}
}
MessageWrapper.defaultProps = {
message: '',
};
MessageWrapper.propTypes = {
message: PropTypes.string,
options: PropTypes.object,
};
......@@ -6,9 +6,11 @@ import React from 'react';
import {OverlayTrigger, Popover} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import MessageWrapper from 'components/message_wrapper.jsx';
import Markdown from 'components/markdown';
import InfoIcon from 'components/svg/info_icon';
const headerMarkdownOptions = {singleline: true, mentionHighlight: false};
export default class NavbarInfoButton extends React.PureComponent {
static propTypes = {
channel: PropTypes.object,
......@@ -31,9 +33,9 @@ export default class NavbarInfoButton extends React.PureComponent {
if (this.props.channel) {
if (this.props.channel.header) {
popoverContent = (
<MessageWrapper
<Markdown
message={this.props.channel.header}
options={{singleline: true, mentionHighlight: false}}
options={headerMarkdownOptions}
/>
);
} else {
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
import {getSiteURL} from 'utils/url.jsx';
import PostMarkdown from './post_markdown';
const getChannelNamesMap = createSelector(
getChannelsNameMapInCurrentTeam,
(state, props) => props,
(channelNamesMap, props) => {
if (props && props.channel_mentions) {
return Object.assign({}, props.channel_mentions, channelNamesMap);
}
return channelNamesMap;
}
);
function mapStateToProps(state) {
const config = getConfig(state);
return {
channelNamesMap: getChannelNamesMap(state),
mentionKeys: getCurrentUserMentionKeys(state),
siteURL: getSiteURL(),
team: getCurrentTeam(state),
hasImageProxy: config.HasImageProxy === 'true',
};
}
export default connect(mapStateToProps)(PostMarkdown);
export default PostMarkdown;
......@@ -4,85 +4,50 @@
import PropTypes from 'prop-types';
import React from 'react';
import messageHtmlToComponent from 'utils/message_html_to_component';
import * as TextFormatting from 'utils/text_formatting.jsx';
import Markdown from 'components/markdown';
import {renderSystemMessage} from './system_message_helpers.jsx';
export default class PostMarkdown extends React.PureComponent {