[MM-37933] Channel preference to auto-follow all threads in the channel (#21430)

* Add auto-follow feature
---------

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Vishal
2023-05-05 12:09:00 +05:30
коммит произвёл GitHub
родитель 9f11fc59b5
Коммит d860548a76
23 изменённых файлов: 299 добавлений и 14 удалений

Просмотреть файл

@@ -1354,6 +1354,10 @@ func (a *App) UpdateChannelMemberNotifyProps(c request.CTX, data map[string]stri
filteredProps[model.IgnoreChannelMentionsNotifyProp] = ignoreChannelMentions
}
if channelAutoFollowThreads, exists := data[model.ChannelAutoFollowThreads]; exists {
filteredProps[model.ChannelAutoFollowThreads] = channelAutoFollowThreads
}
member, err := a.Srv().Store().Channel().UpdateMemberNotifyProps(channelID, userID, filteredProps)
if err != nil {
var appErr *model.AppError

Просмотреть файл

@@ -101,13 +101,15 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
followers := make(model.StringArray, 0)
followers := make(model.StringSet, 0)
if tchan != nil {
result = <-tchan
if result.NErr != nil {
return nil, result.NErr
}
followers = result.Data.([]string)
for _, v := range result.Data.([]string) {
followers.Add(v)
}
}
groups := make(map[string]*model.Group)
@@ -235,6 +237,14 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
threadParticipants[id] = true
}
if channel.Type != model.ChannelTypeDirect {
for id, propsMap := range channelMemberNotifyPropsMap {
if ok := followers.Has(id); !ok && propsMap[model.ChannelAutoFollowThreads] == model.ChannelAutoFollowThreadsOn {
threadParticipants[id] = true
}
}
}
// sema is a counting semaphore to throttle the number of concurrent DB requests.
// A concurrency of 8 should be sufficient.
// We don't want to set a higher limit which can bring down the DB.
@@ -286,8 +296,8 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
followersMutex.Lock()
// add new followers to existing followers
if threadMembership.Following && !followers.Contains(userID) {
followers = append(followers, userID)
if ok := followers.Has(userID); !ok && threadMembership.Following {
followers.Add(userID)
newParticipants[userID] = true
}
followersMutex.Unlock()
@@ -330,7 +340,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
notificationsForCRT := &CRTNotifiers{}
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
for uid := range followers {
profile := profileMap[uid]
if profile == nil || !a.IsCRTEnabledForUser(c, uid) {
continue
@@ -578,7 +588,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
// If this is a reply in a thread, notify participants
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
for uid := range followers {
// A user following a thread but had left the channel won't get a notification
// https://mattermost.atlassian.net/browse/MM-36769
if profileMap[uid] == nil {

Просмотреть файл

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
"github.com/mattermost/mattermost-server/server/v8/channels/utils"
"github.com/mattermost/mattermost-server/server/v8/model"
"github.com/mattermost/mattermost-server/server/v8/platform/shared/i18n"
@@ -2819,3 +2820,66 @@ func TestReplyPostNotificationsWithCRT(t *testing.T) {
assert.Nil(t, membership)
})
}
func TestChannelAutoFollowThreads(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
u1 := th.BasicUser
u2 := th.BasicUser2
u3 := th.CreateUser()
th.LinkUserToTeam(u3, th.BasicTeam)
c1 := th.BasicChannel
th.AddUserToChannel(u2, c1)
th.AddUserToChannel(u3, c1)
// Set auto-follow for user 2
member, appErr := th.App.UpdateChannelMemberNotifyProps(th.Context, map[string]string{model.ChannelAutoFollowThreads: model.ChannelAutoFollowThreadsOn}, c1.Id, u2.Id)
require.Nil(t, appErr)
require.Equal(t, model.ChannelAutoFollowThreadsOn, member.NotifyProps[model.ChannelAutoFollowThreads])
rootPost := &model.Post{
ChannelId: c1.Id,
Message: "root post by user3",
UserId: u3.Id,
}
rpost, appErr := th.App.CreatePost(th.Context, rootPost, c1, false, true)
require.Nil(t, appErr)
replyPost1 := &model.Post{
ChannelId: c1.Id,
Message: "reply post by user1",
UserId: u1.Id,
RootId: rpost.Id,
}
_, appErr = th.App.CreatePost(th.Context, replyPost1, c1, false, true)
require.Nil(t, appErr)
// user-2 starts auto-following thread
threadMembership, appErr := th.App.GetThreadMembershipForUser(u2.Id, rpost.Id)
require.Nil(t, appErr)
require.NotNil(t, threadMembership)
assert.True(t, threadMembership.Following)
// Set "following" to false
_, err := th.App.Srv().Store().Thread().MaintainMembership(u2.Id, rpost.Id, store.ThreadMembershipOpts{
Following: false,
UpdateFollowing: true,
})
require.NoError(t, err)
replyPost2 := &model.Post{
ChannelId: c1.Id,
Message: "reply post 2 by user1",
UserId: u1.Id,
RootId: rpost.Id,
}
_, appErr = th.App.CreatePost(th.Context, replyPost2, c1, false, true)
require.Nil(t, appErr)
// Do NOT start auto-following thread, once "un-followed"
threadMembership, appErr = th.App.GetThreadMembershipForUser(u2.Id, rpost.Id)
require.Nil(t, appErr)
require.NotNil(t, threadMembership)
assert.False(t, threadMembership.Following)
}

Просмотреть файл

@@ -8459,6 +8459,10 @@
"id": "model.channel.is_valid.update_at.app_error",
"translation": "Update at must be a valid time."
},
{
"id": "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error",
"translation": "Invalid channel-auto-follow-threads value."
},
{
"id": "model.channel_member.is_valid.channel_id.app_error",
"translation": "Invalid channel id."

Просмотреть файл

@@ -19,6 +19,9 @@ const (
IgnoreChannelMentionsOff = "off"
IgnoreChannelMentionsOn = "on"
IgnoreChannelMentionsNotifyProp = "ignore_channel_mentions"
ChannelAutoFollowThreadsOff = "off"
ChannelAutoFollowThreadsOn = "on"
ChannelAutoFollowThreads = "channel_auto_follow_threads"
)
type ChannelUnread struct {
@@ -172,6 +175,12 @@ func (o *ChannelMember) IsValid() *AppError {
}
}
if channelAutoFollowThreads, ok := o.NotifyProps[ChannelAutoFollowThreads]; ok {
if len(channelAutoFollowThreads) > 3 || !IsChannelAutoFollowThreadsValid(channelAutoFollowThreads) {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.channel_auto_follow_threads_value.app_error", nil, "channel_auto_follow_threads="+channelAutoFollowThreads, http.StatusBadRequest)
}
}
if len(o.Roles) > UserRolesMaxLength {
return NewAppError("ChannelMember.IsValid", "model.channel_member.is_valid.roles_limit.app_error",
map[string]any{"Limit": UserRolesMaxLength}, "", http.StatusBadRequest)
@@ -223,6 +232,10 @@ func IsIgnoreChannelMentionsValid(ignoreChannelMentions string) bool {
return ignoreChannelMentions == IgnoreChannelMentionsOn || ignoreChannelMentions == IgnoreChannelMentionsOff || ignoreChannelMentions == IgnoreChannelMentionsDefault
}
func IsChannelAutoFollowThreadsValid(channelAutoFollowThreads string) bool {
return channelAutoFollowThreads == ChannelAutoFollowThreadsOn || channelAutoFollowThreads == ChannelAutoFollowThreadsOff
}
func GetDefaultChannelNotifyProps() StringMap {
return StringMap{
DesktopNotifyProp: ChannelNotifyDefault,
@@ -230,5 +243,6 @@ func GetDefaultChannelNotifyProps() StringMap {
PushNotifyProp: ChannelNotifyDefault,
EmailNotifyProp: ChannelNotifyDefault,
IgnoreChannelMentionsNotifyProp: IgnoreChannelMentionsDefault,
ChannelAutoFollowThreads: ChannelAutoFollowThreadsOff,
}
}

Просмотреть файл

@@ -41,8 +41,26 @@ const (
var ErrMaxPropSizeExceeded = fmt.Errorf("max prop size of %d exceeded", maxPropSizeBytes)
type StringInterface map[string]any
type StringSet map[string]struct{}
type StringArray []string
func (ss StringSet) Has(val string) bool {
_, ok := ss[val]
return ok
}
func (ss StringSet) Add(val string) {
ss[val] = struct{}{}
}
func (ss StringSet) Val() []string {
keys := make([]string, 0, len(ss))
for k := range ss {
keys = append(keys, k)
}
return keys
}
func (sa StringArray) Remove(input string) StringArray {
for index := range sa {
if sa[index] == input {

Просмотреть файл

@@ -124,6 +124,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -145,6 +146,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -166,6 +168,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",

Просмотреть файл

@@ -95,6 +95,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -186,6 +187,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -277,6 +279,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",

Просмотреть файл

@@ -284,6 +284,7 @@ exports[`components/ChannelHeader should match snapshot with last active display
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -589,6 +590,7 @@ exports[`components/ChannelHeader should match snapshot with no last active disp
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -893,6 +895,7 @@ exports[`components/ChannelHeader should render active channel files 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -1196,6 +1199,7 @@ exports[`components/ChannelHeader should render active flagged posts 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -1499,6 +1503,7 @@ exports[`components/ChannelHeader should render active mentions posts 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -1802,6 +1807,7 @@ exports[`components/ChannelHeader should render active pinned posts 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -2074,6 +2080,7 @@ exports[`components/ChannelHeader should render archived view 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -2409,6 +2416,7 @@ exports[`components/ChannelHeader should render correct menu when muted 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -2712,6 +2720,7 @@ exports[`components/ChannelHeader should render not active channel files 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -3036,6 +3045,7 @@ exports[`components/ChannelHeader should render properly when custom status is e
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -3381,6 +3391,7 @@ exports[`components/ChannelHeader should render properly when custom status is s
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -3685,6 +3696,7 @@ exports[`components/ChannelHeader should render properly when empty 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -3988,6 +4000,7 @@ exports[`components/ChannelHeader should render properly when populated 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -4310,6 +4323,7 @@ exports[`components/ChannelHeader should render properly when populated with cha
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -4626,6 +4640,7 @@ exports[`components/ChannelHeader should render shared view 1`] = `
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",
@@ -4938,6 +4953,7 @@ exports[`components/ChannelHeader should render the pinned icon with the pinned
"msg_count": 0,
"msg_count_root": 0,
"notify_props": Object {
"channel_auto_follow_threads": "off",
"desktop": "default",
"email": "default",
"ignore_channel_mentions": "default",

Просмотреть файл

@@ -121,6 +121,20 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
serverError={null}
/>
</div>
<div
className="divider-light"
/>
<NotificationSection
channelAutoFollowThreads="off"
expand={false}
ignoreChannelMentions="off"
memberNotificationLevel="all"
onChange={[Function]}
onSubmit={[Function]}
onUpdateSection={[Function]}
section="channelAutoFollowThreads"
serverError={null}
/>
<div
className="divider-dark"
/>

Просмотреть файл

@@ -4,7 +4,7 @@
import React, {ComponentProps} from 'react';
import {shallow} from 'enzyme';
import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import ChannelNotificationsModal from 'components/channel_notifications_modal/channel_notifications_modal';
@@ -25,6 +25,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
mark_unread: NotificationLevels.ALL,
push: NotificationLevels.DEFAULT,
ignore_channel_mentions: IgnoreChannelMentions.DEFAULT,
channel_auto_follow_threads: ChannelAutoFollowThreads.OFF,
desktop_threads: NotificationLevels.ALL,
push_threads: NotificationLevels.DEFAULT,
},
@@ -62,6 +63,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.ALL);
expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.DEFAULT);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.OFF);
expect(wrapper.state('channelAutoFollowThreads')).toEqual(ChannelAutoFollowThreads.OFF);
});
test('should provide correct default when currentUser channel notify props is true', () => {
@@ -207,7 +209,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.NONE);
wrapper.instance().updateSection('');
wrapper.instance().updateSection(NotificationSections.NONE);
expect(wrapper.state('desktopNotifyLevel')).toEqual(baseProps.channelMember?.notify_props.desktop);
});
@@ -348,6 +350,7 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
expect(wrapper.state('markUnreadNotifyLevel')).toEqual(NotificationLevels.MENTION);
expect(wrapper.state('pushNotifyLevel')).toEqual(NotificationLevels.ALL);
expect(wrapper.state('ignoreChannelMentions')).toEqual(IgnoreChannelMentions.ON);
expect(wrapper.state('channelAutoFollowThreads')).toEqual(ChannelAutoFollowThreads.OFF);
wrapper.instance().resetStateFromNotifyProps(currentUserNotifyProps, {...channelMemberNotifyProps, desktop: NotificationLevels.ALL});
expect(wrapper.state('desktopNotifyLevel')).toEqual(NotificationLevels.ALL);

Просмотреть файл

@@ -8,7 +8,7 @@ import {FormattedMessage} from 'react-intl';
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import NotificationSection from 'components/channel_notifications_modal/components/notification_section.jsx';
@@ -47,6 +47,7 @@ type State = {
pushNotifyLevel: ChannelNotifyProps['push'];
pushThreadsNotifyLevel: UserNotifyProps['push_threads'];
ignoreChannelMentions: ChannelNotifyProps['ignore_channel_mentions'];
channelAutoFollowThreads: ChannelNotifyProps['channel_auto_follow_threads'];
};
export default class ChannelNotificationsModal extends React.PureComponent<Props, State> {
@@ -95,6 +96,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
pushNotifyLevel: channelMemberNotifyProps?.push || NotificationLevels.DEFAULT,
pushThreadsNotifyLevel: channelMemberNotifyProps?.push_threads || NotificationLevels.ALL,
ignoreChannelMentions,
channelAutoFollowThreads: channelMemberNotifyProps?.channel_auto_follow_threads || ChannelAutoFollowThreads.OFF,
};
}
@@ -190,7 +192,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
const {ignoreChannelMentions} = this.state;
if (channelNotifyProps?.ignore_channel_mentions === ignoreChannelMentions) {
this.updateSection('');
this.updateSection(NotificationSections.NONE);
return;
}
@@ -198,6 +200,21 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
this.handleUpdateChannelNotifyProps(props);
};
handleUpdateChannelAutoFollowThreads = (channelAutoFollowThreads: ChannelNotifyProps['channel_auto_follow_threads']) => this.setState({channelAutoFollowThreads});
handleSubmitChannelAutoFollowThreads = () => {
const channelNotifyProps = this.props.channelMember && this.props.channelMember.notify_props;
const {channelAutoFollowThreads} = this.state;
if (channelNotifyProps?.channel_auto_follow_threads === channelAutoFollowThreads) {
this.updateSection(NotificationSections.NONE);
return;
}
const props = {channel_auto_follow_threads: channelAutoFollowThreads};
this.handleUpdateChannelNotifyProps(props);
};
render() {
const {
activeSection,
@@ -207,6 +224,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
pushNotifyLevel,
pushThreadsNotifyLevel,
ignoreChannelMentions,
channelAutoFollowThreads,
serverError,
} = this.state;
@@ -301,6 +319,18 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
}
</div>
}
<div className='divider-light'/>
<NotificationSection
section={NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
expand={activeSection === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
memberNotificationLevel={markUnreadNotifyLevel}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
onChange={this.handleUpdateChannelAutoFollowThreads}
onSubmit={this.handleSubmitChannelAutoFollowThreads}
onUpdateSection={this.updateSection}
serverError={serverError}
/>
<div className='divider-dark'/>
</div>
</div>

Просмотреть файл

@@ -10,13 +10,14 @@ import SectionTitle from './section_title';
type Props = {
ignoreChannelMentions?: string;
channelAutoFollowThreads?: string;
onExpandSection: (section: string) => void;
globalNotifyLevel?: string;
memberNotifyLevel: string;
section: string;
}
export default function CollapseView({onExpandSection, globalNotifyLevel, memberNotifyLevel, section, ignoreChannelMentions}: Props) {
export default function CollapseView({onExpandSection, globalNotifyLevel, memberNotifyLevel, section, ignoreChannelMentions, channelAutoFollowThreads}: Props) {
return (
<SettingItemMin
title={<SectionTitle section={section}/>}
@@ -24,6 +25,7 @@ export default function CollapseView({onExpandSection, globalNotifyLevel, member
<Describe
section={section}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
isCollapsed={true}

Просмотреть файл

@@ -4,18 +4,19 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {t} from 'utils/i18n';
type Props = {
globalNotifyLevel?: string;
ignoreChannelMentions?: string;
channelAutoFollowThreads?: string;
memberNotifyLevel: string;
section: string;
isCollapsed?: boolean;
}
export default function Describe({section, isCollapsed, memberNotifyLevel, globalNotifyLevel, ignoreChannelMentions}: Props) {
export default function Describe({section, isCollapsed, memberNotifyLevel, globalNotifyLevel, ignoreChannelMentions, channelAutoFollowThreads}: Props) {
if (memberNotifyLevel === NotificationLevels.DEFAULT && globalNotifyLevel) {
t('channel_notifications.levels.default');
t('channel_notifications.levels.all');
@@ -70,6 +71,26 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa
defaultMessage='Off'
/>
);
} else if (
section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
channelAutoFollowThreads === ChannelAutoFollowThreads.ON
) {
return (
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads.on.title'
defaultMessage='On'
/>
);
} else if (
section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
channelAutoFollowThreads === ChannelAutoFollowThreads.OFF
) {
return (
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads.off.title'
defaultMessage='Off'
/>
);
} else if (memberNotifyLevel === NotificationLevels.MENTION) {
return (
<FormattedMessage

Просмотреть файл

@@ -7,7 +7,7 @@ import {useSelector} from 'react-redux';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import {ChannelAutoFollowThreads, IgnoreChannelMentions, NotificationLevels, NotificationSections} from 'utils/constants';
import SettingItemMax from 'components/setting_item_max';
@@ -17,6 +17,7 @@ import SectionTitle from './section_title';
type Props = {
ignoreChannelMentions?: string;
channelAutoFollowThreads?: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeThreads?: (e: ChangeEvent<HTMLInputElement>) => void;
onCollapseSection: (section: string) => void;
@@ -39,6 +40,7 @@ export default function ExpandView({
serverError,
onCollapseSection,
ignoreChannelMentions,
channelAutoFollowThreads,
}: Props) {
const isCRTEnabled = useSelector(isCollapsedThreadsEnabled);
@@ -153,6 +155,46 @@ export default function ExpandView({
</div>
</fieldset>
}
{section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS &&
<fieldset>
<div className='radio'>
<label>
<input
id='channelAutoFollowThreadsOn'
name='channelAutoFollowThreads'
type='radio'
value={ChannelAutoFollowThreads.ON}
checked={channelAutoFollowThreads === ChannelAutoFollowThreads.ON}
onChange={onChange}
/>
<Describe
section={section}
channelAutoFollowThreads={ChannelAutoFollowThreads.ON}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
<div className='radio'>
<label>
<input
id='channelAutoFollowThreadsOff'
name='channelAutoFollowThreads'
type='radio'
value={ChannelAutoFollowThreads.OFF}
checked={channelAutoFollowThreads === ChannelAutoFollowThreads.OFF}
onChange={onChange}
/>
<Describe
section={section}
channelAutoFollowThreads={ChannelAutoFollowThreads.OFF}
memberNotifyLevel={memberNotifyLevel}
globalNotifyLevel={globalNotifyLevel}
/>
</label>
</div>
</fieldset>
}
{section === NotificationSections.MARK_UNREAD &&
<fieldset>
<div className='radio'>

Просмотреть файл

@@ -48,6 +48,15 @@ export default function ExtraInfo({section}: Props) {
/>
</span>
);
case NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS:
return (
<span>
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads.help'
defaultMessage='When enabled, you will auto-follow all new threads created in this channel unless you unfollow a thread explicitly.'
/>
</span>
);
default:
return null;
}

Просмотреть файл

@@ -37,6 +37,11 @@ export default class NotificationSection extends React.PureComponent {
*/
ignoreChannelMentions: PropTypes.string,
/**
* Auto-follow all new threads in this channel
*/
channelAutoFollowThreads: PropTypes.string,
/**
* User's global notification level
*/
@@ -93,6 +98,7 @@ export default class NotificationSection extends React.PureComponent {
memberNotificationLevel,
memberThreadsNotificationLevel,
ignoreChannelMentions,
channelAutoFollowThreads,
onSubmit,
section,
serverError,
@@ -106,6 +112,7 @@ export default class NotificationSection extends React.PureComponent {
memberThreadsNotifyLevel={memberThreadsNotificationLevel}
globalNotifyLevel={globalNotificationLevel}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
onChange={this.handleOnChange}
onChangeThreads={this.handleOnChangeThreads}
onSubmit={onSubmit}
@@ -122,6 +129,7 @@ export default class NotificationSection extends React.PureComponent {
memberNotifyLevel={memberNotificationLevel}
globalNotifyLevel={globalNotificationLevel}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
/>
);
}

Просмотреть файл

@@ -39,6 +39,13 @@ export default function SectionTitle({section}: Props) {
defaultMessage='Ignore mentions for @channel, @here and @all'
/>
);
} else if (section === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS) {
return (
<FormattedMessage
id='channel_notifications.channelAutoFollowThreads'
defaultMessage='Auto-follow all new threads in this channel'
/>
);
}
return null;

Просмотреть файл

@@ -2978,6 +2978,10 @@
"channel_modal.type.public.description": "Anyone can join",
"channel_modal.type.public.title": "Public Channel",
"channel_notifications.allActivity": "For all activity",
"channel_notifications.channelAutoFollowThreads": "Auto-follow all new threads in this channel",
"channel_notifications.channelAutoFollowThreads.help": "When enabled, you will auto-follow all new threads created in this channel unless you unfollow a thread explicitly.",
"channel_notifications.channelAutoFollowThreads.off.title": "Off",
"channel_notifications.channelAutoFollowThreads.on.title": "On",
"channel_notifications.globalDefault": "Global default ({notifyLevel})",
"channel_notifications.ignoreChannelMentions": "Ignore mentions for @channel, @here and @all",
"channel_notifications.ignoreChannelMentions.help": "When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel.",

Просмотреть файл

@@ -453,6 +453,7 @@ class TestHelper {
mark_unread: 'mention',
push: 'default',
ignore_channel_mentions: 'default',
channel_auto_follow_threads: 'off',
...override,
};
};

Просмотреть файл

@@ -1009,8 +1009,14 @@ export const IgnoreChannelMentions = {
DEFAULT: 'default',
} as const;
export const ChannelAutoFollowThreads = {
ON: 'on',
OFF: 'off',
} as const;
export const NotificationSections = {
IGNORE_CHANNEL_MENTIONS: 'ignoreChannelMentions',
CHANNEL_AUTO_FOLLOW_THREADS: 'channelAutoFollowThreads',
MARK_UNREAD: 'markUnread',
DESKTOP: 'desktop',
PUSH: 'push',

Просмотреть файл

@@ -155,6 +155,7 @@ export class TestHelper {
mark_unread: 'all',
push: 'default',
ignore_channel_mentions: 'default',
channel_auto_follow_threads: 'off',
};
const notifyProps = Object.assign({}, defaultNotifyProps, overrideNotifyProps);

Просмотреть файл

@@ -25,6 +25,7 @@ export type ChannelNotifyProps = {
mark_unread: 'all' | 'mention';
push: 'default' | 'all' | 'mention' | 'none';
ignore_channel_mentions: 'default' | 'off' | 'on';
channel_auto_follow_threads: 'off' | 'on';
};
export type Channel = {