diff options
Diffstat (limited to 'webapp/components')
31 files changed, 301 insertions, 172 deletions
diff --git a/webapp/components/add_users_to_team/add_users_to_team.jsx b/webapp/components/add_users_to_team/add_users_to_team.jsx index 19e0d674b..e3eb8477b 100644 --- a/webapp/components/add_users_to_team/add_users_to_team.jsx +++ b/webapp/components/add_users_to_team/add_users_to_team.jsx @@ -215,6 +215,11 @@ export default class AddUsersToTeam extends React.Component { /> ); + let users = []; + if (this.state.users) { + users = this.state.users.filter((user) => user.delete_at === 0); + } + return ( <Modal dialogClassName={'more-modal more-direct-channels'} @@ -238,7 +243,7 @@ export default class AddUsersToTeam extends React.Component { <Modal.Body> <MultiSelect key='addUsersToTeamKey' - options={this.state.users} + options={users} optionRenderer={this.renderOption} values={this.state.values} valueRenderer={this.renderValue} diff --git a/webapp/components/admin_console/push_settings.jsx b/webapp/components/admin_console/push_settings.jsx index 5461ef730..3b21f727a 100644 --- a/webapp/components/admin_console/push_settings.jsx +++ b/webapp/components/admin_console/push_settings.jsx @@ -127,14 +127,14 @@ export default class PushSettings extends AdminSettings { pushServerHelpText = ( <FormattedHTMLMessage id='admin.email.mhpnsHelp' - defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns" target="_blank">Mattermost Hosted Push Notification Service</a>.' + defaultMessage='Download <a href="https://about.mattermost.com/mattermost-ios-app/" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://about.mattermost.com/mattermost-android-app/" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns" target="_blank">Mattermost Hosted Push Notification Service</a>.' /> ); } else if (this.state.pushNotificationServerType === PUSH_NOTIFICATIONS_MTPNS) { pushServerHelpText = ( <FormattedHTMLMessage id='admin.email.mtpnsHelp' - defaultMessage='Download <a href="https://itunes.apple.com/us/app/mattermost/id984966508?mt=8" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns" target="_blank">Mattermost Test Push Notification Service</a>.' + defaultMessage='Download <a href="https://about.mattermost.com/mattermost-ios-app/" target="_blank">Mattermost iOS app</a> from iTunes. Download <a href="https://about.mattermost.com/mattermost-android-app/" target="_blank">Mattermost Android app</a> from Google Play. Learn more about the <a href="http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns" target="_blank">Mattermost Test Push Notification Service</a>.' /> ); } else { diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx index 4c0c0c8fd..2358660da 100644 --- a/webapp/components/admin_console/saml_settings.jsx +++ b/webapp/components/admin_console/saml_settings.jsx @@ -77,15 +77,15 @@ export default class SamlSettings extends AdminSettings { AdminActions.samlCertificateStatus( (data) => { const files = {}; - if (!data.IdpCertificateFile) { + if (!data.idp_certificate_file) { files.idpCertificateFile = ''; } - if (!data.PublicCertificateFile) { + if (!data.public_certificate_file) { files.publicCertificateFile = ''; } - if (!data.PrivateKeyFile) { + if (!data.private_key_file) { files.privateKeyFile = ''; } this.setState(files); diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 34d58f5aa..10e568794 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -964,7 +964,10 @@ export default class ChannelHeader extends React.Component { placement='bottom' overlay={recentMentionsTooltip} > - <div className='channel-header__icon icon--hidden'> + <div + className='channel-header__icon icon--hidden' + onClick={this.searchMentions} + > <span className='icon icon__mentions' dangerouslySetInnerHTML={{__html: mentionsIcon}} diff --git a/webapp/components/channel_invite_modal/channel_invite_modal.jsx b/webapp/components/channel_invite_modal/channel_invite_modal.jsx index d2fceb2c3..8b09a7496 100644 --- a/webapp/components/channel_invite_modal/channel_invite_modal.jsx +++ b/webapp/components/channel_invite_modal/channel_invite_modal.jsx @@ -143,13 +143,18 @@ export default class ChannelInviteModal extends React.Component { inviteError = (<label className='has-error control-label'>{this.state.inviteError}</label>); } + let users = []; + if (this.state.users) { + users = this.state.users.filter((user) => user.delete_at === 0); + } + let content; if (this.state.loading) { content = (<LoadingScreen/>); } else { content = ( <SearchableUserList - users={this.state.users} + users={users} usersPerPage={USERS_PER_PAGE} total={this.state.total} nextPage={this.nextPage} diff --git a/webapp/components/dot_menu/dot_menu.jsx b/webapp/components/dot_menu/dot_menu.jsx index eb6a6c005..a2cbb9b48 100644 --- a/webapp/components/dot_menu/dot_menu.jsx +++ b/webapp/components/dot_menu/dot_menu.jsx @@ -58,10 +58,12 @@ export default class DotMenu extends Component { constructor(props) { super(props); - this.handleDropdownOpened = this.handleDropdownOpened.bind(this); - this.canDelete = false; - this.canEdit = false; this.editDisableAction = new DelayedAction(this.handleEditDisable); + + this.state = { + canDelete: PostUtils.canDeletePost(props.post), + canEdit: PostUtils.canEditPost(props.post, this.editDisableAction) + }; } componentDidMount() { @@ -69,7 +71,11 @@ export default class DotMenu extends Component { $('#' + this.props.idPrefix + '_dropdown' + this.props.post.id).on('hidden.bs.dropdown', () => this.props.handleDropdownOpened(false)); } - handleDropdownOpened() { + componentWillUnmount() { + this.editDisableAction.cancel(); + } + + handleDropdownOpened = () => { this.props.handleDropdownOpened(true); const position = $('#post-list').height() - $(this.refs.dropdownToggle).offset().top; @@ -80,17 +86,15 @@ export default class DotMenu extends Component { } } - handleEditDisable() { - this.canEdit = false; + handleEditDisable = () => { + this.setState({canEdit: false}); } render() { const isSystemMessage = PostUtils.isSystemMessage(this.props.post); const isMobile = Utils.isMobile(); - this.canDelete = PostUtils.canDeletePost(this.props.post); - this.canEdit = PostUtils.canEditPost(this.props.post, this.editDisableAction); - if (this.props.idPrefix === Constants.CENTER && (!isMobile && isSystemMessage && !this.canDelete && !this.canEdit)) { + if (this.props.idPrefix === Constants.CENTER && (!isMobile && isSystemMessage && !this.state.canDelete && !this.state.canEdit)) { return null; } @@ -157,7 +161,7 @@ export default class DotMenu extends Component { } let dotMenuDelete = null; - if (this.canDelete) { + if (this.state.canDelete) { dotMenuDelete = ( <DotMenuItem idPrefix={idPrefix + 'Delete'} @@ -169,7 +173,7 @@ export default class DotMenu extends Component { } let dotMenuEdit = null; - if (this.canEdit) { + if (this.state.canEdit) { dotMenuEdit = ( <DotMenuEdit idPrefix={idPrefix + 'Edit'} diff --git a/webapp/components/dot_menu/index.js b/webapp/components/dot_menu/index.js index eaa1e8d2c..852fe6791 100644 --- a/webapp/components/dot_menu/index.js +++ b/webapp/components/dot_menu/index.js @@ -3,7 +3,8 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {flagPost, unflagPost, pinPost, unpinPost} from 'mattermost-redux/actions/posts'; +import {flagPost, unflagPost} from 'mattermost-redux/actions/posts'; +import {pinPost, unpinPost} from 'actions/post_actions.jsx'; import DotMenu from './dot_menu.jsx'; diff --git a/webapp/components/emoji_picker/emoji_picker.jsx b/webapp/components/emoji_picker/emoji_picker.jsx index a047c1277..0d9b34176 100644 --- a/webapp/components/emoji_picker/emoji_picker.jsx +++ b/webapp/components/emoji_picker/emoji_picker.jsx @@ -309,7 +309,7 @@ export default class EmojiPicker extends React.Component { right: this.props.rightOffset }; } else { - pickerStyle = this.props.style; + pickerStyle = {...this.props.style}; } } @@ -325,91 +325,111 @@ export default class EmojiPicker extends React.Component { <div className='emoji-picker__categories'> <EmojiPickerCategory category='recent' - icon={<i - className='fa fa-clock-o' - title={Utils.localizeMessage('emoji_picker.recent', 'Recently Used')} - />} + icon={ + <i + className='fa fa-clock-o' + title={Utils.localizeMessage('emoji_picker.recent', 'Recently Used')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'recent'} /> <EmojiPickerCategory category='people' - icon={<i - className='fa fa-smile-o' - title={Utils.localizeMessage('emoji_picker.people', 'People')} - />} + icon={ + <i + className='fa fa-smile-o' + title={Utils.localizeMessage('emoji_picker.people', 'People')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'people'} /> <EmojiPickerCategory category='nature' - icon={<i - className='fa fa-leaf' - title={Utils.localizeMessage('emoji_picker.nature', 'Nature')} - />} + icon={ + <i + className='fa fa-leaf' + title={Utils.localizeMessage('emoji_picker.nature', 'Nature')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'nature'} /> <EmojiPickerCategory category='food' - icon={<i - className='fa fa-cutlery' - title={Utils.localizeMessage('emoji_picker.food', 'Food')} - />} + icon={ + <i + className='fa fa-cutlery' + title={Utils.localizeMessage('emoji_picker.food', 'Food')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'food'} /> <EmojiPickerCategory category='activity' - icon={<i - className='fa fa-futbol-o' - title={Utils.localizeMessage('emoji_picker.activity', 'Activity')} - />} + icon={ + <i + className='fa fa-futbol-o' + title={Utils.localizeMessage('emoji_picker.activity', 'Activity')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'activity'} /> <EmojiPickerCategory category='travel' - icon={<i - className='fa fa-plane' - title={Utils.localizeMessage('emoji_picker.travel', 'Travel')} - />} + icon={ + <i + className='fa fa-plane' + title={Utils.localizeMessage('emoji_picker.travel', 'Travel')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'travel'} /> <EmojiPickerCategory category='objects' - icon={<i - className='fa fa-lightbulb-o' - title={Utils.localizeMessage('emoji_picker.objects', 'Objects')} - />} + icon={ + <i + className='fa fa-lightbulb-o' + title={Utils.localizeMessage('emoji_picker.objects', 'Objects')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'objects'} /> <EmojiPickerCategory category='symbols' - icon={<i - className='fa fa-heart-o' - title={Utils.localizeMessage('emoji_picker.symbols', 'Symbols')} - />} + icon={ + <i + className='fa fa-heart-o' + title={Utils.localizeMessage('emoji_picker.symbols', 'Symbols')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'symbols'} /> <EmojiPickerCategory category='flags' - icon={<i - className='fa fa-flag-o' - title={Utils.localizeMessage('emoji_picker.flags', 'Flags')} - />} + icon={ + <i + className='fa fa-flag-o' + title={Utils.localizeMessage('emoji_picker.flags', 'Flags')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'flags'} /> <EmojiPickerCategory category='custom' - icon={<i - className='fa fa-at' - title={Utils.localizeMessage('emoji_picker.custom', 'Custom')} - />} + icon={ + <i + className='fa fa-at' + title={Utils.localizeMessage('emoji_picker.custom', 'Custom')} + /> + } onCategoryClick={this.handleCategoryClick} selected={this.state.category === 'custom'} /> diff --git a/webapp/components/emoji_picker/emoji_picker_overlay.jsx b/webapp/components/emoji_picker/emoji_picker_overlay.jsx index 0a289a242..7174e004c 100644 --- a/webapp/components/emoji_picker/emoji_picker_overlay.jsx +++ b/webapp/components/emoji_picker/emoji_picker_overlay.jsx @@ -15,7 +15,15 @@ export default class EmojiPickerOverlay extends React.PureComponent { onEmojiClick: PropTypes.func.isRequired, onHide: PropTypes.func.isRequired, rightOffset: PropTypes.number, - topOffset: PropTypes.number + topOffset: PropTypes.number, + spaceRequiredAbove: PropTypes.number, + spaceRequiredBelow: PropTypes.number + } + + // Reasonable defaults calculated from from the center channel + static defaultProps = { + spaceRequiredAbove: 422, + spaceRequiredBelow: 436 } constructor(props) { @@ -28,15 +36,12 @@ export default class EmojiPickerOverlay extends React.PureComponent { componentWillUpdate(nextProps) { if (nextProps.show && !this.props.show) { - const spaceRequiredAbove = 422; - const spaceRequiredBelow = 436; - const targetBounds = nextProps.target().getBoundingClientRect(); let placement; - if (targetBounds.top > spaceRequiredAbove) { + if (targetBounds.top > nextProps.spaceRequiredAbove) { placement = 'top'; - } else if (window.innerHeight - targetBounds.bottom > spaceRequiredBelow) { + } else if (window.innerHeight - targetBounds.bottom > nextProps.spaceRequiredBelow) { placement = 'bottom'; } else { placement = 'left'; diff --git a/webapp/components/integrations/components/edit_incoming_webhook.jsx b/webapp/components/integrations/components/edit_incoming_webhook.jsx index 5a6309212..00cb50cbd 100644 --- a/webapp/components/integrations/components/edit_incoming_webhook.jsx +++ b/webapp/components/integrations/components/edit_incoming_webhook.jsx @@ -31,13 +31,11 @@ export default class EditIncomingWebhook extends AbstractIncomingWebhook { handleIntegrationChange() { const teamId = TeamStore.getCurrentId(); - this.setState({ - hooks: IntegrationStore.getIncomingWebhooks(teamId), - loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId) - }); + const hooks = IntegrationStore.getIncomingWebhooks(teamId); + const loading = !IntegrationStore.hasReceivedIncomingWebhooks(teamId); - if (!this.state.loading) { - this.originalIncomingHook = this.state.hooks.filter((hook) => hook.id === this.props.location.query.id)[0]; + if (!loading) { + this.originalIncomingHook = hooks.filter((hook) => hook.id === this.props.location.query.id)[0]; this.setState({ displayName: this.originalIncomingHook.display_name, diff --git a/webapp/components/leave_team_modal.jsx b/webapp/components/leave_team_modal.jsx index 379ece4c3..a38dd2da1 100644 --- a/webapp/components/leave_team_modal.jsx +++ b/webapp/components/leave_team_modal.jsx @@ -61,6 +61,7 @@ class LeaveTeamModal extends React.Component { } GlobalActions.emitLeaveTeam(); + GlobalActions.toggleSideBarRightMenuAction(); } handleHide() { diff --git a/webapp/components/markdown_image.jsx b/webapp/components/markdown_image.jsx index 75a6ce9ea..2634ef3f6 100644 --- a/webapp/components/markdown_image.jsx +++ b/webapp/components/markdown_image.jsx @@ -1,19 +1,67 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import {postListScrollChange} from 'actions/global_actions.jsx'; -export default class MarkdownImage extends PureComponent { - handleLoad = () => { - postListScrollChange(); +const WAIT_FOR_HEIGHT_TIMEOUT = 100; + +export default class MarkdownImage extends React.PureComponent { + static propTypes = { + + /* + * The href of the image to be loaded + */ + href: PropTypes.string } - render() { - const props = {...this.props}; - props.onLoad = this.handleLoad; + constructor(props) { + super(props); + + this.heightTimeout = 0; + } + + componentDidMount() { + this.waitForHeight(); + } + + componentDidUpdate(prevProps) { + if (this.props.href !== prevProps.href) { + this.waitForHeight(); + } + } - return <img {...props}/>; + componentWillUnmount() { + this.stopWaitingForHeight(); + } + + waitForHeight = () => { + if (this.refs.image.height) { + setTimeout(postListScrollChange, 0); + + this.heightTimeout = 0; + } else { + this.heightTimeout = setTimeout(this.waitForHeight, WAIT_FOR_HEIGHT_TIMEOUT); + } + } + + stopWaitingForHeight = () => { + if (this.heightTimeout !== 0) { + clearTimeout(this.heightTimeout); + this.heightTimeout = 0; + } + } + + render() { + return ( + <img + {...this.props} + ref='image' + onLoad={this.stopWaitingForHeight} + onError={this.stopWaitingForHeight} + /> + ); } } diff --git a/webapp/components/member_list_channel/member_list_channel.jsx b/webapp/components/member_list_channel/member_list_channel.jsx index f47f26cf6..272e210ce 100644 --- a/webapp/components/member_list_channel/member_list_channel.jsx +++ b/webapp/components/member_list_channel/member_list_channel.jsx @@ -146,7 +146,7 @@ export default class MemberListChannel extends React.Component { for (let i = 0; i < users.length; i++) { const user = users[i]; - if (teamMembers[user.id] && channelMembers[user.id]) { + if (teamMembers[user.id] && channelMembers[user.id] && user.delete_at === 0) { usersToDisplay.push(user); actionUserProps[user.id] = { channel: this.props.channel, diff --git a/webapp/components/more_direct_channels/more_direct_channels.jsx b/webapp/components/more_direct_channels/more_direct_channels.jsx index 705c1ac95..0e50eca72 100644 --- a/webapp/components/more_direct_channels/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels/more_direct_channels.jsx @@ -292,6 +292,11 @@ export default class MoreDirectChannels extends React.Component { /> ); + let users = []; + if (this.state.users) { + users = this.state.users.filter((user) => user.delete_at === 0); + } + return ( <Modal dialogClassName={'more-modal more-direct-channels'} @@ -310,7 +315,7 @@ export default class MoreDirectChannels extends React.Component { <Modal.Body> <MultiSelect key='moreDirectChannelsList' - options={this.state.users} + options={users} optionRenderer={this.renderOption} values={this.state.values} valueRenderer={this.renderValue} diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index f61f58a8d..6305f870e 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -698,7 +698,7 @@ export default class Navbar extends React.Component { /> </span> <span - className='icon icon__menu' + className='icon icon__menu icon--sidebarHeaderTextColor' dangerouslySetInnerHTML={{__html: menuIcon}} aria-hidden='true' /> diff --git a/webapp/components/pdf_preview.jsx b/webapp/components/pdf_preview.jsx index 790355561..09913afcf 100644 --- a/webapp/components/pdf_preview.jsx +++ b/webapp/components/pdf_preview.jsx @@ -84,7 +84,7 @@ export default class PDFPreview extends React.Component { success: false }); - PDFJS.getDocument(window.mm_config.SiteURL + props.fileUrl).then(this.onDocumentLoad); + PDFJS.getDocument(props.fileUrl).then(this.onDocumentLoad); } onDocumentLoad(pdf) { diff --git a/webapp/components/post_view/post_body/index.js b/webapp/components/post_view/post_body/index.js index 37cf114b0..90f04e0f9 100644 --- a/webapp/components/post_view/post_body/index.js +++ b/webapp/components/post_view/post_body/index.js @@ -16,7 +16,7 @@ function mapStateToProps(state, ownProps) { let parentPostUser; if (ownProps.post.root_id) { parentPost = getPost(state, ownProps.post.root_id); - parentPostUser = getUser(state, parentPost.user_id); + parentPostUser = parentPost ? getUser(state, parentPost.user_id) : null; } return { diff --git a/webapp/components/post_view/post_body_additional_content.jsx b/webapp/components/post_view/post_body_additional_content.jsx index 768b999d0..be9e37827 100644 --- a/webapp/components/post_view/post_body_additional_content.jsx +++ b/webapp/components/post_view/post_body_additional_content.jsx @@ -191,7 +191,7 @@ export default class PostBodyAdditionalContent extends React.PureComponent { ); const contents = [message]; - if (this.state.linkLoaded) { + if (this.state.linkLoaded || this.props.previewCollapsed.startsWith('true')) { if (prependToggle) { contents.unshift(toggle); } else { diff --git a/webapp/components/post_view/post_info/post_info.jsx b/webapp/components/post_view/post_info/post_info.jsx index d64d1aca6..6eaef0e0b 100644 --- a/webapp/components/post_view/post_info/post_info.jsx +++ b/webapp/components/post_view/post_info/post_info.jsx @@ -81,8 +81,7 @@ export default class PostInfo extends React.PureComponent { this.state = { showEmojiPicker: false, - reactionPickerOffset: 21, - canEdit: PostUtils.canEditPost(props.post, this.editDisableAction) + reactionPickerOffset: 21 }; } diff --git a/webapp/components/post_view/post_list.jsx b/webapp/components/post_view/post_list.jsx index 13cc28da3..d8a56fe83 100644 --- a/webapp/components/post_view/post_list.jsx +++ b/webapp/components/post_view/post_list.jsx @@ -104,12 +104,13 @@ export default class PostList extends React.PureComponent { this.previousScrollTop = Number.MAX_SAFE_INTEGER; this.previousScrollHeight = 0; this.previousClientHeight = 0; + this.atBottom = false; this.state = { atEnd: false, unViewedCount: 0, isScrolling: false, - lastViewed: Number.MAX_SAFE_INTEGER + lastViewed: props.lastViewedAt }; } @@ -144,24 +145,20 @@ export default class PostList extends React.PureComponent { this.hasScrolled = false; this.hasScrolledToFocusedPost = false; this.hasScrolledToNewMessageSeparator = false; - this.setState({atEnd: false}); + this.atBottom = false; + this.setState({atEnd: false, lastViewed: nextProps.lastViewedAt}); if (nextChannel.id) { this.loadPosts(nextChannel.id); } - return; } - if (!this.wasAtBottom() && this.props.posts !== nextProps.posts) { - const unViewedCount = nextProps.posts.reduce((count, post) => { - if (post.create_at > this.state.lastViewed && - post.user_id !== nextProps.currentUserId && - post.state !== Constants.POST_DELETED) { - return count + 1; - } - return count; - }, 0); - this.setState({unViewedCount}); + const nextPosts = nextProps.posts || []; + const posts = this.props.posts || []; + const hasNewPosts = (posts.length === 0 && nextPosts.length > 0) || (posts.length > 0 && nextPosts.length > 0 && posts[0].id !== nextPosts[0].id); + + if (!this.checkBottom() && hasNewPosts) { + this.setUnreadsBelow(nextPosts, nextProps.currentUserId); } } } @@ -184,6 +181,10 @@ export default class PostList extends React.PureComponent { const posts = this.props.posts; const postList = this.refs.postlist; + if (!postList) { + return; + } + // Scroll to focused post on first load const focusedPost = this.refs[this.props.focusedPostId]; if (focusedPost && this.props.posts) { @@ -203,9 +204,13 @@ export default class PostList extends React.PureComponent { if (messageSeparator && !this.hasScrolledToNewMessageSeparator) { const element = ReactDOM.findDOMNode(messageSeparator); element.scrollIntoView(); + if (!this.checkBottom()) { + this.setUnreadsBelow(posts, this.props.currentUserId); + } return; } else if (postList && !this.hasScrolledToNewMessageSeparator) { postList.scrollTop = postList.scrollHeight; + this.atBottom = true; return; } @@ -218,7 +223,7 @@ export default class PostList extends React.PureComponent { const pendingPostId = posts[0].pending_post_id; if (postId !== prevPostId && pendingPostId !== prevPostId) { // If already scrolled to bottom - if (this.wasAtBottom()) { + if (this.atBottom) { doScrollToBottom = true; } @@ -229,6 +234,7 @@ export default class PostList extends React.PureComponent { } if (doScrollToBottom) { + this.atBottom = true; postList.scrollTop = postList.scrollHeight; return; } @@ -240,26 +246,55 @@ export default class PostList extends React.PureComponent { } } + setUnreadsBelow = (posts, currentUserId) => { + const unViewedCount = posts.reduce((count, post) => { + if (post.create_at > this.state.lastViewed && + post.user_id !== currentUserId && + post.state !== Constants.POST_DELETED) { + return count + 1; + } + return count; + }, 0); + this.setState({unViewedCount}); + } + handleScrollStop = () => { this.setState({ isScrolling: false }); } - wasAtBottom = () => { - return this.previousClientHeight + this.previousScrollTop >= this.previousScrollHeight - CLOSE_TO_BOTTOM_SCROLL_MARGIN; + checkBottom = () => { + if (!this.refs.postlist) { + return true; + } + + // No scroll bar so we're at the bottom + if (this.refs.postlist.scrollHeight <= this.refs.postlist.clientHeight) { + return true; + } + + return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - CLOSE_TO_BOTTOM_SCROLL_MARGIN; } handleResize = (forceScrollToBottom) => { const postList = this.refs.postlist; - const doScrollToBottom = this.wasAtBottom() || forceScrollToBottom; + const messageSeparator = this.refs.newMessageSeparator; + const doScrollToBottom = this.atBottom || forceScrollToBottom; - if (postList && doScrollToBottom) { - postList.scrollTop = postList.scrollHeight; + if (postList) { + if (doScrollToBottom) { + postList.scrollTop = postList.scrollHeight; + } else if (!this.hasScrolled && messageSeparator) { + const element = ReactDOM.findDOMNode(messageSeparator); + element.scrollIntoView(); + } this.previousScrollHeight = postList.scrollHeight; this.previousScrollTop = postList.scrollTop; this.previousClientHeight = postList.clientHeight; + + this.atBottom = this.checkBottom(); } } @@ -296,9 +331,18 @@ export default class PostList extends React.PureComponent { } handleScroll = () => { - this.hasScrolled = true; + // Only count as user scroll if we've already performed our first load scroll + this.hasScrolled = this.hasScrolledToNewMessageSeparator || this.hasScrolledToFocusedPost; + if (!this.refs.postlist) { + return; + } + this.previousScrollTop = this.refs.postlist.scrollTop; + if (this.refs.postlist.scrollHeight === this.previousScrollHeight) { + this.atBottom = this.checkBottom(); + } + this.updateFloatingTimestamp(); if (!this.state.isScrolling) { @@ -307,7 +351,7 @@ export default class PostList extends React.PureComponent { }); } - if (this.wasAtBottom()) { + if (this.checkBottom()) { this.setState({ lastViewed: new Date().getTime(), unViewedCount: 0, @@ -509,7 +553,7 @@ export default class PostList extends React.PureComponent { /> <ScrollToBottomArrows isScrolling={this.state.isScrolling} - atBottom={this.wasAtBottom()} + atBottom={this.atBottom} onClick={this.scrollToBottom} /> <NewMessageIndicator diff --git a/webapp/components/quick_switch_modal/quick_switch_modal.jsx b/webapp/components/quick_switch_modal/quick_switch_modal.jsx index 2fbfdb2bd..736b728f0 100644 --- a/webapp/components/quick_switch_modal/quick_switch_modal.jsx +++ b/webapp/components/quick_switch_modal/quick_switch_modal.jsx @@ -305,7 +305,7 @@ export default class QuickSwitchModal extends React.PureComponent { <Modal.Header closeButton={true}/> <Modal.Body> {header} - <div className='modal__hint hidden-xs'> + <div className='modal__hint'> {help} </div> <SuggestionBox diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 202be9748..568d85304 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -11,7 +11,6 @@ import FailedPostOptions from 'components/post_view/failed_post_options'; import DotMenu from 'components/dot_menu'; import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; import {addReaction} from 'actions/post_actions.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -27,6 +26,19 @@ import {Link} from 'react-router/es6'; import {FormattedMessage} from 'react-intl'; export default class RhsComment extends React.Component { + static propTypes = { + post: PropTypes.object, + lastPostCount: PropTypes.number, + user: PropTypes.object.isRequired, + currentUser: PropTypes.object.isRequired, + compactDisplay: PropTypes.bool, + useMilitaryTime: PropTypes.bool.isRequired, + isFlagged: PropTypes.bool, + status: PropTypes.string, + isBusy: PropTypes.bool, + removePost: PropTypes.func.isRequired + }; + constructor(props) { super(props); @@ -56,7 +68,7 @@ export default class RhsComment extends React.Component { } removePost() { - GlobalActions.emitRemovePost(this.props.post); + this.props.removePost(this.props.post); } createRemovePostButton() { @@ -333,9 +345,10 @@ export default class RhsComment extends React.Component { show={this.state.showEmojiPicker} onHide={this.toggleEmojiPicker} target={() => this.refs.dotMenu} - container={this.props.getPostList} onEmojiClick={this.reactEmojiClick} rightOffset={15} + spaceRequiredAbove={342} + spaceRequiredBelow={342} /> <a href='#' @@ -436,16 +449,3 @@ export default class RhsComment extends React.Component { ); } } - -RhsComment.propTypes = { - post: PropTypes.object, - lastPostCount: PropTypes.number, - user: PropTypes.object.isRequired, - currentUser: PropTypes.object.isRequired, - compactDisplay: PropTypes.bool, - useMilitaryTime: PropTypes.bool.isRequired, - isFlagged: PropTypes.bool, - status: PropTypes.string, - isBusy: PropTypes.bool, - getPostList: PropTypes.func.isRequired -}; diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 352de0c67..fa72da167 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -38,8 +38,7 @@ export default class RhsRootPost extends React.Component { isFlagged: PropTypes.bool, status: PropTypes.string, previewCollapsed: PropTypes.string, - isBusy: PropTypes.bool, - getPostList: PropTypes.func.isRequired + isBusy: PropTypes.bool } static defaultProps = { @@ -226,9 +225,10 @@ export default class RhsRootPost extends React.Component { show={this.state.showEmojiPicker} onHide={this.toggleEmojiPicker} target={() => this.refs.dotMenu} - container={this.props.getPostList} onEmojiClick={this.reactEmojiClick} rightOffset={15} + spaceRequiredAbove={342} + spaceRequiredBelow={342} /> <a href='#' diff --git a/webapp/components/rhs_thread/index.js b/webapp/components/rhs_thread/index.js index c4465cafd..ed7618427 100644 --- a/webapp/components/rhs_thread/index.js +++ b/webapp/components/rhs_thread/index.js @@ -2,7 +2,9 @@ // See License.txt for license information. import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; import {getPost, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts'; +import {removePost} from 'mattermost-redux/actions/posts'; import RhsThread from './rhs_thread.jsx'; @@ -24,4 +26,12 @@ function makeMapStateToProps() { }; } -export default connect(makeMapStateToProps)(RhsThread); +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + removePost + }, dispatch) + }; +} + +export default connect(makeMapStateToProps, mapDispatchToProps)(RhsThread); diff --git a/webapp/components/rhs_thread/rhs_thread.jsx b/webapp/components/rhs_thread/rhs_thread.jsx index bbf61af19..58325e8cc 100644 --- a/webapp/components/rhs_thread/rhs_thread.jsx +++ b/webapp/components/rhs_thread/rhs_thread.jsx @@ -59,7 +59,10 @@ export default class RhsThread extends React.Component { currentUser: PropTypes.object.isRequired, useMilitaryTime: PropTypes.bool.isRequired, toggleSize: PropTypes.func, - shrink: PropTypes.func + shrink: PropTypes.func, + actions: PropTypes.shape({ + removePost: PropTypes.func.isRequired + }).isRequired } static defaultProps = { @@ -316,10 +319,6 @@ export default class RhsThread extends React.Component { }); } - getPostListContainer = () => { - return this.refs.postListContainer; - } - getSidebarBody = () => { return this.refs.sidebarbody; } @@ -400,7 +399,7 @@ export default class RhsThread extends React.Component { isFlagged={isFlagged} status={status} isBusy={this.state.isBusy} - getPostList={this.getPostListContainer} + removePost={this.props.actions.removePost} /> </div> ); @@ -435,10 +434,7 @@ export default class RhsThread extends React.Component { renderView={renderView} onScroll={this.handleScroll} > - <div - ref='postListContainer' - className='post-right__scroll' - > + <div className='post-right__scroll'> <DateSeparator date={rootPostDay} /> @@ -454,7 +450,6 @@ export default class RhsThread extends React.Component { status={rootStatus} previewCollapsed={this.state.previewsCollapsed} isBusy={this.state.isBusy} - getPostList={this.getPostListContainer} /> <div ref='rhspostlist' diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx index c0f405eb4..3b632ee5e 100644 --- a/webapp/components/search_results_item.jsx +++ b/webapp/components/search_results_item.jsx @@ -21,7 +21,6 @@ import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import {Posts} from 'mattermost-redux/constants'; import React from 'react'; import PropTypes from 'prop-types'; @@ -187,7 +186,7 @@ export default class SearchResultsItem extends React.Component { let message; let flagContent; let rhsControls; - if (post.state === Posts.POST_DELETED) { + if (post.state === Constants.POST_DELETED) { message = ( <p> <FormattedMessage diff --git a/webapp/components/sidebar_right/sidebar_right.jsx b/webapp/components/sidebar_right/sidebar_right.jsx index 21d3df345..737254682 100644 --- a/webapp/components/sidebar_right/sidebar_right.jsx +++ b/webapp/components/sidebar_right/sidebar_right.jsx @@ -77,12 +77,12 @@ export default class SidebarRight extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - return !Utils.areObjectsEqual(nextState, this.state) || !Utils.areObjectsEqual(nextProps, this.props); + return !Utils.areObjectsEqual(nextState, this.state) || this.props.postRightVisible !== nextProps.postRightVisible; } componentWillUpdate(nextProps, nextState) { const isOpen = this.state.searchVisible || this.props.postRightVisible; - const willOpen = nextState.searchVisible || nextState.postRightVisible; + const willOpen = nextState.searchVisible || nextProps.postRightVisible; if (!isOpen && willOpen) { trackEvent('ui', 'ui_rhs_opened'); diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx index d40e90279..f0e6f10a1 100644 --- a/webapp/components/sidebar_right_menu.jsx +++ b/webapp/components/sidebar_right_menu.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import TeamMembersModal from './team_members_modal.jsx'; import ToggleModalButton from './toggle_modal_button.jsx'; import AboutBuildModal from './about_build_modal.jsx'; @@ -19,7 +18,6 @@ import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; import {Constants, WebrtcActionTypes} from 'utils/constants.jsx'; -const ActionTypes = Constants.ActionTypes; const Preferences = Constants.Preferences; const TutorialSteps = Constants.TutorialSteps; @@ -143,20 +141,7 @@ export default class SidebarRightMenu extends React.Component { hideSidebars() { if (Utils.isMobile()) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SEARCH, - results: null - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST_SELECTED, - postId: null - }); - - document.querySelector('.app__body .inner-wrap').classList.remove('move--right', 'move--left', 'move--left-small'); - document.querySelector('.app__body .sidebar--left').classList.remove('move--right'); - document.querySelector('.app__body .sidebar--right').classList.remove('move--left'); - document.querySelector('.app__body .sidebar--menu').classList.remove('move--left'); + GlobalActions.toggleSideBarRightMenuAction(); } } diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx index 78079b2b4..b3b291ff8 100644 --- a/webapp/components/spinner_button.jsx +++ b/webapp/components/spinner_button.jsx @@ -16,6 +16,12 @@ export default class SpinnerButton extends React.Component { }; } + static get defaultProps() { + return { + spinning: false + }; + } + render() { const {spinning, children, ...props} = this.props; // eslint-disable-line no-use-before-define diff --git a/webapp/components/team_import_tab.jsx b/webapp/components/team_import_tab.jsx index a17442dc9..b310cdc12 100644 --- a/webapp/components/team_import_tab.jsx +++ b/webapp/components/team_import_tab.jsx @@ -29,12 +29,12 @@ class TeamImportTab extends React.Component { }; } - onImportFailure(e, err, res) { - this.setState({status: 'fail', link: 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(res.text)}); + onImportFailure() { + this.setState({status: 'fail'}); } - onImportSuccess(data, res) { - this.setState({status: 'done', link: 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(res.text)}); + onImportSuccess(data) { + this.setState({status: 'done', link: 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(atob(data.results))}); } doImportSlack(file) { diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx index 22d6b6b77..1cd2ef637 100644 --- a/webapp/components/user_profile.jsx +++ b/webapp/components/user_profile.jsx @@ -56,10 +56,6 @@ export default class UserProfile extends React.Component { render() { let name = '...'; let profileImg = ''; - let popoverPosition = 'right'; - if (Utils.isMobile()) { - popoverPosition = 'bottom'; - } if (this.props.user) { name = Utils.displayUsername(this.props.user.id); @@ -78,7 +74,7 @@ export default class UserProfile extends React.Component { <OverlayTrigger ref='overlay' trigger='click' - placement={popoverPosition} + placement='right' rootClose={true} overlay={ <ProfilePopover |