From a4f2ec744c3693b2cefbfa115fed7d46b9f9a87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 12 Jul 2024 20:11:43 +0200 Subject: [PATCH] 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 * Addressing PR review comment * adding consistency to show the DND and Late hours message --------- Co-authored-by: Matthew Birtch --- .../advanced_text_editor.tsx | 40 ++++++- .../do_not_disturb_warning.tsx | 41 +++++++ .../advanced_text_editor/remote_user_hour.tsx | 103 ++++++++++++++++++ .../common/svg_images_components/moon_svg.tsx | 28 +++++ webapp/channels/src/i18n/en.json | 1 + .../src/selectors/entities/channels.ts | 10 ++ webapp/channels/src/utils/constants.tsx | 2 + 7 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 webapp/channels/src/components/advanced_text_editor/do_not_disturb_warning.tsx create mode 100644 webapp/channels/src/components/advanced_text_editor/remote_user_hour.tsx create mode 100644 webapp/channels/src/components/common/svg_images_components/moon_svg.tsx diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx index 437464b058..61fa15762b 100644 --- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx +++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.tsx @@ -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(); @@ -651,6 +680,13 @@ const AdvanceTextEditor = ({ return ( <> + {showDndWarning && } + {showRemoteUserHour && ( + + )}
{ + return ( + + + {chunks}}} + /> + + ); +}; + +export default DoNotDisturbWarning; diff --git a/webapp/channels/src/components/advanced_text_editor/remote_user_hour.tsx b/webapp/channels/src/components/advanced_text_editor/remote_user_hour.tsx new file mode 100644 index 0000000000..3e4acea117 --- /dev/null +++ b/webapp/channels/src/components/advanced_text_editor/remote_user_hour.tsx @@ -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 ( + + + + ), + }} + /> + + ); +}; + +export default RemoteUserHour; diff --git a/webapp/channels/src/components/common/svg_images_components/moon_svg.tsx b/webapp/channels/src/components/common/svg_images_components/moon_svg.tsx new file mode 100644 index 0000000000..0e97e953ad --- /dev/null +++ b/webapp/channels/src/components/common/svg_images_components/moon_svg.tsx @@ -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 ( + + + + + + ); +} diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 5574ac462b..2db749141f 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -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", diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts index 6bd03b7a32..3cea38f758 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts @@ -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]; } diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index e379757074..f0f663b62a 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -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],