Skip to content

Comments

V5 message list layout#1212

Merged
laevandus merged 20 commits intov5from
v5-message-list-layout
Feb 23, 2026
Merged

V5 message list layout#1212
laevandus merged 20 commits intov5from
v5-message-list-layout

Conversation

@laevandus
Copy link
Contributor

@laevandus laevandus commented Feb 18, 2026

πŸ”— Issue Links

Related: IOS-1379

🎯 Goal

Extract the core message rendering logic into a new MessageItemView and fix the layout to match the Figma design system.

πŸ“ Summary

  • Renamed MessageContainerView to MessageItemView; extracted swipe-to-reply into a SwipeToReplyModifier and split layout into focused sub-views (messageBubbleContent, avatarView, threadRepliesView, deliveryStatusView)
  • Added shownAsPreview mode that propagates a previewTextColor (colors.textOnAccent) to thread replies, delivery status, and translation footer views
  • ReactionsOverlayView now delegates message rendering to makeMessageItemView(shownAsPreview: true), removing ~80 lines of duplicated layout code
  • Removed MessageSpacer; alignment handled via VStack alignment and .frame(maxWidth:alignment:)
  • Updated MessageRepliesView thread connector path to use Figma-exported SVG bezier curve
  • Refactored QuotedMessageView and ChatQuotedMessageView so incoming/outgoing style is driven by the parent message (quotedByCurrentUser, shownInMessageList) instead of the quoted message
  • ReferenceMessageViewBackgroundModifier now takes a backgroundColor: Color directly instead of computing it from isSentByCurrentUser
  • Added textColor: UIColor? parameter to MessageAuthorView, MessageReadIndicatorView, MessageTranslationFooterView, MessageRepliesView, and their corresponding factory options
  • Changed MessageDateView.textColor type from Color? to UIColor? for consistency with other views
  • Adjusted default layout values: horizontal padding 8 β†’ 16, group bottom spacing 2 β†’ 4, spacer width availableWidth/4 β†’ 82, reactions top padding 24 β†’ 20
  • Renamed MessageContainerViewOptions to MessageItemViewOptions; added

πŸ§ͺ Manual Testing Notes

N/A

β˜‘οΈ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

@laevandus laevandus requested a review from a team as a code owner February 18, 2026 09:33
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v5-message-list-layout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines 51 to 57
private var backgroundColor: UIColor {
if viewModel.shownInMessageList {
return viewModel.quotedByCurrentUser ? colors.chatBackgroundAttachmentOutgoing : colors.chatBackgroundAttachmentIncoming
} else {
return viewModel.isSentByCurrentUser ? colors.chatBackgroundOutgoing : colors.chatBackgroundIncoming
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly different background colors and logic. Else is used by composer

@@ -7,13 +7,12 @@ import SwiftUI

/// Background modifier for message reference views.
public struct ReferenceMessageViewBackgroundModifier: ViewModifier {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used by composer and message list, needed changes to support different background colors.

message: message,
reactionsShown: topReactionsShown
)
MessageDecoratedView(
Copy link
Contributor Author

@laevandus laevandus Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be used by reactions overlay. Message gesture logic stays here, but the main layout logic is now there along with reactions.

@github-actions
Copy link

1 Warning
⚠️ Big PR
1 Message
πŸ“– There seems to be app changes but CHANGELOG wasn't modified.
Please include an entry if the PR includes user-facing changes.
You can find it at CHANGELOG.md.

Generated by 🚫 Danger

)
}

var body: some View {
Copy link
Contributor Author

@laevandus laevandus Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main changes here is that we use alignment instead of spacers. Fixes many of small layout issues in messages with different attachments and applies updated spacings from Figma.
Next PR will implement updated bottom and top reactions.

Comment on lines -88 to +86
Color(message.isSentByCurrentUser ? colors.chatThreadConnectorOutgoing : colors.chatThreadConnectorIncoming),
Color(message.isSentByCurrentUser ? colors.chatBackgroundOutgoing : colors.chatBackgroundIncoming),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the color for now, it looks too out of place if different color is used. There is an open question about it in Figma.

Copy link
Contributor Author

@laevandus laevandus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code comments

@laevandus laevandus marked this pull request as draft February 18, 2026 09:54
# Conflicts:
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_jumpToUnreadButton.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_typingIndicator.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_viewModelInit_withReactions.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_withReactions.1.png
@laevandus laevandus marked this pull request as ready for review February 19, 2026 14:42
Copy link
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good in general, left few comments. Also, few UI glitches shared on slack.

viewModel: messageViewModel
)
)
.environment(\.channelTranslationLanguage, channel.membership?.language)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we pass the translation language in the options now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, will clean this up in the PR finally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked into this, going to be a bigger refactoring along with message view model env key and translations store. There is a ticket for that, proposing to do this with a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, let's do it in another one

@State private var frame: CGRect = .zero
@State private var computeFrame = false

public init(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bit tricky to track changes because of the new file. What else was changed except the swipe modifier? Which section to focus on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got messy since I renamed, iterated a couple of times.

Handwritten notes

/// A view that renders a single message item in the message list, including
/// the avatar, bubble, reactions, thread replies, delivery status, and gesture handling.
public struct MessageItemView<Factory: ViewFactory>: View {
    
    public init(
        factory: Factory,
        channel: ChatChannel,
        message: ChatMessage,
        width: CGFloat? = nil,
        showsAllInfo: Bool,
        shownAsPreview: Bool = false, <<< for changing colors when shown in the ReactionsOverlayView
        
    public var body: some View {
        HStack(alignment: .bottom) {
// …    
                    .onTapGesture(count: 2) {
                        if messageViewModel.isDoubleTapOverlayEnabled {
                            handleGestureForMessage(showsMessageActions: true)
                        }
                    }
                    .onLongPressGesture(perform: {
                        handleGestureForMessage(showsMessageActions: true)
                    })
                    
                    // Swiping is in a view modifier for making it easier to turn it off when this view is used in the ReactionsOverlayView
                    .modifier(SwipeToReplyModifier(
                        message: message,
                        channel: channel,
                        isSwipeToQuoteReplyPossible: !shownAsPreview && messageViewModel.isSwipeToQuoteReplyPossible,
                        quotedMessage: $quotedMessage
                    ))
            }
// …
    }

    // MARK: - Message Content

    private var messageView: some View {
        
        // layout changes with new spacings and using alignment instead of spacers
        
        
        HStack(alignment: .bottom, spacing: tokens.spacingXs) {
            if !messageViewModel.isRightAligned {
                avatarView
            }

            VStack(
                alignment: messageViewModel.isRightAligned ? .trailing : .leading,
                spacing: tokens.spacingXxs
            ) {
                if messageViewModel.isPinned {
                    MessagePinDetailsView(
                        message: message,
                        reactionsShown: topReactionsShown
                    )
                }

                messageBubbleContent
                    .accessibilityElement(children: .contain)
                    .accessibilityIdentifier("MessageView")

                if !isInThread {
                    threadRepliesView
                }

                if bottomReactionsShown {
                    factory.makeBottomReactionsView(
                        options: ReactionsBottomViewOptions(
                            message: message,
                            showsAllInfo: showsAllInfo,
                            onTap: {
                                handleGestureForMessage(showsMessageActions: false)
                            },
                            onLongPress: {
                                handleGestureForMessage(showsMessageActions: false)
                            }
                        )
                    )
                }

                if messageViewModel.translatedText != nil {
                    factory.makeMessageTranslationFooterView(
                        options: MessageTranslationFooterViewOptions(
                            messageViewModel: messageViewModel,
                            usesInvertedStyle: shownAsPreview <<< - makes the color light when shown on ReactionsOverlayView
                        )
                    )
                }

                // …
    }

    // MARK: - Sub-views

    @ViewBuilder
    private var messageBubbleContent: some View {
        Group {
            if messageViewModel.usesScrollView { <<< special for ReactionsOverlayView when long message becomes scrollable
                ScrollView {
                    MessageView(
                        factory: factory,
                        message: message,
                        contentWidth: contentWidth,
                        isFirst: showsAllInfo,
                        scrolledId: $scrolledId
                    )
                }
            } else {
                MessageView(
                    factory: factory,
                    message: message,
                    contentWidth: contentWidth,
                    isFirst: showsAllInfo,
                    scrolledId: $scrolledId
                )
            }
        }
        .overlay(
            topReactionsShown ?
                factory.makeMessageReactionView(
                    options: MessageReactionViewOptions(
                        message: message,
                        onTapGesture: {
                            handleGestureForMessage(showsMessageActions: false)
                        },
                        onLongPressGesture: {
                            handleGestureForMessage(showsMessageActions: false)
                        }
                    )
                )
                : nil,
            alignment: messageViewModel.isRightAligned ? .trailing : .leading 
        )
        .overlay(
            messageViewModel.failureIndicatorShown ? SendFailureIndicator() : nil
        )
        .frame(maxWidth: contentWidth, alignment: messageViewModel.isRightAligned ? .trailing : .leading)
    }

    @ViewBuilder
    private var avatarView: some View {
        factory.makeUserAvatarView(
            options: UserAvatarViewOptions(
                user: message.author,
                size: AvatarSize.medium,
                showsIndicator: false
            )
        )
        .opacity(isLast || showsAllInfo ? 1 : 0)
    }

    @ViewBuilder
    private var threadRepliesView: some View {
        if message.replyCount > 0 {
            factory.makeMessageRepliesView(
                options: MessageRepliesViewOptions(
                    channel: channel,
                    message: message,
                    replyCount: message.replyCount,
                    usesInvertedStyle: shownAsPreview
                )
            )
            .accessibilityElement(children: .contain)
            .accessibility(identifier: "MessageRepliesView")
        } else if message.showReplyInChannel,
                  let parentId = message.parentMessageId,
                  let controller = utils.channelControllerFactory.currentChannelController,
                  let parentMessage = controller.dataStore.message(id: parentId) {
            factory.makeMessageRepliesShownInChannelView(
                options: MessageRepliesShownInChannelViewOptions(
                    channel: channel,
                    message: message,
                    parentMessage: parentMessage,
                    replyCount: parentMessage.replyCount,
                    usesInvertedStyle: shownAsPreview
                )
            )
            .accessibilityElement(children: .contain)
            .accessibility(identifier: "MessageRepliesView")
        } else if message.showReplyInChannel, let parentId = message.parentMessageId {
            LazyMessageRepliesView(
                factory: factory,
                channel: channel,
                message: message,
                parentMessageController: chatClient.messageController(
                    cid: channel.cid,
                    messageId: parentId
                ),
                usesInvertedStyle: shownAsPreview
            )
            .accessibilityElement(children: .contain)
            .accessibility(identifier: "MessageRepliesView")
        }
    }

    @ViewBuilder
    private var deliveryStatusView: some View {
        
        // Spacings and coloring changes
        
        if message.isSentByCurrentUser && channel.config.readEventsEnabled {
            HStack(spacing: tokens.spacingXxs) {
                factory.makeMessageReadIndicatorView(
                    options: MessageReadIndicatorViewOptions(
                        channel: channel,
                        message: message,
                        usesInvertedStyle: shownAsPreview
                    )
                )

                if messageViewModel.messageDateShown {
                    factory.makeMessageDateView(
                        options: MessageDateViewOptions(message: message, usesInvertedStyle: shownAsPreview)
                    )
                }
            }
            .padding(.bottom, tokens.spacingXxs)
        } else if messageViewModel.authorAndDateShown {
            factory.makeMessageAuthorAndDateView(
                options: MessageAuthorAndDateViewOptions(message: message, usesInvertedStyle: shownAsPreview)
            )
            .padding(.bottom, tokens.spacingXxs)
        } else if messageViewModel.messageDateShown {
            factory.makeMessageDateView(
                options: MessageDateViewOptions(message: message, usesInvertedStyle: shownAsPreview)
            )
            .padding(.bottom, tokens.spacingXxs)
        }
    }

    // MARK: - Computed Properties
//  …
}

// MARK: - Swipe to Reply

private struct SwipeToReplyModifier: ViewModifier {
    let message: ChatMessage
    let channel: ChatChannel
    let isSwipeToQuoteReplyPossible: Bool
    @Binding var quotedMessage: ChatMessage?

    @Injected(\.images) private var images
    @Injected(\.utils) private var utils

    @State private var offsetX: CGFloat = 0
    @GestureState private var offset: CGSize = .zero

    private let replyThreshold: CGFloat = 60

    func body(content: Content) -> some View {
        
        // Same as before, just moved
        
        
        content
            .offset(x: min(offsetX, maximumHorizontalSwipeDisplacement))
            .simultaneousGesture(
                DragGesture(
                    minimumDistance: minimumSwipeDistance,
                    coordinateSpace: .local
                )
                .updating($offset) { (value, gestureState, _) in
                    guard isSwipeToQuoteReplyPossible else {
                        return
                    }
                    let diff = CGSize(
                        width: value.location.x - value.startLocation.x,
                        height: value.location.y - value.startLocation.y
                    )

                    if diff == .zero {
                        gestureState = .zero
                    } else {
                        gestureState = value.translation
                    }
                }
            )
            .onChange(of: offset, perform: { _ in
                if !channel.config.quotesEnabled {
                    return
                }

                if offset == .zero {
                    setOffsetX(value: 0)
                } else {
                    dragChanged(to: offset.width)
                }
            })
            .overlay(
                offsetX > 0 ?
                    TopLeftView {
                        Image(uiImage: images.messageActionInlineReply)
                    }
                    .offset(x: -32)
                    : nil
            )
    }

    private var maximumHorizontalSwipeDisplacement: CGFloat {
        replyThreshold + 30
    }

    private var minimumSwipeDistance: CGFloat {
        utils.messageListConfig.messageDisplayOptions.minimumSwipeGestureDistance
    }

    private func dragChanged(to value: CGFloat) {
        let horizontalTranslation = value

        if horizontalTranslation < 0 {
            return
        }

        if horizontalTranslation >= minimumSwipeDistance {
            offsetX = horizontalTranslation
        } else {
            offsetX = 0
        }

        if offsetX > replyThreshold && quotedMessage != message {
            UIImpactFeedbackGenerator(style: .medium).impactOccurred()
            withAnimation {
                quotedMessage = message
            }
        }
    }

    private func setOffsetX(value: CGFloat) {
        withAnimation(.interpolatingSpring(stiffness: 170, damping: 20)) {
            offsetX = value
        }
    }
}

factory: factory,
channel: channel,
message: message,
parentMessageController: chatClient.messageController(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this will get re-created many times?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before, this has a TODO for fixing it in v5. Needs separate PR.

2 * availableWidth / 3
} else {
availableWidth / 4
82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels random?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will document this

showDelivered: Bool = false,
localState: LocalMessageState? = nil
localState: LocalMessageState? = nil,
textColor: UIColor? = nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we pass this text color everywhere? Can't we put it in the color palete?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In message list this is one value, on reactions overlay this needs to be a different (lighter) color. Let me think if there is nicer way. Environment key could be an option here for communicating this without littering everything with extra color arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change it around.

# Conflicts:
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelViewDateOverlay_Tests/test_chatChannelView_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_liquidGlassStyle_composer_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_themedNavigationBar_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerCurrentUserColor_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerEditedAIGenerated_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerEdited_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerViewSentThisUser_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_myMessageIsNotTranslated_snapshot.default-light.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_myMessageIsNotTranslated_snapshot.extraExtraExtraLarge-light.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_myMessageIsNotTranslated_snapshot.rightToLeftLayout-default.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_myMessageIsNotTranslated_snapshot.small-dark.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListViewLastGroupHeader_Tests/test_messageListView_headerOnTop.1.png
# Conflicts:
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatQuotedMessageView_Tests/test_chatQuotedMessageView_withAttachment.default-light.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatQuotedMessageView_Tests/test_chatQuotedMessageView_withAttachment.extraExtraExtraLarge-light.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatQuotedMessageView_Tests/test_chatQuotedMessageView_withAttachment.rightToLeftLayout-default.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatQuotedMessageView_Tests/test_chatQuotedMessageView_withAttachment.small-dark.png
Comment on lines 109 to 127
@@ -123,6 +123,7 @@ struct LazyGiphyView: View {
.processors([ImageProcessors.Resize(width: width)])
.priority(.high)
.aspectRatio(contentMode: .fit)
.frame(width: width)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout issue when it was shown in the reactions overlay.

@nuno-vieira nuno-vieira self-requested a review February 20, 2026 16:05
Copy link
Member

@nuno-vieira nuno-vieira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks really good πŸ‘Œ I'm only requesting changes because the shownInMessageList: false goes against the new API design of the Quoted Message View. Other than that, all good

options: QuotedMessageViewOptions(
quotedMessage: quotedMessage,
quotedByCurrentUser: true,
shownInMessageList: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be having this. The goal of ChatQuotedMessageView vs ComposerQuotedMessageView is to have the context of where is the child QuotedMessageView is being rendered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight refactor of ReferenceMessageView where there is now outgoing style parameter since the styling in composer uses quotedMessage.isSentByCurrentUser, but in chat quoted message view it is parent view's isSentByCurrentUser. This is needed for the vertical line in that view which needs different color.

Example, here I am quoting my own message and other participant's message. Styling needs to use the outgoing style.

Simulator Screenshot - iPhone 17 Pro - 2026-02-23 at 10 05 35

quotedMessage: quotedMessage
quotedMessage: quotedMessage,
quotedByCurrentUser: parentMessageSentByCurrentUser,
shownInMessageList: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should opt for either passing the background color or moving the ReferenceMessageViewBackgroundModifier to the upper views

Suggested change
shownInMessageList: true
backgroundColor: Color

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved

Comment on lines +12 to +15
let backgroundColor: Color

init(isSentByCurrentUser: Bool) {
self.isSentByCurrentUser = isSentByCurrentUser
init(backgroundColor: Color) {
self.backgroundColor = backgroundColor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup this makes sense. I initially wanted to do this, I was already guessing it would require changes since it was not very flexible the way it was before

# Conflicts:
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelViewDateOverlay_Tests/test_chatChannelView_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_liquidGlassStyle_composer_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_themedNavigationBar_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_imageAttachments_failedWhenMessageTextIsEmpty_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_imageAttachments_failed_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerOtherUserColor_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerViewPinned_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerViewSentOtherUser_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_editingFailed_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_messageContainerView_sendingFailed_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonDisabled_translatedTextShown_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_originalTextShown_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageContainerView_Tests/test_translatedText_showOriginalTranslatedButtonEnabled_translatedTextShown_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListViewLastGroupHeader_Tests/test_messageListView_headerOnTop.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_jumpToUnreadButton.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_typingIndicator.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_viewModelInit_noReactions.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_viewModelInit_unreadIndicator.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_viewModelInit_withReactions.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageListView_Tests/test_messageListView_withReactions.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageViewMultiRowReactions_Tests/test_messageViewMultiRowReactions_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageView_Tests/test_messageRepliesViewShownInChannel_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageView_Tests/test_messageRepliesView_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlayView_noReactions.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlayView_snapshot.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlayView_translated.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlayView_usersReactions.1.png
#	StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ReactionsOverlayView_Tests/test_reactionsOverlay_veryLongMessage.1.png
@github-actions
Copy link

Public Interface

+ public struct MessageItemView: View  
+ 
+   public var body: some View
+   
+ 
+   public init(factory: Factory,channel: ChatChannel,message: ChatMessage,width: CGFloat? = nil,showsAllInfo: Bool,shownAsPreview: Bool = false,isInThread: Bool,isLast: Bool,scrolledId: Binding<String?>,quotedMessage: Binding<ChatMessage?>,onLongPress: @escaping (MessageDisplayInfo) -> Void,viewModel: MessageViewModel? = nil)

+ public final class MessageItemViewOptions: Sendable  
+ 
+   public let channel: ChatChannel
+   public let message: ChatMessage
+   public let width: CGFloat?
+   public let showsAllInfo: Bool
+   public let shownAsPreview: Bool
+   public let isInThread: Bool
+   public let scrolledId: Binding<String?>
+   public let quotedMessage: Binding<ChatMessage?>
+   public let onLongPress: @MainActor (MessageDisplayInfo) -> Void
+   public let isLast: Bool
+   public let viewModel: MessageViewModel?
+   
+ 
+   public init(channel: ChatChannel,message: ChatMessage,width: CGFloat?,showsAllInfo: Bool,shownAsPreview: Bool = false,isInThread: Bool,scrolledId: Binding<String?>,quotedMessage: Binding<ChatMessage?>,onLongPress: @escaping @MainActor (MessageDisplayInfo) -> Void,isLast: Bool,viewModel: MessageViewModel? = nil)

- public struct MessageContainerView: View  
- 
-   public var body: some View
-   
- 
-   public init(factory: Factory,channel: ChatChannel,message: ChatMessage,width: CGFloat? = nil,showsAllInfo: Bool,isInThread: Bool,isLast: Bool,scrolledId: Binding<String?>,quotedMessage: Binding<ChatMessage?>,onLongPress: @escaping (MessageDisplayInfo) -> Void,viewModel: MessageViewModel? = nil)

- public final class MessageContainerViewOptions: Sendable  
- 
-   public let channel: ChatChannel
-   public let message: ChatMessage
-   public let width: CGFloat?
-   public let showsAllInfo: Bool
-   public let isInThread: Bool
-   public let scrolledId: Binding<String?>
-   public let quotedMessage: Binding<ChatMessage?>
-   public let onLongPress: @MainActor (MessageDisplayInfo) -> Void
-   public let isLast: Bool
-   
- 
-   public init(channel: ChatChannel,message: ChatMessage,width: CGFloat?,showsAllInfo: Bool,isInThread: Bool,scrolledId: Binding<String?>,quotedMessage: Binding<ChatMessage?>,onLongPress: @escaping @MainActor (MessageDisplayInfo) -> Void,isLast: Bool)

 public final class MessageRepliesShownInChannelViewOptions: Sendable  
-   
+   public let usesInvertedStyle: Bool
- 
+   
-   public init(channel: ChatChannel,message: ChatMessage,parentMessage: ChatMessage,replyCount: Int)
+ 
+   public init(channel: ChatChannel,message: ChatMessage,parentMessage: ChatMessage,replyCount: Int,usesInvertedStyle: Bool = false)

 public final class MessageDateViewOptions: Sendable  
-   public let textColor: Color?
+   public let usesInvertedStyle: Bool
-   public init(message: ChatMessage,textColor: Color? = nil)
+   public init(message: ChatMessage,usesInvertedStyle: Bool = false)

 @MainActor open class MessageViewModel: ObservableObject  
-   public var originalTextShown: Bool
+   @Published public var usesScrollView: Bool
-   public var systemMessageShown: Bool
+   public var originalTextShown: Bool
-   public var reactionsShown: Bool
+   public var systemMessageShown: Bool
-   public var failureIndicatorShown: Bool
+   public var reactionsShown: Bool
-   open var authorAndDateShown: Bool
+   public var failureIndicatorShown: Bool
-   open var messageDateShown: Bool
+   open var authorAndDateShown: Bool
-   public var isPinned: Bool
+   open var messageDateShown: Bool
-   public var isRightAligned: Bool
+   public var isPinned: Bool
-   public var messageAuthor: ChatUser?
+   public var isRightAligned: Bool
-   open var isSwipeToQuoteReplyPossible: Bool
+   public var messageAuthor: ChatUser?
-   open var textContent: String
+   public var isDoubleTapOverlayEnabled: Bool
-   public var translatedText: String?
+   open var isSwipeToQuoteReplyPossible: Bool
-   public var translatedLanguageText: String?
+   open var textContent: String
-   public var originalTranslationButtonText: String
+   public var translatedText: String?
-   
+   public var translatedLanguageText: String?
- 
+   public var originalTranslationButtonText: String
-   public init(message: ChatMessage,channel: ChatChannel?)
+   
-   
+ 
- 
+   public init(message: ChatMessage,channel: ChatChannel?)
-   public func showOriginalText()
+   
-   public func hideOriginalText()
+ 
+   public func showOriginalText()
+   public func hideOriginalText()
+   public func isHighlighted(messageId: String?)-> Bool

 public final class QuotedMessageViewOptions: Sendable  
-   public let padding: EdgeInsets?
+   public let outgoing: Bool
-   
+   public let padding: EdgeInsets?
- 
+   
-   public init(quotedMessage: ChatMessage,padding: EdgeInsets? = nil)
+ 
+   public init(quotedMessage: ChatMessage,outgoing: Bool,padding: EdgeInsets? = nil)

 public struct MessageTranslationFooterView: View  
-   public init(messageViewModel: MessageViewModel)
+   public init(messageViewModel: MessageViewModel,usesInvertedStyle: Bool = false)

 public final class MessageTranslationFooterViewOptions: Sendable  
-   
+   public let usesInvertedStyle: Bool
- 
+   
-   public init(messageViewModel: MessageViewModel)
+ 
+   public init(messageViewModel: MessageViewModel,usesInvertedStyle: Bool = false)

 public struct ReferenceMessageView: View  
-   public let isSentByCurrentUser: Bool
+   public let outgoing: Bool
-   public init(title: String,subtitle: String,isSentByCurrentUser: Bool,@ViewBuilder iconPreview: () -> IconPreview,@ViewBuilder attachmentPreview: () -> AttachmentPreview)
+   public init(title: String,subtitle: String,outgoing: Bool,@ViewBuilder iconPreview: () -> IconPreview,@ViewBuilder attachmentPreview: () -> AttachmentPreview)

 public final class MessageDisplayOptions  
-   public let showAvatars: Bool
+   public let showIncomingMessageAvatar: Bool
-   public let showAvatarsInGroups: Bool
+   public let showOutgoingMessageAvatar: Bool
-   public let showMessageDate: Bool
+   public let showAvatarsInGroups: Bool
-   public let showAuthorName: Bool
+   public let showMessageDate: Bool
-   public let animateChanges: Bool
+   public let showAuthorName: Bool
-   public let dateLabelSize: CGFloat
+   public let animateChanges: Bool
-   public let lastInGroupHeaderSize: CGFloat
+   public let dateLabelSize: CGFloat
-   public let newMessagesSeparatorSize: CGFloat
+   public let lastInGroupHeaderSize: CGFloat
-   public let minimumSwipeGestureDistance: CGFloat
+   public let newMessagesSeparatorSize: CGFloat
-   public let currentUserMessageTransition: AnyTransition
+   public let minimumSwipeGestureDistance: CGFloat
-   public let otherUserMessageTransition: AnyTransition
+   public let currentUserMessageTransition: AnyTransition
-   public let shouldAnimateReactions: Bool
+   public let otherUserMessageTransition: AnyTransition
-   public let reactionsPlacement: ReactionsPlacement
+   public let shouldAnimateReactions: Bool
-   public let reactionsStyle: ReactionsStyle
+   public let reactionsPlacement: ReactionsPlacement
-   public let showOriginalTranslatedButton: Bool
+   public let reactionsStyle: ReactionsStyle
-   public let messageLinkDisplayResolver: @MainActor (ChatMessage) -> [NSAttributedString.Key: Any]
+   public let showOriginalTranslatedButton: Bool
-   public let spacerWidth: (CGFloat) -> CGFloat
+   public let messageLinkDisplayResolver: @MainActor (ChatMessage) -> [NSAttributedString.Key: Any]
-   public let reactionsTopPadding: (ChatMessage) -> CGFloat
+   public let spacerWidth: @MainActor (CGFloat) -> CGFloat
-   public let dateSeparator: (ChatMessage, ChatMessage) -> Date?
+   public let reactionsTopPadding: (ChatMessage) -> CGFloat
-   public static var defaultLinkDisplay: @MainActor (ChatMessage) -> [NSAttributedString.Key: Any]
+   public let dateSeparator: (ChatMessage, ChatMessage) -> Date?
-   public static var defaultSpacerWidth: (CGFloat) -> (CGFloat)
+   public static var defaultLinkDisplay: @MainActor (ChatMessage) -> [NSAttributedString.Key: Any]
-   public static var defaultReactionsTopPadding: (ChatMessage) -> CGFloat
+   public static var defaultSpacerWidth: @MainActor (CGFloat) -> (CGFloat)
-   
+   public static var defaultReactionsTopPadding: (ChatMessage) -> CGFloat
- 
+   
-   public init(showAvatars: Bool = true,showAvatarsInGroups: Bool? = nil,showMessageDate: Bool = true,showAuthorName: Bool = true,animateChanges: Bool = true,overlayDateLabelSize: CGFloat = 40,lastInGroupHeaderSize: CGFloat = 0,newMessagesSeparatorSize: CGFloat = 50,minimumSwipeGestureDistance: CGFloat = 20,currentUserMessageTransition: AnyTransition = .identity,otherUserMessageTransition: AnyTransition = .identity,shouldAnimateReactions: Bool = true,reactionsPlacement: ReactionsPlacement = .top,reactionsStyle: ReactionsStyle = .segmented,showOriginalTranslatedButton: Bool = false,messageLinkDisplayResolver: @escaping @MainActor (ChatMessage) -> [NSAttributedString.Key: Any] = MessageDisplayOptions
+ 
-             .defaultLinkDisplay,spacerWidth: @escaping (CGFloat) -> CGFloat = MessageDisplayOptions.defaultSpacerWidth,reactionsTopPadding: @escaping (ChatMessage) -> CGFloat = MessageDisplayOptions.defaultReactionsTopPadding,dateSeparator: @escaping (ChatMessage, ChatMessage) -> Date? = MessageDisplayOptions.defaultDateSeparator)
+   public init(showIncomingMessageAvatar: Bool = true,showOutgoingMessageAvatar: Bool = false,showAvatarsInGroups: Bool = true,showMessageDate: Bool = true,showAuthorName: Bool = true,animateChanges: Bool = true,overlayDateLabelSize: CGFloat = 40,lastInGroupHeaderSize: CGFloat = 0,newMessagesSeparatorSize: CGFloat = 50,minimumSwipeGestureDistance: CGFloat = 20,currentUserMessageTransition: AnyTransition = .identity,otherUserMessageTransition: AnyTransition = .identity,shouldAnimateReactions: Bool = true,reactionsPlacement: ReactionsPlacement = .top,reactionsStyle: ReactionsStyle = .segmented,showOriginalTranslatedButton: Bool = false,messageLinkDisplayResolver: @escaping @MainActor (ChatMessage) -> [NSAttributedString.Key: Any] = MessageDisplayOptions
-   
+             .defaultLinkDisplay,spacerWidth: @escaping @MainActor (CGFloat) -> CGFloat = MessageDisplayOptions.defaultSpacerWidth,reactionsTopPadding: @escaping (ChatMessage) -> CGFloat = MessageDisplayOptions.defaultReactionsTopPadding,dateSeparator: @escaping (ChatMessage, ChatMessage) -> Date? = MessageDisplayOptions.defaultDateSeparator)
- 
+   
-   public func showAvatars(for channel: ChatChannel)-> Bool
+ 
-   public static func defaultDateSeparator(message: ChatMessage,previous: ChatMessage)-> Date?
+   public func showAvatars(for channel: ChatChannel,incoming: Bool)-> Bool
+   public static func defaultDateSeparator(message: ChatMessage,previous: ChatMessage)-> Date?

 @MainActor open class QuotedMessageViewModel  
-   open var title: String
+   public let outgoing: Bool
-   open var authorName: String
+   open var title: String
-   open var isSentByCurrentUser: Bool
+   open var authorName: String
-   public init(message: ChatMessage,currentUser: CurrentChatUser?)
+   public init(message: ChatMessage,currentUser: CurrentChatUser?,outgoing: Bool)

 public struct MessageRepliesView: View  
-   public init(factory: Factory,channel: ChatChannel,message: ChatMessage,replyCount: Int,showReplyCount: Bool = true,isRightAligned: Bool? = nil,threadReplyMessage: ChatMessage? = nil)
+   public init(factory: Factory,channel: ChatChannel,message: ChatMessage,replyCount: Int,showReplyCount: Bool = true,isRightAligned: Bool? = nil,usesInvertedStyle: Bool = false,threadReplyMessage: ChatMessage? = nil)

 extension ViewFactory  
-   public func makeMessageContainerView(options: MessageContainerViewOptions)-> some View
+   public func makeMessageItemView(options: MessageItemViewOptions)-> some View

 public struct ReactionsOverlayView: View  
-   public init(factory: Factory,channel: ChatChannel,currentSnapshot: UIImage,messageDisplayInfo: MessageDisplayInfo,topOffset: CGFloat = 0,bottomOffset: CGFloat = 0,onBackgroundTap: @escaping () -> Void,onActionExecuted: @escaping (MessageActionInfo) -> Void,viewModel: ReactionsOverlayViewModel? = nil,messageViewModel: MessageViewModel? = nil)
+   public init(factory: Factory,channel: ChatChannel,currentSnapshot: UIImage,messageDisplayInfo: MessageDisplayInfo,verticalInset: CGFloat = 40,onBackgroundTap: @escaping () -> Void,onActionExecuted: @escaping (MessageActionInfo) -> Void,viewModel: ReactionsOverlayViewModel? = nil,messageViewModel: MessageViewModel? = nil)

 public final class MessageReadIndicatorViewOptions: Sendable  
-   
+   public let usesInvertedStyle: Bool
- 
+   
-   public init(channel: ChatChannel,message: ChatMessage)
+ 
+   public init(channel: ChatChannel,message: ChatMessage,usesInvertedStyle: Bool = false)

 public final class ChatQuotedMessageViewOptions: Sendable  
-   public let scrolledId: Binding<String?>
+   public let parentMessage: ChatMessage
-   
+   public let scrolledId: Binding<String?>
- 
+   
-   public init(quotedMessage: ChatMessage,scrolledId: Binding<String?>)
+ 
+   public init(quotedMessage: ChatMessage,parentMessage: ChatMessage,scrolledId: Binding<String?>)

 public final class MessagePaddings  
-   public init(horizontal: CGFloat = 8,quotedViewPadding: CGFloat = 8,singleBottom: CGFloat = 8,groupBottom: CGFloat = 2)
+   public init(horizontal: CGFloat = 16,quotedViewPadding: CGFloat = 16,singleBottom: CGFloat = 8,groupBottom: CGFloat = 4)

 public struct MessageReadIndicatorView: View  
-   public init(readUsers: [ChatUser],showReadCount: Bool,showDelivered: Bool = false,localState: LocalMessageState? = nil)
+   public init(readUsers: [ChatUser],showReadCount: Bool,showDelivered: Bool = false,localState: LocalMessageState? = nil,usesInvertedStyle: Bool = false)

 public final class MessageRepliesViewOptions: Sendable  
-   
+   public let usesInvertedStyle: Bool
- 
+   
-   public init(channel: ChatChannel,message: ChatMessage,replyCount: Int)
+ 
+   public init(channel: ChatChannel,message: ChatMessage,replyCount: Int,usesInvertedStyle: Bool = false)

 public struct MessageAuthorAndDateView: View  
-   public init(message: ChatMessage)
+   public init(message: ChatMessage,usesInvertedStyle: Bool = false)

 public struct ChatQuotedMessageView: View  
-   public init(factory: Factory,quotedMessage: ChatMessage,scrolledId: Binding<String?>)
+   public init(factory: Factory,quotedMessage: ChatMessage,parentMessage: ChatMessage,scrolledId: Binding<String?>)

 public final class MessageAuthorAndDateViewOptions: Sendable  
-   
+   public let usesInvertedStyle: Bool
- 
+   
-   public init(message: ChatMessage)
+ 
+   public init(message: ChatMessage,usesInvertedStyle: Bool = false)

 public struct MessageAuthorView: View  
-   public init(message: ChatMessage)
+   public init(message: ChatMessage,usesInvertedStyle: Bool = false)

@Stream-SDK-Bot
Copy link
Collaborator

SDK Size

title v5 branch diff status
StreamChatSwiftUI 9.34 MB 9.46 MB +117 KB 🟒

@Stream-SDK-Bot
Copy link
Collaborator

StreamChatSwiftUI XCSize

Object Diff (bytes)
MessageItemView.o +451537
MessageContainerView.o -214957
MessageRepliesView.o -89922
MessageListHelperViews.o -24346
ReactionsOverlayView.o -12280
Show 23 more objects
Object Diff (bytes)
MessageViewModel.o +2727
ChatQuotedMessageView.o +998
MessageViewFactoryOptions.o +853
ComposerQuotedMessageView.o +746
QuotedMessageView.o -670
MessageListConfig.o +662
MessageView.o +644
ComposerViewFactoryOptions.o +605
GiphyAttachmentView.o -514
ReferenceMessageViewBackgroundModifier.o -455
DefaultViewFactory.o +448
MessageTranslationFooterView.o +382
ChatChannelHeaderViewModifier.o -296
VoiceRecordingContainerView.o +136
ImageAttachmentView.o +136
MessageListView.o +132
EditedMessageView.o +116
LinkAttachmentView.o +116
QuotedMessageViewModel.o +106
FileAttachmentView.o +104
VideoAttachmentView.o +92
ReactionsViewFactoryOptions.o -88
ChatThreadListItem.o -76

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
74.9% Coverage on New Code (required β‰₯ 80%)

See analysis details on SonarQube Cloud

Copy link
Member

@nuno-vieira nuno-vieira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! βœ…

@laevandus laevandus merged commit 17864df into v5 Feb 23, 2026
10 of 11 checks passed
@laevandus laevandus deleted the v5-message-list-layout branch February 23, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants