diff --git a/.github/actions/bootstrap/action.yml b/.github/actions/bootstrap/action.yml
index 099aca79b..55a20013b 100644
--- a/.github/actions/bootstrap/action.yml
+++ b/.github/actions/bootstrap/action.yml
@@ -5,15 +5,6 @@ runs:
steps:
- run: echo "IMAGE=${ImageOS}" >> $GITHUB_ENV
shell: bash
- - run: echo "$HOME/.mint/bin" >> $GITHUB_PATH
- shell: bash
- - name: Cache Mint
- uses: actions/cache@v4
- id: mint-cache
- with:
- path: ~/.mint
- key: ${{ env.IMAGE }}-mint-${{ hashFiles('**/Mintfile') }}
- restore-keys: ${{ env.IMAGE }}-mint-
- uses: ./.github/actions/ruby-cache
- uses: ./.github/actions/xcode-cache
- run: ./Scripts/bootstrap.sh
diff --git a/.github/actions/xcode-cache/action.yml b/.github/actions/xcode-cache/action.yml
index 1bed4e45e..4674a3ffc 100644
--- a/.github/actions/xcode-cache/action.yml
+++ b/.github/actions/xcode-cache/action.yml
@@ -5,8 +5,6 @@ runs:
steps:
- run: echo "IMAGE=${ImageOS}-${ImageVersion}" >> $GITHUB_ENV
shell: bash
- - run: echo "$HOME/.mint/bin" >> $GITHUB_PATH
- shell: bash
- uses: mikehardy/buildcache-action@v2
with:
cache_key: ${{ env.IMAGE }}-buildcache-
diff --git a/.github/workflows/cron-checks.yml b/.github/workflows/cron-checks.yml
index ffccc8e09..85c8cac45 100644
--- a/.github/workflows/cron-checks.yml
+++ b/.github/workflows/cron-checks.yml
@@ -49,7 +49,7 @@ jobs:
INSTALL_ALLURE: true
INSTALL_YEETD: true
INSTALL_IPSW: true
- SKIP_MINT_BOOTSTRAP: true
+ SKIP_SWIFT_BOOTSTRAP: true
- uses: ./.github/actions/setup-ios-runtime
if: ${{ matrix.setup_runtime }}
timeout-minutes: 60
diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml
index ba2278704..2ac0f5b2b 100644
--- a/.github/workflows/smoke-checks.yml
+++ b/.github/workflows/smoke-checks.yml
@@ -171,7 +171,7 @@ jobs:
env:
INSTALL_ALLURE: true
INSTALL_YEETD: true
- SKIP_MINT_BOOTSTRAP: true
+ SKIP_SWIFT_BOOTSTRAP: true
- name: Run UI Tests (Debug)
run: bundle exec fastlane test_e2e_mock device:"${{ env.IOS_SIMULATOR_DEVICE }}" batch:'${{ matrix.batch }}' test_without_building:true
timeout-minutes: 100
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index 73d5ac204..025910133 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -21,7 +21,7 @@ jobs:
- uses: ./.github/actions/bootstrap
env:
INSTALL_SONAR: true
- SKIP_MINT_BOOTSTRAP: true
+ SKIP_SWIFT_BOOTSTRAP: true
- uses: actions/github-script@v6
id: get_pr_number
diff --git a/AGENTS.md b/AGENTS.md
index 7e74f389b..e4c60e7a2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,7 +12,7 @@ Agents should optimize for API stability, backwards compatibility, accessibility
• Xcode: 15.x or newer (Apple Silicon supported)
• Platforms / deployment targets: Use the values set in Package.swift/podspecs; do not lower targets without approval
• CI: GitHub Actions (assume PR validation for build + tests + lint)
- • Linters & docs: SwiftLint via Mint
+ • Linters & docs: SwiftLint and SwiftFormat
### Project layout (high level)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 086a83ab4..ebbd17f68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 🔄 Changed
+# [4.89.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.89.0)
+_September 22, 2025_
+
+### ✅ Added
+- Add `toolbarThemed(content:)` for creating custom views with themed navigation bar [#953](https://github.com/GetStream/stream-chat-swiftui/pull/953)
+- Add support for downloading file attachments [#952](https://github.com/GetStream/stream-chat-swiftui/pull/952)
+### 🐞 Fixed
+- Fix updating back button tint with `ColorPalette.navigationBarTintColor` [#953](https://github.com/GetStream/stream-chat-swiftui/pull/953)
+- Fix swipe to reply enabled when quoting a message is disabled [#977](https://github.com/GetStream/stream-chat-swiftui/pull/957)
+- Fix composer not showing images in the composer when editing signed attachments [#956](https://github.com/GetStream/stream-chat-swiftui/pull/956)
+- Fix replacing an image while editing a message not showing the new image in the message list [#956](https://github.com/GetStream/stream-chat-swiftui/pull/956)
+- Improve precision when scrolling to the newest message with long text [#958](https://github.com/GetStream/stream-chat-swiftui/pull/958)
+- Fix draft attachments being sent with local file urls to the server [#964](https://github.com/GetStream/stream-chat-swiftui/pull/964)
+- Fix keyboard showing with attachment picker when editing a message [#965](https://github.com/GetStream/stream-chat-swiftui/pull/965)
+- Fix race condition when clearing text in a regular TextField [#955](https://github.com/GetStream/stream-chat-swiftui/pull/955)
+### 🔄 Changed
+- Change the gallery header view to show the message timestamp instead of online status [#962](https://github.com/GetStream/stream-chat-swiftui/pull/962)
+
# [4.88.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.88.0)
_September 10, 2025_
diff --git a/DemoAppSwiftUI/AppDelegate.swift b/DemoAppSwiftUI/AppDelegate.swift
index a015ba5ec..34e5a4233 100644
--- a/DemoAppSwiftUI/AppDelegate.swift
+++ b/DemoAppSwiftUI/AppDelegate.swift
@@ -74,7 +74,8 @@ class AppDelegate: NSObject, UIApplicationDelegate {
skipEditedMessageLabel: { message in
message.extraData["ai_generated"]?.boolValue == true
},
- draftMessagesEnabled: true
+ draftMessagesEnabled: true,
+ downloadFileAttachmentsEnabled: true
),
composerConfig: ComposerConfig(isVoiceRecordingEnabled: true)
)
diff --git a/DemoAppSwiftUI/ChannelHeader/CustomChannelHeader.swift b/DemoAppSwiftUI/ChannelHeader/CustomChannelHeader.swift
index 3886ff4c9..6778246d0 100644
--- a/DemoAppSwiftUI/ChannelHeader/CustomChannelHeader.swift
+++ b/DemoAppSwiftUI/ChannelHeader/CustomChannelHeader.swift
@@ -33,7 +33,7 @@ public struct CustomChannelHeader: ToolbarContent {
.frame(width: 24, height: 24)
.foregroundColor(Color.white)
.padding(.all, 8)
- .background(colors.tintColor)
+ .background(colors.navigationBarTintColor)
.clipShape(Circle())
}
.accessibilityLabel(Text("New Channel"))
@@ -67,7 +67,7 @@ struct CustomChannelModifier: ChannelListHeaderViewModifier {
func body(content: Content) -> some View {
ZStack {
if #available(iOS 26, *) {
- content.toolbar {
+ content.toolbarThemed {
CustomChannelHeader(
title: title,
currentUserController: chatClient.currentUserController(),
@@ -79,7 +79,7 @@ struct CustomChannelModifier: ChannelListHeaderViewModifier {
#endif
}
} else {
- content.toolbar {
+ content.toolbarThemed {
CustomChannelHeader(
title: title,
currentUserController: chatClient.currentUserController(),
diff --git a/DemoAppSwiftUI/ChannelHeader/NewChatView.swift b/DemoAppSwiftUI/ChannelHeader/NewChatView.swift
index 1ee6410c5..c3995ebdb 100644
--- a/DemoAppSwiftUI/ChannelHeader/NewChatView.swift
+++ b/DemoAppSwiftUI/ChannelHeader/NewChatView.swift
@@ -96,7 +96,13 @@ struct NewChatView: View, KeyboardReadable {
Spacer()
}
}
- .navigationTitle("New Chat")
+ .toolbarThemed {
+ ToolbarItem(placement: .principal) {
+ Text("New Chat")
+ .font(fonts.bodyBold)
+ .foregroundColor(Color(colors.navigationBarTitle))
+ }
+ }
.onReceive(keyboardWillChangePublisher) { visible in
keyboardShown = visible
}
diff --git a/DemoAppSwiftUI/CreateGroupView.swift b/DemoAppSwiftUI/CreateGroupView.swift
index fe761f16b..13fb7977a 100644
--- a/DemoAppSwiftUI/CreateGroupView.swift
+++ b/DemoAppSwiftUI/CreateGroupView.swift
@@ -49,7 +49,7 @@ struct CreateGroupView: View, KeyboardReadable {
}
.listStyle(.plain)
}
- .toolbar(content: {
+ .toolbarThemed(content: {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink {
GroupNameView(
diff --git a/Gemfile.lock b/Gemfile.lock
index d00dc8435..622ff78c3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -335,7 +335,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.4.1)
+ rexml (3.4.2)
rouge (3.28.0)
rubocop (1.38.0)
json (~> 2.3)
diff --git a/Githubfile b/Githubfile
index 0b24052d2..79a16c78c 100644
--- a/Githubfile
+++ b/Githubfile
@@ -3,7 +3,9 @@
export ALLURECTL_VERSION='2.16.0'
export XCRESULTS_VERSION='1.19.1'
export YEETD_VERSION='1.0'
-export MINT_VERSION='0.17.5'
export SONAR_VERSION='6.2.1.4610'
export IPSW_VERSION='3.1.592'
export INTERFACE_ANALYZER_VERSION='1.0.7'
+export SWIFT_LINT_VERSION='0.55.1'
+export SWIFT_FORMAT_VERSION='0.47.12'
+export SWIFT_GEN_VERSION='6.5.1'
diff --git a/Mintfile b/Mintfile
deleted file mode 100644
index 9b0592524..000000000
--- a/Mintfile
+++ /dev/null
@@ -1,3 +0,0 @@
-nicklockwood/SwiftFormat@0.47.12
-SwiftGen/SwiftGen@6.5.1
-realm/SwiftLint@0.55.1
diff --git a/Package.swift b/Package.swift
index 6ef6af55c..0bc5d6053 100644
--- a/Package.swift
+++ b/Package.swift
@@ -16,7 +16,7 @@ let package = Package(
)
],
dependencies: [
- .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.88.0")
+ .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.89.0")
],
targets: [
.target(
diff --git a/README.md b/README.md
index a0556bb11..5069d2526 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
## SwiftUI StreamChat SDK
diff --git a/Scripts/bootstrap.sh b/Scripts/bootstrap.sh
index d6b05bfb5..175b32467 100755
--- a/Scripts/bootstrap.sh
+++ b/Scripts/bootstrap.sh
@@ -2,8 +2,7 @@
# shellcheck source=/dev/null
# Usage: ./bootstrap.sh
# This script will:
-# - install Mint and bootstrap its dependencies
-# - install Vale
+# - install SwiftLint, SwiftFormat, SwiftGen
# - link git hooks
# - install allure dependencies if `INSTALL_ALLURE` environment variable is provided
# - install sonar-scanner if `INSTALL_SONAR` environment variable is provided
@@ -27,19 +26,41 @@ if [ "${GITHUB_ACTIONS:-}" != "true" ]; then
bundle exec lefthook install
fi
-if [ "${SKIP_MINT_BOOTSTRAP:-}" != true ]; then
- puts "Bootstrap Mint dependencies"
- git clone https://github.com/yonaskolb/Mint.git fastlane/mint
- root=$(pwd)
- cd fastlane/mint
- swift run mint install "yonaskolb/mint@${MINT_VERSION}"
- cd $root
- rm -rf fastlane/mint
- mint bootstrap --link
+if [ "${SKIP_SWIFT_BOOTSTRAP:-}" != true ]; then
+ puts "Install SwiftLint v${SWIFT_LINT_VERSION}"
+ DOWNLOAD_URL="https://github.com/realm/SwiftLint/releases/download/${SWIFT_LINT_VERSION}/SwiftLint.pkg"
+ DOWNLOAD_PATH="/tmp/SwiftLint-${SWIFT_LINT_VERSION}.pkg"
+ curl -sL "$DOWNLOAD_URL" -o "$DOWNLOAD_PATH"
+ sudo installer -pkg "$DOWNLOAD_PATH" -target /
+ swiftlint version
+
+ puts "Install SwiftFormat v${SWIFT_FORMAT_VERSION}"
+ DOWNLOAD_URL="https://github.com/nicklockwood/SwiftFormat/releases/download/${SWIFT_FORMAT_VERSION}/swiftformat.zip"
+ DOWNLOAD_PATH="/tmp/swiftformat-${SWIFT_FORMAT_VERSION}.zip"
+ BIN_PATH="/usr/local/bin/swiftformat"
+ brew uninstall swiftformat || true
+ curl -sL "$DOWNLOAD_URL" -o "$DOWNLOAD_PATH"
+ unzip -o "$DOWNLOAD_PATH" -d /tmp/swiftformat-${SWIFT_FORMAT_VERSION}
+ sudo mv /tmp/swiftformat-${SWIFT_FORMAT_VERSION}/swiftformat "$BIN_PATH"
+ sudo chmod +x "$BIN_PATH"
+ swiftformat --version
+
+ puts "Install SwiftGen v${SWIFT_GEN_VERSION}"
+ DOWNLOAD_URL="https://github.com/SwiftGen/SwiftGen/releases/download/${SWIFT_GEN_VERSION}/swiftgen-${SWIFT_GEN_VERSION}.zip"
+ DOWNLOAD_PATH="/tmp/swiftgen-${SWIFT_GEN_VERSION}.zip"
+ INSTALL_DIR="/usr/local/lib/swiftgen"
+ BIN_PATH="/usr/local/bin/swiftgen"
+ curl -sL "$DOWNLOAD_URL" -o "$DOWNLOAD_PATH"
+ sudo rm -rf "$INSTALL_DIR"
+ sudo mkdir -p "$INSTALL_DIR"
+ sudo unzip -o "$DOWNLOAD_PATH" -d "$INSTALL_DIR"
+ sudo sudo rm -f "$BIN_PATH"
+ sudo sudo ln -s "$INSTALL_DIR/bin/swiftgen" "$BIN_PATH"
+ swiftgen --version
fi
if [[ ${INSTALL_SONAR-default} == true ]]; then
- puts "Install sonar scanner"
+ puts "Install sonar scanner v${SONAR_VERSION}"
DOWNLOAD_URL="https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_VERSION}-macosx-x64.zip"
curl -sL "${DOWNLOAD_URL}" -o ./fastlane/sonar.zip
cd fastlane
@@ -51,12 +72,12 @@ if [[ ${INSTALL_SONAR-default} == true ]]; then
fi
if [[ ${INSTALL_ALLURE-default} == true ]]; then
- puts "Install allurectl"
+ puts "Install allurectl v${ALLURECTL_VERSION}"
DOWNLOAD_URL="https://github.com/allure-framework/allurectl/releases/download/${ALLURECTL_VERSION}/allurectl_darwin_amd64"
curl -sL "${DOWNLOAD_URL}" -o ./fastlane/allurectl
chmod +x ./fastlane/allurectl
- puts "Install xcresults"
+ puts "Install xcresults v${XCRESULTS_VERSION}"
DOWNLOAD_URL="https://github.com/eroshenkoam/xcresults/releases/download/${XCRESULTS_VERSION}/xcresults"
curl -sL "${DOWNLOAD_URL}" -o ./fastlane/xcresults
chmod +x ./fastlane/xcresults
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift
index 6280258a5..9ef21fd8a 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift
@@ -77,7 +77,6 @@ public struct DefaultChatChannelHeader: ToolbarContent {
)
.offset(x: 4)
}
- .accentColor(colors.navigationBarTintColor)
.accessibilityLabel(Text(L10n.Channel.Header.Info.title))
NavigationLink(isActive: $isActive) {
@@ -111,8 +110,7 @@ public struct DefaultChannelHeaderModifier: ChatChannelHea
public func body(content: Content) -> some View {
if #available(iOS 26, *) {
content
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
DefaultChatChannelHeader(
factory: factory,
channel: channel,
@@ -125,8 +123,7 @@ public struct DefaultChannelHeaderModifier: ChatChannelHea
}
} else {
content
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
DefaultChatChannelHeader(
factory: factory,
channel: channel,
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/MessageThreadHeaderViewModifier.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/MessageThreadHeaderViewModifier.swift
index 009948c46..2dd48c2cb 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/MessageThreadHeaderViewModifier.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/MessageThreadHeaderViewModifier.swift
@@ -31,8 +31,7 @@ public struct DefaultMessageThreadHeader: ToolbarContent {
public struct DefaultMessageThreadHeaderModifier: MessageThreadHeaderViewModifier {
public func body(content: Content) -> some View {
content
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
DefaultMessageThreadHeader()
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoView.swift
index 60aab3902..1b1ec098c 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/ChatChannelInfoView.swift
@@ -132,8 +132,7 @@ public struct ChatChannelInfoView: View, KeyboardReadable
}
}
}
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Group {
if viewModel.showSingleMemberDMView {
@@ -162,7 +161,6 @@ public struct ChatChannelInfoView: View, KeyboardReadable
.background(colors.tintColor)
.clipShape(Circle())
}
- .accentColor(colors.navigationBarTintColor)
}
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift
index 408dcf130..5d61bb841 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift
@@ -12,6 +12,7 @@ public struct FileAttachmentsView: View {
@Injected(\.colors) private var colors
@Injected(\.fonts) private var fonts
@Injected(\.images) private var images
+ @Injected(\.utils) private var utils
public init(channel: ChatChannel) {
_viewModel = StateObject(
@@ -45,11 +46,17 @@ public struct FileAttachmentsView: View {
Button {
viewModel.selectedAttachment = attachment
} label: {
- FileAttachmentDisplayView(
- url: url,
- title: attachment.title ?? url.lastPathComponent,
- sizeString: attachment.file.sizeString
- )
+ HStack {
+ FileAttachmentDisplayView(
+ url: url,
+ title: attachment.title ?? url.lastPathComponent,
+ sizeString: attachment.file.sizeString
+ )
+ Spacer()
+ if utils.messageListConfig.downloadFileAttachmentsEnabled {
+ DownloadShareAttachmentView(attachment: attachment)
+ }
+ }
.onAppear {
viewModel.loadAdditionalAttachments(
after: monthlyDataSource,
@@ -59,6 +66,7 @@ public struct FileAttachmentsView: View {
.padding(.horizontal, 8)
.padding(.vertical)
}
+ .withDownloadingStateIndicator(for: attachment.downloadingState, url: attachment.assetURL)
.sheet(item: $viewModel.selectedAttachment) { item in
FileAttachmentPreview(title: item.title, url: item.assetURL)
}
@@ -70,14 +78,13 @@ public struct FileAttachmentsView: View {
}
}
}
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(L10n.ChatInfo.Files.title)
.font(fonts.bodyBold)
.foregroundColor(Color(colors.navigationBarTitle))
}
}
- .navigationBarBackground()
.navigationBarTitleDisplayMode(.inline)
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsViewModel.swift
index 746a4c3b5..aa50c4fc9 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsViewModel.swift
@@ -89,16 +89,16 @@ class FileAttachmentsViewModel: ObservableObject, ChatMessageSearchControllerDel
loading = true
messageSearchController.search(query: query, completion: { [weak self] _ in
guard let self = self else { return }
- self.updateAttachments()
+ withAnimation {
+ self.updateAttachments()
+ }
self.loading = false
})
}
private func updateAttachments() {
let messages = messageSearchController.messages
- withAnimation {
- self.attachmentsDataSource = self.loadAttachments(from: messages)
- }
+ attachmentsDataSource = loadAttachments(from: messages)
}
private func loadAttachments(from messages: LazyCachedMapCollection) -> [MonthlyFileAttachments] {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
index 4453b60f0..7cf634bf6 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/MediaAttachmentsView.swift
@@ -100,14 +100,13 @@ public struct MediaAttachmentsView: View {
}
}
}
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(L10n.ChatInfo.Media.title)
.font(fonts.bodyBold)
.foregroundColor(Color(colors.navigationBarTitle))
}
}
- .navigationBarBackground()
.navigationBarTitleDisplayMode(.inline)
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift
index 4191190b7..e65c46536 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/PinnedMessagesView.swift
@@ -73,14 +73,13 @@ public struct PinnedMessagesView: View {
)
}
}
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(L10n.ChatInfo.PinnedMessages.title)
.font(fonts.bodyBold)
.foregroundColor(Color(colors.navigationBarTitle))
}
}
- .navigationBarBackground()
.navigationBarTitleDisplayMode(.inline)
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift
index dfdc25dd6..e01c6bb0d 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift
@@ -138,7 +138,6 @@ public struct ChatChannelView: View, KeyboardReadable {
}
.opacity(0) // Fixes showing accessibility button shape
}
- .accentColor(colors.tintColor)
.overlay(
viewModel.reactionsShown ?
factory.makeReactionsOverlayView(
@@ -206,6 +205,7 @@ public struct ChatChannelView: View, KeyboardReadable {
.accessibilityElement(children: .contain)
.accessibilityIdentifier("ChatChannelView")
.modifier(factory.makeBouncedMessageActionsModifier(viewModel: viewModel))
+ .accentColor(colors.tintColor)
}
private var generatingSnapshot: Bool {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift
index b9c8381d9..9a2da41f5 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift
@@ -36,7 +36,8 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
private var readsString = ""
private var canMarkRead = false
private var hasSetInitialCanMarkRead = false
-
+ private var currentUserSentNewMessage = false
+
private let messageListDateOverlay: DateFormatter = DateFormatter.messageListDateOverlay
private lazy var messagesDateFormatter = utils.dateFormatter
@@ -266,10 +267,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
/// The user tapped on the message sent button.
public func messageSentTapped() {
- // only scroll if the message is not being edited
- if editedMessage == nil {
- scrollToLastMessage()
- }
+ currentUserSentNewMessage = true
}
public func jumpToMessage(messageId: String) -> Bool {
@@ -448,6 +446,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
if !showScrollToLatestButton && scrolledId == nil && !loadingNextMessages {
updateScrolledIdToNewestMessage()
+ } else if changes.first?.isInsertion == true && currentUserSentNewMessage {
+ updateScrolledIdToNewestMessage()
+ currentUserSentNewMessage = false
}
}
@@ -823,25 +824,6 @@ extension ChatMessage: Identifiable {
return repliesCountId
}
- var uploadingStatesId: String {
- var states = imageAttachments.compactMap { $0.uploadingState?.state }
- states += giphyAttachments.compactMap { $0.uploadingState?.state }
- states += videoAttachments.compactMap { $0.uploadingState?.state }
- states += fileAttachments.compactMap { $0.uploadingState?.state }
-
- if states.isEmpty {
- if localState == .sendingFailed {
- return "failed"
- } else {
- return localState?.rawValue ?? "empty"
- }
- }
-
- let strings = states.map { "\($0)" }
- let combined = strings.joined(separator: "-")
- return combined
- }
-
var reactionScoresId: String {
var output = ""
if reactionScores.isEmpty {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift
index 7ca509a2b..ed795c398 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerModels.swift
@@ -62,33 +62,7 @@ extension AddedAsset {
}
extension AnyChatMessageAttachment {
- func toAddedAsset() -> AddedAsset? {
- if let imageAttachment = attachment(payloadType: ImageAttachmentPayload.self),
- let imageData = try? Data(contentsOf: imageAttachment.imageURL),
- let image = UIImage(data: imageData) {
- return AddedAsset(
- image: image,
- id: imageAttachment.id.rawValue,
- url: imageAttachment.imageURL,
- type: .image,
- extraData: imageAttachment.extraData ?? [:],
- payload: imageAttachment.payload
- )
- } else if let videoAttachment = attachment(payloadType: VideoAttachmentPayload.self),
- let thumbnail = imageThumbnail(for: videoAttachment.payload) {
- return AddedAsset(
- image: thumbnail,
- id: videoAttachment.id.rawValue,
- url: videoAttachment.videoURL,
- type: .video,
- extraData: videoAttachment.extraData ?? [:],
- payload: videoAttachment.payload
- )
- }
- return nil
- }
-
- private func imageThumbnail(for videoAttachmentPayload: VideoAttachmentPayload) -> UIImage? {
+ func imageThumbnail(for videoAttachmentPayload: VideoAttachmentPayload) -> UIImage? {
if let thumbnailURL = videoAttachmentPayload.thumbnailURL, let data = try? Data(contentsOf: thumbnailURL) {
return UIImage(data: data)
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift
index 3a0099f84..eb598176f 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift
@@ -181,6 +181,10 @@ public struct MessageComposerView: View, KeyboardReadable
withAnimation(.easeInOut(duration: 0.02)) {
viewModel.pickerTypeState = .expanded(.none)
}
+ } else if editedMessageWillShow {
+ // When editing a message, the keyboard will show.
+ // If the attachment picker is open, we should dismiss it.
+ viewModel.pickerTypeState = .expanded(.none)
}
}
keyboardShown = visible
@@ -220,7 +224,9 @@ public struct MessageComposerView: View, KeyboardReadable
viewModel.fillDraftMessage()
})
.onDisappear(perform: {
- viewModel.updateDraftMessage(quotedMessage: quotedMessage)
+ if editedMessage == nil {
+ viewModel.updateDraftMessage(quotedMessage: quotedMessage)
+ }
})
.accessibilityElement(children: .contain)
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift
index 203b0ef74..0a2e089de 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift
@@ -743,7 +743,6 @@ open class MessageComposerViewModel: ObservableObject {
}
private func clearInputData() {
- text = ""
addedAssets = []
addedFileURLs = []
addedVoiceRecordings = []
@@ -936,7 +935,7 @@ struct FileAddedAsset {
// The converter responsible to map attachments to assets and vice versa.
class MessageAttachmentsConverter {
- let queue = DispatchQueue(label: "MessageAttachmentsConverter")
+ @Injected(\.utils) var utils
/// Converts the added assets to payloads.
func assetsToPayloads(_ assets: ComposerAssets) throws -> [AnyAttachmentPayload] {
@@ -971,59 +970,168 @@ class MessageAttachmentsConverter {
return attachments
}
- /// Converts the attachments to assets.
- ///
- /// This operation is asynchronous to make sure loading expensive assets are not done in the main thread.
+ /// Converts the attachments to assets asynchronously.
func attachmentsToAssets(
_ attachments: [AnyChatMessageAttachment],
completion: @escaping (ComposerAssets) -> Void
) {
- queue.async {
- let addedAssets = self.attachmentsToAssets(attachments)
- DispatchQueue.main.async {
- completion(addedAssets)
- }
- }
+ let group = DispatchGroup()
+ attachmentsToAssets(attachments, with: group, completion: completion)
}
- /// Converts the attachments to assets synchronously.
+ /// Converts the attachments to assets asynchronously or synchronously,
+ /// depending if a DispatchGroup is provided or not.
///
- /// This operation is synchronous and should only be used if all attachments are already loaded.
- /// Like for example, for draft messages.
+ /// For the most part, a DispatchGroup should always be used.
+ /// The synchronously version is mostly used for testing at the moment.
func attachmentsToAssets(
- _ attachments: [AnyChatMessageAttachment]
- ) -> ComposerAssets {
+ _ attachments: [AnyChatMessageAttachment],
+ with group: DispatchGroup?,
+ completion: @escaping (ComposerAssets) -> Void
+ ) {
var addedAssets = ComposerAssets()
attachments.forEach { attachment in
+ group?.enter()
+
switch attachment.type {
- case .image, .video:
- guard let addedAsset = attachment.toAddedAsset() else { break }
- addedAssets.mediaAssets.append(addedAsset)
- case .file:
- guard let filePayload = attachment.attachment(payloadType: FileAttachmentPayload.self) else {
- break
+ case .image:
+ imageAttachmentToAddedAsset(attachment) { asset in
+ guard let addedAsset = asset else {
+ group?.leave()
+ return
+ }
+ addedAssets.mediaAssets.append(addedAsset)
+ group?.leave()
}
- let fileAsset = FileAddedAsset(
- url: filePayload.assetURL,
- payload: filePayload.payload
- )
+ case .video:
+ guard let asset = videoAttachmentToAddedAsset(attachment) else { break }
+ addedAssets.mediaAssets.append(asset)
+ group?.leave()
+ case .file:
+ guard let fileAsset = fileAttachmentToAddedAsset(attachment) else { break }
addedAssets.fileAssets.append(fileAsset)
+ group?.leave()
case .voiceRecording:
guard let addedVoiceRecording = attachment.toAddedVoiceRecording() else { break }
addedAssets.voiceAssets.append(addedVoiceRecording)
+ group?.leave()
case .linkPreview, .audio, .giphy, .unknown:
break
default:
- guard let anyAttachmentPayload = [attachment].toAnyAttachmentPayload().first else { break }
- let customAttachment = CustomAttachment(
- id: attachment.id.rawValue,
- content: anyAttachmentPayload
- )
+ guard let customAttachment = customAttachmentToAddedAsset(attachment) else { break }
addedAssets.customAssets.append(customAttachment)
+ group?.leave()
+ }
+ }
+
+ if let group {
+ group.notify(queue: .main) {
+ completion(addedAssets)
+ }
+ } else {
+ completion(addedAssets)
+ }
+ }
+
+ private func fileAttachmentToAddedAsset(
+ _ attachment: AnyChatMessageAttachment
+ ) -> FileAddedAsset? {
+ guard let filePayload = attachment.attachment(payloadType: FileAttachmentPayload.self) else {
+ return nil
+ }
+ if let localUrl = attachment.uploadingState?.localFileURL {
+ return FileAddedAsset(url: localUrl)
+ }
+ return FileAddedAsset(
+ url: filePayload.assetURL,
+ payload: filePayload.payload
+ )
+ }
+
+ private func videoAttachmentToAddedAsset(
+ _ attachment: AnyChatMessageAttachment
+ ) -> AddedAsset? {
+ guard let videoAttachment = attachment.attachment(payloadType: VideoAttachmentPayload.self) else {
+ return nil
+ }
+ guard let thumbnail = attachment.imageThumbnail(for: videoAttachment.payload) else {
+ return nil
+ }
+
+ if let localUrl = attachment.uploadingState?.localFileURL {
+ return AddedAsset(
+ image: thumbnail,
+ id: videoAttachment.id.rawValue,
+ url: localUrl,
+ type: .video,
+ extraData: videoAttachment.extraData ?? [:]
+ )
+ }
+
+ return AddedAsset(
+ image: thumbnail,
+ id: videoAttachment.id.rawValue,
+ url: videoAttachment.videoURL,
+ type: .video,
+ extraData: videoAttachment.extraData ?? [:],
+ payload: videoAttachment.payload
+ )
+ }
+
+ private func imageAttachmentToAddedAsset(
+ _ attachment: AnyChatMessageAttachment,
+ completion: @escaping (AddedAsset?) -> Void
+ ) {
+ guard let imageAttachment = attachment.attachment(payloadType: ImageAttachmentPayload.self) else {
+ return completion(nil)
+ }
+
+ if let localFileUrl = attachment.uploadingState?.localFileURL,
+ let imageData = try? Data(contentsOf: localFileUrl),
+ let image = UIImage(data: imageData) {
+ let imageAsset = AddedAsset(
+ image: image,
+ id: imageAttachment.id.rawValue,
+ url: localFileUrl,
+ type: .image,
+ extraData: imageAttachment.extraData ?? [:]
+ )
+ completion(imageAsset)
+ return
+ }
+
+ utils.imageLoader.loadImage(
+ url: imageAttachment.imageURL,
+ imageCDN: utils.imageCDN,
+ resize: false,
+ preferredSize: nil
+ ) { result in
+ if let image = try? result.get() {
+ let imageAsset = AddedAsset(
+ image: image,
+ id: imageAttachment.id.rawValue,
+ url: imageAttachment.imageURL,
+ type: .image,
+ extraData: imageAttachment.extraData ?? [:],
+ payload: imageAttachment.payload
+ )
+ completion(imageAsset)
+ return
}
+ completion(nil)
}
+ }
- return addedAssets
+ private func customAttachmentToAddedAsset(
+ _ attachment: AnyChatMessageAttachment
+ ) -> CustomAttachment? {
+ guard let anyAttachmentPayload = [attachment].toAnyAttachmentPayload().first else {
+ return nil
+ }
+ return CustomAttachment(
+ id: attachment.id.rawValue,
+ content: anyAttachmentPayload
+ )
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
index 68ac63f82..f31ff68f7 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Gallery/GalleryView.swift
@@ -13,10 +13,12 @@ public struct GalleryView: View {
@Injected(\.colors) private var colors
@Injected(\.fonts) private var fonts
@Injected(\.images) private var images
+ @Injected(\.utils) private var utils
private let viewFactory: Factory
var mediaAttachments: [MediaAttachment]
var author: ChatUser
+ var message: ChatMessage?
@Binding var isShown: Bool
@State private var selected: Int
@State private var loadedImages = [Int: UIImage]()
@@ -27,7 +29,8 @@ public struct GalleryView: View {
imageAttachments: [ChatMessageImageAttachment],
author: ChatUser,
isShown: Binding,
- selected: Int
+ selected: Int,
+ message: ChatMessage? = nil
) {
let mediaAttachments = imageAttachments.map { MediaAttachment(from: $0) }
self.init(
@@ -35,7 +38,8 @@ public struct GalleryView: View {
mediaAttachments: mediaAttachments,
author: author,
isShown: isShown,
- selected: selected
+ selected: selected,
+ message: message
)
}
@@ -44,13 +48,15 @@ public struct GalleryView: View {
mediaAttachments: [MediaAttachment],
author: ChatUser,
isShown: Binding,
- selected: Int
+ selected: Int,
+ message: ChatMessage? = nil
) {
self.viewFactory = viewFactory
self.mediaAttachments = mediaAttachments
self.author = author
_isShown = isShown
_selected = State(initialValue: selected)
+ self.message = message
}
public var body: some View {
@@ -58,7 +64,9 @@ public struct GalleryView: View {
VStack {
viewFactory.makeGalleryHeaderView(
title: author.name ?? "",
- subtitle: author.onlineText,
+ subtitle: message.map {
+ utils.galleryHeaderViewDateFormatter.string(from: $0.createdAt)
+ } ?? author.onlineText,
shown: $isShown
)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AttachmentDownloadingStateView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AttachmentDownloadingStateView.swift
new file mode 100644
index 000000000..0730af0da
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AttachmentDownloadingStateView.swift
@@ -0,0 +1,62 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import StreamChat
+import SwiftUI
+
+/// View used for displaying progress while an attachment is being downloaded.
+struct AttachmentDownloadingStateView: View {
+ @Injected(\.images) private var images
+ @Injected(\.colors) private var colors
+ @Injected(\.fonts) private var fonts
+
+ var downloadState: AttachmentDownloadingState
+ var url: URL
+
+ var body: some View {
+ Group {
+ switch downloadState.state {
+ case let .downloading(progress: progress):
+ BottomRightView {
+ PercentageProgressView(progress: progress)
+ }
+
+ case .downloadingFailed:
+ BottomRightView {
+ Image(uiImage: images.messageListErrorIndicator)
+ .foregroundColor(Color(colors.alert))
+ .background(Color.white)
+ .clipShape(Circle())
+ .offset(x: -4, y: -4)
+ }
+ case .downloaded:
+ EmptyView()
+ }
+ }
+ .id("\(url.absoluteString)-\(downloadState.state))")
+ }
+}
+
+/// View modifier enabling downloading state display.
+struct AttachmentDownloadingStateViewModifier: ViewModifier {
+ var downloadState: AttachmentDownloadingState?
+ var url: URL
+
+ func body(content: Content) -> some View {
+ content
+ .overlay(
+ downloadState != nil ? AttachmentDownloadingStateView(downloadState: downloadState!, url: url) : nil
+ )
+ }
+}
+
+extension View {
+ /// Attaches a downloading state indicator.
+ /// - Parameters:
+ /// - downloadState: the download state of the attachment.
+ /// - url: the url of the attachment.
+ public func withDownloadingStateIndicator(for downloadState: AttachmentDownloadingState?, url: URL) -> some View {
+ modifier(AttachmentDownloadingStateViewModifier(downloadState: downloadState, url: url))
+ }
+}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentUploadingStateView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AttachmentUploadingStateView.swift
similarity index 73%
rename from Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentUploadingStateView.swift
rename to Sources/StreamChatSwiftUI/ChatChannel/MessageList/AttachmentUploadingStateView.swift
index 37fe3a7a7..5dc2311e5 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentUploadingStateView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AttachmentUploadingStateView.swift
@@ -19,21 +19,7 @@ struct AttachmentUploadingStateView: View {
switch uploadState.state {
case let .uploading(progress: progress):
BottomRightView {
- HStack(spacing: 4) {
- ProgressView()
- .progressViewStyle(
- CircularProgressViewStyle(tint: .white)
- )
- .scaleEffect(0.7)
-
- Text(progressDisplay(for: progress))
- .font(fonts.footnote)
- .foregroundColor(Color(colors.staticColorText))
- }
- .padding(.all, 4)
- .background(Color.black.opacity(0.7))
- .cornerRadius(8)
- .padding(.all, 8)
+ PercentageProgressView(progress: progress)
}
case .uploadingFailed:
@@ -58,11 +44,6 @@ struct AttachmentUploadingStateView: View {
}
.id("\(url.absoluteString)-\(uploadState.state))")
}
-
- private func progressDisplay(for progress: CGFloat) -> String {
- let value = Int(progress * 100)
- return "\(value)%"
- }
}
/// View modifier enabling uploading state display.
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift
index 2b3194e3d..b18cd0d47 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentPreview.swift
@@ -32,7 +32,7 @@ public struct FileAttachmentPreview: View {
}
public var body: some View {
- NavigationView {
+ NavigationContainerView(embedInNavigationView: true) {
ZStack {
if error != nil {
Text(L10n.Message.FileAttachment.errorPreview)
@@ -64,8 +64,7 @@ public struct FileAttachmentPreview: View {
}
}
.navigationBarTitleDisplayMode(.inline)
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(navigationTitle)
.font(fonts.bodyBold)
@@ -80,7 +79,6 @@ public struct FileAttachmentPreview: View {
Image(uiImage: images.close)
.renderingMode(.template)
}
- .accentColor(colors.navigationBarTintColor)
}
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift
index 222ce7f9b..db20b8dba 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/FileAttachmentView.swift
@@ -71,9 +71,11 @@ public struct FileAttachmentsContainer: View {
}
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(\.chatClient) private var chatClient
@State private var fullScreenShown = false
@@ -102,17 +104,30 @@ public struct FileAttachmentView: View {
}
Spacer()
+
+ if utils.messageListConfig.downloadFileAttachmentsEnabled {
+ DownloadShareAttachmentView(attachment: attachment)
+ }
}
.padding(.all, 8)
.background(Color(colors.background))
.frame(width: width)
.roundWithBorder()
.withUploadingStateIndicator(for: attachment.uploadingState, url: attachment.assetURL)
+ .withDownloadingStateIndicator(for: attachment.downloadingState, url: attachment.assetURL)
.sheet(isPresented: $fullScreenShown) {
- FileAttachmentPreview(title: attachment.title, url: attachment.assetURL)
+ FileAttachmentPreview(title: attachment.title, url: previewURL)
}
.accessibilityIdentifier("FileAttachmentView")
}
+
+ private var previewURL: URL {
+ if attachment.downloadingState?.state == .downloaded,
+ let localFileURL = attachment.downloadingState?.localFileURL {
+ return localFileURL
+ }
+ return attachment.assetURL
+ }
}
public struct FileAttachmentDisplayView: View {
@@ -157,3 +172,73 @@ public struct FileAttachmentDisplayView: View {
return images.documentPreviews[iconName] ?? images.fileFallback
}
}
+
+struct DownloadShareAttachmentView: View {
+ @Injected(\.colors) var colors
+ @Injected(\.images) var images
+ @Injected(\.chatClient) var chatClient
+
+ @State private var shareSheetShown = false
+
+ var attachment: ChatMessageAttachment
+
+ var body: some View {
+ Group {
+ if shouldShowDownloadButton {
+ downloadButton
+ } else if shouldShowShareButton {
+ shareButton
+ }
+ }
+ .sheet(isPresented: $shareSheetShown) {
+ if let shareURL = attachment.downloadingState?.localFileURL {
+ ShareSheet(activityItems: [shareURL])
+ }
+ }
+ }
+
+ private var shouldShowShareButton: Bool {
+ attachment.downloadingState?.state == .downloaded
+ }
+
+ private var shouldShowDownloadButton: Bool {
+ (attachment.uploadingState == nil || attachment.uploadingState?.state == .uploaded) && attachment.downloadingState == nil
+ }
+
+ private var downloadButton: some View {
+ Button(action: { downloadAttachment() }) {
+ Image(uiImage: images.download)
+ .renderingMode(.template)
+ .foregroundColor(colors.tintColor)
+ .frame(width: 24, height: 24)
+ }
+ .accessibilityLabel("Download")
+ }
+
+ private var shareButton: some View {
+ Button(action: { shareSheetShown = true }) {
+ Image(uiImage: images.share)
+ .renderingMode(.template)
+ .foregroundColor(colors.tintColor)
+ .frame(width: 24, height: 24)
+ }
+ .accessibilityLabel("Share")
+ }
+
+ private func downloadAttachment() {
+ let messageId = attachment.id.messageId
+ let cid = attachment.id.cid
+ let messageController = chatClient.messageController(cid: cid, messageId: messageId)
+ messageController.downloadAttachment(attachment) { _ in }
+ }
+}
+
+struct ShareSheet: UIViewControllerRepresentable {
+ let activityItems: [Any]
+
+ func makeUIViewController(context: Context) -> UIActivityViewController {
+ UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
+ }
+
+ func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
+}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift
index c9a882657..9d0919a84 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift
@@ -71,15 +71,10 @@ public struct ImageAttachmentContainer: View {
}
.accessibilityIdentifier("ImageAttachmentContainer")
}
-
+
private var sources: [MediaAttachment] {
let videoSources = message.videoAttachments.map { attachment in
- let url: URL
- if let state = attachment.uploadingState {
- url = state.localFileURL
- } else {
- url = attachment.videoURL
- }
+ let url: URL = attachment.videoURL
return MediaAttachment(
url: url,
type: .video,
@@ -87,12 +82,7 @@ public struct ImageAttachmentContainer: View {
)
}
let imageSources = message.imageAttachments.map { attachment in
- let url: URL
- if let state = attachment.uploadingState {
- url = state.localFileURL
- } else {
- url = attachment.imageURL
- }
+ let url: URL = attachment.imageURL
return MediaAttachment(
url: url,
type: .image,
@@ -313,6 +303,7 @@ struct SingleImageView: View {
index: index
)
.frame(width: width, height: height)
+ .id(source.id)
.accessibilityIdentifier("SingleImageView")
}
}
@@ -333,6 +324,7 @@ struct MultiImageView: View {
index: index
)
.frame(width: width, height: height)
+ .id(source.id)
.accessibilityIdentifier("MultiImageView")
}
}
@@ -385,7 +377,7 @@ struct LazyLoadingImage: View {
ProgressView()
}
}
-
+
if source.type == .video && width > 64 && source.uploadingState == nil {
VideoPlayIcon()
.accessibilityHidden(true)
@@ -430,17 +422,17 @@ extension ChatMessage {
}
}
-public struct MediaAttachment: Identifiable {
+public struct MediaAttachment: Identifiable, Equatable {
@Injected(\.utils) var utils
-
+
let url: URL
let type: MediaAttachmentType
var uploadingState: AttachmentUploadingState?
-
+
public var id: String {
url.absoluteString
}
-
+
func generateThumbnail(
resize: Bool,
preferredSize: CGSize,
@@ -461,6 +453,12 @@ public struct MediaAttachment: Identifiable {
)
}
}
+
+ public static func == (lhs: MediaAttachment, rhs: MediaAttachment) -> Bool {
+ lhs.url == rhs.url
+ && lhs.type == rhs.type
+ && lhs.uploadingState == rhs.uploadingState
+ }
}
extension MediaAttachment {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift
index 0af97a836..e0dba4bcf 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift
@@ -35,7 +35,8 @@ public struct MessageListConfig {
userBlockingEnabled: Bool = false,
bouncedMessagesAlertActionsEnabled: Bool = true,
skipEditedMessageLabel: @escaping (ChatMessage) -> Bool = { _ in false },
- draftMessagesEnabled: Bool = false
+ draftMessagesEnabled: Bool = false,
+ downloadFileAttachmentsEnabled: Bool = false
) {
self.messageListType = messageListType
self.typingIndicatorPlacement = typingIndicatorPlacement
@@ -64,6 +65,7 @@ public struct MessageListConfig {
self.bouncedMessagesAlertActionsEnabled = bouncedMessagesAlertActionsEnabled
self.skipEditedMessageLabel = skipEditedMessageLabel
self.draftMessagesEnabled = draftMessagesEnabled
+ self.downloadFileAttachmentsEnabled = downloadFileAttachmentsEnabled
}
public let messageListType: MessageListType
@@ -102,6 +104,9 @@ public struct MessageListConfig {
///
/// If enabled, the SDK will save the message content as a draft when the user navigates away from the composer.
public let draftMessagesEnabled: Bool
+
+ /// A boolean value that determines if download action is shown for file attachments.
+ public let downloadFileAttachmentsEnabled: Bool
}
/// Contains information about the message paddings.
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift
index fab17890f..0696eeb7e 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift
@@ -251,13 +251,19 @@ public struct MessageListView: View, KeyboardReadable {
.frame(maxWidth: .infinity)
.clipped()
.onChange(of: scrolledId) { scrolledId in
- if let scrolledId = scrolledId {
- let shouldJump = onJumpToMessage?(scrolledId) ?? false
- if !shouldJump {
- return
- }
- withAnimation {
- scrollView.scrollTo(scrolledId, anchor: messageListConfig.scrollingAnchor)
+ DispatchQueue.main.async {
+ if let scrolledId = scrolledId {
+ let shouldJump = onJumpToMessage?(scrolledId) ?? false
+ if !shouldJump {
+ return
+ }
+ withAnimation {
+ if messages.first?.id == scrolledId {
+ scrollView.scrollTo(scrolledId, anchor: .top)
+ } else {
+ scrollView.scrollTo(scrolledId, anchor: messageListConfig.scrollingAnchor)
+ }
+ }
}
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageViewModel.swift
index d9c22fcd3..b47a826f4 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageViewModel.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageViewModel.swift
@@ -86,7 +86,7 @@ open class MessageViewModel: ObservableObject {
}
open var isSwipeToQuoteReplyPossible: Bool {
- message.isInteractionEnabled && channel?.config.repliesEnabled == true
+ message.isInteractionEnabled && channel?.config.quotesEnabled == true
}
open var textContent: String {
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/PercentageProgressView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/PercentageProgressView.swift
new file mode 100644
index 000000000..8be39f3d3
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/PercentageProgressView.swift
@@ -0,0 +1,37 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import SwiftUI
+
+/// A view used to show the progress of a task a long with the percentage.
+struct PercentageProgressView: View {
+ @Injected(\.images) private var images
+ @Injected(\.colors) private var colors
+ @Injected(\.fonts) private var fonts
+
+ let progress: CGFloat
+
+ var body: some View {
+ HStack(spacing: 4) {
+ ProgressView()
+ .progressViewStyle(
+ CircularProgressViewStyle(tint: .white)
+ )
+ .scaleEffect(0.7)
+
+ Text(progressDisplay(for: progress))
+ .font(fonts.footnote)
+ .foregroundColor(Color(colors.staticColorText))
+ }
+ .padding(.all, 4)
+ .background(Color.black.opacity(0.7))
+ .cornerRadius(8)
+ .padding(.all, 8)
+ }
+
+ private func progressDisplay(for progress: CGFloat) -> String {
+ let value = Int(progress * 100)
+ return "\(value)%"
+ }
+}
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAllOptionsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAllOptionsView.swift
index c3c1d344b..c0f839188 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAllOptionsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAllOptionsView.swift
@@ -43,8 +43,7 @@ struct PollAllOptionsView: View {
}
.padding()
}
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(L10n.Message.Polls.Toolbar.optionsTitle)
.bold()
@@ -57,7 +56,6 @@ struct PollAllOptionsView: View {
} label: {
Image(systemName: "xmark")
}
- .accentColor(colors.navigationBarTintColor)
}
}
.navigationBarTitleDisplayMode(.inline)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift
index 76a245c1c..9c49e5ecd 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollCommentsView.swift
@@ -82,8 +82,7 @@ struct PollCommentsView: View {
isPresented: $viewModel.errorShown,
action: viewModel.refresh
)
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(L10n.Message.Polls.Toolbar.commentsTitle)
.bold()
@@ -96,7 +95,6 @@ struct PollCommentsView: View {
} label: {
Image(systemName: "xmark")
}
- .accentColor(colors.navigationBarTintColor)
}
}
.navigationBarTitleDisplayMode(.inline)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesView.swift
index 8079daa52..1c9fd4b75 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesView.swift
@@ -36,8 +36,7 @@ struct PollOptionAllVotesView: View {
isPresented: $viewModel.errorShown,
action: viewModel.refresh
)
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(viewModel.option.text)
.bold()
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift
index 5ca5c56f7..095026cac 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollResultsView.swift
@@ -52,8 +52,7 @@ struct PollResultsView: View {
}
}
.background(Color(colors.background).ignoresSafeArea())
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(L10n.Message.Polls.Toolbar.resultsTitle)
.bold()
@@ -66,7 +65,6 @@ struct PollResultsView: View {
} label: {
Image(systemName: "xmark")
}
- .accentColor(colors.navigationBarTintColor)
}
}
.navigationBarTitleDisplayMode(.inline)
diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift
index 56eb04778..be7a180eb 100644
--- a/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift
@@ -164,8 +164,7 @@ public struct CreatePollView: View {
.background(Color(colors.background).ignoresSafeArea())
.listStyle(.plain)
.id(listId)
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .cancellationAction) {
Button {
if viewModel.canShowDiscardConfirmation {
@@ -176,7 +175,6 @@ public struct CreatePollView: View {
} label: {
Text(L10n.Alert.Actions.cancel)
}
- .accentColor(colors.navigationBarTintColor)
}
ToolbarItem(placement: .principal) {
@@ -192,9 +190,7 @@ public struct CreatePollView: View {
}
} label: {
Image(systemName: "paperplane.fill")
- .foregroundColor(colors.tintColor)
}
- .accentColor(colors.navigationBarTintColor)
.disabled(!viewModel.canCreatePoll)
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListHeader.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListHeader.swift
index f06a1632c..555de1b1a 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListHeader.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListHeader.swift
@@ -39,9 +39,8 @@ public struct DefaultChannelListHeaderModifier: ChannelListHeaderViewModifier {
}
public func body(content: Content) -> some View {
- content.toolbar {
+ content.toolbarThemed {
DefaultChatChannelListHeader(title: title)
}
- .navigationBarBackground()
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift
index 49b1671cb..1502a4f54 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListView.swift
@@ -79,7 +79,6 @@ public struct ChatChannelListView: View {
content()
}
.overlay(viewModel.customAlertShown ? customViewOverlay() : nil)
- .accentColor(colors.tintColor)
.if(isIphone || !utils.messageListConfig.iPadSplitViewEnabled, transform: { view in
view.navigationViewStyle(.stack)
})
diff --git a/Sources/StreamChatSwiftUI/ChatChannelList/MoreChannelActionsFullScreenWrappingView.swift b/Sources/StreamChatSwiftUI/ChatChannelList/MoreChannelActionsFullScreenWrappingView.swift
index c739ee97d..486936b54 100644
--- a/Sources/StreamChatSwiftUI/ChatChannelList/MoreChannelActionsFullScreenWrappingView.swift
+++ b/Sources/StreamChatSwiftUI/ChatChannelList/MoreChannelActionsFullScreenWrappingView.swift
@@ -17,8 +17,7 @@ struct MoreChannelActionsFullScreenWrappingView: View {
NavigationContainerView(embedInNavigationView: true) {
presentedView
.navigationBarTitleDisplayMode(.inline)
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .navigationBarLeading) {
Button {
onDismiss()
@@ -27,7 +26,6 @@ struct MoreChannelActionsFullScreenWrappingView: View {
.customizable()
.frame(height: 16)
}
- .accentColor(colors.navigationBarTintColor)
}
}
}
diff --git a/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListHeaderViewModifier.swift b/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListHeaderViewModifier.swift
index 1edcd6af6..df98120d7 100644
--- a/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListHeaderViewModifier.swift
+++ b/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListHeaderViewModifier.swift
@@ -13,8 +13,7 @@ struct ChatThreadListHeaderViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.navigationBarTitleDisplayMode(.inline)
- .navigationBarBackground()
- .toolbar {
+ .toolbarThemed {
ToolbarItem(placement: .principal) {
Text(title)
.font(fonts.bodyBold)
diff --git a/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListView.swift b/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListView.swift
index 1fad8d2cb..4f15450b0 100644
--- a/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListView.swift
+++ b/Sources/StreamChatSwiftUI/ChatThreadList/ChatThreadListView.swift
@@ -69,7 +69,6 @@ public struct ChatThreadListView: View {
viewModel.retryLoadThreads()
}
}
- .accentColor(colors.tintColor)
.background(
viewFactory.makeThreadListBackground(colors: colors)
)
diff --git a/Sources/StreamChatSwiftUI/ColorPalette.swift b/Sources/StreamChatSwiftUI/ColorPalette.swift
index ec883090c..38c521f38 100644
--- a/Sources/StreamChatSwiftUI/ColorPalette.swift
+++ b/Sources/StreamChatSwiftUI/ColorPalette.swift
@@ -103,7 +103,28 @@ public struct ColorPalette {
}
public var navigationBarSubtitle: UIColor
+
+ /// Sets a different tint color for the navigation bar.
+ ///
+ /// ## Example
+ ///
+ /// ```swift
+ /// var colors = ColorPalette()
+ /// colors.navigationBarTintColor = .purple
+ /// colors.navigationBarTitle = .brown
+ /// colors.navigationBarSubtitle = .cyan
+ /// colors.navigationBarBackground = .yellow
+ /// colors.tintColor = .red
+ /// let appearance = Appearance(colors: colors)
+ /// streamChat = StreamChat(chatClient: chatClient, appearance: appearance, utils: utils)
+ /// ```
+ ///
+ /// - Important: `tintColor` must also be customised when setting this color.
public var navigationBarTintColor: Color
+
+ /// Sets a custom background color for navigation bars.
+ ///
+ /// - Important: Customized views must use ``toolbarThemed(content:)``.
public var navigationBarBackground: UIColor?
// MARK: - Threads
diff --git a/Sources/StreamChatSwiftUI/CommonViews/NavigationBarBackgroundViewModifier.swift b/Sources/StreamChatSwiftUI/CommonViews/NavigationBarBackgroundViewModifier.swift
deleted file mode 100644
index 4c87cb553..000000000
--- a/Sources/StreamChatSwiftUI/CommonViews/NavigationBarBackgroundViewModifier.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// Copyright © 2025 Stream.io Inc. All rights reserved.
-//
-
-import SwiftUI
-
-extension View {
- /// Sets background color to navigation bar if ``ColorPalette.navigationBarBackground`` is set.
- func navigationBarBackground() -> some View {
- modifier(NavigationBarBackgroundViewModifier())
- }
-}
-
-private struct NavigationBarBackgroundViewModifier: ViewModifier {
- @Injected(\.colors) var colors
-
- func body(content: Content) -> some View {
- if #available(iOS 16.0, *), let background = colors.navigationBarBackground {
- content
- .toolbarBackground(Color(background), for: .navigationBar)
- .toolbarBackground(.visible, for: .navigationBar)
-
- } else {
- content
- }
- }
-}
diff --git a/Sources/StreamChatSwiftUI/CommonViews/NavigationBarThemeViewModifier.swift b/Sources/StreamChatSwiftUI/CommonViews/NavigationBarThemeViewModifier.swift
new file mode 100644
index 000000000..e7d5b10d9
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/CommonViews/NavigationBarThemeViewModifier.swift
@@ -0,0 +1,41 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+import SwiftUI
+
+extension View {
+ nonisolated public func toolbarThemed(@ToolbarContentBuilder content toolbarContent: @escaping () -> Content) -> some View where Content: ToolbarContent {
+ modifier(NavigationBarThemeViewModifier(toolbarContent: toolbarContent))
+ }
+}
+
+private struct NavigationBarThemeViewModifier: ViewModifier {
+ @Injected(\.colors) var colors
+
+ let toolbarContent: () -> T
+
+ func body(content: Content) -> some View {
+ content
+ .accentColor(colors.tintColor)
+ .modifier(NavigationBarBackgroundViewModifier())
+ .toolbar {
+ toolbarContent()
+ }
+ }
+}
+
+private struct NavigationBarBackgroundViewModifier: ViewModifier {
+ @Injected(\.colors) var colors
+
+ func body(content: Content) -> some View {
+ if #available(iOS 16.0, *), let background = colors.navigationBarBackground {
+ content
+ .toolbarBackground(Color(background), for: .navigationBar)
+ .toolbarBackground(.visible, for: .navigationBar)
+
+ } else {
+ content
+ }
+ }
+}
diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift
index 3c4e2b6bd..b238a265d 100644
--- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift
+++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift
@@ -466,7 +466,8 @@ extension ViewFactory {
mediaAttachments: mediaAttachments,
author: message.author,
isShown: isShown,
- selected: options.selectedIndex
+ selected: options.selectedIndex,
+ message: message
)
}
diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift
index 30e96c452..9f9d07f2b 100644
--- a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift
+++ b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift
@@ -7,5 +7,5 @@ import Foundation
enum SystemEnvironment {
/// A Stream Chat version.
- public static let version: String = "4.88.0"
+ public static let version: String = "4.89.0"
}
diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist
index 12e090b0a..27555c3c2 100644
--- a/Sources/StreamChatSwiftUI/Info.plist
+++ b/Sources/StreamChatSwiftUI/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.88.0
+ 4.89.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPhotoLibraryUsageDescription
diff --git a/Sources/StreamChatSwiftUI/Utils.swift b/Sources/StreamChatSwiftUI/Utils.swift
index f39985f10..d29f913ae 100644
--- a/Sources/StreamChatSwiftUI/Utils.swift
+++ b/Sources/StreamChatSwiftUI/Utils.swift
@@ -16,6 +16,7 @@ public class Utils {
///
/// - SeeAlso: ``ChannelListConfig/messageRelativeDateFormatEnabled``.
public var messageRelativeDateFormatter: DateFormatter
+ public var galleryHeaderViewDateFormatter: DateFormatter
public var videoPreviewLoader: VideoPreviewLoader
public var imageLoader: ImageLoading
public var imageCDN: ImageCDN
@@ -77,6 +78,7 @@ public class Utils {
public init(
dateFormatter: DateFormatter = .makeDefault(),
messageRelativeDateFormatter: DateFormatter = MessageRelativeDateFormatter(),
+ galleryHeaderViewDateFormatter: DateFormatter = GalleryHeaderViewDateFormatter(),
videoPreviewLoader: VideoPreviewLoader = DefaultVideoPreviewLoader(),
imageLoader: ImageLoading = NukeImageLoader(),
imageCDN: ImageCDN = StreamImageCDN(),
@@ -104,6 +106,7 @@ public class Utils {
) {
self.dateFormatter = dateFormatter
self.messageRelativeDateFormatter = messageRelativeDateFormatter
+ self.galleryHeaderViewDateFormatter = galleryHeaderViewDateFormatter
self.videoPreviewLoader = videoPreviewLoader
self.imageLoader = imageLoader
self.imageCDN = imageCDN
diff --git a/Sources/StreamChatSwiftUI/Utils/Common/GalleryHeaderViewDateFormatter.swift b/Sources/StreamChatSwiftUI/Utils/Common/GalleryHeaderViewDateFormatter.swift
new file mode 100644
index 000000000..998e365b4
--- /dev/null
+++ b/Sources/StreamChatSwiftUI/Utils/Common/GalleryHeaderViewDateFormatter.swift
@@ -0,0 +1,40 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+/// A formatter used to display the message timestamp in the gallery header view.
+public final class GalleryHeaderViewDateFormatter: DateFormatter, @unchecked Sendable {
+ override public init() {
+ super.init()
+
+ locale = .autoupdatingCurrent
+ dateStyle = .short
+ timeStyle = .none
+ }
+
+ @available(*, unavailable)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override public func string(from date: Date) -> String {
+ if calendar.isDateInToday(date) {
+ return dayFormatter.string(from: date)
+ }
+
+ if calendar.isDateInYesterday(date) {
+ return dayFormatter.string(from: date)
+ }
+
+ return super.string(from: date)
+ }
+
+ let dayFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.locale = .autoupdatingCurrent
+ formatter.dateStyle = .short
+ formatter.timeStyle = .none
+ formatter.doesRelativeDateFormatting = true
+ return formatter
+ }()
+}
diff --git a/Sources/StreamChatSwiftUI/Utils/NavigationContainerView.swift b/Sources/StreamChatSwiftUI/Utils/NavigationContainerView.swift
index 59882a345..6536d5ce6 100644
--- a/Sources/StreamChatSwiftUI/Utils/NavigationContainerView.swift
+++ b/Sources/StreamChatSwiftUI/Utils/NavigationContainerView.swift
@@ -7,7 +7,7 @@ import SwiftUI
/// Reusable container view to handle the navigation container logic.
struct NavigationContainerView: View {
@Injected(\.colors) var colors
- var embedInNavigationView: Bool
+ var embedInNavigationView: Bool = true
var content: () -> Content
var body: some View {
@@ -15,15 +15,13 @@ struct NavigationContainerView: View {
if #available(iOS 16, *), isIphone {
NavigationStack {
content()
- .accentColor(colors.tintColor)
- .navigationBarBackground()
}
+ .accentColor(colors.navigationBarTintColor)
} else {
NavigationView {
content()
- .accentColor(colors.tintColor)
- .navigationBarBackground()
}
+ .accentColor(colors.navigationBarTintColor)
}
} else {
content()
diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec
index 6ff41172e..d50605d33 100644
--- a/StreamChatSwiftUI-XCFramework.podspec
+++ b/StreamChatSwiftUI-XCFramework.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'StreamChatSwiftUI-XCFramework'
- spec.version = '4.88.0'
+ spec.version = '4.89.0'
spec.summary = 'StreamChat SwiftUI Chat Components'
spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.'
@@ -19,7 +19,7 @@ Pod::Spec.new do |spec|
spec.framework = 'Foundation', 'UIKit', 'SwiftUI'
- spec.dependency 'StreamChat-XCFramework', '~> 4.88.0'
+ spec.dependency 'StreamChat-XCFramework', '~> 4.89.0'
spec.cocoapods_version = '>= 1.11.0'
end
diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec
index 3d6040950..2d57281eb 100644
--- a/StreamChatSwiftUI.podspec
+++ b/StreamChatSwiftUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'StreamChatSwiftUI'
- spec.version = '4.88.0'
+ spec.version = '4.89.0'
spec.summary = 'StreamChat SwiftUI Chat Components'
spec.description = 'StreamChatSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamChat SDK.'
@@ -19,5 +19,5 @@ Pod::Spec.new do |spec|
spec.framework = 'Foundation', 'UIKit', 'SwiftUI'
- spec.dependency 'StreamChat', '~> 4.88.0'
+ spec.dependency 'StreamChat', '~> 4.89.0'
end
diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj
index 0dcc874c7..f84a29315 100644
--- a/StreamChatSwiftUI.xcodeproj/project.pbxproj
+++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj
@@ -13,7 +13,7 @@
4F077EF82C85E05700F06D83 /* DelayedRenderingViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F077EF72C85E05700F06D83 /* DelayedRenderingViewModifier.swift */; };
4F0B67C92E7025B6003DA844 /* GalleryHeaderView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0B67C82E7025B6003DA844 /* GalleryHeaderView_Tests.swift */; };
4F198FDD2C0480EC00148F49 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */; };
- 4F3536942E6EFB510046678E /* NavigationBarBackgroundViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3536932E6EFB4B0046678E /* NavigationBarBackgroundViewModifier.swift */; };
+ 4F3536942E6EFB510046678E /* NavigationBarThemeViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3536932E6EFB4B0046678E /* NavigationBarThemeViewModifier.swift */; };
4F65F1862D06EEA7009F69A8 /* ChooseChannelQueryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F65F1852D06EEA5009F69A8 /* ChooseChannelQueryView.swift */; };
4F65F18A2D071798009F69A8 /* ChannelListQueryIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F65F1892D071798009F69A8 /* ChannelListQueryIdentifier.swift */; };
4F6D83352C0F05040098C298 /* PollCommentsViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */; };
@@ -34,7 +34,6 @@
4FD3592A2C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD359292C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift */; };
4FD964622D353D88001B6838 /* FilePickerView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD964612D353D82001B6838 /* FilePickerView_Tests.swift */; };
4FEAB3182BFF71F70057E511 /* SwiftUI+UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEAB3172BFF71F70057E511 /* SwiftUI+UIAlertController.swift */; };
- 4FEDF72B2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEDF72A2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift */; };
8205B4142AD41CC700265B84 /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8205B4132AD41CC700265B84 /* StreamSwiftTestHelpers */; };
8205B4182AD4267200265B84 /* StreamSwiftTestHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8205B4172AD4267200265B84 /* StreamSwiftTestHelpers */; };
820A61A029D6D78E002257FB /* QuotedReply_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 820A619F29D6D78E002257FB /* QuotedReply_Tests.swift */; };
@@ -528,9 +527,14 @@
AD3AB65C2CB730090014D4D7 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65B2CB730090014D4D7 /* Shimmer.swift */; };
AD3AB65E2CB731360014D4D7 /* ChatThreadListLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */; };
AD3AB6602CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */; };
+ AD3DB8342E7C7BD50023D377 /* MessageAttachmentsConverter_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3DB8332E7C7BD50023D377 /* MessageAttachmentsConverter_Tests.swift */; };
+ AD3DB82F2E7C2E190023D377 /* GalleryHeaderViewDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3DB82E2E7C2E190023D377 /* GalleryHeaderViewDateFormatter.swift */; };
AD51D9182DB9543A0068D0B0 /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD51D9172DB9543A0068D0B0 /* MessageViewModel.swift */; };
AD5C0A5F2D6FDD9700E1E500 /* BouncedMessageActionsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5C0A5E2D6FDD8600E1E500 /* BouncedMessageActionsModifier.swift */; };
AD6B7E052D356E8800ADEF39 /* ReactionsUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */; };
+ AD9138AC2E707F9100581EB0 /* AttachmentDownloadingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9138AB2E707F9100581EB0 /* AttachmentDownloadingStateView.swift */; };
+ AD9138AE2E71C81D00581EB0 /* PercentageProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9138AD2E71C81D00581EB0 /* PercentageProgressView.swift */; };
+ AD9138B02E7241D900581EB0 /* FileAttachmentView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9138AF2E7241D900581EB0 /* FileAttachmentView_Tests.swift */; };
ADA77F052E1EC2B700A3641F /* MessageAvatarView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA77F042E1EC2B700A3641F /* MessageAvatarView_Tests.swift */; };
ADE0F55E2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */; };
ADE0F5602CB846EC0053B8B9 /* FloatingBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */; };
@@ -619,7 +623,7 @@
4F077EF72C85E05700F06D83 /* DelayedRenderingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayedRenderingViewModifier.swift; sourceTree = ""; };
4F0B67C82E7025B6003DA844 /* GalleryHeaderView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryHeaderView_Tests.swift; sourceTree = ""; };
4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = ""; };
- 4F3536932E6EFB4B0046678E /* NavigationBarBackgroundViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarBackgroundViewModifier.swift; sourceTree = ""; };
+ 4F3536932E6EFB4B0046678E /* NavigationBarThemeViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarThemeViewModifier.swift; sourceTree = ""; };
4F65F1852D06EEA5009F69A8 /* ChooseChannelQueryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseChannelQueryView.swift; sourceTree = ""; };
4F65F1892D071798009F69A8 /* ChannelListQueryIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelListQueryIdentifier.swift; sourceTree = ""; };
4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollCommentsViewModel_Tests.swift; sourceTree = ""; };
@@ -640,7 +644,6 @@
4FD359292C05EA8F00B1D63B /* CreatePollViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollViewModel_Tests.swift; sourceTree = ""; };
4FD964612D353D82001B6838 /* FilePickerView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePickerView_Tests.swift; sourceTree = ""; };
4FEAB3172BFF71F70057E511 /* SwiftUI+UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+UIAlertController.swift"; sourceTree = ""; };
- 4FEDF72A2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAttachmentPreview_Tests.swift; sourceTree = ""; };
820A619F29D6D78E002257FB /* QuotedReply_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReply_Tests.swift; sourceTree = ""; };
825AADF3283CCDB000237498 /* ThreadPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPage.swift; sourceTree = ""; };
829AB4D128578ACF002DC629 /* StreamTestCase+Tags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StreamTestCase+Tags.swift"; sourceTree = ""; };
@@ -1139,9 +1142,14 @@
AD3AB65B2CB730090014D4D7 /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = ""; };
AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListLoadingView.swift; sourceTree = ""; };
AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListHeaderViewModifier.swift; sourceTree = ""; };
+ AD3DB8332E7C7BD50023D377 /* MessageAttachmentsConverter_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachmentsConverter_Tests.swift; sourceTree = ""; };
+ AD3DB82E2E7C2E190023D377 /* GalleryHeaderViewDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryHeaderViewDateFormatter.swift; sourceTree = ""; };
AD51D9172DB9543A0068D0B0 /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; };
AD5C0A5E2D6FDD8600E1E500 /* BouncedMessageActionsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BouncedMessageActionsModifier.swift; sourceTree = ""; };
AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsUsersViewModel.swift; sourceTree = ""; };
+ AD9138AB2E707F9100581EB0 /* AttachmentDownloadingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDownloadingStateView.swift; sourceTree = ""; };
+ AD9138AD2E71C81D00581EB0 /* PercentageProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentageProgressView.swift; sourceTree = ""; };
+ AD9138AF2E7241D900581EB0 /* FileAttachmentView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAttachmentView_Tests.swift; sourceTree = ""; };
ADA77F042E1EC2B700A3641F /* MessageAvatarView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAvatarView_Tests.swift; sourceTree = ""; };
ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListErrorBannerView.swift; sourceTree = ""; };
ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingBannerViewModifier.swift; sourceTree = ""; };
@@ -1766,7 +1774,7 @@
4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */,
4F077EF72C85E05700F06D83 /* DelayedRenderingViewModifier.swift */,
84AB7B1C2771F4AA00631A10 /* DiscardButtonView.swift */,
- 4F3536932E6EFB4B0046678E /* NavigationBarBackgroundViewModifier.swift */,
+ 4F3536932E6EFB4B0046678E /* NavigationBarThemeViewModifier.swift */,
ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */,
84F2908D276B92A40045472D /* GalleryHeaderView.swift */,
8465FD4B2746A95600AF091E /* LoadingView.swift */,
@@ -1816,6 +1824,9 @@
84A75FBA274EA29B00225CE8 /* GiphyAttachmentView.swift */,
8465FD002746A95600AF091E /* FileAttachmentPreview.swift */,
8465FD012746A95600AF091E /* FileAttachmentView.swift */,
+ 8465FD192746A95600AF091E /* AttachmentUploadingStateView.swift */,
+ AD9138AB2E707F9100581EB0 /* AttachmentDownloadingStateView.swift */,
+ AD9138AD2E71C81D00581EB0 /* PercentageProgressView.swift */,
8465FD0B2746A95600AF091E /* LinkTextView.swift */,
8465FD0C2746A95600AF091E /* LinkAttachmentView.swift */,
8465FD022746A95600AF091E /* DeletedMessageView.swift */,
@@ -1846,7 +1857,6 @@
8465FD172746A95600AF091E /* AddedImageAttachmentsView.swift */,
8465FD112746A95600AF091E /* AttachmentPickerTypeView.swift */,
8465FD1C2746A95600AF091E /* AttachmentPickerView.swift */,
- 8465FD192746A95600AF091E /* AttachmentUploadingStateView.swift */,
844CC60D2811378D0006548D /* ComposerConfig.swift */,
8465FD1A2746A95600AF091E /* ComposerHelperViews.swift */,
841B64C327744DB60016FF3B /* ComposerModels.swift */,
@@ -1944,6 +1954,7 @@
8465FD382746A95600AF091E /* Common */ = {
isa = PBXGroup;
children = (
+ AD3DB82E2E7C2E190023D377 /* GalleryHeaderViewDateFormatter.swift */,
4FCD7DBC2D633F6C000EEB0F /* AttributedString+Extensions.swift */,
8465FD392746A95600AF091E /* AutoLayoutHelpers.swift */,
8465FD452746A95600AF091E /* Cache.swift */,
@@ -2009,7 +2020,6 @@
84B2B5D528196FD100479CEE /* MediaAttachmentsView_Tests.swift */,
84B2B5D72819778D00479CEE /* FileAttachmentsViewModel_Tests.swift */,
84B2B5D9281985DA00479CEE /* FileAttachmentsView_Tests.swift */,
- 4FEDF72A2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift */,
84507C97281AC40F0081DDC2 /* AddUsersViewModel_Tests.swift */,
84507C99281ACCD70081DDC2 /* AddUsersView_Tests.swift */,
);
@@ -2159,6 +2169,8 @@
children = (
846B15F22817E7440017F7A1 /* ChannelInfo */,
8423C340277CB5C70092DCF1 /* Suggestions */,
+ AD3DB8332E7C7BD50023D377 /* MessageAttachmentsConverter_Tests.swift */,
+ AD9138AF2E7241D900581EB0 /* FileAttachmentView_Tests.swift */,
84779C742AEBBACD000A6A68 /* BottomReactionsView_Tests.swift */,
ADA77F042E1EC2B700A3641F /* MessageAvatarView_Tests.swift */,
844D1D672851DE58000CCCB9 /* ChannelControllerFactory_Tests.swift */,
@@ -2605,7 +2617,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which mint >/dev/null && mint which swiftgen; then\n xcrun --sdk macosx mint run swiftgen config run --config Sources/StreamChatSwiftUI/.swiftgen.yml\nelse\n echo \"Warning: Bootstrap not run, please run ./Scripts/bootstrap.sh\"\nfi\n";
+ shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which swiftgen; then\n xcrun --sdk macosx swiftgen config run --config Sources/StreamChatSwiftUI/.swiftgen.yml\nelse\n echo \"Warning: Bootstrap not run, please run ./Scripts/bootstrap.sh\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -2743,6 +2755,7 @@
8465FDA22746A95700AF091E /* ChatChannelViewModel.swift in Sources */,
8465FD982746A95700AF091E /* ReactionsOverlayView.swift in Sources */,
8465FDCD2746A95700AF091E /* Fonts.swift in Sources */,
+ AD9138AE2E71C81D00581EB0 /* PercentageProgressView.swift in Sources */,
82D64C022AD7E5B700C5C79E /* AVDataAsset.swift in Sources */,
8465FD9A2746A95700AF091E /* ReactionsHelperViews.swift in Sources */,
8465FDC02746A95700AF091E /* ChatChannelList.swift in Sources */,
@@ -2771,6 +2784,7 @@
82D64BD62AD7E5B700C5C79E /* Animator.swift in Sources */,
8465FDB52746A95700AF091E /* Cache.swift in Sources */,
84A1CAD12816C6900046595A /* AddUsersViewModel.swift in Sources */,
+ AD3DB82F2E7C2E190023D377 /* GalleryHeaderViewDateFormatter.swift in Sources */,
82D64BDA2AD7E5B700C5C79E /* UIImage.swift in Sources */,
4F198FDD2C0480EC00148F49 /* Publisher+Extensions.swift in Sources */,
84289BEF2807246E00282ABE /* FileAttachmentsViewModel.swift in Sources */,
@@ -2796,6 +2810,7 @@
8465FD822746A95700AF091E /* LinkTextView.swift in Sources */,
82D64C172AD7E5B700C5C79E /* ImageResponse.swift in Sources */,
84B55F6A2798154C00B99B01 /* MessageListConfig.swift in Sources */,
+ AD9138AC2E707F9100581EB0 /* AttachmentDownloadingStateView.swift in Sources */,
8421BCF027A44EAE000F977D /* SearchResultsView.swift in Sources */,
841B64CC2775C6300016FF3B /* CommandsConfig.swift in Sources */,
8465FD6D2746A95700AF091E /* ViewModelsFactory.swift in Sources */,
@@ -2942,7 +2957,7 @@
82D64C0A2AD7E5B700C5C79E /* LinkedList.swift in Sources */,
84E6EC27279B0C930017207B /* ReactionsUsersView.swift in Sources */,
82D64BEC2AD7E5B700C5C79E /* OperationTask.swift in Sources */,
- 4F3536942E6EFB510046678E /* NavigationBarBackgroundViewModifier.swift in Sources */,
+ 4F3536942E6EFB510046678E /* NavigationBarThemeViewModifier.swift in Sources */,
82D64C132AD7E5B700C5C79E /* ImageDecoding.swift in Sources */,
8465FDA92746A95700AF091E /* AutoLayoutHelpers.swift in Sources */,
8465FDC32746A95700AF091E /* ChatChannelListHeader.swift in Sources */,
@@ -3083,11 +3098,12 @@
84E04796284A444E00BAFA17 /* EventBatcherMock.swift in Sources */,
ADE442F22CBDAAC40066CDF7 /* ChatThreadListItemView_Tests.swift in Sources */,
84E57C5B28103822002213C1 /* TestDataModel.xcdatamodeld in Sources */,
+ AD3DB8342E7C7BD50023D377 /* MessageAttachmentsConverter_Tests.swift in Sources */,
84E04792284A444E00BAFA17 /* MockBackgroundTaskScheduler.swift in Sources */,
84C94D1327578BF2007FE2B9 /* XCTestCase+MockJSON.swift in Sources */,
84C94D5E275A3AA9007FE2B9 /* ImageCDN_Tests.swift in Sources */,
84C94C8027567D3F007FE2B9 /* ChatChannelListViewModel_Tests.swift in Sources */,
- 4FEDF72B2E5DB03D00CE2676 /* FileAttachmentPreview_Tests.swift in Sources */,
+ AD9138B02E7241D900581EB0 /* FileAttachmentView_Tests.swift in Sources */,
84B9B20E27998E9200BFAEAE /* ColorExtensions.swift in Sources */,
8423C344277CC5020092DCF1 /* CommandsHandler_Tests.swift in Sources */,
84B2B5D2281965D000479CEE /* MediaAttachmentsViewModel_Tests.swift in Sources */,
@@ -3920,7 +3936,7 @@
repositoryURL = "https://github.com/GetStream/stream-chat-swift.git";
requirement = {
kind = upToNextMajorVersion;
- minimumVersion = 4.88.0;
+ minimumVersion = 4.89.0;
};
};
E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json
index 6853d55e2..18f200fd1 100644
--- a/StreamChatSwiftUIArtifacts.json
+++ b/StreamChatSwiftUIArtifacts.json
@@ -1 +1 @@
-{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip","4.79.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.0/StreamChatSwiftUI.zip","4.79.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.1/StreamChatSwiftUI.zip","4.80.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.80.0/StreamChatSwiftUI.zip","4.81.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.81.0/StreamChatSwiftUI.zip","4.82.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.82.0/StreamChatSwiftUI.zip","4.83.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.83.0/StreamChatSwiftUI.zip","4.84.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.84.0/StreamChatSwiftUI.zip","4.85.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.85.0/StreamChatSwiftUI.zip","4.86.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.86.0/StreamChatSwiftUI.zip","4.87.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.87.0/StreamChatSwiftUI.zip","4.88.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.88.0/StreamChatSwiftUI.zip"}
\ No newline at end of file
+{"4.40.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.40.0/StreamChatSwiftUI.zip","4.41.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.41.0/StreamChatSwiftUI.zip","4.42.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.42.0/StreamChatSwiftUI.zip","4.43.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.43.0/StreamChatSwiftUI.zip","4.44.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.44.0/StreamChatSwiftUI.zip","4.45.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.45.0/StreamChatSwiftUI.zip","4.46.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.46.0/StreamChatSwiftUI.zip","4.47.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.0/StreamChatSwiftUI.zip","4.47.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.47.1/StreamChatSwiftUI.zip","4.48.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.48.0/StreamChatSwiftUI.zip","4.49.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.49.0/StreamChatSwiftUI.zip","4.50.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.0/StreamChatSwiftUI.zip","4.50.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.50.1/StreamChatSwiftUI.zip","4.51.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.51.0/StreamChatSwiftUI.zip","4.52.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.52.0/StreamChatSwiftUI.zip","4.53.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.53.0/StreamChatSwiftUI.zip","4.54.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.54.0/StreamChatSwiftUI.zip","4.55.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.55.0/StreamChatSwiftUI.zip","4.56.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.56.0/StreamChatSwiftUI.zip","4.57.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.57.0/StreamChatSwiftUI.zip","4.58.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.58.0/StreamChatSwiftUI.zip","4.59.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.59.0/StreamChatSwiftUI.zip","4.60.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.60.0/StreamChatSwiftUI.zip","4.61.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.61.0/StreamChatSwiftUI.zip","4.62.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.62.0/StreamChatSwiftUI.zip","4.63.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.63.0/StreamChatSwiftUI.zip","4.64.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.64.0/StreamChatSwiftUI.zip","4.65.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.65.0/StreamChatSwiftUI.zip","4.66.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.66.0/StreamChatSwiftUI.zip","4.67.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.67.0/StreamChatSwiftUI.zip","4.68.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.68.0/StreamChatSwiftUI.zip","4.69.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.69.0/StreamChatSwiftUI.zip","4.70.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.70.0/StreamChatSwiftUI.zip","4.71.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.71.0/StreamChatSwiftUI.zip","4.72.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.72.0/StreamChatSwiftUI.zip","4.73.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.73.0/StreamChatSwiftUI.zip","4.74.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.74.0/StreamChatSwiftUI.zip","4.75.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.75.0/StreamChatSwiftUI.zip","4.76.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.76.0/StreamChatSwiftUI.zip","4.77.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.77.0/StreamChatSwiftUI.zip","4.78.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.78.0/StreamChatSwiftUI.zip","4.79.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.0/StreamChatSwiftUI.zip","4.79.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.79.1/StreamChatSwiftUI.zip","4.80.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.80.0/StreamChatSwiftUI.zip","4.81.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.81.0/StreamChatSwiftUI.zip","4.82.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.82.0/StreamChatSwiftUI.zip","4.83.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.83.0/StreamChatSwiftUI.zip","4.84.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.84.0/StreamChatSwiftUI.zip","4.85.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.85.0/StreamChatSwiftUI.zip","4.86.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.86.0/StreamChatSwiftUI.zip","4.87.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.87.0/StreamChatSwiftUI.zip","4.88.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.88.0/StreamChatSwiftUI.zip","4.89.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.0/StreamChatSwiftUI.zip"}
\ No newline at end of file
diff --git a/StreamChatSwiftUITests/Infrastructure/Mocks/ImageLoader_Mock.swift b/StreamChatSwiftUITests/Infrastructure/Mocks/ImageLoader_Mock.swift
index a0bea90c9..bca9d2678 100644
--- a/StreamChatSwiftUITests/Infrastructure/Mocks/ImageLoader_Mock.swift
+++ b/StreamChatSwiftUITests/Infrastructure/Mocks/ImageLoader_Mock.swift
@@ -38,3 +38,58 @@ class ImageLoader_Mock: ImageLoading {
completion([Self.defaultLoadedImage])
}
}
+
+/// Mock implementation of `ImageLoading` that returns different TestImages based on URL.
+class TestImagesLoader_Mock: ImageLoading {
+ var loadImageCalled = false
+ var loadImagesCalled = false
+
+ func loadImage(
+ url: URL?,
+ imageCDN: ImageCDN,
+ resize: Bool,
+ preferredSize: CGSize?,
+ completion: @escaping ((Result) -> Void)
+ ) {
+ loadImageCalled = true
+
+ let image = imageForURL(url)
+ completion(.success(image))
+ }
+
+ func loadImages(
+ from urls: [URL],
+ placeholders: [UIImage],
+ loadThumbnails: Bool,
+ thumbnailSize: CGSize,
+ imageCDN: ImageCDN,
+ completion: @escaping (([UIImage]) -> Void)
+ ) {
+ loadImagesCalled = true
+
+ let images = urls.map { imageForURL($0) }
+ completion(images)
+ }
+
+ private func imageForURL(_ url: URL?) -> UIImage {
+ guard let url = url else {
+ return XCTestCase.TestImages.yoda.image
+ }
+
+ let urlString = url.absoluteString
+
+ // Return different TestImages based on URL content
+ if urlString.contains("yoda") {
+ return XCTestCase.TestImages.yoda.image
+ } else if urlString.contains("chewbacca") {
+ return XCTestCase.TestImages.chewbacca.image
+ } else if urlString.contains("r2") || urlString.contains("r2-d2") {
+ return XCTestCase.TestImages.r2.image
+ } else if urlString.contains("vader") {
+ return XCTestCase.TestImages.vader.image
+ } else {
+ // Default fallback
+ return XCTestCase.TestImages.yoda.image
+ }
+ }
+}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChannelInfoMockUtils.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChannelInfoMockUtils.swift
index 578474975..8fc866f4d 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChannelInfoMockUtils.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChannelInfo/ChannelInfoMockUtils.swift
@@ -93,6 +93,24 @@ struct ChannelInfoMockUtils {
return LazyCachedMapCollection(source: result) { $0 }
}
+ static func generateMessagesWithPdfAttachments(
+ count: Int
+ ) -> LazyCachedMapCollection {
+ var result = [ChatMessage]()
+ for i in 0.. [ChatUser] {
var result = [ChatUser]()
for i in 0.. [AnyChatMessageAttachment] {
(0.. ChatMessageFileAttachment {
+ ChatMessageFileAttachment(
+ id: .unique,
+ type: .file,
+ payload: FileAttachmentPayload(
+ title: "test.pdf",
+ assetRemoteURL: URL(string: "https://example.com/test.pdf")!,
+ file: AttachmentFile(type: .pdf, size: 1024, mimeType: "application/pdf"),
+ extraData: nil
+ ),
+ downloadingState: downloadingState,
+ uploadingState: uploadingState
+ )
+ }
+}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/GalleryView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/GalleryView_Tests.swift
index 47f3644f1..99b320cfb 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/GalleryView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/GalleryView_Tests.swift
@@ -46,6 +46,56 @@ class GalleryView_Tests: StreamChatTestCase {
assertSnapshot(matching: header, as: .image(perceptualPrecision: precision))
}
+ func test_galleryHeader_withMessageCreatedToday_snapshot() {
+ // Given
+ let imageMessage = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "test message",
+ author: .mock(id: .unique),
+ createdAt: .now,
+ attachments: ChatChannelTestHelpers.imageAttachments
+ )
+
+ // When
+ let view = GalleryView(
+ imageAttachments: imageMessage.imageAttachments,
+ author: imageMessage.author,
+ isShown: .constant(true),
+ selected: 0,
+ message: imageMessage
+ )
+ .applyDefaultSize()
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
+ func test_galleryHeader_withMessageExactDate_snapshot() {
+ // Given
+ let imageMessage = ChatMessage.mock(
+ id: .unique,
+ cid: .unique,
+ text: "test message",
+ author: .mock(id: .unique),
+ createdAt: Date(timeIntervalSince1970: 1_726_662_904),
+ attachments: ChatChannelTestHelpers.imageAttachments
+ )
+
+ // When
+ let view = GalleryView(
+ imageAttachments: imageMessage.imageAttachments,
+ author: imageMessage.author,
+ isShown: .constant(true),
+ selected: 0,
+ message: imageMessage
+ )
+ .applyDefaultSize()
+
+ // Then
+ assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
+ }
+
func test_gridView_snapshotLoading() {
// Given
let view = GridMediaView(
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageAttachmentsConverter_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageAttachmentsConverter_Tests.swift
new file mode 100644
index 000000000..07afc715f
--- /dev/null
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageAttachmentsConverter_Tests.swift
@@ -0,0 +1,419 @@
+//
+// Copyright © 2025 Stream.io Inc. All rights reserved.
+//
+
+@testable import StreamChat
+@testable import StreamChatSwiftUI
+@testable import StreamChatTestTools
+import XCTest
+
+class MessageAttachmentsConverter_Tests: StreamChatTestCase {
+ private var converter: MessageAttachmentsConverter!
+ private var mockImageData: Data!
+ private var mockImageURL: URL!
+ private var mockFileURL: URL!
+ private var mockVideoURL: URL!
+
+ override func setUp() {
+ super.setUp()
+ converter = MessageAttachmentsConverter()
+
+ // Create mock image data
+ mockImageData = createMockImageData()
+
+ // Create temporary URLs for testing
+ mockImageURL = createTemporaryFileURL(extension: "png")
+ mockFileURL = createTemporaryFileURL(extension: "pdf")
+ mockVideoURL = createTemporaryFileURL(extension: "mp4")
+
+ // Write mock data to files
+ try? mockImageData.write(to: mockImageURL)
+ try? "Mock PDF content".data(using: .utf8)?.write(to: mockFileURL)
+ try? "Mock video content".data(using: .utf8)?.write(to: mockVideoURL)
+ }
+
+ override func tearDown() {
+ // Clean up temporary files
+ try? FileManager.default.removeItem(at: mockImageURL)
+ try? FileManager.default.removeItem(at: mockFileURL)
+ try? FileManager.default.removeItem(at: mockVideoURL)
+
+ converter = nil
+ mockImageData = nil
+ mockImageURL = nil
+ mockFileURL = nil
+ mockVideoURL = nil
+
+ super.tearDown()
+ }
+
+ // MARK: - Public Interface Tests
+
+ func test_attachmentsToAssets_emptyAttachments() {
+ // Given
+ let attachments: [AnyChatMessageAttachment] = []
+ let expectation = XCTestExpectation(description: "Empty attachments conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNotNil(result)
+ XCTAssertTrue(result?.fileAssets.isEmpty ?? false)
+ XCTAssertTrue(result?.mediaAssets.isEmpty ?? false)
+ XCTAssertTrue(result?.voiceAssets.isEmpty ?? false)
+ XCTAssertTrue(result?.customAssets.isEmpty ?? false)
+ }
+
+ func test_attachmentsToAssets_mixedLocalAndRemoteAttachments() {
+ // Given
+ let attachments = [
+ createFileAttachmentWithLocalURL(),
+ createFileAttachmentWithoutLocalURL(),
+ createImageAttachmentWithLocalURL(),
+ createImageAttachmentWithoutLocalURL()
+ ]
+ let expectation = XCTestExpectation(description: "Mixed attachments conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 2.0)
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.fileAssets.count, 2)
+ XCTAssertEqual(result?.mediaAssets.count, 2)
+
+ // Verify both local and remote URLs are handled
+ let fileURLs = result?.fileAssets.map(\.url)
+ XCTAssertTrue(fileURLs?.contains(mockFileURL) ?? false)
+ XCTAssertTrue(fileURLs?.contains(URL(string: "https://example.com/file.pdf")!) ?? false)
+
+ // Verify image assets
+ let imageAssets = result?.mediaAssets.filter { $0.type == .image }
+ XCTAssertEqual(imageAssets?.count, 2)
+ }
+
+ func test_attachmentsToAssets_withCorruptedLocalFile() {
+ // Given
+ let corruptedURL = createTemporaryFileURL(extension: "png")
+ try? "corrupted data".data(using: .utf8)?.write(to: corruptedURL)
+ let attachment = createImageAttachmentWithSpecificLocalURL(corruptedURL)
+ let expectation = XCTestExpectation(description: "Corrupted image conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets([attachment]) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ // Should fall back to remote URL loading since local file is corrupted
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.mediaAssets.count, 1)
+ XCTAssertEqual(result?.mediaAssets.first?.type, .image)
+
+ // Clean up
+ try? FileManager.default.removeItem(at: corruptedURL)
+ }
+
+ func test_attachmentsToAssets_fileAttachmentWithLocalURL() {
+ // Given
+ let attachments = [createFileAttachmentWithLocalURL()]
+ let expectation = XCTestExpectation(description: "File attachment conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.fileAssets.count, 1)
+ XCTAssertEqual(result?.fileAssets.first?.url, mockFileURL)
+ XCTAssertNil(result?.fileAssets.first?.payload) // Should not include payload when using local URL
+ }
+
+ func test_attachmentsToAssets_fileAttachmentWithoutLocalURL() {
+ // Given
+ let attachments = [createFileAttachmentWithoutLocalURL()]
+ let expectation = XCTestExpectation(description: "File attachment conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.fileAssets.count, 1)
+ XCTAssertEqual(result?.fileAssets.first?.url, URL(string: "https://example.com/file.pdf"))
+ XCTAssertNotNil(result?.fileAssets.first?.payload)
+ }
+
+ func test_attachmentsToAssets_videoAttachmentWithLocalURL() {
+ // Given
+ let attachments = [createVideoAttachmentWithLocalURL()]
+ let expectation = XCTestExpectation(description: "Video attachment conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.mediaAssets.count, 1)
+ let videoAsset = result?.mediaAssets.first
+ XCTAssertEqual(videoAsset?.url, mockVideoURL)
+ XCTAssertEqual(videoAsset?.type, .video)
+ XCTAssertNotNil(videoAsset?.image) // Should have thumbnail
+ XCTAssertNil(videoAsset?.payload) // Should not include payload when using local URL
+ }
+
+ func test_attachmentsToAssets_videoAttachmentWithoutLocalURL() {
+ // Given
+ let attachments = [createVideoAttachmentWithoutLocalURL()]
+ let expectation = XCTestExpectation(description: "Video attachment conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.mediaAssets.count, 1)
+ let videoAsset = result?.mediaAssets.first
+ XCTAssertEqual(videoAsset?.url, URL(string: "https://example.com/video.mp4"))
+ XCTAssertEqual(videoAsset?.type, .video)
+ XCTAssertNotNil(videoAsset?.image) // Should have thumbnail
+ XCTAssertNotNil(videoAsset?.payload)
+ }
+
+ func test_attachmentsToAssets_imageAttachmentWithLocalURL() {
+ // Given
+ let attachments = [createImageAttachmentWithLocalURL()]
+ let expectation = XCTestExpectation(description: "Image attachment conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.mediaAssets.count, 1)
+ let imageAsset = result?.mediaAssets.first
+ XCTAssertEqual(imageAsset?.url, mockImageURL)
+ XCTAssertEqual(imageAsset?.type, .image)
+ XCTAssertNotNil(imageAsset?.image)
+ XCTAssertNil(imageAsset?.payload) // Should not include payload when using local URL
+ }
+
+ func test_attachmentsToAssets_imageAttachmentWithoutLocalURL() {
+ // Given
+ let attachments = [createImageAttachmentWithoutLocalURL()]
+ let expectation = XCTestExpectation(description: "Image attachment conversion completion")
+ var result: ComposerAssets?
+
+ // When
+ converter.attachmentsToAssets(attachments) { composerAssets in
+ result = composerAssets
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 1.0)
+ XCTAssertNotNil(result)
+ XCTAssertEqual(result?.mediaAssets.count, 1)
+ let imageAsset = result?.mediaAssets.first
+ XCTAssertEqual(imageAsset?.url, URL(string: "https://example.com/image.png"))
+ XCTAssertEqual(imageAsset?.type, .image)
+ XCTAssertNotNil(imageAsset?.image)
+ XCTAssertNotNil(imageAsset?.payload)
+ }
+
+ // MARK: - Helper Methods
+
+ private func createMockImageData() -> Data {
+ // Create a simple 1x1 PNG image data
+ let size = CGSize(width: 1, height: 1)
+ UIGraphicsBeginImageContext(size)
+ let context = UIGraphicsGetCurrentContext()!
+ context.setFillColor(UIColor.red.cgColor)
+ context.fill(CGRect(origin: .zero, size: size))
+ let image = UIGraphicsGetImageFromCurrentImageContext()!
+ UIGraphicsEndImageContext()
+ return image.pngData()!
+ }
+
+ private func createTemporaryFileURL(extension ext: String) -> URL {
+ let tempDir = FileManager.default.temporaryDirectory
+ return tempDir.appendingPathComponent(UUID().uuidString).appendingPathExtension(ext)
+ }
+
+ private func createFileAttachmentWithLocalURL() -> AnyChatMessageAttachment {
+ let attachmentFile = AttachmentFile(type: .pdf, size: 1024, mimeType: "application/pdf")
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: mockFileURL,
+ state: .pendingUpload,
+ file: attachmentFile
+ )
+
+ return ChatMessageFileAttachment(
+ id: .unique,
+ type: .file,
+ payload: FileAttachmentPayload(
+ title: "Test PDF",
+ assetRemoteURL: URL(string: "https://example.com/file.pdf")!,
+ file: attachmentFile,
+ extraData: ["test": "value"]
+ ),
+ downloadingState: nil,
+ uploadingState: uploadingState
+ ).asAnyAttachment
+ }
+
+ private func createFileAttachmentWithoutLocalURL() -> AnyChatMessageAttachment {
+ let attachmentFile = AttachmentFile(type: .pdf, size: 1024, mimeType: "application/pdf")
+
+ return ChatMessageFileAttachment(
+ id: .unique,
+ type: .file,
+ payload: FileAttachmentPayload(
+ title: "Test PDF",
+ assetRemoteURL: URL(string: "https://example.com/file.pdf")!,
+ file: attachmentFile,
+ extraData: ["test": "value"]
+ ),
+ downloadingState: nil,
+ uploadingState: nil
+ ).asAnyAttachment
+ }
+
+ private func createVideoAttachmentWithLocalURL() -> AnyChatMessageAttachment {
+ let attachmentFile = AttachmentFile(type: .mp4, size: 2048, mimeType: "video/mp4")
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: mockVideoURL,
+ state: .pendingUpload,
+ file: attachmentFile
+ )
+
+ return ChatMessageVideoAttachment(
+ id: .unique,
+ type: .video,
+ payload: VideoAttachmentPayload(
+ title: "Test Video",
+ videoRemoteURL: URL(string: "https://example.com/video.mp4")!,
+ thumbnailURL: TestImages.yoda.url,
+ file: attachmentFile,
+ extraData: ["test": "value"]
+ ),
+ downloadingState: nil,
+ uploadingState: uploadingState
+ ).asAnyAttachment
+ }
+
+ private func createVideoAttachmentWithoutLocalURL() -> AnyChatMessageAttachment {
+ let attachmentFile = AttachmentFile(type: .mp4, size: 2048, mimeType: "video/mp4")
+
+ return ChatMessageVideoAttachment(
+ id: .unique,
+ type: .video,
+ payload: VideoAttachmentPayload(
+ title: "Test Video",
+ videoRemoteURL: URL(string: "https://example.com/video.mp4")!,
+ thumbnailURL: TestImages.yoda.url,
+ file: attachmentFile,
+ extraData: ["test": "value"]
+ ),
+ downloadingState: nil,
+ uploadingState: nil
+ ).asAnyAttachment
+ }
+
+ private func createImageAttachmentWithLocalURL() -> AnyChatMessageAttachment {
+ let attachmentFile = AttachmentFile(type: .png, size: 512, mimeType: "image/png")
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: mockImageURL,
+ state: .pendingUpload,
+ file: attachmentFile
+ )
+
+ return ChatMessageImageAttachment(
+ id: .unique,
+ type: .image,
+ payload: ImageAttachmentPayload(
+ title: "Test Image",
+ imageRemoteURL: URL(string: "https://example.com/image.png")!,
+ extraData: ["test": "value"]
+ ),
+ downloadingState: nil,
+ uploadingState: uploadingState
+ ).asAnyAttachment
+ }
+
+ private func createImageAttachmentWithSpecificLocalURL(_ url: URL) -> AnyChatMessageAttachment {
+ let attachmentFile = AttachmentFile(type: .png, size: 512, mimeType: "image/png")
+ let uploadingState = AttachmentUploadingState(
+ localFileURL: url,
+ state: .pendingUpload,
+ file: attachmentFile
+ )
+
+ return ChatMessageImageAttachment(
+ id: .unique,
+ type: .image,
+ payload: ImageAttachmentPayload(
+ title: "Test Image",
+ imageRemoteURL: URL(string: "https://example.com/image.png")!,
+ extraData: ["test": "value"]
+ ),
+ downloadingState: nil,
+ uploadingState: uploadingState
+ ).asAnyAttachment
+ }
+
+ private func createImageAttachmentWithoutLocalURL() -> AnyChatMessageAttachment {
+ let attachmentFile = AttachmentFile(type: .png, size: 512, mimeType: "image/png")
+
+ return ChatMessageImageAttachment(
+ id: .unique,
+ type: .image,
+ payload: ImageAttachmentPayload(
+ title: "Test Image",
+ imageRemoteURL: URL(string: "https://example.com/image.png")!,
+ extraData: ["test": "value"]
+ ),
+ downloadingState: nil,
+ uploadingState: nil
+ ).asAnyAttachment
+ }
+}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift
index e78154c0e..17623396f 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift
@@ -996,8 +996,12 @@ class MessageComposerViewModel_Tests: StreamChatTestCase {
viewModel.sendMessage(quotedMessage: nil, editedMessage: nil) {}
// Then
- // Sending a message will clear the input, deleting the draft message.
- XCTAssertEqual(channelController.deleteDraftMessage_callCount, 1)
+ let expectation = XCTestExpectation(description: "Text cleared")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
+ XCTAssertEqual(channelController.deleteDraftMessage_callCount, 1)
+ expectation.fulfill()
+ }
+ wait(for: [expectation], timeout: 1.0)
}
func test_messageComposerVM_whenMessagePublished_deleteDraftReply() {
@@ -1020,7 +1024,12 @@ class MessageComposerViewModel_Tests: StreamChatTestCase {
viewModel.sendMessage(quotedMessage: nil, editedMessage: nil) {}
// Then
- XCTAssertEqual(messageController.deleteDraftReply_callCount, 1)
+ let expectation = XCTestExpectation(description: "Text cleared")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
+ XCTAssertEqual(messageController.deleteDraftReply_callCount, 1)
+ expectation.fulfill()
+ }
+ wait(for: [expectation], timeout: 1.0)
}
func test_messageComposerVM_draftMessageUpdatedEvent() throws {
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift
index ddf3fe295..905da904c 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift
@@ -14,7 +14,10 @@ import XCTest
class MessageComposerView_Tests: StreamChatTestCase {
override func setUp() {
super.setUp()
+
+ let imageLoader = TestImagesLoader_Mock()
let utils = Utils(
+ imageLoader: imageLoader,
messageListConfig: MessageListConfig(
becomesFirstResponderOnOpen: true,
draftMessagesEnabled: true
@@ -581,7 +584,8 @@ class MessageComposerView_Tests: StreamChatTestCase {
draftMessage: draftMessage
)
let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil)
- viewModel.attachmentsConverter = SyncAttachmentsConverter()
+ viewModel.attachmentsConverter = SynchronousAttachmentsConverter()
+ viewModel.fillDraftMessage()
return MessageComposerView(
viewFactory: factory,
@@ -787,7 +791,7 @@ class MessageComposerView_Tests: StreamChatTestCase {
let factory = DefaultViewFactory.shared
let channelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient)
let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil)
- viewModel.attachmentsConverter = SyncAttachmentsConverter()
+ viewModel.attachmentsConverter = SynchronousAttachmentsConverter()
viewModel.fillEditedMessage(editedMessage)
return MessageComposerView(
@@ -801,12 +805,11 @@ class MessageComposerView_Tests: StreamChatTestCase {
}
}
-class SyncAttachmentsConverter: MessageAttachmentsConverter {
+class SynchronousAttachmentsConverter: MessageAttachmentsConverter {
override func attachmentsToAssets(
_ attachments: [AnyChatMessageAttachment],
completion: @escaping (ComposerAssets) -> Void
) {
- let addedAssets = attachmentsToAssets(attachments)
- completion(addedAssets)
+ super.attachmentsToAssets(attachments, with: nil, completion: completion)
}
}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift
index 37c72534f..9239d3ff2 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageContainerView_Tests.swift
@@ -454,7 +454,7 @@ class MessageContainerView_Tests: StreamChatTestCase {
isSentByCurrentUser: true
)
- let viewModel = MessageViewModel(message: message, channel: .mockDMChannel(config: .mock(repliesEnabled: false)))
+ let viewModel = MessageViewModel(message: message, channel: .mockDMChannel(config: .mock(quotesEnabled: false)))
XCTAssertFalse(viewModel.isSwipeToQuoteReplyPossible)
}
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_customizedNavigationBar_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_customizedNavigationBar_snapshot.1.png
deleted file mode 100644
index 7d491341c..000000000
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_customizedNavigationBar_snapshot.1.png and /dev/null differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_themedNavigationBar_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_themedNavigationBar_snapshot.1.png
new file mode 100644
index 000000000..ad8d4e1b0
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/ChatChannelView_Tests/test_chatChannelView_themedNavigationBar_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-dark.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-dark.png
index bb129ab64..1e993622c 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-dark.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-dark.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-light.png
index 9acc8f502..709b1861e 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-light.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsDisabledSnapshot.default-light.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-dark.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-dark.png
index 10598d72c..c04795177 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-dark.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-dark.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-light.png
index b5d40e983..c52216ec0 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-light.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_allOptionsEnabledSnapshot.default-light.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_customizedNavigationBarSnapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_customizedNavigationBarSnapshot.1.png
deleted file mode 100644
index 3514e6a87..000000000
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_customizedNavigationBarSnapshot.1.png and /dev/null differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-dark.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-dark.png
index 76b95b624..a82b3c6d9 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-dark.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-dark.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-light.png
index 25626d3ca..2b3c8869d 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-light.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_mixedOptionsSnapshot.default-light.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-dark.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-dark.png
index 4f7bdf01a..64d528e37 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-dark.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-dark.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-light.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-light.png
index 93eb8ad2a..8e8313d83 100644
Binary files a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-light.png and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_snapshot.default-light.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_themedNavigationBarSnapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_themedNavigationBarSnapshot.1.png
new file mode 100644
index 000000000..5d3f4c56c
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/CreatePollView_Tests/test_createPollView_themedNavigationBarSnapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadButton.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadButton.1.png
new file mode 100644
index 000000000..6004ec44c
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadButton.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadDisabled.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadDisabled.1.png
new file mode 100644
index 000000000..2cbe0b63c
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadDisabled.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadFailedState.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadFailedState.1.png
new file mode 100644
index 000000000..1643a004e
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadFailedState.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadedState.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadedState.1.png
new file mode 100644
index 000000000..be5bc8ce6
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadedState.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadingState.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadingState.1.png
new file mode 100644
index 000000000..db782641d
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/FileAttachmentView_Tests/test_fileAttachmentView_downloadingState.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/GalleryView_Tests/test_galleryHeader_withMessageCreatedToday_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/GalleryView_Tests/test_galleryHeader_withMessageCreatedToday_snapshot.1.png
new file mode 100644
index 000000000..24a830960
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/GalleryView_Tests/test_galleryHeader_withMessageCreatedToday_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/GalleryView_Tests/test_galleryHeader_withMessageExactDate_snapshot.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/GalleryView_Tests/test_galleryHeader_withMessageExactDate_snapshot.1.png
new file mode 100644
index 000000000..98046ee43
Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/GalleryView_Tests/test_galleryHeader_withMessageExactDate_snapshot.1.png differ
diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListView_Tests.swift
index 7449d5502..37bc225c6 100644
--- a/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListView_Tests.swift
@@ -121,12 +121,12 @@ class ChatChannelListView_Tests: StreamChatTestCase {
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
}
- func test_channelListView_customizedNavigationBar() {
+ func test_channelListView_themedNavigationBar() {
// Given
+ setThemedNavigationBarAppearance()
let controller = makeChannelListController()
// When
- customizedNavigationBarAppearance()
let view = ChatChannelListView(
channelListController: controller
)
diff --git a/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListView_Tests/test_channelListView_customizedNavigationBar.1.png b/StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListView_Tests/test_channelListView_themedNavigationBar.1.png
similarity index 100%
rename from StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListView_Tests/test_channelListView_customizedNavigationBar.1.png
rename to StreamChatSwiftUITests/Tests/ChatChannelList/__Snapshots__/ChatChannelListView_Tests/test_channelListView_themedNavigationBar.1.png
diff --git a/StreamChatSwiftUITests/Tests/CommonViews/GalleryHeaderView_Tests.swift b/StreamChatSwiftUITests/Tests/CommonViews/GalleryHeaderView_Tests.swift
index 0424dfa84..46dbae194 100644
--- a/StreamChatSwiftUITests/Tests/CommonViews/GalleryHeaderView_Tests.swift
+++ b/StreamChatSwiftUITests/Tests/CommonViews/GalleryHeaderView_Tests.swift
@@ -22,7 +22,7 @@ final class GalleryHeaderView_Tests: StreamChatTestCase {
func test_customized_snapshot() {
// When
- customizedNavigationBarAppearance()
+ setThemedNavigationBarAppearance()
let view = GalleryHeaderView(title: "Title", subtitle: "Subtitle", isShown: .constant(false))
.applySize(size)
diff --git a/StreamChatSwiftUITests/Tests/StreamChatTestCase.swift b/StreamChatSwiftUITests/Tests/StreamChatTestCase.swift
index cbc6c92fc..22897c1d8 100644
--- a/StreamChatSwiftUITests/Tests/StreamChatTestCase.swift
+++ b/StreamChatSwiftUITests/Tests/StreamChatTestCase.swift
@@ -35,7 +35,7 @@ open class StreamChatTestCase: XCTestCase {
streamChat.appearance = appearance
}
- func customizedNavigationBarAppearance() {
+ func setThemedNavigationBarAppearance() {
adjustAppearance { appearance in
appearance.colors.navigationBarTintColor = .purple
appearance.colors.navigationBarTitle = .blue
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 5311566b1..a413ff5e8 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -496,11 +496,11 @@ lane :run_swift_format do |options|
Dir.chdir('..') do
strict = options[:strict] ? '--lint' : nil
sources_matrix[:swiftformat].each do |path|
- sh("mint run swiftformat #{strict} --config .swiftformat #{path}")
+ sh("swiftformat #{strict} --config .swiftformat #{path}")
next if path.include?('Tests')
- sh("mint run swiftlint lint --config .swiftlint.yml --fix --progress --quiet --reporter json #{path}") unless strict
- sh("mint run swiftlint lint --config .swiftlint.yml --strict --progress --quiet --reporter json #{path}")
+ sh("swiftlint lint --config .swiftlint.yml --fix --progress --reporter json #{path}") unless strict
+ sh("swiftlint lint --config .swiftlint.yml --strict --progress --reporter json #{path}")
end
end
end
diff --git a/lefthook.yml b/lefthook.yml
index 2463ab07c..1320df25c 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -1,7 +1,7 @@
pre-commit:
parallel: false
jobs:
- - run: mint run swiftformat --config .swiftformat {staged_files}
+ - run: swiftlint lint --config .swiftlint.yml --fix --progress --reporter json {staged_files}
glob: "*.{swift}"
stage_fixed: true
exclude:
@@ -12,7 +12,7 @@ pre-commit:
- merge
- rebase
- - run: mint run swiftlint lint --config .swiftlint.yml --fix --progress --quiet --reporter json {staged_files}
+ - run: swiftformat --config .swiftformat {staged_files}
glob: "*.{swift}"
stage_fixed: true
exclude:
@@ -25,7 +25,7 @@ pre-commit:
pre-push:
jobs:
- - run: mint run swiftlint lint --config .swiftlint.yml --strict --progress --quiet --reporter json {push_files}
+ - run: swiftlint lint --config .swiftlint.yml --strict --progress --reporter json {push_files}
glob: "*.{swift}"
exclude:
- Sources/StreamChatSwiftUI/Generated/**