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
This commit is contained in:
enahum
2016-08-18 17:37:55 -05:00
committed by Corey Hulen
parent 4a2fbcaf98
commit ed6b69aab3
9 changed files with 193 additions and 37 deletions

View File

@@ -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)))
}
}

View File

@@ -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"
}
]

View File

@@ -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)
}

View File

@@ -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)
});
}

View File

@@ -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,

View File

@@ -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 (
<SettingItemMax
title={
<FormattedMessage
id='user.settings.advance.joinLeaveTitle'
defaultMessage='Enable Join/Leave Messages'
/>
}
inputs={
<div>
<div className='radio'>
<label>
<input
type='radio'
name='join_leave'
checked={this.state.settings.join_leave !== 'false'}
onChange={this.updateSetting.bind(this, 'join_leave', 'true')}
/>
<FormattedMessage
id='user.settings.advance.on'
defaultMessage='On'
/>
</label>
<br/>
</div>
<div className='radio'>
<label>
<input
type='radio'
name='join_leave'
checked={this.state.settings.join_leave === 'false'}
onChange={this.updateSetting.bind(this, 'join_leave', 'false')}
/>
<FormattedMessage
id='user.settings.advance.off'
defaultMessage='Off'
/>
</label>
<br/>
</div>
<div>
<br/>
<FormattedMessage
id='user.settings.advance.joinLeaveDesc'
defaultMessage='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.'
/>
</div>
</div>
}
submit={() => this.handleSubmit('join_leave')}
server_error={this.state.serverError}
updateSection={(e) => {
this.updateSection('');
e.preventDefault();
}}
/>
);
}
return (
<SettingItemMin
title={
<FormattedMessage
id='user.settings.advance.joinLeaveTitle'
defaultMessage='Enable Join/Leave Messages'
/>
}
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 = <div className='divider-light'/>;
}
const displayJoinLeaveSection = this.renderJoinLeaveSection();
let displayJoinLeaveSectionDivider = null;
if (displayJoinLeaveSection) {
displayJoinLeaveSectionDivider = <div className='divider-light'/>;
}
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}
<div className='divider-dark'/>

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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();