Adding Do not disturb and remote user hour warnings (#27138)
* Adding Do not disturb and remote user hour warnings * Do not show the hour warning on your own DMs * style tweaks * Some fixes * Linter fixes * Updating snapshots * Improving the robustness of this solution * Some improvements on keeping up the hour in the interface * i18n-extract and fix linter errors * Removing colon where is not needed * Removing the time from 6-7 to be shown * Addressing PR Review Comments * Changing the remote user hour icon * Changing back to fill and not outline icon * Addressing PR review comments * Fixing the RHS showing this * Removing unneeded check * Fixing linter error * Update webapp/channels/src/components/advanced_text_editor/do_not_disturb_warning.tsx Co-authored-by: Matthew Birtch <mattbirtch@gmail.com> * Addressing PR review comment * adding consistency to show the DND and Late hours message --------- Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a3edc70019
Коммит
a4f2ec744c
@@ -4,7 +4,7 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {useDispatch} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import type {Channel} from '@mattermost/types/channels';
|
||||||
@@ -12,6 +12,10 @@ import type {Emoji} from '@mattermost/types/emojis';
|
|||||||
import type {ServerError} from '@mattermost/types/errors';
|
import type {ServerError} from '@mattermost/types/errors';
|
||||||
import type {FileInfo} from '@mattermost/types/files';
|
import type {FileInfo} from '@mattermost/types/files';
|
||||||
|
|
||||||
|
import {getDirectChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
|
import {getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
||||||
import LocalStorageStore from 'stores/local_storage_store';
|
import LocalStorageStore from 'stores/local_storage_store';
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ import type TextboxClass from 'components/textbox/textbox';
|
|||||||
import Tooltip from 'components/tooltip';
|
import Tooltip from 'components/tooltip';
|
||||||
import {SendMessageTour} from 'components/tours/onboarding_tour';
|
import {SendMessageTour} from 'components/tours/onboarding_tour';
|
||||||
|
|
||||||
import Constants, {Locations} from 'utils/constants';
|
import Constants, {Locations, UserStatuses} from 'utils/constants';
|
||||||
import * as Keyboard from 'utils/keyboard';
|
import * as Keyboard from 'utils/keyboard';
|
||||||
import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||||
import {pasteHandler} from 'utils/paste';
|
import {pasteHandler} from 'utils/paste';
|
||||||
@@ -41,11 +45,14 @@ import {isWithinCodeBlock} from 'utils/post_utils';
|
|||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
import type {PostDraft} from 'types/store/draft';
|
import type {PostDraft} from 'types/store/draft';
|
||||||
|
|
||||||
|
import DoNotDisturbWarning from './do_not_disturb_warning';
|
||||||
import FormattingBar from './formatting_bar';
|
import FormattingBar from './formatting_bar';
|
||||||
import {FormattingBarSpacer, Separator} from './formatting_bar/formatting_bar';
|
import {FormattingBarSpacer, Separator} from './formatting_bar/formatting_bar';
|
||||||
import {IconContainer} from './formatting_bar/formatting_icon';
|
import {IconContainer} from './formatting_bar/formatting_icon';
|
||||||
|
import RemoteUserHour from './remote_user_hour';
|
||||||
import SendButton from './send_button';
|
import SendButton from './send_button';
|
||||||
import ShowFormat from './show_formatting';
|
import ShowFormat from './show_formatting';
|
||||||
import TexteditorActions from './texteditor_actions';
|
import TexteditorActions from './texteditor_actions';
|
||||||
@@ -199,6 +206,28 @@ const AdvanceTextEditor = ({
|
|||||||
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
||||||
const [keepEditorInFocus, setKeepEditorInFocus] = useState(false);
|
const [keepEditorInFocus, setKeepEditorInFocus] = useState(false);
|
||||||
|
|
||||||
|
let showDndWarning = false;
|
||||||
|
let showRemoteUserHour = false;
|
||||||
|
let teammateId = '';
|
||||||
|
const post = useSelector((state: GlobalState) => getPost(state, postId));
|
||||||
|
const postChannel = useSelector((state: GlobalState) => getDirectChannel(state, post?.channel_id));
|
||||||
|
let channel = currentChannel;
|
||||||
|
if (postChannel) {
|
||||||
|
channel = postChannel;
|
||||||
|
}
|
||||||
|
if (channel && channel.type === 'D') {
|
||||||
|
teammateId = channel.teammate_id || '';
|
||||||
|
}
|
||||||
|
const teammateStatus = useSelector((state: GlobalState) => getStatusForUserId(state, teammateId));
|
||||||
|
const teammate = useSelector((state: GlobalState) => getUser(state, teammateId));
|
||||||
|
|
||||||
|
if (teammate && teammateId !== '' && teammateStatus === UserStatuses.DND) {
|
||||||
|
showDndWarning = true;
|
||||||
|
}
|
||||||
|
if (!showDndWarning && teammate && teammateId !== '') {
|
||||||
|
showRemoteUserHour = true;
|
||||||
|
}
|
||||||
|
|
||||||
const isNonFormattedPaste = useRef(false);
|
const isNonFormattedPaste = useRef(false);
|
||||||
const timeoutId = useRef<number>();
|
const timeoutId = useRef<number>();
|
||||||
|
|
||||||
@@ -651,6 +680,13 @@ const AdvanceTextEditor = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{showDndWarning && <DoNotDisturbWarning displayName={Utils.getDisplayName(teammate)}/>}
|
||||||
|
{showRemoteUserHour && (
|
||||||
|
<RemoteUserHour
|
||||||
|
teammate={teammate}
|
||||||
|
displayName={Utils.getDisplayName(teammate)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames('AdvancedTextEditor', {
|
className={classNames('AdvancedTextEditor', {
|
||||||
'AdvancedTextEditor__attachment-disabled': !canUploadFiles,
|
'AdvancedTextEditor__attachment-disabled': !canUploadFiles,
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||||
|
|
||||||
|
& + .AdvancedTextEditor {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Icon = styled.i`
|
||||||
|
color: #d24b4e;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DoNotDisturbWarning = ({displayName}: Props) => {
|
||||||
|
return (
|
||||||
|
<Container className='DoNotDisturbWarning'>
|
||||||
|
<Icon className='icon-minus-circle'/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='advanced_create_post.doNotDisturbWarning'
|
||||||
|
defaultMessage='{displayName} is set to <b>Do Not Disturb</b>'
|
||||||
|
values={{displayName, b: (chunks: React.ReactNode) => <b>{chunks}</b>}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DoNotDisturbWarning;
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {DateTime} from 'luxon';
|
||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {getTimezoneForUserProfile} from 'mattermost-redux/selectors/entities/timezone';
|
||||||
|
|
||||||
|
import Moon from 'components/common/svg_images_components/moon_svg';
|
||||||
|
import Timestamp from 'components/timestamp';
|
||||||
|
|
||||||
|
import Constants from 'utils/constants';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
aling-items: center;
|
||||||
|
padding: 8px 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||||
|
|
||||||
|
& + .AdvancedTextEditor {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
time {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Icon = styled(Moon)`
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
svg path {
|
||||||
|
fill: rgba(var(--center-channel-color-rgb), 0.75);
|
||||||
|
}
|
||||||
|
margin: 0 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
teammate: UserProfile;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemoteUserHour = ({teammate, displayName}: Props) => {
|
||||||
|
const [timestamp, setTimestamp] = useState(0);
|
||||||
|
const [showIt, setShowIt] = useState(false);
|
||||||
|
|
||||||
|
const teammateTimezone = getTimezoneForUserProfile(teammate);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const teammateUserDate = DateTime.local().setZone(teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone);
|
||||||
|
setTimestamp(teammateUserDate.toMillis());
|
||||||
|
setShowIt(teammateUserDate.get('hour') >= Constants.REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY || teammateUserDate.get('hour') < Constants.REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const teammateUserDate = DateTime.local().setZone(teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone);
|
||||||
|
setTimestamp(teammateUserDate.toMillis());
|
||||||
|
setShowIt(teammateUserDate.get('hour') >= Constants.REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY || teammateUserDate.get('hour') < Constants.REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY);
|
||||||
|
}, 1000 * 60);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [teammateTimezone.useAutomaticTimezone, teammateTimezone.automaticTimezone, teammateTimezone.manualTimezone]);
|
||||||
|
|
||||||
|
if (!showIt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamp === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Icon/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='advanced_text_editor.remote_user_hour'
|
||||||
|
defaultMessage='The time for {user} is {time}'
|
||||||
|
values={{
|
||||||
|
user: displayName,
|
||||||
|
time: (
|
||||||
|
<Timestamp
|
||||||
|
useRelative={false}
|
||||||
|
value={timestamp}
|
||||||
|
useDate={false}
|
||||||
|
userTimezone={teammateTimezone}
|
||||||
|
useTime={{
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemoteUserHour;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Moon(props: Props) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
title={props.title}
|
||||||
|
className={props.className}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width='24px'
|
||||||
|
height='24px'
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
version='1.1'
|
||||||
|
role='img'
|
||||||
|
>
|
||||||
|
<path d='M18.73,18C15.4,21.69 9.71,22 6,18.64C2.33,15.31 2.04,9.62 5.37,5.93C6.9,4.25 9,3.2 11.27,3C7.96,6.7 8.27,12.39 12,15.71C13.63,17.19 15.78,18 18,18C18.25,18 18.5,18 18.73,18Z'/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2751,6 +2751,7 @@
|
|||||||
"adminConsole.list.table.rowsCount.20": "20",
|
"adminConsole.list.table.rowsCount.20": "20",
|
||||||
"adminConsole.list.table.rowsCount.50": "50",
|
"adminConsole.list.table.rowsCount.50": "50",
|
||||||
"adminConsole.list.table.rowsCount.show(rowsPerPage)": "rows per page",
|
"adminConsole.list.table.rowsCount.show(rowsPerPage)": "rows per page",
|
||||||
|
"advanced_text_editor.remote_user_hour": "The time for {user} is {time}",
|
||||||
"air_gapped_contact_sales_modal.body": "Please access the link below to contact sales.",
|
"air_gapped_contact_sales_modal.body": "Please access the link below to contact sales.",
|
||||||
"air_gapped_contact_sales_modal.title": "Looks like you do not have access to the internet",
|
"air_gapped_contact_sales_modal.title": "Looks like you do not have access to the internet",
|
||||||
"air_gapped_modal.close": "Close",
|
"air_gapped_modal.close": "Close",
|
||||||
|
|||||||
@@ -187,6 +187,16 @@ export function getChannel(state: GlobalState, id: string): Channel | undefined
|
|||||||
return getAllChannels(state)[id];
|
return getAllChannels(state)[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDirectChannel returns a direct channel channel as it exists in the store filling in any additional details such as the
|
||||||
|
// display_name or teammate_id.
|
||||||
|
export function getDirectChannel(state: GlobalState, id: string): Channel | undefined {
|
||||||
|
const channel = getAllChannels(state)[id];
|
||||||
|
if (channel && channel.type === 'D') {
|
||||||
|
return completeDirectChannelInfo(state.entities.users, getTeammateNameDisplaySetting(state), channel);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getMyChannelMembership(state: GlobalState, channelId: string): ChannelMembership | undefined {
|
export function getMyChannelMembership(state: GlobalState, channelId: string): ChannelMembership | undefined {
|
||||||
return getMyChannelMemberships(state)[channelId];
|
return getMyChannelMemberships(state)[channelId];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1450,6 +1450,8 @@ export const Constants = {
|
|||||||
Locations,
|
Locations,
|
||||||
PostListRowListIds,
|
PostListRowListIds,
|
||||||
MAX_POST_VISIBILITY: 1000000,
|
MAX_POST_VISIBILITY: 1000000,
|
||||||
|
REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY: 22,
|
||||||
|
REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY: 6,
|
||||||
|
|
||||||
IGNORE_POST_TYPES: [PostTypes.JOIN_LEAVE, PostTypes.JOIN_TEAM, PostTypes.LEAVE_TEAM, PostTypes.JOIN_CHANNEL, PostTypes.LEAVE_CHANNEL, PostTypes.REMOVE_FROM_CHANNEL, PostTypes.ADD_REMOVE],
|
IGNORE_POST_TYPES: [PostTypes.JOIN_LEAVE, PostTypes.JOIN_TEAM, PostTypes.LEAVE_TEAM, PostTypes.JOIN_CHANNEL, PostTypes.LEAVE_CHANNEL, PostTypes.REMOVE_FROM_CHANNEL, PostTypes.ADD_REMOVE],
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user