diff options
author | samogot <samogot@gmail.com> | 2016-07-19 15:27:23 +0300 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-07-19 08:27:23 -0400 |
commit | f31e8e09f54418f867f95192a71e67b450340c13 (patch) | |
tree | 313f38a9bd8c999909b26cf49172df32e427dedc | |
parent | febe3a01cd5db03d152e993d42f39800e494a83a (diff) | |
download | chat-f31e8e09f54418f867f95192a71e67b450340c13.tar.gz chat-f31e8e09f54418f867f95192a71e67b450340c13.tar.bz2 chat-f31e8e09f54418f867f95192a71e67b450340c13.zip |
PLT-914 Add mention notifications for replies on a comment thread (#3130)
* PLT-914 Add mention notifications for replies on a comment thread
* remove useless store method
fix highlighting comments posted before th user write something to thread
* refactor out isCommentMention function after rebase
* change comment bar highlighting to replay icon mention highlighting
* settings and always visible highlight
* fix unit tests for new settings
* change highlight behaviour
- if any message in comment thread generates mention - all thread is highlighted
- remove always visible highlightion
* fix bug about the textarea in the center channel not clearing
* fix default settings value notify_props.comments
* do not highlight own comments if there is no other user's messages in thread
* refactor out ReactDOM.findDOMNode
* refactor out using of UserStore from component
-rw-r--r-- | api/post.go | 27 | ||||
-rw-r--r-- | api/user.go | 6 | ||||
-rw-r--r-- | api/user_test.go | 10 | ||||
-rw-r--r-- | i18n/en.json | 4 | ||||
-rw-r--r-- | webapp/components/post_view/components/post.jsx | 6 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_header.jsx | 2 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_info.jsx | 8 | ||||
-rw-r--r-- | webapp/components/post_view/components/post_list.jsx | 27 | ||||
-rw-r--r-- | webapp/components/user_settings/user_settings_notifications.jsx | 154 | ||||
-rw-r--r-- | webapp/i18n/en.json | 5 | ||||
-rw-r--r-- | webapp/sass/layout/_post.scss | 5 |
11 files changed, 232 insertions, 22 deletions
diff --git a/api/post.go b/api/post.go index 4533823f6..951ccb527 100644 --- a/api/post.go +++ b/api/post.go @@ -535,9 +535,8 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * return model.SplitRunes[c] } splitMessage := strings.Fields(post.Message) + var userIds []string for _, word := range splitMessage { - var userIds []string - // Non-case-sensitive check for regular keys if ids, match := keywordMap[strings.ToLower(word)]; match { userIds = append(userIds, ids...) @@ -565,14 +564,30 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * } } } + } + + if len(post.RootId) > 0 { + if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil { + l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err) + return + } else { + list := result.Data.(*model.PostList) - for _, userId := range userIds { - if post.UserId == userId && post.Props["from_webhook"] != "true" { - continue + for _, threadPost := range list.Posts { + profile := profileMap[threadPost.UserId] + if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) { + userIds = append(userIds, threadPost.UserId) + } } + } + } - mentionedUserIds[userId] = true + for _, userId := range userIds { + if post.UserId == userId && post.Props["from_webhook"] != "true" { + continue } + + mentionedUserIds[userId] = true } for id := range mentionedUserIds { diff --git a/api/user.go b/api/user.go index 652da14ad..7dd24fe1b 100644 --- a/api/user.go +++ b/api/user.go @@ -1930,6 +1930,12 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) { return } + comments := props["comments"] + if len(comments) == 0 { + c.SetInvalidParam("updateUserNotify", "comments") + return + } + var user *model.User if result := <-uchan; result.Err != nil { c.Err = result.Err diff --git a/api/user_test.go b/api/user_test.go index 7d1e4026c..1b6662269 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1247,6 +1247,7 @@ func TestUserUpdateNotify(t *testing.T) { data["email"] = "true" data["desktop"] = "all" data["desktop_sound"] = "false" + data["comments"] = "any" if _, err := Client.UpdateUserNotify(data); err == nil { t.Fatal("Should have errored - not logged in") @@ -1267,6 +1268,9 @@ func TestUserUpdateNotify(t *testing.T) { if result.Data.(*model.User).NotifyProps["email"] != data["email"] { t.Fatal("NotifyProps did not update properly - email") } + if result.Data.(*model.User).NotifyProps["comments"] != data["comments"] { + t.Fatal("NotifyProps did not update properly - comments") + } } if _, err := Client.UpdateUserNotify(nil); err == nil { @@ -1300,6 +1304,12 @@ func TestUserUpdateNotify(t *testing.T) { if _, err := Client.UpdateUserNotify(data); err == nil { t.Fatal("Should have errored - empty email") } + + data["email"] = "true" + data["comments"] = "" + if _, err := Client.UpdateUserNotify(data); err == nil { + t.Fatal("Should have errored - empty comments") + } } func TestFuzzyUserCreate(t *testing.T) { diff --git a/i18n/en.json b/i18n/en.json index c29059ebb..f5e2f1e87 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1024,6 +1024,10 @@ "translation": "Failed to update direct channel preference user_id=%v other_user_id=%v err=%v" }, { + "id": "api.post.send_notifications_and_forget.comment_thread.error", + "translation": "Failed to retrieve comment thread posts in notifications root_post_id=%v, err=%v" + }, + { "id": "api.post.send_notifications_and_forget.mention_body", "translation": "You have one new mention." }, diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx index ff443e355..3fdd8094e 100644 --- a/webapp/components/post_view/components/post.jsx +++ b/webapp/components/post_view/components/post.jsx @@ -76,6 +76,10 @@ export default class Post extends React.Component { return true; } + if (nextProps.isCommentMention !== this.props.isCommentMention) { + return true; + } + if (nextProps.shouldHighlight !== this.props.shouldHighlight) { return true; } @@ -231,6 +235,7 @@ export default class Post extends React.Component { post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} + isCommentMention={this.props.isCommentMention} handleCommentClick={this.handleCommentClick} handleDropdownOpened={this.handleDropdownOpened} isLastComment={this.props.isLastComment} @@ -274,6 +279,7 @@ Post.propTypes = { compactDisplay: React.PropTypes.bool, previewCollapsed: React.PropTypes.string, commentCount: React.PropTypes.number, + isCommentMention: React.PropTypes.bool, useMilitaryTime: React.PropTypes.bool.isRequired, emojis: React.PropTypes.object.isRequired }; diff --git a/webapp/components/post_view/components/post_header.jsx b/webapp/components/post_view/components/post_header.jsx index e76358304..07b601baf 100644 --- a/webapp/components/post_view/components/post_header.jsx +++ b/webapp/components/post_view/components/post_header.jsx @@ -63,6 +63,7 @@ export default class PostHeader extends React.Component { <PostInfo post={post} commentCount={this.props.commentCount} + isCommentMention={this.props.isCommentMention} handleCommentClick={this.props.handleCommentClick} handleDropdownOpened={this.props.handleDropdownOpened} allowReply='true' @@ -89,6 +90,7 @@ PostHeader.propTypes = { user: React.PropTypes.object, currentUser: React.PropTypes.object.isRequired, commentCount: React.PropTypes.number.isRequired, + isCommentMention: React.PropTypes.bool.isRequired, isLastComment: React.PropTypes.bool.isRequired, handleCommentClick: React.PropTypes.func.isRequired, handleDropdownOpened: React.PropTypes.func.isRequired, diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx index d74be4c72..98639529e 100644 --- a/webapp/components/post_view/components/post_info.jsx +++ b/webapp/components/post_view/components/post_info.jsx @@ -174,6 +174,7 @@ export default class PostInfo extends React.Component { var post = this.props.post; var comments = ''; var showCommentClass = ''; + var highlightMentionClass = ''; var commentCountText = this.props.commentCount; if (this.props.commentCount >= 1) { @@ -182,11 +183,15 @@ export default class PostInfo extends React.Component { commentCountText = ''; } + if (this.props.isCommentMention) { + highlightMentionClass = ' mention--highlight'; + } + if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !Utils.isPostEphemeral(post)) { comments = ( <a href='#' - className={'comment-icon__container' + showCommentClass} + className={'comment-icon__container' + showCommentClass + highlightMentionClass} onClick={this.props.handleCommentClick} > <span @@ -234,6 +239,7 @@ PostInfo.defaultProps = { PostInfo.propTypes = { post: React.PropTypes.object.isRequired, commentCount: React.PropTypes.number.isRequired, + isCommentMention: React.PropTypes.bool.isRequired, isLastComment: React.PropTypes.bool.isRequired, allowReply: React.PropTypes.string.isRequired, handleCommentClick: React.PropTypes.func.isRequired, diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index 70107c838..9f958a5b6 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -251,15 +251,35 @@ export default class PostList extends React.Component { } let commentCount = 0; + let nonOwnCommentsExists = false; + let isCommentMention = false; let commentRootId; if (parentPost) { commentRootId = post.root_id; } else { commentRootId = post.id; } - for (const postId in posts) { - if (posts[postId].root_id === commentRootId) { - commentCount += 1; + if (commentRootId) { + const commentsNotifyLevel = this.props.currentUser.notify_props.comments || 'never'; + for (const postId in posts) { + if (posts[postId].root_id === commentRootId) { + commentCount += 1; + if (posts[postId].user_id !== this.props.currentUser.id) { + nonOwnCommentsExists = true; + } + if (posts[postId].user_id === this.props.currentUser.id && commentsNotifyLevel === 'any' && !isCommentMention) { + for (const nextPostId in posts) { + if (posts[nextPostId].root_id === commentRootId && posts[nextPostId].user_id !== this.props.currentUser.id && + posts[postId].create_at < posts[nextPostId].create_at) { + isCommentMention = true; + break; + } + } + } + } + } + if (nonOwnCommentsExists && posts[commentRootId].user_id === this.props.currentUser.id && commentsNotifyLevel !== 'never') { + isCommentMention = true; } } @@ -279,6 +299,7 @@ export default class PostList extends React.Component { currentUser={this.props.currentUser} center={this.props.displayPostsInCenter} commentCount={commentCount} + isCommentMention={isCommentMention} compactDisplay={this.props.compactDisplay} previewCollapsed={this.props.previewsCollapsed} useMilitaryTime={this.props.useMilitaryTime} diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx index 5ae2a83af..b9e9b6de1 100644 --- a/webapp/components/user_settings/user_settings_notifications.jsx +++ b/webapp/components/user_settings/user_settings_notifications.jsx @@ -2,7 +2,6 @@ // See License.txt for license information. import $ from 'jquery'; -import ReactDOM from 'react-dom'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; @@ -26,6 +25,10 @@ function getNotificationsStateFromStores() { if (user.notify_props && user.notify_props.desktop) { desktop = user.notify_props.desktop; } + var comments = 'never'; + if (user.notify_props && user.notify_props.comments) { + comments = user.notify_props.comments; + } var email = 'true'; if (user.notify_props && user.notify_props.email) { email = user.notify_props.email; @@ -82,7 +85,8 @@ function getNotificationsStateFromStores() { customKeys, customKeysChecked: customKeys.length > 0, firstNameKey, - channelKey + channelKey, + notifyCommentsLevel: comments }; } @@ -103,6 +107,10 @@ const holders = defineMessages({ id: 'user.settings.notifications.wordsTrigger', defaultMessage: 'Words that trigger mentions' }, + comments: { + id: 'user.settings.notifications.comments', + defaultMessage: 'Comment threads notifications' + }, close: { id: 'user.settings.notifications.close', defaultMessage: 'Close' @@ -140,6 +148,7 @@ class NotificationsTab extends React.Component { data.desktop_sound = this.state.enableSound; data.desktop = this.state.notifyLevel; data.push = this.state.notifyPushLevel; + data.comments = this.state.notifyCommentsLevel; var mentionKeys = []; if (this.state.usernameKey) { @@ -195,21 +204,26 @@ class NotificationsTab extends React.Component { } handleNotifyRadio(notifyLevel) { this.setState({notifyLevel}); - ReactDOM.findDOMNode(this.refs.wrapper).focus(); + this.refs.wrapper.focus(); + } + + handleNotifyCommentsRadio(notifyCommentsLevel) { + this.setState({notifyCommentsLevel}); + this.refs.wrapper.focus(); } handlePushRadio(notifyPushLevel) { this.setState({notifyPushLevel}); - ReactDOM.findDOMNode(this.refs.wrapper).focus(); + this.refs.wrapper.focus(); } handleEmailRadio(enableEmail) { this.setState({enableEmail}); - ReactDOM.findDOMNode(this.refs.wrapper).focus(); + this.refs.wrapper.focus(); } handleSoundRadio(enableSound) { this.setState({enableSound}); - ReactDOM.findDOMNode(this.refs.wrapper).focus(); + this.refs.wrapper.focus(); } updateUsernameKey(val) { this.setState({usernameKey: val}); @@ -224,10 +238,10 @@ class NotificationsTab extends React.Component { this.setState({channelKey: val}); } updateCustomMentionKeys() { - var checked = ReactDOM.findDOMNode(this.refs.customcheck).checked; + var checked = this.refs.customcheck.checked; if (checked) { - var text = ReactDOM.findDOMNode(this.refs.custommentions).value; + var text = this.refs.custommentions.value; // remove all spaces and split string into individual keys this.setState({customKeys: text.replace(/ /g, ''), customKeysChecked: true}); @@ -236,7 +250,7 @@ class NotificationsTab extends React.Component { } } onCustomChange() { - ReactDOM.findDOMNode(this.refs.customcheck).checked = true; + this.refs.customcheck.checked = true; this.updateCustomMentionKeys(); } createPushNotificationSection() { @@ -902,6 +916,126 @@ class NotificationsTab extends React.Component { ); } + var commentsSection; + var handleUpdateCommentsSection; + if (this.props.activeSection === 'comments') { + var commentsActive = [false, false, false]; + if (this.state.notifyCommentsLevel === 'never') { + commentsActive[2] = true; + } else if (this.state.notifyCommentsLevel === 'root') { + commentsActive[1] = true; + } else { + commentsActive[0] = true; + } + + let inputs = []; + + inputs.push( + <div key='userNotificationLevelOption'> + <div className='radio'> + <label> + <input + type='radio' + name='commentsNotificationLevel' + checked={commentsActive[0]} + onChange={this.handleNotifyCommentsRadio.bind(this, 'any')} + /> + <FormattedMessage + id='user.settings.notifications.commentsAny' + defaultMessage='Mention any comments in a thread you participated in (This will include both mentions to your root post and any comments after you commented on a post)' + /> + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + type='radio' + name='commentsNotificationLevel' + checked={commentsActive[1]} + onChange={this.handleNotifyCommentsRadio.bind(this, 'root')} + /> + <FormattedMessage + id='user.settings.notifications.commentsRoot' + defaultMessage='Mention any comments on your post' + /> + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + type='radio' + name='commentsNotificationLevel' + checked={commentsActive[2]} + onChange={this.handleNotifyCommentsRadio.bind(this, 'never')} + /> + <FormattedMessage + id='user.settings.notifications.commentsNever' + defaultMessage='No mentions for comments' + /> + </label> + </div> + </div> + ); + + const extraInfo = ( + <span> + <FormattedMessage + id='user.settings.notifications.commentsInfo' + defaultMessage='Mode of triggering notifications on posts in comment threads you participated in.' + /> + </span> + ); + + commentsSection = ( + <SettingItemMax + title={formatMessage(holders.comments)} + extraInfo={extraInfo} + inputs={inputs} + submit={this.handleSubmit} + server_error={serverError} + updateSection={this.handleCancel} + /> + ); + } else { + let describe = ''; + if (this.state.notifyCommentsLevel === 'never') { + describe = ( + <FormattedMessage + id='user.settings.notifications.commentsNever' + defaultMessage='No mentions for comments' + /> + ); + } else if (this.state.notifyCommentsLevel === 'root') { + describe = ( + <FormattedMessage + id='user.settings.notifications.commentsRoot' + defaultMessage='Mention any comments on your post' + /> + ); + } else { + describe = ( + <FormattedMessage + id='user.settings.notifications.commentsAny' + defaultMessage='Mention any comments in a thread you participated in (This will include both mentions to your root post and any comments after you commented on a post)' + /> + ); + } + + handleUpdateCommentsSection = function updateCommentsSection() { + this.props.updateSection('comments'); + }.bind(this); + + commentsSection = ( + <SettingItemMin + title={formatMessage(holders.comments)} + describe={describe} + updateSection={handleUpdateCommentsSection} + /> + ); + } + const pushNotificationSection = this.createPushNotificationSection(); return ( @@ -952,6 +1086,8 @@ class NotificationsTab extends React.Component { {pushNotificationSection} <div className='divider-light'/> {keysSection} + <div className='divider-light'/> + {commentsSection} <div className='divider-dark'/> </div> </div> diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 27c5b7da4..3a514d0b3 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1610,6 +1610,11 @@ "user.settings.notification.soundConfig": "Please configure notification sounds in your browser settings", "user.settings.notifications.channelWide": "Channel-wide mentions \"@channel\", \"@all\"", "user.settings.notifications.close": "Close", + "user.settings.notifications.comments": "Comment threads notifications", + "user.settings.notifications.commentsAny": "Mention any comments in a thread you participated in (This will include both mentions to your root post and any comments after you commented on a post)", + "user.settings.notifications.commentsInfo": "Mode of triggering notifications on posts in comment threads you participated in.", + "user.settings.notifications.commentsNever": "No mentions for comments", + "user.settings.notifications.commentsRoot": "Mention any comments on your post", "user.settings.notifications.desktop": "Send desktop notifications", "user.settings.notifications.desktopSounds": "Desktop notification sounds", "user.settings.notifications.emailInfo": "Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from {siteName} for more than 5 minutes.", diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss index f95bb3e59..d4dec54d7 100644 --- a/webapp/sass/layout/_post.scss +++ b/webapp/sass/layout/_post.scss @@ -560,7 +560,7 @@ body.ios { .img-div { max-height: 150px; max-width: 150px; - } + } p { line-height: inherit; @@ -572,7 +572,7 @@ body.ios { ol, ul { - clear: both; + clear: both; padding-left: 20px; } } @@ -1070,7 +1070,6 @@ body.ios { display: inline-block; margin-right: 6px; visibility: hidden; - svg { fill: inherit; position: relative; |