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>
Этот коммит содержится в:
Jesús Espino
2024-07-12 20:11:43 +02:00
коммит произвёл GitHub
родитель a3edc70019
Коммит a4f2ec744c
7 изменённых файлов: 223 добавлений и 2 удалений

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

@@ -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],