Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix giphy previews in the channel list and quote replies [#1333](https://github.com/GetStream/stream-chat-swiftui/pull/1333)
- Fix black borders on image preview in composer when editing or quoting a message [#1334](https://github.com/GetStream/stream-chat-swiftui/pull/1334)
- Fix quoted image preview not updating when switching to a different quoted message [#1334](https://github.com/GetStream/stream-chat-swiftui/pull/1334)
- Use fixed width for attachment previews [#1335](https://github.com/GetStream/stream-chat-swiftui/pull/1335)
- Fix showing bubble for quoted message and file or image attachment [#1335](https://github.com/GetStream/stream-chat-swiftui/pull/1335)
- Fix scaling of giphy attachments [#1335](https://github.com/GetStream/stream-chat-swiftui/pull/1335)

### 🔄 Changed
- Renamed the `onMessageSent` callback to `willSendMessage` in `MessageComposerViewModel`, `ViewModelsFactory`, and `ComposerViewFactoryOptions` [#1327](https://github.com/GetStream/stream-chat-swiftui/pull/1327)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public struct VoiceRecordingContainerView<Factory: ViewFactory>: View {
.roundWithBorder(cornerRadius: tokens.messageBubbleRadiusAttachment)
}
}
.frame(width: width, alignment: message.isRightAligned ? .trailing : .leading)
.onReceive(handler.$context, perform: { value in
guard message.voiceRecordingAttachments.count > 1 else { return }
if value.state == .playing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,40 @@ import StreamChat
import SwiftUI

public struct AttachmentTextView<Factory: ViewFactory>: View {
@Injected(\.colors) private var colors
@Injected(\.fonts) private var fonts
@Injected(\.tokens) private var tokens

var factory: Factory
var message: ChatMessage
let injectedBackgroundColor: UIColor?
var availableWidth: CGFloat

public init(factory: Factory = DefaultViewFactory.shared, message: ChatMessage, injectedBackgroundColor: UIColor? = nil) {
public init(
factory: Factory = DefaultViewFactory.shared,
message: ChatMessage,
availableWidth: CGFloat
) {
self.factory = factory
self.message = message
self.injectedBackgroundColor = injectedBackgroundColor
self.availableWidth = availableWidth
}

public var body: some View {
HStack {
factory.makeStreamTextView(options: .init(message: message))
.padding(.horizontal, tokens.spacingXxs)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.background(Color(backgroundColor))
.accessibilityIdentifier("MessageTextView")
factory.makeStreamTextView(options: .init(message: message))
.padding(.horizontal, tokens.spacingXxs)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: maxTextWidth, alignment: .leading)
.accessibilityIdentifier("MessageTextView")
}

private var backgroundColor: UIColor {
if let injectedBackgroundColor {
return injectedBackgroundColor
}
return message.isSentByCurrentUser ? colors.chatBackgroundOutgoing : colors.chatBackgroundIncoming
/// Limit text width for messages with portrait image attachment.
private var maxTextWidth: CGFloat {
guard message.hasSingleMediaAttachmentWithCaption else { return availableWidth }
let mediaAttachments = MediaAttachment.galleryOrdered(from: message)
let orientation = MediaGalleryOrientation(mediaAttachments: mediaAttachments)
let size = MessageMediaAttachmentsContainerView<Factory>.containerSize(
for: mediaAttachments.count,
orientation: orientation,
maxItemWidth: availableWidth
)
return size.width
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@Injected(\.fonts) private var fonts
@Injected(\.images) private var images
@Injected(\.tokens) private var tokens
@Injected(\.utils) private var utils

let factory: Factory
let message: ChatMessage
Expand Down Expand Up @@ -47,7 +48,6 @@
.foregroundColor(colors.chatTextOutgoing.toColor)
.padding(.all, tokens.spacingSm)
}

LazyGiphyView(
source: message.giphyAttachments[0].previewURL,
width: width
Expand All @@ -69,6 +69,8 @@
execute(action: action)
} label: {
Text(action.value.firstUppercased)
.lineLimit(1)
.minimumScaleFactor(0.5)
.padding(.horizontal, 4)
.padding(.vertical)
}
Expand All @@ -83,6 +85,7 @@
}
}
}
.frame(maxWidth: width)
.modifier(
factory.styles.makeMessageViewModifier(
for: MessageModifierInfo(
Expand All @@ -91,7 +94,6 @@
)
)
)
.frame(maxWidth: width)
.accessibilityIdentifier("GiphyAttachmentView")
}

Expand Down Expand Up @@ -127,8 +129,10 @@
if let imageContainer = state.imageContainer {
if imageContainer.type == .gif {
AnimatedGifView(imageContainer: imageContainer)
} else {
state.image
} else if let image = state.image {
image
.resizable()
.scaledToFill()
}
} else if state.error != nil {
Color(.secondarySystemBackground)
Expand All @@ -142,26 +146,42 @@
.onDisappear(.cancel)
.processors([ImageProcessors.Resize(width: width)])
.priority(.high)
.aspectRatio(contentMode: .fit)
.frame(width: width)
.frame(maxHeight: 250)
.frame(width: width, height: width)
.clipped()
}
Comment on lines 147 to 151
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how other attachment views handle aspect ratios
rg -n -A3 'aspectRatio|scaledToFill|scaledToFit' --type swift Sources/StreamChatSwiftUI/ChatMessageList/

Repository: GetStream/stream-chat-swiftui

Length of output: 6731


🏁 Script executed:

cat -n Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift

Repository: GetStream/stream-chat-swiftui

Length of output: 7862


🏁 Script executed:

# Check git history for this file to understand if the square frame was intentional
git log --oneline -10 Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift

Repository: GetStream/stream-chat-swiftui

Length of output: 143


🏁 Script executed:

# Check the diff for the commit that modified this file
git show db055565 -- Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift | head -100

Repository: GetStream/stream-chat-swiftui

Length of output: 3831


🏁 Script executed:

# Check if there was a previous version of GiphyAttachmentView before this merge
git log --oneline --all -- Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift | head -20

Repository: GetStream/stream-chat-swiftui

Length of output: 411


🏁 Script executed:

# Check the previous version before the latest merge
git show 63bdf388:Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift | head -160

Repository: GetStream/stream-chat-swiftui

Length of output: 5355


🏁 Script executed:

# Check the specific changes between the two commits for LazyGiphyView
git diff 63bdf388 db055565 -- Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift | grep -A20 "struct LazyGiphyView"

Repository: GetStream/stream-chat-swiftui

Length of output: 1478


Square aspect ratio forces all GIFs and images into square frame, cropping non-square content.

This change was introduced in the recent fix/message-list-attachment-width merge. The frame now enforces width × width instead of the previous .aspectRatio(contentMode: .fit) with a max height constraint. This will crop or distort any non-square GIFs.

If GIFs should display at their original aspect ratio, revert to using .aspectRatio(contentMode: .fit) with appropriate frame constraints. If the square constraint is intentional for design reasons, add a code comment explaining why.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift` around
lines 148 - 152, The current GiphyAttachmentView forces a square crop by using
.frame(width: width, height: width); revert to preserving original image/GIF
aspect ratio by replacing the fixed square frame with an
aspectRatio(contentMode: .fit) and apply a maximum dimension constraint (e.g.,
.frame(maxWidth: width) or .frame(maxHeight: width)) so images/GIFs scale to fit
without cropping; update the block around the image view where
.processors([ImageProcessors.Resize(width: width)]), .priority(.high) and
.clipped() are called to use aspectRatio(contentMode: .fit) plus a max-dimension
frame, or if the square behavior is intentional, add a comment in
GiphyAttachmentView explaining why square cropping is required.

}

/// Recommended implementation by SwiftyGif for rendering gifs in SwiftUI
/// Nuke dropped gif support and therefore it needs to be implemented separately.
/// The UIImageView is wrapped in a container with auto-layout constraints
/// to ensure contentMode scaling works correctly with SwiftyGif's
/// CADisplayLink-driven frame updates.
private struct AnimatedGifView: UIViewRepresentable {
let imageContainer: ImageContainer

func makeUIView(context: Context) -> UIImageView {
func makeUIView(context: Context) -> UIView {

Check warning on line 162 in Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "context" or name it "_".

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swiftui&issues=AZ1NTjGsI4OxwzX2GkfB&open=AZ1NTjGsI4OxwzX2GkfB&pullRequest=1335
let container = UIView()
container.clipsToBounds = true

let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(imageView)

NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
imageView.topAnchor.constraint(equalTo: container.topAnchor),
imageView.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])

if let gifData = imageContainer.data, let image = try? UIImage(gifData: gifData) {
imageView.setGifImage(image)
}
return imageView

return container
}

func updateUIView(_ uiView: UIImageView, context: Context) {}
func updateUIView(_ uiView: UIView, context: Context) {}

Check warning on line 186 in Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "uiView" or name it "_".

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swiftui&issues=AZ1NTjGsI4OxwzX2GkfD&open=AZ1NTjGsI4OxwzX2GkfD&pullRequest=1335

Check warning on line 186 in Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "context" or name it "_".

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swiftui&issues=AZ1NTjGsI4OxwzX2GkfE&open=AZ1NTjGsI4OxwzX2GkfE&pullRequest=1335

Check failure on line 186 in Sources/StreamChatSwiftUI/ChatMessageList/GiphyAttachmentView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this function is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swiftui&issues=AZ1NTjGsI4OxwzX2GkfC&open=AZ1NTjGsI4OxwzX2GkfC&pullRequest=1335
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public struct LinkAttachmentContainer<Factory: ViewFactory>: View {
isRightAligned: message.isRightAligned,
onImageTap: onImageTap
)
.frame(width: width, alignment: message.isRightAligned ? .trailing : .leading)
.background(MessageAttachmentsBubbleConfiguration.attachmentBackgroundColor(for: message))
.roundWithBorder()
.accessibilityIdentifier("LinkAttachmentContainer")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ public struct MessageAttachmentsView<Factory: ViewFactory>: View {
// Text caption
if !message.text.isEmpty {
factory.makeAttachmentTextView(
options: AttachmentTextViewOptions(message: message)
options: AttachmentTextViewOptions(
message: message,
availableWidth: width
)
)
}
}
Expand Down Expand Up @@ -162,14 +165,19 @@ enum MessageAttachmentsBubbleConfiguration {
}
}

private extension ChatMessage {
extension ChatMessage {
var hasSingleFileOrVoiceAttachmentWithoutCaption: Bool {
guard text.isEmpty else { return false }
guard text.isEmpty, quotedMessage == nil else { return false }
return attachmentCounts.count == 1 && (attachmentCounts[.file] == 1 || attachmentCounts[.voiceRecording] == 1)
}

var hasSingleMediaAttachmentWithoutCaption: Bool {
guard text.isEmpty else { return false }
guard text.isEmpty, quotedMessage == nil else { return false }
return attachmentCounts.count == 1 && (attachmentCounts[.image] == 1 || attachmentCounts[.video] == 1)
}

var hasSingleMediaAttachmentWithCaption: Bool {
guard !text.isEmpty, quotedMessage == nil else { return false }
return attachmentCounts.count == 1 && (attachmentCounts[.image] == 1 || attachmentCounts[.video] == 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,11 @@ public struct MessageItemView<Factory: ViewFactory>: View {
return fixedContentWidth
}
let minimumWidth: CGFloat = 240
let padding = messageListConfig.messagePaddings.horizontal
let avatarWithSpacing = AvatarSize.medium + tokens.spacingXs
let available = (width ?? 0) - spacerWidth - padding - avatarWithSpacing
var padding = messageListConfig.messagePaddings.horizontal
if utils.messageListConfig.messageDisplayOptions.showAvatars(for: channel, incoming: !messageViewModel.isRightAligned) {
padding += AvatarSize.medium + tokens.spacingXs
}
let available = (width ?? 0) - spacerWidth - padding
return max(minimumWidth, available)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import SwiftUI
downloadFileAttachmentsEnabled: Bool = true,
hidesCommandsOverlayOnMessageListTap: Bool = true,
hidesAttachmentsPickersOnMessageListTap: Bool = true,
attachmentPreviewWidth: CGFloat = 256,
navigationBarDisplayMode: NavigationBarItem.TitleDisplayMode = .inline,
supportedMessageActions: @escaping @MainActor (SupportedMessageActionsOptions) -> [MessageAction] = MessageAction.defaultActions(for:)
) {
Expand Down Expand Up @@ -74,6 +75,7 @@ import SwiftUI
self.downloadFileAttachmentsEnabled = downloadFileAttachmentsEnabled
self.hidesCommandsOverlayOnMessageListTap = hidesCommandsOverlayOnMessageListTap
self.hidesAttachmentsPickersOnMessageListTap = hidesAttachmentsPickersOnMessageListTap
self.attachmentPreviewWidth = attachmentPreviewWidth
self.navigationBarDisplayMode = navigationBarDisplayMode
self.supportedMessageActions = supportedMessageActions
}
Expand Down Expand Up @@ -113,6 +115,9 @@ import SwiftUI
/// It is enabled by default.
public let hidesAttachmentsPickersOnMessageListTap: Bool

/// The width used for attachment previews in the message list.
public let attachmentPreviewWidth: CGFloat

/// A boolean to enable the alert actions for bounced messages.
///
/// By default it is true and the bounced actions are displayed as an alert instead of a context menu.
Expand Down Expand Up @@ -280,7 +285,7 @@ public final class MessageDisplayOptions {
public static var defaultSpacerWidth: @MainActor (CGFloat) -> (CGFloat) {
{ availableWidth in
if isIPad && availableWidth > 500 {
return 2 * availableWidth / 3
return (availableWidth * 0.4).rounded()
} else {
@Injected(\.utils) var utils
@Injected(\.tokens) var tokens
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ public enum MediaGalleryOrientation: Sendable {
self = .square
}
}

init(mediaAttachments: [MediaAttachment]) {
if let first = mediaAttachments.first {
self = MediaGalleryOrientation(
width: first.originalWidth,
height: first.originalHeight
)
} else {
self = .landscape
}
}
}

/// A container view that displays media (image and video) attachments in a
Expand Down Expand Up @@ -65,6 +76,8 @@ public struct MessageMediaAttachmentsContainerView<Factory: ViewFactory>: View {
private var spacing: CGFloat { tokens.spacingXxxs }
private var cornerRadius: CGFloat { tokens.messageBubbleRadiusAttachment }
private let maxDisplayedItems = 4
private let orientation: MediaGalleryOrientation
private let sources: [MediaAttachment]

public init(
factory: Factory,
Expand All @@ -76,6 +89,8 @@ public struct MessageMediaAttachmentsContainerView<Factory: ViewFactory>: View {
self.message = message
self.width = width
self.isFirst = isFirst
self.sources = MediaAttachment.galleryOrdered(from: message)
self.orientation = MediaGalleryOrientation(mediaAttachments: sources)
}

public var body: some View {
Expand Down Expand Up @@ -291,36 +306,26 @@ public struct MessageMediaAttachmentsContainerView<Factory: ViewFactory>: View {
)
}

private var orientation: MediaGalleryOrientation {
if let first = sources.first {
return MediaGalleryOrientation(
width: first.originalWidth,
height: first.originalHeight
)
}
return .landscape
}

private var sources: [MediaAttachment] {
MediaAttachment.galleryOrdered(from: message)
}

private func containerSize(for itemCount: Int) -> CGSize {
Self.containerSize(for: itemCount, orientation: orientation, maxItemWidth: width)
}

static func containerSize(
for itemCount: Int,
orientation: MediaGalleryOrientation,
maxItemWidth: CGFloat
) -> CGSize {
guard itemCount > 0 else { return .zero }
let maxItemWidth = width
if itemCount == 1 {
switch orientation {
case .landscape:
// Width-constrained: 256×192 at max width
return CGSize(width: maxItemWidth, height: maxItemWidth * 3.0 / 4.0)
case .portrait:
// Height-constrained: 192×256 at max width
return CGSize(width: maxItemWidth * 3.0 / 4.0, height: maxItemWidth)
case .square:
return CGSize(width: maxItemWidth, height: maxItemWidth)
}
} else {
// Multi-item always uses landscape ratio
return CGSize(width: maxItemWidth, height: maxItemWidth * 3.0 / 4.0)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ public struct MessageView<Factory: ViewFactory>: View {
)
)
} else if let poll = message.poll {
factory.makePollView(options: PollViewOptions(message: message, poll: poll, isFirst: isFirst))
factory.makePollView(
options: PollViewOptions(
message: message,
poll: poll,
isFirst: isFirst,
availableWidth: contentWidth
)
)
} else if messageTypeResolver.hasGiphyAttachment(message: message) {
factory.makeGiphyAttachmentView(
options: GiphyAttachmentViewOptions(
Expand Down
Loading
Loading