[MM-47751][MM-48102] MPA: Send Persistent Notifications (#21619)

* MM-46410: adds urgency on mention counts

We have introduced priority for posts in
https://github.com/mattermost/mattermost-webapp/pull/10951.
We do need to color the mention badges in the webapp with a prominent
color when a mention is posted in an urgent message.
A thread has urgent mentions if the root post is marked as urgent, and
the replies contain mentions to the user viewing the thread.

This PR adds two columns, urgentmentioncount, and isurgent, in
channelmembers, and threads tables respectively.
Furthermore when asking for team/thread mention counts, we also return
urgent mention counts for the user.

* Adds PostAcknowledgements table and apis

* job init and fetch mentions

* add-migrations

* delete-expired

* send-notifications

* Fetches post priority in batches

* stop-notifications

* stop-notification-on-reply

* MM-47750: Adds PostAcknowledgements table and apis

- Adds post acknowledgement api/app/store methods to be able to save and
delete post acknowledgements by users.
- Adds wesbsocket events for acknowledgement created/deleted
- Returns post acknowledgements in the post's metadata

* add-license-check

* add-pagination

* delete on channel and team

* validate guests

* add configs

* move create priority post check from app to api

* Add desktop notifications

* check status

* use config in job

* add IsUrgent check

* Add last-sent-at

* validate max recipients

* Update lastSentAt

* Validate min. recipient

* send email notification only once

* remove email notifications

* use latest time from config to run job

* Add notifications counter

* publish events to mentioned users only

* pickup license updates in scheduler

* don't allow post owner to stop notifications

* follow normal notifications behaviour

* Validates persistent notifications interval

* move logic of handling valid and expired posts into sql

* Adds persistent notifications in the webapp

---------

Co-authored-by: koox00 <3829551+koox00@users.noreply.github.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Vishal
2023-05-18 23:44:12 +05:30
коммит произвёл GitHub
родитель ce165302cf
Коммит 9399ce8637
74 изменённых файлов: 4052 добавлений и 888 удалений

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

@@ -105,6 +105,7 @@ import {redirectUserToDefaultTeam} from 'actions/global_actions';
import {handleNewPost} from 'actions/post_actions';
import * as StatusActions from 'actions/status_actions';
import {loadProfilesForSidebar} from 'actions/user_actions';
import {sendDesktopNotification} from 'actions/notification_actions.jsx';
import store from 'stores/redux_store.jsx';
import WebSocketClient from 'client/web_websocket_client.jsx';
import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins';
@@ -580,6 +581,9 @@ export function handleEvent(msg) {
case SocketEvents.DRAFT_DELETED:
dispatch(handleDeleteDraftEvent(msg));
break;
case SocketEvents.PERSISTENT_NOTIFICATION_TRIGGERED:
dispatch(handlePersistentNotification(msg));
break;
case SocketEvents.HOSTED_CUSTOMER_SIGNUP_PROGRESS_UPDATED:
dispatch(handleHostedCustomerSignupProgressUpdated(msg));
break;
@@ -1722,10 +1726,17 @@ function handleDeleteDraftEvent(msg) {
};
}
function handlePersistentNotification(msg) {
return async (doDispatch) => {
const post = JSON.parse(msg.data.post);
doDispatch(sendDesktopNotification(post, msg.data));
};
}
function handleHostedCustomerSignupProgressUpdated(msg) {
return {
type: HostedCustomerTypes.RECEIVED_SELF_HOSTED_SIGNUP_PROGRESS,
data: msg.data.progress,
};
}

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

@@ -224,6 +224,7 @@ export const it = {
export const validators = {
isRequired: (text, textDefault) => (value) => new ValidationResult(Boolean(value), text, textDefault),
minValue: (min, text, textDefault) => (value) => new ValidationResult((value >= min), text, textDefault),
};
const usesLegacyOauth = (config, state, license, enterpriseReady, consoleAccess, cloud) => {
@@ -2820,6 +2821,132 @@ const AdminDefinition = {
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
isHidden: it.configIsFalse('FeatureFlags', 'PostPriority'),
},
{
type: Constants.SettingsTypes.TYPE_BOOL,
key: 'ServiceSettings.AllowPersistentNotifications',
label: t('admin.posts.persistentNotifications.title'),
label_default: 'Persistent Notifications',
help_text: t('admin.posts.persistentNotifications.desc'),
help_text_default: 'When enabled, users can trigger repeating notifications for the recipients of urgent messages. Learn more about message priority and persistent notifications in our <link>documentation</link>.',
help_text_values: {
link: (msg) => (
<ExternalLink
location='admin_console'
href='https://mattermost.com/pl/message-priority/'
>
{msg}
</ExternalLink>
),
},
help_text_markdown: false,
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
isHidden: it.any(
it.configIsFalse('FeatureFlags', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'PostPriority'),
),
},
{
type: Constants.SettingsTypes.TYPE_NUMBER,
key: 'ServiceSettings.PersistentNotificationMaxRecipients',
label: t('admin.posts.persistentNotificationsMaxRecipients.title'),
label_default: 'Maximum number of recipients for persistent notifications',
help_text: t('admin.posts.persistentNotificationsMaxRecipients.desc'),
help_text_default: 'Configure the maximum number of recipients to which users may send persistent notifications. Learn more about message priority and persistent notifications in our <link>documentation</link>.',
help_text_values: {
link: (msg) => (
<ExternalLink
location='admin_console'
href='https://mattermost.com/pl/message-priority/'
>
{msg}
</ExternalLink>
),
},
help_text_markdown: false,
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
isHidden: it.any(
it.configIsFalse('FeatureFlags', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'AllowPersistentNotifications'),
),
},
{
type: Constants.SettingsTypes.TYPE_NUMBER,
key: 'ServiceSettings.PersistentNotificationIntervalMinutes',
label: t('admin.posts.persistentNotificationsInterval.title'),
label_default: 'Frequency of persistent notifications',
help_text: t('admin.posts.persistentNotificationsInterval.desc'),
help_text_default: '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 <link>documentation</link>.',
help_text_values: {
link: (msg) => (
<ExternalLink
location='admin_console'
href='https://mattermost.com/pl/message-priority/'
>
{msg}
</ExternalLink>
),
},
help_text_markdown: false,
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
isHidden: it.any(
it.configIsFalse('FeatureFlags', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'AllowPersistentNotifications'),
),
validate: validators.minValue(2, t('admin.posts.persistentNotificationsInterval.minValue'), 'Frequency cannot not be set to less than 2 minutes'),
},
{
type: Constants.SettingsTypes.TYPE_NUMBER,
key: 'ServiceSettings.PersistentNotificationMaxCount',
label: t('admin.posts.persistentNotificationsMaxCount.title'),
label_default: 'Total number of persistent notification per post',
help_text: t('admin.posts.persistentNotificationsMaxCount.desc'),
help_text_default: 'Configure the maximum number of times users may receive persistent notifications. Learn more about message priority and persistent notifications in our <link>documentation</link>.',
help_text_values: {
link: (msg) => (
<ExternalLink
location='admin_console'
href='https://mattermost.com/pl/message-priority/'
>
{msg}
</ExternalLink>
),
},
help_text_markdown: false,
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
isHidden: it.any(
it.configIsFalse('FeatureFlags', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'AllowPersistentNotifications'),
),
},
{
type: Constants.SettingsTypes.TYPE_BOOL,
key: 'ServiceSettings.AllowPersistentNotificationsForGuests',
label: t('admin.posts.persistentNotificationsGuests.title'),
label_default: 'Allow guests to send persistent notifications',
help_text: t('admin.posts.persistentNotificationsGuests.desc'),
help_text_default: 'Whether a guest is able to require persistent notifications. Learn more about message priority and persistent notifications in our <link>documentation</link>.',
help_text_values: {
link: (msg) => (
<ExternalLink
location='admin_console'
href='https://mattermost.com/pl/message-priority/'
>
{msg}
</ExternalLink>
),
},
help_text_markdown: false,
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.POSTS)),
isHidden: it.any(
it.configIsFalse('GuestAccountsSettings', 'Enable'),
it.configIsFalse('FeatureFlags', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'PostPriority'),
it.configIsFalse('ServiceSettings', 'AllowPersistentNotifications'),
),
},
{
type: Constants.SettingsTypes.TYPE_BOOL,
key: 'ServiceSettings.EnableLinkPreviews',

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

@@ -20,6 +20,7 @@ exports[`components/advanced_create_post Show tutorial 1`] = `
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -98,6 +99,7 @@ exports[`components/advanced_create_post should match snapshot for center textbo
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -176,6 +178,7 @@ exports[`components/advanced_create_post should match snapshot when cannot post
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -254,6 +257,7 @@ exports[`components/advanced_create_post should match snapshot when file upload
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -332,6 +336,7 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -410,6 +415,7 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -488,6 +494,7 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -566,6 +573,7 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -644,6 +652,7 @@ exports[`components/advanced_create_post should match snapshot, init 1`] = `
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -722,6 +731,7 @@ exports[`components/advanced_create_post should match snapshot, post priority di
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -794,60 +804,11 @@ exports[`components/advanced_create_post should match snapshot, post priority en
<AdvanceTextEditor
additionalControls={
Array [
<React.Fragment>
<Memo(PostPriorityPickerOverlay)
defaultHorizontalPosition="left"
onApply={[Function]}
onHide={[Function]}
show={false}
target={[Function]}
/>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="post-priority-picker-tooltip"
>
<Memo(KeyboardShortcutSequence)
hoistDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Message priority",
"id": "shortcuts.msgs.formatting_bar.post_priority",
},
"mac": Object {
"defaultMessage": "Message priority",
"id": "shortcuts.msgs.formatting_bar.post_priority",
},
}
}
/>
</Tooltip>
}
placement="top"
trigger={
Array [
"hover",
"focus",
]
}
>
<IconContainer
className="control"
disabled={false}
onClick={[Function]}
type="button"
>
<AlertCircleOutlineIcon
color="currentColor"
size={18}
/>
</IconContainer>
</OverlayTrigger>
</React.Fragment>,
<Memo(PostPriorityPickerOverlay)
disabled={false}
onApply={[Function]}
onClose={[Function]}
/>,
]
}
applyMarkdown={[Function]}
@@ -862,6 +823,7 @@ exports[`components/advanced_create_post should match snapshot, post priority en
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -929,65 +891,16 @@ exports[`components/advanced_create_post should match snapshot, post priority en
<AdvanceTextEditor
additionalControls={
Array [
<React.Fragment>
<Memo(PostPriorityPickerOverlay)
defaultHorizontalPosition="left"
onApply={[Function]}
onHide={[Function]}
settings={
Object {
"priority": "important",
}
<Memo(PostPriorityPickerOverlay)
disabled={false}
onApply={[Function]}
onClose={[Function]}
settings={
Object {
"priority": "important",
}
show={false}
target={[Function]}
/>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="post-priority-picker-tooltip"
>
<Memo(KeyboardShortcutSequence)
hoistDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Message priority",
"id": "shortcuts.msgs.formatting_bar.post_priority",
},
"mac": Object {
"defaultMessage": "Message priority",
"id": "shortcuts.msgs.formatting_bar.post_priority",
},
}
}
/>
</Tooltip>
}
placement="top"
trigger={
Array [
"hover",
"focus",
]
}
>
<IconContainer
className="control"
disabled={false}
onClick={[Function]}
type="button"
>
<AlertCircleOutlineIcon
color="currentColor"
size={18}
/>
</IconContainer>
</OverlayTrigger>
</React.Fragment>,
}
/>,
]
}
applyMarkdown={[Function]}
@@ -1002,6 +915,7 @@ exports[`components/advanced_create_post should match snapshot, post priority en
}
}
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
disableSend={false}
draft={
Object {
"fileInfos": Array [],
@@ -1041,65 +955,19 @@ exports[`components/advanced_create_post should match snapshot, post priority en
hideEmojiPicker={[Function]}
isFormattingBarHidden={false}
labels={
<div
className="AdvancedTextEditor__priority"
>
<PriorityLabel
priority="important"
size="xs"
/>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="post-priority-picker-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Remove {priority}"
id="post_priority.remove"
values={
Object {
"priority": "important",
}
}
/>
</Tooltip>
<Memo(PriorityLabels)
canRemove={true}
hasError={false}
onRemove={[Function]}
priority="important"
specialMentions={
Object {
"all": false,
"channel": false,
"here": false,
}
placement="top"
trigger={
Array [
"hover",
"focus",
]
}
>
<button
className="close"
onClick={[Function]}
type="button"
>
<span
aria-hidden="true"
>
×
</span>
<span
className="sr-only"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Remove {priority}"
id="post_priority.remove"
values={
Object {
"priority": "important",
}
}
/>
</span>
</button>
</OverlayTrigger>
</div>
}
/>
}
location="CENTER"
maxPostSize={4000}

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

@@ -4,10 +4,6 @@
/* eslint-disable max-lines */
import React from 'react';
import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {AlertCircleOutlineIcon, CheckCircleOutlineIcon} from '@mattermost/compass-icons/components';
import {isNil} from 'lodash';
@@ -34,6 +30,7 @@ import {
splitMessageBasedOnCaretPosition,
groupsMentionedInText,
mentionsMinusSpecialMentionsInText,
hasRequestedPersistentNotifications,
} from 'utils/post_utils';
import {getTable, hasHtmlLink, formatMarkdownMessage, formatGithubCodePaste, isGitHubCodeBlock, isHttpProtocol, isHttpsProtocol} from 'utils/paste';
import * as UserAgent from 'utils/user_agent';
@@ -41,9 +38,6 @@ import * as Utils from 'utils/utils';
import EmojiMap from 'utils/emoji_map';
import {applyLinkMarkdown, ApplyLinkMarkdownOptions, applyMarkdown, ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
import Tooltip from 'components/tooltip';
import OverlayTrigger from 'components/overlay_trigger';
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
import NotifyConfirmModal from 'components/notify_confirm_modal';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
@@ -51,14 +45,14 @@ import {FileUpload as FileUploadClass} from 'components/file_upload/file_upload'
import ResetStatusModal from 'components/reset_status_modal';
import TextboxClass from 'components/textbox/textbox';
import PostPriorityPickerOverlay from 'components/post_priority/post_priority_picker_overlay';
import PriorityLabel from 'components/post_priority/post_priority_label';
import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal';
import {PostDraft} from 'types/store/draft';
import {ModalData} from 'types/actions';
import {Channel, ChannelMemberCountsByGroup} from '@mattermost/types/channels';
import {Post, PostMetadata, PostPriorityMetadata} from '@mattermost/types/posts';
import {Post, PostMetadata, PostPriority, PostPriorityMetadata} from '@mattermost/types/posts';
import {PreferenceType} from '@mattermost/types/preferences';
import {ServerError} from '@mattermost/types/errors';
import {CommandArgs} from '@mattermost/types/integrations';
@@ -67,10 +61,13 @@ import {FileInfo} from '@mattermost/types/files';
import {Emoji} from '@mattermost/types/emojis';
import AdvancedTextEditor from '../advanced_text_editor/advanced_text_editor';
import {IconContainer} from '../advanced_text_editor/formatting_bar/formatting_icon';
import FileLimitStickyBanner from '../file_limit_sticky_banner';
import {FilePreviewInfo} from '../file_preview/file_preview';
import PriorityLabels from './priority_labels';
const KeyCodes = Constants.KeyCodes;
function isDraftEmpty(draft: PostDraft): boolean {
@@ -276,7 +273,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
private topDiv: React.RefObject<HTMLFormElement>;
private textboxRef: React.RefObject<TextboxClass>;
private fileUploadRef: React.RefObject<FileUploadClass>;
private postPriorityPickerRef: React.RefObject<HTMLButtonElement>;
static getDerivedStateFromProps(props: Props, state: State): Partial<State> {
let updatedState: Partial<State> = {
@@ -317,7 +313,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
this.topDiv = React.createRef<HTMLFormElement>();
this.textboxRef = React.createRef<TextboxClass>();
this.fileUploadRef = React.createRef<FileUploadClass>();
this.postPriorityPickerRef = React.createRef<HTMLButtonElement>();
}
componentDidMount() {
@@ -599,6 +594,20 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
});
};
showPersistNotificationModal = (message: string, specialMentions: {[key: string]: boolean}, channelType: Channel['type']) => {
this.props.actions.openModal({
modalId: ModalIdentifiers.PERSIST_NOTIFICATION_CONFIRM_MODAL,
dialogType: PersistNotificationConfirmModal,
dialogProps: {
currentChannelTeammateUsername: this.props.currentChannelTeammateUsername,
specialMentions,
channelType,
message,
onConfirm: this.handleNotifyAllConfirmation,
},
});
};
getStatusFromSlashCommand = () => {
const {message} = this.state;
const tokens = message.split(' ');
@@ -673,7 +682,17 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
}
}
if (memberNotifyCount > 0) {
const isDirectOrGroup =
updateChannel.type === Constants.DM_CHANNEL || updateChannel.type === Constants.GM_CHANNEL;
if (
this.props.isPostPriorityEnabled &&
hasRequestedPersistentNotifications(this.props.draft?.metadata?.priority)
) {
this.showPersistNotificationModal(this.state.message, specialMentions, updateChannel.type);
this.isDraftSubmitting = false;
return;
} else if (memberNotifyCount > 0) {
this.showNotifyAllModal(mentions, channelTimezoneCount, memberNotifyCount);
this.isDraftSubmitting = false;
return;
@@ -708,8 +727,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
return;
}
const isDirectOrGroup =
updateChannel.type === Constants.DM_CHANNEL || updateChannel.type === Constants.GM_CHANNEL;
if (!isDirectOrGroup && trimRight(this.state.message) === '/purpose') {
const editChannelPurposeModalData = {
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
@@ -837,7 +854,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
return;
}
if (allowSending) {
if (allowSending && this.isValidPersistentNotifications()) {
if (e.persist) {
e.persist();
}
@@ -1555,20 +1572,9 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
};
handlePostPriorityHide = () => {
this.setState({
showPostPriorityPicker: false,
});
this.focusTextbox();
this.focusTextbox(true);
};
togglePostPriorityPicker = () => {
this.setState((prev) => ({
showPostPriorityPicker: !prev.showPostPriorityPicker,
}));
};
getPostPriorityPickerRef = () => this.postPriorityPickerRef.current;
hasPrioritySet = () => {
return (
this.props.isPostPriorityEnabled &&
@@ -1579,7 +1585,41 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
);
};
isValidPersistentNotifications = (): boolean => {
if (!this.hasPrioritySet()) {
return true;
}
const {currentChannel} = this.props;
const {priority, persistent_notifications: persistentNotifications} = this.props.draft.metadata!.priority!;
if (priority !== PostPriority.URGENT || !persistentNotifications) {
return true;
}
if (currentChannel.type === Constants.DM_CHANNEL) {
return true;
}
if (this.hasSpecialMentions()) {
return false;
}
const mentions = mentionsMinusSpecialMentionsInText(this.state.message);
return mentions.length > 0;
};
getSpecialMentions = (): {[key: string]: boolean} => {
return specialMentionsInText(this.state.message);
};
hasSpecialMentions = (): boolean => {
return Object.values(this.getSpecialMentions()).includes(true);
};
render() {
const {draft, canPost} = this.props;
let centerClass = '';
if (!this.props.fullWidthTextBox) {
centerClass = 'center';
@@ -1589,78 +1629,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
return null;
}
const priorityLabels = (
this.hasPrioritySet() ? (
<div className='AdvancedTextEditor__priority'>
{this.props.draft.metadata!.priority!.priority && (
<PriorityLabel
size='xs'
priority={this.props.draft.metadata!.priority!.priority}
/>
)}
{this.props.draft.metadata!.priority!.requested_ack && (
<div className='AdvancedTextEditor__priority-ack'>
<OverlayTrigger
placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
trigger={Constants.OVERLAY_DEFAULT_TRIGGER}
overlay={(
<Tooltip
id='post-priority-picker-ack-tooltip'
className='AdvancedTextEditor__priority-ack-tooltip'
>
<FormattedMessage
id={'post_priority.request_acknowledgement.tooltip'}
defaultMessage={'Acknowledgement will be requested'}
/>
</Tooltip>
)}
>
<CheckCircleOutlineIcon size={14}/>
</OverlayTrigger>
{!(this.props.draft.metadata!.priority!.priority) && (
<FormattedMessage
id={'post_priority.request_acknowledgement'}
defaultMessage={'Request acknowledgement'}
/>
)}
</div>
)}
{!this.props.shouldShowPreview && (
<OverlayTrigger
placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
trigger={Constants.OVERLAY_DEFAULT_TRIGGER}
overlay={(
<Tooltip id='post-priority-picker-tooltip'>
<FormattedMessage
id={'post_priority.remove'}
defaultMessage={'Remove {priority}'}
values={{priority: this.props.draft.metadata!.priority!.priority}}
/>
</Tooltip>
)}
>
<button
type='button'
className='close'
onClick={this.handleRemovePriority}
>
<span aria-hidden='true'>{'×'}</span>
<span className='sr-only'>
<FormattedMessage
id={'post_priority.remove'}
defaultMessage={'Remove {priority}'}
values={{priority: this.props.draft.metadata!.priority!.priority}}
/>
</span>
</button>
</OverlayTrigger>
)}
</div>
) : undefined
);
return (
<form
id='create_post'
@@ -1668,11 +1636,9 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
className={centerClass}
onSubmit={this.handleSubmit}
>
{
this.props.canPost &&
(this.props.draft.fileInfos.length > 0 || this.props.draft.uploadsInProgress.length > 0) &&
{canPost && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0) && (
<FileLimitStickyBanner/>
}
)}
<AdvancedTextEditor
location={Locations.CENTER}
currentUserId={this.props.currentUserId}
@@ -1686,14 +1652,14 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
errorClass={this.state.errorClass}
serverError={this.state.serverError}
isFormattingBarHidden={this.state.isFormattingBarHidden}
draft={this.props.draft}
draft={draft}
showSendTutorialTip={this.props.showSendTutorialTip}
handleSubmit={this.handleSubmit}
removePreview={this.removePreview}
setShowPreview={this.setShowPreview}
shouldShowPreview={this.props.shouldShowPreview}
maxPostSize={this.props.maxPostSize}
canPost={this.props.canPost}
canPost={canPost}
applyMarkdown={this.applyMarkdown}
useChannelMentions={this.props.useChannelMentions}
badConnection={this.props.badConnection}
@@ -1722,46 +1688,27 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
fileUploadRef={this.fileUploadRef}
prefillMessage={this.prefillMessage}
textboxRef={this.textboxRef}
labels={priorityLabels}
disableSend={!this.isValidPersistentNotifications()}
labels={this.hasPrioritySet() ? (
<PriorityLabels
canRemove={!this.props.shouldShowPreview}
hasError={!this.isValidPersistentNotifications()}
specialMentions={this.getSpecialMentions()}
onRemove={this.handleRemovePriority}
persistentNotifications={draft!.metadata!.priority?.persistent_notifications}
priority={draft!.metadata!.priority?.priority}
requestedAck={draft!.metadata!.priority?.requested_ack}
/>
) : undefined}
additionalControls={[
this.props.isPostPriorityEnabled && (
<React.Fragment key='PostPriorityPicker'>
<PostPriorityPickerOverlay
settings={this.props.draft?.metadata?.priority}
show={this.state.showPostPriorityPicker}
target={this.getPostPriorityPickerRef}
onApply={this.handlePostPriorityApply}
onHide={this.handlePostPriorityHide}
defaultHorizontalPosition='left'
/>
<OverlayTrigger
placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
trigger={Constants.OVERLAY_DEFAULT_TRIGGER}
overlay={(
<Tooltip id='post-priority-picker-tooltip'>
<KeyboardShortcutSequence
shortcut={KEYBOARD_SHORTCUTS.msgPostPriority}
hoistDescription={true}
isInsideTooltip={true}
/>
</Tooltip>
)}
>
<IconContainer
ref={this.postPriorityPickerRef}
className={classNames({control: true, active: this.state.showPostPriorityPicker})}
disabled={this.props.shouldShowPreview}
type='button'
onClick={this.togglePostPriorityPicker}
>
<AlertCircleOutlineIcon
size={18}
color='currentColor'
/>
</IconContainer>
</OverlayTrigger>
</React.Fragment>
<PostPriorityPickerOverlay
key='post-priority-picker-key'
settings={draft?.metadata?.priority}
onApply={this.handlePostPriorityApply}
onClose={this.handlePostPriorityHide}
disabled={this.props.shouldShowPreview}
/>
),
].filter(Boolean)}
/>

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

@@ -0,0 +1,192 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, CSSProperties} from 'react';
import {FormattedMessage} from 'react-intl';
import styled from 'styled-components';
import {CheckCircleOutlineIcon, BellRingOutlineIcon} from '@mattermost/compass-icons/components';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import PriorityLabel from 'components/post_priority/post_priority_label';
import {HasNoMentions, HasSpecialMentions} from 'components/post_priority/error_messages';
import Constants from 'utils/constants';
import {PostPriorityMetadata} from '@mattermost/types/posts';
type Props = {
canRemove: boolean;
hasError: boolean;
specialMentions?: {[key: string]: boolean};
onRemove?: () => void;
padding?: CSSProperties['padding'];
persistentNotifications?: PostPriorityMetadata['persistent_notifications'];
priority?: PostPriorityMetadata['priority'];
requestedAck?: PostPriorityMetadata['requested_ack'];
};
type StyledProps = {
hasError: boolean;
};
const Priority = styled.div`
align-items: center;
display: flex;
gap: 6px;
padding: ${(props: {padding: CSSProperties['padding']}) => props.padding || '14px 16px 0'}
`;
const Acknowledgements = styled.div`
align-items: center;
color: ${(props: StyledProps) => (props.hasError ? 'var(--dnd-indicator)' : 'var(--online-indicator)')};
display: flex;
> span {
margin-left: 4px;
font-size: 11px;
font-weight: 600;
}
`;
const Notifications = styled.div`
align-items: center;
color: var(--dnd-indicator);
display: flex;
> span {
margin-left: 4px;
font-size: 11px;
font-weight: 600;
}
`;
const Close = styled.button`
align-items: center;
color: rgb(var(--center-channel-color));
display: flex;
font-size: 17px;
justify-content: center;
margin-top: -1px;
opacity: 0.56;
visibility: hidden;
&:hover {
opacity: 0.72;
}
${Priority}:hover & {
visibility: visible;
}
`;
const Error = styled.div`
color: var(--dnd-indicator);
font-size: 11px;
font-weight: 600;
`;
function PriorityLabels({
canRemove,
hasError,
specialMentions,
onRemove,
padding,
persistentNotifications,
priority,
requestedAck,
}: Props) {
return (
<Priority padding={padding}>
{priority && (
<PriorityLabel
size='xs'
priority={priority}
/>
)}
{persistentNotifications && (
<OverlayTrigger
placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
trigger={Constants.OVERLAY_DEFAULT_TRIGGER}
overlay={(
<Tooltip id='post-priority-picker-persistent-notifications-tooltip'>
<FormattedMessage
id={'post_priority.persistent_notifications.tooltip'}
defaultMessage={'Persistent notifications will be sent'}
/>
</Tooltip>
)}
>
<Notifications>
<BellRingOutlineIcon size={14}/>
</Notifications>
</OverlayTrigger>
)}
{requestedAck && (
<Acknowledgements hasError={hasError}>
<OverlayTrigger
placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
trigger={Constants.OVERLAY_DEFAULT_TRIGGER}
overlay={(
<Tooltip id='post-priority-picker-ack-tooltip'>
<FormattedMessage
id={'post_priority.request_acknowledgement.tooltip'}
defaultMessage={'Acknowledgement will be requested'}
/>
</Tooltip>
)}
>
<CheckCircleOutlineIcon size={14}/>
</OverlayTrigger>
{!(priority) && (
<FormattedMessage
id={'post_priority.request_acknowledgement'}
defaultMessage={'Request acknowledgement'}
/>
)}
</Acknowledgements>
)}
{hasError && (
<Error>
{(specialMentions && Object.values(specialMentions).includes(true)) ? <HasSpecialMentions specialMentions={specialMentions}/> : <HasNoMentions/>}
</Error>
)}
{canRemove && (
<OverlayTrigger
placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
trigger={Constants.OVERLAY_DEFAULT_TRIGGER}
overlay={(
<Tooltip id='post-priority-picker-tooltip'>
<FormattedMessage
id={'post_priority.remove'}
defaultMessage={'Remove {priority}'}
values={{priority}}
/>
</Tooltip>
)}
>
<Close
type='button'
className='close'
onClick={onRemove}
>
<span aria-hidden='true'>{'×'}</span>
<span className='sr-only'>
<FormattedMessage
id={'post_priority.remove'}
defaultMessage={'Remove {priority}'}
values={{priority}}
/>
</span>
</Close>
</OverlayTrigger>
)}
</Priority>
);
}
export default memo(PriorityLabels);

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

@@ -67,50 +67,6 @@
}
}
&__priority {
display: flex;
align-items: center;
padding: 14px 16px 0;
gap: 6px;
&-ack {
display: flex;
align-items: center;
color: var(--online-indicator);
> span {
margin-left: 4px;
font-size: 11px;
font-weight: 600;
}
&-tooltip {
max-width: 230px;
}
}
button.close {
display: flex;
align-items: center;
justify-content: center;
margin-top: -1px;
color: rgb(var(--center-channel-color));
font-size: 17px;
opacity: 0.56;
visibility: hidden;
&:hover {
opacity: 0.72;
}
}
&:hover {
button.close {
visibility: visible;
}
}
}
&__action-button {
display: flex;
width: 32px;

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

@@ -101,6 +101,7 @@ type Props = {
isThreadView?: boolean;
additionalControls?: React.ReactNodeArray;
labels?: React.ReactNode;
disableSend?: boolean;
}
const AdvanceTextEditor = ({
@@ -156,6 +157,7 @@ const AdvanceTextEditor = ({
isThreadView,
additionalControls,
labels,
disableSend = false,
}: Props) => {
const readOnlyChannel = !canPost;
const {formatMessage} = useIntl();
@@ -301,7 +303,7 @@ const AdvanceTextEditor = ({
);
}
const disableSendButton = Boolean(readOnlyChannel || (!message.trim().length && !draft.fileInfos.length));
const disableSendButton = Boolean(readOnlyChannel || (!message.trim().length && !draft.fileInfos.length)) || disableSend;
const sendButton = readOnlyChannel ? null : (
<SendButton
disabled={disableSendButton}

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

@@ -43,6 +43,7 @@ exports[`components/drafts/drafts_row should match snapshot for channel draft 1`
draftId=""
id={Object {}}
isRemote={false}
postPriorityEnabled={false}
status={Object {}}
type="channel"
user={Object {}}
@@ -90,6 +91,7 @@ exports[`components/drafts/drafts_row should match snapshot for undefined channe
draftId=""
id={Object {}}
isRemote={false}
postPriorityEnabled={false}
status={Object {}}
type="channel"
user={Object {}}

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

@@ -26,6 +26,7 @@ describe('components/drafts/drafts_row', () => {
type: 'channel' as 'channel' | 'thread',
user: {} as UserProfile,
value: {} as PostDraft,
postPriorityEnabled: false,
isRemote: false,
};

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

@@ -5,9 +5,13 @@ import React, {memo, useCallback} from 'react';
import {useDispatch} from 'react-redux';
import {useHistory} from 'react-router-dom';
import PersistNotificationConfirmModal from 'components/persist_notification_confirm_modal';
import {openModal} from 'actions/views/modals';
import {createPost} from 'actions/post_actions';
import {removeDraft} from 'actions/views/drafts';
import {PostDraft} from 'types/store/draft';
import {hasRequestedPersistentNotifications, specialMentionsInText} from 'utils/post_utils';
import {ModalIdentifiers} from 'utils/constants';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile, UserStatus} from '@mattermost/types/users';
@@ -25,6 +29,7 @@ type Props = {
displayName: string;
draftId: string;
id: Channel['id'];
postPriorityEnabled: boolean;
status: UserStatus['status'];
type: 'channel' | 'thread';
user: UserProfile;
@@ -37,6 +42,7 @@ function ChannelDraft({
channelUrl,
displayName,
draftId,
postPriorityEnabled,
status,
type,
user,
@@ -48,11 +54,30 @@ function ChannelDraft({
const handleOnEdit = useCallback(() => {
history.push(channelUrl);
}, [channelUrl]);
}, [history, channelUrl]);
const handleOnDelete = useCallback((id: string) => {
dispatch(removeDraft(id, channel.id));
}, [channel.id]);
}, [dispatch, channel.id]);
const doSubmit = useCallback((id: string, post: Post) => {
dispatch(createPost(post, value.fileInfos));
dispatch(removeDraft(id, channel.id));
history.push(channelUrl);
}, [dispatch, history, value.fileInfos, channel.id, channelUrl]);
const showPersistNotificationModal = useCallback((id: string, post: Post) => {
dispatch(openModal({
modalId: ModalIdentifiers.PERSIST_NOTIFICATION_CONFIRM_MODAL,
dialogType: PersistNotificationConfirmModal,
dialogProps: {
message: post.message,
channelType: channel.type,
specialMentions: specialMentionsInText(post.message),
onConfirm: () => doSubmit(id, post),
},
}));
}, [channel.type, dispatch, doSubmit]);
const handleOnSend = useCallback(async (id: string) => {
const post = {} as Post;
@@ -67,11 +92,12 @@ function ChannelDraft({
return;
}
dispatch(createPost(post, value.fileInfos));
dispatch(removeDraft(id, channel.id));
history.push(channelUrl);
}, [value, channelUrl, user.id, channel.id]);
if (postPriorityEnabled && hasRequestedPersistentNotifications(value?.metadata?.priority)) {
showPersistNotificationModal(id, post);
return;
}
doSubmit(id, post);
}, [doSubmit, postPriorityEnabled, value, user.id, showPersistNotificationModal]);
if (!channel) {
return null;

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

@@ -3,6 +3,7 @@
import {connect} from 'react-redux';
import {isPostPriorityEnabled} from 'mattermost-redux/selectors/entities/posts';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
@@ -28,6 +29,7 @@ function makeMapStateToProps() {
return {
channel,
channelUrl,
postPriorityEnabled: isPostPriorityEnabled(state),
};
};
}

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

@@ -583,7 +583,7 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
priority={
Object {
"priority": "important",
"requested_ack": true,
"requested_ack": false,
}
}
status="status"
@@ -776,76 +776,68 @@ exports[`components/drafts/panel/panel_body should match snapshot for priority 1
<strong>
display_name
</strong>
<div
className="DraftPanelBody__priority"
<Memo(PriorityLabels)
canRemove={false}
hasError={false}
padding="0 0 0 8px"
priority="important"
requestedAck={false}
>
<PriorityLabel
priority="important"
size="xs"
<Priority
padding="0 0 0 8px"
>
<Memo(Tag)
icon="alert-circle-outline"
size="xs"
text="Important"
uppercase={true}
variant="info"
<div
className="Priority-unHRQ dkJcdD"
>
<TagWrapper
as="div"
className="Tag Tag--info Tag--xs"
uppercase={true}
<PriorityLabel
priority="important"
size="xs"
>
<div
className="TagWrapper-keYggn hpsCJu Tag Tag--info Tag--xs"
<Memo(Tag)
icon="alert-circle-outline"
size="xs"
text="Important"
uppercase={true}
variant="info"
>
<AlertCircleOutlineIcon
size={10}
<TagWrapper
as="div"
className="Tag Tag--info Tag--xs"
uppercase={true}
>
<svg
fill="currentColor"
height={10}
version="1.1"
viewBox="0 0 24 24"
width={10}
xmlns="http://www.w3.org/2000/svg"
<div
className="TagWrapper-keYggn hpsCJu Tag Tag--info Tag--xs"
>
<path
d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2 M12,20c-4.4,0-8-3.6-8-8s3.6-8,8-8s8,3.6,8,8S16.4,20,12,20z M12.501,13h-1l-0.5-6h2L12.501,13z M13,16c0,0.552-0.448,1-1,1s-1-0.448-1-1s0.448-1,1-1S13,15.448,13,16z"
/>
</svg>
</AlertCircleOutlineIcon>
<TagText>
<span
className="TagText-bWgUzx kzWPbz"
>
Important
</span>
</TagText>
</div>
</TagWrapper>
</Memo(Tag)>
</PriorityLabel>
<div
className="DraftPanelBody__priority-ack"
>
<CheckCircleOutlineIcon
size={14}
>
<svg
fill="currentColor"
height={14}
version="1.1"
viewBox="0 0 24 24"
width={14}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"
/>
</svg>
</CheckCircleOutlineIcon>
</div>
</div>
<AlertCircleOutlineIcon
size={10}
>
<svg
fill="currentColor"
height={10}
version="1.1"
viewBox="0 0 24 24"
width={10}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2 M12,20c-4.4,0-8-3.6-8-8s3.6-8,8-8s8,3.6,8,8S16.4,20,12,20z M12.501,13h-1l-0.5-6h2L12.501,13z M13,16c0,0.552-0.448,1-1,1s-1-0.448-1-1s0.448-1,1-1S13,15.448,13,16z"
/>
</svg>
</AlertCircleOutlineIcon>
<TagText>
<span
className="TagText-bWgUzx kzWPbz"
>
Important
</span>
</TagText>
</div>
</TagWrapper>
</Memo(Tag)>
</PriorityLabel>
</div>
</Priority>
</Memo(PriorityLabels)>
</div>
<div
className="post__body"
@@ -1133,38 +1125,147 @@ exports[`components/drafts/panel/panel_body should match snapshot for requested_
<strong>
display_name
</strong>
<div
className="DraftPanelBody__priority"
<Memo(PriorityLabels)
canRemove={false}
hasError={false}
padding="0 0 0 8px"
priority=""
requestedAck={true}
>
<div
className="DraftPanelBody__priority-ack"
<Priority
padding="0 0 0 8px"
>
<CheckCircleOutlineIcon
size={14}
<div
className="Priority-unHRQ dkJcdD"
>
<svg
fill="currentColor"
height={14}
version="1.1"
viewBox="0 0 24 24"
width={14}
xmlns="http://www.w3.org/2000/svg"
<Acknowledgements
hasError={false}
>
<path
d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"
/>
</svg>
</CheckCircleOutlineIcon>
<FormattedMessage
defaultMessage="Request acknowledgement"
id="post_priority.request_acknowledgement"
>
<span>
Request acknowledgement
</span>
</FormattedMessage>
</div>
</div>
<div
className="Acknowledgements-kPkVCv dNydui"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="post-priority-picker-ack-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Acknowledgement will be requested"
id="post_priority.request_acknowledgement.tooltip"
/>
</Tooltip>
}
placement="top"
trigger={
Array [
"hover",
"focus",
]
}
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
id="post-priority-picker-ack-tooltip"
intl={
Object {
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": "span",
"timeZone": "Etc/UTC",
"wrapRichTextChunksInFragment": undefined,
}
}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Acknowledgement will be requested"
id="post_priority.request_acknowledgement.tooltip"
/>
</OverlayWrapper>
}
placement="top"
trigger={
Array [
"hover",
"focus",
]
}
>
<CheckCircleOutlineIcon
onBlur={[Function]}
onClick={null}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
size={14}
>
<svg
fill="currentColor"
height={14}
onBlur={[Function]}
onClick={null}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
version="1.1"
viewBox="0 0 24 24"
width={14}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z"
/>
</svg>
</CheckCircleOutlineIcon>
</OverlayTrigger>
</OverlayTrigger>
<FormattedMessage
defaultMessage="Request acknowledgement"
id="post_priority.request_acknowledgement"
>
<span>
Request acknowledgement
</span>
</FormattedMessage>
</div>
</Acknowledgements>
</div>
</Priority>
</Memo(PriorityLabels)>
</div>
<div
className="post__body"

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

@@ -10,25 +10,6 @@
background-color: var(--center-channel-bg);
}
&__priority {
display: flex;
align-items: center;
padding-left: 8px;
gap: 6px;
&-ack {
display: flex;
align-items: center;
color: var(--online-indicator);
> span {
margin-left: 4px;
font-size: 11px;
font-weight: 600;
}
}
}
.file-preview__container {
height: auto;
flex-wrap: wrap;

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

@@ -112,7 +112,7 @@ describe('components/drafts/panel/panel_body', () => {
{...baseProps}
priority={{
priority: PostPriority.IMPORTANT,
requested_ack: true,
requested_ack: false,
}}
/>
</Provider>,

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

@@ -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({
<div className='post__header'>
<strong>{displayName}</strong>
{priority && (
<div className='DraftPanelBody__priority'>
{priority.priority && (
<PriorityLabel
size='xs'
priority={priority.priority}
/>
)}
{priority.requested_ack && (
<div className='DraftPanelBody__priority-ack'>
<CheckCircleOutlineIcon size={14}/>
{!priority.priority && (
<FormattedMessage
id={'post_priority.request_acknowledgement'}
defaultMessage={'Request acknowledgement'}
/>
)}
</div>
)}
</div>
<PriorityLabels
canRemove={false}
padding='0 0 0 8px'
hasError={false}
persistentNotifications={priority.persistent_notifications}
priority={priority.priority}
requestedAck={priority.requested_ack}
/>
)}
</div>
<div className='post__body'>

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

@@ -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 = (
<FormattedMessage
id='persist_notification.dm_or_gm.title'
defaultMessage='Send persistent notifications?'
/>
);
body = (
<FormattedMessage
id='persist_notification.dm_or_gm.description'
defaultMessage='<b>{username}</b> will be notified every {interval, plural, one {1 minute} other {{interval} minutes}} until theyve acknowledged the message.'
values={{
interval,
username: currentChannelTeammateUsername,
b: (chunks: string) => <b>{chunks}</b>,
}}
/>
);
confirmBtn = (
<FormattedMessage
id='persist_notification.dm_or_gm'
defaultMessage='Send'
/>
);
} else if (Object.values(specialMentions).includes(true)) {
body = (
<HasSpecialMentions specialMentions={specialMentions}/>
);
confirmBtn = (
<FormattedMessage
id='persist_notification.special_mentions.confirm'
defaultMessage='Got it'
/>
);
} else if (count === 0) {
title = <HasNoMentions/>;
body = (
<FormattedMessage
id='persist_notification.too_few.description'
defaultMessage='There are no recipients mentioned in your message. Youll need add mentions to be able to send persistent notifications.'
/>
);
confirmBtn = (
<FormattedMessage
id='persist_notification.too_few.confirm'
defaultMessage='Got it'
/>
);
} else if (count > Number(maxRecipients)) {
title = (
<FormattedMessage
id='persist_notification.too_many.title'
defaultMessage='Too many recipients'
/>
);
body = (
<FormattedMessage
id='persist_notification.too_many.description'
defaultMessage='You can send persistent notifications to a maximum of <b>{max}</b> recipients. There are <b>{count}</b> recipients mentioned in your message. Youll need to change who youve mentioned before you can send.'
values={{
max: maxRecipients,
count,
b: (chunks: string) => <b>{chunks}</b>,
}}
/>
);
confirmBtn = (
<FormattedMessage
id='persist_notification.too_many.confirm'
defaultMessage='Got it'
/>
);
} else {
handleConfirm = onConfirm;
title = (
<FormattedMessage
id='persist_notification.confirm.title'
defaultMessage='Send persistent notifications?'
/>
);
body = (
<FormattedMessage
id='persist_notification.confirm.description'
defaultMessage='Mentioned recipients will be notified every {interval, plural, one {1 minute} other {{interval} minutes}} until theyve acknowledged the message.'
values={{
interval,
}}
/>
);
confirmBtn = (
<FormattedMessage
id='persist_notification.confirm'
defaultMessage='Send'
/>
);
}
return (
<GenericModal
autoFocusConfirmButton={true}
id='persist_notification_confirm_modal'
autoCloseOnConfirmButton={true}
compassDesign={true}
confirmButtonText={confirmBtn}
enforceFocus={true}
handleCancel={() => {}}
handleConfirm={handleConfirm}
handleEnterKeyPress={handleConfirm}
isDeleteModal={false}
modalHeaderText={title}
onExited={onExited}
>
{body}
</GenericModal>
);
}
export default memo(PersistNotificationConfirmModal);

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

@@ -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 (
<FormattedMessage
id={'post_priority.error.special_mentions'}
defaultMessage={'{mention} cant be used with persistent notifications'}
values={{
mention: (
<FormattedList
value={mentions}
type='disjunction'
/>
),
}}
/>
);
}
export function HasNoMentions() {
return (
<FormattedMessage
id={'post_priority.error.no_mentions'}
defaultMessage={'Recipients must be @mentioned'}
/>
);
}

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

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

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

@@ -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<PostPriority|''>(settings?.priority || '');
const [requestedAck, setRequestedAck] = useState<boolean>(settings?.requested_ack || false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
ref.current?.focus();
}, []);
const [persistentNotifications, setPersistentNotifications] = useState<boolean>(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<HTMLButtonElement>) => {
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 (
<Picker
ref={ref}
tabIndex={-1}
style={pickerStyle}
className='PostPriorityPicker'
>
<Picker className='PostPriorityPicker'>
<Header className='modal-title'>
{formatMessage({
id: 'post_priority.picker.header',
@@ -215,22 +191,44 @@ function PostPriorityPicker({
})}
/>
</MenuGroup>
{postAcknowledgementsEnabled && (
{(postAcknowledgementsEnabled || persistentNotificationsEnabled) && (
<MenuGroup>
<ToggleItem
disabled={false}
onClick={handleAck}
toggled={requestedAck}
icon={<AcknowledgementIcon size={18}/>}
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 && (
<ToggleItem
disabled={false}
onClick={handleAck}
toggled={requestedAck}
icon={<AcknowledgementIcon size={18}/>}
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 && (
<ToggleItem
disabled={priority !== PostPriority.URGENT}
onClick={handlePersistentNotifications}
toggled={persistentNotifications}
icon={<PersistentNotificationsIcon size={18}/>}
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,
},
)}
/>
)}
</MenuGroup>
)}
</Menu>

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

@@ -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 (
<Wrapper
onClick={onClick}
onClick={disabled ? undefined : onClick}
disabled={disabled}
role='button'
>
<ToggleMain>

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

@@ -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<HTMLButtonElement> | 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: (
<FormattedMessage
id='shortcuts.msgs.formatting_bar.post_priority'
defaultMessage={'Message priority'}
/>
),
});
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 (
<Overlay
show={show}
placement={'top'}
rootClose={true}
onHide={onHide}
target={target}
animation={false}
>
<PostPriorityPicker
settings={settings}
leftOffset={offset}
onApply={onApply}
topOffset={-7}
placement={'top'}
onClose={onHide}
/>
</Overlay>
<>
<div
ref={tooltipRef}
{...getTooltipReferenceProps()}
>
<IconContainer
ref={pickerRef}
className={classNames({control: true, active: pickerOpen})}
disabled={disabled}
type='button'
{...getPickerReferenceProps()}
>
<AlertCircleOutlineIcon
size={18}
color='currentColor'
/>
</IconContainer>
</div>
<FloatingPortal id='root-portal'>
{pickerOpen && (
<FloatingFocusManager
context={pickerContext}
modal={true}
returnFocus={false}
initialFocus={-1}
>
<div
ref={pickerFloating}
style={{
width: 'max-content',
position: pickerStrategy,
top: pickerY ?? 0,
left: pickerX ?? 0,
zIndex: 3,
}}
{...getPickerFloatingProps()}
>
<PostPriorityPicker
settings={settings}
onApply={onApply}
onClose={handleClose}
/>
</div>
</FloatingFocusManager>
)}
</FloatingPortal>
{!pickerOpen && tooltip}
</>
);
}

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

@@ -30,7 +30,7 @@
&--disabled,
&:disabled {
background: rgba(var(--online-indicator-rgb), 0.08);
cursor: not-allowed;
cursor: default;
}
&:hover:enabled {

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

@@ -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 <link>documentation</link> 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 <link>documentation</link>.",
"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 <link>documentation</link>.",
"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 <link>documentation</link>.",
"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 <link>documentation</link>.",
"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 <link>documentation</link>.",
"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 <link>documentation</link>.",
"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 theyve acknowledged the message.",
"persist_notification.confirm.title": "Send persistent notifications?",
"persist_notification.dm_or_gm": "Send",
"persist_notification.dm_or_gm.description": "<b>{username}</b> will be notified every {interval, plural, one {1 minute} other {{interval} minutes}} until theyve 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. Youll 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 <b>{max}</b> recipients. There are <b>{count}</b> recipients mentioned in your message. Youll need to change who youve 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} cant 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",

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

@@ -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<UserProfile['id'], PostAcknowledgement['acknowledged_at']> {
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',

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

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

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

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

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

@@ -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<Props, State> {
confirmButton = (
<button
autoFocus={this.props.autoFocusConfirmButton}
type='submit'
className={classNames('GenericModal__button', isConfirmOrDeleteClassName, this.props.confirmButtonClassName, {
disabled: this.props.isConfirmDisabled,

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

@@ -197,6 +197,10 @@ export type ClientConfig = {
PostPriority: string;
ReduceOnBoardingTaskList: string;
PostAcknowledgements: string;
AllowPersistentNotifications: string;
PersistentNotificationMaxRecipients: string;
PersistentNotificationIntervalMinutes: string;
AllowPersistentNotificationsForGuests: string;
DelayChannelAutocomplete: 'true' | 'false';
};
@@ -372,6 +376,11 @@ export type ServiceSettings = {
EnableCustomGroups: boolean;
SelfHostedPurchase: boolean;
AllowSyncedDrafts: boolean;
AllowPersistentNotifications: boolean;
AllowPersistentNotificationsForGuests: boolean;
PersistentNotificationIntervalMinutes: number;
PersistentNotificationMaxCount: number;
PersistentNotificationMaxRecipients: number;
};
export type TeamSettings = {