diff options
Diffstat (limited to 'web')
43 files changed, 289 insertions, 123 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 27959ec7e..ab5686720 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -54,7 +54,7 @@ export default class AccessHistoryModal extends React.Component { } onAuditChange() { var newState = this.getStateFromStoresForAudits(); - if (!Utils.areStatesEqual(newState.audits, this.state.audits)) { + if (!Utils.areObjectsEqual(newState.audits, this.state.audits)) { this.setState(newState); } } diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index ef3077470..af423a601 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -73,7 +73,7 @@ export default class ActivityLogModal extends React.Component { } onListenerChange() { const newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(newState.sessions, this.state.sessions)) { + if (!Utils.areObjectsEqual(newState.sessions, this.state.sessions)) { this.setState(newState); } } diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 895dc5fe4..a8d4ec100 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -39,11 +39,14 @@ export default class ChannelHeader extends React.Component { this.state = state; } getStateFromStores() { + const extraInfo = ChannelStore.getCurrentExtraInfo(); + return { channel: ChannelStore.getCurrent(), memberChannel: ChannelStore.getCurrentMember(), memberTeam: UserStore.getCurrentUser(), - users: ChannelStore.getCurrentExtraInfo().members, + users: extraInfo.members, + userCount: extraInfo.member_count, searchVisible: SearchStore.getSearchResults() !== null }; } @@ -63,7 +66,7 @@ export default class ChannelHeader extends React.Component { } onListenerChange() { const newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); @@ -373,6 +376,7 @@ export default class ChannelHeader extends React.Component { <th> <PopoverListMembers members={this.state.users} + memberCount={this.state.userCount} channelId={channel.id} /> </th> diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx index 7c1032321..47bc50971 100644 --- a/web/react/components/channel_invite_modal.jsx +++ b/web/react/components/channel_invite_modal.jsx @@ -78,7 +78,7 @@ export default class ChannelInviteModal extends React.Component { } onListenerChange() { var newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(this.state, newState)) { + if (!Utils.areObjectsEqual(this.state, newState)) { this.setState(newState); } } diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx index 2fa7ae8ff..5cf3511f4 100644 --- a/web/react/components/channel_members_modal.jsx +++ b/web/react/components/channel_members_modal.jsx @@ -91,7 +91,7 @@ export default class ChannelMembersModal extends React.Component { } onChange() { const newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(this.state, newState)) { + if (!Utils.areObjectsEqual(this.state, newState)) { this.setState(newState); } } diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx index 43700bf36..f57fc12c5 100644 --- a/web/react/components/channel_notifications.jsx +++ b/web/react/components/channel_notifications.jsx @@ -69,7 +69,7 @@ export default class ChannelNotifications extends React.Component { newState.notifyLevel = notifyLevel; newState.markUnreadLevel = markUnreadLevel; - if (!Utils.areStatesEqual(this.state, newState)) { + if (!Utils.areObjectsEqual(this.state, newState)) { this.setState(newState); } } diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 3a3dabce5..f3bead1c2 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -81,7 +81,7 @@ export default class DeletePostModal extends React.Component { } onListenerChange() { var newList = PostStore.getSelectedPost(); - if (!Utils.areStatesEqual(this.state.selectedList, newList)) { + if (!Utils.areObjectsEqual(this.state.selectedList, newList)) { this.setState({selectedList: newList}); } } diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index e707e32f5..d6a30abf9 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -67,7 +67,7 @@ export default class FileAttachment extends React.Component { this.canSetState = false; } shouldComponentUpdate(nextProps, nextState) { - if (!utils.areStatesEqual(nextProps, this.props)) { + if (!utils.areObjectsEqual(nextProps, this.props)) { return true; } diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index c09477a69..3f6ad3358 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -31,7 +31,8 @@ export default class InviteMemberModal extends React.Component { firstNameErrors: {}, lastNameErrors: {}, emailEnabled: global.window.mm_config.SendEmailNotifications === 'true', - showConfirmModal: false + showConfirmModal: false, + isSendingEmails: false }; } @@ -89,10 +90,13 @@ export default class InviteMemberModal extends React.Component { var data = {}; data.invites = invites; + this.setState({isSendingEmails: true}); + Client.inviteMembers( data, () => { this.handleHide(false); + this.setState({isSendingEmails: false}); }, (err) => { if (err.message === 'This person is already on your team') { @@ -101,6 +105,8 @@ export default class InviteMemberModal extends React.Component { } else { this.setState({serverError: err.message}); } + + this.setState({isSendingEmails: false}); } ); } @@ -289,11 +295,6 @@ export default class InviteMemberModal extends React.Component { var content = null; var sendButton = null; - var sendButtonLabel = 'Send Invitation'; - if (this.state.inviteIds.length > 1) { - sendButtonLabel = 'Send Invitations'; - } - if (this.state.emailEnabled) { content = ( <div> @@ -309,14 +310,25 @@ export default class InviteMemberModal extends React.Component { </div> ); - sendButton = - ( - <button - onClick={this.handleSubmit} - type='button' - className='btn btn-primary' - >{sendButtonLabel}</button> + var sendButtonLabel = 'Send Invitation'; + if (this.state.isSendingEmails) { + sendButtonLabel = ( + <span><i className='fa fa-spinner fa-spin' />{' Sending'}</span> ); + } else if (this.state.inviteIds.length > 1) { + sendButtonLabel = 'Send Invitations'; + } + + sendButton = ( + <button + onClick={this.handleSubmit} + type='button' + className='btn btn-primary' + disabled={this.state.isSendingEmails} + > + {sendButtonLabel} + </button> + ); } else { var teamInviteLink = null; if (currentUser && TeamStore.getCurrent().type === 'O') { @@ -351,12 +363,13 @@ export default class InviteMemberModal extends React.Component { return ( <div> <Modal - className='modal-invite-member' + dialogClassName='modal-invite-member' show={this.state.show} onHide={this.handleHide.bind(this, true)} enforceFocus={!this.state.showConfirmModal} + backdrop={this.state.isSendingEmails ? 'static' : true} > - <Modal.Header closeButton={true}> + <Modal.Header closeButton={!this.state.isSendingEmails}> <Modal.Title>{'Invite New Member'}</Modal.Title> </Modal.Header> <Modal.Body ref='modalBody'> @@ -370,6 +383,7 @@ export default class InviteMemberModal extends React.Component { type='button' className='btn btn-default' onClick={this.handleHide.bind(this, true)} + disabled={this.state.isSendingEmails} > {'Cancel'} </button> diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index c4f831c2e..8a6dd84a4 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -46,7 +46,7 @@ export default class MoreChannels extends React.Component { } onListenerChange() { var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState.channels, this.state.channels)) { + if (!utils.areObjectsEqual(newState.channels, this.state.channels)) { this.setState(newState); } } diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index ff53816c7..af29f219e 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -140,7 +140,9 @@ export default class Navbar extends React.Component { role='menuitem' href='#' onClick={() => this.setState({showEditChannelPurposeModal: true})} - /> + > + {'Set Channel Purpose...'} + </a> </li> ); } diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 0b755f377..cf9db055d 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -70,7 +70,7 @@ export default class NavbarDropdown extends React.Component { } onListenerChange() { var newState = getStateFromStores(); - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/components/notify_counts.jsx b/web/react/components/notify_counts.jsx index 54b9e4289..0a4f60989 100644 --- a/web/react/components/notify_counts.jsx +++ b/web/react/components/notify_counts.jsx @@ -39,7 +39,7 @@ export default class NotifyCounts extends React.Component { } onListenerChange() { var newState = getCountsStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index f3c0fa0b4..bd6b6d3bd 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -69,7 +69,6 @@ export default class PopoverListMembers extends React.Component { render() { let popoverHtml = []; - let count = 0; let countText = '-'; const members = this.props.members; const teamMembers = UserStore.getProfilesUsernameMap(); @@ -147,10 +146,10 @@ export default class PopoverListMembers extends React.Component { </div> </div> ); - count++; } }); + const count = this.props.memberCount; if (count > 20) { countText = '20+'; } else if (count > 0) { @@ -195,5 +194,6 @@ export default class PopoverListMembers extends React.Component { PopoverListMembers.propTypes = { members: React.PropTypes.array.isRequired, + memberCount: React.PropTypes.number.isRequired, channelId: React.PropTypes.string.isRequired }; diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index c3c5b3e0b..2b9586345 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -77,7 +77,7 @@ export default class Post extends React.Component { this.forceUpdate(); } shouldComponentUpdate(nextProps) { - if (!utils.areStatesEqual(nextProps.post, this.props.post)) { + if (!utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index c57c4490b..617b4b36c 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -18,6 +18,7 @@ export default class PostBody extends React.Component { this.receivedYoutubeData = false; this.isGifLoading = false; + this.handleUserChange = this.handleUserChange.bind(this); this.parseEmojis = this.parseEmojis.bind(this); this.createEmbed = this.createEmbed.bind(this); this.createGifEmbed = this.createGifEmbed.bind(this); @@ -25,7 +26,14 @@ export default class PostBody extends React.Component { this.createYoutubeEmbed = this.createYoutubeEmbed.bind(this); const linkData = Utils.extractLinks(this.props.post.message); - this.state = {links: linkData.links, message: linkData.text, post: this.props.post}; + const profiles = UserStore.getProfiles(); + + this.state = { + links: linkData.links, + message: linkData.text, + post: this.props.post, + hasUserProfiles: profiles && Object.keys(profiles).length > 1 + }; } getAllChildNodes(nodeIn) { @@ -55,12 +63,26 @@ export default class PostBody extends React.Component { componentDidMount() { this.parseEmojis(); + + UserStore.addChangeListener(this.handleUserChange); } componentDidUpdate() { this.parseEmojis(); } + componentWillUnmount() { + UserStore.removeChangeListener(this.handleUserChange); + } + + handleUserChange() { + if (!this.state.hasProfiles) { + const profiles = UserStore.getProfiles(); + + this.setState({hasProfiles: profiles && Object.keys(profiles).length > 1}); + } + } + componentWillReceiveProps(nextProps) { const linkData = Utils.extractLinks(nextProps.post.message); if (this.props.post.filenames.length === 0 && this.state.links && this.state.links.length > 0) { diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index b782268fa..087ca1df2 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -104,11 +104,13 @@ export default class PostsView extends React.Component { // check if it's the last comment in a consecutive string of comments on the same post // it is the last comment if it is last post in the channel or the next post has a different root post - var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); + const isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); - var postCtl = ( + const keyPrefix = post.id ? post.id : i; + + const postCtl = ( <Post - key={post.id + 'postKey'} + key={keyPrefix + 'postKey'} ref={post.id} sameUser={sameUser} sameRoot={sameRoot} @@ -240,7 +242,7 @@ export default class PostsView extends React.Component { if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) { return true; } - if (!Utils.areStatesEqual(this.props.postList, nextProps.postList)) { + if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) { return true; } diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 5037a86cd..2cb56cd47 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -3,6 +3,7 @@ const PostsView = require('./posts_view.jsx'); const LoadingScreen = require('./loading_screen.jsx'); +const ChannelInviteModal = require('./channel_invite_modal.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const PostStore = require('../stores/post_store.jsx'); @@ -50,6 +51,7 @@ export default class PostsViewContainer extends React.Component { }); } + state.showInviteModal = false; this.state = state; } componentDidMount() { @@ -223,7 +225,7 @@ export default class PostsViewContainer extends React.Component { } } shouldComponentUpdate(nextProps, nextState) { - if (Utils.areStatesEqual(this.state, nextState)) { + if (Utils.areObjectsEqual(this.state, nextState)) { return false; } @@ -248,7 +250,7 @@ export default class PostsViewContainer extends React.Component { postViewScrolled={this.handlePostsViewScroll} loadMorePostsTopClicked={this.loadMorePostsTop} numPostsToDisplay={this.state.numPostsToDisplay} - introText={channel ? createChannelIntroMessage(channel) : null} + introText={channel ? createChannelIntroMessage(channel, () => this.setState({showInviteModal: true})) : null} messageSeparatorTime={this.state.currentLastViewed} /> ); @@ -263,7 +265,13 @@ export default class PostsViewContainer extends React.Component { } return ( - <div id='post-list'>{postListCtls}</div> + <div id='post-list'> + {postListCtls} + <ChannelInviteModal + show={this.state.showInviteModal} + onModalDismissed={() => this.setState({showInviteModal: false})} + /> + </div> ); } } diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 8c6324c72..58cc1cac7 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -61,7 +61,7 @@ export default class RhsComment extends React.Component { this.parseEmojis(); } shouldComponentUpdate(nextProps) { - if (!Utils.areStatesEqual(nextProps.post, this.props.post)) { + if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index e3b023841..69de5d523 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -26,7 +26,7 @@ export default class RhsRootPost extends React.Component { this.parseEmojis(); } shouldComponentUpdate(nextProps) { - if (!utils.areStatesEqual(nextProps.post, this.props.post)) { + if (!utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; } diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index fe57bed28..7c11de7cf 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -82,7 +82,7 @@ export default class RhsThread extends React.Component { } onChange() { var newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } @@ -112,7 +112,7 @@ export default class RhsThread extends React.Component { } var newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index 90865475b..0f749f2cf 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -46,7 +46,7 @@ export default class SearchBar extends React.Component { onListenerChange(doSearch, isMentionSearch) { if (this.mounted) { var newState = this.getSearchTermStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } if (doSearch) { diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index b56a7b006..2f0068908 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -55,7 +55,7 @@ export default class SearchResults extends React.Component { onChange() { if (this.mounted) { var newState = getStateFromStores(); - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index f5ce5c10e..542f433f3 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -106,6 +106,8 @@ export default class Sidebar extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const channels = Object.assign([], ChannelStore.getAll()); + channels.sort((a, b) => a.display_name.localeCompare(b.display_name)); + const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL); const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL); const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL); @@ -142,7 +144,7 @@ export default class Sidebar extends React.Component { } } - const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList().length - visibleDirectChannels.length; + const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - visibleDirectChannels.length; visibleDirectChannels.sort(this.sortChannelsByDisplayName); @@ -173,7 +175,7 @@ export default class Sidebar extends React.Component { window.addEventListener('resize', this.handleResize); } shouldComponentUpdate(nextProps, nextState) { - if (!Utils.areStatesEqual(nextState, this.state)) { + if (!Utils.areObjectsEqual(nextState, this.state)) { return true; } return false; @@ -205,10 +207,7 @@ export default class Sidebar extends React.Component { } } onChange() { - var newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(newState, this.state)) { - this.setState(newState); - } + this.setState(this.getStateFromStores()); } updateTitle() { const channel = ChannelStore.getCurrent(); diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx index e2ef60959..ab558ad0f 100644 --- a/web/react/components/sidebar_right.jsx +++ b/web/react/components/sidebar_right.jsx @@ -66,13 +66,13 @@ export default class SidebarRight extends React.Component { onSelectedChange(fromSearch) { var newState = getStateFromStores(fromSearch); newState.from_search = fromSearch; - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } onSearchChange() { var newState = getStateFromStores(); - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx index ac1ebf52d..afe7f46ec 100644 --- a/web/react/components/team_members.jsx +++ b/web/react/components/team_members.jsx @@ -59,7 +59,7 @@ export default class TeamMembers extends React.Component { onChange() { var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index 09674f1ef..862f3c528 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -23,7 +23,7 @@ export default class TeamSettings extends React.Component { } onChange() { var team = TeamStore.getCurrent(); - if (!Utils.areStatesEqual(this.state.team, team)) { + if (!Utils.areObjectsEqual(this.state.team, team)) { this.setState({team}); } } diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index 82f830038..e6530b941 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -243,7 +243,6 @@ export default class Textbox extends React.Component { const lht = parseInt($(e).css('lineHeight'), 10); const lines = e.scrollHeight / lht; - const previewLinkHeightMod = 20; let mod = 15; if (lines < 2.5 || this.props.messageText === '') { @@ -252,17 +251,17 @@ export default class Textbox extends React.Component { if (e.scrollHeight - mod < 167) { $(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod); - $(w).css({height: 'auto'}).height(e.scrollHeight + 2 + previewLinkHeightMod); + $(w).css({height: 'auto'}).height(e.scrollHeight + 2); $(w).closest('.post-body__cell').removeClass('scroll'); if (this.state.preview) { $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'auto'}).height(e.scrollHeight - mod); } } else { $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod); - $(w).css({height: 'auto'}).height(167 + previewLinkHeightMod); + $(w).css({height: 'auto'}).height(163); $(w).closest('.post-body__cell').addClass('scroll'); if (this.state.preview) { - $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod); + $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(163); } } diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index eb0a8f0ca..a2523ef68 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -29,7 +29,7 @@ export default class UserProfile extends React.Component { return {profile: {id: '0', username: '...'}}; } - return {profile: profile}; + return {profile}; } componentDidMount() { UserStore.addChangeListener(this.onChange); @@ -43,7 +43,7 @@ export default class UserProfile extends React.Component { onChange(userId) { if (!userId || userId === this.props.userId) { var newState = this.getStateFromStores(this.props.userId); - if (!Utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 895d0c500..3dbed72c3 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -129,7 +129,6 @@ export default class CustomThemeChooser extends React.Component { {'Copy and paste to share theme colors:'} </label> <input - readOnly='true' type='text' className='form-control' value={colors} diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index e089ce973..40825ba93 100644 --- a/web/react/components/user_settings/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -36,7 +36,7 @@ export default class UserSettings extends React.Component { onListenerChange() { var user = UserStore.getCurrentUser(); - if (!utils.areStatesEqual(this.state.user, user)) { + if (!utils.areObjectsEqual(this.state.user, user)) { this.setState({user}); } } diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index d73b5f476..029a1af5e 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -1,13 +1,15 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../../stores/user_store.jsx'); -var Client = require('../../utils/client.jsx'); -var Utils = require('../../utils/utils.jsx'); - const CustomThemeChooser = require('./custom_theme_chooser.jsx'); const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); + +const UserStore = require('../../stores/user_store.jsx'); + const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); +const Client = require('../../utils/client.jsx'); +const Utils = require('../../utils/utils.jsx'); + const Constants = require('../../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -66,7 +68,7 @@ export default class UserSettingsAppearance extends React.Component { onChange() { const newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(this.state, newState)) { + if (!Utils.areObjectsEqual(this.state, newState)) { this.setState(newState); } diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index c6f47804f..c958bf5bc 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -1,16 +1,18 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../../stores/user_store.jsx'); -var SettingItemMin = require('../setting_item_min.jsx'); -var SettingItemMax = require('../setting_item_max.jsx'); -var client = require('../../utils/client.jsx'); -var AsyncClient = require('../../utils/async_client.jsx'); -var utils = require('../../utils/utils.jsx'); +const SettingItemMin = require('../setting_item_min.jsx'); +const SettingItemMax = require('../setting_item_max.jsx'); + +const UserStore = require('../../stores/user_store.jsx'); + +const Client = require('../../utils/client.jsx'); +const AsyncClient = require('../../utils/async_client.jsx'); +const Utils = require('../../utils/utils.jsx'); function getNotificationsStateFromStores() { var user = UserStore.getCurrentUser(); - var soundNeeded = !utils.isBrowserFirefox(); + var soundNeeded = !Utils.isBrowserFirefox(); var sound = 'true'; if (user.notify_props && user.notify_props.desktop_sound) { @@ -116,7 +118,7 @@ export default class NotificationsTab extends React.Component { data.all = this.state.allKey.toString(); data.channel = this.state.channelKey.toString(); - client.updateUserNotifyProps(data, + Client.updateUserNotifyProps(data, function success() { this.props.updateSection(''); AsyncClient.getMe(); @@ -138,7 +140,7 @@ export default class NotificationsTab extends React.Component { } onListenerChange() { var newState = getNotificationsStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + if (!Utils.areObjectsEqual(newState, this.state)) { this.setState(newState); } } diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 6b7d671fc..40b64b34b 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -164,6 +164,10 @@ class UserStoreClass extends EventEmitter { } getProfile(userId) { + if (userId === this.getCurrentId()) { + return this.getCurrentUser(); + } + return this.getProfiles()[userId]; } @@ -193,13 +197,13 @@ class UserStoreClass extends EventEmitter { return BrowserStore.getItem('profiles', {}); } - getActiveOnlyProfiles() { + getActiveOnlyProfiles(skipCurrent) { const active = {}; const profiles = this.getProfiles(); const currentId = this.getCurrentId(); for (var key in profiles) { - if (profiles[key].delete_at === 0 && profiles[key].id !== currentId) { + if (!(profiles[key].id === currentId && skipCurrent) && profiles[key].delete_at === 0) { active[key] = profiles[key]; } } diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx index 161c79761..f27e23a82 100644 --- a/web/react/utils/channel_intro_mssages.jsx +++ b/web/react/utils/channel_intro_mssages.jsx @@ -9,15 +9,15 @@ const ChannelStore = require('../stores/channel_store.jsx'); const Constants = require('../utils/constants.jsx'); const TeamStore = require('../stores/team_store.jsx'); -export function createChannelIntroMessage(channel) { +export function createChannelIntroMessage(channel, showInviteModal) { if (channel.type === 'D') { return createDMIntroMessage(channel); } else if (ChannelStore.isDefault(channel)) { return createDefaultIntroMessage(channel); } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { - return createOffTopicIntroMessage(channel); + return createOffTopicIntroMessage(channel, showInviteModal); } else if (channel.type === 'O' || channel.type === 'P') { - return createStandardIntroMessage(channel); + return createStandardIntroMessage(channel, showInviteModal); } } @@ -71,7 +71,7 @@ export function createDMIntroMessage(channel) { ); } -export function createOffTopicIntroMessage(channel) { +export function createOffTopicIntroMessage(channel, showInviteModal) { return ( <div className='channel-intro'> <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4> @@ -91,10 +91,8 @@ export function createOffTopicIntroMessage(channel) { <i className='fa fa-pencil'></i>{'Set a header'} </a> <a - className='intro-links' href='#' - data-toggle='modal' - data-target='#channel_invite' + onClick={showInviteModal} > <i className='fa fa-user-plus'></i>{'Invite others to this channel'} </a> @@ -155,7 +153,7 @@ export function createDefaultIntroMessage(channel) { ); } -export function createStandardIntroMessage(channel) { +export function createStandardIntroMessage(channel, showInviteModal) { var uiName = channel.display_name; var creatorName = ''; @@ -206,14 +204,11 @@ export function createStandardIntroMessage(channel) { <i className='fa fa-pencil'></i>{'Set a header'} </a> <a - className='intro-links' href='#' - data-toggle='modal' - data-target='#channel_invite' + onClick={showInviteModal} > <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType} </a> - </div> ); } diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 38f91b35f..6f3924829 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -311,8 +311,98 @@ export function escapeRegExp(string) { return string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'); } -export function areStatesEqual(state1, state2) { - return JSON.stringify(state1) === JSON.stringify(state2); +// Taken from http://stackoverflow.com/questions/1068834/object-comparison-in-javascript and modified slightly +export function areObjectsEqual(x, y) { + let p; + const leftChain = []; + const rightChain = []; + + // Remember that NaN === NaN returns false + // and isNaN(undefined) returns true + if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') { + return true; + } + + // Compare primitives and functions. + // Check if both arguments link to the same object. + // Especially useful on step when comparing prototypes + if (x === y) { + return true; + } + + // Works in case when functions are created in constructor. + // Comparing dates is a common scenario. Another built-ins? + // We can even handle functions passed across iframes + if ((typeof x === 'function' && typeof y === 'function') || + (x instanceof Date && y instanceof Date) || + (x instanceof RegExp && y instanceof RegExp) || + (x instanceof String && y instanceof String) || + (x instanceof Number && y instanceof Number)) { + return x.toString() === y.toString(); + } + + // At last checking prototypes as good a we can + if (!(x instanceof Object && y instanceof Object)) { + return false; + } + + if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) { + return false; + } + + if (x.constructor !== y.constructor) { + return false; + } + + if (x.prototype !== y.prototype) { + return false; + } + + // Check for infinitive linking loops + if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) { + return false; + } + + // Quick checking of one object beeing a subset of another. + for (p in y) { + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } else if (typeof y[p] !== typeof x[p]) { + return false; + } + } + + for (p in x) { + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } else if (typeof y[p] !== typeof x[p]) { + return false; + } + + switch (typeof (x[p])) { + case 'object': + case 'function': + + leftChain.push(x); + rightChain.push(y); + + if (!areObjectsEqual(x[p], y[p])) { + return false; + } + + leftChain.pop(); + rightChain.pop(); + break; + + default: + if (x[p] !== y[p]) { + return false; + } + break; + } + } + + return true; } export function replaceHtmlEntities(text) { diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index d3ab3b9f8..49fb8e847 100644 --- a/web/sass-files/sass/partials/_files.scss +++ b/web/sass-files/sass/partials/_files.scss @@ -1,5 +1,6 @@ .preview-container { position: relative; + margin-top: 10px; width: 100%; max-height: 110px; height: 110px; diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss index 2e78a8728..c06feffcf 100644 --- a/web/sass-files/sass/partials/_navbar.scss +++ b/web/sass-files/sass/partials/_navbar.scss @@ -96,9 +96,9 @@ } .badge-notify { - background:red; + background: red; position: absolute; - right: -5px; - top: -5px; + left: 4px; + top: 3px; z-index: 100; } diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 3e2d6f045..33748052d 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -46,21 +46,22 @@ body.ios { .textarea-wrapper { position:relative; - min-height:57px; - .textbox-preview-area { - position: absolute; - z-index: 2; - top: 0; - left: 0; - box-shadow: none; - } - .textbox-preview-link { - position: absolute; - z-index: 3; - bottom: 0; - right: 10px; - cursor: pointer; - } + min-height: 36px; + .textbox-preview-area { + position: absolute; + z-index: 2; + top: 0; + left: 0; + box-shadow: none; + } + .textbox-preview-link { + position: absolute; + z-index: 3; + bottom: -23px; + right: 0; + font-size: 13px; + cursor: pointer; + } } .date-separator, .new-separator { @@ -338,9 +339,9 @@ body.ios { } } .msg-typing { - min-height: 20px; - line-height: 18px; - display: inline-block; + min-height: 25px; + line-height: 25px; + display: block; font-size: 13px; @include opacity(0.7); } @@ -658,6 +659,7 @@ body.ios { float: left; width: 80%; padding-right: 5px; + overflow-x: auto; &.attachment__body--no_thumb { width: 100%; } diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss index c1d291073..ba41d3b95 100644 --- a/web/sass-files/sass/partials/_post_right.scss +++ b/web/sass-files/sass/partials/_post_right.scss @@ -13,6 +13,7 @@ &.post--root { padding: 1em 1em 0; margin: 0 0 1em; + width: 100%; hr { border-color: #DDD; margin: 1em 0 0 0; @@ -21,9 +22,10 @@ } .post-create__container { + width: 100%; margin-top: 10px; .textarea-wrapper { - min-height: 120px; + min-height: 100px; } .custom-textarea { min-height: 100px; @@ -31,10 +33,18 @@ .msg-typing { @include opacity(0.7); float: left; - padding-top: 17px; + margin-top: 3px; + font-size: 13px; + line-height: 20px; + min-width: 1px; + display: block; + height: 20px; + max-width: 200px; + @include clearfix; } .post-create-footer { - padding-top: 10px; + width: 100%; + padding-top: 5px; } .post-right-comments-upload-in-progress { padding: 6px 0; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 339412b45..cb140dce6 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -507,8 +507,16 @@ form { padding: 0; } + .post-create-footer { + .msg-typing { + margin-left: 45px; + width: 55%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } .post-create-body { - padding-bottom: 10px; display: table; width: 100%; .post-body__cell { @@ -532,11 +540,10 @@ display: table-cell; } } - .post-create-footer .msg-typing { - display: none; - } } .preview-container { + padding: 0px 10px; + margin-top: 20px; .preview-div { margin-top: 0; } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index b304450bc..0d75a42df 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -64,6 +64,10 @@ } } } + .profile-img { + width: 128px; + height: 128px; + } .settings-table { display: table; table-layout: fixed; diff --git a/web/web.go b/web/web.go index 1cae604ae..ffc791cb7 100644 --- a/web/web.go +++ b/web/web.go @@ -132,7 +132,7 @@ func watchAndParseTemplates() { } } -var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7" +var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" func CheckBrowserCompatability(c *api.Context, r *http.Request) bool { ua := user_agent.New(r.UserAgent()) @@ -143,7 +143,7 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool { version := strings.Split(browser, "/") if strings.HasPrefix(bname, version[0]) && strings.HasPrefix(bversion, version[1]) { - c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 11 or higher, FireFox 14 or higher, Safari 8 or higher", "") + c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 11 or higher, FireFox 14 or higher, Safari 9 or higher", "") return false } } |