diff options
-rw-r--r-- | api/channel.go | 29 | ||||
-rw-r--r-- | i18n/en.json | 6 | ||||
-rw-r--r-- | store/sql_channel_store.go | 52 | ||||
-rw-r--r-- | store/store.go | 1 | ||||
-rw-r--r-- | webapp/actions/channel_actions.jsx | 12 | ||||
-rw-r--r-- | webapp/actions/post_actions.jsx | 52 | ||||
-rw-r--r-- | webapp/components/edit_post_modal.jsx | 82 | ||||
-rw-r--r-- | webapp/components/post_view/components/post.jsx | 9 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_list.jsx | 18 | ||||
-rw-r--r-- | webapp/components/post_view/post_view_controller.jsx | 14 | ||||
-rw-r--r-- | webapp/components/suggestion/suggestion_box.jsx | 1 | ||||
-rw-r--r-- | webapp/sass/layout/_sidebar-left.scss | 4 | ||||
-rw-r--r-- | webapp/stores/channel_store.jsx | 16 | ||||
-rw-r--r-- | webapp/utils/async_client.jsx | 37 |
14 files changed, 296 insertions, 37 deletions
diff --git a/api/channel.go b/api/channel.go index 2a5b6f8b0..3fef273e5 100644 --- a/api/channel.go +++ b/api/channel.go @@ -44,6 +44,7 @@ func InitChannel() { BaseRoutes.NeedChannel.Handle("/add", ApiUserRequired(addMember)).Methods("POST") BaseRoutes.NeedChannel.Handle("/remove", ApiUserRequired(removeMember)).Methods("POST") BaseRoutes.NeedChannel.Handle("/update_last_viewed_at", ApiUserRequired(updateLastViewedAt)).Methods("POST") + BaseRoutes.NeedChannel.Handle("/set_last_viewed_at", ApiUserRequired(setLastViewedAt)).Methods("POST") } func createChannel(c *Context, w http.ResponseWriter, r *http.Request) { @@ -791,6 +792,34 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { } } +func setLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["channel_id"] + + data := model.StringInterfaceFromJson(r.Body) + newLastViewedAt := int64(data["last_viewed_at"].(float64)) + + Srv.Store.Channel().SetLastViewedAt(id, c.Session.UserId, newLastViewedAt) + + preference := model.Preference{ + UserId: c.Session.UserId, + Category: model.PREFERENCE_CATEGORY_LAST, + Name: model.PREFERENCE_NAME_LAST_CHANNEL, + Value: id, + } + + Srv.Store.Preference().Save(&model.Preferences{preference}) + + message := model.NewWebSocketEvent(c.TeamId, id, c.Session.UserId, model.WEBSOCKET_EVENT_CHANNEL_VIEWED) + message.Add("channel_id", id) + + go Publish(message) + + result := make(map[string]string) + result["id"] = id + w.Write([]byte(model.MapToJson(result))) +} + func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["channel_id"] diff --git a/i18n/en.json b/i18n/en.json index 9ec9f393d..961ddc50c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -557,7 +557,7 @@ }, { "id": "api.command_shortcuts.list", - "translation": "### Keyboard Shortcuts\n\n#### Navigation\n\nALT+UP: Previous channel or direct message in left hand sidebar\nALT+DOWN: Next channel or direct message in left hand sidebar\nALT+SHIFT+UP: Previous channel or direct message in left hand sidebar with unread messages\nALT+SHIFT+DOWN: Next channel or direct message in left hand sidebar with unread messages\nCTRL/CMD+K: Open a quick channel switcher dialog\nCTRL/CMD+SHIFT+A: Open account settings\nCTRL/CMD+SHIFT+M: Open recent mentions\n\n#### Files\n\nCTRL/CMD+U: Upload file(s)\n\n#### Messages\n\nCTRL/CMD+UP (in empty input field): Reprint the previous message or slash command you entered\nCTRL/CMD+DOWN (in empty input field): Reprint the next message or slash command you entered\nUP (in empty input field): Edit your last message in the current channel\n@[character]+TAB: Autocomplete @username beginning with [character]\n:[character]+TAB: Autocomplete emoji beginning with [character]\n\n#### Built-in Browser Commands\n\nALT+LEFT/CMD+[: Previous channel in your history\nALT+RIGHT/CMD+]: Next channel in your history\nCTRL/CMD+PLUS: Increase font size (zoom in)\nCTRL/CMD+MINUS: Decrease font size (zoom out)\nSHIFT+UP (in input field): Highlight text to the previous line\nSHIFT+DOWN (in input field): Highlight text to the next line\nSHIFT+ENTER (in input field): Create a new line\n" + "translation": "### Keyboard Shortcuts\n\n#### Navigation\n\nALT+UP: Previous channel or direct message in left hand sidebar\nALT+DOWN: Next channel or direct message in left hand sidebar\nALT+SHIFT+UP: Previous channel or direct message in left hand sidebar with unread messages\nALT+SHIFT+DOWN: Next channel or direct message in left hand sidebar with unread messages\nCTRL/CMD+K: Open a quick channel switcher dialog\nCTRL/CMD+SHIFT+A: Open account settings\nCTRL/CMD+SHIFT+M: Open recent mentions\n\n#### Files\n\nCTRL/CMD+U: Upload file(s)\n\n#### Messages\n\nALT+Click: Set message as unread\nESC: Set all messages in channel as read\nCTRL/CMD+UP (in empty input field): Reprint the previous message or slash command you entered\nCTRL/CMD+DOWN (in empty input field): Reprint the next message or slash command you entered\nUP (in empty input field): Edit your last message in the current channel\n@[character]+TAB: Autocomplete @username beginning with [character]\n:[character]+TAB: Autocomplete emoji beginning with [character]\n\n#### Built-in Browser Commands\n\nALT+LEFT/CMD+[: Previous channel in your history\nALT+RIGHT/CMD+]: Next channel in your history\nCTRL/CMD+PLUS: Increase font size (zoom in)\nCTRL/CMD+MINUS: Decrease font size (zoom out)\nSHIFT+UP (in input field): Highlight text to the previous line\nSHIFT+DOWN (in input field): Highlight text to the next line\nSHIFT+ENTER (in input field): Create a new line\n" }, { "id": "api.command_shortcuts.name", @@ -3516,6 +3516,10 @@ "translation": "We couldn't save the channel member" }, { + "id": "store.sql_channel.set_last_viewed_at.app_error", + "translation": "We couldn't set the last viewed at time" + }, + { "id": "store.sql_channel.update.app_error", "translation": "We couldn't update the channel" }, diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index e5e0aa8ba..2b356d0de 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -856,6 +856,58 @@ func (s SqlChannelStore) CheckOpenChannelPermissions(teamId string, channelId st return storeChannel } +func (s SqlChannelStore) SetLastViewedAt(channelId string, userId string, newLastViewedAt int64) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var query string + + if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { + query = `UPDATE + ChannelMembers + SET + MentionCount = 0, + MsgCount = Channels.TotalMsgCount - (SELECT COUNT(*) + FROM Posts + WHERE ChannelId = :ChannelId + AND CreateAt > :NewLastViewedAt), + LastViewedAt = :NewLastViewedAt + FROM + Channels + WHERE + Channels.Id = ChannelMembers.ChannelId + AND UserId = :UserId + AND ChannelId = :ChannelId` + } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL { + query = `UPDATE + ChannelMembers, Channels + SET + ChannelMembers.MentionCount = 0, + ChannelMembers.MsgCount = Channels.TotalMsgCount - (SELECT COUNT(*) + FROM Posts + WHERE ChannelId = :ChannelId + AND CreateAt > :NewLastViewedAt), + ChannelMembers.LastViewedAt = :NewLastViewedAt + WHERE + Channels.Id = ChannelMembers.ChannelId + AND UserId = :UserId + AND ChannelId = :ChannelId` + } + + _, err := s.GetMaster().Exec(query, map[string]interface{}{"ChannelId": channelId, "UserId": userId, "NewLastViewedAt": newLastViewedAt}) + if err != nil { + result.Err = model.NewLocAppError("SqlChannelStore.SetLastViewedAt", "store.sql_channel.set_last_viewed_at.app_error", nil, "channel_id="+channelId+", user_id="+userId+", "+err.Error()) + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlChannelStore) UpdateLastViewedAt(channelId string, userId string) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/store.go b/store/store.go index f576cc2ab..445de440a 100644 --- a/store/store.go +++ b/store/store.go @@ -99,6 +99,7 @@ type ChannelStore interface { CheckOpenChannelPermissions(teamId string, channelId string) StoreChannel CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel UpdateLastViewedAt(channelId string, userId string) StoreChannel + SetLastViewedAt(channelId string, userId string, newLastViewedAt int64) StoreChannel IncrementMentionCount(channelId string, userId string) StoreChannel AnalyticsTypeCount(teamId string, channelType string) StoreChannel ExtraUpdateByUser(userId string, time int64) StoreChannel diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx index 9e5ecb03b..f8bc61538 100644 --- a/webapp/actions/channel_actions.jsx +++ b/webapp/actions/channel_actions.jsx @@ -5,6 +5,8 @@ import {browserHistory} from 'react-router/es6'; import * as Utils from 'utils/utils.jsx'; import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; import Client from 'utils/web_client.jsx'; export function goToChannel(channel) { @@ -24,3 +26,13 @@ export function goToChannel(channel) { export function executeCommand(channelId, message, suggest, success, error) { Client.executeCommand(channelId, message, suggest, success, error); } + +export function setChannelAsRead(channelIdParam) { + const channelId = channelIdParam || ChannelStore.getCurrentId(); + AsyncClient.updateLastViewedAt(); + ChannelStore.resetCounts(channelId); + ChannelStore.emitChange(); + if (channelId === ChannelStore.getCurrentId()) { + ChannelStore.emitLastViewed(Number.MAX_VALUE, false); + } +} diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx index 866ae5888..a6b464a24 100644 --- a/webapp/actions/post_actions.jsx +++ b/webapp/actions/post_actions.jsx @@ -6,7 +6,9 @@ import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import PostStore from 'stores/post_store.jsx'; import TeamStore from 'stores/team_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import * as PostUtils from 'utils/post_utils.jsx'; import Constants from 'utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; @@ -62,3 +64,53 @@ export function handleNewPost(post, msg) { websocketMessageProps }); } + +export function setUnreadPost(channelId, postId) { + let lastViewed = 0; + let ownNewMessage = false; + const post = PostStore.getPost(channelId, postId); + const posts = PostStore.getVisiblePosts(channelId).posts; + var currentUsedId = UserStore.getCurrentId(); + if (currentUsedId === post.user_id || PostUtils.isSystemMessage(post)) { + for (const otherPostId in posts) { + if (lastViewed < posts[otherPostId].create_at && currentUsedId !== posts[otherPostId].user_id && !PostUtils.isSystemMessage(posts[otherPostId])) { + lastViewed = posts[otherPostId].create_at; + } + } + if (lastViewed === 0) { + lastViewed = Number.MAX_VALUE; + } else if (lastViewed > post.create_at) { + lastViewed = post.create_at - 1; + ownNewMessage = true; + } else { + lastViewed -= 1; + } + } else { + lastViewed = post.create_at - 1; + } + + if (lastViewed === Number.MAX_VALUE) { + AsyncClient.updateLastViewedAt(); + ChannelStore.resetCounts(ChannelStore.getCurrentId()); + ChannelStore.emitChange(); + } else { + let unreadPosts = 0; + for (const otherPostId in posts) { + if (posts[otherPostId].create_at > lastViewed) { + unreadPosts += 1; + } + } + const member = ChannelStore.getMember(channelId); + const channel = ChannelStore.get(channelId); + member.last_viewed_at = lastViewed; + member.msg_count = channel.total_msg_count - unreadPosts; + member.mention_count = 0; + ChannelStore.setChannelMember(member); + ChannelStore.setUnreadCount(channelId); + AsyncClient.setLastViewedAt(lastViewed, channelId); + } + + if (channelId === ChannelStore.getCurrentId()) { + ChannelStore.emitLastViewed(lastViewed, ownNewMessage); + } +} diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx index 4bd23a26d..1ddaee535 100644 --- a/webapp/components/edit_post_modal.jsx +++ b/webapp/components/edit_post_modal.jsx @@ -37,6 +37,11 @@ class EditPostModal extends React.Component { this.handleEditPostEvent = this.handleEditPostEvent.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.onPreferenceChange = this.onPreferenceChange.bind(this); + this.onModalHidden = this.onModalHidden.bind(this); + this.onModalShow = this.onModalShow.bind(this); + this.onModalShown = this.onModalShown.bind(this); + this.onModalHide = this.onModalHide.bind(this); + this.onModalKeyDown = this.onModalKeyDown.bind(this); this.state = {editText: '', originalText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: '', typing: false}; } @@ -116,46 +121,55 @@ class EditPostModal extends React.Component { ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter') }); } - componentDidMount() { - var self = this; - - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', () => { - self.setState({editText: '', originalText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: '', typing: false}); + onModalHidden() { + this.setState({editText: '', originalText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: '', typing: false}); + } + onModalShow(e) { + var button = e.relatedTarget; + if (!button) { + return; + } + this.setState({ + editText: $(button).attr('data-message'), + originalText: $(button).attr('data-message'), + title: $(button).attr('data-title'), + channel_id: $(button).attr('data-channelid'), + post_id: $(button).attr('data-postid'), + comments: $(button).attr('data-comments'), + refocusId: $(button).attr('data-refocusid'), + typing: false }); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => { - var button = e.relatedTarget; - if (!button) { - return; - } - self.setState({ - editText: $(button).attr('data-message'), - originalText: $(button).attr('data-message'), - title: $(button).attr('data-title'), - channel_id: $(button).attr('data-channelid'), - post_id: $(button).attr('data-postid'), - comments: $(button).attr('data-comments'), - refocusId: $(button).attr('data-refocusid'), - typing: false + } + onModalShown() { + this.refs.editbox.focus(); + } + onModalHide() { + if (this.state.refocusId !== '') { + setTimeout(() => { + $(this.state.refocusId).get(0).focus(); }); - }); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', () => { - self.refs.editbox.focus(); - }); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', () => { - if (self.state.refocusId !== '') { - setTimeout(() => { - $(self.state.refocusId).get(0).focus(); - }); - } - }); - + } + } + onModalKeyDown(e) { + if (e.which === Constants.KeyCodes.ESCAPE) { + e.stopPropagation(); + } + } + componentDidMount() { + $(this.refs.modal).on('hidden.bs.modal', this.onModalHidden); + $(this.refs.modal).on('show.bs.modal', this.onModalShow); + $(this.refs.modal).on('shown.bs.modal', this.onModalShown); + $(this.refs.modal).on('hide.bs.modal', this.onModalHide); + $(this.refs.modal).on('keydown', this.onModalKeyDown); PostStore.addEditPostListener(this.handleEditPostEvent); PreferenceStore.addChangeListener(this.onPreferenceChange); } componentWillUnmount() { + $(this.refs.modal).off('hidden.bs.modal', this.onModalHidden); + $(this.refs.modal).off('show.bs.modal', this.onModalShow); + $(this.refs.modal).off('shown.bs.modal', this.onModalShown); + $(this.refs.modal).off('hide.bs.modal', this.onModalHide); + $(this.refs.modal).off('keydown', this.onModalKeyDown); PostStore.removeEditPostListner(this.handleEditPostEvent); PreferenceStore.removeChangeListener(this.onPreferenceChange); } diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index 21d335a51..ff443e355 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -10,6 +10,7 @@ const ActionTypes = Constants.ActionTypes; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; +import * as PostActions from 'actions/post_actions.jsx'; import React from 'react'; @@ -20,6 +21,7 @@ export default class Post extends React.Component { this.handleCommentClick = this.handleCommentClick.bind(this); this.handleDropdownOpened = this.handleDropdownOpened.bind(this); this.forceUpdateInfo = this.forceUpdateInfo.bind(this); + this.handlePostClick = this.handlePostClick.bind(this); this.state = { dropdownOpened: false @@ -47,6 +49,12 @@ export default class Post extends React.Component { this.refs.info.forceUpdate(); this.refs.header.forceUpdate(); } + handlePostClick(e) { + if (e.altKey) { + e.preventDefault(); + PostActions.setUnreadPost(this.props.post.channel_id, this.props.post.id); + } + } shouldComponentUpdate(nextProps, nextState) { if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) { return true; @@ -213,6 +221,7 @@ export default class Post extends React.Component { <div id={'post_' + post.id} className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass + ' ' + hideControls + ' ' + dropdownOpenedClass} + onClick={this.handlePostClick} > <div className={'post__content ' + centerClass}> {profilePicContainer} diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index 690cd96c7..70107c838 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -15,6 +15,8 @@ import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; +import * as ChannelActions from 'actions/channel_actions.jsx'; + import Constants from 'utils/constants.jsx'; const ScrollTypes = Constants.ScrollTypes; @@ -41,6 +43,7 @@ export default class PostList extends React.Component { this.handleResize = this.handleResize.bind(this); this.scrollToBottom = this.scrollToBottom.bind(this); this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.jumpToPostNode = null; this.wasAtBottom = true; @@ -61,6 +64,13 @@ export default class PostList extends React.Component { } } + handleKeyDown(e) { + if (e.which === Constants.KeyCodes.ESCAPE && $('.popover.in,.modal.in').length === 0) { + e.preventDefault(); + ChannelActions.setChannelAsRead(); + } + } + isAtBottom() { // consider the view to be at the bottom if it's within this many pixels of the bottom const atBottomMargin = 10; @@ -297,7 +307,7 @@ export default class PostList extends React.Component { ); } - if (postUserId !== userId && + if ((postUserId !== userId || this.props.ownNewMessage) && this.props.lastViewed !== 0 && post.create_at > this.props.lastViewed && !renderedLastViewed) { @@ -417,10 +427,12 @@ export default class PostList extends React.Component { } window.addEventListener('resize', this.handleResize); + window.addEventListener('keydown', this.handleKeyDown); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize); + window.removeEventListener('keydown', this.handleKeyDown); this.scrollStopAction.cancel(); } @@ -515,7 +527,8 @@ export default class PostList extends React.Component { } PostList.defaultProps = { - lastViewed: 0 + lastViewed: 0, + ownNewMessage: false }; PostList.propTypes = { @@ -529,6 +542,7 @@ PostList.propTypes = { showMoreMessagesTop: React.PropTypes.bool, showMoreMessagesBottom: React.PropTypes.bool, lastViewed: React.PropTypes.number, + ownNewMessage: React.PropTypes.bool, postsToHighlight: React.PropTypes.object, displayNameType: React.PropTypes.string, displayPostsInCenter: React.PropTypes.bool, diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx index 17c3e94ae..3aba569fe 100644 --- a/webapp/components/post_view/post_view_controller.jsx +++ b/webapp/components/post_view/post_view_controller.jsx @@ -27,6 +27,7 @@ export default class PostViewController extends React.Component { this.onPostsChange = this.onPostsChange.bind(this); this.onEmojisChange = this.onEmojisChange.bind(this); this.onPostsViewJumpRequest = this.onPostsViewJumpRequest.bind(this); + this.onSetNewMessageIndicator = this.onSetNewMessageIndicator.bind(this); this.onPostListScroll = this.onPostListScroll.bind(this); this.onActivate = this.onActivate.bind(this); this.onDeactivate = this.onDeactivate.bind(this); @@ -50,6 +51,7 @@ export default class PostViewController extends React.Component { profiles, atTop: PostStore.getVisibilityAtTop(channel.id), lastViewed, + ownNewMessage: false, scrollType: ScrollTypes.NEW_MESSAGE, displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), displayPostsInCenter: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_CENTERED, @@ -117,6 +119,7 @@ export default class PostViewController extends React.Component { PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.onPostsViewJumpRequest); EmojiStore.addChangeListener(this.onEmojisChange); + ChannelStore.addLastViewedListener(this.onSetNewMessageIndicator); } onDeactivate() { @@ -125,6 +128,7 @@ export default class PostViewController extends React.Component { PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.onPostsViewJumpRequest); EmojiStore.removeChangeListener(this.onEmojisChange); + ChannelStore.removeLastViewedListener(this.onSetNewMessageIndicator); } componentWillReceiveProps(nextProps) { @@ -149,6 +153,7 @@ export default class PostViewController extends React.Component { this.setState({ channel, lastViewed, + ownNewMessage: false, profiles: JSON.parse(JSON.stringify(profiles)), postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(channel.id))), displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'), @@ -178,6 +183,10 @@ export default class PostViewController extends React.Component { } } + onSetNewMessageIndicator(lastViewed, ownNewMessage) { + this.setState({lastViewed, ownNewMessage}); + } + onPostListScroll(atBottom) { if (atBottom) { this.setState({scrollType: ScrollTypes.BOTTOM}); @@ -219,6 +228,10 @@ export default class PostViewController extends React.Component { return true; } + if (nextState.ownNewMessage !== this.state.ownNewMessage) { + return true; + } + if (nextState.showMoreMessagesTop !== this.state.showMoreMessagesTop) { return true; } @@ -277,6 +290,7 @@ export default class PostViewController extends React.Component { useMilitaryTime={this.state.useMilitaryTime} lastViewed={this.state.lastViewed} emojis={this.state.emojis} + ownNewMessage={this.state.ownNewMessage} /> ); } diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx index 2184b9fab..47426f6db 100644 --- a/webapp/components/suggestion/suggestion_box.jsx +++ b/webapp/components/suggestion/suggestion_box.jsx @@ -128,6 +128,7 @@ export default class SuggestionBox extends React.Component { e.preventDefault(); } else if (e.which === KeyCodes.ESCAPE) { GlobalActions.emitClearSuggestions(this.suggestionId); + e.stopPropagation(); } else if (this.props.onKeyDown) { this.props.onKeyDown(e); } diff --git a/webapp/sass/layout/_sidebar-left.scss b/webapp/sass/layout/_sidebar-left.scss index d4d01c865..4c718327e 100644 --- a/webapp/sass/layout/_sidebar-left.scss +++ b/webapp/sass/layout/_sidebar-left.scss @@ -178,6 +178,10 @@ border-radius: 0; font-weight: 400; position: relative; + + &.unread-title { + font-weight: 600; + } } } } diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx index dc2577811..542fbdf6b 100644 --- a/webapp/stores/channel_store.jsx +++ b/webapp/stores/channel_store.jsx @@ -13,6 +13,7 @@ const CHANGE_EVENT = 'change'; const LEAVE_EVENT = 'leave'; const MORE_CHANGE_EVENT = 'change'; const EXTRA_INFO_EVENT = 'extra_info'; +const LAST_VIEVED_EVENT = 'last_viewed'; class ChannelStoreClass extends EventEmitter { constructor(props) { @@ -32,6 +33,9 @@ class ChannelStoreClass extends EventEmitter { this.emitLeave = this.emitLeave.bind(this); this.addLeaveListener = this.addLeaveListener.bind(this); this.removeLeaveListener = this.removeLeaveListener.bind(this); + this.emitLastViewed = this.emitLastViewed.bind(this); + this.addLastViewedListener = this.addLastViewedListener.bind(this); + this.removeLastViewedListener = this.removeLastViewedListener.bind(this); this.findFirstBy = this.findFirstBy.bind(this); this.get = this.get.bind(this); this.getMember = this.getMember.bind(this); @@ -109,6 +113,18 @@ class ChannelStoreClass extends EventEmitter { this.removeListener(LEAVE_EVENT, callback); } + emitLastViewed(lastViewed, ownNewMessage) { + this.emit(LAST_VIEVED_EVENT, lastViewed, ownNewMessage); + } + + addLastViewedListener(callback) { + this.on(LAST_VIEVED_EVENT, callback); + } + + removeLastViewedListener(callback) { + this.removeListener(LAST_VIEVED_EVENT, callback); + } + findFirstBy(field, value) { return this.doFindFirst(field, value, this.getChannels()); } diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index e55742140..2e26278b2 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -145,6 +145,43 @@ export function updateLastViewedAt(id) { ); } +export function setLastViewedAt(lastViewedAt, id) { + let channelId; + if (id) { + channelId = id; + } else { + channelId = ChannelStore.getCurrentId(); + } + + if (channelId == null) { + return; + } + + if (lastViewedAt == null) { + return; + } + + if (isCallInProgress(`setLastViewedAt${channelId}${lastViewedAt}`)) { + return; + } + + callTracker[`setLastViewedAt${channelId}${lastViewedAt}`] = utils.getTimestamp(); + Client.setLastViewedAt( + channelId, + lastViewedAt, + () => { + callTracker.setLastViewedAt = 0; + ErrorStore.clearLastError(); + }, + (err) => { + callTracker.setLastViewedAt = 0; + var count = ErrorStore.getConnectionErrorCount(); + ErrorStore.setConnectionErrorCount(count + 1); + dispatchError(err, 'setLastViewedAt'); + } + ); +} + export function getMoreChannels(force) { if (isCallInProgress('getMoreChannels')) { return; |