diff options
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/admin_console/localization_settings.jsx | 2 | ||||
-rw-r--r-- | webapp/components/change_url_modal.jsx | 21 | ||||
-rw-r--r-- | webapp/components/new_channel_flow.jsx | 15 | ||||
-rw-r--r-- | webapp/components/new_channel_modal.jsx | 4 | ||||
-rw-r--r-- | webapp/components/suggestion/at_mention_provider.jsx | 110 | ||||
-rw-r--r-- | webapp/components/suggestion/suggestion_list.jsx | 21 | ||||
-rw-r--r-- | webapp/components/user_profile.jsx | 20 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_general.jsx | 2 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_security.jsx | 4 |
9 files changed, 141 insertions, 58 deletions
diff --git a/webapp/components/admin_console/localization_settings.jsx b/webapp/components/admin_console/localization_settings.jsx index a9a0a8044..b5bc28b52 100644 --- a/webapp/components/admin_console/localization_settings.jsx +++ b/webapp/components/admin_console/localization_settings.jsx @@ -114,7 +114,7 @@ export default class LocalizationSettings extends AdminSettings { helpText={ <FormattedHTMLMessage id='admin.general.localization.availableLocalesDescription' - defaultMessage='Set which languages are available for users in Account Settings (leave this field blank to have all supported languages available).<br /><br />Would like to help with translations? Join the <a href="http://translate.mattermost.com/" target="_blank">Mattermost Translation Server</a> to contribute.' + defaultMessage='Set which languages are available for users in Account Settings (leave this field blank to have all supported languages available). If you’re manually adding new languages, the <strong>Default Client Language</strong> must be added before saving this setting.<br /><br />Would like to help with translations? Join the <a href="http://translate.mattermost.com/" target="_blank">Mattermost Translation Server</a> to contribute.' /> } noResultText={ diff --git a/webapp/components/change_url_modal.jsx b/webapp/components/change_url_modal.jsx index 2219ff317..fa115cf36 100644 --- a/webapp/components/change_url_modal.jsx +++ b/webapp/components/change_url_modal.jsx @@ -119,16 +119,20 @@ export default class ChangeUrlModal extends React.Component { } render() { let urlClass = 'input-group input-group--limit'; - let urlError = null; - let serverError = null; + let error = null; if (this.state.urlError) { urlClass += ' has-error'; - urlError = (<p className='input__help error'>{this.state.urlError}</p>); } - if (this.props.serverError) { - serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>; + if (this.props.serverError || this.state.urlError) { + error = ( + <div className='form-group has-error'> + <p className='input__help error'> + {this.state.urlError || this.props.serverError} + </p> + </div> + ); } const fullTeamUrl = TeamStore.getCurrentTeamUrl(); @@ -173,8 +177,7 @@ export default class ChangeUrlModal extends React.Component { tabIndex='1' /> </div> - {urlError} - {serverError} + {error} </div> </div> </Modal.Body> @@ -211,7 +214,7 @@ ChangeUrlModal.defaultProps = { urlLabel: 'URL', submitButtonText: 'Save', currentURL: '', - serverError: '' + serverError: null }; ChangeUrlModal.propTypes = { @@ -221,7 +224,7 @@ ChangeUrlModal.propTypes = { urlLabel: React.PropTypes.string, submitButtonText: React.PropTypes.string, currentURL: React.PropTypes.string, - serverError: React.PropTypes.string, + serverError: React.PropTypes.node, onModalSubmit: React.PropTypes.func.isRequired, onModalDismissed: React.PropTypes.func.isRequired }; diff --git a/webapp/components/new_channel_flow.jsx b/webapp/components/new_channel_flow.jsx index abec799b5..c6c265725 100644 --- a/webapp/components/new_channel_flow.jsx +++ b/webapp/components/new_channel_flow.jsx @@ -9,7 +9,7 @@ import UserStore from 'stores/user_store.jsx'; import NewChannelModal from './new_channel_modal.jsx'; import ChangeURLModal from './change_url_modal.jsx'; -import {intlShape, injectIntl, defineMessages} from 'react-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; import {browserHistory} from 'react-router/es6'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; @@ -124,7 +124,16 @@ class NewChannelFlow extends React.Component { }, (err) => { if (err.id === 'model.channel.is_valid.2_or_more.app_error') { - this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE}); + this.setState({ + flowState: SHOW_EDIT_URL_THEN_COMPLETE, + serverError: ( + <FormattedMessage + id='channel_flow.handleTooShort' + defaultMessage='Channel URL must be 2 or more lowercase alphanumeric characters' + /> + ) + }); + return; } if (err.id === 'store.sql_channel.update.exists.app_error') { this.setState({serverError: Utils.localizeMessage('channel_flow.alreadyExist', 'A channel with that URL already exists')}); @@ -148,7 +157,7 @@ class NewChannelFlow extends React.Component { if (this.state.flowState === SHOW_EDIT_URL_THEN_COMPLETE) { this.setState({channelName: newURL, nameModified: true}, this.doSubmit); } else { - this.setState({flowState: SHOW_NEW_CHANNEL, serverError: '', channelName: newURL, nameModified: true}); + this.setState({flowState: SHOW_NEW_CHANNEL, serverError: null, channelName: newURL, nameModified: true}); } } urlChangeDismissed() { diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx index e174ddd32..31ed15306 100644 --- a/webapp/components/new_channel_modal.jsx +++ b/webapp/components/new_channel_modal.jsx @@ -372,14 +372,14 @@ class NewChannelModal extends React.Component { NewChannelModal.defaultProps = { show: false, channelType: 'O', - serverError: '' + serverError: null }; NewChannelModal.propTypes = { intl: intlShape.isRequired, show: React.PropTypes.bool.isRequired, channelType: React.PropTypes.string.isRequired, channelData: React.PropTypes.object.isRequired, - serverError: React.PropTypes.string, + serverError: React.PropTypes.node, onSubmitChannel: React.PropTypes.func.isRequired, onModalDismissed: React.PropTypes.func.isRequired, onTypeSwitched: React.PropTypes.func.isRequired, diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx index b9127e8d3..87cdc6894 100644 --- a/webapp/components/suggestion/at_mention_provider.jsx +++ b/webapp/components/suggestion/at_mention_provider.jsx @@ -8,6 +8,7 @@ import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as Utils from 'utils/utils.jsx'; import Client from 'client/web_client.jsx'; +import Constants from 'utils/constants.jsx'; import {FormattedMessage} from 'react-intl'; import Suggestion from './suggestion.jsx'; @@ -98,60 +99,91 @@ class AtMentionSuggestion extends Suggestion { } } +function filterUsersByPrefix(users, prefix, limit) { + const filtered = []; + + for (const id of Object.keys(users)) { + if (filtered.length >= limit) { + break; + } + + const user = users[id]; + + if (user.delete_at > 0) { + continue; + } + + if (user.username.startsWith(prefix) || + (user.first_name && user.first_name.toLowerCase().startsWith(prefix)) || + (user.last_name && user.last_name.toLowerCase().startsWith(prefix)) || + (user.nickname && user.nickname.toLowerCase().startsWith(prefix))) { + filtered.push(user); + } + } + + return filtered; +} + export default class AtMentionProvider { handlePretextChanged(suggestionId, pretext) { const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext.toLowerCase()); if (captured) { const prefix = captured[1]; + // Group users into members and nonmembers of the channel. const users = UserStore.getActiveOnlyProfiles(true); - - const filtered = []; - - for (const id of Object.keys(users)) { - const user = users[id]; - - if (user.delete_at > 0) { - continue; - } - - if (user.username.startsWith(prefix) || - (user.first_name && user.first_name.toLowerCase().startsWith(prefix)) || - (user.last_name && user.last_name.toLowerCase().startsWith(prefix)) || - (user.nickname && user.nickname.toLowerCase().startsWith(prefix))) { - filtered.push(user); - } - - if (filtered.length >= MaxUserSuggestions) { - break; + const channelMembers = {}; + const extra = ChannelStore.getCurrentExtraInfo(); + for (let i = 0; i < extra.members.length; i++) { + const id = extra.members[i].id; + if (users[id]) { + channelMembers[id] = users[id]; + Reflect.deleteProperty(users, id); } } - + const channelNonmembers = users; + + // Filter users by prefix. + const filteredMembers = filterUsersByPrefix( + channelMembers, prefix, MaxUserSuggestions); + const filteredNonmembers = filterUsersByPrefix( + channelNonmembers, prefix, MaxUserSuggestions - filteredMembers.length); + let filteredSpecialMentions = []; if (!pretext.startsWith('/msg')) { - // add dummy users to represent the @channel and @all special mentions when not using the /msg command - if ('channel'.startsWith(prefix)) { - filtered.push({username: 'channel'}); - } - if ('all'.startsWith(prefix)) { - filtered.push({username: 'all'}); - } - if ('here'.startsWith(prefix)) { - filtered.push({username: 'here'}); - } + filteredSpecialMentions = ['here', 'channel', 'all'].filter((item) => { + return item.startsWith(prefix); + }).map((name) => { + return {username: name}; + }); } - filtered.sort((a, b) => { - const aPrefix = a.username.startsWith(prefix); - const bPrefix = b.username.startsWith(prefix); + // Sort users by username. + [filteredMembers, filteredNonmembers].forEach((items) => { + items.sort((a, b) => { + const aPrefix = a.username.startsWith(prefix); + const bPrefix = b.username.startsWith(prefix); - if (aPrefix === bPrefix) { - return a.username.localeCompare(b.username); - } else if (aPrefix) { - return -1; - } + if (aPrefix === bPrefix) { + return a.username.localeCompare(b.username); + } else if (aPrefix) { + return -1; + } + + return 1; + }); + }); - return 1; + filteredMembers.forEach((item) => { + item.type = Constants.MENTION_MEMBERS; }); + filteredNonmembers.forEach((item) => { + item.type = Constants.MENTION_NONMEMBERS; + }); + filteredSpecialMentions.forEach((item) => { + item.type = Constants.MENTION_SPECIAL; + }); + + const filtered = filteredMembers.concat(filteredSpecialMentions).concat(filteredNonmembers); const mentions = filtered.map((user) => '@' + user.username); diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx index 52b85b2f5..7c746ac2a 100644 --- a/webapp/components/suggestion/suggestion_list.jsx +++ b/webapp/components/suggestion/suggestion_list.jsx @@ -5,6 +5,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as GlobalActions from 'actions/global_actions.jsx'; import SuggestionStore from 'stores/suggestion_store.jsx'; +import {FormattedMessage} from 'react-intl'; import React from 'react'; @@ -92,19 +93,39 @@ export default class SuggestionList extends React.Component { } } + renderDivider(type) { + return ( + <div + key={type + '-divider'} + className='suggestion-list__divider' + > + <span> + <FormattedMessage id={'suggestion.' + type}/> + </span> + </div> + ); + } + render() { if (this.state.items.length === 0) { return null; } const items = []; + let lastType; for (let i = 0; i < this.state.items.length; i++) { + const item = this.state.items[i]; const term = this.state.terms[i]; const isSelection = term === this.state.selection; // ReactComponent names need to be upper case when used in JSX const Component = this.state.components[i]; + if (item.type !== lastType) { + items.push(this.renderDivider(item.type)); + lastType = item.type; + } + items.push( <Component key={term} diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx index c7020fed9..bc542165a 100644 --- a/webapp/components/user_profile.jsx +++ b/webapp/components/user_profile.jsx @@ -79,6 +79,24 @@ export default class UserProfile extends React.Component { /> ); + let fullname = Utils.getFullName(this.props.user); + if (fullname) { + dataContent.push( + <div + data-toggle='tooltip' + title={fullname} + key='user-popover-fullname' + > + + <p + className='text-nowrap' + > + {fullname} + </p> + </div> + ); + } + if (global.window.mm_config.ShowEmailAddress === 'true' || UserStore.isSystemAdminForCurrentUser() || this.props.user === UserStore.getCurrentUser()) { dataContent.push( <div @@ -103,7 +121,7 @@ export default class UserProfile extends React.Component { rootClose={true} overlay={ <Popover - title={name} + title={'@' + this.props.user.username} id='user-profile-popover' > {dataContent} diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx index d1c195c7e..e725060ab 100644 --- a/webapp/components/user_settings/user_settings_general.jsx +++ b/webapp/components/user_settings/user_settings_general.jsx @@ -27,7 +27,7 @@ const holders = defineMessages({ }, validEmail: { id: 'user.settings.general.validEmail', - defaultMessage: 'Please enter a valid email address' + defaultMessage: 'Please enter a valid email address.' }, emailMatch: { id: 'user.settings.general.emailMatch', diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index 428c88e25..769959432 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -22,7 +22,7 @@ import {Link} from 'react-router/es6'; const holders = defineMessages({ currentPasswordError: { id: 'user.settings.security.currentPasswordError', - defaultMessage: 'Please enter your current password' + defaultMessage: 'Please enter your current password.' }, passwordLengthError: { id: 'user.settings.security.passwordLengthError', @@ -30,7 +30,7 @@ const holders = defineMessages({ }, passwordMatchError: { id: 'user.settings.security.passwordMatchError', - defaultMessage: 'The new passwords you entered do not match' + defaultMessage: 'The new passwords you entered do not match.' }, method: { id: 'user.settings.security.method', |