[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 удалений

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

@@ -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 = {