span {
- margin-left: 4px;
- font-size: 11px;
- font-weight: 600;
- }
- }
- }
-
.file-preview__container {
height: auto;
flex-wrap: wrap;
diff --git a/webapp/channels/src/components/drafts/panel/panel_body.test.tsx b/webapp/channels/src/components/drafts/panel/panel_body.test.tsx
index c40a87781f..007362e0e9 100644
--- a/webapp/channels/src/components/drafts/panel/panel_body.test.tsx
+++ b/webapp/channels/src/components/drafts/panel/panel_body.test.tsx
@@ -112,7 +112,7 @@ describe('components/drafts/panel/panel_body', () => {
{...baseProps}
priority={{
priority: PostPriority.IMPORTANT,
- requested_ack: true,
+ requested_ack: false,
}}
/>
,
diff --git a/webapp/channels/src/components/drafts/panel/panel_body.tsx b/webapp/channels/src/components/drafts/panel/panel_body.tsx
index 5a0e7164a4..e112b2a589 100644
--- a/webapp/channels/src/components/drafts/panel/panel_body.tsx
+++ b/webapp/channels/src/components/drafts/panel/panel_body.tsx
@@ -3,16 +3,13 @@
import React, {useCallback} from 'react';
import {useSelector} from 'react-redux';
-import {FormattedMessage} from 'react-intl';
-
-import {CheckCircleOutlineIcon} from '@mattermost/compass-icons/components';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import Markdown from 'components/markdown';
import FilePreview from 'components/file_preview';
import ProfilePicture from 'components/profile_picture';
-import PriorityLabel from 'components/post_priority/post_priority_label';
+import PriorityLabels from 'components/advanced_create_post/priority_labels';
import {imageURLForUser, handleFormattedTextClick} from 'utils/utils';
import type {PostDraft} from 'types/store/draft';
@@ -77,25 +74,14 @@ function PanelBody({
{displayName}
{priority && (
-
- {priority.priority && (
-
- )}
- {priority.requested_ack && (
-
-
- {!priority.priority && (
-
- )}
-
- )}
-
+
)}
diff --git a/webapp/channels/src/components/persist_notification_confirm_modal.tsx b/webapp/channels/src/components/persist_notification_confirm_modal.tsx
new file mode 100644
index 0000000000..5dc8fcbb6b
--- /dev/null
+++ b/webapp/channels/src/components/persist_notification_confirm_modal.tsx
@@ -0,0 +1,165 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {memo, useMemo} from 'react';
+import {FormattedMessage} from 'react-intl';
+import {useSelector} from 'react-redux';
+
+import {getPersistentNotificationIntervalMinutes, getPersistentNotificationMaxRecipients} from 'mattermost-redux/selectors/entities/posts';
+
+import {GlobalState} from 'types/store';
+import {makeGetUserOrGroupMentionCountFromMessage} from 'utils/post_utils';
+import Constants from 'utils/constants';
+
+import GenericModal from 'components/generic_modal';
+import {UserProfile} from '@mattermost/types/users';
+import {Channel} from '@mattermost/types/channels';
+
+import {HasNoMentions, HasSpecialMentions} from './post_priority/error_messages';
+
+type Props = {
+ currentChannelTeammateUsername?: UserProfile['username'];
+ specialMentions: {[key: string]: boolean};
+ channelType: Channel['type'];
+ message: string;
+ onConfirm: () => void;
+ onExited: () => void;
+};
+
+function PersistNotificationConfirmModal({
+ channelType,
+ currentChannelTeammateUsername,
+ specialMentions,
+ message,
+ onConfirm,
+ onExited,
+}: Props) {
+ let body: React.ReactNode = '';
+ let title: React.ReactNode = '';
+ let confirmBtn: React.ReactNode = '';
+ let handleConfirm = () => {};
+
+ const getMentionCount = useMemo(makeGetUserOrGroupMentionCountFromMessage, []);
+ const maxRecipients = useSelector(getPersistentNotificationMaxRecipients);
+ const interval = useSelector(getPersistentNotificationIntervalMinutes);
+ const count = useSelector((state: GlobalState) => getMentionCount(state, message));
+
+ if (channelType === Constants.DM_CHANNEL) {
+ handleConfirm = onConfirm;
+ title = (
+
+ );
+ body = (
+
{chunks},
+ }}
+ />
+ );
+ confirmBtn = (
+
+ );
+ } else if (Object.values(specialMentions).includes(true)) {
+ body = (
+
+ );
+ confirmBtn = (
+
+ );
+ } else if (count === 0) {
+ title = ;
+ body = (
+
+ );
+ confirmBtn = (
+
+ );
+ } else if (count > Number(maxRecipients)) {
+ title = (
+
+ );
+ body = (
+ {chunks},
+ }}
+ />
+ );
+ confirmBtn = (
+
+ );
+ } else {
+ handleConfirm = onConfirm;
+ title = (
+
+ );
+ body = (
+
+ );
+ confirmBtn = (
+
+ );
+ }
+
+ return (
+ {}}
+ handleConfirm={handleConfirm}
+ handleEnterKeyPress={handleConfirm}
+ isDeleteModal={false}
+ modalHeaderText={title}
+ onExited={onExited}
+ >
+ {body}
+
+ );
+}
+
+export default memo(PersistNotificationConfirmModal);
diff --git a/webapp/channels/src/components/post_priority/error_messages.tsx b/webapp/channels/src/components/post_priority/error_messages.tsx
new file mode 100644
index 0000000000..48f15aabd0
--- /dev/null
+++ b/webapp/channels/src/components/post_priority/error_messages.tsx
@@ -0,0 +1,44 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useMemo} from 'react';
+import {FormattedMessage, FormattedList} from 'react-intl';
+
+export function HasSpecialMentions({specialMentions}: {specialMentions: {[key: string]: boolean}}) {
+ const mentions = useMemo(() => {
+ return Object.keys(specialMentions).
+ filter((key) => specialMentions[key]).
+ map((key) => `@${key}`);
+
+ /* eslint-disable react-hooks/exhaustive-deps */
+ }, [
+ specialMentions.all,
+ specialMentions.here,
+ specialMentions.channel,
+ ]);
+ /* eslint-enable react-hooks/exhaustive-deps */
+
+ return (
+
+ ),
+ }}
+ />
+ );
+}
+
+export function HasNoMentions() {
+ return (
+
+ );
+}
diff --git a/webapp/channels/src/components/post_priority/post_priority_badge.tsx b/webapp/channels/src/components/post_priority/post_priority_badge.tsx
index 907e8cc618..a534025a4f 100644
--- a/webapp/channels/src/components/post_priority/post_priority_badge.tsx
+++ b/webapp/channels/src/components/post_priority/post_priority_badge.tsx
@@ -19,8 +19,8 @@ const Badge = styled.span`
justify-content: center;
height: 20px;
width: 20px;
+ margin-left: 8px;
min-width: 20px;
- margin-right: 10px;
border-radius: 10px;
color: #fff;
diff --git a/webapp/channels/src/components/post_priority/post_priority_picker.tsx b/webapp/channels/src/components/post_priority/post_priority_picker.tsx
index 6f8ca90d30..4ab5233098 100644
--- a/webapp/channels/src/components/post_priority/post_priority_picker.tsx
+++ b/webapp/channels/src/components/post_priority/post_priority_picker.tsx
@@ -1,14 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {useCallback, useEffect, useRef, useState, memo} from 'react';
+import React, {useCallback, useState, memo} from 'react';
import {useSelector} from 'react-redux';
import {FormattedMessage, useIntl} from 'react-intl';
import styled from 'styled-components';
-import {AlertOutlineIcon, AlertCircleOutlineIcon, MessageTextOutlineIcon, CheckCircleOutlineIcon} from '@mattermost/compass-icons/components';
+import {AlertOutlineIcon, AlertCircleOutlineIcon, MessageTextOutlineIcon, CheckCircleOutlineIcon, BellRingOutlineIcon} from '@mattermost/compass-icons/components';
-import {isPostAcknowledgementsEnabled} from 'mattermost-redux/selectors/entities/posts';
+import {getPersistentNotificationIntervalMinutes, isPersistentNotificationsEnabled, isPostAcknowledgementsEnabled} from 'mattermost-redux/selectors/entities/posts';
import BetaTag from '../widgets/tag/beta_tag';
@@ -21,11 +21,6 @@ type Props = {
settings?: PostPriorityMetadata;
onClose: () => void;
onApply: (props: PostPriorityMetadata) => void;
- placement: string;
- rightOffset?: number;
- topOffset?: number;
- leftOffset?: number;
- style?: React.CSSProperties;
}
const UrgentIcon = styled(AlertOutlineIcon)`
@@ -44,6 +39,10 @@ const AcknowledgementIcon = styled(CheckCircleOutlineIcon)`
fill: rgba(var(--center-channel-color-rgb), 0.56);
`;
+const PersistentNotificationsIcon = styled(BellRingOutlineIcon)`
+ fill: rgba(var(--center-channel-color-rgb), 0.56);
+`;
+
const Header = styled.h4`
align-items: center;
display: flex;
@@ -73,53 +72,50 @@ const Footer = styled.div`
`;
const Picker = styled.div`
- position: absolute;
- z-index: 1100;
- display: flex;
- flex-direction: column;
- border: solid 1px rgba(var(--center-channel-color-rgb), 0.16);
- margin-right: 3px;
+ *zoom: 1;
background: var(--center-channel-bg);
border-radius: 4px;
+ border: solid 1px rgba(var(--center-channel-color-rgb), 0.16);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
- user-select: none;
+ display: flex;
+ flex-direction: column;
+ left: 0;
+ margin-right: 3px;
+ min-width: 0;
overflow: hidden;
- *zoom: 1;
+ user-select: none;
+ width: max-content;
`;
function PostPriorityPicker({
- leftOffset = 0,
onApply,
onClose,
- placement,
- rightOffset = 0,
settings,
- style,
- topOffset = 0,
}: Props) {
const {formatMessage} = useIntl();
const [priority, setPriority] = useState(settings?.priority || '');
const [requestedAck, setRequestedAck] = useState(settings?.requested_ack || false);
-
- const ref = useRef(null);
-
- useEffect(() => {
- ref.current?.focus();
- }, []);
+ const [persistentNotifications, setPersistentNotifications] = useState(settings?.persistent_notifications || false);
const postAcknowledgementsEnabled = useSelector(isPostAcknowledgementsEnabled);
+ const persistentNotificationsEnabled = useSelector(isPersistentNotificationsEnabled) && postAcknowledgementsEnabled;
+ const interval = useSelector(getPersistentNotificationIntervalMinutes);
+
+ const makeOnSelectPriority = useCallback((type?: PostPriority) => (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
- const makeOnSelectPriority = useCallback((type?: PostPriority) => () => {
setPriority(type || '');
if (!postAcknowledgementsEnabled) {
onApply({
priority: type || '',
requested_ack: false,
+ persistent_notifications: false,
});
onClose();
- } else if (type === PostPriority.URGENT) {
- setRequestedAck(true);
+ } else if (type !== PostPriority.URGENT) {
+ setPersistentNotifications(false);
}
}, [onApply, onClose, postAcknowledgementsEnabled]);
@@ -127,43 +123,23 @@ function PostPriorityPicker({
setRequestedAck(!requestedAck);
}, [requestedAck]);
+ const handlePersistentNotifications = useCallback(() => {
+ setPersistentNotifications(!persistentNotifications);
+ }, [persistentNotifications]);
+
const handleApply = () => {
onApply({
priority,
requested_ack: requestedAck,
+ persistent_notifications: persistentNotifications,
});
onClose();
};
- let pickerStyle: React.CSSProperties = {};
- if (style && !(style.left === 0 && style.top === 0)) {
- if (placement === 'top' || placement === 'bottom') {
- // Only take the top/bottom position passed by React Bootstrap since we want to be left-aligned
- pickerStyle = {
- top: style.top,
- bottom: style.bottom,
- left: leftOffset,
- };
- } else {
- pickerStyle = {...style};
- }
-
- pickerStyle.top = pickerStyle.top ? Number(pickerStyle.top) + topOffset : topOffset;
-
- if (pickerStyle.right) {
- pickerStyle.right = Number(pickerStyle.right) + rightOffset;
- }
- }
-
const feedbackLink = postAcknowledgementsEnabled ? 'https://forms.gle/noA8Azg7RdaBZtMB6' : 'https://forms.gle/mMcRFQzyKAo9Sv49A';
return (
-
+
{formatMessage({
id: 'post_priority.picker.header',
@@ -215,22 +191,44 @@ function PostPriorityPicker({
})}
/>
- {postAcknowledgementsEnabled && (
+ {(postAcknowledgementsEnabled || persistentNotificationsEnabled) && (
- }
- text={formatMessage({
- id: 'post_priority.requested_ack.text',
- defaultMessage: 'Request acknowledgement',
- })}
- description={formatMessage({
- id: 'post_priority.requested_ack.description',
- defaultMessage: 'An acknowledgement button will appear with your message',
- })}
- />
+ {postAcknowledgementsEnabled && (
+ }
+ text={formatMessage({
+ id: 'post_priority.requested_ack.text',
+ defaultMessage: 'Request acknowledgement',
+ })}
+ description={formatMessage({
+ id: 'post_priority.requested_ack.description',
+ defaultMessage: 'An acknowledgement button will appear with your message',
+ })}
+ />
+ )}
+ {priority === PostPriority.URGENT && persistentNotificationsEnabled && (
+ }
+ text={formatMessage({
+ id: 'post_priority.persistent_notifications.text',
+ defaultMessage: 'Send persistent notifications',
+ })}
+ description={formatMessage(
+ {
+ id: 'post_priority.persistent_notifications.description',
+ defaultMessage: 'Recipients will be notified every {interval, plural, one {1 minute} other {{interval} minutes}} until they acknowledge or reply',
+ }, {
+ interval,
+ },
+ )}
+ />
+ )}
)}
diff --git a/webapp/channels/src/components/post_priority/post_priority_picker_item.tsx b/webapp/channels/src/components/post_priority/post_priority_picker_item.tsx
index 4ca58b83c2..df4dd4da8d 100644
--- a/webapp/channels/src/components/post_priority/post_priority_picker_item.tsx
+++ b/webapp/channels/src/components/post_priority/post_priority_picker_item.tsx
@@ -33,7 +33,7 @@ const ItemButton = styled.button`
`;
const Wrapper = styled.div`
- cursor: pointer;
+ cursor: ${(props) => (props.disabled ? 'default' : 'pointer')};
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.1);
@@ -113,7 +113,8 @@ function ToggleItem({
}: ToggleProps) {
return (
diff --git a/webapp/channels/src/components/post_priority/post_priority_picker_overlay.tsx b/webapp/channels/src/components/post_priority/post_priority_picker_overlay.tsx
index 9aec58b1e6..0866ca9838 100644
--- a/webapp/channels/src/components/post_priority/post_priority_picker_overlay.tsx
+++ b/webapp/channels/src/components/post_priority/post_priority_picker_overlay.tsx
@@ -1,56 +1,147 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {memo} from 'react';
-import {Overlay} from 'react-bootstrap';
-import memoize from 'memoize-one';
+import React, {memo, useCallback, useState} from 'react';
+import {FormattedMessage} from 'react-intl';
+import classNames from 'classnames';
+import {
+ FloatingFocusManager,
+ FloatingPortal,
+ autoUpdate,
+ offset,
+ useClick,
+ useDismiss,
+ useFloating,
+ useInteractions,
+ useRole,
+ flip,
+ shift,
+} from '@floating-ui/react-dom-interactions';
+
+import {AlertCircleOutlineIcon} from '@mattermost/compass-icons/components';
+
+import {IconContainer} from 'components/advanced_text_editor/formatting_bar/formatting_icon';
+import useTooltip from 'components/common/hooks/useTooltip';
import {PostPriorityMetadata} from '@mattermost/types/posts';
import PostPriorityPicker from './post_priority_picker';
type Props = {
- show: boolean;
+ disabled: boolean;
settings?: PostPriorityMetadata;
- target: () => React.RefObject | React.ReactInstance | null;
onApply: (props: PostPriorityMetadata) => void;
- onHide: () => void;
- defaultHorizontalPosition: 'left'|'right';
+ onClose: () => void;
};
function PostPriorityPickerOverlay({
- show,
+ disabled,
settings,
- target,
onApply,
- onHide,
+ onClose,
}: Props) {
- const pickerPosition = memoize((trigger, show) => {
- if (show && trigger) {
- return trigger.getBoundingClientRect().left;
- }
- return 0;
+ const [pickerOpen, setPickerOpen] = useState(false);
+
+ const {
+ reference: tooltipRef,
+ getReferenceProps: getTooltipReferenceProps,
+ tooltip,
+ } = useTooltip({
+ placement: 'top',
+ message: (
+
+ ),
});
- const offset = pickerPosition(target(), show);
+
+ const handleClose = useCallback(() => {
+ setPickerOpen(false);
+ onClose();
+ }, [onClose]);
+
+ const {
+ x: pickerX,
+ y: pickerY,
+ reference: pickerRef,
+ floating: pickerFloating,
+ strategy: pickerStrategy,
+ context: pickerContext,
+ } = useFloating({
+ open: pickerOpen,
+ onOpenChange: setPickerOpen,
+ placement: 'top-start',
+ whileElementsMounted: autoUpdate,
+ middleware: [
+ offset({mainAxis: 4}),
+ flip({
+ fallbackPlacements: ['top'],
+ }),
+ shift({
+ padding: 16,
+ }),
+ ],
+ });
+
+ const {
+ getFloatingProps: getPickerFloatingProps,
+ getReferenceProps: getPickerReferenceProps,
+ } = useInteractions([
+ useClick(pickerContext),
+ useDismiss(pickerContext),
+ useRole(pickerContext),
+ ]);
return (
-
-
-
+ <>
+
+
+ {pickerOpen && (
+
+
+
+ )}
+
+ {!pickerOpen && tooltip}
+ >
);
}
diff --git a/webapp/channels/src/components/post_view/acknowledgements/post_acknowledgements.scss b/webapp/channels/src/components/post_view/acknowledgements/post_acknowledgements.scss
index 2c00fed89f..411104a86f 100644
--- a/webapp/channels/src/components/post_view/acknowledgements/post_acknowledgements.scss
+++ b/webapp/channels/src/components/post_view/acknowledgements/post_acknowledgements.scss
@@ -30,7 +30,7 @@
&--disabled,
&:disabled {
background: rgba(var(--online-indicator-rgb), 0.08);
- cursor: not-allowed;
+ cursor: default;
}
&:hover:enabled {
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index e23b704b5b..203a07f67f 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -1913,6 +1913,17 @@
"admin.plugins.settings.marketplaceUrlDesc.empty": " Marketplace URL is a required field.",
"admin.plugins.settings.requirePluginSignature": "Require Plugin Signature:",
"admin.plugins.settings.requirePluginSignatureDesc": "When true, uploading plugins is disabled and may only be installed through the Marketplace. Plugins are always verified during Mattermost server startup and initialization. See documentation to learn more.",
+ "admin.posts.persistentNotifications.desc": "When enabled, users can trigger repeating notifications for the recipients of urgent messages. Learn more about message priority and persistent notifications in our documentation.",
+ "admin.posts.persistentNotifications.title": "Persistent Notifications",
+ "admin.posts.persistentNotificationsGuests.desc": "Whether a guest is able to require persistent notifications. Learn more about message priority and persistent notifications in our documentation.",
+ "admin.posts.persistentNotificationsGuests.title": "Allow guests to send persistent notifications",
+ "admin.posts.persistentNotificationsInterval.desc": "Configure the number of minutes between repeated notifications for urgent messages send with persistent notifications. Learn more about message priority and persistent notifications in our documentation.",
+ "admin.posts.persistentNotificationsInterval.minValue": "Frequency must be at least two minutes",
+ "admin.posts.persistentNotificationsInterval.title": "Frequency of persistent notifications",
+ "admin.posts.persistentNotificationsMaxCount.desc": "Configure the maximum number of times users may receive persistent notifications. Learn more about message priority and persistent notifications in our documentation.",
+ "admin.posts.persistentNotificationsMaxCount.title": "Total number of persistent notification per post",
+ "admin.posts.persistentNotificationsMaxRecipients.desc": "Configure the maximum number of recipients to which users may send persistent notifications. Learn more about message priority and persistent notifications in our documentation.",
+ "admin.posts.persistentNotificationsMaxRecipients.title": "Maximum number of recipients for persistent notifications",
"admin.posts.postPriority.desc": "When enabled, users can configure a visual indicator to communicate messages that are important or urgent. Learn more about message priority in our documentation.",
"admin.posts.postPriority.title": "Message Priority",
"admin.privacy.showEmailDescription": "When false, hides the email address of members from everyone except System Administrators.",
@@ -4331,6 +4342,18 @@
"permalink.show_dialog_warn.description": "You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?",
"permalink.show_dialog_warn.join": "Join",
"permalink.show_dialog_warn.title": "Join private channel",
+ "persist_notification.confirm": "Send",
+ "persist_notification.confirm.description": "Mentioned recipients will be notified every {interval, plural, one {1 minute} other {{interval} minutes}} until they’ve acknowledged the message.",
+ "persist_notification.confirm.title": "Send persistent notifications?",
+ "persist_notification.dm_or_gm": "Send",
+ "persist_notification.dm_or_gm.description": "{username} will be notified every {interval, plural, one {1 minute} other {{interval} minutes}} until they’ve acknowledged the message.",
+ "persist_notification.dm_or_gm.title": "Send persistent notifications?",
+ "persist_notification.special_mentions.confirm": "Got it",
+ "persist_notification.too_few.confirm": "Got it",
+ "persist_notification.too_few.description": "There are no recipients mentioned in your message. You’ll need add mentions to be able to send persistent notifications.",
+ "persist_notification.too_many.confirm": "Got it",
+ "persist_notification.too_many.description": "You can send persistent notifications to a maximum of {max} recipients. There are {count} recipients mentioned in your message. You’ll need to change who you’ve mentioned before you can send.",
+ "persist_notification.too_many.title": "Too many recipients",
"picture_selector.image.ariaLabel": "Picture selector image",
"picture_selector.remove_picture": "Remove picture",
"picture_selector.select_button.ariaLabel": "Select picture",
@@ -4408,6 +4431,11 @@
"post_pre_header.pinned": "Pinned",
"post_priority.acknowledgements.title": "Acknowledgements",
"post_priority.button.acknowledge": "Acknowledge",
+ "post_priority.error.no_mentions": "Recipients must be @mentioned",
+ "post_priority.error.special_mentions": "{mention} can’t be used with persistent notifications",
+ "post_priority.persistent_notifications.description": "Recipients will be notified every {interval, plural, one {1 minute} other {{interval} minutes}} until they acknowledge or reply",
+ "post_priority.persistent_notifications.text": "Send persistent notifications",
+ "post_priority.persistent_notifications.tooltip": "Persistent notifications will be sent",
"post_priority.picker.apply": "Apply",
"post_priority.picker.cancel": "Cancel",
"post_priority.picker.feedback": "Give feedback",
diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/posts.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/posts.ts
index 5c5f07a315..897177a504 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/posts.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/posts.ts
@@ -24,6 +24,7 @@ import {
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
import {shouldShowJoinLeaveMessages} from 'mattermost-redux/utils/post_list';
+import {isGuest} from 'mattermost-redux/utils/user_utils';
import {Channel} from '@mattermost/types/channels';
import {
@@ -775,10 +776,40 @@ export function isPostAcknowledgementsEnabled(state: GlobalState) {
);
}
+export function getAllowPersistentNotifications(state: GlobalState) {
+ return (
+ isPostPriorityEnabled(state) &&
+ getConfig(state).AllowPersistentNotifications === 'true'
+ );
+}
+
+export function getPersistentNotificationMaxRecipients(state: GlobalState) {
+ return getConfig(state).PersistentNotificationMaxRecipients;
+}
+
+export function getPersistentNotificationIntervalMinutes(state: GlobalState) {
+ return getConfig(state).PersistentNotificationIntervalMinutes;
+}
+
+export function getAllowPersistentNotificationsForGuests(state: GlobalState) {
+ return (
+ isPostPriorityEnabled(state) &&
+ getConfig(state).AllowPersistentNotificationsForGuests === 'true'
+ );
+}
+
export function getPostAcknowledgements(state: GlobalState, postId: Post['id']): Record {
return state.entities.posts.acknowledgements[postId];
}
+export const isPersistentNotificationsEnabled = createSelector(
+ 'getPersistentNotificationsEnabled',
+ getCurrentUser,
+ getAllowPersistentNotifications,
+ getAllowPersistentNotificationsForGuests,
+ (user, forAll, forGuests) => (isGuest(user.roles) ? (forAll && forGuests) : forAll),
+);
+
export function makeGetPostAcknowledgementsWithProfiles(): (state: GlobalState, postId: Post['id']) => Array<{user: UserProfile; acknowledgedAt: PostAcknowledgement['acknowledged_at']}> {
return createSelector(
'makeGetPostAcknowledgementsWithProfiles',
diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx
index 37deaf2a0f..7c8a60340a 100644
--- a/webapp/channels/src/utils/constants.tsx
+++ b/webapp/channels/src/utils/constants.tsx
@@ -448,6 +448,7 @@ export const ModalIdentifiers = {
MARK_ALL_THREADS_AS_READ: 'mark_all_threads_as_read_modal',
DELINQUENCY_MODAL_DOWNGRADE: 'delinquency_modal_downgrade',
CLOUD_LIMITS_DOWNGRADE: 'cloud_limits_downgrade',
+ PERSIST_NOTIFICATION_CONFIRM_MODAL: 'persist_notification_confirm_modal',
AIR_GAPPED_SELF_HOSTED_PURCHASE: 'air_gapped_self_hosted_purchase',
WORK_TEMPLATE: 'work_template',
DOWNGRADE_MODAL: 'downgrade_modal',
@@ -651,6 +652,7 @@ export const SocketEvents = {
DRAFT_CREATED: 'draft_created',
DRAFT_UPDATED: 'draft_updated',
DRAFT_DELETED: 'draft_deleted',
+ PERSISTENT_NOTIFICATION_TRIGGERED: 'persistent_notification_triggered',
HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED: 'hosted_customer_signup_progress_updated',
};
diff --git a/webapp/channels/src/utils/post_utils.ts b/webapp/channels/src/utils/post_utils.ts
index fea73c028d..d7dfb01d5d 100644
--- a/webapp/channels/src/utils/post_utils.ts
+++ b/webapp/channels/src/utils/post_utils.ts
@@ -20,6 +20,7 @@ import {get, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from 'mat
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
import {getCurrentTeamId, getTeam} from 'mattermost-redux/selectors/entities/teams';
import {makeGetDisplayName, getCurrentUserId, getUser, UserMentionKey, getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
+import {getAllGroupsForReferenceByName} from 'mattermost-redux/selectors/entities/groups';
import {memoizeResult} from 'mattermost-redux/utils/helpers';
@@ -27,7 +28,7 @@ import {Channel} from '@mattermost/types/channels';
import {ClientConfig, ClientLicense} from '@mattermost/types/config';
import {ServerError} from '@mattermost/types/errors';
import {Group} from '@mattermost/types/groups';
-import {Post} from '@mattermost/types/posts';
+import {Post, PostPriority, PostPriorityMetadata} from '@mattermost/types/posts';
import {Reaction} from '@mattermost/types/reactions';
import {UserProfile} from '@mattermost/types/users';
@@ -738,3 +739,41 @@ export function mentionsMinusSpecialMentionsInText(message: string) {
return mentions;
}
+
+function isUserProfile(entity: UserProfile | Group): entity is UserProfile {
+ return (entity as UserProfile).username !== undefined;
+}
+
+export function makeGetUserOrGroupMentionCountFromMessage(): (state: GlobalState, message: Post['message']) => number {
+ return createSelector(
+ 'getUserOrGroupMentionCountFromMessage',
+ (_state: GlobalState, message: Post['message']) => message,
+ getUsersByUsername,
+ getAllGroupsForReferenceByName,
+ (message, users, groups) => {
+ let count = 0;
+ const markdownCleanedText = formatWithRenderer(message, new MentionableRenderer());
+ const mentions = new Set(markdownCleanedText.match(Constants.MENTIONS_REGEX) || []);
+ mentions.forEach((mention) => {
+ const data = {...groups, ...users};
+ const userOrGroup = getUserOrGroupFromMentionName(data, mention.substring(1));
+
+ if (userOrGroup) {
+ if (isUserProfile(userOrGroup)) {
+ count++;
+ } else {
+ count += userOrGroup.member_count;
+ }
+ }
+ });
+ return count;
+ },
+ );
+}
+
+export function hasRequestedPersistentNotifications(priority?: PostPriorityMetadata) {
+ return (
+ priority?.priority === PostPriority.URGENT &&
+ priority?.persistent_notifications
+ );
+}
diff --git a/webapp/platform/components/src/legacy_generic_modal/legacy_generic_modal.tsx b/webapp/platform/components/src/legacy_generic_modal/legacy_generic_modal.tsx
index 640c44c501..eaec42b8a3 100644
--- a/webapp/platform/components/src/legacy_generic_modal/legacy_generic_modal.tsx
+++ b/webapp/platform/components/src/legacy_generic_modal/legacy_generic_modal.tsx
@@ -40,6 +40,7 @@ export type Props = {
backdropClassName?: string;
tabIndex?: number;
children: React.ReactNode;
+ autoFocusConfirmButton?: boolean;
keyboardEscape?: boolean;
headerInput?: React.ReactNode;
bodyPadding?: boolean;
@@ -130,6 +131,7 @@ export class LegacyGenericModal extends React.PureComponent {
confirmButton = (