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 React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
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 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 {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 LocalStorageStore from 'stores/local_storage_store';
|
||||
|
||||
@@ -33,7 +37,7 @@ import type TextboxClass from 'components/textbox/textbox';
|
||||
import Tooltip from 'components/tooltip';
|
||||
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 type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||
import {pasteHandler} from 'utils/paste';
|
||||
@@ -41,11 +45,14 @@ import {isWithinCodeBlock} from 'utils/post_utils';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
|
||||
import DoNotDisturbWarning from './do_not_disturb_warning';
|
||||
import FormattingBar from './formatting_bar';
|
||||
import {FormattingBarSpacer, Separator} from './formatting_bar/formatting_bar';
|
||||
import {IconContainer} from './formatting_bar/formatting_icon';
|
||||
import RemoteUserHour from './remote_user_hour';
|
||||
import SendButton from './send_button';
|
||||
import ShowFormat from './show_formatting';
|
||||
import TexteditorActions from './texteditor_actions';
|
||||
@@ -199,6 +206,28 @@ const AdvanceTextEditor = ({
|
||||
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
||||
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 timeoutId = useRef<number>();
|
||||
|
||||
@@ -651,6 +680,13 @@ const AdvanceTextEditor = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDndWarning && <DoNotDisturbWarning displayName={Utils.getDisplayName(teammate)}/>}
|
||||
{showRemoteUserHour && (
|
||||
<RemoteUserHour
|
||||
teammate={teammate}
|
||||
displayName={Utils.getDisplayName(teammate)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={classNames('AdvancedTextEditor', {
|
||||
'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.50": "50",
|
||||
"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.title": "Looks like you do not have access to the internet",
|
||||
"air_gapped_modal.close": "Close",
|
||||
|
||||
@@ -187,6 +187,16 @@ export function getChannel(state: GlobalState, id: string): Channel | undefined
|
||||
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 {
|
||||
return getMyChannelMemberships(state)[channelId];
|
||||
}
|
||||
|
||||
@@ -1450,6 +1450,8 @@ export const Constants = {
|
||||
Locations,
|
||||
PostListRowListIds,
|
||||
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],
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user