diff options
Diffstat (limited to 'web/react/components')
-rw-r--r-- | web/react/components/create_comment.jsx | 2 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 12 | ||||
-rw-r--r-- | web/react/components/error_bar.jsx | 12 | ||||
-rw-r--r-- | web/react/components/mention_list.jsx | 7 | ||||
-rw-r--r-- | web/react/components/more_direct_channels.jsx | 62 | ||||
-rw-r--r-- | web/react/components/msg_typing.jsx | 55 | ||||
-rw-r--r-- | web/react/components/popover_list_members.jsx | 145 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 64 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_general.jsx | 2 |
9 files changed, 247 insertions, 114 deletions
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 435c7d542..18936e808 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -147,7 +147,7 @@ export default class CreateComment extends React.Component { } const t = Date.now(); - if ((t - this.lastTime) > 5000) { + if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}}); this.lastTime = t; } diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 055be112d..32ee31efe 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -208,7 +208,7 @@ export default class CreatePost extends React.Component { } const t = Date.now(); - if ((t - this.lastTime) > 5000) { + if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}}); this.lastTime = t; } @@ -253,8 +253,14 @@ export default class CreatePost extends React.Component { this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); } handleUploadError(err, clientId) { + let message = err; + if (message && typeof message !== 'string') { + // err is an AppError from the server + message = err.message; + } + if (clientId === -1) { - this.setState({serverError: err}); + this.setState({serverError: message}); } else { const draft = PostStore.getDraft(this.state.channelId); @@ -265,7 +271,7 @@ export default class CreatePost extends React.Component { PostStore.storeDraft(this.state.channelId, draft); - this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err}); + this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message}); } } handleTextDrop(text) { diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx index 6311d9460..f098384aa 100644 --- a/web/react/components/error_bar.jsx +++ b/web/react/components/error_bar.jsx @@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component { this.onErrorChange = this.onErrorChange.bind(this); this.handleClose = this.handleClose.bind(this); - this.prevTimer = null; this.state = ErrorStore.getLastError(); - if (this.isValidError(this.state)) { - this.prevTimer = setTimeout(this.handleClose, 10000); - } } isValidError(s) { @@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component { onErrorChange() { var newState = ErrorStore.getLastError(); - if (this.prevTimer != null) { - clearInterval(this.prevTimer); - this.prevTimer = null; - } - if (newState) { this.setState(newState); - if (!this.isConnectionError(newState)) { - this.prevTimer = setTimeout(this.handleClose, 10000); - } } else { this.setState({message: null}); } diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx index cb7f71f15..61a24c09c 100644 --- a/web/react/components/mention_list.jsx +++ b/web/react/components/mention_list.jsx @@ -217,12 +217,17 @@ export default class MentionList extends React.Component { if (this.state.selectedMention === index) { isFocused = 'mentions-focus'; } + + if (!users[i].secondary_text) { + users[i].secondary_text = Utils.getFullName(users[i]); + } + mentions[index] = ( <Mention key={'mention_key_' + index} ref={'mention' + index} username={users[i].username} - secondary_text={Utils.getFullName(users[i])} + secondary_text={users[i].secondary_text} id={users[i].id} listId={index} isFocused={isFocused} diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 41746d1d7..b0232fc08 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -1,13 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -const AsyncClient = require('../utils/async_client.jsx'); -const ChannelStore = require('../stores/channel_store.jsx'); -const Constants = require('../utils/constants.jsx'); -const Client = require('../utils/client.jsx'); const Modal = ReactBootstrap.Modal; -const PreferenceStore = require('../stores/preference_store.jsx'); -const TeamStore = require('../stores/team_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const Utils = require('../utils/utils.jsx'); @@ -70,52 +64,24 @@ export default class MoreDirectChannels extends React.Component { } handleShowDirectChannel(teammate, e) { + e.preventDefault(); + if (this.state.loadingDMChannel !== -1) { return; } - e.preventDefault(); - - const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id); - let channel = ChannelStore.getByName(channelName); - - const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true'); - AsyncClient.savePreferences([preference]); - - if (channel) { - Utils.switchChannel(channel); - - this.handleHide(); - } else { - this.setState({loadingDMChannel: teammate.id}); - - channel = { - name: channelName, - last_post_at: 0, - total_msg_count: 0, - type: 'D', - display_name: teammate.username, - teammate_id: teammate.id, - status: UserStore.getStatus(teammate.id) - }; - - Client.createDirectChannel( - channel, - teammate.id, - (data) => { - this.setState({loadingDMChannel: -1}); - - AsyncClient.getChannel(data.id); - Utils.switchChannel(data); - - this.handleHide(); - }, - () => { - this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName; - } - ); - } + this.setState({loadingDMChannel: teammate.id}); + Utils.openDirectChannelToUser( + teammate, + (channel) => { + Utils.switchChannel(channel); + this.setState({loadingDMChannel: -1}); + this.handleHide(); + }, + () => { + this.setState({loadingDMChannel: -1}); + } + ); } handleUserChange() { diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index 1bd23c55c..ccf8a2445 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -11,11 +11,11 @@ export default class MsgTyping extends React.Component { constructor(props) { super(props); - this.timer = null; - this.lastTime = 0; - this.onChange = this.onChange.bind(this); + this.updateTypingText = this.updateTypingText.bind(this); + this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this); + this.typingUsers = {}; this.state = { text: '' }; @@ -27,7 +27,7 @@ export default class MsgTyping extends React.Component { componentWillReceiveProps(newProps) { if (this.props.channelId !== newProps.channelId) { - this.setState({text: ''}); + this.updateTypingText(); } } @@ -36,28 +36,51 @@ export default class MsgTyping extends React.Component { } onChange(msg) { + let username = 'Someone'; if (msg.action === SocketEvents.TYPING && this.props.channelId === msg.channel_id && this.props.parentId === msg.props.parent_id) { - this.lastTime = new Date().getTime(); - - var username = 'Someone'; if (UserStore.hasProfile(msg.user_id)) { username = UserStore.getProfile(msg.user_id).username; } - this.setState({text: username + ' is typing...'}); - - if (!this.timer) { - this.timer = setInterval(function myTimer() { - if ((new Date().getTime() - this.lastTime) > 8000) { - this.setState({text: ''}); - } - }.bind(this), 3000); + if (this.typingUsers[username]) { + clearTimeout(this.typingUsers[username]); } + + this.typingUsers[username] = setTimeout(function myTimer(user) { + delete this.typingUsers[user]; + this.updateTypingText(); + }.bind(this, username), Constants.UPDATE_TYPING_MS); + + this.updateTypingText(); } else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) { - this.setState({text: ''}); + if (UserStore.hasProfile(msg.user_id)) { + username = UserStore.getProfile(msg.user_id).username; + } + clearTimeout(this.typingUsers[username]); + delete this.typingUsers[username]; + this.updateTypingText(); + } + } + + updateTypingText() { + const users = Object.keys(this.typingUsers); + let text = ''; + switch (users.length) { + case 0: + text = ''; + break; + case 1: + text = users[0] + ' is typing...'; + break; + default: + const last = users.pop(); + text = users.join(', ') + ' and ' + last + ' are typing...'; + break; } + + this.setState({text}); } render() { diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index 155e88600..9cffa2400 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -3,9 +3,23 @@ var UserStore = require('../stores/user_store.jsx'); var Popover = ReactBootstrap.Popover; -var OverlayTrigger = ReactBootstrap.OverlayTrigger; +var Overlay = ReactBootstrap.Overlay; +const Utils = require('../utils/utils.jsx'); + +const ChannelStore = require('../stores/channel_store.jsx'); export default class PopoverListMembers extends React.Component { + constructor(props) { + super(props); + + this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this); + this.closePopover = this.closePopover.bind(this); + } + + componentWillMount() { + this.setState({showPopover: false}); + } + componentDidMount() { const originalLeave = $.fn.popover.Constructor.prototype.leave; $.fn.popover.Constructor.prototype.leave = function onLeave(obj) { @@ -27,12 +41,36 @@ export default class PopoverListMembers extends React.Component { } }; } + + handleShowDirectChannel(teammate, e) { + e.preventDefault(); + + Utils.openDirectChannelToUser( + teammate, + (channel, channelAlreadyExisted) => { + Utils.switchChannel(channel); + if (channelAlreadyExisted) { + this.closePopover(); + } + }, + () => { + this.closePopover(); + } + ); + } + + closePopover() { + this.setState({showPopover: false}); + } + render() { let popoverHtml = []; let count = 0; let countText = '-'; const members = this.props.members; const teamMembers = UserStore.getProfilesUsernameMap(); + const currentUserId = UserStore.getCurrentId(); + const ch = ChannelStore.getCurrent(); if (members && teamMembers) { members.sort((a, b) => { @@ -40,13 +78,74 @@ export default class PopoverListMembers extends React.Component { }); members.forEach((m, i) => { + const details = []; + + const fullName = Utils.getFullName(m); + if (fullName) { + details.push( + <span + key={`${m.id}__full-name`} + className='full-name' + > + {fullName} + </span> + ); + } + + if (m.nickname) { + const separator = fullName ? ' - ' : ''; + details.push( + <span + key={`${m.nickname}__nickname`} + > + {separator + m.nickname} + </span> + ); + } + + let button = ''; + if (currentUserId !== m.id && ch.type !== 'D') { + button = ( + <button + type='button' + className='btn btn-primary btn-message' + onClick={(e) => this.handleShowDirectChannel(m, e)} + > + {'Message'} + </button> + ); + } + if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) { popoverHtml.push( <div className='text--nowrap' key={'popover-member-' + i} > - {m.username} + + <img + className='profile-img pull-left' + width='38' + height='38' + src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`} + /> + <div className='pull-left'> + <div + className='more-name' + > + {m.username} + </div> + <div + className='more-description' + > + {details} + </div> + </div> + <div + className='pull-right profile-action' + > + {button} + </div> </div> ); count++; @@ -61,29 +160,37 @@ export default class PopoverListMembers extends React.Component { } return ( - <OverlayTrigger - trigger='click' - placement='bottom' - rootClose={true} - overlay={ + <div> + <div + id='member_popover' + ref='member_popover_target' + onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})} + > + <div> + {countText} + <span + className='fa fa-user' + aria-hidden='true' + /> + </div> + </div> + <Overlay + rootClose={true} + onHide={this.closePopover} + show={this.state.showPopover} + target={() => this.state.popoverTarget} + placement='bottom' + > <Popover title='Members' id='member-list-popover' > - {popoverHtml} + <div> + {popoverHtml} + </div> </Popover> - } - > - <div id='member_popover'> - <div> - {countText} - <span - className='fa fa-user' - aria-hidden='true' - /> - </div> + </Overlay> </div> - </OverlayTrigger> ); } } diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index ed2c84057..5cb6d168b 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -40,6 +40,9 @@ export default class Sidebar extends React.Component { this.hideMoreDirectChannelsModal = this.hideMoreDirectChannelsModal.bind(this); this.createChannelElement = this.createChannelElement.bind(this); + this.updateTitle = this.updateTitle.bind(this); + this.setUnreadCountPerChannel = this.setUnreadCountPerChannel.bind(this); + this.getUnreadCount = this.getUnreadCount.bind(this); this.isLeaving = new Map(); @@ -48,8 +51,45 @@ export default class Sidebar extends React.Component { state.showDirectChannelsModal = false; state.loadingDMChannel = -1; state.windowWidth = Utils.windowWidth(); - this.state = state; + + this.unreadCountPerChannel = {}; + this.setUnreadCountPerChannel(); + } + setUnreadCountPerChannel() { + const channels = ChannelStore.getAll(); + const members = ChannelStore.getAllMembers(); + const channelUnreadCounts = {}; + + channels.forEach((ch) => { + const chMember = members[ch.id]; + let chMentionCount = chMember.mention_count; + let chUnreadCount = ch.total_msg_count - chMember.msg_count - chMentionCount; + + if (ch.type === 'D') { + chMentionCount = chUnreadCount; + chUnreadCount = 0; + } + + channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount}; + }); + + this.unreadCountPerChannel = channelUnreadCounts; + } + getUnreadCount(channelId) { + let mentions = 0; + let msgs = 0; + + if (channelId) { + return this.unreadCountPerChannel[channelId] ? this.unreadCountPerChannel[channelId] : {msgs, mentions}; + } + + Object.keys(this.unreadCountPerChannel).forEach((chId) => { + msgs += this.unreadCountPerChannel[chId].msgs; + mentions += this.unreadCountPerChannel[chId].mentions; + }); + + return {msgs, mentions}; } getStateFromStores() { const members = ChannelStore.getAllMembers(); @@ -192,7 +232,10 @@ export default class Sidebar extends React.Component { currentChannelName = Utils.getDirectTeammate(channel.id).username; } - document.title = currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName; + const unread = this.getUnreadCount(); + const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; + const unreadTitle = unread.msgs > 0 ? '* ' : ''; + document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.props.teamDisplayName + ' ' + currentSiteName; } } onScroll() { @@ -273,6 +316,7 @@ export default class Sidebar extends React.Component { var members = this.state.members; var activeId = this.state.activeId; var channelMember = members[channel.id]; + var unreadCount = this.getUnreadCount(channel.id); var msgCount; var linkClass = ''; @@ -284,7 +328,7 @@ export default class Sidebar extends React.Component { var unread = false; if (channelMember) { - msgCount = channel.total_msg_count - channelMember.msg_count; + msgCount = unreadCount.msgs + unreadCount.mentions; unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0; } @@ -301,16 +345,8 @@ export default class Sidebar extends React.Component { var badge = null; if (channelMember) { - if (channel.type === 'D') { - // direct message channels show badges for any number of unread posts - msgCount = channel.total_msg_count - channelMember.msg_count; - if (msgCount > 0) { - badge = <span className='badge pull-right small'>{msgCount}</span>; - this.badgesActive = true; - } - } else if (channelMember.mention_count > 0) { - // public and private channels only show badges for mentions - badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; + if (unreadCount.mentions) { + badge = <span className='badge pull-right small'>{unreadCount.mentions}</span>; this.badgesActive = true; } } else if (this.state.loadingDMChannel === index && channel.type === 'D') { @@ -434,6 +470,8 @@ export default class Sidebar extends React.Component { render() { this.badgesActive = false; + this.setUnreadCountPerChannel(); + // keep track of the first and last unread channels so we can use them to set the unread indicators this.firstUnreadChannel = null; this.lastUnreadChannel = null; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 70e559c30..1c8ce3c79 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component { }.bind(this), function imageUploadFailure(err) { var state = this.setupInitialState(this.props); - state.serverError = err; + state.serverError = err.message; this.setState(state); }.bind(this) ); |