* Make GM behave as DM

* Fix lint

* Add desktop notification special behavior

* Change notification preferences menu

* Make changes to the GM channel intro

* Fix tests

* Fix i18n and style lint

* Add system notice and update style

* Fix style and fix tests

* Fix tests

* Handle push notifications as desktop notifications

* Fix tests

* Add test and default GMs to none when user level config is none

* Fix test

* Update only for mentions text

* Add tests

* Fix lint

* Fix lint
Этот коммит содержится в:
Daniel Espino García
2023-09-19 15:29:57 +02:00
коммит произвёл GitHub
родитель 39d6cb8008
Коммит 88d043a971
40 изменённых файлов: 1824 добавлений и 511 удалений

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

@@ -9,6 +9,7 @@ import {
getTeammateNameDisplaySetting,
isCollapsedThreadsEnabled,
} from 'mattermost-redux/selectors/entities/preferences';
import {getAllUserMentionKeys} from 'mattermost-redux/selectors/entities/search';
import {getCurrentUserId, getCurrentUser, getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import {isSystemMessage, isUserAddedInChannel} from 'mattermost-redux/utils/post_utils';
@@ -18,11 +19,13 @@ import {getChannelURL, getPermalinkURL} from 'selectors/urls';
import {isThreadOpen} from 'selectors/views/threads';
import {getHistory} from 'utils/browser_history';
import Constants, {NotificationLevels, UserStatuses} from 'utils/constants';
import Constants, {NotificationLevels, UserStatuses, IgnoreChannelMentions} from 'utils/constants';
import {t} from 'utils/i18n';
import {stripMarkdown} from 'utils/markdown';
import {stripMarkdown, formatWithRenderer} from 'utils/markdown';
import MentionableRenderer from 'utils/markdown/mentionable_renderer';
import * as NotificationSounds from 'utils/notification_sounds';
import {showNotification} from 'utils/notifications';
import {cjkrPattern, escapeRegex} from 'utils/text_formatting';
import {isDesktopApp, isMobileApp, isWindowsApp} from 'utils/user_agent';
import * as Utils from 'utils/utils';
@@ -93,14 +96,94 @@ export function sendDesktopNotification(post, msgProps) {
return;
}
let notifyLevel = member?.notify_props?.desktop || NotificationLevels.DEFAULT;
const channelNotifyProp = member?.notify_props?.desktop || NotificationLevels.DEFAULT;
let notifyLevel = channelNotifyProp;
if (notifyLevel === NotificationLevels.DEFAULT) {
notifyLevel = user?.notify_props?.desktop || NotificationLevels.ALL;
}
if (channel.type === 'G' && channelNotifyProp === NotificationLevels.DEFAULT && user?.notify_props?.desktop === NotificationLevels.MENTION) {
notifyLevel = NotificationLevels.ALL;
}
if (notifyLevel === NotificationLevels.NONE) {
return;
} else if (channel.type === 'G' && notifyLevel === NotificationLevels.MENTION) {
// Compose the whole text in the message, including interactive messages.
let text = post.message;
// We do this on a try catch block to avoid errors from malformed props
try {
if (post.props && post.props.attachments) {
const attachments = post.props.attachments;
function appendText(toAppend) {
if (toAppend) {
text += `\n${toAppend}`;
}
}
for (const attachment of attachments) {
appendText(attachment.pretext);
appendText(attachment.title);
appendText(attachment.text);
appendText(attachment.footer);
if (attachment.fields) {
for (const field of attachment.fields) {
appendText(field.title);
appendText(field.value);
}
}
}
}
} catch (e) {
// eslint-disable-next-line no-console
console.log('Could not process the whole attachment for mentions', e);
}
const allMentions = getAllUserMentionKeys(state);
const ignoreChannelMentionProp = member?.notify_props?.ignore_channel_mentions || IgnoreChannelMentions.DEFAULT;
let ignoreChannelMention = ignoreChannelMentionProp === IgnoreChannelMentions.ON;
if (ignoreChannelMentionProp === IgnoreChannelMentions.DEFAULT) {
ignoreChannelMention = user?.notify_props?.channel === 'false';
}
const mentionableText = formatWithRenderer(text, new MentionableRenderer());
let isExplicitlyMentioned = false;
for (const mention of allMentions) {
if (!mention || !mention.key) {
continue;
}
if (ignoreChannelMention && ['@all', '@here', '@channel'].includes(mention.key)) {
continue;
}
let flags = 'g';
if (!mention.caseSensitive) {
flags += 'i';
}
let pattern;
if (cjkrPattern.test(mention.key)) {
// In the case of CJK mention key, even if there's no delimiters (such as spaces) at both ends of a word, it is recognized as a mention key
pattern = new RegExp(`()(${escapeRegex(mention.key)})()`, flags);
} else {
pattern = new RegExp(
`(^|\\W)(${escapeRegex(mention.key)})(\\b|_+\\b)`,
flags,
);
}
if (pattern.test(mentionableText)) {
isExplicitlyMentioned = true;
break;
}
}
if (!isExplicitlyMentioned) {
return;
}
} else if (notifyLevel === NotificationLevels.MENTION && mentions.indexOf(user.id) === -1 && msgProps.channel_type !== Constants.DM_CHANNEL) {
return;
} else if (isCrtReply && notifyLevel === NotificationLevels.ALL && followers.indexOf(currentUserId) === -1) {

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

@@ -36,6 +36,9 @@ describe('notification_actions', () => {
desktop: NotificationLevels.ALL,
desktop_sound: false,
desktop_threads: NotificationLevels.ALL,
mention_keys: 'mentionkey',
first_name: 'true',
channel: 'true',
};
post = {
@@ -79,8 +82,13 @@ describe('notification_actions', () => {
current_user_id: {
id: 'current_user_id',
notify_props: userSettings,
username: 'currentusername',
first_name: 'currentuserfirstname',
},
},
profilesInChannel: {
gm_channel: new Set(['current_user_id']),
},
},
teams: {
currentTeamId: 'team_id',
@@ -109,12 +117,20 @@ describe('notification_actions', () => {
id: 'another_channel_id',
team_id: 'team_id',
},
gm_channel: {
id: 'gm_channel',
type: 'G',
},
},
myMembers: {
channel_id: {
id: 'current_user_id',
notify_props: channelSettings,
},
gm_channel: {
id: 'gm_channel',
notify_props: channelSettings,
},
},
membersInChannel: {
channel_id: {
@@ -123,6 +139,12 @@ describe('notification_actions', () => {
notify_props: channelSettings,
},
},
gm_channel: {
current_user_id: {
id: 'gm_channel',
notify_props: channelSettings,
},
},
muted_channel_id: {
current_user_id: {
id: 'current_user_id',
@@ -138,6 +160,10 @@ describe('notification_actions', () => {
'display_settings--collapsed_reply_threads': crt,
},
},
groups: {
groups: {},
myGroups: [],
},
},
views: {
browser: {
@@ -401,5 +427,85 @@ describe('notification_actions', () => {
});
});
});
describe('GMs', () => {
test('should notify for any message when channel setting is DEFAULT and user setting is MENTION', async () => {
const store = testConfigureStore(baseState);
userSettings.desktop = NotificationLevels.MENTION;
channelSettings.desktop = NotificationLevels.DEFAULT;
post.channel_id = 'gm_channel';
msgProps.team_id = '';
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
expect(spy).toHaveBeenCalled();
});
});
test('should not notify for any message when channel setting is DEFAULT and user setting is NONE', async () => {
const store = testConfigureStore(baseState);
userSettings.desktop = NotificationLevels.NONE;
channelSettings.desktop = NotificationLevels.DEFAULT;
post.message = '@username';
post.channel_id = 'gm_channel';
msgProps.team_id = '';
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
expect(spy).not.toHaveBeenCalled();
});
});
test('should notify when channel setting MENTION and there is a explicit mention', async () => {
const store = testConfigureStore(baseState);
channelSettings.desktop = NotificationLevels.MENTION;
post.message = '@currentusername';
post.channel_id = 'gm_channel';
msgProps.team_id = '';
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
expect(spy).toHaveBeenCalled();
});
});
test('should notify when channel setting MENTION and there is a keyword mention', async () => {
const store = testConfigureStore(baseState);
channelSettings.desktop = NotificationLevels.MENTION;
post.message = 'mentionkey';
post.channel_id = 'gm_channel';
msgProps.team_id = '';
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
expect(spy).toHaveBeenCalled();
});
});
test('should notify when channel setting MENTION and there is the first name', async () => {
const store = testConfigureStore(baseState);
channelSettings.desktop = NotificationLevels.MENTION;
post.message = 'currentuserfirstname';
post.channel_id = 'gm_channel';
msgProps.team_id = '';
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
expect(spy).toHaveBeenCalled();
});
});
test('should notify when channel setting MENTION and there is a channel mention', async () => {
const store = testConfigureStore(baseState);
channelSettings.desktop = NotificationLevels.MENTION;
post.message = '@all';
post.channel_id = 'gm_channel';
msgProps.team_id = '';
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
expect(spy).toHaveBeenCalled();
});
});
test('should not notify when channel setting MENTION and there is no explicit mention', async () => {
const store = testConfigureStore(baseState);
channelSettings.desktop = NotificationLevels.MENTION;
post.channel_id = 'gm_channel';
msgProps.team_id = '';
return store.dispatch(sendDesktopNotification(post, msgProps)).then(() => {
expect(spy).not.toHaveBeenCalled();
});
});
});
});
});

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

@@ -70,6 +70,7 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
/>
<NotificationSection
expand={false}
isGM={false}
memberNotificationLevel="all"
onChange={[Function]}
onSubmit={[Function]}
@@ -83,6 +84,7 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
<NotificationSection
expand={false}
ignoreChannelMentions="off"
isGM={false}
memberNotificationLevel="all"
onChange={[Function]}
onSubmit={[Function]}
@@ -98,6 +100,7 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
expand={false}
globalNotificationLevel="all"
globalNotificationSound="Bing"
isGM={false}
isNotificationsSettingSameAsGlobal={true}
memberDesktopNotificationSound="Bing"
memberDesktopSound="on"
@@ -118,6 +121,8 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
/>
<NotificationSection
expand={false}
globalNotificationLevel="all"
isGM={false}
isNotificationsSettingSameAsGlobal={true}
memberNotificationLevel="all"
memberThreadsNotificationLevel="all"
@@ -137,6 +142,7 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
channelAutoFollowThreads="off"
expand={false}
ignoreChannelMentions="off"
isGM={false}
memberNotificationLevel="all"
onChange={[Function]}
onSubmit={[Function]}
@@ -153,3 +159,148 @@ exports[`components/channel_notifications_modal/ChannelNotificationsModal should
</ModalBody>
</Modal>
`;
exports[`components/channel_notifications_modal/ChannelNotificationsModal should match snapshot for GMs 1`] = `
<Modal
animation={true}
aria-labelledby="channelNotificationModalLabel"
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal settings-modal settings-modal--tabless"
dialogComponentClass={[Function]}
enforceFocus={true}
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
role="dialog"
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
id="channelNotificationModalLabel"
>
<MemoizedFormattedMessage
defaultMessage="Notification Preferences for "
id="channel_notifications.preferences"
/>
<span
className="name"
>
channel_display_name
</span>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<div
className="settings-table"
>
<div
className="settings-content"
>
<div
className="user-settings"
>
<br />
<div
className="divider-dark first"
/>
<NotificationSection
expand={false}
isGM={true}
memberNotificationLevel="all"
onChange={[Function]}
onSubmit={[Function]}
onUpdateSection={[Function]}
section="markUnread"
serverError={null}
/>
<div
className="divider-light"
/>
<NotificationSection
expand={false}
ignoreChannelMentions="off"
isGM={true}
memberNotificationLevel="all"
onChange={[Function]}
onSubmit={[Function]}
onUpdateSection={[Function]}
section="ignoreChannelMentions"
serverError={null}
/>
<div>
<div
className="divider-light"
/>
<NotificationSection
expand={false}
globalNotificationLevel="all"
globalNotificationSound="Bing"
isGM={true}
isNotificationsSettingSameAsGlobal={false}
memberDesktopNotificationSound="Bing"
memberDesktopSound="on"
memberNotificationLevel="mention"
memberThreadsNotificationLevel="all"
onChange={[Function]}
onChangeDesktopSound={[Function]}
onChangeNotificationSound={[Function]}
onChangeThreads={[Function]}
onReset={[Function]}
onSubmit={[Function]}
onUpdateSection={[Function]}
section="desktop"
serverError={null}
/>
<div
className="divider-light"
/>
<NotificationSection
expand={false}
globalNotificationLevel="all"
isGM={true}
isNotificationsSettingSameAsGlobal={false}
memberNotificationLevel="mention"
memberThreadsNotificationLevel="all"
onChange={[Function]}
onChangeThreads={[Function]}
onReset={[Function]}
onSubmit={[Function]}
onUpdateSection={[Function]}
section="push"
serverError={null}
/>
</div>
<div
className="divider-dark"
/>
</div>
</div>
</div>
</ModalBody>
</Modal>
`;

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

@@ -54,6 +54,37 @@ describe('components/channel_notifications_modal/ChannelNotificationsModal', ()
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for GMs', () => {
const wrapper = shallow(
<ChannelNotificationsModal
{...{
...baseProps,
channel: TestHelper.getChannelMock({
id: 'channel_id',
display_name: 'channel_display_name',
type: 'G',
}),
channelMember: {
notify_props: {
...baseProps.channelMember!.notify_props,
desktop: NotificationLevels.MENTION,
push: NotificationLevels.MENTION,
},
} as unknown as ChannelMembership,
currentUser: TestHelper.getUserMock({
id: 'current_user_id',
notify_props: {
desktop: NotificationLevels.MENTION,
desktop_threads: NotificationLevels.ALL,
} as UserNotifyProps,
}),
}}
/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should provide default notify props when missing', () => {
const wrapper = shallow(
<ChannelNotificationsModal

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

@@ -56,9 +56,13 @@ type State = {
export type DesktopNotificationProps = Pick<State, 'desktopNotifyLevel' | 'desktopNotifySound' | 'desktopSound' | 'desktopThreadsNotifyLevel'>
export type PushNotificationProps = Pick<State, 'pushNotifyLevel' | 'pushThreadsNotifyLevel'>
const getDefaultDesktopNotificationLevel = (currentUserNotifyProps: UserNotifyProps): Exclude<ChannelMemberNotifyProps['desktop'], undefined> => {
const getDefaultDesktopNotificationLevel = (currentUserNotifyProps: UserNotifyProps, isGM: boolean): Exclude<ChannelMemberNotifyProps['desktop'], undefined> => {
if (currentUserNotifyProps?.desktop) {
if (currentUserNotifyProps.desktop === 'default') {
if (currentUserNotifyProps.desktop === NotificationLevels.DEFAULT) {
return NotificationLevels.ALL;
}
if (isGM && currentUserNotifyProps.desktop === NotificationLevels.MENTION) {
return NotificationLevels.ALL;
}
return currentUserNotifyProps.desktop;
@@ -86,11 +90,16 @@ const getDefaultDesktopThreadsNotifyLevel = (currentUserNotifyProps: UserNotifyP
return NotificationLevels.ALL;
};
const getDefaultPushNotifyLevel = (currentUserNotifyProps: UserNotifyProps): Exclude<ChannelMemberNotifyProps['push'], undefined> => {
const getDefaultPushNotifyLevel = (currentUserNotifyProps: UserNotifyProps, isGM: boolean): Exclude<ChannelMemberNotifyProps['push'], undefined> => {
if (currentUserNotifyProps?.push) {
if (currentUserNotifyProps.push === 'default') {
if (currentUserNotifyProps.push === NotificationLevels.DEFAULT) {
return NotificationLevels.ALL;
}
if (isGM && currentUserNotifyProps.desktop === NotificationLevels.MENTION) {
return NotificationLevels.ALL;
}
return currentUserNotifyProps.push;
}
return NotificationLevels.ALL;
@@ -142,7 +151,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
const currentUserNotifyProps = this.props.currentUser.notify_props;
if (
desktopNotifyLevel === getDefaultDesktopNotificationLevel(currentUserNotifyProps) &&
desktopNotifyLevel === getDefaultDesktopNotificationLevel(currentUserNotifyProps, this.isGM()) &&
desktopNotifySound === getDefaultDesktopNotificationSound(currentUserNotifyProps) &&
desktopSound === getDefaultDesktopSound(currentUserNotifyProps) &&
desktopThreadsNotifyLevel === getDefaultDesktopThreadsNotifyLevel(currentUserNotifyProps)
@@ -152,6 +161,10 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
return false;
}
isGM() {
return this.props.channel.type === 'G';
}
verifyPushNotificationsSettingSameAsGlobal({
pushNotifyLevel,
pushThreadsNotifyLevel,
@@ -159,7 +172,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
const currentUserNotifyProps = this.props.currentUser.notify_props;
if (
pushNotifyLevel === getDefaultPushNotifyLevel(currentUserNotifyProps) &&
pushNotifyLevel === getDefaultPushNotifyLevel(currentUserNotifyProps, this.isGM()) &&
pushThreadsNotifyLevel === getDefaultPushThreadsNotifyLevel(currentUserNotifyProps)
) {
return true;
@@ -170,13 +183,15 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
getStateFromNotifyProps(currentUserNotifyProps: UserNotifyProps, channelMemberNotifyProps?: ChannelMemberNotifyProps) {
let ignoreChannelMentionsDefault: ChannelNotifyProps['ignore_channel_mentions'] = IgnoreChannelMentions.OFF;
let desktopNotifyLevelDefault: ChannelNotifyProps['desktop'] = getDefaultDesktopNotificationLevel(currentUserNotifyProps);
let pushNotifyLevelDefault: ChannelMemberNotifyProps['push'] = getDefaultPushNotifyLevel(currentUserNotifyProps);
let desktopNotifyLevelDefault: ChannelNotifyProps['desktop'] = getDefaultDesktopNotificationLevel(currentUserNotifyProps, this.isGM());
let pushNotifyLevelDefault: ChannelMemberNotifyProps['push'] = getDefaultPushNotifyLevel(currentUserNotifyProps, this.isGM());
let pushThreadsNotifyLevelDefault: ChannelMemberNotifyProps['push_threads'] = getDefaultPushThreadsNotifyLevel(currentUserNotifyProps);
if (channelMemberNotifyProps?.desktop) {
if (channelMemberNotifyProps.desktop !== 'default') {
desktopNotifyLevelDefault = channelMemberNotifyProps.desktop;
} else if (this.isGM()) {
desktopNotifyLevelDefault = NotificationLevels.ALL;
}
}
if (channelMemberNotifyProps?.push) {
@@ -187,6 +202,8 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
if (channelMemberNotifyProps?.push_threads) {
if (channelMemberNotifyProps.push_threads !== 'default') {
pushThreadsNotifyLevelDefault = channelMemberNotifyProps.push_threads;
} else if (this.isGM()) {
pushThreadsNotifyLevelDefault = NotificationLevels.ALL;
}
}
@@ -246,7 +263,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
const currentUserNotifyProps = this.props.currentUser.notify_props;
const userDesktopNotificationDefaults = {
desktopNotifyLevel: getDefaultDesktopNotificationLevel(currentUserNotifyProps),
desktopNotifyLevel: getDefaultDesktopNotificationLevel(currentUserNotifyProps, this.isGM()),
desktopSound: getDefaultDesktopSound(currentUserNotifyProps),
desktopNotifySound: getDefaultDesktopNotificationSound(currentUserNotifyProps),
desktopThreadsNotifyLevel: getDefaultDesktopThreadsNotifyLevel(currentUserNotifyProps),
@@ -259,7 +276,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
const currentUserNotifyProps = this.props.currentUser.notify_props;
const userPushNotificationDefaults = {
pushNotifyLevel: getDefaultPushNotifyLevel(currentUserNotifyProps),
pushNotifyLevel: getDefaultPushNotifyLevel(currentUserNotifyProps, this.isGM()),
pushThreadsNotifyLevel: getDefaultPushThreadsNotifyLevel(currentUserNotifyProps),
};
@@ -400,6 +417,8 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
serverErrorTag = <div className='form-group has-error'><label className='control-label'>{serverError}</label></div>;
}
const isGM = this.isGM();
return (
<Modal
dialogClassName='a11y__modal settings-modal settings-modal--tabless'
@@ -435,6 +454,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
onSubmit={this.handleSubmitMarkUnreadLevel}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
<div className='divider-light'/>
<NotificationSection
@@ -446,6 +466,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
onSubmit={this.handleSubmitIgnoreChannelMentions}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
{!isChannelMuted(channelMember) &&
<div>
@@ -457,8 +478,8 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
memberThreadsNotificationLevel={desktopThreadsNotifyLevel}
memberDesktopSound={desktopSound}
memberDesktopNotificationSound={desktopNotifySound}
globalNotificationLevel={currentUser.notify_props ? currentUser.notify_props.desktop : NotificationLevels.ALL}
globalNotificationSound={(currentUser.notify_props && currentUser.notify_props.desktop_notification_sound) ? currentUser.notify_props.desktop_notification_sound : 'Bing'}
globalNotificationLevel={getDefaultDesktopNotificationLevel(currentUser.notify_props, isGM)}
globalNotificationSound={getDefaultDesktopNotificationSound(currentUser.notify_props)}
isNotificationsSettingSameAsGlobal={isNotificationsSettingSameAsGlobal}
onChange={this.handleUpdateDesktopNotifyLevel}
onChangeThreads={this.handleUpdateDesktopThreadsNotifyLevel}
@@ -468,6 +489,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
onSubmit={this.handleSubmitDesktopNotification}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
<div className='divider-light'/>
{sendPushNotifications &&
@@ -476,7 +498,7 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
expand={activeSection === NotificationSections.PUSH}
memberNotificationLevel={pushNotifyLevel}
memberThreadsNotificationLevel={pushThreadsNotifyLevel}
globalNotificationLevel={currentUser.notify_props ? currentUser.notify_props.push : NotificationLevels.ALL}
globalNotificationLevel={getDefaultPushNotifyLevel(currentUser.notify_props, isGM)}
isNotificationsSettingSameAsGlobal={isPushNotificationsSettingSameAsGlobal}
onChange={this.handleUpdatePushNotificationLevel}
onReset={this.handleResetPushNotification}
@@ -484,22 +506,28 @@ export default class ChannelNotificationsModal extends React.PureComponent<Props
onSubmit={this.handleSubmitPushNotificationLevel}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
}
</div>
}
<div className='divider-light'/>
<NotificationSection
section={NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
expand={activeSection === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
memberNotificationLevel={markUnreadNotifyLevel}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
onChange={this.handleUpdateChannelAutoFollowThreads}
onSubmit={this.handleSubmitChannelAutoFollowThreads}
onUpdateSection={this.updateSection}
serverError={serverError}
/>
{!isGM &&
<>
<div className='divider-light'/>
<NotificationSection
section={NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
expand={activeSection === NotificationSections.CHANNEL_AUTO_FOLLOW_THREADS}
memberNotificationLevel={markUnreadNotifyLevel}
ignoreChannelMentions={ignoreChannelMentions}
channelAutoFollowThreads={channelAutoFollowThreads}
onChange={this.handleUpdateChannelAutoFollowThreads}
onSubmit={this.handleSubmitChannelAutoFollowThreads}
onUpdateSection={this.updateSection}
serverError={serverError}
isGM={isGM}
/>
</>
}
<div className='divider-dark'/>
</div>
</div>

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

@@ -2,7 +2,7 @@
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on DESKTOP/PUSH & ALL 1`] = `
<MemoizedFormattedMessage
defaultMessage="For all activity ({isDefault})"
defaultMessage="For all activity {isDefault}"
id="channel_notifications.allActivity"
values={
Object {
@@ -21,7 +21,7 @@ exports[`components/channel_notifications_modal/NotificationSection should match
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on MENTION 1`] = `
<MemoizedFormattedMessage
defaultMessage="Only for mentions ({isDefault})"
defaultMessage="Only for mentions {isDefault}"
id="channel_notifications.onlyMentions"
values={
Object {
@@ -33,7 +33,7 @@ exports[`components/channel_notifications_modal/NotificationSection should match
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, on NONE 1`] = `
<MemoizedFormattedMessage
defaultMessage="Never ({isDefault})"
defaultMessage="Never {isDefault}"
id="channel_notifications.never"
values={
Object {

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

@@ -1,6 +1,269 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/ExpandView should match snapshot, DESKTOP on expanded view 1`] = `
exports[`components/channel_notifications_modal/ExpandView gms should match snapshot, DESKTOP on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send desktop notifications"
id="channel_notifications.sendDesktop"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="desktop"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="desktop"
/>
</div>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sound"
id="channel_notifications.sound"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelDesktopSoundOn"
name="channelDesktopSound"
type="radio"
value="on"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="On"
id="channel_notifications.sound.on.title"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelDesktopSoundOff"
name="channelDesktopSound"
type="radio"
value="off"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="channel_notifications.sound.off.title"
/>
</label>
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps."
id="channel_notifications.sound_info"
/>
</div>
</fieldset>
</React.Fragment>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="desktop"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView gms should match snapshot, PUSH on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send mobile push notifications"
id="channel_notifications.sendMobilePush"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="push"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="push"
/>
</div>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="push"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, DESKTOP on expanded view 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
@@ -160,7 +423,206 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot
/>
`;
exports[`components/channel_notifications_modal/ExpandView should match snapshot, MARK_UNREAD on expanded view 1`] = `
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, DESKTOP on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send desktop notifications"
id="channel_notifications.sendDesktop"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="desktop"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="desktop"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="desktop"
/>
</div>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Thread reply notifications"
id="user.settings.notifications.threads.desktop"
/>
</legend>
<div
className="checkbox"
>
<label>
<input
checked={true}
id="desktopThreadsNotificationAllActivity"
name="desktopThreadsNotificationLevel"
onChange={[MockFunction]}
type="checkbox"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notify me about threads I'm following"
id="user.settings.notifications.threads.allActivity"
/>
</label>
<br />
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="When enabled, any reply to a thread you're following will send a desktop notification."
id="user.settings.notifications.threads"
/>
</div>
</fieldset>
</React.Fragment>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sound"
id="channel_notifications.sound"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelDesktopSoundOn"
name="channelDesktopSound"
type="radio"
value="on"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="On"
id="channel_notifications.sound.on.title"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelDesktopSoundOff"
name="channelDesktopSound"
type="radio"
value="off"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
id="channel_notifications.sound.off.title"
/>
</label>
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notification sounds are available on Firefox, Edge, Safari, Chrome and Mattermost Desktop Apps."
id="channel_notifications.sound_info"
/>
</div>
</fieldset>
</React.Fragment>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="desktop"
/>
}
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, MARK_UNREAD on expanded view 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
@@ -234,7 +696,7 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot
/>
`;
exports[`components/channel_notifications_modal/ExpandView should match snapshot, PUSH on expanded view 1`] = `
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, PUSH on expanded view 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
@@ -336,3 +798,145 @@ exports[`components/channel_notifications_modal/ExpandView should match snapshot
updateSection={[MockFunction]}
/>
`;
exports[`components/channel_notifications_modal/ExpandView normal channels should match snapshot, PUSH on expanded view when mentions is selected 1`] = `
<SettingItemMax
containerStyle=""
infoPosition="bottom"
inputs={
Array [
<div>
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Send mobile push notifications"
id="channel_notifications.sendMobilePush"
/>
</legend>
<div
className="radio"
>
<label
className=""
>
<input
checked={false}
id="channelNotificationAllActivity"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="all"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="all"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label
className=""
>
<input
checked={true}
id="channelNotificationMentions"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="mention"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="mention"
section="push"
/>
</label>
</div>
<div
className="radio"
>
<label>
<input
checked={false}
id="channelNotificationNever"
name="channelNotifications"
onChange={[MockFunction]}
type="radio"
value="none"
/>
<Describe
globalNotifyLevel="default"
memberNotifyLevel="none"
section="push"
/>
</label>
</div>
</fieldset>
<div
className="mt-5"
>
<ExtraInfo
section="push"
/>
</div>
<React.Fragment>
<hr />
<fieldset>
<legend
className="form-legend"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Thread reply notifications"
id="user.settings.notifications.threads.push"
/>
</legend>
<div
className="checkbox"
>
<label>
<input
checked={true}
id="pushThreadsNotificationAllActivity"
name="pushThreadsNotificationLevel"
onChange={[MockFunction]}
type="checkbox"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Notify me about threads I'm following"
id="user.settings.notifications.push_threads.allActivity"
/>
</label>
<br />
</div>
<div
className="mt-5"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="When enabled, any reply to a thread you're following will send a mobile push notification."
id="user.settings.notifications.push_threads"
/>
</div>
</fieldset>
</React.Fragment>
</div>,
]
}
saving={false}
section=""
serverError=""
submit={[MockFunction]}
title={
<SectionTitle
isExpanded={true}
onClickResetButton={[MockFunction]}
section="push"
/>
}
updateSection={[MockFunction]}
/>
`;

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

@@ -21,6 +21,7 @@ exports[`components/channel_notifications_modal/NotificationSection should match
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, DESKTOP on expanded view 1`] = `
<ExpandView
globalNotifyLevel="default"
isGM={false}
memberNotifyLevel="all"
memberThreadsNotifyLevel="all"
onChange={[Function]}
@@ -47,6 +48,7 @@ exports[`components/channel_notifications_modal/NotificationSection should match
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, MARK_UNREAD on expanded view 1`] = `
<ExpandView
globalNotifyLevel={null}
isGM={false}
memberNotifyLevel="all"
memberThreadsNotifyLevel="all"
onChange={[Function]}
@@ -73,6 +75,7 @@ exports[`components/channel_notifications_modal/NotificationSection should match
exports[`components/channel_notifications_modal/NotificationSection should match snapshot, PUSH on expanded view 1`] = `
<ExpandView
globalNotifyLevel="default"
isGM={false}
memberNotifyLevel="all"
memberThreadsNotifyLevel="all"
onChange={[Function]}

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

@@ -102,7 +102,7 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa
return (
<FormattedMessage
id='channel_notifications.onlyMentions'
defaultMessage='Only for mentions ({isDefault})'
defaultMessage='Only for mentions {isDefault}'
values={{isDefault: globalNotifyLevel === NotificationLevels.MENTION ? defaultOption : <></>}}
/>
);
@@ -113,7 +113,7 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa
return (
<FormattedMessage
id='channel_notifications.allActivity'
defaultMessage='For all activity ({isDefault})'
defaultMessage='For all activity {isDefault}'
values={{isDefault: globalNotifyLevel === NotificationLevels.ALL ? defaultOption : <></>}}
/>
);
@@ -132,7 +132,7 @@ export default function Describe({section, isCollapsed, memberNotifyLevel, globa
return (
<FormattedMessage
id='channel_notifications.never'
defaultMessage='Never ({isDefault})'
defaultMessage='Never {isDefault}'
values={{isDefault: globalNotifyLevel === NotificationLevels.NONE ? defaultOption : <></>}}
/>
);

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

@@ -10,7 +10,7 @@ import {NotificationLevels, NotificationSections} from 'utils/constants';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
useSelector: jest.fn(() => true),
}));
describe('components/channel_notifications_modal/ExpandView', () => {
@@ -25,31 +25,72 @@ describe('components/channel_notifications_modal/ExpandView', () => {
onCollapseSection: jest.fn(),
onSubmit: jest.fn(),
onReset: jest.fn(),
isGM: false,
};
test('should match snapshot, DESKTOP on expanded view', () => {
const wrapper = shallow(
<ExpandView {...baseProps}/>,
);
describe('normal channels', () => {
test('should match snapshot, DESKTOP on expanded view', () => {
const wrapper = shallow(
<ExpandView {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on expanded view', () => {
const props = {...baseProps, section: NotificationSections.PUSH};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, MARK_UNREAD on expanded view', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, DESKTOP on expanded view when mentions is selected', () => {
const props = {...baseProps, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, PUSH on expanded view when mentions is selected', () => {
const props = {...baseProps, section: NotificationSections.PUSH, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
});
test('should match snapshot, PUSH on expanded view', () => {
const props = {...baseProps, section: NotificationSections.PUSH};
const wrapper = shallow(
<ExpandView {...props}/>,
);
describe('gms', () => {
test('should match snapshot, DESKTOP on expanded view when mentions is selected', () => {
const props = {...baseProps, isGM: true, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, MARK_UNREAD on expanded view', () => {
const props = {...baseProps, section: NotificationSections.MARK_UNREAD};
const wrapper = shallow(
<ExpandView {...props}/>,
);
test('should match snapshot, PUSH on expanded view when mentions is selected', () => {
const props = {...baseProps, section: NotificationSections.PUSH, isGM: true, memberNotifyLevel: NotificationLevels.MENTION};
const wrapper = shallow(
<ExpandView {...props}/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});
});

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

@@ -45,6 +45,7 @@ type Props = {
memberDesktopNotificationSound?: string;
section: string;
serverError?: string;
isGM: boolean;
}
const sounds = Array.from(notificationSounds.keys());
@@ -74,6 +75,7 @@ export default function ExpandView({
onCollapseSection,
ignoreChannelMentions,
channelAutoFollowThreads,
isGM,
}: Props) {
const isCRTEnabled = useSelector(isCollapsedThreadsEnabled);
@@ -275,6 +277,7 @@ export default function ExpandView({
{isCRTEnabled &&
section === NotificationSections.DESKTOP &&
memberNotifyLevel === NotificationLevels.MENTION &&
!isGM &&
<>
<hr/>
<fieldset>
@@ -378,6 +381,7 @@ export default function ExpandView({
{isCRTEnabled &&
section === NotificationSections.PUSH &&
memberNotifyLevel === NotificationLevels.MENTION &&
!isGM &&
<>
<hr/>
<fieldset>

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

@@ -88,6 +88,11 @@ export default class NotificationSection extends React.PureComponent {
* Error string from the server
*/
serverError: PropTypes.string,
/**
* Whether the preferences are those of a GM
*/
isGM: PropTypes.bool,
};
handleOnChange = (e) => {
@@ -134,6 +139,7 @@ export default class NotificationSection extends React.PureComponent {
onReset,
section,
serverError,
isGM,
} = this.props;
if (expand) {
@@ -157,6 +163,7 @@ export default class NotificationSection extends React.PureComponent {
onSubmit={onSubmit}
serverError={serverError}
onCollapseSection={this.handleCollapseSection}
isGM={isGM}
/>
);
}

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

@@ -21,6 +21,7 @@ describe('components/channel_notifications_modal/NotificationSection', () => {
onSubmit: () => {}, //eslint-disable-line no-empty-function
onUpdateSection: () => {}, //eslint-disable-line no-empty-function
serverError: '',
isGM: false,
};
test('should match snapshot, DESKTOP on collapsed view', () => {

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

@@ -32,6 +32,7 @@ describe('components/post_view/ChannelIntroMessages', () => {
const users = [
{id: 'user1', roles: 'system_user'},
{id: 'guest1', roles: 'system_guest'},
{id: 'test-user-id', roles: 'system_user'},
] as UserProfile[];
const baseProps = {
@@ -153,6 +154,9 @@ describe('components/post_view/ChannelIntroMessages', () => {
expect(editIcon).toBeInTheDocument();
expect(editIcon).toHaveClass('icon-pencil-outline');
const notificationPreferencesButton = screen.getByText('Notification Preferences');
expect(notificationPreferencesButton).toBeInTheDocument();
});
});

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

@@ -4,12 +4,14 @@
import React from 'react';
import {FormattedDate, FormattedMessage} from 'react-intl';
import {BellRingOutlineIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile as UserProfileRedux} from '@mattermost/types/users';
import type {UserProfile as UserProfileType} from '@mattermost/types/users';
import {Permissions} from 'mattermost-redux/constants';
import AddGroupsToTeamModal from 'components/add_groups_to_team_modal';
import ChannelNotificationsModal from 'components/channel_notifications_modal';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import LocalizedIcon from 'components/localized_icon';
@@ -32,12 +34,12 @@ type Props = {
channel: Channel;
fullWidth: boolean;
locale: string;
channelProfiles: UserProfileRedux[];
channelProfiles: UserProfileType[];
enableUserCreation?: boolean;
isReadOnly?: boolean;
teamIsGroupConstrained?: boolean;
creatorName: string;
teammate?: UserProfileRedux;
teammate?: UserProfileType;
teammateName?: string;
stats: any;
usersLimit: number;
@@ -89,10 +91,11 @@ export default class ChannelIntroMessage extends React.PureComponent<Props> {
}
}
function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles: UserProfileRedux[], currentUserId: string) {
function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles: UserProfileType[], currentUserId: string) {
const channelIntroId = 'channelIntro';
if (profiles.length > 0) {
const currentUserProfile = profiles.find((v) => v.id === currentUserId);
const pictures = profiles.
filter((profile) => profile.id !== currentUserId).
map((profile) => (
@@ -114,16 +117,21 @@ function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles:
{pictures}
</div>
<p className='channel-intro-text'>
<FormattedMarkdownMessage
<FormattedMessage
id='intro_messages.GM'
defaultMessage='This is the start of your group message history with {names}.\nMessages and files shared here are not shown to people outside this area.'
defaultMessage={'This is the start of your group message history with {names}.{br}You\'ll be notified <b>for all activity</b> in this group message.'}
values={{
b: (chunks) => <b>{chunks}</b>,
names: channel.display_name,
br: <br/>,
}}
/>
</p>
<PluggableIntroButtons channel={channel}/>
{createSetHeaderButton(channel)}
<div style={{display: 'flex'}}>
{createNotificationPreferencesButton(channel, currentUserProfile)}
<PluggableIntroButtons channel={channel}/>
{createSetHeaderButton(channel)}
</div>
</div>
);
}
@@ -143,7 +151,7 @@ function createGMIntroMessage(channel: Channel, centeredIntro: string, profiles:
);
}
function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?: UserProfileRedux, teammateName?: string) {
function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?: UserProfileType, teammateName?: string) {
const channelIntroId = 'channelIntro';
if (teammate) {
const src = teammate ? Utils.imageURLForUser(teammate.id, teammate.last_picture_update) : '';
@@ -185,8 +193,10 @@ function createDMIntroMessage(channel: Channel, centeredIntro: string, teammate?
}}
/>
</p>
{pluggableButton}
{setHeaderButton}
<div style={{display: 'flex'}}>
{pluggableButton}
{setHeaderButton}
</div>
</div>
);
}
@@ -555,3 +565,26 @@ function createSetHeaderButton(channel: Channel) {
</ToggleModalButton>
);
}
function createNotificationPreferencesButton(channel: Channel, currentUser?: UserProfileType) {
const isGM = channel.type === 'G';
if (!isGM || !currentUser) {
return null;
}
return (
<ToggleModalButton
modalId={ModalIdentifiers.CHANNEL_NOTIFICATIONS}
ariaLabel={Utils.localizeMessage('intro_messages.notificationPreferences', 'Notification Preferences')}
className={'intro-links color--link channelIntroButton'}
dialogType={ChannelNotificationsModal}
dialogProps={{channel, currentUser}}
>
<BellRingOutlineIcon size={16}/>
<FormattedMessage
id='intro_messages.notificationPreferences'
defaultMessage='Notification Preferences'
/>
</ToggleModalButton>
);
}

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

@@ -5,64 +5,60 @@ exports[`components/SystemNotice should match snapshot for admin, admin notice 1
className="system-notice bg--white shadow--2"
>
<div
className="system-notice__header"
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__body"
>
<div
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__title"
>
some title
</div>
</div>
<div
className="system-notice__body"
>
some body
</div>
<div
className="system-notice__info"
>
<LocalizedIcon
className="fa fa-eye"
title={
Object {
"defaultMessage": "Only visible to System Admins Icon",
"id": "system_notice.adminVisible.icon",
<div
className="system-notice__info"
>
<LocalizedIcon
className="fa fa-eye"
title={
Object {
"defaultMessage": "Only visible to System Admins Icon",
"id": "system_notice.adminVisible.icon",
}
}
}
/>
<MemoizedFormattedMessage
defaultMessage="Only visible to System Admins"
id="system_notice.adminVisible"
/>
</div>
<div
className="system-notice__footer"
>
<button
className="btn btn-transparent"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn btn-transparent"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
defaultMessage="Only visible to System Admins"
id="system_notice.adminVisible"
/>
</button>
</div>
<div
className="system-notice__footer"
>
<button
className="btn btn-primary"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
</div>
</div>
</div>
`;
@@ -72,47 +68,43 @@ exports[`components/SystemNotice should match snapshot for admin, regular notice
className="system-notice bg--white shadow--2"
>
<div
className="system-notice__header"
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__body"
>
<div
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__title"
>
some title
</div>
</div>
<div
className="system-notice__body"
>
some body
</div>
<div
className="system-notice__footer"
>
<button
className="btn btn-transparent"
id="systemnotice_remindme"
onClick={[Function]}
<div
className="system-notice__footer"
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn btn-transparent"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
<button
className="btn btn-primary"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
</div>
</div>
</div>
`;
@@ -122,47 +114,43 @@ exports[`components/SystemNotice should match snapshot for regular user, admin a
className="system-notice bg--white shadow--2"
>
<div
className="system-notice__header"
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__body"
>
<div
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__title"
>
some title2
</div>
</div>
<div
className="system-notice__body"
>
some body2
</div>
<div
className="system-notice__footer"
>
<button
className="btn btn-transparent"
id="systemnotice_remindme"
onClick={[Function]}
<div
className="system-notice__footer"
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn btn-transparent"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
<button
className="btn btn-primary"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
</div>
</div>
</div>
`;
@@ -180,47 +168,43 @@ exports[`components/SystemNotice should match snapshot for regular user, regular
className="system-notice bg--white shadow--2"
>
<div
className="system-notice__header"
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__body"
>
<div
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__title"
>
some title
</div>
</div>
<div
className="system-notice__body"
>
some body
</div>
<div
className="system-notice__footer"
>
<button
className="btn btn-transparent"
id="systemnotice_remindme"
onClick={[Function]}
<div
className="system-notice__footer"
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn btn-transparent"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
<button
className="btn btn-primary"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
</div>
</div>
</div>
`;
@@ -232,47 +216,43 @@ exports[`components/SystemNotice should match snapshot for show function returni
className="system-notice bg--white shadow--2"
>
<div
className="system-notice__header"
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__body"
>
<div
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__title"
>
some title
</div>
</div>
<div
className="system-notice__body"
>
some body
</div>
<div
className="system-notice__footer"
>
<button
className="btn btn-transparent"
id="systemnotice_remindme"
onClick={[Function]}
<div
className="system-notice__footer"
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn btn-transparent"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
<button
className="btn btn-primary"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
</div>
</div>
</div>
`;
@@ -282,37 +262,81 @@ exports[`components/SystemNotice should match snapshot for with allowForget equa
className="system-notice bg--white shadow--2"
>
<div
className="system-notice__header"
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__body"
>
<div
className="system-notice__logo"
>
<MattermostLogo />
</div>
<div
className="system-notice__title"
>
some title
</div>
some body
<div
className="system-notice__footer"
>
<button
className="btn btn-primary"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
</div>
</div>
</div>
`;
exports[`components/SystemNotice should match snapshot when a custom icon is passed 1`] = `
<div
className="system-notice bg--white shadow--2"
>
<div
className="system-notice__logo"
>
<span>
icon
</span>
</div>
<div
className="system-notice__body"
>
some body
</div>
<div
className="system-notice__footer"
>
<button
className="btn btn-transparent"
id="systemnotice_remindme"
onClick={[Function]}
<div
className="system-notice__title"
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
some title
</div>
some body
<div
className="system-notice__footer"
>
<button
className="btn btn-primary"
id="systemnotice_remindme"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Remind Me Later"
id="system_notice.remind_me"
/>
</button>
<button
className="btn"
id="systemnotice_dontshow"
onClick={[Function]}
>
<MemoizedFormattedMessage
defaultMessage="Don't Show Again"
id="system_notice.dont_show"
/>
</button>
</div>
</div>
</div>
`;

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

@@ -11,6 +11,7 @@ import {getStandardAnalytics} from 'mattermost-redux/actions/admin';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {Permissions} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
@@ -55,6 +56,7 @@ function makeMapStateToProps() {
license,
serverVersion,
analytics,
currentChannel: getCurrentChannel(state),
};
};
}

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

@@ -7,8 +7,8 @@ import {FormattedMessage} from 'react-intl';
import ExternalLink from 'components/external_link';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import type {Notice} from 'components/system_notice/types';
import InfoIcon from 'components/widgets/icons/info_icon';
import mattermostIcon from 'images/icon50x50.png';
import {DocLinks} from 'utils/constants';
import * as ServerVersion from 'utils/server_version';
import * as UserAgent from 'utils/user_agent';
@@ -29,12 +29,11 @@ const notices: Notice[] = [
name: 'apiv3_deprecation',
adminOnly: true,
title: (
<FormattedMarkdownMessage
<FormattedMessage
id='system_notice.title'
defaultMessage='**Notice**\nfrom Mattermost'
defaultMessage='Notice from Mattermost'
/>
),
icon: mattermostIcon,
body: (
<FormattedMessage
id='system_notice.body.api3'
@@ -63,12 +62,11 @@ const notices: Notice[] = [
name: 'advanced_permissions',
adminOnly: true,
title: (
<FormattedMarkdownMessage
<FormattedMessage
id='system_notice.title'
defaultMessage='**Notice**\nfrom Mattermost'
defaultMessage='Notice from Mattermost'
/>
),
icon: mattermostIcon,
body: (
<FormattedMessage
id='system_notice.body.permissions'
@@ -103,12 +101,11 @@ const notices: Notice[] = [
name: 'ee_upgrade_advice',
adminOnly: true,
title: (
<FormattedMarkdownMessage
<FormattedMessage
id='system_notice.title'
defaultMessage='**Notice**\nfrom Mattermost'
defaultMessage='Notice from Mattermost'
/>
),
icon: mattermostIcon,
body: (
<FormattedMessage
id='system_notice.body.ee_upgrade_advice'
@@ -150,10 +147,9 @@ const notices: Notice[] = [
title: (
<FormattedMarkdownMessage
id='system_notice.title'
defaultMessage='**Notice**\nfrom Mattermost'
defaultMessage='Notice from Mattermost'
/>
),
icon: mattermostIcon,
allowForget: false,
body: (
<FormattedMessage
@@ -185,6 +181,30 @@ const notices: Notice[] = [
return true;
},
},
{
// This notice is marked as viewed by default for new users on the server.
// Any change on this notice should be handled also in the server side.
name: 'GMasDM',
allowForget: true,
title: (
<FormattedMessage
id='system_notice.title.gm_as_dm'
defaultMessage='Updates to Group Messages'
/>
),
icon: (<InfoIcon/>),
body: (
<FormattedMessage
id='system_noticy.body.gm_as_dm'
defaultMessage='You wil now be notified for all activity in your group messages along with a notification badge for every new message.{br}{br}You can configure this in notification preferences for each group message.'
values={{br: (<br/>)}}
/>
),
show: (serverVersion, config, license, analytics, currentChannel) => {
return currentChannel?.type === 'G';
},
},
];
export default notices;

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

@@ -6,15 +6,13 @@ import React from 'react';
import SystemNotice from 'components/system_notice/system_notice';
import mattermostIcon from 'images/icon50x50.png';
describe('components/SystemNotice', () => {
const baseProps = {
currentUserId: 'someid',
preferences: {},
dismissedNotices: {},
isSystemAdmin: false,
notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}],
notices: [{name: 'notice1', adminOnly: false, title: 'some title', body: 'some body', allowForget: true, show: () => true}],
serverVersion: '5.1',
license: {IsLicensed: 'true'},
config: {},
@@ -38,13 +36,17 @@ describe('components/SystemNotice', () => {
});
test('should match snapshot for regular user, admin notice', () => {
const props = {...baseProps, notices: [{name: 'notice1', adminOnly: true, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}]};
const props = {...baseProps, notices: [{...baseProps.notices[0], adminOnly: true}]};
const wrapper = shallow(<SystemNotice {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for regular user, admin and regular notice', () => {
const props = {...baseProps, notices: [{name: 'notice1', adminOnly: true, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true}, {name: 'notice2', adminOnly: false, title: 'some title2', icon: mattermostIcon, body: 'some body2', allowForget: true, show: () => true}]};
const props = {...baseProps,
notices: [
{...baseProps.notices[0], adminOnly: true},
{...baseProps.notices[0], name: 'notice2', title: 'some title2', body: 'some body2'},
]};
const wrapper = shallow(<SystemNotice {...props}/>);
expect(wrapper).toMatchSnapshot();
});
@@ -56,7 +58,7 @@ describe('components/SystemNotice', () => {
});
test('should match snapshot for admin, admin notice', () => {
const props = {...baseProps, isSystemAdmin: true, notices: [{name: 'notice1', adminOnly: true, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}]};
const props = {...baseProps, isSystemAdmin: true, notices: [{...baseProps.notices[0], adminOnly: true}]};
const wrapper = shallow(<SystemNotice {...props}/>);
expect(wrapper).toMatchSnapshot();
});
@@ -74,19 +76,25 @@ describe('components/SystemNotice', () => {
});
test('should match snapshot for show function returning false', () => {
const props = {...baseProps, notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => false}]};
const props = {...baseProps, notices: [{...baseProps.notices[0], show: () => false}]};
const wrapper = shallow(<SystemNotice {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for show function returning true', () => {
const props = {...baseProps, notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: true, show: () => true}]};
const props = {...baseProps, notices: [{...baseProps.notices[0], show: () => true}]};
const wrapper = shallow(<SystemNotice {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for with allowForget equal false', () => {
const props = {...baseProps, notices: [{name: 'notice1', adminOnly: false, title: 'some title', icon: mattermostIcon, body: 'some body', allowForget: false, show: () => true}]};
const props = {...baseProps, notices: [{...baseProps.notices[0], allowForget: false}]};
const wrapper = shallow(<SystemNotice {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot when a custom icon is passed', () => {
const props = {...baseProps, notices: [{...baseProps.notices[0], icon: <span>{'icon'}</span>}]};
const wrapper = shallow(<SystemNotice {...props}/>);
expect(wrapper).toMatchSnapshot();
});

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

@@ -5,6 +5,7 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {AnalyticsRow} from '@mattermost/types/admin';
import type {Channel} from '@mattermost/types/channels';
import type {ClientConfig, ClientLicense} from '@mattermost/types/config';
import type {PreferenceType} from '@mattermost/types/preferences';
@@ -25,6 +26,7 @@ type Props = {
config: Partial<ClientConfig>;
license: ClientLicense;
analytics?: Record<string, number | AnalyticsRow[]>;
currentChannel?: Channel;
actions: {
savePreferences(userId: string, preferences: PreferenceType[]): void;
dismissNotice(type: string): void;
@@ -60,7 +62,13 @@ export default class SystemNotice extends React.PureComponent<Props> {
continue;
}
if (!notice.show?.(this.props.serverVersion, this.props.config, this.props.license, this.props.analytics)) {
if (!notice.show?.(
this.props.serverVersion,
this.props.config,
this.props.license,
this.props.analytics,
this.props.currentChannel,
)) {
continue;
}
@@ -118,44 +126,44 @@ export default class SystemNotice extends React.PureComponent<Props> {
);
}
const icon = notice.icon || <MattermostLogo/>;
return (
<div
className='system-notice bg--white shadow--2'
>
<div className='system-notice__header'>
<div className='system-notice__logo'>
<MattermostLogo/>
</div>
<div className='system-notice__logo'>
{icon}
</div>
<div className='system-notice__body'>
<div className='system-notice__title'>
{notice.title}
</div>
</div>
<div className='system-notice__body'>
{notice.body}
</div>
{visibleMessage}
<div className='system-notice__footer'>
<button
id='systemnotice_remindme'
className='btn btn-transparent'
onClick={this.hideAndRemind}
>
<FormattedMessage
id='system_notice.remind_me'
defaultMessage='Remind Me Later'
/>
</button>
{notice.allowForget &&
{visibleMessage}
<div className='system-notice__footer'>
<button
id='systemnotice_dontshow'
className='btn btn-transparent'
onClick={this.hideAndForget}
id='systemnotice_remindme'
className='btn btn-primary'
onClick={this.hideAndRemind}
>
<FormattedMessage
id='system_notice.dont_show'
defaultMessage="Don't Show Again"
id='system_notice.remind_me'
defaultMessage='Remind Me Later'
/>
</button>}
</button>
{notice.allowForget &&
<button
id='systemnotice_dontshow'
className='btn'
onClick={this.hideAndForget}
>
<FormattedMessage
id='system_notice.dont_show'
defaultMessage="Don't Show Again"
/>
</button>}
</div>
</div>
</div>
);

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

@@ -4,17 +4,20 @@
import type React from 'react';
import type {AnalyticsRow} from '@mattermost/types/admin';
import type {Channel} from '@mattermost/types/channels';
export type Notice = {
name: string;
adminOnly?: boolean;
title: React.ReactNode;
icon: string;
icon?: React.ReactNode;
body: React.ReactNode;
allowForget: boolean;
show?(
serverVersion: string,
config: any,
license: any,
analytics?: Record<string, number | AnalyticsRow[]>): boolean;
analytics?: Record<string, number | AnalyticsRow[]>,
currentChannel?: Channel,
): boolean;
}

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

@@ -50,7 +50,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Only for mentions and direct messages"
defaultMessage="Only for mentions, direct messages, and group messages"
id="user.settings.notifications.onlyMentions"
/>
</label>
@@ -205,7 +205,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Only for mentions and direct messages"
defaultMessage="Only for mentions, direct messages, and group messages"
id="user.settings.notifications.onlyMentions"
/>
</label>
@@ -330,7 +330,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Only for mentions and direct messages"
defaultMessage="Only for mentions, direct messages, and group messages"
id="user.settings.notifications.onlyMentions"
/>
</label>
@@ -485,7 +485,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Only for mentions and direct messages"
defaultMessage="Only for mentions, direct messages, and group messages"
id="user.settings.notifications.onlyMentions"
/>
</label>
@@ -693,7 +693,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Only for mentions and direct messages"
defaultMessage="Only for mentions, direct messages, and group messages"
id="user.settings.notifications.onlyMentions"
/>
</label>
@@ -942,7 +942,7 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
type="radio"
/>
<Memo(MemoizedFormattedMessage)
defaultMessage="Only for mentions and direct messages"
defaultMessage="Only for mentions, direct messages, and group messages"
id="user.settings.notifications.onlyMentions"
/>
</label>

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

@@ -397,7 +397,7 @@ export default class DesktopNotificationSettings extends React.PureComponent<Pro
/>
<FormattedMessage
id='user.settings.notifications.onlyMentions'
defaultMessage='Only for mentions and direct messages'
defaultMessage='Only for mentions, direct messages, and group messages'
/>
</label>
<br/>

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

@@ -2994,7 +2994,7 @@
"channel_modal.type.private.title": "Private Channel",
"channel_modal.type.public.description": "Anyone can join",
"channel_modal.type.public.title": "Public Channel",
"channel_notifications.allActivity": "For all activity",
"channel_notifications.allActivity": "For all activity {isDefault}",
"channel_notifications.channelAutoFollowThreads": "Auto-follow all new threads in this channel",
"channel_notifications.channelAutoFollowThreads.help": "When enabled, you will auto-follow all new threads created in this channel unless you unfollow a thread explicitly.",
"channel_notifications.channelAutoFollowThreads.off.title": "Off",
@@ -3760,7 +3760,7 @@
"intro_messages.creatorPrivate": "This is the start of the {name} private channel, created by {creator} on {date}.",
"intro_messages.default": "**Welcome to {display_name}!**\n \nPost messages here that you want everyone to see. Everyone automatically becomes a permanent member of this channel when they join the team.",
"intro_messages.DM": "This is the start of your direct message history with {teammate}.\nDirect messages and files shared here are not shown to people outside this area.",
"intro_messages.GM": "This is the start of your group message history with {names}.\nMessages and files shared here are not shown to people outside this area.",
"intro_messages.GM": "This is the start of your group message history with {names}.{br}You'll be notified <b>for all activity</b> in this group message.",
"intro_messages.group_message": "This is the start of your group message history with these teammates. Messages and files shared here are not shown to people outside this area.",
"intro_messages.inviteGropusToChannel.button": "Add groups to this private channel",
"intro_messages.inviteMembersToChannel.button": "Add members to this channel",
@@ -3770,6 +3770,7 @@
"intro_messages.inviteOthersToWorkspace.title": "Lets add some people to the workspace!",
"intro_messages.noCreator": "This is the start of the {name} channel, created on {date}.",
"intro_messages.noCreatorPrivate": "This is the start of the {name} private channel, created on {date}.",
"intro_messages.notificationPreferences": "Notification Preferences",
"intro_messages.offTopic": "This is the start of {display_name}, a channel for non-work-related conversations.",
"intro_messages.onlyInvited": " Only invited members can see this private channel.",
"intro_messages.purpose": " This channel's purpose is: {purpose}",
@@ -4999,7 +5000,9 @@
"system_notice.body.permissions": "Some policy and permission System Console settings have moved with the release of <link>advanced permissions</link> into Mattermost Free and Professional.",
"system_notice.dont_show": "Don't Show Again",
"system_notice.remind_me": "Remind me Later",
"system_notice.title": "**Notice**\nfrom Mattermost",
"system_notice.title": "Notice from Mattermost",
"system_notice.title.gm_as_dm": "Updates to Group Messages",
"system_noticy.body.gm_as_dm": "You wil now be notified for all activity in your group messages along with a notification badge for every new message.{br}{br}You can configure this in notification preferences for each group message.",
"system_users_list.count": "{count, number} {count, plural, one {user} other {users}}",
"system_users_list.countPage": "{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total, number} total",
"system_users_list.countSearch": "{count, number} {count, plural, one {user} other {users}} of {total, number} total",
@@ -5448,7 +5451,7 @@
"user.settings.notifications.never": "Never",
"user.settings.notifications.off": "Off",
"user.settings.notifications.on": "On",
"user.settings.notifications.onlyMentions": "Only for mentions and direct messages",
"user.settings.notifications.onlyMentions": "Only for mentions, direct messages, and group messages",
"user.settings.notifications.push": "Mobile Push Notifications",
"user.settings.notifications.push_notification.status": "Trigger push notifications when",
"user.settings.notifications.push_threads": "When enabled, any reply to a thread you're following will send a mobile push notification.",

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

@@ -5,7 +5,8 @@
z-index: 9999;
right: 12px;
bottom: 12px;
width: 280px;
display: flex;
width: 386px;
padding: 18px 20px 0;
border: 1px solid alpha-color($black, 0.15);
background-color: var(--center-channel-bg);
@@ -13,33 +14,27 @@
box-shadow: 0 20px 30px alpha-color($black, 0.07), 0 14px 20px alpha-color($black, 0.07);
}
.system-notice__header {
display: flex;
align-items: flex-start;
}
.system-notice__logo {
height: 36px;
height: 20px;
svg {
width: 36px;
height: 36px;
fill: rgb(22, 109, 224);
width: 20px;
height: 20px;
fill: var(--button-bg);
}
}
.system-notice__title {
overflow: hidden;
flex: 10 1 auto;
padding: 3px 0 0 8px;
padding-bottom: 10px;
font-weight: bold;
line-height: 16px;
opacity: 0.7;
text-overflow: ellipsis;
white-space: nowrap;
}
.system-notice__info {
margin-bottom: 12px;
margin-top: 12px;
font-size: 12px;
opacity: 0.5;
@@ -49,33 +44,22 @@
}
.system-notice__body {
padding: 18px 0 16px;
padding: 0 0 16px 16px;
line-height: 16px;
opacity: 0.7;
}
.system-notice__footer {
display: flex;
border-top: 1px solid alpha-color($black, 0.2);
margin: 0 -20px;
margin-top: 16px;
.btn {
overflow: hidden;
flex: 1;
font-weight: bold;
text-overflow: ellipsis;
&:hover {
background: rgb(22, 109, 224);
color: $white;
}
&:first-child {
border-radius: 0 0 0 4px;
}
&:last-child {
border-left: 1px solid alpha-color($black, 0.2);
border-radius: 0 0 4px 0;
margin-left: 5px;
}
}
}

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

@@ -524,6 +524,7 @@
margin-bottom: 10px;
.fa,
svg,
i {
margin-right: 5px;
}

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {formatWithRenderer} from './markdown';
import MentionableRenderer from './markdown/mentionable_renderer';
import PlainRenderer from './markdown/plain_renderer';
export const emoticonPatterns: { [key: string]: RegExp } = {
slightly_smiling_face: /(^|\B)(:-?\))($|\B)/g, // :)
@@ -28,7 +28,7 @@ export const emoticonPatterns: { [key: string]: RegExp } = {
export const EMOJI_PATTERN = /(:([a-zA-Z0-9_+-]+):)/g;
export function matchEmoticons(text: string): RegExpMatchArray | null {
const markdownCleanedText = formatWithRenderer(text, new MentionableRenderer());
const markdownCleanedText = formatWithRenderer(text, new PlainRenderer());
let emojis = markdownCleanedText.match(EMOJI_PATTERN);
for (const name of Object.keys(emoticonPatterns)) {

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

@@ -1,81 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import marked from 'marked';
import {EMOJI_PATTERN} from 'utils/emoticons';
import PlainRenderer from './plain_renderer';
/** A Markdown renderer that converts a post into plain text that we can search for mentions */
export default class MentionableRenderer extends marked.Renderer {
public code() {
// Code blocks can't contain mentions
return '\n';
}
public blockquote(text: string) {
return text + '\n';
}
public heading(text: string) {
return text + '\n';
}
public hr() {
return '\n';
}
public list(body: string) {
return body + '\n';
}
public listitem(text: string) {
return text + '\n';
}
public paragraph(text: string) {
return text + '\n';
}
public table(header: string, body: string) {
return header + '\n' + body;
}
public tablerow(content: string) {
return content;
}
public tablecell(content: string) {
return content + '\n';
}
public strong(text: string) {
return ' ' + text + ' ';
}
public em(text: string) {
return ' ' + text + ' ';
}
public codespan() {
// Code spans can't contain mentions
return ' ';
}
public br() {
return '\n';
}
public del(text: string) {
return ' ' + text + ' ';
}
public link(href: string, title: string, text: string) {
return ' ' + text + ' ';
}
public image(href: string, title: string, text: string) {
return ' ' + text + ' ';
}
export default class MentionableRenderer extends PlainRenderer {
public text(text: string) {
return text;
// Remove all emojis
return text.replace(EMOJI_PATTERN, '');
}
}

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

@@ -0,0 +1,81 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import marked from 'marked';
/** A Markdown renderer that converts a post into plain text */
export default class PlainRenderer extends marked.Renderer {
public code() {
// Code blocks can't contain mentions
return '\n';
}
public blockquote(text: string) {
return text + '\n';
}
public heading(text: string) {
return text + '\n';
}
public hr() {
return '\n';
}
public list(body: string) {
return body + '\n';
}
public listitem(text: string) {
return text + '\n';
}
public paragraph(text: string) {
return text + '\n';
}
public table(header: string, body: string) {
return header + '\n' + body;
}
public tablerow(content: string) {
return content;
}
public tablecell(content: string) {
return content + '\n';
}
public strong(text: string) {
return ' ' + text + ' ';
}
public em(text: string) {
return ' ' + text + ' ';
}
public codespan() {
// Code spans can't contain mentions
return ' ';
}
public br() {
return '\n';
}
public del(text: string) {
return ' ' + text + ' ';
}
public link(href: string, title: string, text: string) {
return ' ' + text + ' ';
}
public image(href: string, title: string, text: string) {
return ' ' + text + ' ';
}
public text(text: string) {
return text;
}
}

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

@@ -227,7 +227,7 @@ const DEFAULT_OPTIONS: TextFormattingOptions = {
* Additional CJK and Hangul compatibility characters: \u2de0-\u2dff
**/
// eslint-disable-next-line no-misleading-character-class
const cjkrPattern = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf\uac00-\ud7a3\u1100-\u11ff\u3130-\u318f\u0400-\u04ff\u0500-\u052f\u2de0-\u2dff]/;
export const cjkrPattern = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf\uac00-\ud7a3\u1100-\u11ff\u3130-\u318f\u0400-\u04ff\u0500-\u052f\u2de0-\u2dff]/;
export function formatText(
text: string,