summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/channel.go29
-rw-r--r--i18n/en.json6
-rw-r--r--store/sql_channel_store.go52
-rw-r--r--store/store.go1
-rw-r--r--webapp/actions/channel_actions.jsx12
-rw-r--r--webapp/actions/post_actions.jsx52
-rw-r--r--webapp/components/edit_post_modal.jsx82
-rw-r--r--webapp/components/post_view/components/post.jsx9
-rw-r--r--webapp/components/post_view/components/post_list.jsx18
-rw-r--r--webapp/components/post_view/post_view_controller.jsx14
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx1
-rw-r--r--webapp/sass/layout/_sidebar-left.scss4
-rw-r--r--webapp/stores/channel_store.jsx16
-rw-r--r--webapp/utils/async_client.jsx37
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;