From ed6b69aab3136b2a5bcddbab77659640cd4d6534 Mon Sep 17 00:00:00 2001 From: enahum Date: Thu, 18 Aug 2016 17:37:55 -0500 Subject: [PATCH] PLT-3754 EE: Add a Display Option to disable Join/Leave messages (#3808) * PLT-3754 EE: Add a Display Option to disable Join/Leave messages * Differentiate between join/leave add/remove messages * Update user removed from channel text message --- api/channel.go | 70 ++++++++------ i18n/en.json | 8 ++ model/post.go | 3 +- .../post_view/post_focus_view_controller.jsx | 16 +++- .../post_view/post_view_controller.jsx | 15 ++- .../user_settings/user_settings_advanced.jsx | 93 +++++++++++++++++++ webapp/i18n/en.json | 2 + webapp/stores/notification_store.jsx | 5 +- webapp/stores/post_store.jsx | 18 ++++ 9 files changed, 193 insertions(+), 37 deletions(-) diff --git a/api/channel.go b/api/channel.go index 3fef273e5a..5d3021cee9 100644 --- a/api/channel.go +++ b/api/channel.go @@ -530,7 +530,7 @@ func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel stor if _, err := AddUserToChannel(user, channel); err != nil { return err, nil } - go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username)) + go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE) } else { return model.NewLocAppError("join", "api.channel.join_channel.permissions.app_error", nil, ""), nil } @@ -538,11 +538,11 @@ func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel stor } } -func PostUserAddRemoveMessage(c *Context, channelId string, message string) { +func PostUserAddRemoveMessage(c *Context, channelId string, message, postType string) { post := &model.Post{ ChannelId: channelId, Message: message, - Type: model.POST_JOIN_LEAVE, + Type: postType, } if _, err := CreatePost(c, post, false); err != nil { l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err) @@ -677,7 +677,7 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) { RemoveUserFromChannel(c.Session.UserId, c.Session.UserId, channel) - go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username)) + go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE) result := make(map[string]string) result["id"] = channel.Id @@ -992,7 +992,7 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("name=" + channel.Name + " user_id=" + userId) - go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username)) + go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE) <-Srv.Store.Channel().UpdateLastViewedAt(id, oUser.Id) w.Write([]byte(cm.ToJson())) @@ -1014,38 +1014,48 @@ func removeMember(c *Context, w http.ResponseWriter, r *http.Request) { sc := Srv.Store.Channel().Get(channelId) cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId) + ouc := Srv.Store.User().Get(userIdToRemove) - if cresult := <-sc; cresult.Err != nil { - c.Err = cresult.Err - return - } else if cmcresult := <-cmc; cmcresult.Err != nil { - c.Err = cmcresult.Err + if oresult := <-ouc; oresult.Err != nil { + c.Err = model.NewLocAppError("removeMember", "api.channel.remove_member.user.app_error", nil, "") return } else { - channel := cresult.Data.(*model.Channel) - removerChannelMember := cmcresult.Data.(model.ChannelMember) + oUser := oresult.Data.(*model.User) - if !c.HasPermissionsToTeam(channel.TeamId, "removeMember") { + if cresult := <-sc; cresult.Err != nil { + c.Err = cresult.Err return - } - - if !strings.Contains(removerChannelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !c.IsTeamAdmin() { - c.Err = model.NewLocAppError("updateChannel", "api.channel.remove_member.permissions.app_error", nil, "") - c.Err.StatusCode = http.StatusForbidden + } else if cmcresult := <-cmc; cmcresult.Err != nil { + c.Err = cmcresult.Err return + } else { + channel := cresult.Data.(*model.Channel) + removerChannelMember := cmcresult.Data.(model.ChannelMember) + + if !c.HasPermissionsToTeam(channel.TeamId, "removeMember") { + return + } + + if !strings.Contains(removerChannelMember.Roles, model.CHANNEL_ROLE_ADMIN) && !c.IsTeamAdmin() { + c.Err = model.NewLocAppError("updateChannel", "api.channel.remove_member.permissions.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden + return + } + + if err := RemoveUserFromChannel(userIdToRemove, c.Session.UserId, channel); err != nil { + c.Err = model.NewLocAppError("updateChannel", "api.channel.remove_member.unable.app_error", nil, err.Message) + return + } + + c.LogAudit("name=" + channel.Name + " user_id=" + userIdToRemove) + + go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.remove_member.removed"), oUser.Username), model.POST_ADD_REMOVE) + + result := make(map[string]string) + result["channel_id"] = channel.Id + result["removed_user_id"] = userIdToRemove + w.Write([]byte(model.MapToJson(result))) } - - if err := RemoveUserFromChannel(userIdToRemove, c.Session.UserId, channel); err != nil { - c.Err = model.NewLocAppError("updateChannel", "api.channel.remove_member.unable.app_error", nil, err.Message) - return - } - - c.LogAudit("name=" + channel.Name + " user_id=" + userIdToRemove) - - result := make(map[string]string) - result["channel_id"] = channel.Id - result["removed_user_id"] = userIdToRemove - w.Write([]byte(model.MapToJson(result))) } } diff --git a/i18n/en.json b/i18n/en.json index 823a6f2597..a4d01239ba 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4822,5 +4822,13 @@ { "id": "web.watcher_fail.error", "translation": "Failed to add directory to watcher %v" + }, + { + "id": "api.channel.remove_member.removed", + "translation": "%v was removed from the channel." + }, + { + "id": "api.channel.remove_member.user.app_error", + "translation": "Failed to find user to be removed" } ] diff --git a/model/post.go b/model/post.go index 175aecdd71..33caeb9ea2 100644 --- a/model/post.go +++ b/model/post.go @@ -15,6 +15,7 @@ const ( POST_SLACK_ATTACHMENT = "slack_attachment" POST_SYSTEM_GENERIC = "system_generic" POST_JOIN_LEAVE = "system_join_leave" + POST_ADD_REMOVE = "system_add_remove" POST_HEADER_CHANGE = "system_header_change" POST_CHANNEL_DELETED = "system_channel_deleted" POST_EPHEMERAL = "system_ephemeral" @@ -109,7 +110,7 @@ func (o *Post) IsValid() *AppError { } // should be removed once more message types are supported - if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE) { + if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_ADD_REMOVE || o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE) { return NewLocAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type) } diff --git a/webapp/components/post_view/post_focus_view_controller.jsx b/webapp/components/post_view/post_focus_view_controller.jsx index 4a7d312f53..a1c4741843 100644 --- a/webapp/components/post_view/post_focus_view_controller.jsx +++ b/webapp/components/post_view/post_focus_view_controller.jsx @@ -34,8 +34,10 @@ export default class PostFocusView extends React.Component { profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); } + const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); + this.state = { - postList: PostStore.getVisiblePosts(focusedPostId), + postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled), currentUser: UserStore.getCurrentUser(), profiles, scrollType: ScrollTypes.POST, @@ -79,9 +81,11 @@ export default class PostFocusView extends React.Component { return; } + const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); + this.setState({ scrollPostId: focusedPostId, - postList: PostStore.getVisiblePosts(focusedPostId), + postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled), atTop: PostStore.getVisibilityAtTop(focusedPostId), atBottom: PostStore.getVisibilityAtBottom(focusedPostId) }); @@ -103,7 +107,15 @@ export default class PostFocusView extends React.Component { } onPreferenceChange() { + const focusedPostId = PostStore.getFocusedPostId(); + if (focusedPostId == null) { + return; + } + + const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); + this.setState({ + postList: PostStore.filterPosts(focusedPostId, joinLeaveEnabled), flaggedPosts: PreferenceStore.getCategory(Constants.Preferences.CATEGORY_FLAGGED_POST) }); } diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx index 1dd5e9176a..2451dfc8d3 100644 --- a/webapp/components/post_view/post_view_controller.jsx +++ b/webapp/components/post_view/post_view_controller.jsx @@ -44,9 +44,11 @@ export default class PostViewController extends React.Component { lastViewed = member.last_viewed_at; } + const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); + this.state = { channel, - postList: PostStore.getVisiblePosts(channel.id), + postList: PostStore.filterPosts(channel.id, joinLeaveEnabled), currentUser: UserStore.getCurrentUser(), profiles, atTop: PostStore.getVisibilityAtTop(channel.id), @@ -83,7 +85,10 @@ export default class PostViewController extends React.Component { previewSuffix = '_' + Utils.generateId(); } + const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); + this.setState({ + postList: PostStore.filterPosts(this.state.channel.id, joinLeaveEnabled), 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, compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, @@ -103,8 +108,10 @@ export default class PostViewController extends React.Component { } onPostsChange() { + const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); + this.setState({ - postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(this.state.channel.id))), + postList: PostStore.filterPosts(this.state.channel.id, joinLeaveEnabled), atTop: PostStore.getVisibilityAtTop(this.state.channel.id) }); } @@ -152,12 +159,14 @@ export default class PostViewController extends React.Component { profiles = Object.assign({}, profiles, UserStore.getDirectProfiles()); } + const joinLeaveEnabled = PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true); + this.setState({ channel, lastViewed, ownNewMessage: false, profiles: JSON.parse(JSON.stringify(profiles)), - postList: JSON.parse(JSON.stringify(PostStore.getVisiblePosts(channel.id))), + postList: PostStore.filterPosts(channel.id, joinLeaveEnabled), 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, compactDisplay: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx index c647dd0fd0..157488559c 100644 --- a/webapp/components/user_settings/user_settings_advanced.jsx +++ b/webapp/components/user_settings/user_settings_advanced.jsx @@ -27,6 +27,7 @@ export default class AdvancedSettingsDisplay extends React.Component { this.saveEnabledFeatures = this.saveEnabledFeatures.bind(this); this.renderFormattingSection = this.renderFormattingSection.bind(this); + this.renderJoinLeaveSection = this.renderJoinLeaveSection.bind(this); this.state = this.getStateFromStores(); } @@ -44,6 +45,11 @@ export default class AdvancedSettingsDisplay extends React.Component { Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'formatting', 'true' + ), + join_leave: PreferenceStore.get( + Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, + 'join_leave', + 'true' ) }; @@ -232,6 +238,85 @@ export default class AdvancedSettingsDisplay extends React.Component { ); } + renderJoinLeaveSection() { + if (window.mm_config.BuildEnterpriseReady === 'true' && window.mm_license && window.mm_license.IsLicensed === 'true') { + if (this.props.activeSection === 'join_leave') { + return ( + + } + inputs={ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+ } + submit={() => this.handleSubmit('join_leave')} + server_error={this.state.serverError} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } + + return ( + + } + describe={this.renderOnOffLabel(this.state.settings.join_leave)} + updateSection={() => this.props.updateSection('join_leave')} + /> + ); + } + + return null; + } + renderFeatureLabel(feature) { switch (feature) { case 'MARKDOWN_PREVIEW': @@ -342,6 +427,12 @@ export default class AdvancedSettingsDisplay extends React.Component { formattingSectionDivider =
; } + const displayJoinLeaveSection = this.renderJoinLeaveSection(); + let displayJoinLeaveSectionDivider = null; + if (displayJoinLeaveSection) { + displayJoinLeaveSectionDivider =
; + } + let previewFeaturesSection; let previewFeaturesSectionDivider; if (this.state.preReleaseFeaturesKeys.length > 0) { @@ -454,6 +545,8 @@ export default class AdvancedSettingsDisplay extends React.Component { {ctrlSendSection} {formattingSectionDivider} {formattingSection} + {displayJoinLeaveSectionDivider} + {displayJoinLeaveSection} {previewFeaturesSectionDivider} {previewFeaturesSection}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 6c69c3f7cb..d8a5dfc6b5 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -1713,6 +1713,8 @@ "user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}} Enabled", "user.settings.advance.formattingDesc": "If enabled, posts will be formatted to create links, show emoji, style the text, and add line breaks. By default, this setting is enabled. Changing this setting requires the page to be refreshed.", "user.settings.advance.formattingTitle": "Enable Post Formatting", + "user.settings.advance.joinLeaveDesc": "When \"On\", System Messages saying a user has joined or left a channel will be visible. When \"Off\", the System Messages about joining or leaving a channel will be hidden. A message will still show up when you are added to a channel, so you can receive a notification.", + "user.settings.advance.joinLeaveTitle": "Enable Join/Leave Messages", "user.settings.advance.markdown_preview": "Show markdown preview option in message input box", "user.settings.advance.off": "Off", "user.settings.advance.on": "On", diff --git a/webapp/stores/notification_store.jsx b/webapp/stores/notification_store.jsx index e05d203291..507954a10a 100644 --- a/webapp/stores/notification_store.jsx +++ b/webapp/stores/notification_store.jsx @@ -6,6 +6,7 @@ import EventEmitter from 'events'; import Constants from 'utils/constants.jsx'; import UserStore from './user_store.jsx'; import ChannelStore from './channel_store.jsx'; +import PreferenceStore from './preference_store.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; const ActionTypes = Constants.ActionTypes; @@ -28,7 +29,9 @@ class NotificationStoreClass extends EventEmitter { handleRecievedPost(post, msgProps) { // Send desktop notification if ((UserStore.getCurrentId() !== post.user_id || post.props.from_webhook === 'true')) { - if (PostUtils.isSystemMessage(post) && post.type !== 'system_join_leave') { + if (PostUtils.isSystemMessage(post)) { + return; + } else if (!PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'join_leave', true) && post.type === Constants.POST_TYPE_JOIN_LEAVE) { return; } diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx index 135563866a..0b22772556 100644 --- a/webapp/stores/post_store.jsx +++ b/webapp/stores/post_store.jsx @@ -548,6 +548,24 @@ class PostStoreClass extends EventEmitter { return commentCount; } + + filterPosts(channelId, joinLeave) { + const postsList = JSON.parse(JSON.stringify(this.getVisiblePosts(channelId))); + + if (!joinLeave && postsList) { + postsList.order = postsList.order.filter((id) => { + if (postsList.posts[id].type === Constants.POST_TYPE_JOIN_LEAVE) { + Reflect.deleteProperty(postsList.posts, id); + + return false; + } + + return true; + }); + } + + return postsList; + } } var PostStore = new PostStoreClass();