diff options
Diffstat (limited to 'web')
25 files changed, 397 insertions, 175 deletions
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index d0565a0e0..238ace3da 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -18,6 +18,7 @@ export default class EmailSettings extends React.Component { this.state = { sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications, + sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications, saveNeeded: false, serverError: null, emailSuccess: null, @@ -36,6 +37,14 @@ export default class EmailSettings extends React.Component { s.sendEmailNotifications = false; } + if (action === 'sendPushNotifications_true') { + s.sendPushNotifications = true; + } + + if (action === 'sendPushNotifications_false') { + s.sendPushNotifications = false; + } + this.setState(s); } @@ -43,11 +52,12 @@ export default class EmailSettings extends React.Component { var config = this.props.config; config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked; config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked; + config.EmailSettings.SendPushlNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked; config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked; - config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked; config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim(); config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim(); config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim(); + config.EmailSettings.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).value.trim(); config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim(); config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim(); config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim(); @@ -526,6 +536,61 @@ export default class EmailSettings extends React.Component { </div> <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='sendPushNotifications' + > + {'Send Push Notifications: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='sendPushNotifications' + value='true' + ref='sendPushNotifications' + defaultChecked={this.props.config.EmailSettings.SendPushNotifications} + onChange={this.handleChange.bind(this, 'sendPushNotifications_true')} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='sendPushNotifications' + value='false' + defaultChecked={!this.props.config.EmailSettings.SendPushNotifications} + onChange={this.handleChange.bind(this, 'sendPushNotifications_false')} + /> + {'false'} + </label> + <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send iOS and Android push notifications through the push notification server.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='PushNotificationServer' + > + {'Push Notification Server:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='PushNotificationServer' + ref='PushNotificationServer' + placeholder='E.g.: "https://push.mattermost.com"' + defaultValue={this.props.config.EmailSettings.PushNotificationServer} + onChange={this.handleChange} + disabled={!this.state.sendPushNotifications} + /> + <p className='help-text'>{'Location of the push notification server.'}</p> + </div> + </div> + + <div className='form-group'> <div className='col-sm-12'> {serverError} <button diff --git a/web/react/components/channel_notifications_modal.jsx b/web/react/components/channel_notifications_modal.jsx index 79b769c8a..887589468 100644 --- a/web/react/components/channel_notifications_modal.jsx +++ b/web/react/components/channel_notifications_modal.jsx @@ -32,11 +32,13 @@ export default class ChannelNotificationsModal extends React.Component { activeSection: '' }; } - componentDidMount() { - ChannelStore.addChangeListener(this.onListenerChange); - } - componentWillUnmount() { - ChannelStore.removeChangeListener(this.onListenerChange); + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + this.onListenerChange(); + ChannelStore.addChangeListener(this.onListenerChange); + } else { + ChannelStore.removeChangeListener(this.onListenerChange); + } } onListenerChange() { const curChannelId = ChannelStore.getCurrentId(); diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index df5d6b8e1..fd20834f4 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -75,7 +75,7 @@ export default class GetLinkModal extends React.Component { onHide={this.onHide} > <Modal.Header closeButton={true}> - {this.props.title} + <h4 className='modal-title'>{this.props.title}</h4> </Modal.Header> <Modal.Body> {helpText} diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index 390d25f2e..f5d5ab28b 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -110,7 +110,7 @@ export default class MemberListItem extends React.Component { height='36' width='36' /> - <div className='member-name'>{member.username}</div> + <div className='member-name'>{Utils.displayUsername(member.id)}</div> <div className='member-description'>{member.email}</div> </td> <td className='td--action lg'>{invite}</td> diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 27fb6a4c1..316fad01a 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -174,7 +174,7 @@ export default class MemberListTeamItem extends React.Component { height='36' width='36' /> - <span className='member-name'>{Utils.getDisplayName(user)}</span> + <span className='member-name'>{Utils.displayUsername(user.id)}</span> <span className='member-email'>{email}</span> <div className='dropdown member-drop'> <a diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index b5000141a..f4cb542e4 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -5,6 +5,7 @@ import UserStore from '../stores/user_store.jsx'; var Popover = ReactBootstrap.Popover; var Overlay = ReactBootstrap.Overlay; import * as Utils from '../utils/utils.jsx'; +import Constants from '../utils/constants.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -68,7 +69,7 @@ export default class PopoverListMembers extends React.Component { } render() { - let popoverHtml = []; + const popoverHtml = []; const members = this.props.members; const teamMembers = UserStore.getProfilesUsernameMap(); const currentUserId = UserStore.getCurrentId(); @@ -76,35 +77,13 @@ export default class PopoverListMembers extends React.Component { if (members && teamMembers) { members.sort((a, b) => { - return a.username.localeCompare(b.username); + const aName = Utils.displayUsername(a.id); + const bName = Utils.displayUsername(b.id); + + return aName.localeCompare(bName); }); members.forEach((m, i) => { - const details = []; - - const fullName = Utils.getFullName(m); - if (fullName) { - details.push( - <span - key={`${m.id}__full-name`} - className='full-name' - > - {fullName} - </span> - ); - } - - if (m.nickname) { - const separator = fullName ? ' - ' : ''; - details.push( - <span - key={`${m.nickname}__nickname`} - > - {separator + m.nickname} - </span> - ); - } - let button = ''; if (currentUserId !== m.id && ch.type !== 'D') { button = ( @@ -118,7 +97,12 @@ export default class PopoverListMembers extends React.Component { ); } - if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) { + let name = ''; + if (teamMembers[m.username]) { + name = Utils.displayUsername(teamMembers[m.username].id); + } + + if (name && teamMembers[m.username].delete_at <= 0) { popoverHtml.push( <div className='text-nowrap' @@ -135,7 +119,7 @@ export default class PopoverListMembers extends React.Component { <div className='more-name' > - {m.username} + {name} </div> </div> <div @@ -157,8 +141,8 @@ export default class PopoverListMembers extends React.Component { count = members.length; } - if (count > 20) { - countText = '20+'; + if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) { + countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+'; } else if (count > 0) { countText = count.toString(); } diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 278261e22..66d8c507a 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -87,6 +87,10 @@ export default class Post extends React.Component { return true; } + if (nextProps.displayNameType !== this.props.displayNameType) { + return true; + } + if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) { return true; } @@ -224,5 +228,6 @@ Post.propTypes = { sameRoot: React.PropTypes.bool, hideProfilePic: React.PropTypes.bool, isLastComment: React.PropTypes.bool, - shouldHighlight: React.PropTypes.bool + shouldHighlight: React.PropTypes.bool, + displayNameType: React.PropTypes.string }; diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index 5e374b877..242b26b91 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -2,15 +2,18 @@ // See License.txt for license information. import UserStore from '../stores/user_store.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import * as Utils from '../utils/utils.jsx'; import Post from './post.jsx'; import Constants from '../utils/constants.jsx'; +const Preferences = Constants.Preferences; export default class PostsView extends React.Component { constructor(props) { super(props); + this.updateState = this.updateState.bind(this); this.handleScroll = this.handleScroll.bind(this); this.isAtBottom = this.isAtBottom.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); @@ -22,6 +25,8 @@ export default class PostsView extends React.Component { this.jumpToPostNode = null; this.wasAtBottom = true; this.scrollHeight = 0; + + this.state = {displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value}; } static get SCROLL_TYPE_FREE() { return 1; @@ -38,6 +43,9 @@ export default class PostsView extends React.Component { static get SCROLL_TYPE_POST() { return 5; } + updateState() { + this.setState({displayNameType: PreferenceStore.getPreference(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value}); + } isAtBottom() { return ((this.refs.postlist.scrollHeight - this.refs.postlist.scrollTop) === this.refs.postlist.clientHeight); } @@ -94,22 +102,53 @@ export default class PostsView extends React.Component { const prevPostIsComment = Utils.isComment(prevPost); const postFromWebhook = Boolean(post.props && post.props.from_webhook); const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook); + let prevWebhookName = ''; + if (prevPost.props && prevPost.props.override_username) { + prevWebhookName = prevPost.props.override_username; + } + let curWebhookName = ''; + if (post.props && post.props.override_username) { + curWebhookName = post.props.override_username; + } + + // consider posts from the same user if: + // the previous post was made by the same user as the current post, + // the previous post was made within 5 minutes of the current post, + // the previous post and current post are both from webhooks or both not, + // the previous post and current post have the same webhook usernames + if (prevPost.user_id === post.user_id && + post.create_at - prevPost.create_at <= 1000 * 60 * 5 && + postFromWebhook === prevPostFromWebhook && + prevWebhookName === curWebhookName) { + sameUser = true; + } - sameUser = prevPost.user_id === post.user_id && postFromWebhook === prevPostFromWebhook && - post.create_at - prevPost.create_at <= 1000 * 60 * 5; - sameRoot = (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) || (!postIsComment && !prevPostIsComment && sameUser); + // consider posts from the same root if: + // the current post is a comment, + // the current post has the same root as the previous post + if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) { + sameRoot = true; + } + + // consider posts from the same root if: + // the current post is not a comment, + // the previous post is not a comment, + // the previous post is from the same user + if (!postIsComment && !prevPostIsComment && sameUser) { + sameRoot = true; + } // hide the profile pic if: // the previous post was made by the same user as the current post, // the previous post is not a comment, // the current post is not a comment, - // the current post is not from a webhook - // and the previous post is not from a webhook - if ((prevPost.user_id === post.user_id) && + // the previous post and current post are both from webhooks or both not, + // the previous post and current post have the same webhook usernames + if (prevPost.user_id === post.user_id && !prevPostIsComment && !postIsComment && - !postFromWebhook && - !prevPostFromWebhook) { + postFromWebhook === prevPostFromWebhook && + prevWebhookName === curWebhookName) { hideProfilePic = true; } } @@ -135,6 +174,7 @@ export default class PostsView extends React.Component { isLastComment={isLastComment} shouldHighlight={shouldHighlight} onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + displayNameType={this.state.displayNameType} /> ); @@ -240,16 +280,20 @@ export default class PostsView extends React.Component { this.updateScrolling(); } window.addEventListener('resize', this.handleResize); + $(this.refs.postlist).perfectScrollbar(); + PreferenceStore.addChangeListener(this.updateState); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); + PreferenceStore.removeChangeListener(this.updateState); } componentDidUpdate() { if (this.props.postList != null) { this.updateScrolling(); } + $(this.refs.postlist).perfectScrollbar('update'); } - shouldComponentUpdate(nextProps) { + shouldComponentUpdate(nextProps, nextState) { if (this.props.isActive !== nextProps.isActive) { return true; } @@ -268,6 +312,9 @@ export default class PostsView extends React.Component { if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) { return true; } + if (nextState.displayNameType !== this.state.displayNameType) { + return true; + } return false; } @@ -326,7 +373,7 @@ export default class PostsView extends React.Component { return ( <div ref='postlist' - className={'post-list-holder-by-time ' + activeClass} + className={'ps-container post-list-holder-by-time ' + activeClass} onScroll={this.handleScroll} > <div className='post-list__table'> diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 367d3687e..6d6694fec 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -99,10 +99,12 @@ export default class PostsViewContainer extends React.Component { if (newIndex === -1) { newIndex = channels.length; channels.push(channelId); - postLists[newIndex] = this.getChannelPosts(channelId); atTop[newIndex] = PostStore.getVisibilityAtTop(channelId); } + // make sure we have the latest posts from the store + postLists[newIndex] = this.getChannelPosts(channelId); + this.setState({ currentChannelIndex: newIndex, currentLastViewed: lastViewed, diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx index 100600c4b..f49b33f73 100644 --- a/web/react/components/register_app_modal.jsx +++ b/web/react/components/register_app_modal.jsx @@ -2,21 +2,57 @@ // See License.txt for license information. import * as Client from '../utils/client.jsx'; +import ModalStore from '../stores/modal_store.jsx'; + +const Modal = ReactBootstrap.Modal; + +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; export default class RegisterAppModal extends React.Component { constructor() { super(); - this.register = this.register.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); this.onHide = this.onHide.bind(this); this.save = this.save.bind(this); + this.updateShow = this.updateShow.bind(this); - this.state = {clientId: '', clientSecret: '', saved: false}; + this.state = { + clientId: '', + clientSecret: '', + saved: false, + show: false + }; } componentDidMount() { - $(ReactDOM.findDOMNode(this)).on('hide.bs.modal', this.onHide); + ModalStore.addModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow); + } + componentWillUnmount() { + ModalStore.removeModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow); + } + updateShow(show) { + if (!show) { + if (this.state.clientId !== '' && !this.state.saved) { + return; + } + + this.setState({ + clientId: '', + clientSecret: '', + saved: false, + homepageError: null, + callbackError: null, + serverError: null, + nameError: null + }); + } + + this.setState({show}); } - register() { + handleSubmit(e) { + e.preventDefault(); + var state = this.state; state.serverError = null; @@ -94,6 +130,7 @@ export default class RegisterAppModal extends React.Component { } var body = ''; + var footer = ''; if (this.state.clientId === '') { body = ( <div className='settings-modal'> @@ -148,24 +185,29 @@ export default class RegisterAppModal extends React.Component { </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> </div> ); + + footer = ( + <div> + <button + type='button' + className='btn btn-default' + onClick={() => this.updateShow(false)} + > + {'Cancel'} + </button> + <button + onClick={this.handleSubmit} + type='submit' + className='btn btn-primary' + tabIndex='3' + > + {'Register'} + </button> + </div> + ); } else { var btnClass = ' disabled'; if (this.state.saved) { @@ -173,17 +215,35 @@ export default class RegisterAppModal extends React.Component { } body = ( - <div className='form-group user-settings'> - <h3>{'Your Application Credentials'}</h3> - <br/> - <br/> - <label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label> - <label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label> + <div className='form-horizontal user-settings'> + <h4 className='padding-bottom x3'>{'Your Application Credentials'}</h4> <br/> + <div className='row'> + <label className='col-sm-4 control-label'>{'Client ID'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + value={this.state.clientId} + readOnly='true' + /> + </div> + </div> <br/> + <div className='row padding-top x2'> + <label className='col-sm-4 control-label'>{'Client Secret'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + value={this.state.clientSecret} + readOnly='true' + /> + </div> + </div> <br/> <br/> - <strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong> + <strong>{'Save these somewhere SAFE and SECURE. Treat your Client ID as your app\'s username and your Client Secret as the app\'s password.'}</strong> <br/> <br/> <div className='checkbox'> @@ -192,56 +252,50 @@ export default class RegisterAppModal extends React.Component { ref='save' type='checkbox' checked={this.state.saved} - onClick={this.save} - > - {'I have saved both my Client Id and Client Secret somewhere safe'} - </input> + onChange={this.save} + /> + {'I have saved both my Client Id and Client Secret somewhere safe'} </label> </div> - <a - className={'btn btn-sm btn-primary pull-right' + btnClass} - href='#' - data-dismiss='modal' - aria-label='Close' - > - {'Close'} - </a> </div> ); + + footer = ( + <a + className={'btn btn-sm btn-primary pull-right' + btnClass} + href='#' + onClick={(e) => { + e.preventDefault(); + this.updateShow(false); + }} + > + {'Close'} + </a> + ); } return ( - <div - className='modal fade' - ref='modal' - id='register_app' - role='dialog' - aria-hidden='true' - > - <div className='modal-dialog'> - <div className='modal-content'> - <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' - > - {'Developer Applications'} - </h4> - </div> - <div className='modal-body'> - {body} - </div> - </div> - </div> - </div> + <span> + <Modal + show={this.state.show} + onHide={() => this.updateShow(false)} + > + <Modal.Header closeButton={true}> + <Modal.Title>{'Developer Applications'}</Modal.Title> + </Modal.Header> + <form + role='form' + className='form-horizontal' + > + <Modal.Body> + {body} + </Modal.Body> + <Modal.Footer> + {footer} + </Modal.Footer> + </form> + </Modal> + </span> ); } } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 30422ff7d..5c8e73874 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -3,7 +3,6 @@ import NewChannelFlow from './new_channel_flow.jsx'; import MoreDirectChannels from './more_direct_channels.jsx'; -import SearchBox from './search_bar.jsx'; import SidebarHeader from './sidebar_header.jsx'; import UnreadChannelIndicator from './unread_channel_indicator.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; @@ -173,6 +172,10 @@ export default class Sidebar extends React.Component { this.updateScrollbar(); window.addEventListener('resize', this.handleResize); + + if ($(window).width() > 768) { + $('.nav-pills__container').perfectScrollbar(); + } } shouldComponentUpdate(nextProps, nextState) { if (!Utils.areObjectsEqual(nextState, this.state)) { @@ -582,7 +585,6 @@ export default class Sidebar extends React.Component { teamName={TeamStore.getCurrent().name} teamType={TeamStore.getCurrent().type} /> - <SearchBox /> <UnreadChannelIndicator show={this.state.showTopUnread} diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 438c0bc82..ea104fedb 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -54,9 +54,11 @@ export default class UserProfile extends React.Component { } } render() { - var name = this.state.profile.username; + var name = Utils.displayUsername(this.state.profile.id); if (this.props.overwriteName) { name = this.props.overwriteName; + } else if (!name) { + name = '...'; } if (this.props.disablePopover) { @@ -107,7 +109,7 @@ export default class UserProfile extends React.Component { rootClose={true} overlay={ <Popover - title={this.state.profile.username} + title={name} id='user-profile-popover' > {dataContent} diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 2d02c255a..01e13be57 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -3,16 +3,19 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; +import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; export default class DeveloperTab extends React.Component { constructor(props) { super(props); + this.register = this.register.bind(this); + this.state = {}; } register() { - $('#user_settings1').modal('hide'); - $('#register_app').modal('show'); + this.props.closeModal(); + EventHelpers.showRegisterAppModal(); } render() { var appSection; @@ -21,7 +24,10 @@ export default class DeveloperTab extends React.Component { var inputs = []; inputs.push( - <div className='form-group'> + <div + key='registerbtn' + className='form-group' + > <div className='col-sm-7'> <a className='btn btn-sm btn-primary' diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 91f4b3bdc..820f8fd8e 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -423,24 +423,29 @@ export default class ViewImageModal extends React.Component { onClick={this.props.onModalDismissed} > <div - className={'image-wrapper ' + bgClass} - onMouseEnter={this.onMouseEnterImage} - onMouseLeave={this.onMouseLeaveImage} - onClick={(e) => e.stopPropagation()} + className={'image-wrapper'} + onClick={this.props.onModalDismissed} > <div - className={closeButtonClass} - onClick={this.props.onModalDismissed} - /> - {content} - <ViewImagePopoverBar - show={this.state.showFooter} - fileId={this.state.imgId} - totalFiles={this.props.filenames.length} - filename={name} - fileURL={fileUrl} - getPublicLink={this.getPublicLink} - /> + className={bgClass} + onMouseEnter={this.onMouseEnterImage} + onMouseLeave={this.onMouseLeaveImage} + onClick={(e) => e.stopPropagation()} + > + <div + className={closeButtonClass} + onClick={this.props.onModalDismissed} + /> + {content} + <ViewImagePopoverBar + show={this.state.showFooter} + fileId={this.state.imgId} + totalFiles={this.props.filenames.length} + filename={name} + fileURL={fileUrl} + getPublicLink={this.getPublicLink} + /> + </div> </div> {leftArrow} {rightArrow} diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx index d7f255aaa..856eec2f1 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/dispatcher/event_helpers.jsx @@ -104,3 +104,10 @@ export function showInviteMemberModal() { value: true }); } + +export function showRegisterAppModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, + value: true + }); +} diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx index a26a97f53..9f33cf022 100644 --- a/web/react/stores/modal_store.jsx +++ b/web/react/stores/modal_store.jsx @@ -35,6 +35,7 @@ class ModalStoreClass extends EventEmitter { case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL: case ActionTypes.TOGGLE_DELETE_POST_MODAL: case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL: + case ActionTypes.TOGGLE_REGISTER_APP_MODAL: this.emit(type, value, args); break; } diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index a8f0f9c63..2212edadb 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -211,7 +211,7 @@ class PostStoreClass extends EventEmitter { postList.order = this.postsInfo[id].pendingPosts.order.concat(postList.order); } - // Add delteted posts + // Add deleted posts if (this.postsInfo[id].hasOwnProperty('deletedPosts')) { Object.assign(postList.posts, this.postsInfo[id].deletedPosts); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 2009e07dd..99bd2453c 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -49,7 +49,8 @@ export default { TOGGLE_IMPORT_THEME_MODAL: null, TOGGLE_INVITE_MEMBER_MODAL: null, TOGGLE_DELETE_POST_MODAL: null, - TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null + TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null, + TOGGLE_REGISTER_APP_MODAL: null }), PayloadSources: keyMirror({ @@ -137,6 +138,7 @@ export default { ], MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], MAX_DMS: 20, + MAX_CHANNEL_POPOVER_COUNT: 20, DM_CHANNEL: 'D', OPEN_CHANNEL: 'O', PRIVATE_CHANNEL: 'P', diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 9d9bdfb7a..f2721c81d 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -223,6 +223,16 @@ class MattermostMarkdownRenderer extends marked.Renderer { return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`; } + listitem(text) { + const taskListReg = /^\[([ |xX])\] /; + const isTaskList = taskListReg.exec(text); + + if (isTaskList) { + return `<li>${'<input type="checkbox" disabled="disabled" ' + (isTaskList[1] === ' ' ? '' : 'checked="checked" ') + '/> '}${text.replace(taskListReg, '')}</li>`; + } + return `<li>${text}</li>`; + } + text(txt) { return TextFormatting.doFormatText(txt, this.formattingOptions); } diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 3a912fd75..f0bd46f9d 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -188,7 +188,7 @@ function highlightCurrentMentions(text, tokens) { const newAlias = `MM_SELFMENTION${index}`; newTokens.set(newAlias, { - value: `<span class='mention-highlight'>${alias}</span>` + token.extraText, + value: `<span class='mention-highlight'>${alias}</span>` + (token.extraText || ''), originalText: token.originalText }); output = output.replace(alias, newAlias); diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 80c377d7f..d6ed34e70 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -607,7 +607,7 @@ export function applyTheme(theme) { changeCss('.popover.right>.arrow:after, .tip-overlay.tip-overlay--sidebar .arrow, .tip-overlay.tip-overlay--header .arrow', 'border-right-color:' + theme.centerChannelBg, 1); changeCss('.popover.left>.arrow:after', 'border-left-color:' + theme.centerChannelBg, 1); changeCss('.popover.top>.arrow:after, .tip-overlay.tip-overlay--chat .arrow', 'border-top-color:' + theme.centerChannelBg, 1); - changeCss('.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1); + changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'background:' + theme.centerChannelBg, 1); changeCss('.attachment__content', 'background:' + theme.centerChannelBg, 1); } @@ -638,8 +638,7 @@ export function applyTheme(theme) { changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2); changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2); changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1); - changeCss('.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2); - changeCss('@media(max-width: 960px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1); + changeCss('@media(min-width: 960px){.search-bar__container .search__form .search-bar, .form-control', 'color:' + theme.centerChannelColor, 2); changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); changeCss('.attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); @@ -854,7 +853,7 @@ export function isMobile() { export function isComment(post) { if ('root_id' in post) { - return post.root_id !== ''; + return post.root_id !== '' && post.root_id != null; } return false; } @@ -981,13 +980,15 @@ export function displayUsername(userId) { const nameFormat = PreferenceStore.getPreference(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', {value: 'false'}).value; let username = ''; - if (nameFormat === 'nickname_full_name') { - username = user.nickname || getFullName(user); - } else if (nameFormat === 'full_name') { - username = getFullName(user); - } - if (!username.trim().length) { - username = user.username; + if (user) { + if (nameFormat === 'nickname_full_name') { + username = user.nickname || getFullName(user); + } else if (nameFormat === 'full_name') { + username = getFullName(user); + } + if (!username.trim().length) { + username = user.username; + } } return username; diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index 6270c8608..4a56bc6c7 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -3,6 +3,7 @@ } .modal-body { padding: 20px 15px; + overflow: auto; } .modal { width: 100%; @@ -47,9 +48,6 @@ margin-left: auto; margin-right: auto; } - .modal-body { - overflow: auto; - } .modal-push-down { margin-top: 5%; } @@ -195,21 +193,35 @@ width:100%; height: 100%; margin: 0 auto; + max-width: 100%; + + .modal-body { + @include clearfix; + height: 100%; + display: table; + table-layout: fixed; + width: 100%; + max-height: 100%; + } + .image-wrapper { position: relative; max-width: 90%; - min-height: 100px; - min-width: 320px; @include border-radius(3px); - display: table; - margin: 0 auto; + display: table-cell; + vertical-align: middle; + text-align: center; + width: 100%; + &:hover { @include border-radius(3px 3px 0 0); } + &.default { width: 100%; height: 80%; } + .modal-close { background: url("../images/close.png") no-repeat; @include background-size(100% 100%); @@ -225,24 +237,31 @@ transition: opacity 0.6s; cursor: pointer; z-index: 9999; + &.modal-close--show { @include opacity(1); } + } - > a { + + > div { + min-height: 100px; + min-width: 320px; background: #FFF; - display: table-cell; - vertical-align: middle; + display: inline-block; position: relative; &:hover .file-playback-controls.stop { @include opacity(1); } + } + img { max-width: 100%; max-height: 100%; } + .spinner.file__loading { z-index: 2; position: absolute; @@ -259,7 +278,6 @@ height: 100%; padding: 0; border: none; - display: table; } .image-body { vertical-align: middle; diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index b7a305427..ed1632681 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -607,6 +607,11 @@ body.ios { li ul, li ol { padding: 0 0 0 20px } + + li input[type="checkbox"]:disabled { + vertical-align: sub; + cursor: default; + } } } diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 00fa7d817..a56c4bb17 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -484,7 +484,8 @@ padding: 0 10px 0 31px; background: rgba(black, 0.2); @include border-radius(3px); - color: inherit; + color: #444; + background: #fff; } } } @@ -509,15 +510,15 @@ &.move--right { @include translate3d(0, 0, 0); } + > div { + padding-bottom: 0; + } .nav-pills__unread-indicator-bottom { bottom: 10px; } .badge { top: 13px; } - > div { - padding-bottom: 65px; - } .team__header { display: none; @include clearfix; @@ -621,8 +622,10 @@ .modal { .modal-image { .image-wrapper { - font-size: 12px; - min-width: 250px; + > div { + font-size: 12px; + min-width: 250px; + } .modal-close { @include opacity(1); } diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index 27b55f214..b7f658114 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -38,6 +38,7 @@ font-size: 14px; @include opacity(0.5); display: none; + color: #777; } .search__form { .search-bar__container & { |