diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index 8588b115b8..e0b4036c99 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -1254,7 +1254,7 @@ type UserContactMethod { value: String! @deprecated(reason: "Use dest instead.") @goField(forceResolver: true) - formattedValue: String! + formattedValue: String! @deprecated(reason: "Use dest.displayInfo instead.") disabled: Boolean! pending: Boolean! diff --git a/web/src/app/users/SendTestDialog.tsx b/web/src/app/users/SendTestDialog.tsx index abb4b4577e..8ab7718b21 100644 --- a/web/src/app/users/SendTestDialog.tsx +++ b/web/src/app/users/SendTestDialog.tsx @@ -1,8 +1,6 @@ import React, { useEffect, MouseEvent, useState } from 'react' import { gql, useQuery, useMutation } from 'urql' - import Spinner from '../loading/components/Spinner' - import { Button, Dialog, @@ -12,16 +10,23 @@ import { DialogContentText, } from '@mui/material' import toTitleCase from '../util/toTitleCase' -import DialogContentError from '../dialogs/components/DialogContentError' -import { DateTime } from 'luxon' -import { ContactMethodType, UserContactMethod } from '../../schema' +import { DateTime, Duration } from 'luxon' +import { + DestinationInput, + NotificationState, + UserContactMethod, +} from '../../schema' +import DestinationInputChip from '../util/DestinationInputChip' +import { Time } from '../util/Time' const query = gql` query ($id: ID!) { userContactMethod(id: $id) { id - type - formattedValue + dest { + type + args + } lastTestVerifyAt lastTestMessageState { details @@ -38,103 +43,137 @@ const mutation = gql` } ` +function getTestStatusColor(status?: string | null): string { + switch (status) { + case 'OK': + return 'success' + case 'ERROR': + return 'error' + default: + return 'warning' + } +} + +type SendTestContentProps = { + dest: DestinationInput + isSending: boolean + isWaiting: boolean + + sentTime?: string | null + sentState?: NotificationState | null +} + +export function SendTestContent(props: SendTestContentProps): React.ReactNode { + return ( + + + + GoAlert is sending a test to{' '} + . + + + {props.sentTime && ( + + The test message was scheduled for delivery at{' '} + + )} + {props.sentState?.formattedSrcValue && ( + + Sender: {props.sentState.formattedSrcValue} + + )} + + {props.isWaiting && } + {props.isSending && } + {props.sentState?.details === 'Pending' ? ( + + ) : ( + + Status: {toTitleCase(props.sentState?.details || '')} + + )} + + + ) +} + export default function SendTestDialog( props: SendTestDialogProps, ): JSX.Element { - const { title = 'Test Delivery Status', onClose, messageID } = props - + const { onClose, contactMethodID } = props const [sendTestStatus, sendTest] = useMutation(mutation) - const [{ data, fetching, error }] = useQuery<{ + const [cmInfo, refreshCMInfo] = useQuery<{ userContactMethod: UserContactMethod }>({ query, variables: { - id: messageID, + id: contactMethodID, }, requestPolicy: 'network-only', }) - // We keep a stable timestamp to track how long the dialog has been open - const [now] = useState(DateTime.utc()) - const status = data?.userContactMethod?.lastTestMessageState?.status ?? '' - const cmDestValue = data?.userContactMethod?.formattedValue ?? '' - const cmType: ContactMethodType | '' = data?.userContactMethod.type ?? '' - const lastSent = data?.userContactMethod?.lastTestVerifyAt - ? DateTime.fromISO(data.userContactMethod.lastTestVerifyAt) - : now.plus({ day: -1 }) - const fromValue = - data?.userContactMethod?.lastTestMessageState?.formattedSrcValue ?? '' - const errorMessage = (error?.message || sendTestStatus.error?.message) ?? '' - - const hasSent = - Boolean(data?.userContactMethod.lastTestMessageState) && - now.diff(lastSent).as('seconds') < 60 + // Should not happen, but just in case. + if (cmInfo.error) throw cmInfo.error + const cm = cmInfo.data?.userContactMethod + if (!cm) throw new Error('missing contact method') // should be impossible (since we already checked the error) + + // We expect the status to update over time, so we manually refresh + // as long as the dialog is open. useEffect(() => { - if (fetching || !data?.userContactMethod) return - if (errorMessage || sendTestStatus.data) return - - if (hasSent) return - - sendTest({ id: messageID }) - }, [lastSent.toISO(), fetching, data, errorMessage, sendTestStatus.data]) - - const details = - (hasSent && data?.userContactMethod?.lastTestMessageState?.details) || '' - - const isLoading = - sendTestStatus.fetching || - (!!details && !!errorMessage) || - ['pending', 'sending'].includes(details.toLowerCase()) - - const getTestStatusColor = (status: string): string => { - switch (status) { - case 'OK': - return 'success' - case 'ERROR': - return 'error' - default: - return 'warning' - } - } + const t = setInterval(refreshCMInfo, 3000) + return () => clearInterval(t) + }, []) - const msg = (): string => { - switch (cmType) { - case 'SMS': - case 'VOICE': - return `${ - cmType === 'SMS' ? 'SMS message' : 'voice call' - } to ${cmDestValue}` - case 'EMAIL': - return `email to ${cmDestValue}` - default: - return `to ${cmDestValue}` - } - } + // We keep a stable timestamp to track how long the dialog has been open. + const [now] = useState(DateTime.utc()) - return ( - - {title} + const isWaitingToSend = + (cm.lastTestVerifyAt + ? now.diff(DateTime.fromISO(cm.lastTestVerifyAt)) + : Duration.fromObject({ day: 1 }) + ).as('seconds') < 60 - - - GoAlert is sending a test {msg()}. - - {isLoading && } - {fromValue && ( - - The test message was sent from {fromValue}. - - )} - {!!details && ( - - {toTitleCase(details)} - - )} - + // already sent a test message recently + const [alreadySent, setAlreadySent] = useState( + !!cm.lastTestMessageState && isWaitingToSend, + ) - {errorMessage && } + useEffect(() => { + if (alreadySent) return + + // wait until at least a minute has passed + if (isWaitingToSend) return + + sendTest( + { id: contactMethodID }, + { + additionalTypenames: ['UserContactMethod'], + }, + ) + setAlreadySent(true) + }, [isWaitingToSend, alreadySent]) + + return ( + + Test Contact Method +