diff --git a/api/channel.go b/api/channel.go index 3fef273e5a0..5d3021cee9c 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 823a6f2597c..a4d01239bac 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 175aecdd71f..33caeb9ea25 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 4a7d312f53b..a1c4741843d 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 1dd5e9176a2..2451dfc8d34 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 c647dd0fd04..157488559c0 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 6c69c3f7cbd..d8a5dfc6b54 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 e05d2032916..507954a10a1 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 135563866a3..0b22772556f 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();