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 { ...@@ -472,11 +472,11 @@ export default class AdminConsole extends React.Component {
extraProps={extraProps} extraProps={extraProps}
/> />
<SCRoute <SCRoute
path={`${props.match.url}/link_previews`} path={`${props.match.url}/posts`}
component={SchemaAdminSettings} component={SchemaAdminSettings}
extraProps={{ extraProps={{
...extraProps, ...extraProps,
schema: AdminDefinition.settings.customization.link_previews.schema, schema: AdminDefinition.settings.customization.posts.schema,
}} }}
/> />
<SCRoute <SCRoute
......
...@@ -9,13 +9,14 @@ import {ldapTest, invalidateAllCaches, reloadConfig, testS3Connection} from 'act ...@@ -9,13 +9,14 @@ import {ldapTest, invalidateAllCaches, reloadConfig, testS3Connection} from 'act
import SystemAnalytics from 'components/analytics/system_analytics'; import SystemAnalytics from 'components/analytics/system_analytics';
import TeamAnalytics from 'components/analytics/team_analytics'; import TeamAnalytics from 'components/analytics/team_analytics';
import SystemUsers from './system_users';
import ServerLogs from './server_logs';
import Audits from './audits'; import Audits from './audits';
import CustomUrlSchemesSetting from './custom_url_schemes_setting.jsx';
import LicenseSettings from './license_settings'; import LicenseSettings from './license_settings';
import PermissionSchemesSettings from './permission_schemes_settings'; import PermissionSchemesSettings from './permission_schemes_settings';
import PermissionSystemSchemeSettings from './permission_schemes_settings/permission_system_scheme_settings'; import PermissionSystemSchemeSettings from './permission_schemes_settings/permission_system_scheme_settings';
import PermissionTeamSchemeSettings from './permission_schemes_settings/permission_team_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'; import * as DefinitionConstants from './admin_definition_constants';
...@@ -1489,11 +1490,11 @@ export default { ...@@ -1489,11 +1490,11 @@ export default {
], ],
}, },
}, },
link_previews: { posts: {
schema: { schema: {
id: 'LinkPreviewsSettings', id: 'PostSettings',
name: 'admin.customization.linkPreviews', name: 'admin.customization.posts',
name_default: 'Link Previews', name_default: 'Posts',
settings: [ settings: [
{ {
type: Constants.SettingsTypes.TYPE_BOOL, type: Constants.SettingsTypes.TYPE_BOOL,
...@@ -1503,6 +1504,11 @@ export default { ...@@ -1503,6 +1504,11 @@ export default {
help_text: 'admin.customization.enableLinkPreviewsDesc', 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.', 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 { ...@@ -732,11 +732,11 @@ export default class AdminSidebar extends React.Component {
} }
/> />
<AdminSidebarSection <AdminSidebarSection
name='link_previews' name='posts'
title={ title={
<FormattedMessage <FormattedMessage
id='admin.sidebar.linkPreviews' id='admin.sidebar.posts'
defaultMessage='Link Previews' 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 { ...@@ -461,6 +461,11 @@ export default class SchemaAdminSettings extends AdminSettings {
return ( return (
<CustomComponent <CustomComponent
key={this.props.schema.id + '_userautocomplete_' + setting.key} 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'; ...@@ -15,13 +15,9 @@ import * as GlobalActions from 'actions/global_actions.jsx';
import * as WebrtcActions from 'actions/webrtc_actions.jsx'; import * as WebrtcActions from 'actions/webrtc_actions.jsx';
import WebrtcStore from 'stores/webrtc_store.jsx'; import WebrtcStore from 'stores/webrtc_store.jsx';
import TeamStore from 'stores/team_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 {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 * as Utils from 'utils/utils.jsx';
import ChannelInfoModal from 'components/channel_info_modal'; import ChannelInfoModal from 'components/channel_info_modal';
import ChannelInviteModal from 'components/channel_invite_modal'; import ChannelInviteModal from 'components/channel_invite_modal';
...@@ -48,6 +44,8 @@ import Pluggable from 'plugins/pluggable'; ...@@ -48,6 +44,8 @@ import Pluggable from 'plugins/pluggable';
import HeaderIconWrapper from './components/header_icon_wrapper'; import HeaderIconWrapper from './components/header_icon_wrapper';
const headerMarkdownOptions = {singleline: true, mentionHighlight: false, atMentions: true};
const SEARCH_BAR_MINIMUM_WINDOW_SIZE = 1140; const SEARCH_BAR_MINIMUM_WINDOW_SIZE = 1140;
export default class ChannelHeader extends React.Component { export default class ChannelHeader extends React.Component {
...@@ -73,7 +71,6 @@ export default class ChannelHeader extends React.Component { ...@@ -73,7 +71,6 @@ export default class ChannelHeader extends React.Component {
dmUser: PropTypes.object, dmUser: PropTypes.object,
dmUserStatus: PropTypes.object, dmUserStatus: PropTypes.object,
dmUserIsInCall: PropTypes.bool, dmUserIsInCall: PropTypes.bool,
enableFormatting: PropTypes.bool.isRequired,
isReadOnly: PropTypes.bool, isReadOnly: PropTypes.bool,
rhsState: PropTypes.oneOf( rhsState: PropTypes.oneOf(
Object.values(RHSStates) Object.values(RHSStates)
...@@ -308,7 +305,6 @@ export default class ChannelHeader extends React.Component { ...@@ -308,7 +305,6 @@ export default class ChannelHeader extends React.Component {
const channel = this.props.channel; const channel = this.props.channel;
const textFormattingOptions = {singleline: true, mentionHighlight: false, siteURL: getSiteURL(), channelNamesMap: ChannelStore.getChannelNamesMap(), team: TeamStore.getCurrent(), atMentions: true};
const popoverContent = ( const popoverContent = (
<Popover <Popover
id='header-popover' id='header-popover'
...@@ -319,9 +315,9 @@ export default class ChannelHeader extends React.Component { ...@@ -319,9 +315,9 @@ export default class ChannelHeader extends React.Component {
onMouseOver={this.handleOnMouseOver} onMouseOver={this.handleOnMouseOver}
onMouseOut={this.handleOnMouseOut} onMouseOut={this.handleOnMouseOut}
> >
<MessageWrapper <Markdown
message={channel.header} message={channel.header}
options={textFormattingOptions} options={headerMarkdownOptions}
/> />
</Popover> </Popover>
); );
...@@ -840,10 +836,14 @@ export default class ChannelHeader extends React.Component { ...@@ -840,10 +836,14 @@ export default class ChannelHeader extends React.Component {
let headerTextContainer; let headerTextContainer;
if (channel.header) { if (channel.header) {
let headerTextElement; headerTextContainer = (
const formattedText = TextFormatting.formatText(channel.header, textFormattingOptions); <OverlayTrigger
if (this.props.enableFormatting) { trigger={'click'}
headerTextElement = ( placement='bottom'
rootClose={true}
overlay={popoverContent}
ref='headerOverlay'
>
<div <div
id='channelHeaderDescription' id='channelHeaderDescription'
className='channel-header__description' className='channel-header__description'
...@@ -851,33 +851,12 @@ export default class ChannelHeader extends React.Component { ...@@ -851,33 +851,12 @@ export default class ChannelHeader extends React.Component {
{dmHeaderIconStatus} {dmHeaderIconStatus}
{dmHeaderTextStatus} {dmHeaderTextStatus}
<span onClick={Utils.handleFormattedTextClick}> <span onClick={Utils.handleFormattedTextClick}>
{messageHtmlToComponent(formattedText, false, {mentions: false})} <Markdown
message={channel.header}
options={headerMarkdownOptions}
/>
</span> </span>
</div> </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> </OverlayTrigger>
); );
} else { } else {
......
...@@ -5,9 +5,8 @@ import {connect} from 'react-redux'; ...@@ -5,9 +5,8 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux'; import {bindActionCreators} from 'redux';
import {favoriteChannel, leaveChannel, unfavoriteChannel, updateChannelNotifyProps} from 'mattermost-redux/actions/channels'; import {favoriteChannel, leaveChannel, unfavoriteChannel, updateChannelNotifyProps} from 'mattermost-redux/actions/channels';
import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis'; 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 {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 {getMyTeamMember} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users'; import {getCurrentUser, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {getUserIdFromChannelName, isDefault, isFavoriteChannel} from 'mattermost-redux/utils/channel_utils'; import {getUserIdFromChannelName, isDefault, isFavoriteChannel} from 'mattermost-redux/utils/channel_utils';
...@@ -52,7 +51,6 @@ function mapStateToProps(state, ownProps) { ...@@ -52,7 +51,6 @@ function mapStateToProps(state, ownProps) {
currentUser: user, currentUser: user,
dmUser, dmUser,
dmUserStatus, dmUserStatus,
enableFormatting: getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', true),
rhsState: getRhsState(state), rhsState: getRhsState(state),
isLicensed: license.IsLicensed === 'true', isLicensed: license.IsLicensed === 'true',
enableWebrtc: config.EnableWebrtc === 'true', enableWebrtc: config.EnableWebrtc === 'true',
......
...@@ -6,13 +6,16 @@ import React from 'react'; ...@@ -6,13 +6,16 @@ import React from 'react';
import {Modal} from 'react-bootstrap'; import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import Markdown from 'components/markdown';
import GlobeIcon from 'components/svg/globe_icon'; import GlobeIcon from 'components/svg/globe_icon';
import LockIcon from 'components/svg/lock_icon'; import LockIcon from 'components/svg/lock_icon';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import {getSiteURL} from 'utils/url.jsx'; import {getSiteURL} from 'utils/url.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
const headerMarkdownOptions = {singleline: false, mentionHighlight: false};
export default class ChannelInfoModal extends React.PureComponent { export default class ChannelInfoModal extends React.PureComponent {
static propTypes = { static propTypes = {
...@@ -109,10 +112,12 @@ export default class ChannelInfoModal extends React.PureComponent { ...@@ -109,10 +112,12 @@ export default class ChannelInfoModal extends React.PureComponent {
defaultMessage='Header:' defaultMessage='Header:'
/> />
</div> </div>
<div <div className='info__value'>
className='info__value' <Markdown
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: false, mentionHighlight: false})}} message={channel.header}
/> options={headerMarkdownOptions}
/>
</div>
</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) {