[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>
Этот коммит содержится в:
@@ -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 they’ve 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. You’ll 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. You’ll need to change who you’ve 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 they’ve 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);
|
||||
44
webapp/channels/src/components/post_priority/error_messages.tsx
Обычный файл
44
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 (
|
||||
<FormattedMessage
|
||||
id={'post_priority.error.special_mentions'}
|
||||
defaultMessage={'{mention} can’t 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 they’ve 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 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 <b>{max}</b> recipients. There are <b>{count}</b> 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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Ссылка в новой задаче
Block a user