diff --git a/webapp/channels/src/components/user_settings/notifications/index.ts b/webapp/channels/src/components/user_settings/notifications/index.ts
index fb1abd8d9e8..b68bbbe8f78 100644
--- a/webapp/channels/src/components/user_settings/notifications/index.ts
+++ b/webapp/channels/src/components/user_settings/notifications/index.ts
@@ -6,7 +6,7 @@ import {connect, type ConnectedProps} from 'react-redux';
import type {PreferencesType} from '@mattermost/types/preferences';
import type {UserProfile} from '@mattermost/types/users';
-import {patchUser, updateMe} from 'mattermost-redux/actions/users';
+import {patchUser, setStatus, updateMe} from 'mattermost-redux/actions/users';
import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {
@@ -57,6 +57,7 @@ const mapStateToProps = (state: GlobalState, props: OwnProps) => {
const mapDispatchToProps = {
updateMe,
patchUser,
+ setStatus,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
diff --git a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx
index 5acca09eea0..94a4d7236d0 100644
--- a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.test.tsx
@@ -8,7 +8,7 @@ import {renderWithContext, screen} from 'tests/react_testing_utils';
import {NotificationLevels} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
-import UserSettingsNotifications, {areDesktopAndMobileSettingsDifferent} from './user_settings_notifications';
+import UserSettingsNotifications, {areDesktopAndMobileSettingsDifferent, shouldSilenceForDndSchedule} from './user_settings_notifications';
jest.mock('components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice', () => () =>
);
jest.mock('components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag', () => () =>
);
@@ -22,6 +22,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
collapseModal: jest.fn(),
updateMe: jest.fn(() => Promise.resolve({})),
patchUser: jest.fn(() => Promise.resolve({})),
+ setStatus: jest.fn(() => Promise.resolve({})),
isCollapsedThreadsEnabled: true,
sendPushNotifications: false,
enableAutoResponder: false,
@@ -91,3 +92,65 @@ describe('areDesktopAndMobileSettingsDifferent', () => {
expect(areDesktopAndMobileSettingsDifferent(NotificationLevels.ALL, NotificationLevels.ALL, NotificationLevels.ALL, undefined as any, true)).toBe(true);
});
});
+
+describe('shouldSilenceForDndSchedule', () => {
+ test('notifies when schedule is deactivated', () => {
+ const shouldSilence = shouldSilenceForDndSchedule({
+ isScheduleEnabled: false,
+ allowNotificationsOnWeekends: false,
+ fromTime: '09:00',
+ toTime: '17:00',
+ now: new Date('2026-04-20T20:00:00'),
+ });
+
+ expect(shouldSilence).toBe(false);
+ });
+
+ test('silences when schedule is activated and current time is outside schedule window', () => {
+ const shouldSilence = shouldSilenceForDndSchedule({
+ isScheduleEnabled: true,
+ allowNotificationsOnWeekends: false,
+ fromTime: '09:00',
+ toTime: '17:00',
+ now: new Date('2026-04-20T20:00:00'),
+ });
+
+ expect(shouldSilence).toBe(true);
+ });
+
+ test('notifies when schedule is activated and current time is inside schedule window', () => {
+ const shouldSilence = shouldSilenceForDndSchedule({
+ isScheduleEnabled: true,
+ allowNotificationsOnWeekends: false,
+ fromTime: '09:00',
+ toTime: '17:00',
+ now: new Date('2026-04-20T10:00:00'),
+ });
+
+ expect(shouldSilence).toBe(false);
+ });
+
+ test('notifies on weekends when weekend notifications are allowed', () => {
+ const shouldSilence = shouldSilenceForDndSchedule({
+ isScheduleEnabled: true,
+ allowNotificationsOnWeekends: true,
+ fromTime: '09:00',
+ toTime: '17:00',
+ now: new Date('2026-04-19T20:00:00'),
+ });
+
+ expect(shouldSilence).toBe(false);
+ });
+
+ test('silences on weekends when weekend notifications are not allowed and time is outside schedule window', () => {
+ const shouldSilence = shouldSilenceForDndSchedule({
+ isScheduleEnabled: true,
+ allowNotificationsOnWeekends: false,
+ fromTime: '09:00',
+ toTime: '17:00',
+ now: new Date('2026-04-19T20:00:00'),
+ });
+
+ expect(shouldSilence).toBe(true);
+ });
+});
diff --git a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx
index 3d667e695d9..1207071168b 100644
--- a/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx
+++ b/webapp/channels/src/components/user_settings/notifications/user_settings_notifications.tsx
@@ -20,6 +20,7 @@ import SettingItemMax from 'components/setting_item_max';
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
import Constants, {NotificationLevels, MattermostFeatures, LicenseSkus, UserSettingsNotificationSections} from 'utils/constants';
+import {toUTCUnixInSeconds} from 'utils/datetime';
import {notificationSoundKeys, stopTryNotificationRing} from 'utils/notification_sounds';
import {a11yFocus} from 'utils/utils';
@@ -36,6 +37,67 @@ import type {OwnProps, PropsFromRedux} from './index';
const WHITE_SPACE_REGEX = /\s+/g;
const COMMA_REGEX = /,/g;
+const DO_NOT_DISTURB_SCHEDULE_SECTION = 'do_not_disturb_schedule';
+const DND_SCHEDULE_ENABLED_KEY = 'dnd_schedule_enabled';
+const DND_SCHEDULE_FROM_TIME_KEY = 'dnd_schedule_from_time';
+const DND_SCHEDULE_TO_TIME_KEY = 'dnd_schedule_to_time';
+const DND_SCHEDULE_ALLOW_WEEKENDS_KEY = 'dnd_schedule_allow_weekends';
+
+export function parseMinutesFromTimeValue(value: string): number {
+ const [hoursString = '0', minutesString = '0'] = value.split(':');
+ const hours = Number.parseInt(hoursString, 10);
+ const minutes = Number.parseInt(minutesString, 10);
+
+ if (Number.isNaN(hours) || Number.isNaN(minutes)) {
+ return 0;
+ }
+
+ return (hours * 60) + minutes;
+}
+
+export function isTimeInsideScheduleWindow(now: Date, fromMinutes: number, toMinutes: number): boolean {
+ const currentMinutes = (now.getHours() * 60) + now.getMinutes();
+
+ if (fromMinutes === toMinutes) {
+ return true;
+ }
+
+ if (fromMinutes < toMinutes) {
+ return currentMinutes >= fromMinutes && currentMinutes < toMinutes;
+ }
+
+ return currentMinutes >= fromMinutes || currentMinutes < toMinutes;
+}
+
+export function shouldSilenceForDndSchedule({
+ isScheduleEnabled,
+ allowNotificationsOnWeekends,
+ fromTime,
+ toTime,
+ now,
+}: {
+ isScheduleEnabled: boolean;
+ allowNotificationsOnWeekends: boolean;
+ fromTime: string;
+ toTime: string;
+ now: Date;
+}): boolean {
+ if (!isScheduleEnabled) {
+ return false;
+ }
+
+ const dayOfWeek = now.getDay();
+ const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
+ if (allowNotificationsOnWeekends && isWeekend) {
+ return false;
+ }
+
+ const fromMinutes = parseMinutesFromTimeValue(fromTime);
+ const toMinutes = parseMinutesFromTimeValue(toTime);
+ const isInsideWindow = isTimeInsideScheduleWindow(now, fromMinutes, toMinutes);
+
+ return !isInsideWindow;
+}
type MultiInputValue = {
label: string;
@@ -67,6 +129,10 @@ type State = {
autoResponderActive: boolean;
autoResponderMessage: UserNotifyProps['auto_responder_message'];
notifyCommentsLevel: UserNotifyProps['comments'];
+ dndScheduleEnabled: boolean;
+ dndScheduleFromTime: string;
+ dndScheduleToTime: string;
+ dndAllowNotificationsOnWeekends: boolean;
isSaving: boolean;
serverError: string;
desktopAndMobileSettingsDifferent: boolean;
@@ -90,6 +156,10 @@ function getDefaultStateFromProps(props: Props): State {
id: 'user.settings.notifications.autoResponderDefault',
defaultMessage: 'Hello, I am out of office and unable to respond to messages.',
});
+ let dndScheduleEnabled = false;
+ let dndScheduleFromTime = '09:00';
+ let dndScheduleToTime = '17:00';
+ let dndAllowNotificationsOnWeekends = false;
let desktopAndMobileSettingsDifferent = true;
if (props.user.notify_props) {
@@ -138,6 +208,13 @@ function getDefaultStateFromProps(props: Props): State {
autoResponderMessage = props.user.notify_props.auto_responder_message;
}
+ const notifyProps = props.user.notify_props as UserNotifyProps & Record
;
+
+ dndScheduleEnabled = notifyProps[DND_SCHEDULE_ENABLED_KEY] === 'true';
+ dndScheduleFromTime = notifyProps[DND_SCHEDULE_FROM_TIME_KEY] || dndScheduleFromTime;
+ dndScheduleToTime = notifyProps[DND_SCHEDULE_TO_TIME_KEY] || dndScheduleToTime;
+ dndAllowNotificationsOnWeekends = notifyProps[DND_SCHEDULE_ALLOW_WEEKENDS_KEY] === 'true';
+
if (props.user.notify_props.desktop && props.user.notify_props.push) {
desktopAndMobileSettingsDifferent = areDesktopAndMobileSettingsDifferent(props.user.notify_props.desktop, props.user.notify_props.push, props.user.notify_props?.desktop_threads, props.user.notify_props?.push_threads, props.isCollapsedThreadsEnabled);
}
@@ -207,6 +284,10 @@ function getDefaultStateFromProps(props: Props): State {
autoResponderActive,
autoResponderMessage,
notifyCommentsLevel: comments,
+ dndScheduleEnabled,
+ dndScheduleFromTime,
+ dndScheduleToTime,
+ dndAllowNotificationsOnWeekends,
isSaving: false,
serverError: '',
desktopAndMobileSettingsDifferent,
@@ -253,6 +334,12 @@ class NotificationsTab extends React.PureComponent {
data.first_name = this.state.firstNameKey ? 'true' : 'false';
data.channel = this.state.channelKey ? 'true' : 'false';
+ const notifyData = data as UserNotifyProps & Record;
+ notifyData[DND_SCHEDULE_ENABLED_KEY] = this.state.dndScheduleEnabled ? 'true' : 'false';
+ notifyData[DND_SCHEDULE_FROM_TIME_KEY] = this.state.dndScheduleFromTime;
+ notifyData[DND_SCHEDULE_TO_TIME_KEY] = this.state.dndScheduleToTime;
+ notifyData[DND_SCHEDULE_ALLOW_WEEKENDS_KEY] = this.state.dndAllowNotificationsOnWeekends ? 'true' : 'false';
+
if (this.state.desktopAndMobileSettingsDifferent) {
data.push = this.state.pushActivity;
data.push_threads = this.state.pushThreads;
@@ -487,6 +574,71 @@ class NotificationsTab extends React.PureComponent {
this.props.closeModal();
};
+ handleChangeForDndScheduleEnabled = async (event: ChangeEvent) => {
+ const {target: {checked}} = event;
+ this.setState({dndScheduleEnabled: checked});
+
+ if (checked) {
+ await this.applyTimedDndFromSchedule();
+ }
+ };
+
+ handleChangeForDndScheduleFromTime = (event: ChangeEvent) => {
+ this.setState({dndScheduleFromTime: event.target.value});
+ };
+
+ handleChangeForDndScheduleToTime = (event: ChangeEvent) => {
+ this.setState({dndScheduleToTime: event.target.value});
+ };
+
+ handleChangeForDndAllowNotificationsOnWeekends = (event: ChangeEvent) => {
+ const {target: {checked}} = event;
+ this.setState({dndAllowNotificationsOnWeekends: checked});
+ };
+
+ getNextDndEndDate = (now: Date, toMinutes: number): Date => {
+ const endDate = new Date(now);
+ endDate.setSeconds(0, 0);
+ endDate.setHours(Math.floor(toMinutes / 60), toMinutes % 60, 0, 0);
+
+ if (endDate <= now) {
+ endDate.setDate(endDate.getDate() + 1);
+ }
+
+ return endDate;
+ };
+
+ applyTimedDndFromSchedule = async () => {
+ const now = new Date();
+ const shouldSilence = shouldSilenceForDndSchedule({
+ isScheduleEnabled: this.state.dndScheduleEnabled,
+ allowNotificationsOnWeekends: this.state.dndAllowNotificationsOnWeekends,
+ fromTime: this.state.dndScheduleFromTime,
+ toTime: this.state.dndScheduleToTime,
+ now,
+ });
+
+ if (!shouldSilence) {
+ return;
+ }
+
+ const fromMinutes = parseMinutesFromTimeValue(this.state.dndScheduleFromTime);
+
+ const endDate = this.getNextDndEndDate(now, fromMinutes);
+
+ const result = await this.props.setStatus({
+ user_id: this.props.user.id,
+ status: Constants.UserStatuses.DND,
+ dnd_end_time: toUTCUnixInSeconds(endDate),
+ manual: true,
+ last_activity_at: toUTCUnixInSeconds(now),
+ });
+
+ if (result.error) {
+ this.setState({serverError: result.error.message});
+ }
+ };
+
createKeywordsWithNotificationSection = () => {
const serverError = this.state.serverError;
const user = this.props.user;
@@ -975,11 +1127,132 @@ class NotificationsTab extends React.PureComponent {
);
};
+ createDoNotDisturbScheduleSection = () => {
+ const isSectionExpanded = this.props.activeSection === DO_NOT_DISTURB_SCHEDULE_SECTION;
+ const serverError = this.state.serverError;
+
+ let max = null;
+ if (isSectionExpanded) {
+ const inputs = (
+
+ );
+
+ max = (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
render() {
const keywordsWithNotificationSection = this.createKeywordsWithNotificationSection();
const keywordsWithHighlightSection = this.createKeywordsWithHighlightSection();
const commentsSection = this.createCommentsSection();
const autoResponderSection = this.createAutoResponderSection();
+ const doNotDisturbScheduleSection = this.createDoNotDisturbScheduleSection();
const areAllSectionsInactive = this.props.activeSection === '';
@@ -1080,6 +1353,8 @@ class NotificationsTab extends React.PureComponent {
threads={this.state.emailThreads || ''}
/>
+ {doNotDisturbScheduleSection}
+
{keywordsWithNotificationSection}
{(!this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady) && (
<>