From 529d286c2fe274dc03f0bf1888e07eab820a3bb9 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Thu, 26 Feb 2026 15:32:18 +0530 Subject: [PATCH 1/2] 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 2418f3e039595259bbe12c540c8a4feecd577e97 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Fri, 27 Feb 2026 17:29:54 +0530 Subject: [PATCH 2/2] fix: message issues with alignment and styles --- package/src/components/Message/Message.tsx | 208 +- .../Message/MessageSimple/MessageBubble.tsx | 117 +- .../Message/MessageSimple/MessageHeader.tsx | 25 +- .../__snapshots__/Thread.test.js.snap | 1682 ++++++++--------- 4 files changed, 977 insertions(+), 1055 deletions(-) diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index ecbe268e3a..392bb3e808 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { GestureResponderEvent, StyleProp, + StyleSheet, useWindowDimensions, View, ViewStyle, @@ -323,14 +324,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => { ); const isMessageTypeDeleted = message.type === 'deleted'; const { client } = chatContext; - const { - theme: { - colors: { bg_gradient_start }, - messageSimple: { targetedMessageContainer, unreadUnderlayColor = bg_gradient_start, wrapper }, - screenPadding, - semantics, - }, - } = useTheme(); const [rect, setRect] = useState<{ w: number; h: number; x: number; y: number } | undefined>( undefined, @@ -802,6 +795,11 @@ const MessageWithContext = (props: MessagePropsWithContext) => { } }, [overlayActive, message]); + const styles = useStyles({ + showUnreadUnderlay, + highlightedMessage: (isTargetedMessage || message.pinned) && !isMessageTypeDeleted, + }); + if (!(isMessageTypeDeleted || messageContentOrder.length)) { return null; } @@ -812,103 +810,82 @@ const MessageWithContext = (props: MessagePropsWithContext) => { return ( - - + + {overlayActive && rect ? ( + + ) : null} + {/*TODO: V9: Find a way to separate these in a dedicated file*/} + {overlayActive && rect ? ( { + const { width: w, height: h } = e.nativeEvent.layout; + + setOverlayTopH({ + h, + w, + x: isMyMessage ? screenW - rect.x - w : rect.x, + y: rect.y - h, + }); }} - /> + > + + ) : null} - {/*TODO: V9: Find a way to separate these in a dedicated file*/} - - {overlayActive && rect ? ( - { - const { width: w, height: h } = e.nativeEvent.layout; - - setOverlayTopH({ - h, - w, - x: isMyMessage ? screenW - rect.x - w : rect.x, - y: rect.y - h, - }); - }} - > - - - ) : null} - - + + + + {showMessageReactions ? ( + setShowMessageReactions(false)} + visible={showMessageReactions} + height={424} > - - - {showMessageReactions ? ( - setShowMessageReactions(false)} - visible={showMessageReactions} - height={424} + + + ) : null} + + {overlayActive && rect ? ( + { + const { width: w, height: h } = e.nativeEvent.layout; + setOverlayBottomH({ + h, + w, + x: isMyMessage ? screenW - rect.x - w : rect.x, + y: rect.y + rect.h, + }); + }} > - - - ) : null} - - {overlayActive && rect ? ( - { - const { width: w, height: h } = e.nativeEvent.layout; - setOverlayBottomH({ - h, - w, - x: isMyMessage ? screenW - rect.x - w : rect.x, - y: rect.y + rect.h, - }); - }} - > - - - ) : null} - - {isBounceDialogOpen ? ( - + ) : null} - + + {isBounceDialogOpen ? ( + + ) : null} ); @@ -1131,3 +1108,40 @@ export const Message = (props: MessageProps) => { /> ); }; + +const useStyles = ({ + showUnreadUnderlay, + highlightedMessage, +}: { + showUnreadUnderlay?: boolean; + highlightedMessage?: boolean; +}) => { + const { + theme: { + colors: { bg_gradient_start }, + messageSimple: { wrapper, unreadUnderlayColor = bg_gradient_start, targetedMessageContainer }, + screenPadding, + semantics, + }, + } = useTheme(); + return useMemo(() => { + return StyleSheet.create({ + wrapper: { + paddingHorizontal: screenPadding, + backgroundColor: showUnreadUnderlay ? unreadUnderlayColor : undefined, + ...(highlightedMessage + ? { backgroundColor: semantics.backgroundCoreHighlight, ...targetedMessageContainer } + : {}), + ...wrapper, + }, + }); + }, [ + wrapper, + screenPadding, + showUnreadUnderlay, + unreadUnderlayColor, + highlightedMessage, + semantics, + targetedMessageContainer, + ]); +}; diff --git a/package/src/components/Message/MessageSimple/MessageBubble.tsx b/package/src/components/Message/MessageSimple/MessageBubble.tsx index 7d9c18afbf..f772064431 100644 --- a/package/src/components/Message/MessageSimple/MessageBubble.tsx +++ b/package/src/components/Message/MessageSimple/MessageBubble.tsx @@ -55,36 +55,18 @@ export const MessageBubble = React.memo( MessageError, message, }: MessageBubbleProps) => { - const { - theme: { - messageSimple: { - bubble: { contentContainer, errorContainer, reactionListTopContainer, wrapper }, - }, - }, - } = useTheme(); + const styles = useStyles({ alignment }); const isMessageErrorType = message?.type === 'error' || message?.status === MessageStatusTypes.FAILED; return ( - + {reactionListPosition === 'top' && ReactionListTop ? ( - + ) : null} - + {isMessageErrorType ? ( - + ) : null} @@ -118,11 +100,7 @@ export const SwipableMessageBubble = React.memo( const { MessageSwipeContent, messageSwipeToReplyHitSlop, onSwipe, ...messageBubbleProps } = props; - const { - theme: { - messageSimple: { contentWrapper, swipeContentContainer }, - }, - } = useTheme(); + const styles = useStyles({ alignment: props.alignment }); const translateX = useSharedValue(0); const touchStart = useSharedValue<{ x: number; y: number } | null>(null); @@ -218,20 +196,13 @@ export const SwipableMessageBubble = React.memo( hitSlop={messageSwipeToReplyHitSlop} style={[ styles.contentWrapper, - contentWrapper, props.messageContentWidth > 0 && shouldRenderAnimatedWrapper ? { width: props.messageContentWidth } : {}, ]} > {shouldRenderAnimatedWrapper ? ( - + {MessageSwipeContent ? : null} ) : null} @@ -242,25 +213,55 @@ export const SwipableMessageBubble = React.memo( }, ); -const styles = StyleSheet.create({ - contentWrapper: { - alignItems: 'center', - flexDirection: 'row', - zIndex: 1, // To hide the stick inside the message content - }, - contentContainer: { - alignSelf: 'flex-start', - }, - swipeContentContainer: { - flexShrink: 0, - overflow: 'hidden', - position: 'relative', - }, - errorContainer: { - position: 'absolute', - top: 8, - right: -12, - }, - reactionListTopContainer: {}, - wrapper: {}, -}); +const useStyles = ({ alignment }: { alignment?: 'left' | 'right' }) => { + const { + theme: { + messageSimple: { + bubble: { contentContainer, errorContainer, reactionListTopContainer, wrapper }, + contentWrapper, + swipeContentContainer, + }, + }, + } = useTheme(); + return useMemo(() => { + return StyleSheet.create({ + contentWrapper: { + alignItems: 'center', + flexDirection: 'row', + zIndex: 1, // To hide the stick inside the message content + ...contentWrapper, + }, + contentContainer: { + alignSelf: alignment === 'left' ? 'flex-start' : 'flex-end', + ...contentContainer, + }, + swipeContentContainer: { + flexShrink: 0, + overflow: 'hidden', + position: 'relative', + ...swipeContentContainer, + }, + errorContainer: { + position: 'absolute', + top: 8, + right: -12, + ...errorContainer, + }, + reactionListTopContainer: { + alignSelf: alignment === 'left' ? 'flex-end' : 'flex-start', + ...reactionListTopContainer, + }, + wrapper: { + ...wrapper, + }, + }); + }, [ + alignment, + contentContainer, + contentWrapper, + errorContainer, + reactionListTopContainer, + swipeContentContainer, + wrapper, + ]); +}; diff --git a/package/src/components/Message/MessageSimple/MessageHeader.tsx b/package/src/components/Message/MessageSimple/MessageHeader.tsx index 2bddd14aad..0762b38c9f 100644 --- a/package/src/components/Message/MessageSimple/MessageHeader.tsx +++ b/package/src/components/Message/MessageSimple/MessageHeader.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useMemo } from 'react'; -import { View } from 'react-native'; +import { View, ViewStyle } from 'react-native'; import { MessageContextValue, @@ -12,7 +12,7 @@ import { } from '../../../contexts/messagesContext/MessagesContext'; import { useMessageReminder } from '../../../hooks/useMessageReminder'; -type MessageHeaderPropsWithContext = Pick & +type MessageHeaderPropsWithContext = Pick & Pick< MessagesContextValue, | 'MessagePinnedHeader' @@ -28,6 +28,7 @@ type MessageHeaderPropsWithContext = Pick & const MessageHeaderWithContext = (props: MessageHeaderPropsWithContext) => { const { + alignment, message, MessagePinnedHeader, shouldShowSavedForLaterHeader, @@ -39,8 +40,14 @@ const MessageHeaderWithContext = (props: MessageHeaderPropsWithContext) => { SentToChannelHeader, } = props; + const containerStyle: ViewStyle = useMemo(() => { + return { + alignItems: alignment === 'left' ? 'flex-start' : 'flex-end', + }; + }, [alignment]); + return ( - + {shouldShowReminderHeader && } {shouldShowSavedForLaterHeader && } {shouldShowPinnedHeader && } @@ -54,18 +61,25 @@ const areEqual = ( nextProps: MessageHeaderPropsWithContext, ) => { const { + alignment: prevAlignment, shouldShowSavedForLaterHeader: prevShouldShowSavedForLaterHeader, shouldShowPinnedHeader: prevShouldShowPinnedHeader, shouldShowReminderHeader: prevShouldShowReminderHeader, shouldShowSentToChannelHeader: prevShouldShowSentToChannelHeader, } = prevProps; const { + alignment: nextAlignment, shouldShowSavedForLaterHeader: nextShouldShowSavedForLaterHeader, shouldShowPinnedHeader: nextShouldShowPinnedHeader, shouldShowReminderHeader: nextShouldShowReminderHeader, shouldShowSentToChannelHeader: nextShouldShowSentToChannelHeader, } = nextProps; + const alignmentEqual = prevAlignment === nextAlignment; + if (!alignmentEqual) { + return false; + } + const shouldShowSavedForLaterHeaderEqual = prevShouldShowSavedForLaterHeader === nextShouldShowSavedForLaterHeader; if (!shouldShowSavedForLaterHeaderEqual) { @@ -100,7 +114,7 @@ const MemoizedMessageHeader = React.memo( export type MessageHeaderProps = Partial>; export const MessageHeader = (props: MessageHeaderProps) => { - const { message } = useMessageContext(); + const { alignment, message } = useMessageContext(); const { MessagePinnedHeader, MessageReminderHeader, @@ -125,6 +139,7 @@ export const MessageHeader = (props: MessageHeaderProps) => { return ( - - + + - - + - - - + /> + + + - + - - - - Message6 - + Message6 - + @@ -648,50 +621,50 @@ exports[`Thread should match thread snapshot 1`] = ` - + - + + - - Edited - - + Edited + - + @@ -728,285 +701,258 @@ exports[`Thread should match thread snapshot 1`] = ` ], { "backgroundColor": undefined, + "paddingHorizontal": 16, }, ] } + testID="message-wrapper" > - - + + - - + - - - + /> + + + - + - - - - Message5 - + Message5 - + @@ -1015,50 +961,50 @@ exports[`Thread should match thread snapshot 1`] = ` - + - + + - - Edited - - + Edited + - + @@ -1131,285 +1077,258 @@ exports[`Thread should match thread snapshot 1`] = ` ], { "backgroundColor": undefined, + "paddingHorizontal": 16, }, ] } + testID="message-wrapper" > - - + + - - + - - - + /> + + + - + - - - - Message4 - + Message4 - + @@ -1418,50 +1337,50 @@ exports[`Thread should match thread snapshot 1`] = ` - + - + + - - Edited - - + Edited + - + @@ -1499,286 +1418,259 @@ exports[`Thread should match thread snapshot 1`] = ` undefined, { "backgroundColor": undefined, + "paddingHorizontal": 16, }, ] } + testID="message-wrapper" > - - + + - - + - - - + /> + + + - + - - - - Message3 - + Message3 - + @@ -1787,50 +1679,50 @@ exports[`Thread should match thread snapshot 1`] = ` - + - + + - - Edited - - + Edited + - +