diff options
Diffstat (limited to 'webapp')
25 files changed, 429 insertions, 357 deletions
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx index 00cbbdb0c..d46732d40 100644 --- a/webapp/components/admin_console/admin_navbar_dropdown.jsx +++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx @@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'; import TeamStore from 'stores/team_store.jsx'; import Constants from 'utils/constants.jsx'; +import AboutBuildModal from 'components/about_build_modal.jsx'; import {sortTeamsByDisplayName} from 'utils/team_utils.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; @@ -22,10 +23,13 @@ export default class AdminNavbarDropdown extends React.Component { super(props); this.blockToggle = false; this.onTeamChange = this.onTeamChange.bind(this); + this.handleAboutModal = this.handleAboutModal.bind(this); + this.aboutModalDismissed = this.aboutModalDismissed.bind(this); this.state = { teams: TeamStore.getAll(), - teamMembers: TeamStore.getMyTeamMembers() + teamMembers: TeamStore.getMyTeamMembers(), + showAboutModal: false }; } @@ -45,6 +49,16 @@ export default class AdminNavbarDropdown extends React.Component { TeamStore.removeChangeListener(this.onTeamChange); } + handleAboutModal(e) { + e.preventDefault(); + + this.setState({showAboutModal: true}); + } + + aboutModalDismissed() { + this.setState({showAboutModal: false}); + } + onTeamChange() { this.setState({ teams: TeamStore.getAll(), @@ -53,6 +67,7 @@ export default class AdminNavbarDropdown extends React.Component { } render() { + const config = global.window.mm_config; var teamsArray = []; // Array of team objects var teams = []; // Array of team components let switchTeams; @@ -138,6 +153,54 @@ export default class AdminNavbarDropdown extends React.Component { className='divider' /> <li> + <Link + to={config.AdministratorsGuideLink} + rel='noopener noreferrer' + target='_blank' + > + <FormattedMessage + id='admin.nav.administratorsGuide' + defaultMessage='Administrator Guide' + /> + </Link> + </li> + <li> + <Link + to={config.TroubleshootingForumLink} + rel='noopener noreferrer' + target='_blank' + > + <FormattedMessage + id='admin.nav.troubleshootingForum' + defaultMessage='Troubleshooting Forum' + /> + </Link> + </li> + <li> + <Link + to={config.CommercialSupportLink} + rel='noopener noreferrer' + target='_blank' + > + <FormattedMessage + id='admin.nav.commercialSupport' + defaultMessage='Commercial Support' + /> + </Link> + </li> + <li> + <a + href='#' + onClick={this.handleAboutModal} + > + <FormattedMessage + id='navbar_dropdown.about' + defaultMessage='About Mattermost' + /> + </a> + </li> + <li className='divider'/> + <li> <a href='#' id='logout' @@ -149,6 +212,10 @@ export default class AdminNavbarDropdown extends React.Component { /> </a> </li> + <AboutBuildModal + show={this.state.showAboutModal} + onModalDismissed={this.aboutModalDismissed} + /> </ul> </li> </ul> diff --git a/webapp/components/common/comment_icon.jsx b/webapp/components/common/comment_icon.jsx new file mode 100644 index 000000000..e8be773e5 --- /dev/null +++ b/webapp/components/common/comment_icon.jsx @@ -0,0 +1,55 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default function CommentIcon(props) { + let commentCountSpan = ''; + let iconStyle = 'comment-icon__container'; + if (props.commentCount > 0) { + iconStyle += ' icon--show'; + commentCountSpan = ( + <span className='comment-count'> + {props.commentCount} + </span> + ); + } else if (props.searchStyle !== '') { + iconStyle = iconStyle + ' ' + props.searchStyle; + } + + let commentIconId = props.idPrefix; + if (props.idCount > -1) { + commentIconId += props.idCount; + } + + return ( + <a + id={Utils.createSafeId(commentIconId)} + href='#' + className={iconStyle} + onClick={props.handleCommentClick} + > + <span + className='comment-icon' + dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}} + /> + {commentCountSpan} + </a> + ); +} + +CommentIcon.propTypes = { + idPrefix: React.PropTypes.string.isRequired, + idCount: React.PropTypes.number, + handleCommentClick: React.PropTypes.func.isRequired, + searchStyle: React.PropTypes.string, + commentCount: React.PropTypes.number +}; + +CommentIcon.defaultProps = { + idCount: -1, + searchStyle: '', + commentCount: 0 +};
\ No newline at end of file diff --git a/webapp/components/common/post_flag_icon.jsx b/webapp/components/common/post_flag_icon.jsx new file mode 100644 index 000000000..eb993d9f6 --- /dev/null +++ b/webapp/components/common/post_flag_icon.jsx @@ -0,0 +1,87 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {FormattedMessage} from 'react-intl'; +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; + +import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +function flagToolTip(isFlagged) { + return ( + <Tooltip id='flagTooltip'> + <FormattedMessage + id={isFlagged ? 'flag_post.unflag' : 'flag_post.flag'} + defaultMessage={isFlagged ? 'Unflag' : 'Flag for follow up'} + /> + </Tooltip> + ); +} + +function flagIcon() { + return ( + <span + className='icon' + dangerouslySetInnerHTML={{__html: Constants.FLAG_ICON_SVG}} + /> + ); +} + +export default function PostFlagIcon(props) { + function onFlagPost(e) { + e.preventDefault(); + flagPost(props.postId); + } + + function onUnflagPost(e) { + e.preventDefault(); + unflagPost(props.postId); + } + + const flagFunc = props.isFlagged ? onUnflagPost : onFlagPost; + const flagVisible = props.isFlagged ? 'visible' : ''; + + let flagIconId = null; + if (props.idCount > -1) { + flagIconId = Utils.createSafeId(props.idPrefix + props.idCount); + } + + if (!props.isEphemeral) { + return ( + <OverlayTrigger + key={'flagtooltipkey' + flagVisible} + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={flagToolTip(props.isFlagged)} + > + <a + id={flagIconId} + href='#' + className={'flag-icon__container ' + flagVisible} + onClick={flagFunc} + > + {flagIcon()} + </a> + </OverlayTrigger> + ); + } + + return null; +} + +PostFlagIcon.propTypes = { + idPrefix: React.PropTypes.string.isRequired, + idCount: React.PropTypes.number, + postId: React.PropTypes.string.isRequired, + isFlagged: React.PropTypes.bool.isRequired, + isEphemeral: React.PropTypes.bool +}; + +PostFlagIcon.defaultProps = { + idCount: -1, + postId: '', + isFlagged: false, + isEphemeral: false +}; diff --git a/webapp/components/error_bar.jsx b/webapp/components/error_bar.jsx index 94555ce04..97fbbdca0 100644 --- a/webapp/components/error_bar.jsx +++ b/webapp/components/error_bar.jsx @@ -151,7 +151,7 @@ export default class ErrorBar extends React.Component { message = ( <FormattedHTMLMessage id={ErrorBarTypes.LICENSE_EXPIRING} - defaultMessage='Enterprise license expires on {date}. <a href="{link}" target="_blank">Please renew.</a>' + defaultMessage='Enterprise license expires on {date}. <a href="{link}" target="_blank">Please renew</a>.' values={{ date: displayExpiryDate(), link: renewalLink @@ -162,7 +162,7 @@ export default class ErrorBar extends React.Component { message = ( <FormattedHTMLMessage id={ErrorBarTypes.LICENSE_EXPIRED} - defaultMessage='Enterprise license is expired and some features may be disabled. <a href="{link}" target="_blank">Please renew.</a>' + defaultMessage='Enterprise license is expired and some features may be disabled. <a href="{link}" target="_blank">Please renew</a>.' values={{ link: renewalLink }} diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx index 6426a6a2b..f4b2d0555 100644 --- a/webapp/components/invite_member_modal.jsx +++ b/webapp/components/invite_member_modal.jsx @@ -115,21 +115,20 @@ class InviteMemberModal extends React.Component { var valid = true; for (var i = 0; i < count; i++) { - var index = inviteIds[i]; var invite = {}; + var index = inviteIds[i]; invite.email = ReactDOM.findDOMNode(this.refs['email' + index]).value.trim(); - if (!invite.email || !utils.isEmail(invite.email)) { - emailErrors[index] = this.props.intl.formatMessage(holders.emailError); - valid = false; - } else { - emailErrors[index] = ''; - } - invite.firstName = ReactDOM.findDOMNode(this.refs['first_name' + index]).value.trim(); - invite.lastName = ReactDOM.findDOMNode(this.refs['last_name' + index]).value.trim(); - - invites.push(invite); + if (invite.email !== '' || index === 0) { + if (!invite.email || !utils.isEmail(invite.email)) { + emailErrors[index] = this.props.intl.formatMessage(holders.emailError); + valid = false; + } else { + emailErrors[index] = ''; + } + invites.push(invite); + } } this.setState({emailErrors, firstNameErrors, lastNameErrors}); diff --git a/webapp/components/more_direct_channels/more_direct_channels.jsx b/webapp/components/more_direct_channels/more_direct_channels.jsx index 768e802a3..16a45aa9a 100644 --- a/webapp/components/more_direct_channels/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels/more_direct_channels.jsx @@ -226,6 +226,7 @@ export default class MoreDirectChannels extends React.Component { > <ProfilePicture src={`${Client.getUsersRoute()}/${option.id}/image?time=${option.last_picture_update}`} + status={`${UserStore.getStatus(option.id)}`} width='32' height='32' /> diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index 8ca84aa20..38f95a85b 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -22,7 +22,7 @@ export default class Post extends Component { sameUser: PropTypes.bool, sameRoot: PropTypes.bool, hideProfilePic: PropTypes.bool, - isLastPost: PropTypes.bool, + lastPostCount: PropTypes.number, isLastComment: PropTypes.bool, shouldHighlight: PropTypes.bool, displayNameType: PropTypes.string, @@ -137,6 +137,10 @@ export default class Post extends Component { return true; } + if (nextProps.lastPostCount !== this.props.lastPostCount) { + return true; + } + return false; } @@ -288,6 +292,7 @@ export default class Post extends Component { ref='header' post={post} sameRoot={this.props.sameRoot} + lastPostCount={this.props.lastPostCount} commentCount={this.props.commentCount} handleCommentClick={this.handleCommentClick} handleDropdownOpened={this.handleDropdownOpened} @@ -306,7 +311,7 @@ export default class Post extends Component { post={post} currentUser={this.props.currentUser} sameRoot={this.props.sameRoot} - isLastPost={this.props.isLastPost} + lastPostCount={this.props.lastPostCount} parentPost={parentPost} handleCommentClick={this.handleCommentClick} compactDisplay={this.props.compactDisplay} diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx index 108605eea..2ad9f96d1 100644 --- a/webapp/components/post_view/components/post_body.jsx +++ b/webapp/components/post_view/components/post_body.jsx @@ -51,6 +51,10 @@ export default class PostBody extends React.Component { return true; } + if (nextProps.lastPostCount !== this.props.lastPostCount) { + return true; + } + return false; } @@ -164,7 +168,7 @@ export default class PostBody extends React.Component { > {loading} <PostMessageContainer - isLastPost={this.props.isLastPost} + lastPostCount={this.props.lastPostCount} post={this.props.post} /> </div> @@ -208,7 +212,7 @@ PostBody.propTypes = { currentUser: React.PropTypes.object.isRequired, parentPost: React.PropTypes.object, retryPost: React.PropTypes.func, - isLastPost: React.PropTypes.bool, + lastPostCount: React.PropTypes.number, handleCommentClick: React.PropTypes.func.isRequired, compactDisplay: React.PropTypes.bool, previewCollapsed: React.PropTypes.string, diff --git a/webapp/components/post_view/components/post_header.jsx b/webapp/components/post_view/components/post_header.jsx index 9de0b7e79..eccd092b5 100644 --- a/webapp/components/post_view/components/post_header.jsx +++ b/webapp/components/post_view/components/post_header.jsx @@ -79,6 +79,7 @@ export default class PostHeader extends React.Component { <li className='col'> <PostInfo post={post} + lastPostCount={this.props.lastPostCount} commentCount={this.props.commentCount} handleCommentClick={this.props.handleCommentClick} handleDropdownOpened={this.props.handleDropdownOpened} @@ -105,6 +106,7 @@ PostHeader.propTypes = { post: React.PropTypes.object.isRequired, user: React.PropTypes.object, currentUser: React.PropTypes.object.isRequired, + lastPostCount: React.PropTypes.number, commentCount: React.PropTypes.number.isRequired, isLastComment: React.PropTypes.bool.isRequired, handleCommentClick: React.PropTypes.func.isRequired, diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index 0cb8ff5ac..3833f5058 100644 --- a/webapp/components/post_view/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -5,15 +5,17 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import PostTime from './post_time.jsx'; +import PostFlagIcon from 'components/common/post_flag_icon.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import * as PostActions from 'actions/post_actions.jsx'; +import CommentIcon from 'components/common/comment_icon.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; -import {Tooltip, OverlayTrigger, Overlay} from 'react-bootstrap'; +import {Overlay} from 'react-bootstrap'; import EmojiPicker from 'components/emoji_picker/emoji_picker.jsx'; import React from 'react'; @@ -325,7 +327,11 @@ export default class PostInfo extends React.Component { render() { var post = this.props.post; - const flagIcon = Constants.FLAG_ICON_SVG; + + let idCount = -1; + if (this.props.lastPostCount >= 0 && this.props.lastPostCount < Constants.TEST_ID_COUNT) { + idCount = this.props.lastPostCount; + } this.canDelete = PostUtils.canDeletePost(post); this.canEdit = PostUtils.canEditPost(post, this.editDisableAction); @@ -337,30 +343,13 @@ export default class PostInfo extends React.Component { let comments = null; let react = null; if (!isEphemeral && !isPending && !isSystemMessage) { - let showCommentClass; - let commentCountText; - if (this.props.commentCount >= 1) { - showCommentClass = ' icon--show'; - commentCountText = this.props.commentCount; - } else { - showCommentClass = ''; - commentCountText = ''; - } - comments = ( - <a - href='#' - className={'comment-icon__container' + showCommentClass} - onClick={this.props.handleCommentClick} - > - <span - className='comment-icon' - dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}} - /> - <span className='comment-count'> - {commentCountText} - </span> - </a> + <CommentIcon + idPrefix={'commentIcon'} + idCount={idCount} + handleCommentClick={this.props.handleCommentClick} + commentCount={this.props.commentCount} + /> ); if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMOJI_PICKER_PREVIEW)) { @@ -420,64 +409,6 @@ export default class PostInfo extends React.Component { } } - let flag; - let flagFunc; - let flagVisible = ''; - let flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.flag' - defaultMessage='Flag for follow up' - /> - </Tooltip> - ); - if (this.props.isFlagged) { - flagVisible = 'visible'; - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - flagFunc = this.unflagPost; - flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.unflag' - defaultMessage='Unflag' - /> - </Tooltip> - ); - } else { - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - flagFunc = this.flagPost; - } - - let flagTrigger; - if (!isEphemeral) { - flagTrigger = ( - <OverlayTrigger - key={'flagtooltipkey' + flagVisible} - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={flagTooltip} - > - <a - href='#' - className={'flag-icon__container ' + flagVisible} - onClick={flagFunc} - > - {flag} - </a> - </OverlayTrigger> - ); - } - let pinnedBadge; if (post.is_pinned) { pinnedBadge = ( @@ -502,7 +433,13 @@ export default class PostInfo extends React.Component { /> {pinnedBadge} {this.state.showEmojiPicker} - {flagTrigger} + <PostFlagIcon + idPrefix={'centerPostFlag'} + idCount={idCount} + postId={post.id} + isFlagged={this.props.isFlagged} + isEphemeral={isEphemeral} + /> </li> {options} </ul> @@ -518,6 +455,7 @@ PostInfo.defaultProps = { }; PostInfo.propTypes = { post: React.PropTypes.object.isRequired, + lastPostCount: React.PropTypes.number, commentCount: React.PropTypes.number.isRequired, isLastComment: React.PropTypes.bool.isRequired, handleCommentClick: React.PropTypes.func.isRequired, diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index 483ff78c8..f233884ac 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -339,7 +339,7 @@ export default class PostList extends React.Component { <Post key={keyPrefix + 'postKey'} ref={post.id} - isLastPost={i === 0} + lastPostCount={(i >= 0 && i < Constants.TEST_ID_COUNT) ? i : -1} sameUser={sameUser} sameRoot={sameRoot} post={post} diff --git a/webapp/components/post_view/components/post_message_container.jsx b/webapp/components/post_view/components/post_message_container.jsx index 5d324ba46..4cb3cb76c 100644 --- a/webapp/components/post_view/components/post_message_container.jsx +++ b/webapp/components/post_view/components/post_message_container.jsx @@ -16,7 +16,7 @@ export default class PostMessageContainer extends React.Component { static propTypes = { post: React.PropTypes.object.isRequired, options: React.PropTypes.object, - isLastPost: React.PropTypes.bool + lastPostCount: React.PropTypes.number }; static defaultProps = { @@ -91,7 +91,7 @@ export default class PostMessageContainer extends React.Component { <PostMessageView options={this.props.options} post={this.props.post} - isLastPost={this.props.isLastPost} + lastPostCount={this.props.lastPostCount} emojis={this.state.emojis} enableFormatting={this.state.enableFormatting} mentionKeys={this.state.mentionKeys} diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/components/post_message_view.jsx index 5b0790f36..d6610f813 100644 --- a/webapp/components/post_view/components/post_message_view.jsx +++ b/webapp/components/post_view/components/post_message_view.jsx @@ -22,7 +22,7 @@ export default class PostMessageView extends React.Component { usernameMap: React.PropTypes.object.isRequired, channelNamesMap: React.PropTypes.object.isRequired, team: React.PropTypes.object.isRequired, - isLastPost: React.PropTypes.bool + lastPostCount: React.PropTypes.number }; shouldComponentUpdate(nextProps) { @@ -55,6 +55,10 @@ export default class PostMessageView extends React.Component { return true; } + if (nextProps.lastPostCount !== this.props.lastPostCount) { + return true; + } + // Don't check if props.usernameMap changes since it is very large and inefficient to do so. // This mimics previous behaviour, but could be changed if we decide it's worth it. // The same choice (and reasoning) is also applied to the this.props.channelNamesMap. @@ -111,10 +115,15 @@ export default class PostMessageView extends React.Component { return <div>{renderedSystemMessage}</div>; } + let postId = null; + if (this.props.lastPostCount >= 0) { + postId = Utils.createSafeId('lastPostMessageText' + this.props.lastPostCount); + } + return ( <div> <span - id={this.props.isLastPost ? 'lastPostMessageText' : null} + id={postId} className='post-message__text' onClick={Utils.handleFormattedTextClick} dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, options)}} diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index fb0972804..88e8c1ca6 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -8,6 +8,7 @@ import PostMessageContainer from 'components/post_view/components/post_message_c import ProfilePicture from 'components/profile_picture.jsx'; import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx'; import RhsDropdown from 'components/rhs_dropdown.jsx'; +import PostFlagIcon from 'components/common/post_flag_icon.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {flagPost, unflagPost, pinPost, unpinPost, addReaction} from 'actions/post_actions.jsx'; @@ -19,7 +20,7 @@ import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; -import {Tooltip, OverlayTrigger, Overlay} from 'react-bootstrap'; +import {Overlay} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; @@ -128,6 +129,10 @@ export default class RhsComment extends React.Component { return true; } + if (nextProps.lastPostCount !== this.props.lastPostCount) { + return true; + } + return false; } @@ -384,9 +389,13 @@ export default class RhsComment extends React.Component { render() { const post = this.props.post; - const flagIcon = Constants.FLAG_ICON_SVG; const mattermostLogo = Constants.MATTERMOST_ICON_SVG; + let idCount = -1; + if (this.props.lastPostCount >= 0 && this.props.lastPostCount < Constants.TEST_ID_COUNT) { + idCount = this.props.lastPostCount; + } + const isEphemeral = Utils.isPostEphemeral(post); const isPending = post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING; const isSystemMessage = PostUtils.isSystemMessage(post); @@ -523,64 +532,6 @@ export default class RhsComment extends React.Component { ); } - let flag; - let flagFunc; - let flagVisible = ''; - let flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.flag' - defaultMessage='Flag for follow up' - /> - </Tooltip> - ); - if (this.props.isFlagged) { - flagVisible = 'visible'; - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - flagFunc = this.unflagPost; - flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.unflag' - defaultMessage='Unflag' - /> - </Tooltip> - ); - } else { - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - flagFunc = this.flagPost; - } - - let flagTrigger; - if (!isEphemeral) { - flagTrigger = ( - <OverlayTrigger - key={'commentflagtooltipkey' + flagVisible} - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={flagTooltip} - > - <a - href='#' - className={'flag-icon__container ' + flagVisible} - onClick={flagFunc} - > - {flag} - </a> - </OverlayTrigger> - ); - } - let react; let reactOverlay; @@ -668,7 +619,13 @@ export default class RhsComment extends React.Component { <li className='col'> {this.renderTimeTag(post, timeOptions)} {pinnedBadge} - {flagTrigger} + <PostFlagIcon + idPrefix={'rhsCommentFlag'} + idCount={idCount} + postId={post.id} + isFlagged={this.props.isFlagged} + isEphemeral={isEphemeral} + /> </li> {options} </ul> @@ -689,6 +646,7 @@ export default class RhsComment extends React.Component { RhsComment.propTypes = { post: React.PropTypes.object, + lastPostCount: React.PropTypes.number, user: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired, compactDisplay: React.PropTypes.bool, diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 65bc52f73..bf9748636 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -8,6 +8,7 @@ import FileAttachmentListContainer from './file_attachment_list_container.jsx'; import ProfilePicture from 'components/profile_picture.jsx'; import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx'; import RhsDropdown from 'components/rhs_dropdown.jsx'; +import PostFlagIcon from 'components/common/post_flag_icon.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; @@ -24,7 +25,7 @@ import ReactDOM from 'react-dom'; import Constants from 'utils/constants.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; -import {Tooltip, OverlayTrigger, Overlay} from 'react-bootstrap'; +import {Overlay} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; @@ -203,7 +204,6 @@ export default class RhsRootPost extends React.Component { const mattermostLogo = Constants.MATTERMOST_ICON_SVG; var timestamp = user ? user.last_picture_update : 0; var channel = ChannelStore.get(post.channel_id); - const flagIcon = Constants.FLAG_ICON_SVG; this.canDelete = PostUtils.canDeletePost(post); this.canEdit = PostUtils.canEditPost(post, this.editDisableAction); @@ -530,44 +530,6 @@ export default class RhsRootPost extends React.Component { const profilePicContainer = (<div className='post__img'>{profilePic}</div>); - let flag; - let flagFunc; - let flagVisible = ''; - let flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.flag' - defaultMessage='Flag for follow up' - /> - </Tooltip> - ); - if (this.props.isFlagged) { - flagVisible = 'visible'; - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - flagFunc = this.unflagPost; - flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.unflag' - defaultMessage='Unflag' - /> - </Tooltip> - ); - } else { - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - flagFunc = this.flagPost; - } - let pinnedBadge; if (post.is_pinned) { pinnedBadge = ( @@ -601,20 +563,11 @@ export default class RhsRootPost extends React.Component { <li className='col'> {this.renderTimeTag(post, timeOptions)} {pinnedBadge} - <OverlayTrigger - key={'rootpostflagtooltipkey' + flagVisible} - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={flagTooltip} - > - <a - href='#' - className={'flag-icon__container ' + flagVisible} - onClick={flagFunc} - > - {flag} - </a> - </OverlayTrigger> + <PostFlagIcon + idPrefix={'rhsRootPostFlag'} + postId={post.id} + isFlagged={this.props.isFlagged} + /> </li> <li className='col col__reply'> {reactOverlay} @@ -645,6 +598,7 @@ RhsRootPost.defaultProps = { }; RhsRootPost.propTypes = { post: React.PropTypes.object.isRequired, + lastPostCount: React.PropTypes.number, user: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired, commentCount: React.PropTypes.number, diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx index 1b4eb720f..82e54f6ff 100644 --- a/webapp/components/rhs_thread.jsx +++ b/webapp/components/rhs_thread.jsx @@ -321,18 +321,15 @@ export default class RhsThread extends React.Component { } render() { - const postsArray = this.state.postsArray; - const selected = this.state.selected; - const profiles = this.state.profiles || {}; - - if (postsArray == null || selected == null) { + if (this.state.postsArray == null || this.state.selected == null) { return ( <div/> ); } - const rootPostDay = Utils.getDateForUnixTicks(selected.create_at); - let previousPostDay = rootPostDay; + const postsArray = this.state.postsArray; + const selected = this.state.selected; + const profiles = this.state.profiles || {}; let profile; if (UserStore.getCurrentId() === selected.user_id) { @@ -351,8 +348,12 @@ export default class RhsThread extends React.Component { rootStatus = this.state.statuses[selected.user_id] || 'offline'; } + const rootPostDay = Utils.getDateForUnixTicks(selected.create_at); + let previousPostDay = rootPostDay; + const commentsLists = []; - for (let i = 0; i < postsArray.length; i++) { + const postsLength = postsArray.length; + for (let i = 0; i < postsLength; i++) { const comPost = postsArray[i]; let p; if (UserStore.getCurrentId() === comPost.user_id) { @@ -371,10 +372,7 @@ export default class RhsThread extends React.Component { status = this.state.statuses[p.id] || 'offline'; } - const keyPrefix = comPost.id ? comPost.id : comPost.pending_post_id; - const currentPostDay = Utils.getDateForUnixTicks(comPost.create_at); - if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { previousPostDay = currentPostDay; commentsLists.push( @@ -383,11 +381,14 @@ export default class RhsThread extends React.Component { />); } + const keyPrefix = comPost.id ? comPost.id : comPost.pending_post_id; + const reverseCount = postsLength - i - 1; commentsLists.push( <div key={keyPrefix + 'commentKey'}> <Comment ref={comPost.id} post={comPost} + lastPostCount={(reverseCount >= 0 && reverseCount < Constants.TEST_ID_COUNT) ? reverseCount : -1} user={p} currentUser={this.props.currentUser} compactDisplay={this.state.compactDisplay} @@ -431,12 +432,12 @@ export default class RhsThread extends React.Component { className='post-right__scroll' > <DateSeparator - date={rootPostDay.toDateString()} + date={rootPostDay} /> <RootPost ref={selected.id} post={selected} - commentCount={postsArray.length} + commentCount={postsLength} user={profile} currentUser={this.props.currentUser} compactDisplay={this.state.compactDisplay} @@ -456,7 +457,7 @@ export default class RhsThread extends React.Component { <CreateComment channelId={selected.channel_id} rootId={selected.id} - latestPostId={postsArray.length > 0 ? postsArray[postsArray.length - 1].id : selected.id} + latestPostId={postsLength > 0 ? postsArray[postsLength - 1].id : selected.id} /> </div> </div> diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx index 23f7d1762..1ed8445f8 100644 --- a/webapp/components/search_bar.jsx +++ b/webapp/components/search_bar.jsx @@ -248,6 +248,8 @@ export default class SearchBar extends React.Component { let mentionBtn; let flagBtn; if (this.props.showMentionFlagBtns) { + var mentionBtnClass = SearchStore.isMentionSearch ? 'active' : ''; + mentionBtn = ( <div className='dropdown channel-header__links' @@ -262,6 +264,7 @@ export default class SearchBar extends React.Component { href='#' type='button' onClick={this.searchMentions} + className={mentionBtnClass} > {'@'} </a> @@ -269,6 +272,8 @@ export default class SearchBar extends React.Component { </div> ); + var flagBtnClass = SearchStore.isFlaggedPosts ? 'active' : ''; + flagBtn = ( <div className='dropdown channel-header__links' @@ -283,6 +288,7 @@ export default class SearchBar extends React.Component { href='#' type='button' onClick={this.getFlagged} + className={flagBtnClass} > <span className='icon icon__flag' diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx index 682b04e2a..64e5a7c93 100644 --- a/webapp/components/search_results.jsx +++ b/webapp/components/search_results.jsx @@ -267,7 +267,7 @@ export default class SearchResults extends React.Component { </div> ); } else { - ctls = results.order.map(function mymap(id) { + ctls = results.order.map(function searchResults(id, idx, arr) { const post = results.posts[id]; let profile; if (UserStore.getCurrentId() === post.user_id) { @@ -285,12 +285,16 @@ export default class SearchResults extends React.Component { if (this.state.flaggedPosts) { isFlagged = this.state.flaggedPosts.get(post.id) === 'true'; } + + const reverseCount = arr.length - idx - 1; + return ( <SearchResultsItem key={post.id} channel={this.state.channels.get(post.channel_id)} compactDisplay={this.state.compactDisplay} post={post} + lastPostCount={(reverseCount >= 0 && reverseCount < Constants.TEST_ID_COUNT) ? reverseCount : -1} user={profile} term={searchTerm} isMentionSearch={this.props.isMentionSearch} diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index 09ea8c427..d3b7cfe00 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -6,6 +6,7 @@ import PostMessageContainer from 'components/post_view/components/post_message_c import UserProfile from './user_profile.jsx'; import FileAttachmentListContainer from './file_attachment_list_container.jsx'; import ProfilePicture from './profile_picture.jsx'; +import CommentIcon from 'components/common/comment_icon.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; @@ -13,12 +14,12 @@ import UserStore from 'stores/user_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import PostFlagIcon from 'components/common/post_flag_icon.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; -import {Tooltip, OverlayTrigger} from 'react-bootstrap'; const ActionTypes = Constants.ActionTypes; import React from 'react'; @@ -114,7 +115,11 @@ export default class SearchResultsItem extends React.Component { const timestamp = UserStore.getCurrentUser().last_picture_update; const user = this.props.user || {}; const post = this.props.post; - const flagIcon = Constants.FLAG_ICON_SVG; + + let idCount = -1; + if (this.props.lastPostCount >= 0 && this.props.lastPostCount < Constants.TEST_ID_COUNT) { + idCount = this.props.lastPostCount; + } if (channel) { channelName = channel.display_name; @@ -185,73 +190,23 @@ export default class SearchResultsItem extends React.Component { </p> ); } else { - let flag; - let flagFunc; - let flagVisible = ''; - let flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.flag' - defaultMessage='Flag for follow up' - /> - </Tooltip> - ); - - if (this.props.isFlagged) { - flagVisible = 'visible'; - flagTooltip = ( - <Tooltip id='flagTooltip'> - <FormattedMessage - id='flag_post.unflag' - defaultMessage='Unflag' - /> - </Tooltip> - ); - flagFunc = this.unflagPost; - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - } else { - flag = ( - <span - className='icon' - dangerouslySetInnerHTML={{__html: flagIcon}} - /> - ); - flagFunc = this.flagPost; - } - flagContent = ( - <OverlayTrigger - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={flagTooltip} - > - <a - href='#' - className={'flag-icon__container ' + flagVisible} - onClick={flagFunc} - > - {flag} - </a> - </OverlayTrigger> + <PostFlagIcon + idPrefix={'searchPostFlag'} + idCount={idCount} + postId={post.id} + isFlagged={this.props.isFlagged} + /> ); rhsControls = ( <li className='col__controls'> - <a - href='#' - className='comment-icon__container search-item__comment' - onClick={this.handleFocusRHSClick} - > - <span - className='comment-icon' - dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}} - /> - </a> + <CommentIcon + idPrefix={'searchCommentIcon'} + idCount={idCount} + handleCommentClick={this.handleFocusRHSClick} + searchStyle={'search-item__comment'} + /> <a onClick={ () => { @@ -364,6 +319,7 @@ export default class SearchResultsItem extends React.Component { SearchResultsItem.propTypes = { post: React.PropTypes.object, + lastPostCount: React.PropTypes.number, user: React.PropTypes.object, channel: React.PropTypes.object, compactDisplay: React.PropTypes.bool, diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index f92bd13f0..fd3c2ef85 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -545,10 +545,13 @@ "admin.mfa.bannerDesc": "<a href='https://docs.mattermost.com/deployment/auth.html' target='_blank'>Multi-factor authentication</a> is available for accounts with AD/LDAP or email login. If other login methods are used, MFA should be configured with the authentication provider.", "admin.mfa.cluster": "High", "admin.mfa.title": "Multi-factor Authentication", + "admin.nav.administratorsGuide": "Administrator's Guide", + "admin.nav.commercialSupport": "Commercial Support", "admin.nav.help": "Help", "admin.nav.logout": "Logout", "admin.nav.report": "Report a Problem", "admin.nav.switch": "Team Selection", + "admin.nav.troubleshootingForum": "Troubleshooting Forum", "admin.notifications.email": "Email", "admin.notifications.push": "Mobile Push", "admin.notifications.title": "Notification Settings", @@ -1313,8 +1316,8 @@ "error.not_found.link_message": "Back to Mattermost", "error.not_found.message": "The page you were trying to reach does not exist", "error.not_found.title": "Page not found", - "error_bar.expired": "Enterprise license is expired and some features may be disabled. <a href='{link}' target='_blank'>Please renew.</a>", - "error_bar.expiring": "Enterprise license expires on {date}. <a href='{link}' target='_blank'>Please renew.</a>", + "error_bar.expired": "Enterprise license is expired and some features may be disabled. <a href='{link}' target='_blank'>Please renew</a>.", + "error_bar.expiring": "Enterprise license expires on {date}. <a href='{link}' target='_blank'>Please renew</a>.", "error_bar.past_grace": "Enterprise license is expired and some features may be disabled. Please contact your System Administrator for details.", "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured", "error_bar.site_url": "Please configure your {docsLink} in the {link}.", diff --git a/webapp/root.jsx b/webapp/root.jsx index 94645b661..6c7643f17 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -101,6 +101,17 @@ function preRenderSetup(callwhendone) { } else { I18n.safariFix(afterIntl); } + + // Prevent drag and drop files from navigating away from the app + document.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + + document.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); } function renderRootComponent() { diff --git a/webapp/sass/layout/_headers.scss b/webapp/sass/layout/_headers.scss index 79bba495d..e4325c149 100644 --- a/webapp/sass/layout/_headers.scss +++ b/webapp/sass/layout/_headers.scss @@ -464,12 +464,20 @@ text-decoration: none; &:hover { - @include opacity(1); + @include opacity(0.7); } &:focus { color: inherit; } + + &.active { + color: $primary-color; + @include opacity(1); + .icon { + fill: $primary-color; + } + } } } diff --git a/webapp/sass/routes/_about-modal.scss b/webapp/sass/routes/_about-modal.scss index 4506eac4e..46c1676ea 100644 --- a/webapp/sass/routes/_about-modal.scss +++ b/webapp/sass/routes/_about-modal.scss @@ -1,9 +1,13 @@ @charset 'UTF-8'; -.app__body { +.modal { + .modal-content { + @include box-shadow(0 0 10px rgba($black, .5)); + border-radius: $border-rad; + } - .modal { - .about-modal { + .about-modal { + .modal-content { .modal-header { background: transparent; border: none; @@ -25,69 +29,68 @@ margin: 0 4px 0 8px; } } + } - .modal-body { - padding: 20px 25px 5px; - } + .modal-body { + padding: 20px 25px 10px; + } - &.large { - .modal-body { - padding-bottom: 20px; - } + &.large { + .modal-body { + padding-bottom: 20px; } + } - .about-modal__content { - @include clearfix; - @include display-flex; - @include flex-direction(row); - padding: 1em 0 3em; - } + .about-modal__content { + @include clearfix; + @include display-flex; + @include flex-direction(row); + padding: 1em 0 3em; + } - .about-modal__copyright { - @include opacity(.6); - margin-top: .5em; - } + .about-modal__copyright { + @include opacity(.6); + margin-top: .5em; + } - .about-modal__footer { - font-size: 13.5px; - } + .about-modal__footer { + font-size: 13.5px; + } - .about-modal__title { - line-height: 1.5; - margin: 0 0 10px; - } + .about-modal__title { + line-height: 1.5; + margin: 0 0 10px; + } - .about-modal__subtitle { - @include opacity(.6); - } + .about-modal__subtitle { + @include opacity(.6); + } - .about-modal__hash { - @include opacity(.4); - font-size: .75em; - text-align: right; + .about-modal__hash { + @include opacity(.4); + font-size: .75em; + text-align: right; - p { - &:first-child { - float: left; - text-align: left; - } + p { + &:first-child { + float: left; + text-align: left; } } + } - .about-modal__logo { - @include opacity(.9); - padding: 0 40px 0 20px; + .about-modal__logo { + @include opacity(.9); + padding: 0 40px 0 20px; - svg { - height: 125px; - width: 125px; - } + svg { + height: 125px; + width: 125px; + } - path { - fill: inherit; - } + path { + fill: inherit; } } } - -} +}
\ No newline at end of file diff --git a/webapp/stores/search_store.jsx b/webapp/stores/search_store.jsx index 2ccf02f94..dd4b97522 100644 --- a/webapp/stores/search_store.jsx +++ b/webapp/stores/search_store.jsx @@ -122,7 +122,7 @@ class SearchStoreClass extends EventEmitter { togglePinPost(postId, isPinned) { const results = this.getSearchResults(); - if (results == null) { + if (results == null || results.posts == null) { return; } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 2b6c5d6be..619d37a74 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -952,7 +952,8 @@ export const Constants = { AUTOCOMPLETE_TIMEOUT: 100, ANIMATION_TIMEOUT: 1000, SEARCH_TIMEOUT_MILLISECONDS: 100, - DIAGNOSTICS_SEGMENT_KEY: 'fwb7VPbFeQ7SKp3wHm1RzFUuXZudqVok' + DIAGNOSTICS_SEGMENT_KEY: 'fwb7VPbFeQ7SKp3wHm1RzFUuXZudqVok', + TEST_ID_COUNT: 10 }; export default Constants; |