From 529d286c2fe274dc03f0bf1888e07eab820a3bb9 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 26 Feb 2026 15:32:18 +0530 Subject: [PATCH 1/9] fix: gallery layout issue --- package/src/components/Attachment/Gallery.tsx | 16 ++++++++++++++-- .../src/components/Attachment/GalleryImage.tsx | 6 +++--- .../buildGallery/buildGalleryOfSingleImage.ts | 1 + .../Message/MessageSimple/MessageContent.tsx | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/package/src/components/Attachment/Gallery.tsx b/package/src/components/Attachment/Gallery.tsx index ff41031538..41c4731020 100644 --- a/package/src/components/Attachment/Gallery.tsx +++ b/package/src/components/Attachment/Gallery.tsx @@ -43,6 +43,7 @@ import { getUrlWithoutParams } from '../../utils/utils'; export type GalleryPropsWithContext = Pick & Pick< MessageContextValue, + | 'alignment' | 'images' | 'videos' | 'onLongPress' @@ -69,6 +70,7 @@ export type GalleryPropsWithContext = Pick { const { additionalPressableProps, + alignment, imageGalleryStateStore, ImageLoadingFailedIndicator, ImageLoadingIndicator, @@ -141,6 +143,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => { styles.container, { flexDirection: invertedDirections ? 'column' : 'row', + alignSelf: alignment === 'right' ? 'flex-end' : 'flex-start', }, images.length !== 1 ? { width: gridWidth, height: gridHeight } @@ -439,18 +442,25 @@ const GalleryImageThumbnail = ({ const areEqual = (prevProps: GalleryPropsWithContext, nextProps: GalleryPropsWithContext) => { const { + alignment: prevAlignment, images: prevImages, message: prevMessage, myMessageTheme: prevMyMessageTheme, videos: prevVideos, } = prevProps; const { + alignment: nextAlignment, images: nextImages, message: nextMessage, myMessageTheme: nextMyMessageTheme, videos: nextVideos, } = nextProps; + const alignmentEqual = prevAlignment === nextAlignment; + if (!alignmentEqual) { + return false; + } + const messageEqual = prevMessage?.id === nextMessage?.id && `${prevMessage?.updated_at}` === `${nextMessage?.updated_at}`; @@ -498,6 +508,7 @@ export type GalleryProps = Partial; */ export const Gallery = (props: GalleryProps) => { const { + alignment: propAlignment, additionalPressableProps: propAdditionalPressableProps, ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator, ImageLoadingIndicator: PropImageLoadingIndicator, @@ -517,6 +528,7 @@ export const Gallery = (props: GalleryProps) => { const { imageGalleryStateStore } = useImageGalleryContext(); const { + alignment: contextAlignment, images: contextImages, message: contextMessage, onLongPress: contextOnLongPress, @@ -539,7 +551,7 @@ export const Gallery = (props: GalleryProps) => { const images = propImages || contextImages; const videos = propVideos || contextVideos; const message = propMessage || contextMessage; - + const alignment = propAlignment || contextAlignment; if (!images.length && !videos.length) { return null; } @@ -568,6 +580,7 @@ export const Gallery = (props: GalleryProps) => { { }, container: { flexDirection: 'row', - justifyContent: 'center', gap: primitives.spacingXxs, }, imageContainer: {}, diff --git a/package/src/components/Attachment/GalleryImage.tsx b/package/src/components/Attachment/GalleryImage.tsx index f8e252c98f..fa174b0831 100644 --- a/package/src/components/Attachment/GalleryImage.tsx +++ b/package/src/components/Attachment/GalleryImage.tsx @@ -9,7 +9,7 @@ export type GalleryImageWithContextProps = GalleryImageProps & Pick; export const GalleryImageWithContext = (props: GalleryImageWithContextProps) => { - const { ImageComponent = Image, uri, ...rest } = props; + const { ImageComponent = Image, uri, style, ...rest } = props; // Caching image components such as FastImage will not work with local images. // This for the case of local uris, we use the default Image component. @@ -18,7 +18,7 @@ export const GalleryImageWithContext = (props: GalleryImageWithContextProps) => { return ( From 14350365ce4b70b527faffa11b8b0e9f2422d6cd Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 27 Feb 2026 04:59:58 +0530 Subject: [PATCH 2/9] feat: message headers redesign --- .../Reminders/MessageReminderHeader.tsx | 92 ---------- examples/SampleApp/src/icons/Bell.tsx | 22 --- .../SampleApp/src/screens/ChannelScreen.tsx | 4 +- .../SampleApp/src/screens/ThreadScreen.tsx | 21 ++- examples/SampleApp/src/types.ts | 1 + .../SampleApp/src/utils/messageActions.tsx | 4 +- package/src/components/Channel/Channel.tsx | 4 +- .../Channel/hooks/useCreateThreadContext.ts | 2 + .../ChannelPreview/ChannelPreviewMessage.tsx | 2 +- package/src/components/Message/Message.tsx | 1 + .../Headers/MessageReminderHeader.tsx | 139 +++++++++++++++ .../Headers/MessageSavedForLaterHeader.tsx | 55 ++++++ .../Headers/SentToChannelHeader.tsx | 159 ++++++++++++++++++ .../Message/MessageSimple/MessageHeader.tsx | 86 +++++++++- .../Message/hooks/useCreateMessageContext.ts | 2 + .../messageContext/MessageContext.tsx | 7 + .../src/contexts/themeContext/utils/theme.ts | 32 ++++ .../contexts/threadContext/ThreadContext.tsx | 6 + package/src/i18n/en.json | 9 +- package/src/i18n/es.json | 7 +- package/src/i18n/fr.json | 7 +- package/src/i18n/he.json | 7 +- package/src/i18n/hi.json | 7 +- package/src/i18n/it.json | 7 +- package/src/i18n/ja.json | 7 +- package/src/i18n/ko.json | 7 +- package/src/i18n/nl.json | 7 +- package/src/i18n/pt-br.json | 7 +- package/src/i18n/ru.json | 7 +- package/src/i18n/tr.json | 7 +- package/src/icons/ArrowUpRight.tsx | 18 ++ package/src/icons/Bell.tsx | 18 ++ package/src/icons/Bookmark.tsx | 17 ++ package/src/icons/index.ts | 1 + 34 files changed, 631 insertions(+), 148 deletions(-) delete mode 100644 examples/SampleApp/src/components/Reminders/MessageReminderHeader.tsx delete mode 100644 examples/SampleApp/src/icons/Bell.tsx create mode 100644 package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx create mode 100644 package/src/components/Message/MessageSimple/Headers/MessageSavedForLaterHeader.tsx create mode 100644 package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx create mode 100644 package/src/icons/ArrowUpRight.tsx create mode 100644 package/src/icons/Bell.tsx create mode 100644 package/src/icons/Bookmark.tsx diff --git a/examples/SampleApp/src/components/Reminders/MessageReminderHeader.tsx b/examples/SampleApp/src/components/Reminders/MessageReminderHeader.tsx deleted file mode 100644 index f57d827c40..0000000000 --- a/examples/SampleApp/src/components/Reminders/MessageReminderHeader.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - MessageHeaderProps, - MessagePinnedHeader, - Time, - useMessageContext, - useMessageReminder, - useStateStore, - useTheme, - useTranslationContext, -} from 'stream-chat-react-native'; -import { ReminderState } from 'stream-chat'; -import { StyleSheet, Text, View } from 'react-native'; - -const reminderStateSelector = (state: ReminderState) => ({ - timeLeftMs: state.timeLeftMs, -}); - -export const MessageReminderHeader = ({ message }: MessageHeaderProps) => { - const messageId = message?.id ?? ''; - const reminder = useMessageReminder(messageId); - const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {}; - const { t } = useTranslationContext(); - - const { - theme: { semantics }, - } = useTheme(); - - const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs; - const stopRefreshTimeStamp = - reminder?.remindAt && stopRefreshBoundaryMs - ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs - : undefined; - - const isBehindRefreshBoundary = - !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp; - - if (!reminder) { - return null; - } - - // This is for "Saved for Later" - if (!reminder.remindAt) { - return ( - - 🔖 Saved for Later - - ); - } - - if (reminder.remindAt && timeLeftMs !== null) { - return ( - - - ); - } -}; - -export const MessageHeader = () => { - const { message } = useMessageContext(); - return ( - <> - - - - ); -}; - -const styles = StyleSheet.create({ - headerContainer: { - flexDirection: 'row', - alignItems: 'center', - }, - headerTitle: { - fontSize: 14, - fontWeight: '500', - marginLeft: 4, - }, -}); diff --git a/examples/SampleApp/src/icons/Bell.tsx b/examples/SampleApp/src/icons/Bell.tsx deleted file mode 100644 index e2e669941a..0000000000 --- a/examples/SampleApp/src/icons/Bell.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import Svg, { Path } from 'react-native-svg'; -import { useTheme } from 'stream-chat-react-native'; - -import { IconProps } from '../utils/base'; - -export const Bell: React.FC = ({ height = 512, width = 512 }) => { - const { - theme: { - semantics - }, - } = useTheme(); - - return ( - - - - ); -}; diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 92c178100a..1edb3d9b93 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -176,7 +176,7 @@ export const ChannelScreen: React.FC = ({ }; const onThreadSelect = useCallback( - (thread: LocalMessage | null) => { + (thread: LocalMessage | null, targetedMessageId?: string) => { if (!thread || !channel) { return; } @@ -185,6 +185,7 @@ export const ChannelScreen: React.FC = ({ navigation.navigate('ThreadScreen', { channel, thread, + targetedMessageId, }); }, [channel, navigation, setThread], @@ -233,7 +234,6 @@ export const ChannelScreen: React.FC = ({ initialScrollToFirstUnreadMessage keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -300} messageActions={messageActions} - MessageHeader={MessageHeader} MessageLocation={MessageLocation} messageId={messageId} NetworkDownIndicator={() => null} diff --git a/examples/SampleApp/src/screens/ThreadScreen.tsx b/examples/SampleApp/src/screens/ThreadScreen.tsx index 75fd66e7f3..63a7adb0ca 100644 --- a/examples/SampleApp/src/screens/ThreadScreen.tsx +++ b/examples/SampleApp/src/screens/ThreadScreen.tsx @@ -15,12 +15,11 @@ import { useStateStore } from 'stream-chat-react-native'; import { ScreenHeader } from '../components/ScreenHeader'; -import type { RouteProp } from '@react-navigation/native'; +import { type RouteProp } from '@react-navigation/native'; import type { StackNavigatorParamList } from '../types'; import { LocalMessage, ThreadState, UserResponse } from 'stream-chat'; import { useCreateDraftFocusEffect } from '../utils/useCreateDraftFocusEffect.tsx'; -import { MessageReminderHeader } from '../components/Reminders/MessageReminderHeader.tsx'; import { channelMessageActions } from '../utils/messageActions.tsx'; import { useStreamChatContext } from '../context/StreamChatContext.tsx'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -74,7 +73,7 @@ const ThreadHeader: React.FC = ({ thread }) => { export const ThreadScreen: React.FC = ({ navigation, route: { - params: { channel, thread }, + params: { channel, thread, targetedMessageId }, }, }) => { const { @@ -87,7 +86,6 @@ export const ThreadScreen: React.FC = ({ const { t } = useTranslationContext(); const { setThread } = useStreamChatContext(); const { messageInputFloating, messageListImplementation } = useAppContext(); - const onPressMessage: NonNullable['onPressMessage']> = ( payload, ) => { @@ -118,6 +116,13 @@ export const ThreadScreen: React.FC = ({ setThread(null); }, [setThread]); + const onBackPressThread = useCallback( + (messageId?: string) => { + navigation.popTo('ChannelScreen', { messageId: messageId }); + }, + [navigation], + ); + return ( = ({ keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -300} messageActions={messageActions} messageInputFloating={messageInputFloating} - MessageHeader={MessageReminderHeader} MessageLocation={MessageLocation} onPressMessage={onPressMessage} thread={thread} threadList + onBackPressThread={onBackPressThread} + messageId={targetedMessageId} > - + ); diff --git a/examples/SampleApp/src/types.ts b/examples/SampleApp/src/types.ts index 1fc11fc7f6..621ffeb907 100644 --- a/examples/SampleApp/src/types.ts +++ b/examples/SampleApp/src/types.ts @@ -40,6 +40,7 @@ export type StackNavigatorParamList = { ThreadScreen: { channel: Channel; thread: LocalMessage | ThreadType; + targetedMessageId?: string; }; }; diff --git a/examples/SampleApp/src/utils/messageActions.tsx b/examples/SampleApp/src/utils/messageActions.tsx index f104737a4c..0c54000ae3 100644 --- a/examples/SampleApp/src/utils/messageActions.tsx +++ b/examples/SampleApp/src/utils/messageActions.tsx @@ -9,8 +9,8 @@ import { MessageActionsParams, Time, TranslationContextValue, + Bell, } from 'stream-chat-react-native'; -import { Bell } from '../icons/Bell'; import { Theme } from 'stream-chat-react-native'; export function channelMessageActions({ @@ -94,7 +94,7 @@ export function channelMessageActions({ }, actionType: reminder ? 'remove-reminder' : 'remind-me', title: reminder ? 'Remove Reminder' : 'Remind Me', - icon: , + icon: , type: 'standard', }); actions.push({ diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 7960b7af4d..ce9fa23b8a 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -404,7 +404,7 @@ export type ChannelPropsWithContext = Pick & > > & Partial> & - Partial> & { + Partial> & { shouldSyncChannel: boolean; thread: ThreadType; /** @@ -704,6 +704,7 @@ const ChannelWithContext = (props: PropsWithChildren) = onLongPressMessage, onPressInMessage, onPressMessage, + onBackPressThread, openPollCreationDialog, overrideOwnCapabilities, PollContent, @@ -1984,6 +1985,7 @@ const ChannelWithContext = (props: PropsWithChildren) = const threadContext = useCreateThreadContext({ allowThreadMessagesInChannel, + onBackPressThread, closeThread, loadMoreThread, openThread, diff --git a/package/src/components/Channel/hooks/useCreateThreadContext.ts b/package/src/components/Channel/hooks/useCreateThreadContext.ts index 43addaafe8..098dbe9596 100644 --- a/package/src/components/Channel/hooks/useCreateThreadContext.ts +++ b/package/src/components/Channel/hooks/useCreateThreadContext.ts @@ -12,6 +12,7 @@ const selector = (nextValue: ThreadState) => export const useCreateThreadContext = ({ allowThreadMessagesInChannel, + onBackPressThread, closeThread, loadMoreThread, openThread, @@ -39,6 +40,7 @@ export const useCreateThreadContext = ({ return { allowThreadMessagesInChannel, + onBackPressThread, closeThread, loadMoreThread, openThread, diff --git a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx index 0c363a29f3..ab2b663db0 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx @@ -79,7 +79,7 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => { if (draftMessage) { return ( - {t('Draft:')} + {t('Draft')}: {renderMessagePreview(draftMessage)} ); diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 17c8b7a662..ecbe268e3a 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -773,6 +773,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => { } } : null, + onThreadSelect, otherAttachments: attachments.other, preventPress: overlayActive ? true : preventPress, reactions, diff --git a/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx b/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx new file mode 100644 index 0000000000..5f2f45d96c --- /dev/null +++ b/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx @@ -0,0 +1,139 @@ +import React, { useMemo } from 'react'; + +import { StyleSheet, Text, View } from 'react-native'; + +import { ReminderState } from 'stream-chat'; + +import { + MessageContextValue, + useMessageContext, +} from '../../../../contexts/messageContext/MessageContext'; +import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; +import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext'; +import { useMessageReminder } from '../../../../hooks/useMessageReminder'; +import { useStateStore } from '../../../../hooks/useStateStore'; +import { Bell } from '../../../../icons'; +import { primitives } from '../../../../theme'; + +const reminderStateSelector = (state: ReminderState) => ({ + timeLeftMs: state.timeLeftMs, +}); + +type MessageReminderHeaderPropsWithContext = Pick & { + timeLeftMs?: number; + isReminderTimeLeft: boolean; +}; + +const MessageReminderHeaderWithContext = (props: MessageReminderHeaderPropsWithContext) => { + const { timeLeftMs, isReminderTimeLeft } = props; + const { + theme: { semantics }, + } = useTheme(); + const { t } = useTranslationContext(); + const styles = useStyles(); + + return ( + + + + {isReminderTimeLeft ? t('Reminder set') : t('Reminder overdue')} + + · + + {t('{{ timeLeft }}', { + timeLeft: t('duration/Message reminder', { + milliseconds: timeLeftMs, + }), + })} + + + ); +}; + +const areEqual = ( + prevProps: MessageReminderHeaderPropsWithContext, + nextProps: MessageReminderHeaderPropsWithContext, +) => { + const { timeLeftMs: prevTimeLeftMs, isReminderTimeLeft: prevIsReminderTimeLeft } = prevProps; + const { timeLeftMs: nextTimeLeftMs, isReminderTimeLeft: nextIsReminderTimeLeft } = nextProps; + + const timeLeftMsEqual = prevTimeLeftMs === nextTimeLeftMs; + if (!timeLeftMsEqual) { + return false; + } + + const isReminderTimeLeftEqual = prevIsReminderTimeLeft === nextIsReminderTimeLeft; + if (!isReminderTimeLeftEqual) { + return false; + } + + return true; +}; + +const MemoizedMessageReminderHeader = React.memo( + MessageReminderHeaderWithContext, + areEqual, +) as typeof MessageReminderHeaderWithContext; + +export type MessageReminderHeaderProps = Partial; + +export const MessageReminderHeader = (props: MessageReminderHeaderProps) => { + const { message } = useMessageContext(); + const reminder = useMessageReminder(message.id); + const { timeLeftMs } = useStateStore(reminder?.state, reminderStateSelector) ?? {}; + + const isReminderTimeLeft = !!(timeLeftMs && timeLeftMs > 0); + + return ( + + ); +}; + +const useStyles = () => { + const { + theme: { + semantics, + messageSimple: { + reminderHeader: { container, label, dot, time }, + }, + }, + } = useTheme(); + + return useMemo(() => { + return StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + gap: primitives.spacingXxs, + paddingVertical: primitives.spacingXxs, + ...container, + }, + label: { + color: semantics.textPrimary, + fontSize: primitives.typographyFontSizeXs, + fontWeight: primitives.typographyFontWeightSemiBold, + lineHeight: primitives.typographyLineHeightTight, + ...label, + }, + dot: { + color: semantics.textPrimary, + fontSize: primitives.typographyFontSizeXs, + fontWeight: primitives.typographyFontWeightRegular, + lineHeight: primitives.typographyLineHeightTight, + ...dot, + }, + time: { + color: semantics.textPrimary, + fontSize: primitives.typographyFontSizeXs, + fontWeight: primitives.typographyFontWeightRegular, + lineHeight: primitives.typographyLineHeightTight, + ...time, + }, + }); + }, [container, semantics, label, dot, time]); +}; diff --git a/package/src/components/Message/MessageSimple/Headers/MessageSavedForLaterHeader.tsx b/package/src/components/Message/MessageSimple/Headers/MessageSavedForLaterHeader.tsx new file mode 100644 index 0000000000..96b64554d5 --- /dev/null +++ b/package/src/components/Message/MessageSimple/Headers/MessageSavedForLaterHeader.tsx @@ -0,0 +1,55 @@ +import React, { useMemo } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +import { MessageContextValue } from '../../../../contexts/messageContext/MessageContext'; +import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; +import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext'; +import { Bookmark } from '../../../../icons/Bookmark'; +import { primitives } from '../../../../theme'; + +export type MessageSavedForLaterHeaderProps = Partial>; + +export const MessageSavedForLaterHeader = () => { + const { + theme: { semantics }, + } = useTheme(); + const styles = useStyles(); + const { t } = useTranslationContext(); + + return ( + + + {t('Saved For Later')} + + ); +}; + +const useStyles = () => { + const { + theme: { + semantics, + messageSimple: { + savedForLaterHeader: { container, label }, + }, + }, + } = useTheme(); + + return useMemo(() => { + return StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + gap: primitives.spacingXxs, + paddingVertical: primitives.spacingXxs, + ...container, + }, + label: { + color: semantics.accentPrimary, + fontSize: primitives.typographyFontSizeXs, + fontWeight: primitives.typographyFontWeightSemiBold, + lineHeight: primitives.typographyLineHeightTight, + ...label, + }, + }); + }, [semantics, container, label]); +}; diff --git a/package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx b/package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx new file mode 100644 index 0000000000..54443a8956 --- /dev/null +++ b/package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx @@ -0,0 +1,159 @@ +import React, { useMemo } from 'react'; + +import { Pressable, StyleSheet, Text, View } from 'react-native'; + +import { formatMessage } from 'stream-chat'; + +import { + ChannelContextValue, + useChannelContext, +} from '../../../../contexts/channelContext/ChannelContext'; +import { + MessageContextValue, + useMessageContext, +} from '../../../../contexts/messageContext/MessageContext'; +import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; +import { + ThreadContextValue, + useThreadContext, +} from '../../../../contexts/threadContext/ThreadContext'; +import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext'; +import { ArrowUpRight } from '../../../../icons/ArrowUpRight'; +import { primitives } from '../../../../theme'; + +type SentToChannelHeaderPropsWithContext = Pick & + Pick & + Pick; + +const SentToChannelHeaderWithContext = (props: SentToChannelHeaderPropsWithContext) => { + const { onBackPressThread, threadList, onThreadSelect, message, channel, setTargetedMessage } = + props; + const { + theme: { semantics }, + } = useTheme(); + const { t } = useTranslationContext(); + const styles = useStyles(); + + const queryParentMessageAndMoveToTargetedMessage = () => { + return channel + .getClient() + .search({ cid: channel.cid }, { id: message.parent_id }) + .then(({ results }) => { + if (!results.length) { + return; + } + const targetMessage = formatMessage(results[0].message); + onThreadSelect?.(targetMessage, message.id); + }) + .catch((error) => { + console.error('Error querying parent message:', error); + }); + }; + + const handleOnPress = async () => { + if (!threadList) { + await queryParentMessageAndMoveToTargetedMessage(); + } else { + setTargetedMessage(message.id); + onBackPressThread?.(message.id); + } + }; + + return ( + + + + {threadList ? t('Also sent in channel') : t('Replied to a thread')} + + {(!threadList && onThreadSelect) || (threadList && onBackPressThread) ? ( + <> + · + + {t('View')} + + + ) : null} + + ); +}; + +const areEqual = ( + prevProps: SentToChannelHeaderPropsWithContext, + nextProps: SentToChannelHeaderPropsWithContext, +) => { + const { threadList: prevThreadList } = prevProps; + const { threadList: nextThreadList } = nextProps; + if (prevThreadList !== nextThreadList) { + return false; + } + return true; +}; + +const MemoizedSentToChannelHeader = React.memo( + SentToChannelHeaderWithContext, + areEqual, +) as typeof SentToChannelHeaderWithContext; + +export type SentToChannelHeaderProps = Partial; + +export const SentToChannelHeader = (props: SentToChannelHeaderProps) => { + const { onBackPressThread } = useThreadContext(); + const { threadList, channel, setTargetedMessage } = useChannelContext(); + const { onThreadSelect, message } = useMessageContext(); + + return ( + + ); +}; + +const useStyles = () => { + const { + theme: { + semantics, + messageSimple: { + sentToChannelHeader: { container, label, dot, link }, + }, + }, + } = useTheme(); + + return useMemo(() => { + return StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + gap: primitives.spacingXxs, + paddingVertical: primitives.spacingXxs, + ...container, + }, + label: { + color: semantics.textPrimary, + fontSize: primitives.typographyFontSizeXs, + fontWeight: primitives.typographyFontWeightSemiBold, + lineHeight: primitives.typographyLineHeightTight, + ...label, + }, + dot: { + color: semantics.textPrimary, + fontSize: primitives.typographyFontSizeXs, + fontWeight: primitives.typographyFontWeightSemiBold, + lineHeight: primitives.typographyLineHeightTight, + ...dot, + }, + link: { + color: semantics.accentPrimary, + fontSize: primitives.typographyFontSizeXs, + fontWeight: primitives.typographyFontWeightRegular, + lineHeight: primitives.typographyLineHeightTight, + ...link, + }, + }); + }, [container, semantics, label, dot, link]); +}; diff --git a/package/src/components/Message/MessageSimple/MessageHeader.tsx b/package/src/components/Message/MessageSimple/MessageHeader.tsx index a76f6e2162..4b3424e30c 100644 --- a/package/src/components/Message/MessageSimple/MessageHeader.tsx +++ b/package/src/components/Message/MessageSimple/MessageHeader.tsx @@ -1,5 +1,12 @@ import React from 'react'; +import { View } from 'react-native'; + +import { MessageReminderHeader } from './Headers/MessageReminderHeader'; +import { MessageSavedForLaterHeader } from './Headers/MessageSavedForLaterHeader'; + +import { SentToChannelHeader } from './Headers/SentToChannelHeader'; + import { MessageContextValue, useMessageContext, @@ -8,26 +15,73 @@ import { MessagesContextValue, useMessagesContext, } from '../../../contexts/messagesContext/MessagesContext'; +import { useMessageReminder } from '../../../hooks/useMessageReminder'; type MessageHeaderPropsWithContext = Pick & - Pick; + Pick & { + shouldShowSavedForLaterHeader?: boolean; + shouldShowPinnedHeader: boolean; + shouldShowReminderHeader: boolean; + shouldShowSentToChannelHeader: boolean; + }; const MessageHeaderWithContext = (props: MessageHeaderPropsWithContext) => { - const { message, MessagePinnedHeader } = props; + const { + message, + MessagePinnedHeader, + shouldShowSavedForLaterHeader, + shouldShowPinnedHeader, + shouldShowReminderHeader, + shouldShowSentToChannelHeader, + } = props; - return ; + return ( + + {shouldShowReminderHeader && } + {shouldShowSavedForLaterHeader && } + {shouldShowPinnedHeader && } + {shouldShowSentToChannelHeader && } + + ); }; const areEqual = ( prevProps: MessageHeaderPropsWithContext, nextProps: MessageHeaderPropsWithContext, ) => { - const { message: prevMessage } = prevProps; - const { message: nextMessage } = nextProps; + const { + shouldShowSavedForLaterHeader: prevShouldShowSavedForLaterHeader, + shouldShowPinnedHeader: prevShouldShowPinnedHeader, + shouldShowReminderHeader: prevShouldShowReminderHeader, + shouldShowSentToChannelHeader: prevShouldShowSentToChannelHeader, + } = prevProps; + const { + shouldShowSavedForLaterHeader: nextShouldShowSavedForLaterHeader, + shouldShowPinnedHeader: nextShouldShowPinnedHeader, + shouldShowReminderHeader: nextShouldShowReminderHeader, + shouldShowSentToChannelHeader: nextShouldShowSentToChannelHeader, + } = nextProps; + + const shouldShowSavedForLaterHeaderEqual = + prevShouldShowSavedForLaterHeader === nextShouldShowSavedForLaterHeader; + if (!shouldShowSavedForLaterHeaderEqual) { + return false; + } - const messageEqual = - prevMessage.id === nextMessage.id && prevMessage.pinned === nextMessage.pinned; - if (!messageEqual) { + const shouldShowPinnedHeaderEqual = prevShouldShowPinnedHeader === nextShouldShowPinnedHeader; + if (!shouldShowPinnedHeaderEqual) { + return false; + } + + const shouldShowReminderHeaderEqual = + prevShouldShowReminderHeader === nextShouldShowReminderHeader; + if (!shouldShowReminderHeaderEqual) { + return false; + } + + const shouldShowSentToChannelHeaderEqual = + prevShouldShowSentToChannelHeader === nextShouldShowSentToChannelHeader; + if (!shouldShowSentToChannelHeaderEqual) { return false; } @@ -44,8 +98,22 @@ export type MessageHeaderProps = Partial>; export const MessageHeader = (props: MessageHeaderProps) => { const { message } = useMessageContext(); const { MessagePinnedHeader } = useMessagesContext(); + const reminder = useMessageReminder(message.id); + + const shouldShowSavedForLaterHeader = reminder && !reminder.remindAt; + const shouldShowReminderHeader = reminder && reminder.remindAt; + const shouldShowPinnedHeader = !!message?.pinned; + const shouldShowSentToChannelHeader = !!message?.show_in_channel; return ( - + ); }; diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index 0484fb0a2e..6fc3953a83 100644 --- a/package/src/components/Message/hooks/useCreateMessageContext.ts +++ b/package/src/components/Message/hooks/useCreateMessageContext.ts @@ -41,6 +41,7 @@ export const useCreateMessageContext = ({ onOpenThread, onPress, onPressIn, + onThreadSelect, otherAttachments, preventPress, reactions, @@ -92,6 +93,7 @@ export const useCreateMessageContext = ({ onOpenThread, onPress, onPressIn, + onThreadSelect, otherAttachments, preventPress, reactions, diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index 391da93775..4ccbfac54b 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -119,6 +119,13 @@ export type MessageContextValue = { preventPress?: boolean; /** Whether or not the avatar show show next to Message */ showAvatar?: boolean; + /** + * Function to handle thread select + * @param message - The message to select + * @param targetedMessageId - The id of the targeted message + * @returns void + */ + onThreadSelect?: (message: LocalMessage, targetedMessageId?: string) => void; } & Pick & Pick; diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index 814f178faf..00d1bd319b 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -699,6 +699,22 @@ export type Theme = { container: ViewStyle; label: TextStyle; }; + savedForLaterHeader: { + container: ViewStyle; + label: TextStyle; + }; + reminderHeader: { + container: ViewStyle; + label: TextStyle; + dot: TextStyle; + time: TextStyle; + }; + sentToChannelHeader: { + container: ViewStyle; + label: TextStyle; + dot: TextStyle; + link: TextStyle; + }; reactionListBottom: { contentContainer: ViewStyle; columnWrapper: ViewStyle; @@ -1536,6 +1552,22 @@ export const defaultTheme: Theme = { container: {}, label: {}, }, + savedForLaterHeader: { + container: {}, + label: {}, + }, + reminderHeader: { + container: {}, + label: {}, + dot: {}, + time: {}, + }, + sentToChannelHeader: { + container: {}, + label: {}, + dot: {}, + link: {}, + }, reactionListBottom: { contentContainer: {}, columnWrapper: {}, diff --git a/package/src/contexts/threadContext/ThreadContext.tsx b/package/src/contexts/threadContext/ThreadContext.tsx index d60d6b58b1..7270763cb6 100644 --- a/package/src/contexts/threadContext/ThreadContext.tsx +++ b/package/src/contexts/threadContext/ThreadContext.tsx @@ -26,6 +26,12 @@ export type ThreadContextValue = { threadInstance?: Thread | null; threadLoadingMore?: boolean; threadLoadingMoreRecent?: boolean; + /** + * Function to handle back press on thread + * @param messageId - The id of the message to go back to + * @returns void + */ + onBackPressThread?: (messageId?: string) => void; }; export const ThreadContext = React.createContext(DEFAULT_BASE_CONTEXT_VALUE as ThreadContextValue); diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json index 6f12a2f8e9..2eca2062da 100644 --- a/package/src/i18n/en.json +++ b/package/src/i18n/en.json @@ -28,7 +28,7 @@ "Device gallery permissions is used to take photos or videos.": "Device gallery permissions is used to take photos or videos.", "Do you want to send a copy of this message to a moderator for further investigation?": "Do you want to send a copy of this message to a moderator for further investigation?", "Due since {{ dueSince }}": "Due since {{ dueSince }}", - "Due {{ timeLeft }}": "Due {{ timeLeft }}", + "{{ timeLeft }}": "{{ timeLeft }}", "Edit Message": "Edit Message", "Edited": "Edited", "Editing Message": "Editing Message", @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} Reaction", "{{count}} Reactions_other": "{{count}} Reactions", "Tap to remove": "Tap to remove", - "Draft": "Draft" + "Draft": "Draft", + "Reminder set": "Reminder set", + "Also sent in channel": "Also sent in channel", + "Replied to a thread": "Replied to a thread", + "View": "View", + "Reminder overdue": "Reminder overdue" } diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json index 8073e73845..c46ac78a55 100644 --- a/package/src/i18n/es.json +++ b/package/src/i18n/es.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} reacción", "{{count}} Reactions_other": "{{count}} reacciones", "Tap to remove": "Toca para quitar", - "Draft": "Borrador" + "Draft": "Borrador", + "Reminder set": "Recordatorio establecido", + "Also sent in channel": "También enviado en el canal", + "Replied to a thread": "Respondido a un hilo", + "View": "Ver", + "Reminder overdue": "Recordatorio vencido" } diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json index eb065f56ff..44947ccc18 100644 --- a/package/src/i18n/fr.json +++ b/package/src/i18n/fr.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} réaction", "{{count}} Reactions_other": "{{count}} réactions", "Tap to remove": "Appuyez pour retirer", - "Draft": "Brouillon" + "Draft": "Brouillon", + "Reminder set": "Recordatorio establecido", + "Also sent in channel": "También enviado en el canal", + "Replied to a thread": "Respondido a un hilo", + "View": "Ver", + "Reminder overdue": "Recordatorio vencido" } diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json index dd10ce8bf6..5fb01cfad3 100644 --- a/package/src/i18n/he.json +++ b/package/src/i18n/he.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} תגובה", "{{count}} Reactions_other": "{{count}} תגובות", "Tap to remove": "הקש כדי להסיר", - "Draft": "טיוטה" + "Draft": "טיוטה", + "Reminder set": "Recordatorio establecido", + "Also sent in channel": "También enviado en el canal", + "Replied to a thread": "Respondido a un hilo", + "View": "Ver", + "Reminder overdue": "Recordatorio vencido" } diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json index 46ee2096be..ffe37b4a6b 100644 --- a/package/src/i18n/hi.json +++ b/package/src/i18n/hi.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} प्रतिक्रिया", "{{count}} Reactions_other": "{{count}} प्रतिक्रियाएँ", "Tap to remove": "हटाने के लिए टैप करें", - "Draft": "ड्राफ्ट" + "Draft": "ड्राफ्ट", + "Reminder set": "रीमिंडर सेट किया गया", + "Also sent in channel": "चैनल में भी भेजा गया", + "Replied to a thread": "थ्रेड में उत्तर दिया", + "View": "देखें", + "Reminder overdue": "रीमिंडर ओवरडो" } diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json index 93bb20f5d7..5c3969d1b3 100644 --- a/package/src/i18n/it.json +++ b/package/src/i18n/it.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} reazione", "{{count}} Reactions_other": "{{count}} reazioni", "Tap to remove": "Tocca per rimuovere", - "Draft": "Borrador" + "Draft": "Borrador", + "Reminder set": "Recordatorio establecido", + "Also sent in channel": "También enviado en el canal", + "Replied to a thread": "Respondido a un hilo", + "View": "Ver", + "Reminder overdue": "Recordatorio vencido" } diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json index 6277931098..1a99c78fd4 100644 --- a/package/src/i18n/ja.json +++ b/package/src/i18n/ja.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}}件のリアクション", "{{count}} Reactions_other": "{{count}}件のリアクション", "Tap to remove": "タップして削除", - "Draft": "下書き" + "Draft": "下書き", + "Reminder set": "リマインダー設定", + "Also sent in channel": "チャンネルにも送信", + "Replied to a thread": "スレッドに返信", + "View": "表示", + "Reminder overdue": "リマインダー期限切れ" } diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json index b186d0a0cc..b14d43a214 100644 --- a/package/src/i18n/ko.json +++ b/package/src/i18n/ko.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}}개의 반응", "{{count}} Reactions_other": "{{count}}개의 반응", "Tap to remove": "탭하여 제거", - "Draft": "초안" + "Draft": "초안", + "Reminder set": "리마인더 설정", + "Also sent in channel": "채널에도 전송", + "Replied to a thread": "스레드에 답장", + "View": "보기", + "Reminder overdue": "리마인더 만료" } diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json index 1ab63f14a5..09fb600e44 100644 --- a/package/src/i18n/nl.json +++ b/package/src/i18n/nl.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} reactie", "{{count}} Reactions_other": "{{count}} reacties", "Tap to remove": "Tik om te verwijderen", - "Draft": "Ontwerp" + "Draft": "Ontwerp", + "Reminder set": "Reminder set", + "Also sent in channel": "Also sent in channel", + "Replied to a thread": "Replied to a thread", + "View": "View", + "Reminder overdue": "Reminder overdue" } diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json index cf7157b8d8..e609cf9855 100644 --- a/package/src/i18n/pt-br.json +++ b/package/src/i18n/pt-br.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} reação", "{{count}} Reactions_other": "{{count}} reações", "Tap to remove": "Toque para remover", - "Draft": "Rascunho" + "Draft": "Rascunho", + "Reminder set": "Recordatorio establecido", + "Also sent in channel": "También enviado en el canal", + "Replied to a thread": "Respondido a un hilo", + "View": "Ver", + "Reminder overdue": "Recordatorio vencido" } diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json index e1818175ff..ace34f7f8e 100644 --- a/package/src/i18n/ru.json +++ b/package/src/i18n/ru.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} реакция", "{{count}} Reactions_other": "{{count}} реакций", "Tap to remove": "Нажмите, чтобы удалить", - "Draft": "Черновик" + "Draft": "Черновик", + "Reminder set": "Напоминание установлено", + "Also sent in channel": "Также отправлено в канал", + "Replied to a thread": "Ответил на тему", + "View": "Посмотреть", + "Reminder overdue": "Напоминание просрочено" } diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json index 5d0147efa1..929bdf519c 100644 --- a/package/src/i18n/tr.json +++ b/package/src/i18n/tr.json @@ -172,5 +172,10 @@ "{{count}} Reactions_one": "{{count}} tepki", "{{count}} Reactions_other": "{{count}} tepki", "Tap to remove": "Kaldırmak için dokunun", - "Draft": "Taslak" + "Draft": "Taslak", + "Reminder set": "Hatırlatıcı ayarlandı", + "Also sent in channel": "Kanala da gönderildi", + "Replied to a thread": "Konuya yanıt verildi", + "View": "Görüntüle", + "Reminder overdue": "Hatırlatıcı süresi doldu" } diff --git a/package/src/icons/ArrowUpRight.tsx b/package/src/icons/ArrowUpRight.tsx new file mode 100644 index 0000000000..d7cfd21239 --- /dev/null +++ b/package/src/icons/ArrowUpRight.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; + +export const ArrowUpRight = ({ height = 16, width = 16, ...rest }: IconProps) => { + return ( + + + + ); +}; diff --git a/package/src/icons/Bell.tsx b/package/src/icons/Bell.tsx new file mode 100644 index 0000000000..095472f198 --- /dev/null +++ b/package/src/icons/Bell.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import Svg, { Path } from 'react-native-svg'; + +import { IconProps } from './utils/base'; + +export const Bell = ({ height = 16, width = 16, ...rest }: IconProps) => { + return ( + + + + ); +}; diff --git a/package/src/icons/Bookmark.tsx b/package/src/icons/Bookmark.tsx new file mode 100644 index 0000000000..b2c6766bf3 --- /dev/null +++ b/package/src/icons/Bookmark.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { Path, Svg } from 'react-native-svg'; + +import { IconProps } from './utils/base'; + +export const Bookmark = ({ height, width, ...rest }: IconProps) => ( + + + +); diff --git a/package/src/icons/index.ts b/package/src/icons/index.ts index f0f1f1d374..15a3f7f9f4 100644 --- a/package/src/icons/index.ts +++ b/package/src/icons/index.ts @@ -105,3 +105,4 @@ export * from './UnreadIndicator'; export * from './FilePickerIcon'; export * from './CommandsIcon'; export * from './CurveLineLeftUpReply'; +export * from './Bell'; From 93bb04e83cf4c44c6d56297b9f61ef7f49bc002a Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 27 Feb 2026 05:04:40 +0530 Subject: [PATCH 3/9] feat: message headers redesign --- .../Message/MessageSimple/Headers/MessageReminderHeader.tsx | 6 ++---- .../Thread/__tests__/__snapshots__/Thread.test.js.snap | 4 ++++ package/src/i18n/en.json | 1 - package/src/i18n/es.json | 1 - package/src/i18n/fr.json | 1 - package/src/i18n/he.json | 1 - package/src/i18n/hi.json | 1 - package/src/i18n/it.json | 1 - package/src/i18n/ja.json | 1 - package/src/i18n/ko.json | 1 - package/src/i18n/nl.json | 1 - package/src/i18n/pt-br.json | 1 - package/src/i18n/ru.json | 1 - package/src/i18n/tr.json | 1 - 14 files changed, 6 insertions(+), 16 deletions(-) diff --git a/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx b/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx index 5f2f45d96c..3d456429fe 100644 --- a/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx +++ b/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx @@ -40,10 +40,8 @@ const MessageReminderHeaderWithContext = (props: MessageReminderHeaderPropsWithC · - {t('{{ timeLeft }}', { - timeLeft: t('duration/Message reminder', { - milliseconds: timeLeftMs, - }), + {t('duration/Message reminder', { + milliseconds: timeLeftMs, })} 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 042c0a79b5..c83eca1a86 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -458,6 +458,7 @@ exports[`Thread should match thread snapshot 1`] = ` } testID="message-components" > + + + + Date: Fri, 27 Feb 2026 12:04:39 +0530 Subject: [PATCH 4/9] feat: configure props --- package/src/components/Channel/Channel.tsx | 12 +++++++++ .../Channel/hooks/useCreateMessagesContext.ts | 6 +++++ .../Message/MessageSimple/Headers/index.tsx | 4 +++ .../Message/MessageSimple/MessageHeader.tsx | 26 ++++++++++++++----- package/src/components/index.ts | 2 +- .../messagesContext/MessagesContext.tsx | 15 +++++++++++ 6 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 package/src/components/Message/MessageSimple/Headers/index.tsx diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index ce9fa23b8a..b6d37ca59a 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -141,6 +141,9 @@ import { } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView'; import { Message as MessageDefault } from '../Message/Message'; import { MessagePinnedHeader as MessagePinnedHeaderDefault } from '../Message/MessageSimple/Headers/MessagePinnedHeader'; +import { MessageReminderHeader as MessageReminderHeaderDefault } from '../Message/MessageSimple/Headers/MessageReminderHeader'; +import { MessageSavedForLaterHeader as MessageSavedForLaterHeaderDefault } from '../Message/MessageSimple/Headers/MessageSavedForLaterHeader'; +import { SentToChannelHeader as SentToChannelHeaderDefault } from '../Message/MessageSimple/Headers/SentToChannelHeader'; import { MessageAvatar as MessageAvatarDefault } from '../Message/MessageSimple/MessageAvatar'; import { MessageBlocked as MessageBlockedDefault } from '../Message/MessageSimple/MessageBlocked'; import { MessageBounce as MessageBounceDefault } from '../Message/MessageSimple/MessageBounce'; @@ -366,6 +369,9 @@ export type ChannelPropsWithContext = Pick & | 'MessageLocation' | 'MessageMenu' | 'MessagePinnedHeader' + | 'MessageReminderHeader' + | 'MessageSavedForLaterHeader' + | 'SentToChannelHeader' | 'MessageReplies' | 'MessageRepliesAvatars' | 'MessageSimple' @@ -683,6 +689,9 @@ const ChannelWithContext = (props: PropsWithChildren) = MessageLocation, MessageMenu = MessageMenuDefault, MessagePinnedHeader = MessagePinnedHeaderDefault, + MessageReminderHeader = MessageReminderHeaderDefault, + MessageSavedForLaterHeader = MessageSavedForLaterHeaderDefault, + SentToChannelHeader = SentToChannelHeaderDefault, MessageReactionPicker = MessageReactionPickerDefault, MessageReplies = MessageRepliesDefault, MessageRepliesAvatars = MessageRepliesAvatarsDefault, @@ -1942,6 +1951,9 @@ const ChannelWithContext = (props: PropsWithChildren) = MessageLocation, MessageMenu, MessagePinnedHeader, + MessageReminderHeader, + MessageSavedForLaterHeader, + SentToChannelHeader, MessageReactionPicker, MessageReplies, MessageRepliesAvatars, diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index 2d04824d2e..be64622728 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -65,6 +65,9 @@ export const useCreateMessagesContext = ({ MessageLocation, MessageMenu, MessagePinnedHeader, + MessageReminderHeader, + MessageSavedForLaterHeader, + SentToChannelHeader, MessageReactionPicker, MessageReplies, MessageRepliesAvatars, @@ -178,6 +181,9 @@ export const useCreateMessagesContext = ({ MessageLocation, MessageMenu, MessagePinnedHeader, + MessageReminderHeader, + MessageSavedForLaterHeader, + SentToChannelHeader, MessageReactionPicker, MessageReplies, MessageRepliesAvatars, diff --git a/package/src/components/Message/MessageSimple/Headers/index.tsx b/package/src/components/Message/MessageSimple/Headers/index.tsx new file mode 100644 index 0000000000..81b226621e --- /dev/null +++ b/package/src/components/Message/MessageSimple/Headers/index.tsx @@ -0,0 +1,4 @@ +export * from './MessagePinnedHeader'; +export * from './MessageReminderHeader'; +export * from './MessageSavedForLaterHeader'; +export * from './SentToChannelHeader'; diff --git a/package/src/components/Message/MessageSimple/MessageHeader.tsx b/package/src/components/Message/MessageSimple/MessageHeader.tsx index 4b3424e30c..0ef97690b1 100644 --- a/package/src/components/Message/MessageSimple/MessageHeader.tsx +++ b/package/src/components/Message/MessageSimple/MessageHeader.tsx @@ -2,11 +2,6 @@ import React from 'react'; import { View } from 'react-native'; -import { MessageReminderHeader } from './Headers/MessageReminderHeader'; -import { MessageSavedForLaterHeader } from './Headers/MessageSavedForLaterHeader'; - -import { SentToChannelHeader } from './Headers/SentToChannelHeader'; - import { MessageContextValue, useMessageContext, @@ -18,7 +13,13 @@ import { import { useMessageReminder } from '../../../hooks/useMessageReminder'; type MessageHeaderPropsWithContext = Pick & - Pick & { + Pick< + MessagesContextValue, + | 'MessagePinnedHeader' + | 'MessageReminderHeader' + | 'MessageSavedForLaterHeader' + | 'SentToChannelHeader' + > & { shouldShowSavedForLaterHeader?: boolean; shouldShowPinnedHeader: boolean; shouldShowReminderHeader: boolean; @@ -33,6 +34,9 @@ const MessageHeaderWithContext = (props: MessageHeaderPropsWithContext) => { shouldShowPinnedHeader, shouldShowReminderHeader, shouldShowSentToChannelHeader, + MessageReminderHeader, + MessageSavedForLaterHeader, + SentToChannelHeader, } = props; return ( @@ -97,7 +101,12 @@ export type MessageHeaderProps = Partial>; export const MessageHeader = (props: MessageHeaderProps) => { const { message } = useMessageContext(); - const { MessagePinnedHeader } = useMessagesContext(); + const { + MessagePinnedHeader, + MessageReminderHeader, + MessageSavedForLaterHeader, + SentToChannelHeader, + } = useMessagesContext(); const reminder = useMessageReminder(message.id); const shouldShowSavedForLaterHeader = reminder && !reminder.remindAt; @@ -113,6 +122,9 @@ export const MessageHeader = (props: MessageHeaderProps) => { shouldShowPinnedHeader={shouldShowPinnedHeader} shouldShowReminderHeader={!!shouldShowReminderHeader} shouldShowSentToChannelHeader={shouldShowSentToChannelHeader} + MessageReminderHeader={MessageReminderHeader} + MessageSavedForLaterHeader={MessageSavedForLaterHeader} + SentToChannelHeader={SentToChannelHeader} {...props} /> ); diff --git a/package/src/components/index.ts b/package/src/components/index.ts index 81c347a587..a23fc62bac 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -98,7 +98,7 @@ export * from './Message/MessageSimple/MessageDeleted'; export * from './Message/MessageSimple/MessageError'; export * from './Message/MessageSimple/MessageFooter'; export * from './Message/MessageSimple/MessageHeader'; -export * from './Message/MessageSimple/Headers/MessagePinnedHeader'; +export * from './Message/MessageSimple/Headers'; export * from './Message/MessageSimple/MessageReplies'; export * from './Message/MessageSimple/MessageRepliesAvatars'; export * from './Message/MessageSimple/MessageSimple'; diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 4dbef94bf8..fb15642f58 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -30,6 +30,9 @@ import type { MessageProps, } from '../../components/Message/Message'; import type { MessagePinnedHeaderProps } from '../../components/Message/MessageSimple/Headers/MessagePinnedHeader'; +import type { MessageReminderHeaderProps } from '../../components/Message/MessageSimple/Headers/MessageReminderHeader'; +import type { MessageSavedForLaterHeaderProps } from '../../components/Message/MessageSimple/Headers/MessageSavedForLaterHeader'; +import type { SentToChannelHeaderProps } from '../../components/Message/MessageSimple/Headers/SentToChannelHeader'; import type { MessageAvatarProps } from '../../components/Message/MessageSimple/MessageAvatar'; import type { MessageBlockedProps } from '../../components/Message/MessageSimple/MessageBlocked'; import type { MessageBounceProps } from '../../components/Message/MessageSimple/MessageBounce'; @@ -237,6 +240,18 @@ export type MessagesContextValue = Pick; + /** + * Custom message reminder component + */ + MessageReminderHeader: React.ComponentType; + /** + * Custom message saved for later component + */ + MessageSavedForLaterHeader: React.ComponentType; + /** + * Custom message sent to channel component + */ + SentToChannelHeader: React.ComponentType; /** * UI component for MessageReactionPicker */ From 4f108080d4355cc34399a4c771c613f128aa6607 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 27 Feb 2026 12:33:02 +0530 Subject: [PATCH 5/9] chore: resolve conflicts --- examples/SampleApp/src/screens/ChannelScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 1edb3d9b93..e3b21e5b97 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -27,7 +27,6 @@ import { useChannelMembersStatus } from '../hooks/useChannelMembersStatus'; import type { StackNavigatorParamList } from '../types'; import { NetworkDownIndicator } from '../components/NetworkDownIndicator'; import { useCreateDraftFocusEffect } from '../utils/useCreateDraftFocusEffect.tsx'; -import { MessageHeader } from '../components/Reminders/MessageReminderHeader.tsx'; import { channelMessageActions } from '../utils/messageActions.tsx'; import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx'; import { useStreamChatContext } from '../context/StreamChatContext.tsx'; From e032bc6e5f9de3c9c5210e0db3fe1017001d2350 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 27 Feb 2026 14:24:25 +0530 Subject: [PATCH 6/9] fix: issues --- .../Headers/MessageReminderHeader.tsx | 35 ++------------- .../Headers/SentToChannelHeader.tsx | 44 ++++++++++--------- .../messageContext/MessageContext.tsx | 2 + 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx b/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx index 3d456429fe..ef06b07e2f 100644 --- a/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx +++ b/package/src/components/Message/MessageSimple/Headers/MessageReminderHeader.tsx @@ -4,10 +4,7 @@ import { StyleSheet, Text, View } from 'react-native'; import { ReminderState } from 'stream-chat'; -import { - MessageContextValue, - useMessageContext, -} from '../../../../contexts/messageContext/MessageContext'; +import { useMessageContext } from '../../../../contexts/messageContext/MessageContext'; import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext'; import { useMessageReminder } from '../../../../hooks/useMessageReminder'; @@ -19,7 +16,7 @@ const reminderStateSelector = (state: ReminderState) => ({ timeLeftMs: state.timeLeftMs, }); -type MessageReminderHeaderPropsWithContext = Pick & { +type MessageReminderHeaderPropsWithContext = { timeLeftMs?: number; isReminderTimeLeft: boolean; }; @@ -48,31 +45,6 @@ const MessageReminderHeaderWithContext = (props: MessageReminderHeaderPropsWithC ); }; -const areEqual = ( - prevProps: MessageReminderHeaderPropsWithContext, - nextProps: MessageReminderHeaderPropsWithContext, -) => { - const { timeLeftMs: prevTimeLeftMs, isReminderTimeLeft: prevIsReminderTimeLeft } = prevProps; - const { timeLeftMs: nextTimeLeftMs, isReminderTimeLeft: nextIsReminderTimeLeft } = nextProps; - - const timeLeftMsEqual = prevTimeLeftMs === nextTimeLeftMs; - if (!timeLeftMsEqual) { - return false; - } - - const isReminderTimeLeftEqual = prevIsReminderTimeLeft === nextIsReminderTimeLeft; - if (!isReminderTimeLeftEqual) { - return false; - } - - return true; -}; - -const MemoizedMessageReminderHeader = React.memo( - MessageReminderHeaderWithContext, - areEqual, -) as typeof MessageReminderHeaderWithContext; - export type MessageReminderHeaderProps = Partial; export const MessageReminderHeader = (props: MessageReminderHeaderProps) => { @@ -83,8 +55,7 @@ export const MessageReminderHeader = (props: MessageReminderHeaderProps) => { const isReminderTimeLeft = !!(timeLeftMs && timeLeftMs > 0); return ( - { - return channel - .getClient() - .search({ cid: channel.cid }, { id: message.parent_id }) - .then(({ results }) => { - if (!results.length) { - return; - } - const targetMessage = formatMessage(results[0].message); - onThreadSelect?.(targetMessage, message.id); - }) - .catch((error) => { - console.error('Error querying parent message:', error); - }); - }; - - const handleOnPress = async () => { + const handleOnPress = useCallback(async () => { if (!threadList) { - await queryParentMessageAndMoveToTargetedMessage(); + return await channel + .getClient() + .search({ cid: channel.cid }, { id: message.parent_id }) + .then(({ results }) => { + if (!results.length) { + return; + } + const targetMessage = formatMessage(results[0].message); + onThreadSelect?.(targetMessage, message.id); + }) + .catch((error) => { + console.error('Error querying parent message:', error); + }); } else { setTargetedMessage(message.id); onBackPressThread?.(message.id); } - }; + }, [ + channel, + message.id, + message.parent_id, + onBackPressThread, + onThreadSelect, + setTargetedMessage, + threadList, + ]); return ( diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index 4ccbfac54b..043c70b793 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -124,6 +124,8 @@ export type MessageContextValue = { * @param message - The message to select * @param targetedMessageId - The id of the targeted message * @returns void + * + * TODO: V9: Change function params to an object */ onThreadSelect?: (message: LocalMessage, targetedMessageId?: string) => void; } & Pick & From 900154e3975cb29b53225cca82d3a1d5606b3b92 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 27 Feb 2026 15:11:19 +0530 Subject: [PATCH 7/9] fix: sent to channel header memo issues --- .../Headers/SentToChannelHeader.tsx | 112 +++++++----------- 1 file changed, 45 insertions(+), 67 deletions(-) diff --git a/package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx b/package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx index 8ba34a245d..973110ded1 100644 --- a/package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx +++ b/package/src/components/Message/MessageSimple/Headers/SentToChannelHeader.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; @@ -8,71 +8,45 @@ import { ChannelContextValue, useChannelContext, } from '../../../../contexts/channelContext/ChannelContext'; -import { - MessageContextValue, - useMessageContext, -} from '../../../../contexts/messageContext/MessageContext'; +import { useMessageContext } from '../../../../contexts/messageContext/MessageContext'; import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; -import { - ThreadContextValue, - useThreadContext, -} from '../../../../contexts/threadContext/ThreadContext'; +import { useThreadContext } from '../../../../contexts/threadContext/ThreadContext'; import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext'; +import { useStableCallback } from '../../../../hooks'; import { ArrowUpRight } from '../../../../icons/ArrowUpRight'; import { primitives } from '../../../../theme'; -type SentToChannelHeaderPropsWithContext = Pick & - Pick & - Pick; +type SentToChannelHeaderPropsWithContext = Pick & { + /** + * Function to handle press on the sent to channel header + * @returns void + */ + onPress: () => void; + /** + * Boolean to show the view text + * @default false + */ + showViewText?: boolean; +}; const SentToChannelHeaderWithContext = (props: SentToChannelHeaderPropsWithContext) => { - const { onBackPressThread, threadList, onThreadSelect, message, channel, setTargetedMessage } = - props; + const { threadList, onPress, showViewText } = props; const { theme: { semantics }, } = useTheme(); const { t } = useTranslationContext(); const styles = useStyles(); - const handleOnPress = useCallback(async () => { - if (!threadList) { - return await channel - .getClient() - .search({ cid: channel.cid }, { id: message.parent_id }) - .then(({ results }) => { - if (!results.length) { - return; - } - const targetMessage = formatMessage(results[0].message); - onThreadSelect?.(targetMessage, message.id); - }) - .catch((error) => { - console.error('Error querying parent message:', error); - }); - } else { - setTargetedMessage(message.id); - onBackPressThread?.(message.id); - } - }, [ - channel, - message.id, - message.parent_id, - onBackPressThread, - onThreadSelect, - setTargetedMessage, - threadList, - ]); - return ( {threadList ? t('Also sent in channel') : t('Replied to a thread')} - {(!threadList && onThreadSelect) || (threadList && onBackPressThread) ? ( + {showViewText ? ( <> · - + {t('View')} @@ -81,21 +55,8 @@ const SentToChannelHeaderWithContext = (props: SentToChannelHeaderPropsWithConte ); }; -const areEqual = ( - prevProps: SentToChannelHeaderPropsWithContext, - nextProps: SentToChannelHeaderPropsWithContext, -) => { - const { threadList: prevThreadList } = prevProps; - const { threadList: nextThreadList } = nextProps; - if (prevThreadList !== nextThreadList) { - return false; - } - return true; -}; - const MemoizedSentToChannelHeader = React.memo( SentToChannelHeaderWithContext, - areEqual, ) as typeof SentToChannelHeaderWithContext; export type SentToChannelHeaderProps = Partial; @@ -105,16 +66,33 @@ export const SentToChannelHeader = (props: SentToChannelHeaderProps) => { const { threadList, channel, setTargetedMessage } = useChannelContext(); const { onThreadSelect, message } = useMessageContext(); + const handleOnPress = useStableCallback(async () => { + if (!threadList) { + return await channel + .getClient() + .search({ cid: channel.cid }, { id: message.parent_id }) + .then(({ results }) => { + if (!results.length) { + return; + } + const targetMessage = formatMessage(results[0].message); + onThreadSelect?.(targetMessage, message.id); + }) + .catch((error) => { + console.error('Error querying parent message:', error); + }); + } else { + setTargetedMessage(message.id); + onBackPressThread?.(message.id); + } + }); + + const showViewText = useMemo(() => { + return !!((!threadList && onThreadSelect) || (threadList && onBackPressThread)); + }, [threadList, onThreadSelect, onBackPressThread]); + return ( - + ); }; From 8a0a36d198045add3b9ab2c54ab5556b8fd1ee89 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 27 Feb 2026 15:15:42 +0530 Subject: [PATCH 8/9] fix: sent to channel header memo issues --- .../components/Message/MessageSimple/MessageHeader.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/package/src/components/Message/MessageSimple/MessageHeader.tsx b/package/src/components/Message/MessageSimple/MessageHeader.tsx index 0ef97690b1..2bddd14aad 100644 --- a/package/src/components/Message/MessageSimple/MessageHeader.tsx +++ b/package/src/components/Message/MessageSimple/MessageHeader.tsx @@ -114,6 +114,15 @@ export const MessageHeader = (props: MessageHeaderProps) => { const shouldShowPinnedHeader = !!message?.pinned; const shouldShowSentToChannelHeader = !!message?.show_in_channel; + if ( + !shouldShowPinnedHeader && + !shouldShowSavedForLaterHeader && + !shouldShowReminderHeader && + !shouldShowSentToChannelHeader + ) { + return null; + } + return ( Date: Fri, 27 Feb 2026 10:52:38 +0100 Subject: [PATCH 9/9] fix: tests --- .../Thread/__tests__/__snapshots__/Thread.test.js.snap | 4 ---- 1 file changed, 4 deletions(-) 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 eadbbb32fd..ff582aaea3 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -458,7 +458,6 @@ exports[`Thread should match thread snapshot 1`] = ` } testID="message-components" > - - - -