diff --git a/package/src/components/Attachment/Audio/AudioAttachment.tsx b/package/src/components/Attachment/Audio/AudioAttachment.tsx index 14b4eca54f..7e5674a095 100644 --- a/package/src/components/Attachment/Audio/AudioAttachment.tsx +++ b/package/src/components/Attachment/Audio/AudioAttachment.tsx @@ -168,7 +168,6 @@ export const AudioAttachment = (props: AudioAttachmentProps) => { const dragEnd = async (currentProgress: number) => { const positionInSeconds = (currentProgress * duration) / ONE_SECOND_IN_MILLISECONDS; await audioPlayer.seek(positionInSeconds); - audioPlayer.play(); }; const onSpeedChangeHandler = async () => { diff --git a/package/src/components/Message/MessageItemView/MessageFooter.tsx b/package/src/components/Message/MessageItemView/MessageFooter.tsx index d5b06caf18..c0ada724f9 100644 --- a/package/src/components/Message/MessageItemView/MessageFooter.tsx +++ b/package/src/components/Message/MessageItemView/MessageFooter.tsx @@ -37,7 +37,7 @@ type MessageFooterPropsWithContext = Pick< | 'lastGroupMessage' | 'isMessageAIGenerated' > & - Pick & + Pick & MessageFooterComponentProps; const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => { @@ -50,6 +50,7 @@ const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => { members, message, MessageStatus, + MessageTimestamp, showMessageStatus, } = props; const styles = useStyles(); @@ -77,9 +78,12 @@ const MessageFooterWithContext = (props: MessageFooterPropsWithContext) => { return ( {Object.keys(members).length > 2 && alignment === 'left' && message.user?.name ? ( - {message.user.name} + + {message.user.name} + ) : null} - {showMessageStatus && } + {showMessageStatus ? : null} + {isEdited ? {t('Edited')} : null} ); @@ -201,6 +205,7 @@ const useStyles = () => { return useMemo(() => { return StyleSheet.create({ container: { + maxWidth: '100%', alignItems: 'center', flexDirection: 'row', justifyContent: 'center', @@ -208,6 +213,7 @@ const useStyles = () => { gap: primitives.spacingXs, }, name: { + flexShrink: 1, color: shouldUseOverlayStyles ? semantics.textOnAccent : semantics.chatTextUsername, fontSize: primitives.typographyFontSizeXs, fontWeight: primitives.typographyFontWeightSemiBold, diff --git a/package/src/components/Message/MessageItemView/MessageStatus.tsx b/package/src/components/Message/MessageItemView/MessageStatus.tsx index 348cd07e15..80761846c7 100644 --- a/package/src/components/Message/MessageItemView/MessageStatus.tsx +++ b/package/src/components/Message/MessageItemView/MessageStatus.tsx @@ -6,10 +6,6 @@ import { MessageContextValue, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { Check } from '../../../icons/Check'; import { CheckAll } from '../../../icons/CheckAll'; @@ -21,14 +17,10 @@ import { useShouldUseOverlayStyles } from '../hooks/useShouldUseOverlayStyles'; export type MessageStatusPropsWithContext = Pick< MessageContextValue, 'deliveredToCount' | 'message' | 'readBy' -> & - Pick & { - formattedDate?: string | Date; - timestamp?: string | Date; - }; +>; const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { - const { deliveredToCount, formattedDate, message, readBy, timestamp, MessageTimestamp } = props; + const { deliveredToCount, message, readBy } = props; const styles = useStyles(); @@ -92,7 +84,6 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { {...checkIcon} /> ) : null} - ); }; @@ -101,20 +92,8 @@ const areEqual = ( prevProps: MessageStatusPropsWithContext, nextProps: MessageStatusPropsWithContext, ) => { - const { - deliveredToCount: prevDeliveredBy, - message: prevMessage, - readBy: prevReadBy, - formattedDate: prevFormattedDate, - timestamp: prevTimestamp, - } = prevProps; - const { - deliveredToCount: nextDeliveredBy, - message: nextMessage, - readBy: nextReadBy, - formattedDate: nextFormattedDate, - timestamp: nextTimestamp, - } = nextProps; + const { deliveredToCount: prevDeliveredBy, message: prevMessage, readBy: prevReadBy } = prevProps; + const { deliveredToCount: nextDeliveredBy, message: nextMessage, readBy: nextReadBy } = nextProps; const deliveredByEqual = prevDeliveredBy === nextDeliveredBy; if (!deliveredByEqual) { @@ -132,16 +111,6 @@ const areEqual = ( return false; } - const timestampEqual = prevTimestamp === nextTimestamp; - if (!timestampEqual) { - return false; - } - - const formattedDateEqual = prevFormattedDate === nextFormattedDate; - if (!formattedDateEqual) { - return false; - } - return true; }; @@ -155,7 +124,6 @@ export type MessageStatusProps = Partial; export const MessageStatus = (props: MessageStatusProps) => { const { channel } = useChannelContext(); const { deliveredToCount, message, readBy } = useMessageContext(); - const { MessageTimestamp } = useMessagesContext(); const channelMembersCount = Object.keys(channel?.state.members).length; @@ -167,7 +135,6 @@ export const MessageStatus = (props: MessageStatusProps) => { deliveredToCount, message, readBy, - MessageTimestamp, }} {...props} /> diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.tsx index 5fa94cbc50..1b3e341d8b 100644 --- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.tsx +++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingPreview.tsx @@ -108,7 +108,6 @@ export const AudioRecordingPreview = () => { const dragEnd = useStableCallback(async (currentProgress: number) => { const positionInSeconds = (currentProgress * duration) / ONE_SECOND_IN_MILLISECONDS; await audioPlayer.seek(positionInSeconds); - audioPlayer.play(); }); return ( diff --git a/package/src/components/MessageInput/hooks/useAudioRecorder.tsx b/package/src/components/MessageInput/hooks/useAudioRecorder.tsx index 2a5aacafca..d691d31740 100644 --- a/package/src/components/MessageInput/hooks/useAudioRecorder.tsx +++ b/package/src/components/MessageInput/hooks/useAudioRecorder.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect } from 'react'; import { LocalVoiceRecordingAttachment } from 'stream-chat'; @@ -12,14 +12,11 @@ import { resampleWaveformData } from '../utils/audioSampling'; /** * The hook that controls all the async audio core features including start/stop or recording, player, upload/delete of the recorded audio. - * - * FIXME: Change the name to `useAudioRecorder` in the next major version as the hook will only be used for audio recording. */ export const useAudioRecorder = ({ audioRecorderManager, sendMessage, }: Pick) => { - const [isScheduledForSubmit, setIsScheduleForSubmit] = useState(false); const { attachmentManager } = useMessageComposer(); /** @@ -43,13 +40,6 @@ export const useAudioRecorder = ({ [stopVoiceRecording], ); - useEffect(() => { - if (isScheduledForSubmit) { - sendMessage(); - setIsScheduleForSubmit(false); - } - }, [isScheduledForSubmit, sendMessage]); - /** * Function to start voice recording. Will return whether access is granted * with regards to the microphone permission as that's how the underlying @@ -113,8 +103,8 @@ export const useAudioRecorder = ({ audioRecorderManager.reset(); if (sendOnComplete) { - await attachmentManager.uploadAttachment(audioFile); - setIsScheduleForSubmit(true); + attachmentManager.upsertAttachments([audioFile]); + sendMessage(); } else { await attachmentManager.uploadAttachment(audioFile); } @@ -122,7 +112,7 @@ export const useAudioRecorder = ({ console.log('Error uploading voice recording: ', error); } }, - [audioRecorderManager, attachmentManager, stopVoiceRecording], + [audioRecorderManager, attachmentManager, sendMessage, stopVoiceRecording], ); return { diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index b55a003ebc..4020f79dd8 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -620,6 +620,7 @@ exports[`Thread should match thread snapshot 1`] = ` "flexDirection": "row", "gap": 8, "justifyContent": "center", + "maxWidth": "100%", "paddingVertical": 4, }, {}, @@ -627,6 +628,18 @@ exports[`Thread should match thread snapshot 1`] = ` } testID="message-status-time" > + + 2:50 PM + + + 2:50 PM + + + 2:50 PM + + + 2:50 PM + ({ + text: state.text, +}); + +const attachmentManagerStateSelector = (state: AttachmentManagerState) => ({ + attachments: state.attachments, +}); + export const ThreadListItemComponent = () => { const { channel, dateString, deletedAtDateString, + draftMessage, lastReply, ownUnreadMessageCount, parentMessage, @@ -57,10 +73,7 @@ export const ThreadListItemComponent = () => { const styles = useStyles(); const { t } = useTranslationContext(); - useEffect(() => { - const unsubscribe = thread.messageComposer.registerDraftEventSubscriptions(); - return () => unsubscribe(); - }, [thread.messageComposer]); + const shouldRenderPreview = !!draftMessage || !!lastReply; return ( @@ -87,12 +100,14 @@ export const ThreadListItemComponent = () => { {displayName || 'N/A'} - {lastReply ? ( + {shouldRenderPreview ? ( - + {!draftMessage ? ( + + ) : null} ) : null} @@ -129,6 +144,14 @@ export const ThreadListItem = (props: ThreadListItemProps) => { const { t, tDateTimeParser } = useTranslationContext(); const { thread, timestampTranslationKey = 'timestamp/ThreadListItem' } = props; const { ThreadListItem = ThreadListItemComponent } = useThreadsContext(); + const { text: draftText } = useStateStore( + thread.messageComposer.textComposer.state, + textComposerStateSelector, + ); + const { attachments } = useStateStore( + thread.messageComposer.attachmentManager.state, + attachmentManagerStateSelector, + ); const selector = useCallback( (nextValue: ThreadState) => @@ -150,6 +173,27 @@ export const ThreadListItem = (props: ThreadListItemProps) => { const timestamp = lastReply?.created_at; + useEffect(() => { + const unsubscribe = thread.messageComposer.registerDraftEventSubscriptions(); + return () => unsubscribe(); + }, [thread.messageComposer]); + + const draftMessage = useMemo(() => { + if (thread.messageComposer.compositionIsEmpty) { + return undefined; + } + + if (!draftText && !attachments?.length) { + return undefined; + } + + return { + attachments, + id: thread.messageComposer.id, + text: draftText ?? '', + }; + }, [attachments, draftText, thread.messageComposer]); + // TODO: Please rethink this, we have the same line of code in about 5 places in the SDK. const dateString = useMemo( () => @@ -178,6 +222,7 @@ export const ThreadListItem = (props: ThreadListItemProps) => { channel, dateString, deletedAtDateString, + draftMessage, lastReply, ownUnreadMessageCount, parentMessage, @@ -224,6 +269,7 @@ const useStyles = () => { }, previewMessageContainer: { flexDirection: 'row', + alignItems: 'center', gap: primitives.spacingXxs, ...threadListItem.previewMessageContainer, }, diff --git a/package/src/components/ThreadList/ThreadListItemMessagePreview.tsx b/package/src/components/ThreadList/ThreadListItemMessagePreview.tsx index 5a07d28643..3f4bc875de 100644 --- a/package/src/components/ThreadList/ThreadListItemMessagePreview.tsx +++ b/package/src/components/ThreadList/ThreadListItemMessagePreview.tsx @@ -4,6 +4,8 @@ import { View, Text, StyleSheet } from 'react-native'; import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { useThreadListItemContext } from '../../contexts/threadsContext/ThreadListItemContext'; +import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; import { useMessagePreviewIcon, useMessagePreviewText } from '../../hooks'; import { primitives } from '../../theme'; @@ -12,15 +14,20 @@ export type ThreadListItemMessagePreviewProps = { }; export const ThreadListItemMessagePreview = ({ message }: ThreadListItemMessagePreviewProps) => { + const { draftMessage } = useThreadListItemContext(); const { theme: { semantics }, } = useTheme(); const styles = useStyles(); - const MessagePreviewIcon = useMessagePreviewIcon({ message }); - const messagePreviewTitle = useMessagePreviewText({ message }); + const { t } = useTranslationContext(); + const previewMessage = draftMessage ?? message; + const MessagePreviewIcon = useMessagePreviewIcon({ message: previewMessage }); + const messagePreviewTitle = useMessagePreviewText({ message: previewMessage }); + const isDraft = !!draftMessage; return ( + {isDraft ? {t('Draft')}: : null} {MessagePreviewIcon ? ( ) : null} @@ -56,6 +63,14 @@ const useStyles = () => { flexShrink: 1, ...messagePreview.subtitle, }, + draftText: { + color: semantics.accentPrimary, + fontSize: primitives.typographyFontSizeMd, + fontWeight: primitives.typographyFontWeightSemiBold, + includeFontPadding: false, + lineHeight: primitives.typographyLineHeightNormal, + ...messagePreview.draftText, + }, }); - }, [messagePreview.container, messagePreview.subtitle, semantics.textPrimary]); + }, [messagePreview.container, messagePreview.draftText, messagePreview.subtitle, semantics]); }; diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index b59fc80a99..ac9f9e5584 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -238,6 +238,7 @@ export type Theme = { }; messagePreview: { container: ViewStyle; + draftText: TextStyle; subtitle: TextStyle; }; }; @@ -989,6 +990,7 @@ export type Theme = { unreadBubbleWrapper: ViewStyle; messagePreview: { container: ViewStyle; + draftText: TextStyle; subtitle: TextStyle; }; messagePreviewDeliveryStatus: { @@ -1167,6 +1169,7 @@ export const defaultTheme: Theme = { wrapper: {}, messagePreview: { container: {}, + draftText: {}, subtitle: {}, }, }, @@ -1893,6 +1896,7 @@ export const defaultTheme: Theme = { messageRepliesText: {}, messagePreview: { container: {}, + draftText: {}, subtitle: {}, }, messagePreviewDeliveryStatus: { diff --git a/package/src/contexts/threadsContext/ThreadListItemContext.tsx b/package/src/contexts/threadsContext/ThreadListItemContext.tsx index 6a1bcd1e62..bb530fa658 100644 --- a/package/src/contexts/threadsContext/ThreadListItemContext.tsx +++ b/package/src/contexts/threadsContext/ThreadListItemContext.tsx @@ -1,6 +1,6 @@ import React, { PropsWithChildren, useContext } from 'react'; -import { Channel, LocalMessage, Thread } from 'stream-chat'; +import { Channel, DraftMessage, LocalMessage, Thread } from 'stream-chat'; import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; @@ -8,6 +8,7 @@ export type ThreadListItemContextValue = { channel: Channel; dateString: string | number | undefined; deletedAtDateString: string | number | undefined; + draftMessage?: DraftMessage; lastReply: LocalMessage | undefined; ownUnreadMessageCount: number; parentMessage: LocalMessage | undefined;