diff --git a/webapp/channels/src/actions/post_actions.ts b/webapp/channels/src/actions/post_actions.ts index 6264b09b52..2793bed417 100644 --- a/webapp/channels/src/actions/post_actions.ts +++ b/webapp/channels/src/actions/post_actions.ts @@ -58,6 +58,7 @@ import type {OnSubmitOptions, SubmitPostReturnType} from './views/create_comment export type CreatePostOptions = { keepDraft?: boolean; + ignorePostError?: boolean; } export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}): ActionFuncAsync { diff --git a/webapp/channels/src/components/advanced_text_editor/use_submit.tsx b/webapp/channels/src/components/advanced_text_editor/use_submit.tsx index 94a55c2f88..2ca496c366 100644 --- a/webapp/channels/src/components/advanced_text_editor/use_submit.tsx +++ b/webapp/channels/src/components/advanced_text_editor/use_submit.tsx @@ -126,7 +126,7 @@ const useSubmit = ( return; } - if (postError) { + if (postError && !createPostOptions?.ignorePostError) { setErrorClass('animation--highlight'); setTimeout(() => { setErrorClass(null); diff --git a/webapp/channels/src/components/channel_view/index.ts b/webapp/channels/src/components/channel_view/index.ts index efc9876bfb..bc28ee9d52 100644 --- a/webapp/channels/src/components/channel_view/index.ts +++ b/webapp/channels/src/components/channel_view/index.ts @@ -7,7 +7,11 @@ import {withRouter} from 'react-router-dom'; import type {Channel} from '@mattermost/types/channels'; -import {getCurrentChannel, getDirectTeammate, getMyChannelMembership} from 'mattermost-redux/selectors/entities/channels'; +import { + getCurrentChannel, + getMyChannelMembership, + isDeactivatedDirectChannel, +} from 'mattermost-redux/selectors/entities/channels'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; import {getRoles} from 'mattermost-redux/selectors/entities/roles_helpers'; import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams'; @@ -21,12 +25,6 @@ import type {GlobalState} from 'types/store'; import ChannelView from './channel_view'; -function isDeactivatedChannel(state: GlobalState, channelId: string) { - const teammate = getDirectTeammate(state, channelId); - - return Boolean(teammate && teammate.delete_at); -} - function isMissingChannelRoles(state: GlobalState, channel?: Channel) { const channelRoles = channel ? getMyChannelMembership(state, channel.id)?.roles || '' : ''; return !channelRoles.split(' ').some((v) => Boolean(getRoles(state)[v])); @@ -45,7 +43,7 @@ function mapStateToProps(state: GlobalState) { return { channelId: channel ? channel.id : '', - deactivatedChannel: channel ? isDeactivatedChannel(state, channel.id) : false, + deactivatedChannel: channel ? isDeactivatedDirectChannel(state, channel.id) : false, enableOnboardingFlow, channelIsArchived: channel ? channel.delete_at !== 0 : false, viewArchivedChannels, diff --git a/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_modal.tsx b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_modal.tsx index b9439d200d..17a832becb 100644 --- a/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_modal.tsx +++ b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_modal.tsx @@ -8,7 +8,7 @@ import {FormattedMessage, useIntl} from 'react-intl'; import {GenericModal} from '@mattermost/components'; type Props = { - channelDisplayName: string; + channelDisplayName?: string; onConfirm: () => Promise<{error?: string}>; onExited: () => void; } @@ -54,14 +54,25 @@ export default function DeleteScheduledPostModal({ autoCloseOnConfirmButton={false} errorText={errorMessage} > - {displayName}?'} - values={{ - strong: (chunk: string) => {chunk}, - displayName: channelDisplayName, - }} - /> + { + channelDisplayName && + {displayName}?'} + values={{ + strong: (chunk: string) => {chunk}, + displayName: channelDisplayName, + }} + /> + } + + { + !channelDisplayName && + + } ); } diff --git a/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx index 84a7df252a..4336b3ddf8 100644 --- a/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx +++ b/webapp/channels/src/components/drafts/draft_actions/schedule_post_actions/scheduled_post_actions.tsx @@ -2,12 +2,15 @@ // See LICENSE.txt for license information. import moment from 'moment'; -import React, {memo, useCallback} from 'react'; +import React, {memo, useCallback, useEffect} from 'react'; import {FormattedMessage} from 'react-intl'; import {useDispatch, useSelector} from 'react-redux'; +import type {Channel} from '@mattermost/types/channels'; import type {ScheduledPost} from '@mattermost/types/schedule_post'; +import {fetchMissingChannels} from 'mattermost-redux/actions/channels'; +import {isDeactivatedDirectChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {openModal} from 'actions/views/modals'; @@ -19,9 +22,10 @@ import DeleteScheduledPostModal from 'components/drafts/draft_actions/schedule_post_actions/delete_scheduled_post_modal'; import SendDraftModal from 'components/drafts/draft_actions/send_draft_modal'; -import {ModalIdentifiers} from 'utils/constants'; +import Constants, {ModalIdentifiers} from 'utils/constants'; import './style.scss'; +import type {GlobalState} from 'types/store'; const deleteTooltipText = ( Promise<{error?: string}>; onDelete: (scheduledPostId: string) => Promise<{error?: string}>; onSend: (scheduledPostId: string) => void; onEdit: () => void; } -function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDisplayName, onSend, onEdit}: Props) { +function ScheduledPostActions({scheduledPost, channel, onReschedule, onDelete, onSend, onEdit}: Props) { const dispatch = useDispatch(); const userTimezone = useSelector(getCurrentTimezone); + useEffect(() => { + // this ensures the DM is loaded in redux store and is available + // later when we check if the DM is with a deactivated user. + if (channel?.type === Constants.DM_CHANNEL) { + // fetchMissingChannels uses DataLoader which de-duplicates all requested data, + // so even if we have multiple scheduled posts in a DM, + // the data loader ensured we fetch that DM only once. + dispatch(fetchMissingChannels([channel.id])); + } + }, [channel, dispatch]); + const handleReschedulePost = useCallback(() => { const initialTime = moment.tz(scheduledPost.scheduled_at, userTimezone); @@ -83,22 +98,34 @@ function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDis modalId: ModalIdentifiers.DELETE_DRAFT, dialogType: DeleteScheduledPostModal, dialogProps: { - channelDisplayName, + channelDisplayName: channel?.display_name, onConfirm: () => onDelete(scheduledPost.id), }, })); - }, [channelDisplayName, dispatch, onDelete, scheduledPost.id]); + }, [channel, dispatch, onDelete, scheduledPost.id]); const handleSend = useCallback(() => { + if (!channel) { + return; + } + dispatch(openModal({ modalId: ModalIdentifiers.SEND_DRAFT, dialogType: SendDraftModal, dialogProps: { - displayName: channelDisplayName, + displayName: channel.display_name, onConfirm: () => onSend(scheduledPost.id), }, })); - }, [channelDisplayName, dispatch, onSend, scheduledPost.id]); + }, [channel, dispatch, onSend, scheduledPost.id]); + + const showEditOption = !scheduledPost.error_code; + + const isChannelArchived = Boolean(channel?.delete_at); + const isDeactivatedDM = useSelector((state: GlobalState) => isDeactivatedDirectChannel(state, scheduledPost.channel_id)); + const showSendNowOption = (!scheduledPost.error_code || scheduledPost.error_code === 'unknown' || scheduledPost.error_code === 'unable_to_send') && channel && !isChannelArchived && !isDeactivatedDM; + + const showRescheduleOption = !scheduledPost.error_code || scheduledPost.error_code === 'unknown' || scheduledPost.error_code === 'unable_to_send'; return (
@@ -111,36 +138,38 @@ function ScheduledPostActions({scheduledPost, onReschedule, onDelete, channelDis /> { - !scheduledPost.error_code && ( - - - - - - - - ) + /> } + { + showRescheduleOption && + + } + + { + showSendNowOption && + + }
); } diff --git a/webapp/channels/src/components/drafts/draft_row.tsx b/webapp/channels/src/components/drafts/draft_row.tsx index 20afd881e1..789ca48c87 100644 --- a/webapp/channels/src/components/drafts/draft_row.tsx +++ b/webapp/channels/src/components/drafts/draft_row.tsx @@ -15,7 +15,7 @@ import type {UserProfile, UserStatus} from '@mattermost/types/users'; import {getPost as getPostAction} from 'mattermost-redux/actions/posts'; import {deleteScheduledPost, updateScheduledPost} from 'mattermost-redux/actions/scheduled_posts'; import {Permissions} from 'mattermost-redux/constants'; -import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; +import {isDeactivatedDirectChannel, makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'; @@ -107,13 +107,17 @@ function DraftRow({ const connectionId = useSelector(getConnectionId); + const isChannelArchived = Boolean(channel?.delete_at); + const isDeactivatedDM = useSelector((state: GlobalState) => isDeactivatedDirectChannel(state, channelId)); + let postError = ''; if (isScheduledPost) { // This is applicable only for scheduled post. if (item.error_code) { postError = getErrorStringFromCode(intl, item.error_code); - postError = getErrorStringFromCode(intl, item.error_code); + } else if (isChannelArchived || isDeactivatedDM) { + postError = getErrorStringFromCode(intl, 'channel_archived'); } } else if (rootPostDeleted) { postError = intl.formatMessage({id: 'drafts.error.post_not_found', defaultMessage: 'Thread not found'}); @@ -279,19 +283,15 @@ function DraftRow({ isScheduledPostBeingSent.current = true; const postDraft = scheduledPostToPostDraft(item as ScheduledPost); - handleOnSend(postDraft, undefined, {keepDraft: true}); + handleOnSend(postDraft, undefined, {keepDraft: true, ignorePostError: true}); return Promise.resolve({}); }, [handleOnSend, item, handleCancelEdit]); const scheduledPostActions = useMemo(() => { - if (!channel) { - return null; - } - return ( ({ }); export function getErrorStringFromCode(intl: IntlShape, errorCode: ScheduledPostErrorCode = 'unknown') { - return intl.formatMessage(errorCodeToErrorMessage[errorCode]).toUpperCase(); + const textDefinition = errorCodeToErrorMessage[errorCode] ?? errorCodeToErrorMessage.unknown; + return intl.formatMessage(textDefinition).toUpperCase(); } diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 209d540c0c..19ede2b6cb 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -4868,6 +4868,7 @@ "scheduled_post.channel_indicator.single": "Message scheduled for {dateTime}.", "scheduled_post.channel_indicator.with_other_user_late_time": "You have {count, plural, =1 {one} other {#}} scheduled {count, plural, =1 {message} other {messages}}.", "scheduled_post.delete_modal.body": "Are you sure you want to delete this scheduled post to {displayName}?", + "scheduled_post.delete_modal.body_no_channel": "Are you sure you want to delete this scheduled post?", "scheduled_post.delete_modal.title": "Delete scheduled post", "scheduled_post.error_code.channel_archived": "Channel Archived", "scheduled_post.error_code.channel_removed": "Channel Removed", 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 fdb4014a59..a17fb2ffb2 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 @@ -1341,7 +1341,7 @@ export function searchChannelsInPolicy(state: GlobalState, policyId: string, ter export function getDirectTeammate(state: GlobalState, channelId: string): UserProfile | undefined { const channel = getChannel(state, channelId); - if (!channel) { + if (!channel || channel.type !== 'D') { return undefined; } @@ -1444,3 +1444,8 @@ export const getRecentProfilesFromDMs: (state: GlobalState) => UserProfile[] = c return [...sortedUserProfiles]; }, ); + +export const isDeactivatedDirectChannel = (state: GlobalState, channelId: string) => { + const teammate = getDirectTeammate(state, channelId); + return Boolean(teammate && teammate.delete_at); +};