diff options
Diffstat (limited to 'web/react')
36 files changed, 719 insertions, 475 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index c8af2553d..f0a31ce90 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -90,8 +90,9 @@ export default class AccessHistoryModal extends React.Component { case '/channels/update': currentAuditDesc = 'Updated the ' + channelName + ' channel/group name'; break; - case '/channels/update_desc': - currentAuditDesc = 'Updated the ' + channelName + ' channel/group description'; + case '/channels/update_desc': // support the old path + case '/channels/update_header': + currentAuditDesc = 'Updated the ' + channelName + ' channel/group header'; break; default: let userIdField = []; diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index dd8812ad0..a945a551c 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -56,6 +56,8 @@ export default class TeamAnalytics extends React.Component { teamId, 'post_counts_day', (data) => { + data.reverse(); + var chartData = { labels: [], datasets: [{ @@ -89,6 +91,8 @@ export default class TeamAnalytics extends React.Component { teamId, 'user_counts_with_posts_day', (data) => { + data.reverse(); + var chartData = { labels: [], datasets: [{ diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx index ffb412159..b44aba56e 100644 --- a/web/react/components/admin_console/team_users.jsx +++ b/web/react/components/admin_console/team_users.jsx @@ -33,14 +33,6 @@ export default class UserList extends React.Component { this.getTeamProfiles(this.props.team.id); } - // this.setState({ - // teamId: this.state.teamId, - // users: this.state.users, - // serverError: this.state.serverError, - // showPasswordModal: this.state.showPasswordModal, - // user: this.state.user - // }); - getTeamProfiles(teamId) { Client.getProfilesForTeam( teamId, @@ -95,8 +87,6 @@ export default class UserList extends React.Component { } doPasswordResetDismiss() { - this.state.showPasswordModal = false; - this.state.user = null; this.setState({ teamId: this.state.teamId, users: this.state.users, diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index d66777cc6..101fd85e5 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -11,6 +11,7 @@ const TextFormatting = require('../utils/text_formatting.jsx'); const Utils = require('../utils/utils.jsx'); const MessageWrapper = require('./message_wrapper.jsx'); const PopoverListMembers = require('./popover_list_members.jsx'); +const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); const Constants = require('../utils/constants.jsx'); @@ -27,7 +28,9 @@ export default class ChannelHeader extends React.Component { this.handleLeave = this.handleLeave.bind(this); this.searchMentions = this.searchMentions.bind(this); - this.state = this.getStateFromStores(); + const state = this.getStateFromStores(); + state.showEditChannelPurposeModal = false; + this.state = state; } getStateFromStores() { return { @@ -110,11 +113,11 @@ export default class ChannelHeader extends React.Component { bSize='large' placement='bottom' className='description' - onMouseOver={() => this.refs.descriptionOverlay.show()} - onMouseOut={() => this.refs.descriptionOverlay.hide()} + onMouseOver={() => this.refs.headerOverlay.show()} + onMouseOut={() => this.refs.headerOverlay.hide()} > <MessageWrapper - message={channel.description} + message={channel.header} /> </Popover> ); @@ -144,7 +147,7 @@ export default class ChannelHeader extends React.Component { if (isDirect) { dropdownContents.push( <li - key='edit_description_direct' + key='edit_header_direct' role='presentation' > <a @@ -152,11 +155,11 @@ export default class ChannelHeader extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Description... + Set Channel Header... </a> </li> ); @@ -216,7 +219,7 @@ export default class ChannelHeader extends React.Component { dropdownContents.push( <li - key='set_channel_description' + key='set_channel_header' role='presentation' > <a @@ -224,11 +227,25 @@ export default class ChannelHeader extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - Set {channelTerm} Description... + Set {channelTerm} Header... + </a> + </li> + ); + dropdownContents.push( + <li + key='set_channel_purpose' + role='presentation' + > + <a + role='menuitem' + href='#' + onClick={() => this.setState({showEditChannelPurposeModal: true})} + > + Set {channelTerm} Purpose... </a> </li> ); @@ -307,84 +324,91 @@ export default class ChannelHeader extends React.Component { } return ( - <table className='channel-header alt'> - <tbody> - <tr> - <th> - <div className='channel-header__info'> - <div className='dropdown'> + <div> + <table className='channel-header alt'> + <tbody> + <tr> + <th> + <div className='channel-header__info'> + <div className='dropdown'> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <strong className='heading'>{channelTitle} </strong> + <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' /> + </a> + <ul + className='dropdown-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {dropdownContents} + </ul> + </div> + <OverlayTrigger + trigger={['hover', 'focus']} + placement='bottom' + overlay={popoverContent} + ref='headerOverlay' + > + <div + onClick={TextFormatting.handleClick} + className='description' + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}} + /> + </OverlayTrigger> + </div> + </th> + <th> + <PopoverListMembers + members={this.state.users} + channelId={channel.id} + /> + </th> + <th className='search-bar__container'><NavbarSearchBox /></th> + <th> + <div className='dropdown channel-header__links'> <a href='#' className='dropdown-toggle theme' type='button' - id='channel_header_dropdown' + id='channel_header_right_dropdown' data-toggle='dropdown' aria-expanded='true' > - <strong className='heading'>{channelTitle} </strong> - <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' /> + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> </a> <ul - className='dropdown-menu' + className='dropdown-menu dropdown-menu-right' role='menu' - aria-labelledby='channel_header_dropdown' + aria-labelledby='channel_header_right_dropdown' > - {dropdownContents} + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.searchMentions} + > + Recent Mentions + </a> + </li> </ul> </div> - <OverlayTrigger - trigger={['hover', 'focus']} - placement='bottom' - overlay={popoverContent} - ref='descriptionOverlay' - > - <div - onClick={TextFormatting.handleClick} - className='description' - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}} - /> - </OverlayTrigger> - </div> - </th> - <th> - <PopoverListMembers - members={this.state.users} - channelId={channel.id} - /> - </th> - <th className='search-bar__container'><NavbarSearchBox /></th> - <th> - <div className='dropdown channel-header__links'> - <a - href='#' - className='dropdown-toggle theme' - type='button' - id='channel_header_right_dropdown' - data-toggle='dropdown' - aria-expanded='true' - > - <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> - </a> - <ul - className='dropdown-menu dropdown-menu-right' - role='menu' - aria-labelledby='channel_header_right_dropdown' - > - <li role='presentation'> - <a - role='menuitem' - href='#' - onClick={this.searchMentions} - > - Recent Mentions - </a> - </li> - </ul> - </div> - </th> - </tr> - </tbody> - </table> + </th> + </tr> + </tbody> + </table> + <EditChannelPurposeModal + show={this.state.showEditChannelPurposeModal} + onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} + channel={channel} + /> + </div> ); } } diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 18936e808..058594165 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -8,6 +8,7 @@ const SocketStore = require('../stores/socket_store.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const PostStore = require('../stores/post_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); const Textbox = require('./textbox.jsx'); const MsgTyping = require('./msg_typing.jsx'); const FileUpload = require('./file_upload.jsx'); @@ -27,7 +28,7 @@ export default class CreateComment extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this); this.handleUserInput = this.handleUserInput.bind(this); - this.handleArrowUp = this.handleArrowUp.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.handleUploadStart = this.handleUploadStart.bind(this); this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); this.handleUploadError = this.handleUploadError.bind(this); @@ -36,6 +37,7 @@ export default class CreateComment extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); this.getFileCount = this.getFileCount.bind(this); this.handleResize = this.handleResize.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); PostStore.clearCommentDraftUploads(); @@ -45,15 +47,23 @@ export default class CreateComment extends React.Component { uploadsInProgress: draft.uploadsInProgress, previews: draft.previews, submitting: false, - windowWidth: Utils.windowWidth() + windowWidth: Utils.windowWidth(), + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value }; } componentDidMount() { + PreferenceStore.addChangeListener(this.onPreferenceChange); window.addEventListener('resize', this.handleResize); } componentWillUnmount() { + PreferenceStore.removeChangeListener(this.onPreferenceChange); window.removeEventListener('resize', this.handleResize); } + onPreferenceChange() { + this.setState({ + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + }); + } handleResize() { this.setState({windowWidth: Utils.windowWidth()}); } @@ -140,10 +150,12 @@ export default class CreateComment extends React.Component { this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); } commentMsgKeyPress(e) { - if (e.which === 13 && !e.shiftKey && !e.altKey) { - e.preventDefault(); - ReactDOM.findDOMNode(this.refs.textbox).blur(); - this.handleSubmit(e); + if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') { + if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.textbox).blur(); + this.handleSubmit(e); + } } const t = Date.now(); @@ -161,7 +173,12 @@ export default class CreateComment extends React.Component { $('.post-right__scroll').perfectScrollbar('update'); this.setState({messageText: messageText}); } - handleArrowUp(e) { + handleKeyDown(e) { + if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.commentMsgKeyPress(e); + return; + } + if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { e.preventDefault(); @@ -313,7 +330,7 @@ export default class CreateComment extends React.Component { <Textbox onUserInput={this.handleUserInput} onKeyPress={this.commentMsgKeyPress} - onKeyDown={this.handleArrowUp} + onKeyDown={this.handleKeyDown} messageText={this.state.messageText} createMessage='Add a comment...' initialText='' diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 32ee31efe..cdbc3bc6d 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -8,6 +8,7 @@ const ChannelStore = require('../stores/channel_store.jsx'); const PostStore = require('../stores/post_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const SocketStore = require('../stores/socket_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); const MsgTyping = require('./msg_typing.jsx'); const Textbox = require('./textbox.jsx'); const FileUpload = require('./file_upload.jsx'); @@ -36,9 +37,10 @@ export default class CreatePost extends React.Component { this.removePreview = this.removePreview.bind(this); this.onChange = this.onChange.bind(this); this.getFileCount = this.getFileCount.bind(this); - this.handleArrowUp = this.handleArrowUp.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.handleResize = this.handleResize.bind(this); this.sendMessage = this.sendMessage.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); PostStore.clearDraftUploads(); @@ -52,8 +54,16 @@ export default class CreatePost extends React.Component { submitting: false, initialText: draft.messageText, windowWidth: Utils.windowWidth(), - windowHeight: Utils.windowHeight() + windowHeight: Utils.windowHeight(), + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value }; + + PreferenceStore.addChangeListener(this.onPreferenceChange); + } + onPreferenceChange() { + this.setState({ + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + }); } handleResize() { this.setState({ @@ -201,10 +211,12 @@ export default class CreatePost extends React.Component { ); } postMsgKeyPress(e) { - if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { - e.preventDefault(); - ReactDOM.findDOMNode(this.refs.textbox).blur(); - this.handleSubmit(e); + if (this.state.ctrlSend === 'true' && e.ctrlKey || this.state.ctrlSend === 'false') { + if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.textbox).blur(); + this.handleSubmit(e); + } } const t = Date.now(); @@ -328,7 +340,12 @@ export default class CreatePost extends React.Component { const draft = PostStore.getDraft(channelId); return draft.previews.length + draft.uploadsInProgress.length; } - handleArrowUp(e) { + handleKeyDown(e) { + if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.postMsgKeyPress(e); + return; + } + if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { e.preventDefault(); @@ -393,7 +410,7 @@ export default class CreatePost extends React.Component { <Textbox onUserInput={this.handleUserInput} onKeyPress={this.postMsgKeyPress} - onKeyDown={this.handleArrowUp} + onKeyDown={this.handleKeyDown} onHeightChange={this.resizePostHolder} messageText={this.state.messageText} createMessage='Write a message...' diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index d63a1db30..6f3826f75 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -14,7 +14,7 @@ export default class EditChannelModal extends React.Component { this.onShow = this.onShow.bind(this); this.state = { - description: '', + header: '', title: '', channelId: '', serverError: '' @@ -28,32 +28,32 @@ export default class EditChannelModal extends React.Component { return; } - data.channel_description = this.state.description.trim(); + data.channel_header = this.state.header.trim(); - Client.updateChannelDesc(data, - function handleUpdateSuccess() { + Client.updateChannelHeader(data, + () => { this.setState({serverError: ''}); AsyncClient.getChannel(this.state.channelId); $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide'); - }.bind(this), - function handleUpdateError(err) { - if (err.message === 'Invalid channel_description parameter') { - this.setState({serverError: 'This description is too long, please enter a shorter one'}); + }, + (err) => { + if (err.message === 'Invalid channel_header parameter') { + this.setState({serverError: 'This channel header is too long, please enter a shorter one'}); } else { this.setState({serverError: err.message}); } - }.bind(this) + } ); } handleUserInput(e) { - this.setState({description: e.target.value}); + this.setState({header: e.target.value}); } handleClose() { - this.setState({description: '', serverError: ''}); + this.setState({header: '', serverError: ''}); } onShow(e) { const button = e.relatedTarget; - this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''}); + this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''}); } componentDidMount() { $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow); @@ -73,7 +73,7 @@ export default class EditChannelModal extends React.Component { className='modal-title' ref='title' > - Edit Description + Edit Header </h4> ); if (this.state.title) { @@ -82,7 +82,7 @@ export default class EditChannelModal extends React.Component { className='modal-title' ref='title' > - Edit Description for <span className='name'>{this.state.title}</span> + Edit Header for <span className='name'>{this.state.title}</span> </h4> ); } @@ -113,9 +113,8 @@ export default class EditChannelModal extends React.Component { <textarea className='form-control no-resize' rows='6' - ref='channelDesc' maxLength='1024' - value={this.state.description} + value={this.state.header} onChange={this.handleUserInput} /> {serverError} diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx new file mode 100644 index 000000000..d8102642e --- /dev/null +++ b/web/react/components/edit_channel_purpose_modal.jsx @@ -0,0 +1,118 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); +const Modal = ReactBootstrap.Modal; + +export default class EditChannelPurposeModal extends React.Component { + constructor(props) { + super(props); + + this.handleHide = this.handleHide.bind(this); + this.handleSave = this.handleSave.bind(this); + + this.state = {serverError: ''}; + } + + handleHide() { + this.setState({serverError: ''}); + + if (this.props.onModalDismissed) { + this.props.onModalDismissed(); + } + } + + handleSave() { + if (!this.props.channel) { + return; + } + + const data = { + channel_id: this.props.channel.id, + channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim() + }; + + Client.updateChannelPurpose(data, + () => { + AsyncClient.getChannel(this.props.channel.id); + + this.handleHide(); + }, + (err) => { + if (err.message === 'Invalid channel_purpose parameter') { + this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'}); + } else { + this.setState({serverError: err.message}); + } + } + ); + } + + render() { + if (!this.props.show) { + return null; + } + + let serverError = null; + if (this.state.serverError) { + serverError = ( + <div className='form-group has-error'> + <br/> + <label className='control-label'>{this.state.serverError}</label> + </div> + ); + } + + let title = <span>{'Edit Purpose'}</span>; + if (this.props.channel.display_name) { + title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>; + } + + return ( + <Modal + className='modal-edit-channel-purpose' + show={this.props.show} + onHide={this.handleHide} + > + <Modal.Header closeButton={true}> + <Modal.Title> + {title} + </Modal.Title> + </Modal.Header> + <Modal.Body> + <textarea + ref='purpose' + className='form-control no-resize' + rows='6' + maxLength='128' + defaultValue={this.props.channel.purpose} + /> + {serverError} + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.handleHide} + > + {'Cancel'} + </button> + <button + type='button' + className='btn btn-primary' + onClick={this.handleSave} + > + {'Save'} + </button> + </Modal.Footer> + </Modal> + ); + } +} + +EditChannelPurposeModal.propTypes = { + show: React.PropTypes.bool.isRequired, + channel: React.PropTypes.object, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 4d4e8390c..e707e32f5 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -270,7 +270,7 @@ export default class FileAttachment extends React.Component { href={fileUrl} download={filenameString} data-toggle='tooltip' - title={filenameString} + title={'Download ' + filenameString} className='post-image__name' > {trimmedFilename} diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index 325e86f3d..8839bc3c7 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -98,7 +98,7 @@ export default class GetLinkModal extends React.Component { <br /><br /> </p> <textarea - className='form-control no-resize' + className='form-control no-resize min-height' readOnly='true' ref='textarea' value={this.state.value} diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index a0084ad30..c4f831c2e 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -109,7 +109,7 @@ export default class MoreChannels extends React.Component { <tr key={channel.id}> <td> <p className='more-name'>{channel.display_name}</p> - <p className='more-description'>{channel.description}</p> + <p className='more-purpose'>{channel.purpose}</p> </td> <td className='td--action'> {joinButton} diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index f9cd525fd..f7778f25f 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var MessageWrapper = require('./message_wrapper.jsx'); var NotifyCounts = require('./notify_counts.jsx'); +const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); const Utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); @@ -26,7 +27,9 @@ export default class Navbar extends React.Component { this.createCollapseButtons = this.createCollapseButtons.bind(this); this.createDropdown = this.createDropdown.bind(this); - this.state = this.getStateFromStores(); + const state = this.getStateFromStores(); + state.showEditChannelPurposeModal = false; + this.state = state; } getStateFromStores() { return { @@ -106,22 +109,35 @@ export default class Navbar extends React.Component { </li> ); - var setChannelDescriptionOption = ( + var setChannelHeaderOption = ( <li role='presentation'> <a role='menuitem' href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Description... + Set Channel Header... </a> </li> ); + var setChannelPurposeOption = null; + if (!isDirect) { + setChannelPurposeOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={() => this.setState({showEditChannelPurposeModal: true})} + /> + </li> + ); + } + var addMembersOption; var leaveChannelOption; if (!isDirect && !ChannelStore.isDefault(channel)) { @@ -249,7 +265,8 @@ export default class Navbar extends React.Component { {viewInfoOption} {addMembersOption} {manageMembersOption} - {setChannelDescriptionOption} + {setChannelHeaderOption} + {setChannelPurposeOption} {notificationPreferenceOption} {renameChannelOption} {deleteChannelOption} @@ -335,10 +352,10 @@ export default class Navbar extends React.Component { <Popover bsStyle='info' placement='bottom' - id='description-popover' + id='header-popover' > <MessageWrapper - message={channel.description} + message={channel.header} options={{singleline: true, mentionHighlight: false}} /> </Popover> @@ -360,20 +377,20 @@ export default class Navbar extends React.Component { } } - if (channel.description.length === 0) { + if (channel.header.length === 0) { popoverContent = ( <Popover bsStyle='info' placement='bottom' - id='description-popover' + id='header-popover' > <div> - {'No channel description yet.'} + {'No channel header yet.'} <br/> <a href='#' data-toggle='modal' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} data-target='#edit_channel' @@ -392,17 +409,24 @@ export default class Navbar extends React.Component { var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent); return ( - <nav - className='navbar navbar-default navbar-fixed-top' - role='navigation' - > - <div className='container-fluid theme'> - <div className='navbar-header'> - {collapseButtons} - {channelMenuDropdown} + <div> + <nav + className='navbar navbar-default navbar-fixed-top' + role='navigation' + > + <div className='container-fluid theme'> + <div className='navbar-header'> + {collapseButtons} + {channelMenuDropdown} + </div> </div> - </div> - </nav> + </nav> + <EditChannelPurposeModal + show={this.state.showEditChannelPurposeModal} + onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} + channel={channel} + /> + </div> ); } } diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 2b68645e5..2b0f3c40e 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -58,6 +58,7 @@ export default class NavbarDropdown extends React.Component { TeamStore.addChangeListener(this.onListenerChange); $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => { + $('.sidebar--left .dropdown-menu').scrollTop(0); this.blockToggle = true; setTimeout(() => { this.blockToggle = false; diff --git a/web/react/components/new_channel_flow.jsx b/web/react/components/new_channel_flow.jsx index 186cfc2b0..d6280d118 100644 --- a/web/react/components/new_channel_flow.jsx +++ b/web/react/components/new_channel_flow.jsx @@ -30,7 +30,7 @@ export default class NewChannelFlow extends React.Component { flowState: SHOW_NEW_CHANNEL, channelDisplayName: '', channelName: '', - channelDescription: '', + channelPurpose: '', nameModified: false }; } @@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component { flowState: SHOW_NEW_CHANNEL, channelDisplayName: '', channelName: '', - channelDescription: '', + channelPurpose: '', nameModified: false }); } @@ -65,7 +65,7 @@ export default class NewChannelFlow extends React.Component { const cu = UserStore.getCurrentUser(); channel.team_id = cu.team_id; - channel.description = this.state.channelDescription; + channel.purpose = this.state.channelPurpose; channel.type = this.state.channelType; Client.createChannel(channel, @@ -109,7 +109,7 @@ export default class NewChannelFlow extends React.Component { channelDataChanged(data) { this.setState({ channelDisplayName: data.displayName, - channelDescription: data.description + channelPurpose: data.purpose }); if (!this.state.nameModified) { this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())}); @@ -119,7 +119,7 @@ export default class NewChannelFlow extends React.Component { const channelData = { name: this.state.channelName, displayName: this.state.channelDisplayName, - description: this.state.channelDescription + purpose: this.state.channelPurpose }; let showChannelModal = false; diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx index 4e6280c99..c0cea496f 100644 --- a/web/react/components/new_channel_modal.jsx +++ b/web/react/components/new_channel_modal.jsx @@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component { handleChange() { const newData = { displayName: ReactDOM.findDOMNode(this.refs.display_name).value, - description: ReactDOM.findDOMNode(this.refs.channel_desc).value + purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value }; this.props.onDataChanged(newData); } @@ -136,22 +136,22 @@ export default class NewChannelModal extends React.Component { </div> <div className='form-group less'> <div className='col-sm-3'> - <label className='form__label control-label'>{'Description'}</label> + <label className='form__label control-label'>{'Purpose'}</label> <label className='form__label light'>{'(optional)'}</label> </div> <div className='col-sm-9'> <textarea className='form-control no-resize' - ref='channel_desc' + ref='channel_purpose' rows='4' - placeholder='Description' - maxLength='1024' - value={this.props.channelData.description} + placeholder='Purpose' + maxLength='128' + value={this.props.channelData.purpose} onChange={this.handleChange} tabIndex='2' /> <p className='input__help'> - {'Description helps others decide whether to join this channel.'} + {`Describe how this ${channelTerm} should be used.`} </p> {serverError} </div> diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 45eae8c6a..7138e2cb4 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -297,7 +297,7 @@ export default class PostBody extends React.Component { } let embed; - if (filenames.length === 0 && this.state.links) { + if (filenames.length === 0 && this.state.links && this.state.links.length > 0) { embed = this.createEmbed(this.state.links[0]); } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 3ceef478c..0d69e56bf 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -358,11 +358,11 @@ export default class PostList extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>{'Set a description'} + <i className='fa fa-pencil'></i>{'Set a header'} </a> </div> ); @@ -413,11 +413,11 @@ export default class PostList extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>{'Set a description'} + <i className='fa fa-pencil'></i>{'Set a header'} </a> <a className='intro-links' @@ -479,11 +479,11 @@ export default class PostList extends React.Component { href='#' data-toggle='modal' data-target='#edit_channel' - data-desc={channel.description} + data-header={channel.header} data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>{'Set a description'} + <i className='fa fa-pencil'></i>{'Set a header'} </a> <a className='intro-links' diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx index 3d4d9bf45..c40409dcc 100644 --- a/web/react/components/register_app_modal.jsx +++ b/web/react/components/register_app_modal.jsx @@ -96,75 +96,74 @@ export default class RegisterAppModal extends React.Component { var body = ''; if (this.state.clientId === '') { body = ( - <div className='form-group user-settings'> - <h3>{'Register a New Application'}</h3> - <br/> - <label className='col-sm-4 control-label'>{'Application Name'}</label> - <div className='col-sm-7'> - <input - ref='name' - className='form-control' - type='text' - placeholder='Required' - /> - {nameError} - </div> - <br/> - <br/> - <label className='col-sm-4 control-label'>{'Homepage URL'}</label> - <div className='col-sm-7'> - <input - ref='homepage' - className='form-control' - type='text' - placeholder='Required' - /> - {homepageError} - </div> - <br/> - <br/> - <label className='col-sm-4 control-label'>{'Description'}</label> - <div className='col-sm-7'> - <input - ref='desc' - className='form-control' - type='text' - placeholder='Optional' - /> - </div> - <br/> - <br/> - <label className='col-sm-4 control-label'>{'Callback URL'}</label> - <div className='col-sm-7'> - <textarea - ref='callback' - className='form-control' - type='text' - placeholder='Required' - rows='5' - /> - {callbackError} + <div className='settings-modal'> + <div className='form-horizontal user-settings'> + <h4 className='padding-bottom x3'>{'Register a New Application'}</h4> + <div className='row'> + <label className='col-sm-4 control-label'>{'Application Name'}</label> + <div className='col-sm-7'> + <input + ref='name' + className='form-control' + type='text' + placeholder='Required' + /> + {nameError} + </div> + </div> + <div className='row padding-top x2'> + <label className='col-sm-4 control-label'>{'Homepage URL'}</label> + <div className='col-sm-7'> + <input + ref='homepage' + className='form-control' + type='text' + placeholder='Required' + /> + {homepageError} + </div> + </div> + <div className='row padding-top x2'> + <label className='col-sm-4 control-label'>{'Description'}</label> + <div className='col-sm-7'> + <input + ref='desc' + className='form-control' + type='text' + placeholder='Optional' + /> + </div> + </div> + <div className='row padding-top padding-bottom x2'> + <label className='col-sm-4 control-label'>{'Callback URL'}</label> + <div className='col-sm-7'> + <textarea + ref='callback' + className='form-control' + type='text' + placeholder='Required' + rows='5' + /> + {callbackError} + </div> + </div> + {serverError} + <hr /> + <a + className='btn btn-sm theme pull-right' + href='#' + data-dismiss='modal' + aria-label='Close' + > + {'Cancel'} + </a> + <a + className='btn btn-sm btn-primary pull-right' + onClick={this.register} + > + {'Register'} + </a> </div> - <br/> - <br/> - <br/> - <br/> - <br/> - {serverError} - <a - className='btn btn-sm theme pull-right' - href='#' - data-dismiss='modal' - aria-label='Close' - > - {'Cancel'} - </a> - <a - className='btn btn-sm btn-primary pull-right' - onClick={this.register} - > - {'Register'} - </a> </div> ); } else { diff --git a/web/react/components/search_autocomplete.jsx b/web/react/components/search_autocomplete.jsx index 03c7b894c..f7d772677 100644 --- a/web/react/components/search_autocomplete.jsx +++ b/web/react/components/search_autocomplete.jsx @@ -10,6 +10,7 @@ const patterns = new Map([ ['channels', /\b(?:in|channel):\s*(\S*)$/i], ['users', /\bfrom:\s*(\S*)$/i] ]); +const Popover = ReactBootstrap.Popover; export default class SearchAutocomplete extends React.Component { constructor(props) { @@ -36,6 +37,11 @@ export default class SearchAutocomplete extends React.Component { $(document).on('click', this.handleDocumentClick); } + componentDidUpdate() { + $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar(); + $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200); + } + componentWillUnmount() { $(document).off('click', this.handleDocumentClick); } @@ -193,7 +199,7 @@ export default class SearchAutocomplete extends React.Component { if (this.state.mode === 'channels') { suggestions = this.state.suggestions.map((channel, index) => { - let className = 'search-autocomplete__channel'; + let className = 'search-autocomplete__item'; if (this.state.selection === index) { className += ' selected'; } @@ -211,7 +217,7 @@ export default class SearchAutocomplete extends React.Component { }); } else if (this.state.mode === 'users') { suggestions = this.state.suggestions.map((user, index) => { - let className = 'search-autocomplete__user'; + let className = 'search-autocomplete__item'; if (this.state.selection === index) { className += ' selected'; } @@ -224,7 +230,7 @@ export default class SearchAutocomplete extends React.Component { className={className} > <img - className='profile-img' + className='profile-img rounded' src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at} /> {user.username} @@ -234,12 +240,15 @@ export default class SearchAutocomplete extends React.Component { } return ( - <div - ref='container' - className='search-autocomplete' + <Popover + ref='searchPopover' + onShow={this.componentDidMount} + id='search-autocomplete__popover' + className='search-help-popover autocomplete visible' + placement='bottom' > {suggestions} - </div> + </Popover> ); } } diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 774f98a43..d6c4b0d4b 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -35,8 +35,10 @@ export default class SettingItemMax extends React.Component { var widthClass; if (this.props.width === 'full') { widthClass = 'col-sm-12'; - } else { + } else if (this.props.width === 'medium') { widthClass = 'col-sm-10 col-sm-offset-2'; + } else { + widthClass = 'col-sm-9 col-sm-offset-3'; } return ( diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index b6bcb13a6..e69412cca 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -42,7 +42,7 @@ export default class SettingPicture extends React.Component { img = ( <img ref='image' - className='profile-img' + className='profile-img rounded' src='' /> ); @@ -50,7 +50,7 @@ export default class SettingPicture extends React.Component { img = ( <img ref='image' - className='profile-img' + className='profile-img rounded' src={this.props.src} /> ); diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index de28a8374..65e4c6d7e 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -5,6 +5,9 @@ var NavbarDropdown = require('./navbar_dropdown.jsx'); var UserStore = require('../stores/user_store.jsx'); const Utils = require('../utils/utils.jsx'); +const Tooltip = ReactBootstrap.Tooltip; +const OverlayTrigger = ReactBootstrap.OverlayTrigger; + export default class SidebarHeader extends React.Component { constructor(props) { super(props); @@ -47,7 +50,15 @@ export default class SidebarHeader extends React.Component { {profilePicture} <div className='header__info'> <div className='user__name'>{'@' + me.username}</div> + <OverlayTrigger + trigger={['hover', 'focus']} + delayShow={1000} + placement='bottom' + overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>} + ref='descriptionOverlay' + > <div className='team__name'>{this.props.teamDisplayName}</div> + </OverlayTrigger> </div> </a> <NavbarDropdown diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx index ff4ccd4d8..021713f04 100644 --- a/web/react/components/team_signup_with_email.jsx +++ b/web/react/components/team_signup_with_email.jsx @@ -71,7 +71,7 @@ export default class EmailSignUpPage extends React.Component { className='btn btn-md btn-primary' type='submit' > - {'Sign up'} + {'Create Team'} </button> {serverError} </div> diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx deleted file mode 100644 index eef4b24ba..000000000 --- a/web/react/components/user_settings/code_theme_chooser.jsx +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -var Constants = require('../../utils/constants.jsx'); - -export default class CodeThemeChooser extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - render() { - const theme = this.props.theme; - - const premadeThemes = []; - for (const k in Constants.CODE_THEMES) { - if (Constants.CODE_THEMES.hasOwnProperty(k)) { - let activeClass = ''; - if (k === theme.codeTheme) { - activeClass = 'active'; - } - - premadeThemes.push( - <div - className='col-xs-6 col-sm-3 premade-themes' - key={'premade-theme-key' + k} - > - <div - className={activeClass} - onClick={() => this.props.updateTheme(k)} - > - <label> - <img - className='img-responsive' - src={'/static/images/themes/code_themes/' + k + '.png'} - /> - <div className='theme-label'>{Constants.CODE_THEMES[k]}</div> - </label> - </div> - </div> - ); - } - } - - return ( - <div className='row'> - {premadeThemes} - </div> - ); - } -} - -CodeThemeChooser.propTypes = { - theme: React.PropTypes.object.isRequired, - updateTheme: React.PropTypes.func.isRequired -}; diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index 6e9b2205d..4c56db0a1 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -236,6 +236,7 @@ export default class ManageOutgoingHooks extends React.Component { return ( <div key='addOutgoingHook'> <label className='control-label'>{'Add a new outgoing webhook'}</label> + <div className='padding-top divider-light'></div> <div className='padding-top'> <div> <label className='control-label'>{'Channel'}</label> diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index 15bf961d6..546e26ca3 100644 --- a/web/react/components/user_settings/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -10,6 +10,7 @@ var AppearanceTab = require('./user_settings_appearance.jsx'); var DeveloperTab = require('./user_settings_developer.jsx'); var IntegrationsTab = require('./user_settings_integrations.jsx'); var DisplayTab = require('./user_settings_display.jsx'); +var AdvancedTab = require('./user_settings_advanced.jsx'); export default class UserSettings extends React.Component { constructor(props) { @@ -110,6 +111,17 @@ export default class UserSettings extends React.Component { /> </div> ); + } else if (this.props.activeTab === 'advanced') { + return ( + <div> + <AdvancedTab + user={this.state.user} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + updateTab={this.props.updateTab} + /> + </div> + ); } return <div/>; diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx new file mode 100644 index 000000000..910444735 --- /dev/null +++ b/web/react/components/user_settings/user_settings_advanced.jsx @@ -0,0 +1,169 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const Client = require('../../utils/client.jsx'); +const SettingItemMin = require('../setting_item_min.jsx'); +const SettingItemMax = require('../setting_item_max.jsx'); +const Constants = require('../../utils/constants.jsx'); +const PreferenceStore = require('../../stores/preference_store.jsx'); + +export default class AdvancedSettingsDisplay extends React.Component { + constructor(props) { + super(props); + + this.updateSection = this.updateSection.bind(this); + this.updateSetting = this.updateSetting.bind(this); + this.handleClose = this.handleClose.bind(this); + this.setupInitialState = this.setupInitialState.bind(this); + + this.state = this.setupInitialState(); + } + + setupInitialState() { + const sendOnCtrlEnter = PreferenceStore.getPreference( + Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, + 'send_on_ctrl_enter', + {value: 'false'} + ).value; + + return { + settings: {send_on_ctrl_enter: sendOnCtrlEnter} + }; + } + + updateSetting(setting, value) { + const settings = this.state.settings; + settings[setting] = value; + this.setState(settings); + } + + handleSubmit(setting) { + const preference = PreferenceStore.setPreference( + Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, + setting, + this.state.settings[setting] + ); + + Client.savePreferences([preference], + () => { + PreferenceStore.emitChange(); + this.updateSection(''); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + updateSection(section) { + this.props.updateSection(section); + } + + handleClose() { + this.updateSection(''); + } + + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + } + + render() { + const serverError = this.state.serverError || null; + let ctrlSendSection; + + if (this.props.activeSection === 'advancedCtrlSend') { + const ctrlSendActive = [ + this.state.settings.send_on_ctrl_enter === 'true', + this.state.settings.send_on_ctrl_enter === 'false' + ]; + + const inputs = [ + <div key='ctrlSendSetting'> + <div className='radio'> + <label> + <input + type='radio' + checked={ctrlSendActive[0]} + onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'true')} + /> + {'On'} + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + type='radio' + checked={ctrlSendActive[1]} + onChange={this.updateSetting.bind(this, 'send_on_ctrl_enter', 'false')} + /> + {'Off'} + </label> + <br/> + </div> + <div><br/>{'If enabled \'Enter\' inserts a new line and \'Ctrl + Enter\' submits the message.'}</div> + </div> + ]; + + ctrlSendSection = ( + <SettingItemMax + title='Send messages on Ctrl + Enter' + inputs={inputs} + submit={() => this.handleSubmit('send_on_ctrl_enter')} + server_error={serverError} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + ctrlSendSection = ( + <SettingItemMin + title='Send messages on Ctrl + Enter' + describe={this.state.settings.send_on_ctrl_enter === 'true' ? 'On' : 'Off'} + updateSection={() => this.props.updateSection('advancedCtrlSend')} + /> + ); + } + + return ( + <div> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>{'×'}</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i> + {'Advanced Settings'} + </h4> + </div> + <div className='user-settings'> + <h3 className='tab-header'>{'Advanced Settings'}</h3> + <div className='divider-dark first'/> + {ctrlSendSection} + <div className='divider-dark'/> + </div> + </div> + ); + } +} + +AdvancedSettingsDisplay.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index e94894a1d..8c62a189d 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -7,7 +7,6 @@ var Utils = require('../../utils/utils.jsx'); const CustomThemeChooser = require('./custom_theme_chooser.jsx'); const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); -const CodeThemeChooser = require('./code_theme_chooser.jsx'); const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); const Constants = require('../../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -19,14 +18,12 @@ export default class UserSettingsAppearance extends React.Component { this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); - this.updateCodeTheme = this.updateCodeTheme.bind(this); this.handleClose = this.handleClose.bind(this); this.handleImportModal = this.handleImportModal.bind(this); this.state = this.getStateFromStores(); this.originalTheme = this.state.theme; - this.originalCodeTheme = this.state.theme.codeTheme; } componentDidMount() { UserStore.addChangeListener(this.onChange); @@ -61,10 +58,6 @@ export default class UserSettingsAppearance extends React.Component { type = 'custom'; } - if (!theme.codeTheme) { - theme.codeTheme = Constants.DEFAULT_CODE_THEME; - } - return {theme, type}; } onChange() { @@ -100,13 +93,6 @@ export default class UserSettingsAppearance extends React.Component { ); } updateTheme(theme) { - theme.codeTheme = this.state.theme.codeTheme; - this.setState({theme}); - Utils.applyTheme(theme); - } - updateCodeTheme(codeTheme) { - var theme = this.state.theme; - theme.codeTheme = codeTheme; this.setState({theme}); Utils.applyTheme(theme); } @@ -116,7 +102,6 @@ export default class UserSettingsAppearance extends React.Component { handleClose() { const state = this.getStateFromStores(); state.serverError = null; - state.theme.codeTheme = this.originalCodeTheme; Utils.applyTheme(state.theme); @@ -185,13 +170,7 @@ export default class UserSettingsAppearance extends React.Component { </div> {custom} <hr /> - <strong className='radio'>{'Code Theme'}</strong> - <CodeThemeChooser - theme={this.state.theme} - updateTheme={this.updateCodeTheme} - /> - <hr /> - {serverError} + {serverError} <a className='btn btn-sm btn-primary' href='#' diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 1c8ce3c79..3adac197a 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -570,6 +570,7 @@ export default class UserSettingsGeneralTab extends React.Component { /> ); } + return ( <div> <div className='modal-header'> diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 4b1e5e532..9bee74343 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -43,6 +43,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMax title='Incoming Webhooks' + width='medium' inputs={inputs} updateSection={(e) => { this.updateSection(''); @@ -54,6 +55,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { incomingHooksSection = ( <SettingItemMin title='Incoming Webhooks' + width='medium' describe='Manage your incoming webhooks (Developer feature)' updateSection={() => { this.updateSection('incoming-hooks'); @@ -72,6 +74,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { outgoingHooksSection = ( <SettingItemMax title='Outgoing Webhooks' + width='medium' inputs={inputs} updateSection={(e) => { this.updateSection(''); @@ -83,6 +86,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { outgoingHooksSection = ( <SettingItemMin title='Outgoing Webhooks' + width='medium' describe='Manage your outgoing webhooks' updateSection={() => { this.updateSection('outgoing-hooks'); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 5449ae91e..18dd490e7 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -43,6 +43,7 @@ export default class UserSettingsModal extends React.Component { tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); } tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'}); + tabs.push({name: 'advanced', uiName: 'Advanced', icon: 'glyphicon glyphicon-list-alt'}); return ( <div diff --git a/web/react/package.json b/web/react/package.json index 9af6f5880..e6a662375 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -6,7 +6,6 @@ "autolinker": "0.18.1", "babel-runtime": "5.8.24", "flux": "2.1.1", - "highlight.js": "^8.9.1", "keymirror": "0.1.1", "marked": "0.3.5", "object-assign": "3.0.0", diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index bf117b3b3..aeb39d8a8 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -589,21 +589,38 @@ export function updateChannel(channel, success, error) { track('api', 'api_channels_update'); } -export function updateChannelDesc(data, success, error) { +export function updateChannelHeader(data, success, error) { $.ajax({ - url: '/api/v1/channels/update_desc', + url: '/api/v1/channels/update_header', dataType: 'json', contentType: 'application/json', type: 'POST', data: JSON.stringify(data), success, error: function onError(xhr, status, err) { - var e = handleError('updateChannelDesc', xhr, status, err); + var e = handleError('updateChannelHeader', xhr, status, err); error(e); } }); - track('api', 'api_channels_desc'); + track('api', 'api_channels_header'); +} + +export function updateChannelPurpose(data, success, error) { + $.ajax({ + url: '/api/v1/channels/update_purpose', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: function onError(xhr, status, err) { + var e = handleError('updateChannelPurpose', xhr, status, err); + error(e); + } + }); + + track('api', 'api_channels_purpose'); } export function updateNotifyProps(data, success, error) { diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 0e89b9470..69e3b007d 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -302,16 +302,10 @@ module.exports = { uiName: 'Mention Highlight Link' } ], - CODE_THEMES: { - github: 'GitHub', - solarized_light: 'Solarized light', - monokai: 'Monokai', - solarized_dark: 'Solarized Dark' - }, - DEFAULT_CODE_THEME: 'github', Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', - CATEGORY_DISPLAY_SETTINGS: 'display_settings' + CATEGORY_DISPLAY_SETTINGS: 'display_settings', + CATEGORY_ADVANCED_SETTINGS: 'advanced_settings' }, KeyCodes: { UP: 38, @@ -322,30 +316,5 @@ module.exports = { ENTER: 13, ESCAPE: 27, SPACE: 32 - }, - HighlightedLanguages: { - diff: 'Diff', - apache: 'Apache', - makefile: 'Makefile', - http: 'HTTP', - json: 'JSON', - markdown: 'Markdown', - javascript: 'JavaScript', - css: 'CSS', - nginx: 'nginx', - objectivec: 'Objective-C', - python: 'Python', - xml: 'XML', - perl: 'Perl', - bash: 'Bash', - php: 'PHP', - coffeescript: 'CoffeeScript', - cs: 'C#', - cpp: 'C++', - sql: 'SQL', - go: 'Go', - ruby: 'Ruby', - java: 'Java', - ini: 'ini' } }; diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index ad11a95ac..26587dd6e 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -6,34 +6,6 @@ const Utils = require('./utils.jsx'); const marked = require('marked'); -const highlightJs = require('highlight.js/lib/highlight.js'); -const highlightJsDiff = require('highlight.js/lib/languages/diff.js'); -const highlightJsApache = require('highlight.js/lib/languages/apache.js'); -const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js'); -const highlightJsHttp = require('highlight.js/lib/languages/http.js'); -const highlightJsJson = require('highlight.js/lib/languages/json.js'); -const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js'); -const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js'); -const highlightJsCss = require('highlight.js/lib/languages/css.js'); -const highlightJsNginx = require('highlight.js/lib/languages/nginx.js'); -const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js'); -const highlightJsPython = require('highlight.js/lib/languages/python.js'); -const highlightJsXml = require('highlight.js/lib/languages/xml.js'); -const highlightJsPerl = require('highlight.js/lib/languages/perl.js'); -const highlightJsBash = require('highlight.js/lib/languages/bash.js'); -const highlightJsPhp = require('highlight.js/lib/languages/php.js'); -const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js'); -const highlightJsCs = require('highlight.js/lib/languages/cs.js'); -const highlightJsCpp = require('highlight.js/lib/languages/cpp.js'); -const highlightJsSql = require('highlight.js/lib/languages/sql.js'); -const highlightJsGo = require('highlight.js/lib/languages/go.js'); -const highlightJsRuby = require('highlight.js/lib/languages/ruby.js'); -const highlightJsJava = require('highlight.js/lib/languages/java.js'); -const highlightJsIni = require('highlight.js/lib/languages/ini.js'); - -const Constants = require('../utils/constants.jsx'); -const HighlightedLanguages = Constants.HighlightedLanguages; - export class MattermostMarkdownRenderer extends marked.Renderer { constructor(options, formattingOptions = {}) { super(options); @@ -43,43 +15,6 @@ export class MattermostMarkdownRenderer extends marked.Renderer { this.text = this.text.bind(this); this.formattingOptions = formattingOptions; - - highlightJs.registerLanguage('diff', highlightJsDiff); - highlightJs.registerLanguage('apache', highlightJsApache); - highlightJs.registerLanguage('makefile', highlightJsMakefile); - highlightJs.registerLanguage('http', highlightJsHttp); - highlightJs.registerLanguage('json', highlightJsJson); - highlightJs.registerLanguage('markdown', highlightJsMarkdown); - highlightJs.registerLanguage('javascript', highlightJsJavascript); - highlightJs.registerLanguage('css', highlightJsCss); - highlightJs.registerLanguage('nginx', highlightJsNginx); - highlightJs.registerLanguage('objectivec', highlightJsObjectivec); - highlightJs.registerLanguage('python', highlightJsPython); - highlightJs.registerLanguage('xml', highlightJsXml); - highlightJs.registerLanguage('perl', highlightJsPerl); - highlightJs.registerLanguage('bash', highlightJsBash); - highlightJs.registerLanguage('php', highlightJsPhp); - highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript); - highlightJs.registerLanguage('cs', highlightJsCs); - highlightJs.registerLanguage('cpp', highlightJsCpp); - highlightJs.registerLanguage('sql', highlightJsSql); - highlightJs.registerLanguage('go', highlightJsGo); - highlightJs.registerLanguage('ruby', highlightJsRuby); - highlightJs.registerLanguage('java', highlightJsJava); - highlightJs.registerLanguage('ini', highlightJsIni); - } - - code(code, language) { - if (!language || highlightJs.listLanguages().indexOf(language) < 0) { - let parsed = super.code(code, language); - return '<code class="hljs">' + $(parsed).text() + '</code>'; - } - - let parsed = highlightJs.highlight(language, code); - return '<div class="post-body--code">' + - '<span class="post-body--code__language">' + HighlightedLanguages[language] + '</span>' + - '<code style="white-space: pre;" class="hljs">' + parsed.value + '</code>' + - '</div>'; } br() { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index fadab27a7..35ce49ae2 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -211,11 +211,15 @@ export function displayDateTime(ticks) { } interval = Math.floor(seconds / 60); - if (interval > 1) { + if (interval >= 2) { return interval + ' minutes ago'; } - return '1 minute ago'; + if (interval >= 1) { + return '1 minute ago'; + } + + return 'just now'; } export function displayCommentDateTime(ticks) { @@ -227,46 +231,62 @@ export function getTimestamp() { return Date.now(); } -function testUrlMatch(text) { - var urlMatcher = new Autolinker.matchParser.MatchParser({ +// extracts links not styled by Markdown +export function extractLinks(text) { + const urlMatcher = new Autolinker.matchParser.MatchParser({ urls: true, emails: false, twitter: false, phone: false, hashtag: false }); - var result = []; + const links = []; + let replaceText = text; + + // pull out the Markdown code blocks + const codeBlocks = []; + const splitText = replaceText.split('`'); // also handles ``` + for (let i = 1; i < splitText.length; i += 2) { + if (splitText[i].trim() !== '') { + codeBlocks.push(splitText[i]); + } + } + function replaceFn(match) { - var linkData = {}; - var matchText = match.getMatchedText(); + let link = ''; + const matchText = match.getMatchedText(); + const tempText = replaceText; + + const start = replaceText.indexOf(matchText); + const end = start + matchText.length; + + replaceText = replaceText.substring(0, start) + replaceText.substring(end); + + // if it's a Markdown link, just skip it + if (start > 1) { + if (tempText.charAt(start - 2) === ']' && tempText.charAt(start - 1) === '(' && tempText.charAt(end) === ')') { + return; + } + } + + // if it's in a Markdown code block, skip it + for (const i in codeBlocks) { + if (codeBlocks[i].indexOf(matchText) === 0) { + codeBlocks[i] = codeBlocks[i].replace(matchText, ''); + return; + } + } - linkData.text = matchText; if (matchText.trim().indexOf('http') === 0) { - linkData.link = matchText; + link = matchText; } else { - linkData.link = 'http://' + matchText; + link = 'http://' + matchText; } - result.push(linkData); + links.push(link); } urlMatcher.replace(text, replaceFn, this); - return result; -} - -export function extractLinks(text) { - var repRegex = new RegExp('<br>', 'g'); - var matches = testUrlMatch(text.replace(repRegex, '\n')); - - if (!matches.length) { - return {links: null, text: text}; - } - - var links = []; - for (var i = 0; i < matches.length; i++) { - links.push(matches[i].link); - } - - return {links: links, text: text}; + return {links, text}; } export function escapeRegExp(string) { @@ -404,11 +424,6 @@ export function toTitleCase(str) { } export function applyTheme(theme) { - if (!theme.codeTheme) { - theme.codeTheme = Constants.DEFAULT_CODE_THEME; - } - updateCodeTheme(theme.codeTheme); - if (theme.sidebarBg) { changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } @@ -439,6 +454,8 @@ export function applyTheme(theme) { if (theme.sidebarTextActiveColor) { changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1); + changeCss('.search-help-popover .search-autocomplete__item:hover', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.05), 1); + changeCss('.search-help-popover .search-autocomplete__item.selected', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.15), 1); } if (theme.sidebarHeaderBg) { @@ -595,27 +612,6 @@ export function rgb2hex(rgbIn) { return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); } -export function updateCodeTheme(theme) { - const path = '/static/css/highlight/' + theme + '.css'; - const $link = $('link.code_theme'); - if (path !== $link.attr('href')) { - changeCss('code.hljs', 'visibility: hidden'); - var xmlHTTP = new XMLHttpRequest(); - xmlHTTP.open('GET', path, true); - xmlHTTP.onload = function onLoad() { - $link.attr('href', path); - if (isBrowserFirefox()) { - $link.one('load', () => { - changeCss('code.hljs', 'visibility: visible'); - }); - } else { - changeCss('code.hljs', 'visibility: visible'); - } - }; - xmlHTTP.send(); - } -} - export function placeCaretAtEnd(el) { el.focus(); if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') { |