diff options
Diffstat (limited to 'webapp/components')
40 files changed, 636 insertions, 163 deletions
diff --git a/webapp/components/admin_console/password_settings.jsx b/webapp/components/admin_console/password_settings.jsx index 3707977b8..43ec40904 100644 --- a/webapp/components/admin_console/password_settings.jsx +++ b/webapp/components/admin_console/password_settings.jsx @@ -39,16 +39,16 @@ export default class PasswordSettings extends AdminSettings { if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.PasswordRequirements === 'true') { let sampleErrorMsgId = 'user.settings.security.passwordError'; if (props.config.PasswordSettings.Lowercase) { - sampleErrorMsgId = sampleErrorMsgId + 'Lowercase'; + sampleErrorMsgId += 'Lowercase'; } if (props.config.PasswordSettings.Uppercase) { - sampleErrorMsgId = sampleErrorMsgId + 'Uppercase'; + sampleErrorMsgId += 'Uppercase'; } if (props.config.PasswordSettings.Number) { - sampleErrorMsgId = sampleErrorMsgId + 'Number'; + sampleErrorMsgId += 'Number'; } if (props.config.PasswordSettings.Symbol) { - sampleErrorMsgId = sampleErrorMsgId + 'Symbol'; + sampleErrorMsgId += 'Symbol'; } this.sampleErrorMsg = ( <FormattedMessage @@ -101,16 +101,16 @@ export default class PasswordSettings extends AdminSettings { } let sampleErrorMsgId = 'user.settings.security.passwordError'; if (this.refs.lowercase.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Lowercase'; + sampleErrorMsgId += 'Lowercase'; } if (this.refs.uppercase.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Uppercase'; + sampleErrorMsgId += 'Uppercase'; } if (this.refs.number.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Number'; + sampleErrorMsgId += 'Number'; } if (this.refs.symbol.checked) { - sampleErrorMsgId = sampleErrorMsgId + 'Symbol'; + sampleErrorMsgId += 'Symbol'; } return ( <FormattedMessage diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx index 757f85517..1b9e5b37a 100644 --- a/webapp/components/admin_console/reset_password_modal.jsx +++ b/webapp/components/admin_console/reset_password_modal.jsx @@ -61,7 +61,7 @@ class ResetPasswordModal extends React.Component { if (this.state.serverError) { urlClass += ' has-error'; - serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>; + serverError = <div className='has-error'><p className='input__help error'>{this.state.serverError}</p></div>; } let title; diff --git a/webapp/components/audio_video_preview.jsx b/webapp/components/audio_video_preview.jsx index 4956900a9..9a55e4835 100644 --- a/webapp/components/audio_video_preview.jsx +++ b/webapp/components/audio_video_preview.jsx @@ -94,7 +94,6 @@ export default class AudioVideoPreview extends React.Component { <video key={this.props.fileInfo.id} ref='video' - style={{maxHeight: this.props.maxHeight}} data-setup='{}' controls='controls' width={width} @@ -111,6 +110,5 @@ export default class AudioVideoPreview extends React.Component { AudioVideoPreview.propTypes = { fileInfo: React.PropTypes.object.isRequired, - fileUrl: React.PropTypes.string.isRequired, - maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired + fileUrl: React.PropTypes.string.isRequired }; diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 01e1e98cf..120846b8d 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -30,7 +30,7 @@ import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; import {getSiteURL} from 'utils/url.jsx'; import * as TextFormatting from 'utils/text_formatting.jsx'; -import {getFlaggedPosts} from 'actions/post_actions.jsx'; +import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; @@ -53,6 +53,7 @@ export default class ChannelHeader extends React.Component { this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); this.handleShortcut = this.handleShortcut.bind(this); this.getFlagged = this.getFlagged.bind(this); + this.getPinnedPosts = this.getPinnedPosts.bind(this); this.initWebrtc = this.initWebrtc.bind(this); this.onBusy = this.onBusy.bind(this); this.openDirectMessageModal = this.openDirectMessageModal.bind(this); @@ -158,6 +159,15 @@ export default class ChannelHeader extends React.Component { } } + getPinnedPosts(e) { + e.preventDefault(); + if (SearchStore.isPinnedPosts) { + GlobalActions.toggleSideBarAction(false); + } else { + getPinnedPosts(this.props.channelId); + } + } + getFlagged(e) { e.preventDefault(); if (SearchStore.isFlaggedPosts) { @@ -211,6 +221,7 @@ export default class ChannelHeader extends React.Component { render() { const flagIcon = Constants.FLAG_ICON_SVG; + const pinIcon = Constants.PIN_ICON; if (!this.validState()) { // Use an empty div to make sure the header's height stays constant @@ -230,7 +241,10 @@ export default class ChannelHeader extends React.Component { ); const flaggedTooltip = ( - <Tooltip id='flaggedTooltip'> + <Tooltip + id='flaggedTooltip' + className='text-nowrap' + > <FormattedMessage id='channel_header.flagged' defaultMessage='Flagged Posts' @@ -665,19 +679,27 @@ export default class ChannelHeader extends React.Component { headerText = channel.header; } - const toggleFavoriteTooltip = ( - <Tooltip id='favoriteTooltip'> - {this.state.isFavorite ? + let toggleFavoriteTooltip; + if (this.state.isFavorite) { + toggleFavoriteTooltip = ( + <Tooltip id='favoriteTooltip'> <FormattedMessage id='channelHeader.removeFromFavorites' defaultMessage='Remove from Favorites' - /> : - <FormattedMessage - id='channelHeader.addToFavorites' - defaultMessage='Add to Favorites' - />} - </Tooltip> - ); + /> + </Tooltip> + ); + } else { + toggleFavoriteTooltip = ( + <Tooltip id='favoriteTooltip'> + <FormattedMessage + id='channelHeader.addToFavorites' + defaultMessage='Add to Favorites' + /> + </Tooltip> + ); + } + const toggleFavorite = ( <OverlayTrigger delayShow={Constants.OVERLAY_TIME_DELAY} @@ -762,8 +784,20 @@ export default class ChannelHeader extends React.Component { </OverlayTrigger> </div> </th> - <th className='header-list__members'> + <th className='header-list__right'> {popoverListMembers} + <a + href='#' + type='button' + id='pinned-posts-button' + className='pinned-posts-button' + onClick={this.getPinnedPosts} + > + <span + dangerouslySetInnerHTML={{__html: pinIcon}} + aria-hidden='true' + /> + </a> </th> <th className='search-bar__container'> <NavbarSearchBox diff --git a/webapp/components/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx index aeb8afbb9..865c0e6db 100644 --- a/webapp/components/create_team/components/display_name.jsx +++ b/webapp/components/create_team/components/display_name.jsx @@ -10,7 +10,6 @@ import logoImage from 'images/logo.png'; import React from 'react'; import ReactDOM from 'react-dom'; -import {Link} from 'react-router/es6'; import {FormattedMessage} from 'react-intl'; export default class TeamSignupDisplayNamePage extends React.Component { @@ -118,14 +117,6 @@ export default class TeamSignupDisplayNamePage extends React.Component { defaultMessage='Next' /><i className='fa fa-chevron-right'/> </button> - <div className='margin--extra'> - <Link to='/select_team'> - <FormattedMessage - id='create_team.display_name.back' - defaultMessage='Back to previous step' - /> - </Link> - </div> </form> </div> ); diff --git a/webapp/components/integrations/components/confirm_integration.jsx b/webapp/components/integrations/components/confirm_integration.jsx index 6d778f241..b4f299d1c 100644 --- a/webapp/components/integrations/components/confirm_integration.jsx +++ b/webapp/components/integrations/components/confirm_integration.jsx @@ -5,7 +5,7 @@ import React from 'react'; import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; -import {Link} from 'react-router/es6'; +import {Link, browserHistory} from 'react-router/es6'; import UserStore from 'stores/user_store.jsx'; import IntegrationStore from 'stores/integration_store.jsx'; @@ -25,6 +25,7 @@ export default class ConfirmIntegration extends React.Component { super(props); this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); const userId = UserStore.getCurrentId(); @@ -38,10 +39,12 @@ export default class ConfirmIntegration extends React.Component { componentDidMount() { IntegrationStore.addChangeListener(this.handleIntegrationChange); + window.addEventListener('keypress', this.handleKeyPress); } componentWillUnmount() { IntegrationStore.removeChangeListener(this.handleIntegrationChange); + window.removeEventListener('keypress', this.handleKeyPress); } handleIntegrationChange() { @@ -53,6 +56,12 @@ export default class ConfirmIntegration extends React.Component { }); } + handleKeyPress(e) { + if (e.key === 'Enter') { + browserHistory.push('/' + this.props.team.name + '/integrations/' + this.state.type); + } + } + render() { let headerText = null; let helpText = null; diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx index 38c594cf7..01da0199f 100644 --- a/webapp/components/login/login_controller.jsx +++ b/webapp/components/login/login_controller.jsx @@ -413,6 +413,7 @@ export default class LoginController extends React.Component { </div> <div className='form-group'> <button + id='loginButton' type='submit' className='btn btn-primary' > diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index c945a0b9c..28d8fae05 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -19,12 +19,15 @@ import UserStore from 'stores/user_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; +import SearchStore from 'stores/search_store.jsx'; import ChannelSwitchModal from './channel_switch_modal.jsx'; import * as Utils from 'utils/utils.jsx'; import * as ChannelUtils from 'utils/channel_utils.jsx'; import * as ChannelActions from 'actions/channel_actions.jsx'; +import * as GlobalActions from 'actions/global_actions.jsx'; +import {getPinnedPosts} from 'actions/post_actions.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -62,6 +65,7 @@ export default class Navbar extends React.Component { this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this); this.openDirectMessageModal = this.openDirectMessageModal.bind(this); + this.getPinnedPosts = this.getPinnedPosts.bind(this); const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; @@ -216,6 +220,15 @@ export default class Navbar extends React.Component { }); } + getPinnedPosts(e) { + e.preventDefault(); + if (SearchStore.isPinnedPosts) { + GlobalActions.toggleSideBarAction(false); + } else { + getPinnedPosts(this.state.channel.id); + } + } + toggleFavorite = (e) => { e.preventDefault(); @@ -244,6 +257,7 @@ export default class Navbar extends React.Component { } let viewInfoOption; + let viewPinnedPostsOption; let addMembersOption; let manageMembersOption; let setChannelHeaderOption; @@ -335,6 +349,21 @@ export default class Navbar extends React.Component { </li> ); + viewPinnedPostsOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.getPinnedPosts} + > + <FormattedMessage + id='navbar.viewPinnedPosts' + defaultMessage='View Pinned Posts' + /> + </a> + </li> + ); + if (!ChannelStore.isDefault(channel)) { addMembersOption = ( <li role='presentation'> @@ -525,10 +554,10 @@ export default class Navbar extends React.Component { id='channelHeader.removeFromFavorites' defaultMessage='Remove from Favorites' /> : - <FormattedMessage - id='channelHeader.addToFavorites' - defaultMessage='Add to Favorites' - />} + <FormattedMessage + id='channelHeader.addToFavorites' + defaultMessage='Add to Favorites' + />} </a> </li> ); @@ -561,6 +590,7 @@ export default class Navbar extends React.Component { role='menu' > {viewInfoOption} + {viewPinnedPostsOption} {notificationPreferenceOption} {addMembersOption} {manageMembersOption} diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index 5cb714ebc..11e75bfb7 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -12,6 +12,7 @@ import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; +import PostStore from 'stores/post_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx'; import {startPeriodicSync, stopPeriodicSync} from 'actions/websocket_actions.jsx'; @@ -177,12 +178,25 @@ export default class NeedsTeam extends React.Component { </div> ); } + + let channel = ChannelStore.getByName(this.props.params.channel); + if (channel == null) { + // the permalink view is not really tied to a particular channel but still needs it + const postId = PostStore.getFocusedPostId(); + const post = PostStore.getEarliestPostFromPage(postId); + + // the post take some time before being available on page load + if (post != null) { + channel = ChannelStore.get(post.channel_id); + } + } + return ( <div className='channel-view'> <ErrorBar/> <WebrtcNotification/> <div className='container-fluid'> - <SidebarRight/> + <SidebarRight channel={channel}/> <SidebarRightMenu teamType={this.state.team.type}/> <WebrtcSidebar/> {content} diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx index 2f9533b0e..f16b4596f 100644 --- a/webapp/components/new_channel_modal.jsx +++ b/webapp/components/new_channel_modal.jsx @@ -77,7 +77,7 @@ export default class NewChannelModal extends React.Component { e.preventDefault(); const displayName = ReactDOM.findDOMNode(this.refs.display_name).value.trim(); - if (displayName.length < 1) { + if (displayName.length < Constants.MIN_CHANNELNAME_LENGTH) { this.setState({displayNameError: true}); return; } @@ -104,7 +104,7 @@ export default class NewChannelModal extends React.Component { <p className='input__help error'> <FormattedMessage id='channel_modal.displayNameError' - defaultMessage='This field is required' + defaultMessage='Channel name must be 2 or more characters' /> {this.state.displayNameError} </p> @@ -232,7 +232,7 @@ export default class NewChannelModal extends React.Component { ref='display_name' className='form-control' placeholder={Utils.localizeMessage('channel_modal.nameEx', 'E.g.: "Bugs", "Marketing", "客户支持"')} - maxLength='22' + maxLength={Constants.MAX_CHANNELNAME_LENGTH} value={this.props.channelData.displayName} autoFocus={true} tabIndex='1' diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx index bd2f744c7..1518b1ebf 100644 --- a/webapp/components/popover_list_members.jsx +++ b/webapp/components/popover_list_members.jsx @@ -233,7 +233,7 @@ export default class PopoverListMembers extends React.Component { } return ( - <div> + <div className='member-popover__container'> <div id='member_popover' className='member-popover__trigger' @@ -243,13 +243,11 @@ export default class PopoverListMembers extends React.Component { AsyncClient.getProfilesInChannel(this.props.channel.id, 0); }} > - <div> - {countText} - <span - className='fa fa-user' - aria-hidden='true' - /> - </div> + {countText} + <span + className='fa fa-user' + aria-hidden='true' + /> </div> <Overlay rootClose={true} diff --git a/webapp/components/post_view/components/date_separator.jsx b/webapp/components/post_view/components/date_separator.jsx new file mode 100644 index 000000000..18dc0c7ff --- /dev/null +++ b/webapp/components/post_view/components/date_separator.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import {FormattedDate} from 'react-intl'; + +export default class DateSeparator extends React.Component { + render() { + return ( + <div + className='date-separator' + > + <hr className='separator__hr'/> + <div className='separator__text'> + <FormattedDate + value={this.props.date} + weekday='short' + month='short' + day='2-digit' + year='numeric' + /> + </div> + </div> + ); + } +} + +DateSeparator.propTypes = { + date: React.PropTypes.instanceOf(Date) +}; diff --git a/webapp/components/post_view/components/post_attachment.jsx b/webapp/components/post_view/components/post_attachment.jsx index 57335b94a..1b2cddcd6 100644 --- a/webapp/components/post_view/components/post_attachment.jsx +++ b/webapp/components/post_view/components/post_attachment.jsx @@ -184,6 +184,7 @@ class PostAttachment extends React.Component { author.push( <img className='attachment__author-icon' + crossOrigin='anonymous' src={data.author_icon} key={'attachment__author-icon'} height='14' @@ -257,6 +258,7 @@ class PostAttachment extends React.Component { image = ( <img className='attachment__image' + crossOrigin='anonymous' src={data.image_url} /> ); @@ -269,6 +271,7 @@ class PostAttachment extends React.Component { className='attachment__thumb-container' > <img + crossOrigin='anonymous' src={data.thumb_url} /> </div> diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx index 12437e672..13171202a 100644 --- a/webapp/components/post_view/components/post_attachment_opengraph.jsx +++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx @@ -32,7 +32,6 @@ export default class PostAttachmentOpenGraph extends React.Component { this.onImageLoad = this.onImageLoad.bind(this); this.onImageError = this.onImageError.bind(this); this.truncateText = this.truncateText.bind(this); - this.setImageWidth = this.setImageWidth.bind(this); } IMAGE_LOADED = { @@ -75,20 +74,16 @@ export default class PostAttachmentOpenGraph extends React.Component { componentDidMount() { OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange); - this.setImageWidth(); - window.addEventListener('resize', this.setImageWidth); } componentDidUpdate() { if (this.props.childComponentDidUpdateFunction) { this.props.childComponentDidUpdateFunction(); } - this.setImageWidth(); } componentWillUnmount() { OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange); - window.removeEventListener('resize', this.setImageWidth); } onOpenGraphMetadataChange(url) { @@ -163,9 +158,6 @@ export default class PostAttachmentOpenGraph extends React.Component { return ( <div className='attachment__image__container--openraph' - style={{ - width: (this.imageDimentions.height * this.imageRatio) + this.smallImageContainerLeftPadding - }} // Initially set the width accordinly to max image heigh, ie 80px. Later on it would be modified according to actul height of image. ref={(div) => { this.smallImageContainer = div; }} @@ -201,6 +193,7 @@ export default class PostAttachmentOpenGraph extends React.Component { element = this.wrapInSmallImageContainer( <img className={'attachment__image attachment__image--openraph'} + crossOrigin='anonymous' src={imageUrl} ref={(img) => { this.smallImageElement = img; @@ -215,20 +208,6 @@ export default class PostAttachmentOpenGraph extends React.Component { return element; } - setImageWidth() { - if ( - this.state.imageLoaded === this.IMAGE_LOADED.YES && - this.smallImageContainer && - this.smallImageElement - ) { - this.smallImageContainer.style.width = ( - (this.smallImageElement.offsetHeight * this.imageRatio) + - this.smallImageContainerLeftPadding + - 'px' - ); - } - } - truncateText(text, maxLength = this.textMaxLenght, ellipsis = this.textEllipsis) { if (text.length > maxLength) { return text.substring(0, maxLength - ellipsis.length) + ellipsis; diff --git a/webapp/components/post_view/components/post_image.jsx b/webapp/components/post_view/components/post_image.jsx index 9a761bfca..6fe954e99 100644 --- a/webapp/components/post_view/components/post_image.jsx +++ b/webapp/components/post_view/components/post_image.jsx @@ -67,6 +67,7 @@ export default class PostImageEmbed extends React.Component { return ( <img className='img-div placeholder' + crossOrigin='anonymous' height='500px' /> ); @@ -75,6 +76,7 @@ export default class PostImageEmbed extends React.Component { return ( <img className='img-div' + crossOrigin='anonymous' src={this.props.link} /> ); diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index 331fdeb00..5318ec272 100644 --- a/webapp/components/post_view/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -26,6 +26,8 @@ export default class PostInfo extends React.Component { this.removePost = this.removePost.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -148,6 +150,42 @@ export default class PostInfo extends React.Component { ); } + if (this.props.post.is_pinned) { + dropdownContents.push( + <li + key='unpinLink' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='post_info.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='pinLink' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='post_info.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -250,6 +288,16 @@ export default class PostInfo extends React.Component { ); } + pinPost(e) { + e.preventDefault(); + PostActions.pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + PostActions.unpinPost(this.props.post.channel_id, this.props.post.id); + } + flagPost(e) { e.preventDefault(); PostActions.flagPost(this.props.post.id); @@ -374,6 +422,18 @@ export default class PostInfo extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + return ( <ul className='post__header--info'> <li className='col'> @@ -384,6 +444,7 @@ export default class PostInfo extends React.Component { useMilitaryTime={this.props.useMilitaryTime} postId={post.id} /> + {pinnedBadge} {flagTrigger} </li> {options} diff --git a/webapp/components/post_view/components/post_time.jsx b/webapp/components/post_view/components/post_time.jsx index 25d533e0a..77f3f3266 100644 --- a/webapp/components/post_view/components/post_time.jsx +++ b/webapp/components/post_view/components/post_time.jsx @@ -40,12 +40,15 @@ export default class PostTime extends React.Component { } renderTimeTag() { + const date = getDateForUnixTicks(this.props.eventTime); + return ( <time className='post__time' - dateTime={getDateForUnixTicks(this.props.eventTime).toISOString()} + dateTime={date.toISOString()} + title={date} > - {getDateForUnixTicks(this.props.eventTime).toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})} + {date.toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})} </time> ); } diff --git a/webapp/components/profile_picture.jsx b/webapp/components/profile_picture.jsx index 7a5f892db..737a4400b 100644 --- a/webapp/components/profile_picture.jsx +++ b/webapp/components/profile_picture.jsx @@ -69,6 +69,7 @@ export default class ProfilePicture extends React.Component { width={this.props.width} height={this.props.width} src={this.props.src} + crossOrigin='anonymous' /> <StatusIcon status={this.props.status}/> </span> @@ -82,6 +83,7 @@ export default class ProfilePicture extends React.Component { width={this.props.width} height={this.props.width} src={this.props.src} + crossOrigin='anonymous' /> <StatusIcon status={this.props.status}/> </span> diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover.jsx index c7d45474f..e21716cb3 100644 --- a/webapp/components/profile_popover.jsx +++ b/webapp/components/profile_popover.jsx @@ -182,6 +182,7 @@ export default class ProfilePopover extends React.Component { height='128' width='128' key='user-popover-image' + crossOrigin='anonymous' /> ); diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index cb527d850..52e4d9851 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -10,7 +10,7 @@ import ReactionListContainer from 'components/post_view/components/reaction_list import RhsDropdown from 'components/rhs_dropdown.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -36,6 +36,8 @@ export default class RhsComment extends React.Component { this.removePost = this.removePost.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -128,6 +130,16 @@ export default class RhsComment extends React.Component { unflagPost(this.props.post.id); } + pinPost(e) { + e.preventDefault(); + pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + unpinPost(this.props.post.channel_id, this.props.post.id); + } + createDropdown() { const post = this.props.post; @@ -195,6 +207,42 @@ export default class RhsComment extends React.Component { </li> ); + if (post.is_pinned) { + dropdownContents.push( + <li + key='rhs-comment-unpin' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='rhs_root.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='rhs-comment-pin' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='rhs_root.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -503,10 +551,19 @@ export default class RhsComment extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + const timeOptions = { - day: 'numeric', - month: 'short', - year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime @@ -524,6 +581,7 @@ export default class RhsComment extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post, timeOptions)} + {pinnedBadge} {flagTrigger} </li> {options} diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 0c1037501..83d930bca 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -14,7 +14,7 @@ import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; import * as GlobalActions from 'actions/global_actions.jsx'; -import {flagPost, unflagPost} from 'actions/post_actions.jsx'; +import {flagPost, unflagPost, pinPost, unpinPost} from 'actions/post_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; @@ -35,6 +35,8 @@ export default class RhsRootPost extends React.Component { this.handlePermalink = this.handlePermalink.bind(this); this.flagPost = this.flagPost.bind(this); this.unflagPost = this.unflagPost.bind(this); + this.pinPost = this.pinPost.bind(this); + this.unpinPost = this.unpinPost.bind(this); this.canEdit = false; this.canDelete = false; @@ -143,6 +145,16 @@ export default class RhsRootPost extends React.Component { ); } + pinPost(e) { + e.preventDefault(); + pinPost(this.props.post.channel_id, this.props.post.id); + } + + unpinPost(e) { + e.preventDefault(); + unpinPost(this.props.post.channel_id, this.props.post.id); + } + render() { const post = this.props.post; const user = this.props.user; @@ -240,6 +252,42 @@ export default class RhsRootPost extends React.Component { </li> ); + if (post.is_pinned) { + dropdownContents.push( + <li + key='rhs-root-unpin' + role='presentation' + > + <a + href='#' + onClick={this.unpinPost} + > + <FormattedMessage + id='rhs_root.unpin' + defaultMessage='Un-pin from channel' + /> + </a> + </li> + ); + } else { + dropdownContents.push( + <li + key='rhs-root-pin' + role='presentation' + > + <a + href='#' + onClick={this.pinPost} + > + <FormattedMessage + id='rhs_root.pin' + defaultMessage='Pin to channel' + /> + </a> + </li> + ); + } + if (this.canDelete) { dropdownContents.push( <li @@ -450,10 +498,19 @@ export default class RhsRootPost extends React.Component { flagFunc = this.flagPost; } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + const timeOptions = { - day: 'numeric', - month: 'short', - year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime @@ -470,6 +527,7 @@ export default class RhsRootPost extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post, timeOptions)} + {pinnedBadge} <OverlayTrigger key={'rootpostflagtooltipkey' + flagVisible} delayShow={Constants.OVERLAY_TIME_DELAY} diff --git a/webapp/components/rhs_thread.jsx b/webapp/components/rhs_thread.jsx index 3c0b2e114..2c1d03901 100644 --- a/webapp/components/rhs_thread.jsx +++ b/webapp/components/rhs_thread.jsx @@ -8,6 +8,7 @@ import RootPost from './rhs_root_post.jsx'; import Comment from './rhs_comment.jsx'; import FileUploadOverlay from './file_upload_overlay.jsx'; import FloatingTimestamp from './post_view/components/floating_timestamp.jsx'; +import DateSeparator from './post_view/components/date_separator.jsx'; import PostStore from 'stores/post_store.jsx'; import UserStore from 'stores/user_store.jsx'; @@ -325,6 +326,7 @@ export default class RhsThread extends React.Component { const postsArray = this.state.postsArray; const selected = this.state.selected; const profiles = this.state.profiles || {}; + let previousPostDay = Utils.getDateForUnixTicks(selected.create_at); if (postsArray == null || selected == null) { return ( @@ -355,6 +357,55 @@ export default class RhsThread extends React.Component { rootStatus = this.state.statuses[selected.user_id] || 'offline'; } + const commentsLists = []; + for (let i = 0; i < postsArray.length; i++) { + const comPost = postsArray[i]; + let p; + if (UserStore.getCurrentId() === comPost.user_id) { + p = UserStore.getCurrentUser(); + } else { + p = profiles[comPost.user_id]; + } + + let isFlagged = false; + if (this.state.flaggedPosts) { + isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true'; + } + + let status = 'offline'; + if (this.state.statuses && p && p.id) { + 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( + <DateSeparator + date={currentPostDay} + />); + } + + commentsLists.push( + <div key={keyPrefix + 'commentKey'}> + <Comment + ref={comPost.id} + post={comPost} + user={p} + currentUser={this.props.currentUser} + compactDisplay={this.state.compactDisplay} + useMilitaryTime={this.props.useMilitaryTime} + isFlagged={isFlagged} + status={status} + isBusy={this.state.isBusy} + /> + </div> + ); + } + return ( <div className='post-right__container'> <FileUploadOverlay overlayType='right'/> @@ -384,6 +435,9 @@ export default class RhsThread extends React.Component { onScroll={this.handleScroll} > <div className='post-right__scroll'> + <DateSeparator + date={previousPostDay} + /> <RootPost ref={selected.id} post={selected} @@ -401,41 +455,7 @@ export default class RhsThread extends React.Component { ref='rhspostlist' className='post-right-comments-container' > - {postsArray.map((comPost) => { - let p; - if (UserStore.getCurrentId() === comPost.user_id) { - p = UserStore.getCurrentUser(); - } else { - p = profiles[comPost.user_id]; - } - - let isFlagged = false; - if (this.state.flaggedPosts) { - isFlagged = this.state.flaggedPosts.get(comPost.id) === 'true'; - } - - let status = 'offline'; - if (this.state.statuses && p && p.id) { - status = this.state.statuses[p.id] || 'offline'; - } - - const keyPrefix = comPost.id ? comPost.id : comPost.pending_post_id; - - return ( - <Comment - ref={comPost.id} - key={keyPrefix + 'commentKey'} - post={comPost} - user={p} - currentUser={this.props.currentUser} - compactDisplay={this.state.compactDisplay} - useMilitaryTime={this.props.useMilitaryTime} - isFlagged={isFlagged} - status={status} - isBusy={this.state.isBusy} - /> - ); - })} + {commentsLists} </div> <div className='post-create__container'> <CreateComment diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx index 1c9f607e6..b88e67a11 100644 --- a/webapp/components/search_bar.jsx +++ b/webapp/components/search_bar.jsx @@ -216,7 +216,10 @@ export default class SearchBar extends React.Component { ); const flaggedTooltip = ( - <Tooltip id='flaggedTooltip'> + <Tooltip + id='flaggedTooltip' + className='text-nowrap' + > <FormattedMessage id='channel_header.flagged' defaultMessage='Flagged Posts' diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx index 4c0105738..ceafd766c 100644 --- a/webapp/components/search_results.jsx +++ b/webapp/components/search_results.jsx @@ -213,6 +213,31 @@ export default class SearchResults extends React.Component { </ul> </div> ); + } else if (this.props.isPinnedPosts && noResults) { + ctls = ( + <div className='sidebar--right__subheader'> + <ul> + <li> + <FormattedHTMLMessage + id='search_results.usagePin1' + defaultMessage='There are no pinned messages yet.' + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usagePin2' + defaultMessage={'You can pin a message by clicking the "Pin to channel" option from the message\'s menu.'} + /> + </li> + <li> + <FormattedHTMLMessage + id='search_results.usagePin3' + defaultMessage='Pinned messages are accessible by all channel members and are a way to mark messages for future reference.' + /> + </li> + </ul> + </div> + ); } else if (!searchTerm && noResults) { ctls = ( <div className='sidebar--right__subheader'> @@ -289,6 +314,8 @@ export default class SearchResults extends React.Component { toggleSize={this.props.toggleSize} shrink={this.props.shrink} isFlaggedPosts={this.props.isFlaggedPosts} + isPinnedPosts={this.props.isPinnedPosts} + channelDisplayName={this.props.channelDisplayName} /> <div id='search-items-container' @@ -307,5 +334,7 @@ SearchResults.propTypes = { useMilitaryTime: React.PropTypes.bool.isRequired, toggleSize: React.PropTypes.func, shrink: React.PropTypes.func, - isFlaggedPosts: React.PropTypes.bool + isFlaggedPosts: React.PropTypes.bool, + isPinnedPosts: React.PropTypes.bool, + channelDisplayName: React.PropTypes.string.isRequired }; diff --git a/webapp/components/search_results_header.jsx b/webapp/components/search_results_header.jsx index 1f2818e98..288d883ee 100644 --- a/webapp/components/search_results_header.jsx +++ b/webapp/components/search_results_header.jsx @@ -79,6 +79,16 @@ export default class SearchResultsHeader extends React.Component { defaultMessage='Flagged Posts' /> ); + } else if (this.props.isPinnedPosts) { + title = ( + <FormattedMessage + id='search_header.title4' + defaultMessage='Pinned posts in {channelDisplayName}' + values={{ + channelDisplayName: this.props.channelDisplayName + }} + /> + ); } return ( @@ -131,5 +141,7 @@ SearchResultsHeader.propTypes = { isMentionSearch: React.PropTypes.bool, toggleSize: React.PropTypes.func, shrink: React.PropTypes.func, - isFlaggedPosts: React.PropTypes.bool + isFlaggedPosts: React.PropTypes.bool, + isPinnedPosts: React.PropTypes.bool, + channelDisplayName: React.PropTypes.string.isRequired }; diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index b3de3492c..1c7309f51 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -289,6 +289,18 @@ export default class SearchResultsItem extends React.Component { ); } + let pinnedBadge; + if (post.is_pinned) { + pinnedBadge = ( + <span className='post__pinned-badge'> + <FormattedMessage + id='post_info.pinned' + defaultMessage='Pinned' + /> + </span> + ); + } + return ( <div className='search-item__container'> <div className='date-separator'> @@ -322,6 +334,7 @@ export default class SearchResultsItem extends React.Component { {botIndicator} <li className='col'> {this.renderTimeTag(post)} + {pinnedBadge} {flagContent} </li> {rhsControls} diff --git a/webapp/components/searchable_user_list.jsx b/webapp/components/searchable_user_list.jsx index d25c8a506..ab3f9ee9b 100644 --- a/webapp/components/searchable_user_list.jsx +++ b/webapp/components/searchable_user_list.jsx @@ -19,6 +19,7 @@ export default class SearchableUserList extends React.Component { this.nextPage = this.nextPage.bind(this); this.previousPage = this.previousPage.bind(this); this.doSearch = this.doSearch.bind(this); + this.focusSearchBar = this.focusSearchBar.bind(this); this.nextTimeoutId = 0; @@ -30,15 +31,14 @@ export default class SearchableUserList extends React.Component { } componentDidMount() { - if (this.props.focusOnMount) { - this.refs.filter.focus(); - } + this.focusSearchBar(); } componentDidUpdate(prevProps, prevState) { if (this.state.page !== prevState.page) { $(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0); } + this.focusSearchBar(); } componentWillUnmount() { @@ -57,6 +57,12 @@ export default class SearchableUserList extends React.Component { this.setState({page: this.state.page - 1}); } + focusSearchBar() { + if (this.props.focusOnMount) { + this.refs.filter.focus(); + } + } + doSearch() { const term = this.refs.filter.value; this.props.search(term); diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx index 5b6a5d53a..9f3c4f0cf 100644 --- a/webapp/components/setting_item_max.jsx +++ b/webapp/components/setting_item_max.jsx @@ -31,12 +31,30 @@ export default class SettingItemMax extends React.Component { render() { var clientError = null; if (this.props.client_error) { - clientError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.client_error}</label></div>); + clientError = ( + <div className='form-group'> + <label + id='clientError' + className='col-sm-12 has-error' + > + {this.props.client_error} + </label> + </div> + ); } var serverError = null; if (this.props.server_error) { - serverError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.server_error}</label></div>); + serverError = ( + <div className='form-group'> + <label + id='serverError' + className='col-sm-12 has-error' + > + {this.props.server_error} + </label> + </div> + ); } var extraInfo = null; @@ -48,6 +66,7 @@ export default class SettingItemMax extends React.Component { if (this.props.submit) { submit = ( <input + id='saveSetting' type='submit' className='btn btn-sm btn-primary' href='#' @@ -88,6 +107,7 @@ export default class SettingItemMax extends React.Component { {clientError} {submit} <a + id={this.props.title + 'Cancel'} className='btn btn-sm' href='#' onClick={this.props.updateSection} diff --git a/webapp/components/setting_item_min.jsx b/webapp/components/setting_item_min.jsx index 96d8bf459..4f756c46e 100644 --- a/webapp/components/setting_item_min.jsx +++ b/webapp/components/setting_item_min.jsx @@ -12,6 +12,7 @@ export default class SettingItemMin extends React.Component { editButton = ( <li className='col-xs-12 col-sm-3 section-edit'> <a + id={this.props.title} className='theme' href='#' onClick={this.props.updateSection} @@ -33,7 +34,12 @@ export default class SettingItemMin extends React.Component { > <li className='col-xs-12 col-sm-9 section-title'>{this.props.title}</li> {editButton} - <li className='col-xs-12 section-describe'>{this.props.describe}</li> + <li + id={this.props.title + 'Desc'} + className='col-xs-12 section-describe' + > + {this.props.describe} + </li> </ul> ); } diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx index b74ee8eb7..45ac4096d 100644 --- a/webapp/components/setting_picture.jsx +++ b/webapp/components/setting_picture.jsx @@ -1,8 +1,6 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; -import ReactDOM from 'react-dom'; import {FormattedMessage} from 'react-intl'; import loadingGif from 'images/load.gif'; @@ -14,17 +12,35 @@ export default class SettingPicture extends React.Component { super(props); this.setPicture = this.setPicture.bind(this); + this.confirmImage = this.confirmImage.bind(this); } setPicture(file) { if (file) { var reader = new FileReader(); - var img = ReactDOM.findDOMNode(this.refs.image); - reader.onload = function load(e) { - $(img).attr('src', e.target.result); - }; + reader.onload = (e) => { + const canvas = this.refs.profileImageCanvas; + const context = canvas.getContext('2d'); + const imageObj = new Image(); + imageObj.onload = () => { + if (imageObj.width > imageObj.height) { + const side = imageObj.height; + const rem = imageObj.width - side; + const startX = parseInt(rem / 2, 10); + context.drawImage(imageObj, startX, 0, side, side, + 0, 0, canvas.width, canvas.height); + } else { + const side = imageObj.width; + const rem = imageObj.height - side; + const startY = parseInt(rem / 2, 10); + context.drawImage(imageObj, 0, startY, side, side, + 0, 0, canvas.width, canvas.height); + } + }; + imageObj.src = e.target.result; + }; reader.readAsDataURL(file); } } @@ -48,10 +64,11 @@ export default class SettingPicture extends React.Component { var img = null; if (this.props.picture) { img = ( - <img - ref='image' - className='profile-img rounded' - src='' + <canvas + ref='profileImageCanvas' + className='profile-img' + width='256px' + height='256px' /> ); } else { @@ -83,7 +100,7 @@ export default class SettingPicture extends React.Component { confirmButton = ( <a className={confirmButtonClass} - onClick={this.props.submit} + onClick={this.confirmImage} > <FormattedMessage id='setting_picture.save' @@ -147,6 +164,16 @@ export default class SettingPicture extends React.Component { </ul> ); } + + confirmImage(e) { + e.persist(); + this.refs.profileImageCanvas.toBlob((blob) => { + blob.lastModifiedDate = new Date(); + blob.name = 'image.jpg'; + this.props.imageCropChange(blob); + this.props.submit(e); + }, 'image/jpeg', 0.95); + } } SettingPicture.propTypes = { @@ -158,5 +185,6 @@ SettingPicture.propTypes = { submitActive: React.PropTypes.bool, submit: React.PropTypes.func, title: React.PropTypes.string, - pictureChange: React.PropTypes.func + pictureChange: React.PropTypes.func, + imageCropChange: React.PropTypes.func }; diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 08d80d363..b9356c5a1 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -636,13 +636,15 @@ export default class Sidebar extends React.Component { this.lastUnreadChannel = null; // create elements for all 4 types of channels - const favoriteItems = this.state.favoriteChannels.map((channel, index, arr) => { - if (channel.type === Constants.DM_CHANNEL) { - return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); - } + const favoriteItems = this.state.favoriteChannels. + sort(Utils.sortTeamsByDisplayName). + map((channel, index, arr) => { + if (channel.type === Constants.DM_CHANNEL) { + return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); + } - return this.createChannelElement(channel); - }); + return this.createChannelElement(channel); + }); const publicChannelItems = this.state.publicChannels.map(this.createChannelElement); diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx index 484ca3298..34c228ac2 100644 --- a/webapp/components/sidebar_header_dropdown.jsx +++ b/webapp/components/sidebar_header_dropdown.jsx @@ -467,6 +467,7 @@ export default class SidebarHeaderDropdown extends React.Component { <Dropdown.Menu> <li> <a + id='accountSettings' href='#' onClick={this.toggleAccountSettingsModal} > @@ -480,6 +481,7 @@ export default class SidebarHeaderDropdown extends React.Component { {teamLink} <li> <a + id='logout' href='#' onClick={() => GlobalActions.emitUserLoggedOutEvent()} > diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx index fb120337a..42b7381f4 100644 --- a/webapp/components/sidebar_right.jsx +++ b/webapp/components/sidebar_right.jsx @@ -11,13 +11,13 @@ import UserStore from 'stores/user_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx'; import WebrtcStore from 'stores/webrtc_store.jsx'; -import {getFlaggedPosts} from 'actions/post_actions.jsx'; +import {getFlaggedPosts, getPinnedPosts} from 'actions/post_actions.jsx'; import {trackEvent} from 'actions/diagnostics_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; -import React from 'react'; +import React, {PropTypes} from 'react'; export default class SidebarRight extends React.Component { constructor(props) { @@ -27,6 +27,7 @@ export default class SidebarRight extends React.Component { this.onPreferenceChange = this.onPreferenceChange.bind(this); this.onSelectedChange = this.onSelectedChange.bind(this); + this.onPostPinnedChange = this.onPostPinnedChange.bind(this); this.onSearchChange = this.onSearchChange.bind(this); this.onUserChange = this.onUserChange.bind(this); this.onShowSearch = this.onShowSearch.bind(this); @@ -39,6 +40,7 @@ export default class SidebarRight extends React.Component { searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch(), isFlaggedPosts: SearchStore.getIsFlaggedPosts(), + isPinnedPosts: SearchStore.getIsPinnedPosts(), postRightVisible: Boolean(PostStore.getSelectedPost()), expanded: false, fromSearch: false, @@ -50,6 +52,7 @@ export default class SidebarRight extends React.Component { componentDidMount() { SearchStore.addSearchChangeListener(this.onSearchChange); PostStore.addSelectedPostChangeListener(this.onSelectedChange); + PostStore.addPostPinnedChangeListener(this.onPostPinnedChange); SearchStore.addShowSearchListener(this.onShowSearch); UserStore.addChangeListener(this.onUserChange); PreferenceStore.addChangeListener(this.onPreferenceChange); @@ -59,6 +62,7 @@ export default class SidebarRight extends React.Component { componentWillUnmount() { SearchStore.removeSearchChangeListener(this.onSearchChange); PostStore.removeSelectedPostChangeListener(this.onSelectedChange); + PostStore.removePostPinnedChangeListener(this.onPostPinnedChange); SearchStore.removeShowSearchListener(this.onShowSearch); UserStore.removeChangeListener(this.onUserChange); PreferenceStore.removeChangeListener(this.onPreferenceChange); @@ -137,6 +141,12 @@ export default class SidebarRight extends React.Component { }); } + onPostPinnedChange() { + if (this.props.channel && this.state.isPinnedPosts) { + getPinnedPosts(this.props.channel.id); + } + } + onShrink() { this.setState({ expanded: false @@ -147,7 +157,8 @@ export default class SidebarRight extends React.Component { this.setState({ searchVisible: SearchStore.getSearchResults() !== null, isMentionSearch: SearchStore.getIsMentionSearch(), - isFlaggedPosts: SearchStore.getIsFlaggedPosts() + isFlaggedPosts: SearchStore.getIsFlaggedPosts(), + isPinnedPosts: SearchStore.getIsPinnedPosts() }); } @@ -182,9 +193,11 @@ export default class SidebarRight extends React.Component { <SearchResults isMentionSearch={this.state.isMentionSearch} isFlaggedPosts={this.state.isFlaggedPosts} + isPinnedPosts={this.state.isPinnedPosts} useMilitaryTime={this.state.useMilitaryTime} toggleSize={this.toggleSize} shrink={this.onShrink} + channelDisplayName={this.props.channel ? this.props.channel.display_name : ''} /> ); } else if (this.state.postRightVisible) { @@ -222,3 +235,7 @@ export default class SidebarRight extends React.Component { ); } } + +SidebarRight.propTypes = { + channel: PropTypes.object +}; diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx index 0100cad64..bc6c70e7f 100644 --- a/webapp/components/team_general_tab.jsx +++ b/webapp/components/team_general_tab.jsx @@ -106,14 +106,11 @@ class GeneralTab extends React.Component { let valid = true; const name = this.state.name.trim(); - if (!name) { + if (name) { + state.clientError = ''; + } else { state.clientError = Utils.localizeMessage('general_tab.required', 'This field is required'); valid = false; - } else if (name === this.props.team.display_name) { - state.clientError = Utils.localizeMessage('general_tab.chooseName', 'Please choose a new name for your team'); - valid = false; - } else { - state.clientError = ''; } this.setState(state); diff --git a/webapp/components/user_settings/desktop_notification_settings.jsx b/webapp/components/user_settings/desktop_notification_settings.jsx index 3a330b623..be403ebb6 100644 --- a/webapp/components/user_settings/desktop_notification_settings.jsx +++ b/webapp/components/user_settings/desktop_notification_settings.jsx @@ -74,6 +74,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundOn' type='radio' name='notificationSounds' checked={soundRadio[0]} @@ -89,6 +90,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundOff' type='radio' name='notificationSounds' checked={soundRadio[1]} @@ -136,6 +138,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDuration3' type='radio' name='desktopDuration' checked={durationRadio[0]} @@ -154,6 +157,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDuration5' type='radio' name='desktopDuration' checked={durationRadio[1]} @@ -172,6 +176,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDuration10' type='radio' name='desktopDuration' checked={durationRadio[2]} @@ -189,6 +194,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='soundDurationUnlimited' type='radio' name='desktopDuration' checked={durationRadio[3]} @@ -225,6 +231,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='desktopNotificationAllActivity' type='radio' name='desktopNotificationLevel' checked={activityRadio[0]} @@ -240,6 +247,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='desktopNotificationMentions' type='radio' name='desktopNotificationLevel' checked={activityRadio[1]} @@ -255,6 +263,7 @@ export default class DesktopNotificationSettings extends React.Component { <div className='radio'> <label> <input + id='desktopNotificationNever' type='radio' name='desktopNotificationLevel' checked={activityRadio[2]} diff --git a/webapp/components/user_settings/email_notification_setting.jsx b/webapp/components/user_settings/email_notification_setting.jsx index 457512507..1e6c5d7f5 100644 --- a/webapp/components/user_settings/email_notification_setting.jsx +++ b/webapp/components/user_settings/email_notification_setting.jsx @@ -113,6 +113,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationMinutes' type='radio' name='emailNotifications' checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_FIFTEEN_MINUTES} @@ -128,6 +129,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationHour' type='radio' name='emailNotifications' checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_HOUR} @@ -164,6 +166,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationImmediately' type='radio' name='emailNotifications' checked={this.props.enableEmail && this.state.emailInterval === Preferences.INTERVAL_IMMEDIATE} @@ -179,6 +182,7 @@ export default class EmailNotificationSetting extends React.Component { <div className='radio'> <label> <input + id='emailNotificationNever' type='radio' name='emailNotifications' checked={!this.props.enableEmail} diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx index f9c624aa0..d558958f0 100644 --- a/webapp/components/user_settings/user_settings_general.jsx +++ b/webapp/components/user_settings/user_settings_general.jsx @@ -101,6 +101,7 @@ class UserSettingsGeneralTab extends React.Component { this.updatePicture = this.updatePicture.bind(this); this.updateSection = this.updateSection.bind(this); this.updatePosition = this.updatePosition.bind(this); + this.updatedCroppedPicture = this.updatedCroppedPicture.bind(this); this.state = this.setupInitialState(props); } @@ -311,6 +312,17 @@ class UserSettingsGeneralTab extends React.Component { this.setState({confirmEmail: e.target.value}); } + updatedCroppedPicture(file) { + if (file) { + this.setState({picture: file}); + + this.submitActive = true; + this.setState({clientError: null}); + } else { + this.setState({picture: null}); + } + } + updatePicture(e) { if (e.target.files && e.target.files[0]) { this.setState({picture: e.target.files[0]}); @@ -410,6 +422,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='primaryEmail' className='form-control' type='email' onChange={this.updateEmail} @@ -431,6 +444,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='confirmEmail' className='form-control' type='email' onChange={this.updateConfirmEmail} @@ -684,6 +698,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='firstName' className='form-control' type='text' onChange={this.updateFirstName} @@ -706,6 +721,7 @@ class UserSettingsGeneralTab extends React.Component { </label> <div className='col-sm-7'> <input + id='lastName' className='form-control' type='text' onChange={this.updateLastName} @@ -832,6 +848,7 @@ class UserSettingsGeneralTab extends React.Component { <label className='col-sm-5 control-label'>{nicknameLabel}</label> <div className='col-sm-7'> <input + id='nickname' className='form-control' type='text' onChange={this.updateNickname} @@ -916,6 +933,7 @@ class UserSettingsGeneralTab extends React.Component { <label className='col-sm-5 control-label'>{usernameLabel}</label> <div className='col-sm-7'> <input + id='username' maxLength={Constants.MAX_USERNAME_LENGTH} className='form-control' type='text' @@ -1006,6 +1024,7 @@ class UserSettingsGeneralTab extends React.Component { <label className='col-sm-5 control-label'>{positionLabel}</label> <div className='col-sm-7'> <input + id='position' className='form-control' type='text' onChange={this.updatePosition} @@ -1086,6 +1105,7 @@ class UserSettingsGeneralTab extends React.Component { pictureChange={this.updatePicture} submitActive={this.submitActive} loadingPicture={this.state.loadingPicture} + imageCropChange={this.updatedCroppedPicture} /> ); } else { @@ -1123,6 +1143,7 @@ class UserSettingsGeneralTab extends React.Component { <div> <div className='modal-header'> <button + id='closeUserSettings' type='button' className='close' data-dismiss='modal' diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx index 7c82488f6..ebd43e5af 100644 --- a/webapp/components/user_settings/user_settings_notifications.jsx +++ b/webapp/components/user_settings/user_settings_notifications.jsx @@ -64,6 +64,9 @@ function getNotificationsStateFromStores() { } else { usernameKey = true; keys.splice(keys.indexOf(user.username), 1); + if (keys.indexOf(`@${user.username}`) !== -1) { + keys.splice(keys.indexOf(`@${user.username}`), 1); + } } customKeys = keys.join(','); @@ -281,6 +284,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationOnline' type='radio' name='pushNotificationStatus' checked={pushStatusRadio[0]} @@ -296,6 +300,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationAway' type='radio' name='pushNotificationStatus' checked={pushStatusRadio[1]} @@ -311,6 +316,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationOffline' type='radio' name='pushNotificationStatus' checked={pushStatusRadio[2]} @@ -347,6 +353,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationAllActivity' type='radio' name='pushNotificationLevel' checked={pushActivityRadio[0]} @@ -362,6 +369,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationMentions' type='radio' name='pushNotificationLevel' checked={pushActivityRadio[1]} @@ -377,6 +385,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='pushNotificationNever' type='radio' name='pushNotificationLevel' checked={pushActivityRadio[2]} @@ -520,6 +529,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerFirst' type='checkbox' checked={this.state.firstNameKey} onChange={handleUpdateFirstNameKey} @@ -545,6 +555,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerUsername' type='checkbox' checked={this.state.usernameKey} onChange={handleUpdateUsernameKey} @@ -569,6 +580,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerShouts' type='checkbox' checked={this.state.channelKey} onChange={handleUpdateChannelKey} @@ -587,6 +599,7 @@ export default class NotificationsTab extends React.Component { <div className='checkbox'> <label> <input + id='notificationTriggerCustom' ref='customcheck' type='checkbox' checked={this.state.customKeysChecked} @@ -599,6 +612,7 @@ export default class NotificationsTab extends React.Component { </label> </div> <input + id='notificationTriggerCustomText' ref='custommentions' className='form-control mentions-input' type='text' @@ -697,6 +711,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='notificationCommentsAny' type='radio' name='commentsNotificationLevel' checked={commentsActive[0]} @@ -712,6 +727,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='notificationCommentsRoot' type='radio' name='commentsNotificationLevel' checked={commentsActive[1]} @@ -727,6 +743,7 @@ export default class NotificationsTab extends React.Component { <div className='radio'> <label> <input + id='notificationCommentsNever' type='radio' name='commentsNotificationLevel' checked={commentsActive[2]} @@ -804,6 +821,7 @@ export default class NotificationsTab extends React.Component { <div> <div className='modal-header'> <button + id='closeButton' type='button' className='close' data-dismiss='modal' diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx index b6ee2d915..9ca7f4b62 100644 --- a/webapp/components/user_settings/user_settings_security.jsx +++ b/webapp/components/user_settings/user_settings_security.jsx @@ -331,6 +331,7 @@ export default class SecurityTab extends React.Component { </label> <div className='col-sm-7'> <input + id='currentPassword' className='form-control' type='password' onChange={this.updateCurrentPassword} @@ -352,6 +353,7 @@ export default class SecurityTab extends React.Component { </label> <div className='col-sm-7'> <input + id='newPassword' className='form-control' type='password' onChange={this.updateNewPassword} @@ -373,6 +375,7 @@ export default class SecurityTab extends React.Component { </label> <div className='col-sm-7'> <input + id='confirmPassword' className='form-control' type='password' onChange={this.updateConfirmPassword} diff --git a/webapp/components/view_image.jsx b/webapp/components/view_image.jsx index 385138d54..e5c3caa0a 100644 --- a/webapp/components/view_image.jsx +++ b/webapp/components/view_image.jsx @@ -185,7 +185,6 @@ export default class ViewImageModal extends React.Component { <ImagePreview fileInfo={fileInfo} fileUrl={fileUrl} - maxHeight={this.state.imgHeight} /> ); } else if (fileType === 'video' || fileType === 'audio') { @@ -193,7 +192,6 @@ export default class ViewImageModal extends React.Component { <AudioVideoPreview fileInfo={fileInfo} fileUrl={fileUrl} - maxHeight={this.state.imgHeight} /> ); } else if (PDFPreview.supports(fileInfo)) { @@ -344,7 +342,7 @@ LoadingImagePreview.propTypes = { loading: React.PropTypes.string }; -function ImagePreview({fileInfo, fileUrl, maxHeight}) { +function ImagePreview({fileInfo, fileUrl}) { let previewUrl; if (fileInfo.has_preview_image) { previewUrl = FileStore.getFilePreviewUrl(fileInfo.id); @@ -359,16 +357,12 @@ function ImagePreview({fileInfo, fileUrl, maxHeight}) { rel='noopener noreferrer' download={true} > - <img - style={{maxHeight}} - src={previewUrl} - /> + <img src={previewUrl}/> </a> ); } ImagePreview.propTypes = { fileInfo: React.PropTypes.object.isRequired, - fileUrl: React.PropTypes.string.isRequired, - maxHeight: React.PropTypes.number.isRequired + fileUrl: React.PropTypes.string.isRequired }; |