diff --git a/src/components/MessageComposer/MessageComposerUI.tsx b/src/components/MessageComposer/MessageComposerUI.tsx index f70a909060..ecb03aad86 100644 --- a/src/components/MessageComposer/MessageComposerUI.tsx +++ b/src/components/MessageComposer/MessageComposerUI.tsx @@ -36,6 +36,7 @@ import { } from 'stream-chat'; import { CommandChip as DefaultCommandChip } from './CommandChip'; import { GeolocationPreview } from './AttachmentPreviewList/GeolocationPreview'; +import { ScheduledMessagesBanner as DefaultScheduledMessagesBanner } from '../ScheduledMessages/ScheduledMessagesBanner'; const messageComposerStateSelector = ({ editedMessage, @@ -151,6 +152,7 @@ export const MessageComposerUI = () => { AttachmentSelector = message ? SimpleAttachmentSelector : DefaultAttachmentSelector, AudioRecorder = DefaultAudioRecorder, CommandChip = DefaultCommandChip, + ScheduledMessagesBanner = DefaultScheduledMessagesBanner, SendToChannelCheckbox = DefaultSendToChannelCheckbox, TextareaComposer = DefaultTextareaComposer, } = useComponentContext(); @@ -160,6 +162,7 @@ export const MessageComposerUI = () => { className='str-chat__message-composer-container' component='div' > + {!message && ScheduledMessagesBanner && }
document.getElementById(CHANNEL_CONTAINER_ID); +import { getChannelContainer } from './utils'; const getMenuDialogId = (threadId?: string | null) => threadId ? `${SCHEDULE_MESSAGE_MENU_ID}-thread` : SCHEDULE_MESSAGE_MENU_ID; diff --git a/src/components/ScheduledMessages/ScheduledMessageGroupHeader.tsx b/src/components/ScheduledMessages/ScheduledMessageGroupHeader.tsx new file mode 100644 index 0000000000..9159a2f0bf --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageGroupHeader.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { useTranslationContext } from '../../context'; +import { ScheduledMessageGroupType } from './constants'; +import type { ScheduledMessageGroupHeaderProps } from './types'; + +export const ScheduledMessageGroupHeader = ({ + group, +}: ScheduledMessageGroupHeaderProps) => { + const { t } = useTranslationContext(); + + const { day, type } = group; + + const title = + type === ScheduledMessageGroupType.Failed + ? t('Failed') + : t('timestamp/ScheduledMessageGroup', { timestamp: day }); + + return

{title}

; +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageItem.tsx b/src/components/ScheduledMessages/ScheduledMessageItem.tsx new file mode 100644 index 0000000000..5d1275f22c --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageItem.tsx @@ -0,0 +1,62 @@ +import clsx from 'clsx'; +import React, { useMemo } from 'react'; + +import { useChatContext, useTranslationContext } from '../../context'; +import { ChannelAvatar } from '../Avatar'; +import { useChannelPreviewInfo } from '../ChannelListItem/hooks/useChannelPreviewInfo'; +import { SummarizedMessagePreview } from '../SummarizedMessagePreview'; +import { ScheduledMessageItemStatus } from './ScheduledMessageItemStatus'; +import type { ScheduledMessageItemProps } from './types'; +import { isFailedScheduledMessage, toStoredScheduledMessagePreview } from './utils'; + +export const ScheduledMessageItem = ({ scheduledMessage }: ScheduledMessageItemProps) => { + const { t } = useTranslationContext(); + const { client } = useChatContext(); + + const { cid, scheduled_at } = scheduledMessage; + const channel = client.activeChannels[cid]; + const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({ + channel, + }); + + const previewMessage = useMemo( + () => toStoredScheduledMessagePreview(scheduledMessage), + [scheduledMessage], + ); + + const isFailed = isFailedScheduledMessage(scheduledMessage); + const sendTime = t('timestamp/ScheduledMessageSendTime', { + timestamp: new Date(scheduled_at), + }); + + return ( +
+ +
+
+
+ ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx b/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx new file mode 100644 index 0000000000..dd3cbb68c1 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageItemStatus.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { ScheduledMessageStatus } from 'stream-chat'; + +import { useComponentContextIcons, useTranslationContext } from '../../context'; +import type { ScheduledMessageItemStatusProps } from './types'; +import { translateScheduledMessageFailureReason } from './utils'; + +export const ScheduledMessageItemStatus = ({ + scheduledMessage, +}: ScheduledMessageItemStatusProps) => { + const { t } = useTranslationContext(); + const { IconExclamationTriangleFill, IconLoading } = useComponentContextIcons(); + + const { failure_reason, status } = scheduledMessage; + + if (status === ScheduledMessageStatus.Failed) { + return ( + + + {translateScheduledMessageFailureReason(t, failure_reason)} + + ); + } + + if (status === ScheduledMessageStatus.Sending) { + return ( + + + {t('Sending...')} + + ); + } + + return null; +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageList.tsx b/src/components/ScheduledMessages/ScheduledMessageList.tsx new file mode 100644 index 0000000000..4610fa6488 --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageList.tsx @@ -0,0 +1,78 @@ +import React from 'react'; + +import { useComponentContext, useTranslationContext } from '../../context'; +import { Button } from '../Button'; +import { + useScheduledMessageGroups, + useScheduledMessagesLoadState, + useScheduledMessagesPagination, +} from './hooks'; +import { ScheduledMessageGroupHeader } from './ScheduledMessageGroupHeader'; +import { ScheduledMessageItem as DefaultScheduledMessageItem } from './ScheduledMessageItem'; +import { ScheduledMessageListLoading } from './ScheduledMessageListLoading'; +import { ScheduledMessagesEmptyList } from './ScheduledMessagesEmptyList'; +import { ScheduledMessagesLoadError } from './ScheduledMessagesLoadError'; +import { ScheduledMessagesNoResults } from './ScheduledMessagesNoResults'; +import type { ScheduledMessageListProps } from './types'; + +export const ScheduledMessageList = ({ + cid, + parentId, + searchQuery, +}: ScheduledMessageListProps) => { + const { t } = useTranslationContext(); + const { ScheduledMessageItem = DefaultScheduledMessageItem } = useComponentContext(); + + const { groups, hasScheduledMessages } = useScheduledMessageGroups({ + cid, + parentId, + searchQuery, + }); + const { canLoadMore, isLoading, loadMore } = useScheduledMessagesPagination(); + const { hasLoadFailed, isFirstLoad, reload } = useScheduledMessagesLoadState(); + + if (!groups.length) { + if (hasScheduledMessages) return ; + + if (isFirstLoad) return ; + + if (hasLoadFailed) return ; + + return ; + } + + return ( +
+ {groups.map((group) => ( +
+ +
    + {group.scheduledMessages.map((scheduledMessage) => ( +
  • + +
  • + ))} +
+
+ ))} + {canLoadMore && ( + + )} +
+ ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx b/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx new file mode 100644 index 0000000000..53d6a0497a --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageListDialog.tsx @@ -0,0 +1,53 @@ +import React, { useCallback, useState } from 'react'; +import type { ChangeEvent } from 'react'; + +import { useComponentContextIcons, useTranslationContext } from '../../context'; +import { Prompt } from '../Dialog'; +import { TextInput } from '../Form'; +import { useScheduledMessageCount } from './hooks'; +import { ScheduledMessageList } from './ScheduledMessageList'; +import type { ScheduledMessageListDialogProps } from './types'; + +export const ScheduledMessageListDialog = ({ + cid, + close, + parentId, +}: ScheduledMessageListDialogProps) => { + const { t } = useTranslationContext(); + const { IconSearch } = useComponentContextIcons(); + + const scheduledMessageCount = useScheduledMessageCount({ cid, parentId }); + + const [searchQuery, setSearchQuery] = useState(''); + + const updateSearchQuery = useCallback( + ({ target }: ChangeEvent) => setSearchQuery(target.value), + [], + ); + + const title = parentId ? t('Scheduled replies') : t('Scheduled messages'); + + return ( + + + + {scheduledMessageCount > 0 && ( + } + onChange={updateSearchQuery} + placeholder={t('Search')} + type='search' + value={searchQuery} + /> + )} + + + + ); +}; diff --git a/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx b/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx new file mode 100644 index 0000000000..60093e56ab --- /dev/null +++ b/src/components/ScheduledMessages/ScheduledMessageListLoading.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { useTranslationContext } from '../../context'; +import { SCHEDULED_MESSAGE_LIST_LOADING_ROW_COUNT } from './constants'; + +const LOADING_ROW_KEYS = Array.from( + { length: SCHEDULED_MESSAGE_LIST_LOADING_ROW_COUNT }, + (_, index) => `loading-row-${index}`, +); + +export const ScheduledMessageListLoading = () => { + const { t } = useTranslationContext(); + + return ( +
+ {LOADING_ROW_KEYS.map((key) => ( +