Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
42c025c
Add new ChatMediaAttachmentContainerView to manage media layout handling
laevandus Mar 3, 2026
d872297
Merge branch 'v5' into v5-media-attachment-refresh
laevandus Mar 3, 2026
68b164c
Add shared loading spinner
laevandus Mar 3, 2026
88810ec
Implement stacked attachments for different types
laevandus Mar 4, 2026
580e573
Update snapshots
laevandus Mar 4, 2026
3db03d8
Cleanup unused types
laevandus Mar 4, 2026
b78ed39
Fix snapshots
laevandus Mar 4, 2026
bcfc15a
Update image loading in snapshots
laevandus Mar 4, 2026
e53cb1a
Use mocked video loader
laevandus Mar 4, 2026
b7c1d35
Update file and voice views
laevandus Mar 4, 2026
90f756f
Add VideoPlayIndicatorView
laevandus Mar 4, 2026
a1f4338
Merge branch 'v5' into v5-media-attachment-refresh
laevandus Mar 5, 2026
5c5cbc8
Move loading spinner to each of the thumbnail view
laevandus Mar 5, 2026
cd98c27
Fix attachment alignment
laevandus Mar 5, 2026
4712642
Fix e2e tests
laevandus Mar 5, 2026
39ebde7
Unify accessibility identifiers for message view
laevandus Mar 5, 2026
67f80eb
Clean text view factories
laevandus Mar 5, 2026
e7460b7
Merge branch 'v5' into v5-media-attachment-refresh
laevandus Mar 5, 2026
f4ab851
Tidy orientation
laevandus Mar 5, 2026
c58fa00
Tidy text view
laevandus Mar 5, 2026
ba86cb7
Merge branch 'v5' into v5-media-attachment-refresh
laevandus Mar 6, 2026
ad2c95e
Adjust voice recording for the stacked attachment list
laevandus Mar 6, 2026
0e21e24
Revert makeLoadingView changes
laevandus Mar 6, 2026
f3584b8
Tidy bubble styling logic
laevandus Mar 6, 2026
2e76d1f
Update snapshots
laevandus Mar 6, 2026
5a1c10e
Fix foreach
laevandus Mar 6, 2026
0237de7
Fix e2e tests
laevandus Mar 6, 2026
c7845e2
Fix image gallery layout for landscape
laevandus Mar 6, 2026
be8510e
Revert view factory naming
laevandus Mar 6, 2026
c1f07af
[CI] Snapshots (#1271)
Stream-SDK-Bot Mar 6, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public struct ChatChannelListView<Factory: ViewFactory>: View {
private func content() -> some View {
Group {
if viewModel.loading {
viewFactory.makeLoadingView(options: LoadingViewOptions())
viewFactory.makeLoadingView(options: LoadingViewOptions(type: .redactedChannelList))
} else if viewModel.channels.isEmpty {
viewFactory.makeNoChannelsView(options: NoChannelsViewOptions())
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
public struct VoiceRecordingContainerView<Factory: ViewFactory>: View {
@Injected(\.colors) var colors
@Injected(\.images) var images
@Injected(\.tokens) var tokens
@Injected(\.utils) var utils

let factory: Factory
Expand Down Expand Up @@ -39,41 +40,23 @@
}

public var body: some View {
VStack(spacing: 0) {
VStack {
if let quotedMessage = message.quotedMessage {
factory.makeChatQuotedMessageView(
options: ChatQuotedMessageViewOptions(
quotedMessage: quotedMessage,
parentMessage: message,
scrolledId: $scrolledId
)
)
}
VStack(spacing: 2) {
ForEach(message.voiceRecordingAttachments, id: \.self) { attachment in
VoiceRecordingView(
handler: handler,
textColor: textColor(for: message),
addedVoiceRecording: AddedVoiceRecording(
url: attachment.payload.voiceRecordingURL,
duration: attachment.payload.duration ?? 0,
waveform: attachment.payload.waveformData ?? []
),
index: index(for: attachment)
)
.padding(.all, 8)
.background(Color(colors.background8))
.roundWithBorder(cornerRadius: 14)
}
}
}
if !message.text.isEmpty {
AttachmentTextView(factory: factory, message: message)
.frame(maxWidth: .infinity)
VStack(spacing: 2) {
ForEach(message.voiceRecordingAttachments, id: \.self) { attachment in
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to teach AI to not do this :)

Copy link
Contributor

Choose a reason for hiding this comment

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

if these are not identifiable, we can do it in a follow up PR

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 is actually existing code and not AI generated.

VoiceRecordingView(
handler: handler,
textColor: textColor(for: message),
addedVoiceRecording: AddedVoiceRecording(
url: attachment.payload.voiceRecordingURL,
duration: attachment.payload.duration ?? 0,
waveform: attachment.payload.waveformData ?? []
),
index: index(for: attachment)
)
.padding(.all, tokens.spacingXs)
.background(backgroundColor)
.roundWithBorder(cornerRadius: tokens.messageBubbleRadiusAttachment)
}
}
.padding(.all, 2)
.onReceive(handler.$context, perform: { value in
guard message.voiceRecordingAttachments.count > 1 else { return }
if value.state == .playing {
Expand All @@ -95,26 +78,26 @@
.onAppear {
player.subscribe(handler)
}
.modifier(
factory.styles.makeMessageViewModifier(
for: MessageModifierInfo(
message: message,
isFirst: isFirst,
cornerRadius: 16
)
)
)
}

private func index(for attachment: ChatMessageVoiceRecordingAttachment) -> Int {
message.voiceRecordingAttachments.firstIndex(of: attachment) ?? 0
}

private var backgroundColor: Color {
let attachmentCounts = message.attachmentCounts
if message.text.isEmpty, attachmentCounts.count == 1, attachmentCounts[.voiceRecording] == 1 {
return .clear
}
return Color(message.isSentByCurrentUser ? colors.chatBackgroundAttachmentOutgoing : colors.chatBackgroundAttachmentIncoming)
}
}

struct VoiceRecordingView: View {
@Injected(\.utils) var utils
@Injected(\.colors) var colors
@Injected(\.images) var images
@Injected(\.tokens) var tokens

@State var isPlaying: Bool = false
@State var loading: Bool = false
Expand All @@ -139,7 +122,7 @@
}

var body: some View {
HStack {
HStack(spacing: tokens.spacingXs) {
Button(action: {
handlePlayTap()
}, label: {
Expand Down Expand Up @@ -169,7 +152,7 @@
.lineLimit(1)
.foregroundColor(textColor)

HStack {
HStack(spacing: tokens.spacingXs) {
RecordingDurationView(
duration: showContextDuration ? handler.context.currentTime : addedVoiceRecording.duration
)
Expand Down Expand Up @@ -256,7 +239,7 @@
@Published var context: AudioPlaybackContext = .notLoaded

func audioPlayer(
_ audioPlayer: AudioPlaying,

Check warning on line 242 in Sources/StreamChatSwiftUI/ChatMessageList/AsyncVoiceMessages/VoiceRecordingContainerView.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

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

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swiftui&issues=AZy5WsSdgE9ChFi-UZ48&open=AZy5WsSdgE9ChFi-UZ48&pullRequest=1264
didUpdateContext context: AudioPlaybackContext
) {
self.context = context
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// Copyright Β© 2026 Stream.io Inc. All rights reserved.
//

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?

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

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")
}

private var backgroundColor: UIColor {
if let injectedBackgroundColor {
return injectedBackgroundColor
}
return message.isSentByCurrentUser ? colors.chatBackgroundOutgoing : colors.chatBackgroundIncoming
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import SwiftUI

public struct FileAttachmentsContainer<Factory: ViewFactory>: View {
@Injected(\.colors) var colors
@Injected(\.tokens) var tokens
var factory: Factory
var message: ChatMessage
var width: CGFloat
Expand All @@ -27,55 +29,35 @@
}

public var body: some View {
VStack(alignment: message.alignmentInBubble) {
if let quotedMessage = message.quotedMessage {
factory.makeChatQuotedMessageView(
options: ChatQuotedMessageViewOptions(
quotedMessage: quotedMessage,
parentMessage: message,
scrolledId: $scrolledId
)
VStack(spacing: 4) {
ForEach(message.fileAttachments, id: \.self) { attachment in
Copy link
Contributor

Choose a reason for hiding this comment

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

Another one here

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 is also existing code, just moved.

FileAttachmentView(
attachment: attachment,
width: width,
isFirst: isFirst
)
.background(backgroundColor)
.roundWithBorder(cornerRadius: tokens.messageBubbleRadiusAttachment)
}

VStack(spacing: 0) {
VStack(spacing: 4) {
ForEach(message.fileAttachments, id: \.self) { attachment in
FileAttachmentView(
attachment: attachment,
width: width,
isFirst: isFirst
)
}
}
if !message.text.isEmpty {
HStack {
Text(message.adjustedText)
.foregroundColor(textColor(for: message))
.standardPadding()
Spacer()
}
}
}
.padding(.all, 4)
}
.modifier(
factory.styles.makeMessageViewModifier(
for: MessageModifierInfo(
message: message,
isFirst: isFirst
)
)
)
.accessibilityIdentifier("FileAttachmentsContainer")
}

private var backgroundColor: Color {
let attachmentCounts = message.attachmentCounts
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like we're repeating this stuff, maybe we can centralize this logic?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, something similar to MessagePreviewResolver πŸ€”

if message.text.isEmpty, attachmentCounts.count == 1, attachmentCounts[.file] == 1 {
return .clear
}
return Color(message.isSentByCurrentUser ? colors.chatBackgroundAttachmentOutgoing : colors.chatBackgroundAttachmentIncoming)
}
}

public struct FileAttachmentView: View {
@Injected(\.utils) private var utils
@Injected(\.images) private var images
@Injected(\.fonts) private var fonts
@Injected(\.colors) private var colors
@Injected(\.tokens) private var tokens
@Injected(\.chatClient) private var chatClient

@State private var fullScreenShown = false
Expand Down Expand Up @@ -110,10 +92,8 @@
DownloadShareAttachmentView(attachment: attachment)
}
}
.padding(.all, 8)
.background(Color(colors.background))
.padding(.all, tokens.spacingSm)
.frame(width: width)
.roundWithBorder()
.withUploadingStateIndicator(for: attachment.uploadingState, url: attachment.assetURL)
.withDownloadingStateIndicator(for: attachment.downloadingState, url: attachment.assetURL)
.sheet(isPresented: $fullScreenShown) {
Expand All @@ -135,6 +115,7 @@
@Injected(\.images) private var images
@Injected(\.fonts) private var fonts
@Injected(\.colors) private var colors
@Injected(\.tokens) private var tokens

var url: URL
var title: String
Expand All @@ -147,13 +128,13 @@
}

public var body: some View {
HStack {
HStack(spacing: tokens.spacingSm) {
Image(uiImage: previewImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 34, height: 40)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: tokens.spacingXxs) {
Text(title)
.font(fonts.bodyBold)
.lineLimit(1)
Expand Down Expand Up @@ -241,5 +222,5 @@
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}

func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}

Check warning on line 225 in Sources/StreamChatSwiftUI/ChatMessageList/FileAttachmentView.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=AZy5WsVVgE9ChFi-UZ5P&open=AZy5WsVVgE9ChFi-UZ5P&pullRequest=1264
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
scrolledId: $scrolledId
)
)
.padding(tokens.spacingXs)
}

if visibleOnlyToCurrentUser {
Expand Down Expand Up @@ -152,7 +153,7 @@
private struct AnimatedGifView: UIViewRepresentable {
let imageContainer: ImageContainer

func makeUIView(context: Context) -> UIImageView {

Check warning on line 156 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=AZy5WsTZgE9ChFi-UZ5A&open=AZy5WsTZgE9ChFi-UZ5A&pullRequest=1264
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
if let gifData = imageContainer.data, let image = try? UIImage(gifData: gifData) {
Expand All @@ -161,5 +162,5 @@
return imageView
}

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

Check warning on line 165 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=AZy5WsTZgE9ChFi-UZ5C&open=AZy5WsTZgE9ChFi-UZ5C&pullRequest=1264
}
Loading
Loading