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 @@

- StreamChatSwiftUI + StreamChatSwiftUI

## 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/**