diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml index a05407b1c..e71245a9a 100644 --- a/.github/workflows/sdk-size-metrics.yml +++ b/.github/workflows/sdk-size-metrics.yml @@ -18,6 +18,7 @@ jobs: runs-on: macos-15 env: GITHUB_TOKEN: '${{ secrets.CI_BOT_GITHUB_TOKEN }}' + GITHUB_PR_NUM: ${{ github.event.pull_request.number }} steps: - name: Connect Bot uses: webfactory/ssh-agent@v0.7.0 @@ -28,10 +29,13 @@ jobs: - uses: ./.github/actions/bootstrap - - name: Run SDK Size Metrics + - name: Run General SDK Size Metrics run: bundle exec fastlane show_frameworks_sizes timeout-minutes: 30 env: - GITHUB_PR_NUM: ${{ github.event.pull_request.number }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} + + - name: Run Detailed SDK Size Metrics + run: bundle exec fastlane size_analyze + timeout-minutes: 30 diff --git a/.gitignore b/.gitignore index 45ae8d6e0..06ee34d3b 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ App Thinning Size Report.txt app-thinning.plist *.dmg yeetd-normal.pkg +*LinkMap.txt # VSCode .vscode diff --git a/.spi.yml b/.spi.yml index c5dc1a7ca..201ffcb2f 100644 --- a/.spi.yml +++ b/.spi.yml @@ -4,4 +4,4 @@ builder: - platform: ios documentation_targets: [StreamChatSwiftUI] scheme: StreamChatSwiftUI - swift_version: 5.9 + swift_version: '6.1' diff --git a/.swiftformat b/.swiftformat index 09b4ef180..d07296f9a 100644 --- a/.swiftformat +++ b/.swiftformat @@ -35,7 +35,7 @@ --rules redundantRawValues --rules redundantVoidReturnType --rules semicolons ---rules sortedImports +--rules sortImports --rules spaceAroundBraces --rules spaceAroundBrackets --rules spaceAroundComments @@ -81,4 +81,4 @@ --wrapcollections before-first # Exclude paths ---exclude Sources/StreamChatSwiftUI/Generated,Sources/StreamChatSwiftUI/StreamSwiftyGif,Sources/StreamChatSwiftUI/StreamNuke +--exclude Sources/StreamChatSwiftUI/Generated,Sources/StreamChatSwiftUI/StreamSwiftyGif,Sources/StreamChatSwiftUI/StreamNuke,vendor/bundle,Pods,spm_cache,derived_data,.build diff --git a/.swiftlint.yml b/.swiftlint.yml index 6a5da8f1b..4020346bd 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,6 +5,7 @@ excluded: - Sources/StreamChatSwiftUI/Generated - Sources/StreamChatSwiftUI/StreamSwiftyGif - Sources/StreamChatSwiftUI/StreamNuke + - vendor/bundle only_rules: - attribute_name_spacing @@ -43,7 +44,6 @@ only_rules: - trailing_comma - trailing_newline - trailing_semicolon - - trailing_whitespace - unneeded_break_in_switch - unneeded_override - unused_closure_parameter @@ -55,8 +55,5 @@ only_rules: multiline_arguments: only_enforce_after_first_closure_on_first_line: true -trailing_whitespace: - ignores_empty_lines: true - file_name_no_space: severity: error diff --git a/CHANGELOG.md b/CHANGELOG.md index 139f0c9d2..5512f5bad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🔄 Changed +# [4.91.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.91.0) +_October 22, 2025_ + +### ✅ Added +- Add the `makeAttachmentTextView` method to ViewFactory [#1013](https://github.com/GetStream/stream-chat-swiftui/pull/1013) +- Allow dismissing commands overlay when tapping the message list [#1024](https://github.com/GetStream/stream-chat-swiftui/pull/1024) +- Allows dismissing the keyboard attachments picker when tapping the message list [#1024](https://github.com/GetStream/stream-chat-swiftui/pull/1024) +### 🐞 Fixed +- Fix composer not being locked after the channel was frozen [#1015](https://github.com/GetStream/stream-chat-swiftui/pull/1015) +- Fix `PollOptionAllVotesView` not updated on poll cast events [#1025](https://github.com/GetStream/stream-chat-swiftui/pull/1025) +- Fix action sheet not showing when discarding Poll creation on iOS 26 [#1027](https://github.com/GetStream/stream-chat-swiftui/pull/1027) + # [4.90.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.90.0) _October 08, 2025_ diff --git a/Gemfile.lock b/Gemfile.lock index 622ff78c3..300196b57 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,9 +208,11 @@ GEM fastlane pry fastlane-plugin-sonarcloud_metric_kit (0.2.1) - fastlane-plugin-stream_actions (0.3.90) + fastlane-plugin-stream_actions (0.3.101) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.7.1) + fastlane-plugin-xcsize (1.1.0) + xcsize (= 1.1.0) fastlane-sirp (1.0.0) sysrandom (~> 1.0) ffi (1.17.2) @@ -283,7 +285,7 @@ GEM molinillo (0.8.0) multi_json (1.17.0) multipart-post (2.4.1) - mustermann (3.0.3) + mustermann (3.0.4) ruby2_keywords (~> 0.0.1) mutex_m (0.3.0) nanaimo (0.4.0) @@ -316,8 +318,8 @@ GEM puma (6.6.1) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.0) - rack-protection (4.1.1) + rack (3.2.3) + rack-protection (4.2.0) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) @@ -372,11 +374,11 @@ GEM simctl (1.6.10) CFPropertyList naturally - sinatra (4.1.1) + sinatra (4.2.0) logger (>= 1.6.0) mustermann (~> 3.0) rack (>= 3.0.0, < 4) - rack-protection (= 4.1.1) + rack-protection (= 4.2.0) rack-session (>= 2.0.0, < 3) tilt (~> 2.0) slather (2.8.5) @@ -413,6 +415,8 @@ GEM rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) + xcsize (1.1.0) + commander (>= 4.6, < 6.0) xctest_list (1.2.1) PLATFORMS @@ -426,8 +430,9 @@ DEPENDENCIES fastlane-plugin-create_xcframework fastlane-plugin-lizard fastlane-plugin-sonarcloud_metric_kit - fastlane-plugin-stream_actions (= 0.3.90) + fastlane-plugin-stream_actions (= 0.3.101) fastlane-plugin-versioning + fastlane-plugin-xcsize (= 1.1.0) json lefthook plist diff --git a/Package.swift b/Package.swift index 0e96f688f..aa0f4fefb 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.90.0") + .package(url: "https://github.com/GetStream/stream-chat-swift.git", from: "4.91.0") ], targets: [ .target( @@ -28,8 +28,6 @@ let package = Package( ] ) -#if swift(>=5.6) package.dependencies.append( .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") ) -#endif diff --git a/README.md b/README.md index de473ce8d..8f3264061 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- StreamChatSwiftUI + StreamChatSwiftUI

## SwiftUI StreamChat SDK diff --git a/Scripts/GenerateSPMFileLists.swift b/Scripts/GenerateSPMFileLists.swift index e49785bc1..2debc37b2 100755 --- a/Scripts/GenerateSPMFileLists.swift +++ b/Scripts/GenerateSPMFileLists.swift @@ -34,7 +34,7 @@ func sourceFileList(at url: URL) -> [String] { let basePathRange = path.range(of: url.path + "/")! return String(path[basePathRange.upperBound...]) } - .filter { $0.hasSuffix("_Tests.swift") || $0.hasSuffix("_Mock.swift") || $0.contains("__Snapshots__")} + .filter { $0.hasSuffix("_Tests.swift") || $0.hasSuffix("_Mock.swift") || $0.contains("__Snapshots__") } return sourceFiles } @@ -59,8 +59,6 @@ newGeneratedContent += "] }\n" newGeneratedContent += "\n" - - // StreamChatUI excluded source files let streamChatUIExcludedFiles = sourceFileList(at: URL(string: "Sources/StreamChatUI")!) newGeneratedContent += "var streamChatUIFilesExcluded: [String] { [\n" diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index e01c6bb0d..47ff42773 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift @@ -73,6 +73,9 @@ public struct ChatChannelView: View, KeyboardReadable { }, onJumpToMessage: viewModel.jumpToMessage(messageId:) ) + .dismissKeyboardOnTap(enabled: true) { + hideComposerCommandsAndAttachmentsPicker() + } .overlay( viewModel.currentDateString != nil ? factory.makeDateIndicatorView(dateString: viewModel.currentDateString!) @@ -81,7 +84,9 @@ public struct ChatChannelView: View, KeyboardReadable { } else { ZStack { factory.makeEmptyMessagesView(for: channel, colors: colors) - .dismissKeyboardOnTap(enabled: keyboardShown) + .dismissKeyboardOnTap(enabled: keyboardShown) { + hideComposerCommandsAndAttachmentsPicker() + } if viewModel.shouldShowTypingIndicator { factory.makeTypingIndicatorBottomView( channel: channel, @@ -183,13 +188,6 @@ public struct ChatChannelView: View, KeyboardReadable { viewModel.reactionsShown = false messageDisplayInfo = nil } - .onChange(of: presentationMode.wrappedValue, perform: { newValue in - if newValue.isPresented == false { - viewModel.onViewDissappear() - } else { - viewModel.setActive() - } - }) .background( Color(colors.background).background( TabBarAccessor { _ in @@ -220,10 +218,13 @@ public struct ChatChannelView: View, KeyboardReadable { let bottomPadding = topVC()?.view.safeAreaInsets.bottom ?? 0 return bottomPadding } -} -extension PresentationMode: Equatable { - public static func == (lhs: PresentationMode, rhs: PresentationMode) -> Bool { - lhs.isPresented == rhs.isPresented + private func hideComposerCommandsAndAttachmentsPicker() { + NotificationCenter.default.post( + name: .attachmentPickerHiddenNotification, object: nil + ) + NotificationCenter.default.post( + name: .commandsOverlayHiddenNotification, object: nil + ) } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift index 9e0c09947..a021e153e 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift @@ -50,7 +50,7 @@ public struct AttachmentPickerTypeView: View { HStack(spacing: 16) { switch pickerTypeState { case let .expanded(attachmentPickerType): - if composerViewModel.channelController.channel?.canUploadFile == true { + if composerViewModel.channelController.channel?.canUploadFile == true && composerViewModel.isSendMessageEnabled { PickerTypeButton( pickerTypeState: $pickerTypeState, pickerType: .media, @@ -60,7 +60,7 @@ public struct AttachmentPickerTypeView: View { .accessibilityIdentifier("PickerTypeButtonMedia") } - if commandsAvailable { + if commandsAvailable && composerViewModel.isSendMessageEnabled { PickerTypeButton( pickerTypeState: $pickerTypeState, pickerType: .instantCommands, @@ -70,16 +70,18 @@ public struct AttachmentPickerTypeView: View { .accessibilityIdentifier("PickerTypeButtonCommands") } case .collapsed: - Button { - withAnimation { - pickerTypeState = .expanded(.none) + if composerViewModel.isSendMessageEnabled { + Button { + withAnimation { + pickerTypeState = .expanded(.none) + } + } label: { + Image(uiImage: images.shrinkInputArrow) + .renderingMode(.template) + .foregroundColor(Color(colors.highlightedAccentBackground)) } - } label: { - Image(uiImage: images.shrinkInputArrow) - .renderingMode(.template) - .foregroundColor(Color(colors.highlightedAccentBackground)) + .accessibilityIdentifier("PickerTypeButtonCollapsed") } - .accessibilityIdentifier("PickerTypeButtonCollapsed") } } .accessibilityElement(children: .contain) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift index eb598176f..c4712a21d 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift @@ -9,6 +9,7 @@ import SwiftUI public struct MessageComposerView: View, KeyboardReadable { @Injected(\.colors) private var colors @Injected(\.fonts) private var fonts + @Injected(\.utils) private var utils // Initial popup size, before the keyboard is shown. @State private var popupSize: CGFloat = 350 @@ -228,6 +229,18 @@ public struct MessageComposerView: View, KeyboardReadable viewModel.updateDraftMessage(quotedMessage: quotedMessage) } }) + .onReceive(NotificationCenter.default.publisher(for: .commandsOverlayHiddenNotification)) { _ in + guard utils.messageListConfig.hidesCommandsOverlayOnMessageListTap else { + return + } + viewModel.composerCommand = nil + } + .onReceive(NotificationCenter.default.publisher(for: .attachmentPickerHiddenNotification)) { _ in + guard utils.messageListConfig.hidesAttachmentsPickersOnMessageListTap else { + return + } + viewModel.pickerTypeState = .expanded(.none) + } .accessibilityElement(children: .contain) } } @@ -375,8 +388,8 @@ public struct ComposerInputView: View, KeyboardReadable { text: $text, height: $textHeight, selectedRangeLocation: $selectedRangeLocation, - placeholder: isInCooldown ? L10n.Composer.Placeholder.slowMode : L10n.Composer.Placeholder.message, - editable: !isInCooldown, + placeholder: isInCooldown ? L10n.Composer.Placeholder.slowMode : (isChannelFrozen ? L10n.Composer.Placeholder.messageDisabled : L10n.Composer.Placeholder.message), + editable: !isInputDisabled, maxMessageLength: maxMessageLength, currentHeight: textFieldHeight ) @@ -435,4 +448,22 @@ public struct ComposerInputView: View, KeyboardReadable { private var isInCooldown: Bool { cooldownDuration > 0 } + + private var isChannelFrozen: Bool { + !viewModel.isSendMessageEnabled + } + + private var isInputDisabled: Bool { + isInCooldown || isChannelFrozen + } +} + +// MARK: - Notification Names + +extension Notification.Name { + /// Notification sent when the attachments picker should be hidden. + static let attachmentPickerHiddenNotification = Notification.Name("attachmentPickerHiddenNotification") + + /// Notification sent when the commands overlay should be hidden. + static let commandsOverlayHiddenNotification = Notification.Name("commandsOverlayHiddenNotification") } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift index 8c2afd6c2..48e1b1058 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift @@ -444,6 +444,11 @@ open class MessageComposerViewModel: ObservableObject { } } + /// A Boolean value indicating whether sending message is enabled. + public var isSendMessageEnabled: Bool { + channelController.channel?.canSendMessage ?? true + } + public var sendButtonEnabled: Bool { if let composerCommand = composerCommand, let handler = commandsHandler.commandHandler(for: composerCommand) { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/TrailingComposerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/TrailingComposerView.swift index 817f31710..7bd1621fc 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/TrailingComposerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/TrailingComposerView.swift @@ -17,7 +17,7 @@ public struct TrailingComposerView: View { public var body: some View { Group { - if viewModel.cooldownDuration == 0 { + if viewModel.cooldownDuration == 0 && viewModel.isSendMessageEnabled { HStack(spacing: 16) { SendMessageButton( enabled: viewModel.sendButtonEnabled, @@ -28,7 +28,7 @@ public struct TrailingComposerView: View { } } .padding(.bottom, 8) - } else { + } else if viewModel.cooldownDuration > 0 { SlowModeView( cooldownDuration: viewModel.cooldownDuration ) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AsyncVoiceMessages/VoiceRecordingContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AsyncVoiceMessages/VoiceRecordingContainerView.swift index 5c4854acf..032941ed8 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AsyncVoiceMessages/VoiceRecordingContainerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AsyncVoiceMessages/VoiceRecordingContainerView.swift @@ -67,7 +67,7 @@ public struct VoiceRecordingContainerView: View { } } if !message.text.isEmpty { - AttachmentTextView(message: message) + AttachmentTextView(factory: factory, message: message) .frame(maxWidth: .infinity) } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift index 7a0aa7812..3ebeb57e5 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/ImageAttachmentView.swift @@ -47,7 +47,7 @@ public struct ImageAttachmentContainer: View { } if !message.text.isEmpty { - AttachmentTextView(message: message) + AttachmentTextView(factory: factory, message: message) .frame(width: width) } } @@ -93,21 +93,23 @@ public struct ImageAttachmentContainer: View { } } -public struct AttachmentTextView: View { +public struct AttachmentTextView: View { @Injected(\.colors) private var colors @Injected(\.fonts) private var fonts + var factory: Factory var message: ChatMessage let injectedBackgroundColor: UIColor? - public init(message: ChatMessage, injectedBackgroundColor: UIColor? = nil) { + public init(factory: Factory = DefaultViewFactory.shared, message: ChatMessage, injectedBackgroundColor: UIColor? = nil) { + self.factory = factory self.message = message self.injectedBackgroundColor = injectedBackgroundColor } public var body: some View { HStack { - StreamTextView(message: message) + factory.makeAttachmentTextView(options: .init(mesage: message)) .standardPadding() .fixedSize(horizontal: false, vertical: true) Spacer() diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/LinkAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/LinkAttachmentView.swift index 47208901e..2e1dab237 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/LinkAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/LinkAttachmentView.swift @@ -51,7 +51,7 @@ public struct LinkAttachmentContainer: View { if #available(iOS 15, *) { HStack { - StreamTextView(message: message) + factory.makeAttachmentTextView(options: .init(mesage: message)) .standardPadding() Spacer() } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift index e0dba4bcf..b03d85500 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift @@ -36,7 +36,9 @@ public struct MessageListConfig { bouncedMessagesAlertActionsEnabled: Bool = true, skipEditedMessageLabel: @escaping (ChatMessage) -> Bool = { _ in false }, draftMessagesEnabled: Bool = false, - downloadFileAttachmentsEnabled: Bool = false + downloadFileAttachmentsEnabled: Bool = false, + hidesCommandsOverlayOnMessageListTap: Bool = true, + hidesAttachmentsPickersOnMessageListTap: Bool = true ) { self.messageListType = messageListType self.typingIndicatorPlacement = typingIndicatorPlacement @@ -66,6 +68,8 @@ public struct MessageListConfig { self.skipEditedMessageLabel = skipEditedMessageLabel self.draftMessagesEnabled = draftMessagesEnabled self.downloadFileAttachmentsEnabled = downloadFileAttachmentsEnabled + self.hidesCommandsOverlayOnMessageListTap = hidesCommandsOverlayOnMessageListTap + self.hidesAttachmentsPickersOnMessageListTap = hidesAttachmentsPickersOnMessageListTap } public let messageListType: MessageListType @@ -93,6 +97,16 @@ public struct MessageListConfig { public let markdownSupportEnabled: Bool public let userBlockingEnabled: Bool + /// A boolean to enable hiding the commands overlay when tapping the message list. + /// + /// It is enabled by default. + public let hidesCommandsOverlayOnMessageListTap: Bool + + /// A boolean to enable hiding the attachments keyboard picker when tapping the message list. + /// + /// It is enabled by default. + public let hidesAttachmentsPickersOnMessageListTap: Bool + /// A boolean to enable the alert actions for bounced messages. /// /// By default it is true and the bounced actions are displayed as an alert instead of a context menu. diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift index 0696eeb7e..2bfe8a96b 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift @@ -313,7 +313,6 @@ public struct MessageListView: View, KeyboardReadable { ) : nil ) .modifier(factory.makeMessageListContainerModifier()) - .dismissKeyboardOnTap(enabled: keyboardShown) .onDisappear { messageRenderingUtil.update(previousTopMessage: nil) } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift index b1c32f094..433b48735 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift @@ -167,7 +167,7 @@ public struct MessageTextView: View { ) } - StreamTextView(message: message) + factory.makeAttachmentTextView(options: .init(mesage: message)) .padding(.leading, leadingPadding) .padding(.trailing, trailingPadding) .padding(.top, topPadding) @@ -247,6 +247,16 @@ struct StreamTextView: View { } } +// Options for the attachment text view. +public class AttachmentTextViewOptions { + // The message to display the text for. + public let message: ChatMessage + + public init(mesage: ChatMessage) { + self.message = mesage + } +} + @available(iOS 15, *) public struct LinkDetectionTextView: View { @Environment(\.layoutDirection) var layoutDirection diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesViewModel.swift index aeba9bf45..fb85a6a53 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesViewModel.swift @@ -7,10 +7,10 @@ import StreamChat import SwiftUI class PollOptionAllVotesViewModel: ObservableObject, PollVoteListControllerDelegate { - let poll: Poll let option: PollOption let controller: PollVoteListController - + + @Published var poll: Poll @Published var pollVotes = [PollVote]() @Published var errorShown = false @@ -70,7 +70,11 @@ class PollOptionAllVotesViewModel: ObservableObject, PollVoteListControllerDeleg pollVotes = Array(controller.votes) } } - + + func controller(_ controller: PollVoteListController, didUpdatePoll poll: Poll) { + self.poll = poll + } + private func loadVotes() { loadingVotes = true diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift index 7cc4e217b..ba458830a 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift @@ -46,7 +46,7 @@ public struct VideoAttachmentsContainer: View { } if !message.text.isEmpty { - AttachmentTextView(message: message) + AttachmentTextView(factory: factory, message: message) .frame(width: width) } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift index be7a180eb..8064166d6 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift @@ -175,6 +175,17 @@ public struct CreatePollView: View { } label: { Text(L10n.Alert.Actions.cancel) } + .actionSheet(isPresented: $viewModel.discardConfirmationShown) { + ActionSheet( + title: Text(L10n.Composer.Polls.actionSheetDiscardTitle), + buttons: [ + .destructive(Text(L10n.Alert.Actions.discardChanges)) { + presentationMode.wrappedValue.dismiss() + }, + .default(Text(L10n.Alert.Actions.keepEditing)) + ] + ) + } } ToolbarItem(placement: .principal) { @@ -195,17 +206,6 @@ public struct CreatePollView: View { } } .navigationBarTitleDisplayMode(.inline) - .actionSheet(isPresented: $viewModel.discardConfirmationShown) { - ActionSheet( - title: Text(L10n.Composer.Polls.actionSheetDiscardTitle), - buttons: [ - .destructive(Text(L10n.Alert.Actions.discardChanges)) { - presentationMode.wrappedValue.dismiss() - }, - .cancel(Text(L10n.Alert.Actions.keepEditing)) - ] - ) - } .alert(isPresented: $viewModel.errorShown) { Alert.defaultErrorAlert } diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index b238a265d..3d881999f 100644 --- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift +++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift @@ -1151,6 +1151,12 @@ extension ViewFactory { ) -> some View { AddUsersView(loadedUserIds: options.loadedUsers.map(\.id), onUserTap: onUserTap) } + + public func makeAttachmentTextView( + options: AttachmentTextViewOptions + ) -> some View { + StreamTextView(message: options.message) + } } /// Default class conforming to `ViewFactory`, used throughout the SDK. diff --git a/Sources/StreamChatSwiftUI/Generated/L10n.swift b/Sources/StreamChatSwiftUI/Generated/L10n.swift index 79a7dddf7..9c4dcf11c 100644 --- a/Sources/StreamChatSwiftUI/Generated/L10n.swift +++ b/Sources/StreamChatSwiftUI/Generated/L10n.swift @@ -257,6 +257,8 @@ internal enum L10n { internal static var giphy: String { L10n.tr("Localizable", "composer.placeholder.giphy") } /// Send a message internal static var message: String { L10n.tr("Localizable", "composer.placeholder.message") } + /// You can't send messages in this channel + internal static var messageDisabled: String { L10n.tr("Localizable", "composer.placeholder.messageDisabled") } /// Slow mode ON internal static var slowMode: String { L10n.tr("Localizable", "composer.placeholder.slow-mode") } } diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index b97f1f3b2..a5c3c79c9 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.90.0" + public static let version: String = "4.91.0" } diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist index eca9a0a57..268afdcca 100644 --- a/Sources/StreamChatSwiftUI/Info.plist +++ b/Sources/StreamChatSwiftUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.90.0 + 4.91.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPhotoLibraryUsageDescription diff --git a/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings b/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings index 7ac2913f6..619e24b32 100644 --- a/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings +++ b/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings @@ -121,6 +121,7 @@ "composer.title.edit" = "Edit Message"; "composer.title.reply" = "Reply to Message"; "composer.placeholder.message" = "Send a message"; +"composer.placeholder.messageDisabled" = "You can't send messages in this channel"; "composer.placeholder.slow-mode" = "Slow mode ON"; "composer.placeholder.giphy" = "Search GIFs"; "composer.checkmark.direct-message-reply" = "Also send as direct message"; diff --git a/Sources/StreamChatSwiftUI/Utils/KeyboardHandling.swift b/Sources/StreamChatSwiftUI/Utils/KeyboardHandling.swift index ec483dbff..5a21e9dc8 100644 --- a/Sources/StreamChatSwiftUI/Utils/KeyboardHandling.swift +++ b/Sources/StreamChatSwiftUI/Utils/KeyboardHandling.swift @@ -63,7 +63,7 @@ extension View { /// - enabled: If true, tapping on the view dismisses the view, otherwise keyboard stays visible. /// - onTapped: A closure which is triggered when keyboard is dismissed after tapping the view. func dismissKeyboardOnTap(enabled: Bool, onKeyboardDismissed: (() -> Void)? = nil) -> some View { - modifier(HideKeyboardOnTapGesture(shouldAdd: enabled)) + modifier(HideKeyboardOnTapGesture(shouldAdd: enabled, onTapped: onKeyboardDismissed)) } } diff --git a/Sources/StreamChatSwiftUI/ViewFactory.swift b/Sources/StreamChatSwiftUI/ViewFactory.swift index 9c7465737..0439b8d19 100644 --- a/Sources/StreamChatSwiftUI/ViewFactory.swift +++ b/Sources/StreamChatSwiftUI/ViewFactory.swift @@ -1182,4 +1182,12 @@ public protocol ViewFactory: AnyObject { options: AddUsersOptions, onUserTap: @escaping (ChatUser) -> Void ) -> AddUsersViewType + + associatedtype AttachmentTextViewType: View + /// Creates a view for displaying the text of an attachment. + /// - Parameter options: Configuration options for the attachment text view, such as message. + /// - Returns: The view shown in the attachment text slot. + func makeAttachmentTextView( + options: AttachmentTextViewOptions + ) -> AttachmentTextViewType } diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index 75dfae9b1..6d88ce8d5 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.90.0' + spec.version = '4.91.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.90.0' + spec.dependency 'StreamChat-XCFramework', '~> 4.91.0' spec.cocoapods_version = '>= 1.11.0' end diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec index 0d1ce08e2..e8cd2f445 100644 --- a/StreamChatSwiftUI.podspec +++ b/StreamChatSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamChatSwiftUI' - spec.version = '4.90.0' + spec.version = '4.91.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.90.0' + spec.dependency 'StreamChat', '~> 4.91.0' end diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index a87a019fb..904fe0c47 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3296,6 +3296,8 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_GENERATE_MAP_FILE = YES; + LD_MAP_FILE_PATH = "linkmaps/$(PRODUCT_NAME)-$(CURRENT_ARCH)-LinkMap.txt"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3674,6 +3676,8 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_GENERATE_MAP_FILE = YES; + LD_MAP_FILE_PATH = "linkmaps/$(PRODUCT_NAME)-$(CURRENT_ARCH)-LinkMap.txt"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3709,6 +3713,8 @@ INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_GENERATE_MAP_FILE = YES; + LD_MAP_FILE_PATH = "linkmaps/$(PRODUCT_NAME)-$(CURRENT_ARCH)-LinkMap.txt"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3940,7 +3946,7 @@ repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.90.0; + minimumVersion = 4.91.0; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json index 212e8b2ec..84875efd1 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","4.89.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.0/StreamChatSwiftUI.zip","4.89.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.1/StreamChatSwiftUI.zip","4.90.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.90.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","4.89.1":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.89.1/StreamChatSwiftUI.zip","4.90.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.90.0/StreamChatSwiftUI.zip","4.91.0":"https://github.com/GetStream/stream-chat-swiftui/releases/download/4.91.0/StreamChatSwiftUI.zip"} \ No newline at end of file diff --git a/StreamChatSwiftUITests/Infrastructure/TestTools/ViewFrameUtils.swift b/StreamChatSwiftUITests/Infrastructure/TestTools/ViewFrameUtils.swift index c9186bbe6..77a7130d6 100644 --- a/StreamChatSwiftUITests/Infrastructure/TestTools/ViewFrameUtils.swift +++ b/StreamChatSwiftUITests/Infrastructure/TestTools/ViewFrameUtils.swift @@ -15,4 +15,15 @@ extension View { func applySize(_ size: CGSize) -> some View { frame(width: size.width, height: size.height) } + + @discardableResult + /// Add SwiftUI View to a fake hierarchy so that it can receive UI events. + func addToViewHierarchy() -> some View { + let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 400, height: 400)) + let hostingController = UIHostingController(rootView: self) + window.rootViewController = hostingController + window.makeKeyAndVisible() + hostingController.view.layoutIfNeeded() + return self + } } diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelTestHelpers.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelTestHelpers.swift index 8b8183048..05926d9a6 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelTestHelpers.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelTestHelpers.swift @@ -17,7 +17,7 @@ class ChatChannelTestHelpers { let config = ChannelConfig(commands: [Command(name: "giphy", description: "", set: "", args: "")]) let channel = chatChannel ?? ChatChannel.mockDMChannel( config: config, - ownCapabilities: [.uploadFile], + ownCapabilities: [.sendMessage, .uploadFile], lastActiveWatchers: lastActiveWatchers ) let channelQuery = ChannelQuery(cid: channel.cid) diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift index 905da904c..34663602e 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift @@ -204,7 +204,7 @@ class MessageComposerView_Tests: StreamChatTestCase { // Given let factory = DefaultViewFactory.shared let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) - mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile]) + mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile]) let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil) // When @@ -234,6 +234,97 @@ class MessageComposerView_Tests: StreamChatTestCase { assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } + // MARK: - Frozen Channel Tests + + func test_messageComposerView_frozenChannel() { + // Given + let factory = DefaultViewFactory.shared + let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + // Create a channel without sendMessage capability (simulating frozen channel) + mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents]) + let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil) + + // When + let view = MessageComposerView( + viewFactory: factory, + viewModel: viewModel, + channelController: mockChannelController, + messageController: nil, + quotedMessage: .constant(nil), + editedMessage: .constant(nil), + onMessageSent: {} + ) + .frame(width: defaultScreenSize.width, height: 100) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_composerInputView_frozenChannel() { + // Given + let factory = DefaultViewFactory.shared + let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents]) + let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil) + + // When + let view = ComposerInputView( + factory: factory, + text: .constant(""), + selectedRangeLocation: .constant(0), + command: .constant(nil), + addedAssets: [], + addedFileURLs: [], + addedCustomAttachments: [], + quotedMessage: .constant(nil), + cooldownDuration: 0, + onCustomAttachmentTap: { _ in }, + removeAttachmentWithId: { _ in } + ) + .environmentObject(viewModel) + .frame(width: defaultScreenSize.width, height: 100) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_leadingComposerView_frozenChannel() { + // Given + let factory = DefaultViewFactory.shared + let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents]) + let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil) + + // When + let pickerTypeState: Binding = .constant(.expanded(.none)) + let view = factory.makeLeadingComposerView(state: pickerTypeState, channelConfig: nil) + .environmentObject(viewModel) + .frame(width: 36, height: 36) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + + func test_trailingComposerView_frozenChannel() { + // Given + let factory = DefaultViewFactory.shared + let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents]) + let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil) + + // When + let view = factory.makeTrailingComposerView( + enabled: true, + cooldownDuration: 0, + onTap: {} + ) + .environmentObject(viewModel) + .frame(width: 100, height: 40) + + // Then + assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) + } + func test_composerInputView_inputTextView() { // Given let view = InputTextView( @@ -803,6 +894,166 @@ class MessageComposerView_Tests: StreamChatTestCase { onMessageSent: {} ) } + + // MARK: - Notification Tests + + func test_commandsOverlayHiddenNotification_hidesCommandsOverlay() { + // Given + let utils = Utils( + messageListConfig: MessageListConfig( + hidesCommandsOverlayOnMessageListTap: true + ) + ) + streamChat = StreamChat(chatClient: chatClient, utils: utils) + + let factory = DefaultViewFactory.shared + let channelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil) + + // Set up a command to be shown + viewModel.composerCommand = ComposerCommand( + id: "testCommand", + typingSuggestion: TypingSuggestion.empty, + displayInfo: nil + ) + + let view = MessageComposerView( + viewFactory: factory, + viewModel: viewModel, + channelController: channelController, + messageController: nil, + quotedMessage: .constant(nil), + editedMessage: .constant(nil), + onMessageSent: {} + ) + view.addToViewHierarchy() + + // When + NotificationCenter.default.post( + name: .commandsOverlayHiddenNotification, + object: nil + ) + + // Then + XCTAssertNil(viewModel.composerCommand) + } + + func test_commandsOverlayHiddenNotification_respectsConfigSetting() { + // Given + let utils = Utils( + messageListConfig: MessageListConfig( + hidesCommandsOverlayOnMessageListTap: false + ) + ) + streamChat = StreamChat(chatClient: chatClient, utils: utils) + + let factory = DefaultViewFactory.shared + let channelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil) + + // Set up a command to be shown + let testCommand = ComposerCommand( + id: "testCommand", + typingSuggestion: TypingSuggestion.empty, + displayInfo: nil + ) + viewModel.composerCommand = testCommand + + let view = MessageComposerView( + viewFactory: factory, + viewModel: viewModel, + channelController: channelController, + messageController: nil, + quotedMessage: .constant(nil), + editedMessage: .constant(nil), + onMessageSent: {} + ) + view.addToViewHierarchy() + + // When + NotificationCenter.default.post( + name: .commandsOverlayHiddenNotification, + object: nil + ) + + // Then + XCTAssertNotNil(viewModel.composerCommand) + XCTAssertEqual(viewModel.composerCommand?.id, testCommand.id) + } + + func test_attachmentPickerHiddenNotification_hidesAttachmentPicker() { + // Given + let utils = Utils( + messageListConfig: MessageListConfig( + hidesAttachmentsPickersOnMessageListTap: true + ) + ) + streamChat = StreamChat(chatClient: chatClient, utils: utils) + + let factory = DefaultViewFactory.shared + let channelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil) + + // Set up attachment picker to be shown + viewModel.pickerTypeState = .expanded(.media) + + let view = MessageComposerView( + viewFactory: factory, + viewModel: viewModel, + channelController: channelController, + messageController: nil, + quotedMessage: .constant(nil), + editedMessage: .constant(nil), + onMessageSent: {} + ) + view.addToViewHierarchy() + + // When + NotificationCenter.default.post( + name: .attachmentPickerHiddenNotification, + object: nil + ) + + // Then + XCTAssertEqual(viewModel.pickerTypeState, .expanded(.none)) + } + + func test_attachmentPickerHiddenNotification_respectsConfigSetting() { + // Given + let utils = Utils( + messageListConfig: MessageListConfig( + hidesAttachmentsPickersOnMessageListTap: false + ) + ) + streamChat = StreamChat(chatClient: chatClient, utils: utils) + + let factory = DefaultViewFactory.shared + let channelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient) + let viewModel = MessageComposerViewModel(channelController: channelController, messageController: nil) + + // Set up attachment picker to be shown + viewModel.pickerTypeState = .expanded(.media) + + let view = MessageComposerView( + viewFactory: factory, + viewModel: viewModel, + channelController: channelController, + messageController: nil, + quotedMessage: .constant(nil), + editedMessage: .constant(nil), + onMessageSent: {} + ) + view.addToViewHierarchy() + + // When + NotificationCenter.default.post( + name: .attachmentPickerHiddenNotification, + object: nil + ) + + // Then + XCTAssertEqual(viewModel.pickerTypeState, .expanded(.media)) + } } class SynchronousAttachmentsConverter: MessageAttachmentsConverter { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerInputView_frozenChannel.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerInputView_frozenChannel.1.png new file mode 100644 index 000000000..c0474044c Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerInputView_frozenChannel.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_leadingComposerView_frozenChannel.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_leadingComposerView_frozenChannel.1.png new file mode 100644 index 000000000..86c39cbb3 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_leadingComposerView_frozenChannel.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_messageComposerView_frozenChannel.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_messageComposerView_frozenChannel.1.png new file mode 100644 index 000000000..9f366ec2d Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_messageComposerView_frozenChannel.1.png differ diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_trailingComposerView_frozenChannel.1.png b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_trailingComposerView_frozenChannel.1.png new file mode 100644 index 000000000..18daa12e6 Binary files /dev/null and b/StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_trailingComposerView_frozenChannel.1.png differ diff --git a/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift b/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift index a2974a46d..0b4e693f0 100644 --- a/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift +++ b/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift @@ -1043,6 +1043,17 @@ class ViewFactory_Tests: StreamChatTestCase { // Then XCTAssert(view is AddUsersView) } + + func test_viewFactory_makeAttachmentTextView() { + // Given + let viewFactory = DefaultViewFactory.shared + + // When + let view = viewFactory.makeAttachmentTextView(options: .init(mesage: message)) + + // Then + XCTAssert(view is StreamTextView) + } } extension ChannelAction: Equatable { diff --git a/StreamChatSwiftUITestsApp/AppDelegate.swift b/StreamChatSwiftUITestsApp/AppDelegate.swift index d3d6a5606..7a9bfa7c4 100644 --- a/StreamChatSwiftUITestsApp/AppDelegate.swift +++ b/StreamChatSwiftUITestsApp/AppDelegate.swift @@ -5,18 +5,21 @@ import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { - - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { disableAnimations() registerForPushNotifications() UNUserNotificationCenter.current().delegate = NotificationsHandler.shared return true } - func application(_ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) sceneConfig.delegateClass = SceneDelegate.self return sceneConfig @@ -44,13 +47,14 @@ class AppDelegate: NSObject, UIApplicationDelegate { } func application( - _ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } let token = tokenParts.joined() print("Device Token: \(token)") } + func application( _ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error diff --git a/StreamChatSwiftUITestsApp/CustomChannelHeader.swift b/StreamChatSwiftUITestsApp/CustomChannelHeader.swift index a3d98f602..c3269c9da 100644 --- a/StreamChatSwiftUITestsApp/CustomChannelHeader.swift +++ b/StreamChatSwiftUITestsApp/CustomChannelHeader.swift @@ -7,7 +7,6 @@ import StreamChatSwiftUI import SwiftUI public struct CustomChannelHeader: ToolbarContent { - @Injected(\.fonts) var fonts @Injected(\.images) var images @Injected(\.colors) var colors @@ -36,7 +35,6 @@ public struct CustomChannelHeader: ToolbarContent { } struct CustomChannelModifier: ChannelListHeaderViewModifier { - @Injected(\.chatClient) var chatClient var title: String diff --git a/StreamChatSwiftUITestsApp/InternetConnectionMonitor_Mock.swift b/StreamChatSwiftUITestsApp/InternetConnectionMonitor_Mock.swift index ec1e8026e..cb5a87b6d 100644 --- a/StreamChatSwiftUITestsApp/InternetConnectionMonitor_Mock.swift +++ b/StreamChatSwiftUITestsApp/InternetConnectionMonitor_Mock.swift @@ -4,7 +4,7 @@ import Foundation -//#if TESTS +// #if TESTS @testable import StreamChat final class InternetConnectionMonitor_Mock: InternetConnectionMonitor { @@ -19,6 +19,6 @@ final class InternetConnectionMonitor_Mock: InternetConnectionMonitor { self.status = status delegate?.internetConnectionStatusDidChange(status: status) } - } -//#endif + +// #endif diff --git a/StreamChatSwiftUITestsApp/StartPage.swift b/StreamChatSwiftUITestsApp/StartPage.swift index 280b967c3..69e61f7d7 100644 --- a/StreamChatSwiftUITestsApp/StartPage.swift +++ b/StreamChatSwiftUITestsApp/StartPage.swift @@ -2,12 +2,11 @@ // Copyright © 2025 Stream.io Inc. All rights reserved. // -import SwiftUI import StreamChat import StreamChatSwiftUI +import SwiftUI struct StartPage: View { - @State var streamChat: StreamChat? @State var chatShown = false @ObservedObject var appState = AppState.shared @@ -67,8 +66,8 @@ struct StartPage: View { streamChat = StreamChat(chatClient: chatClient) chatClient.connectUser( - userInfo: .init(id: credentials.id, name: credentials.name, imageURL: credentials.avatarURL), - token: token + userInfo: .init(id: credentials.id, name: credentials.name, imageURL: credentials.avatarURL), + token: token ) { error in if let error = error { log.error("connecting the user failed \(error)") @@ -79,7 +78,6 @@ struct StartPage: View { } class DemoAppFactory: ViewFactory { - @Injected(\.chatClient) public var chatClient private init() {} diff --git a/StreamChatSwiftUITestsApp/StreamChatSwiftUITestsAppApp.swift b/StreamChatSwiftUITestsApp/StreamChatSwiftUITestsAppApp.swift index 0b6fc9e6d..0fccfefff 100644 --- a/StreamChatSwiftUITestsApp/StreamChatSwiftUITestsAppApp.swift +++ b/StreamChatSwiftUITestsApp/StreamChatSwiftUITestsAppApp.swift @@ -7,7 +7,6 @@ import SwiftUI @main struct StreamChatSwiftUITestsAppApp: App { - @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { @@ -18,7 +17,6 @@ struct StreamChatSwiftUITestsAppApp: App { } class AppState: ObservableObject, Equatable { - static func == (lhs: AppState, rhs: AppState) -> Bool { lhs.userState == rhs.userState } diff --git a/StreamChatSwiftUITestsApp/StreamChatWrapper.swift b/StreamChatSwiftUITestsApp/StreamChatWrapper.swift index a18853d6b..78c4e217c 100644 --- a/StreamChatSwiftUITestsApp/StreamChatWrapper.swift +++ b/StreamChatSwiftUITestsApp/StreamChatWrapper.swift @@ -4,8 +4,8 @@ import Foundation #if TESTS -@testable import StreamChat import OHHTTPStubs +@testable import StreamChat #else import StreamChat #endif @@ -13,7 +13,6 @@ import StreamChatSwiftUI import UIKit final class StreamChatWrapper { - @Injected(\.chatClient) var client static let shared = StreamChatWrapper() @@ -26,9 +25,11 @@ final class StreamChatWrapper { let baseURL = self.client.config.baseURL.restAPIBaseURL.absoluteString return request.url?.absoluteString.contains(baseURL) ?? false }, withStubResponse: { _ -> HTTPStubsResponse in - let error = NSError(domain: "NSURLErrorDomain", - code: -1009, - userInfo: nil) + let error = NSError( + domain: "NSURLErrorDomain", + code: -1009, + userInfo: nil + ) return HTTPStubsResponse(error: error) }) @@ -51,5 +52,4 @@ final class StreamChatWrapper { } #endif } - } diff --git a/StreamChatSwiftUITestsApp/UserCredentials.swift b/StreamChatSwiftUITestsApp/UserCredentials.swift index df293db54..64c216211 100644 --- a/StreamChatSwiftUITestsApp/UserCredentials.swift +++ b/StreamChatSwiftUITestsApp/UserCredentials.swift @@ -18,15 +18,15 @@ public struct UserCredentials { } extension UserCredentials { - static func builtInUsersByID(id: String) -> UserCredentials? { mock } - static let mock: UserCredentials = UserCredentials(id: "luke_skywalker", - name: "Luke Skywalker", - avatarURL: URL(string: "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg")!, - token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibHVrZV9za3l3YWxrZXIifQ.b6EiC8dq2AHk0JPfI-6PN-AM9TVzt8JV-qB1N9kchlI", - birthLand: "Tatooine") - + static let mock: UserCredentials = UserCredentials( + id: "luke_skywalker", + name: "Luke Skywalker", + avatarURL: URL(string: "https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg")!, + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibHVrZV9za3l3YWxrZXIifQ.b6EiC8dq2AHk0JPfI-6PN-AM9TVzt8JV-qB1N9kchlI", + birthLand: "Tatooine" + ) } diff --git a/StreamChatSwiftUITestsAppTests/Pages/ChannelListPage.swift b/StreamChatSwiftUITestsAppTests/Pages/ChannelListPage.swift index f926db289..4cf37f9b8 100644 --- a/StreamChatSwiftUITestsAppTests/Pages/ChannelListPage.swift +++ b/StreamChatSwiftUITestsAppTests/Pages/ChannelListPage.swift @@ -3,11 +3,10 @@ // import Foundation -import XCTest import StreamChat +import XCTest enum ChannelListPage { - static var userAvatar: XCUIElement { return app.buttons["LogoutButton"] } @@ -55,5 +54,4 @@ enum ChannelListPage { return cell.images["readIndicatorCheckmark"] } } - } diff --git a/StreamChatSwiftUITestsAppTests/Pages/MessageListPage.swift b/StreamChatSwiftUITestsAppTests/Pages/MessageListPage.swift index 314d04d46..1eac12e41 100644 --- a/StreamChatSwiftUITestsAppTests/Pages/MessageListPage.swift +++ b/StreamChatSwiftUITestsAppTests/Pages/MessageListPage.swift @@ -3,14 +3,13 @@ // import Foundation -import XCTest import StreamChat @testable import StreamChatSwiftUI +import XCTest // swiftlint:disable convenience_type class MessageListPage { - static var cells: XCUIElementQuery { app.otherElements.matching(identifier: "MessageContainerView") } @@ -44,7 +43,6 @@ class MessageListPage { } enum NavigationBar { - static var chatAvatar: XCUIElement { app.images["ChannelAvatarView"] } @@ -57,13 +55,13 @@ class MessageListPage { app.staticTexts.matching(identifier: "ChannelTitleView").lastMatch! } - // FIXME + // FIXME: static var debugMenu: XCUIElement { app.buttons[""].firstMatch } } - // FIXME + // FIXME: enum Alert { enum Debug { // Add member @@ -168,7 +166,7 @@ class MessageListPage { messageCell.staticTexts["readIndicatorCount"] } - // FIXME + // FIXME: static func statusCheckmark(for status: MessageDeliveryStatus? = nil, in messageCell: XCUIElement) -> XCUIElement { messageCell.images["readIndicatorCheckmark"] } @@ -314,7 +312,7 @@ class MessageListPage { } } - struct Element { + enum Element { static var actionsView: XCUIElement { app.otherElements["MessageActionsView"] } static var reply: XCUIElement { app.otherElements["messageAction-reply_message_action"].images.firstMatch } static var threadReply: XCUIElement { app.otherElements["messageAction-thread_message_action"].images.firstMatch } @@ -324,7 +322,7 @@ class MessageListPage { static var unmute: XCUIElement { app.otherElements["messageAction-unmute_message_action"].images.firstMatch } static var edit: XCUIElement { app.otherElements["messageAction-edit_message_action"].images.firstMatch } static var delete: XCUIElement { app.otherElements["messageAction-delete_message_action"].images.firstMatch } - static var hardDelete: XCUIElement { app.otherElements["messageAction-delete_message_action"].images.firstMatch } // FIXME + static var hardDelete: XCUIElement { app.otherElements["messageAction-delete_message_action"].images.firstMatch } // FIXME: static var resend: XCUIElement { app.otherElements["messageAction-resend_message_action"].images.firstMatch } static var pin: XCUIElement { app.otherElements["messageAction-pin_message_action"].images.firstMatch } static var unpin: XCUIElement { app.otherElements["messageAction-unpin_message_action"].images.firstMatch } @@ -348,6 +346,7 @@ class MessageListPage { static var cancelButton: XCUIElement { app.scrollViews.buttons.matching(NSPredicate(format: "label LIKE 'Cancel'")).firstMatch } + static var images: XCUIElementQuery { app.scrollViews["AttachmentTypeContainer"].images } } @@ -374,5 +373,4 @@ class MessageListPage { app.scrollViews["CommandsContainerView"].otherElements.matching(NSPredicate(format: "identifier LIKE 'MessageAvatarView'")) } } - } diff --git a/StreamChatSwiftUITestsAppTests/Pages/SpringBoard.swift b/StreamChatSwiftUITestsAppTests/Pages/SpringBoard.swift index ea7568949..e94aecf42 100644 --- a/StreamChatSwiftUITestsAppTests/Pages/SpringBoard.swift +++ b/StreamChatSwiftUITestsAppTests/Pages/SpringBoard.swift @@ -14,9 +14,9 @@ enum SpringBoard { static var notificationBanner: XCUIElement { app.otherElements["Notification"] - .descendants(matching: .any) - .matching(NSPredicate(format: "label CONTAINS[c] ', now,'")) - .firstMatch + .descendants(matching: .any) + .matching(NSPredicate(format: "label CONTAINS[c] ', now,'")) + .firstMatch } static var testAppIcon: XCUIElement { diff --git a/StreamChatSwiftUITestsAppTests/Pages/ThreadPage.swift b/StreamChatSwiftUITestsAppTests/Pages/ThreadPage.swift index 9a6daecb1..eca7e35b9 100644 --- a/StreamChatSwiftUITestsAppTests/Pages/ThreadPage.swift +++ b/StreamChatSwiftUITestsAppTests/Pages/ThreadPage.swift @@ -6,7 +6,5 @@ import Foundation import XCTest class ThreadPage: MessageListPage { - static var alsoSendInChannelCheckbox: XCUIElement { app.buttons["SendInChannelView"] } - } diff --git a/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift b/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift index 637cdb19a..3ab1f0b8b 100644 --- a/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift +++ b/StreamChatSwiftUITestsAppTests/Robots/UserRobot+Asserts.swift @@ -3,9 +3,9 @@ // import Foundation -import XCTest import StreamChat @testable import StreamChatSwiftUI +import XCTest let channelAttributes = ChannelListPage.Attributes.self let channelCells = ChannelListPage.cells @@ -13,26 +13,28 @@ let attributes = MessageListPage.Attributes.self let cells = MessageListPage.cells // MARK: Channel List -extension UserRobot { +extension UserRobot { @discardableResult - func channelCell(withIndex index: Int? = nil, - file: StaticString = #filePath, - line: UInt = #line) -> XCUIElement { - guard let index = index else { - return channelCells.firstMatch - } + func channelCell( + withIndex index: Int? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> XCUIElement { + guard let index = index else { + return channelCells.firstMatch + } - let minExpectedCount = index + 1 - let cells = cells.waitCount(index) - XCTAssertGreaterThanOrEqual( - cells.count, - minExpectedCount, - "Message cell is not found at index #\(index)", - file: file, - line: line - ) - return channelCells.element(boundBy: index) + let minExpectedCount = index + 1 + let cells = cells.waitCount(index) + XCTAssertGreaterThanOrEqual( + cells.count, + minExpectedCount, + "Message cell is not found at index #\(index)", + file: file, + line: line + ) + return channelCells.element(boundBy: index) } @discardableResult @@ -45,10 +47,12 @@ extension UserRobot { let cell = channelCell(withIndex: cellIndex, file: file, line: line) let message = channelAttributes.lastMessage(in: cell) let actualText = message.waitForText(text, mustBeEqual: false).text - XCTAssertTrue(actualText.contains(text), - "'\(actualText)' does not contain '\(text)'", - file: file, - line: line) + XCTAssertTrue( + actualText.contains(text), + "'\(actualText)' does not contain '\(text)'", + file: file, + line: line + ) return self } @@ -116,10 +120,12 @@ extension UserRobot { let expectedChannel = ChannelListPage.channel(withName: "\(expectedCount)") var expectedChannelExist = expectedChannel.exists - XCTAssertFalse(expectedChannelExist, - "Expected channel should not be visible", - file: file, - line: line) + XCTAssertFalse( + expectedChannelExist, + "Expected channel should not be visible", + file: file, + line: line + ) let endTime = Date().timeIntervalSince1970 * 1000 + XCUIElement.waitTimeout * 1000 while !expectedChannelExist && endTime > Date().timeIntervalSince1970 * 1000 { @@ -127,10 +133,12 @@ extension UserRobot { expectedChannelExist = expectedChannel.exists } - XCTAssertTrue(expectedChannelExist, - "Expected channel should be visible", - file: file, - line: line) + XCTAssertTrue( + expectedChannelExist, + "Expected channel should be visible", + file: file, + line: line + ) return self } @@ -147,12 +155,14 @@ extension UserRobot { } // MARK: Message List -extension UserRobot { +extension UserRobot { @discardableResult - func messageCell(withIndex index: Int? = nil, - file: StaticString = #filePath, - line: UInt = #line) -> XCUIElement { + func messageCell( + withIndex index: Int? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> XCUIElement { let messageCell: XCUIElement if let index = index { let minExpectedCount = index + 1 @@ -193,29 +203,37 @@ extension UserRobot { line: UInt = #line ) -> Self { let pushNotification = SpringBoard.notificationBanner.wait() - XCTAssertTrue(pushNotification.exists, - "Push notification should appear", - file: file, - line: line) + XCTAssertTrue( + pushNotification.exists, + "Push notification should appear", + file: file, + line: line + ) let pushNotificationContent = pushNotification.text - XCTAssertTrue(pushNotificationContent.contains(text), - "\(pushNotificationContent) does not contain \(text)", - file: file, - line: line) - XCTAssertTrue(pushNotificationContent.contains(sender), - "\(pushNotificationContent) does not contain \(sender)", - file: file, - line: line) + XCTAssertTrue( + pushNotificationContent.contains(text), + "\(pushNotificationContent) does not contain \(text)", + file: file, + line: line + ) + XCTAssertTrue( + pushNotificationContent.contains(sender), + "\(pushNotificationContent) does not contain \(sender)", + file: file, + line: line + ) return self } @discardableResult func assertPushNotificationDoesNotAppear(file: StaticString = #filePath, line: UInt = #line) -> Self { - XCTAssertFalse(SpringBoard.notificationBanner.exists, - "Push notification should not appear", - file: file, - line: line) + XCTAssertFalse( + SpringBoard.notificationBanner.exists, + "Push notification should not appear", + file: file, + line: line + ) return self } @@ -227,11 +245,13 @@ extension UserRobot { ) -> Self { SpringBoard.notificationBanner.wait() let appIconValue = SpringBoard.testAppIcon.value as? String - XCTAssertEqual(appIconValue?.contains("1"), - shouldBeVisible, - "Badge should be visible: \(shouldBeVisible)", - file: file, - line: line) + XCTAssertEqual( + appIconValue?.contains("1"), + shouldBeVisible, + "Badge should be visible: \(shouldBeVisible)", + file: file, + line: line + ) return self } @@ -334,6 +354,7 @@ extension UserRobot { file: StaticString = #filePath, line: UInt = #line ) -> Self { + // swiftformat:disable:next isEmpty if MessageListPage.cells.count > 0 { let messageCell = messageCell(withIndex: messageCellIndex, file: file, line: line) let actualText = attributes.text(in: messageCell).waitForTextDisappearance(deletedText).text @@ -356,7 +377,7 @@ extension UserRobot { XCTAssertEqual(author, actualAuthor, file: file, line: line) return self } - + @discardableResult func assertScrollToBottomButton( isVisible: Bool, @@ -365,14 +386,16 @@ extension UserRobot { ) -> Self { var btn = MessageListPage.scrollToBottomButton btn = isVisible ? btn.wait() : btn.waitForDisappearance() - XCTAssertEqual(isVisible, - btn.exists, - "Scroll to bottom button should be \(isVisible ? "visible" : "hidden")", - file: file, - line: line) + XCTAssertEqual( + isVisible, + btn.exists, + "Scroll to bottom button should be \(isVisible ? "visible" : "hidden")", + file: file, + line: line + ) return self } - + @discardableResult func assertScrollToBottomButtonUnreadCount( _ expectedCount: Int, @@ -399,14 +422,18 @@ extension UserRobot { line: UInt = #line ) -> Self { let typingIndicatorView = MessageListPage.typingIndicator.wait(timeout: waitTimeout) - XCTAssertTrue(typingIndicatorView.exists, - "Element hidden", - file: file, - line: line) - XCTAssertTrue(typingIndicatorView.text.contains(typingUserName), - "User name is wrong", - file: file, - line: line) + XCTAssertTrue( + typingIndicatorView.exists, + "Element hidden", + file: file, + line: line + ) + XCTAssertTrue( + typingIndicatorView.text.contains(typingUserName), + "User name is wrong", + file: file, + line: line + ) return self } @@ -422,10 +449,12 @@ extension UserRobot { } @discardableResult - func assertContextMenuOptionNotAvailable(option: MessageListPage.ContextMenu, - forMessageAtIndex index: Int = 0, - file: StaticString = #filePath, - line: UInt = #line) -> Self { + func assertContextMenuOptionNotAvailable( + option: MessageListPage.ContextMenu, + forMessageAtIndex index: Int = 0, + file: StaticString = #filePath, + line: UInt = #line + ) -> Self { openContextMenu(messageCellIndex: index) XCTAssertFalse(option.element.exists, "Context menu option is visible", file: file, line: line) return self @@ -442,7 +471,7 @@ extension UserRobot { XCTAssertTrue(errorButton.exists, "There is no error icon", file: file, line: line) return self } - + @discardableResult func waitForMessageDeliveryStatus( _ deliveryStatus: MessageDeliveryStatus?, @@ -489,9 +518,11 @@ extension UserRobot { return self } - func assertComposerLimits(toNumberOfLines limit: Int, - file: StaticString = #filePath, - line: UInt = #line) { + func assertComposerLimits( + toNumberOfLines limit: Int, + file: StaticString = #filePath, + line: UInt = #line + ) { let composer = MessageListPage.Composer.inputField var composerHeight = composer.height for i in 1.. Self { _ = messageCell(withIndex: messageCellIndex).waitForHitPoint() @@ -680,8 +715,8 @@ extension UserRobot { } // MARK: Quoted Messages -extension UserRobot { +extension UserRobot { @discardableResult func assertQuotedMessage( replyText: String = "", // empty text by default for attachment messages @@ -695,7 +730,7 @@ extension UserRobot { let actualText = message.waitForText(quotedText).text XCTAssertEqual(quotedText, actualText) XCTAssertTrue(message.exists, "Quoted message was not showed") - + if !replyText.isEmpty { let message = attributes.text(in: messageCell).wait() let actualText = message.waitForText(replyText).text @@ -703,7 +738,7 @@ extension UserRobot { } return self } - + @discardableResult func assertQuotedMessageWithAttachment( quotedText: String, @@ -720,15 +755,17 @@ extension UserRobot { } // MARK: Thread Replies -extension UserRobot { +extension UserRobot { @discardableResult func assertThreadIsOpen(file: StaticString = #filePath, line: UInt = #line) -> Self { let alsoSendInChannelCheckbox = ThreadPage.alsoSendInChannelCheckbox.wait() - XCTAssertTrue(alsoSendInChannelCheckbox.exists, - "alsoSendInChannel checkbox is not visible", - file: file, - line: line) + XCTAssertTrue( + alsoSendInChannelCheckbox.exists, + "alsoSendInChannel checkbox is not visible", + file: file, + line: line + ) return self } @@ -777,27 +814,35 @@ extension UserRobot { @discardableResult func assertCooldownIsShown(file: StaticString = #filePath, line: UInt = #line) -> Self { - XCTAssertEqual(MessageListPage.Composer.placeholder.text, - L10n.Composer.Placeholder.slowMode, - file: file, - line: line) - XCTAssertTrue(MessageListPage.Composer.cooldown.wait().exists, - "Cooldown should be visible", - file: file, - line: line) + XCTAssertEqual( + MessageListPage.Composer.placeholder.text, + L10n.Composer.Placeholder.slowMode, + file: file, + line: line + ) + XCTAssertTrue( + MessageListPage.Composer.cooldown.wait().exists, + "Cooldown should be visible", + file: file, + line: line + ) return self } @discardableResult func assertCooldownIsNotShown(file: StaticString = #filePath, line: UInt = #line) -> Self { - XCTAssertNotEqual(MessageListPage.Composer.placeholder.text, - L10n.Composer.Placeholder.slowMode, - file: file, - line: line) - XCTAssertFalse(MessageListPage.Composer.cooldown.exists, - "Cooldown should not be visible", - file: file, - line: line) + XCTAssertNotEqual( + MessageListPage.Composer.placeholder.text, + L10n.Composer.Placeholder.slowMode, + file: file, + line: line + ) + XCTAssertFalse( + MessageListPage.Composer.cooldown.exists, + "Cooldown should not be visible", + file: file, + line: line + ) return self } @@ -816,15 +861,18 @@ extension UserRobot { ) -> Self { let messageCell = messageCell(withIndex: messageCellIndex, file: file, line: line) let threadReplyCountButton = attributes.threadReplyCountButton(in: messageCell).wait() - XCTAssertTrue(threadReplyCountButton.exists, - "There is no thread reply count button", - file: file, - line: line) + XCTAssertTrue( + threadReplyCountButton.exists, + "There is no thread reply count button", + file: file, + line: line + ) return self } } // MARK: Reactions + extension UserRobot { @discardableResult func assertReaction( @@ -845,9 +893,11 @@ extension UserRobot { /// /// - Returns: Self @discardableResult - func waitForNewReaction(at messageCellIndex: Int? = nil, - file: StaticString = #filePath, - line: UInt = #line) -> Self { + func waitForNewReaction( + at messageCellIndex: Int? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) -> Self { let cell = messageCell(withIndex: messageCellIndex, file: file, line: line).wait() attributes.reactionButton(in: cell).wait() return self @@ -857,7 +907,6 @@ extension UserRobot { // MARK: Ephemeral messages extension UserRobot { - @discardableResult func assertGiphyImage( at messageCellIndex: Int? = nil, @@ -898,8 +947,8 @@ extension UserRobot { } // MARK: Keyboard -extension UserRobot { +extension UserRobot { @discardableResult func assertKeyboard( isVisible: Bool, @@ -908,18 +957,20 @@ extension UserRobot { ) -> Self { let keyboard = app.keyboards.firstMatch keyboard.wait(timeout: 1.5) - XCTAssertEqual(isVisible, - keyboard.exists, - "Keyboard should be \(isVisible ? "visible" : "hidden")", - file: file, - line: line) + XCTAssertEqual( + isVisible, + keyboard.exists, + "Keyboard should be \(isVisible ? "visible" : "hidden")", + file: file, + line: line + ) return self } } // MARK: Attachments -extension UserRobot { +extension UserRobot { @discardableResult func assertImage( isPresent: Bool, diff --git a/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift b/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift index 9f2d112f3..f03af913b 100644 --- a/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift +++ b/StreamChatSwiftUITestsAppTests/Robots/UserRobot.swift @@ -3,12 +3,11 @@ // import Foundation -import XCTest import StreamChat +import XCTest /// Simulates user behavior final class UserRobot: Robot { - let composer = MessageListPage.Composer.self let contextMenu = MessageListPage.ContextMenu.self let debugAlert = MessageListPage.Alert.Debug.self @@ -64,7 +63,6 @@ final class UserRobot: Robot { // MARK: Message List extension UserRobot { - @discardableResult func openContextMenu(messageCellIndex: Int = 0) -> Self { messageCell(withIndex: messageCellIndex).press(forDuration: 1) @@ -82,11 +80,13 @@ extension UserRobot { } @discardableResult - func sendMessage(_ text: String, - at messageCellIndex: Int? = nil, - waitForAppearance: Bool = true, - file: StaticString = #filePath, - line: UInt = #line) -> Self { + func sendMessage( + _ text: String, + at messageCellIndex: Int? = nil, + waitForAppearance: Bool = true, + file: StaticString = #filePath, + line: UInt = #line + ) -> Self { server.channelsEndpointWasCalled = false typeText(text) @@ -194,17 +194,21 @@ extension UserRobot { } @discardableResult - func quoteMessage(_ text: String, - messageCellIndex: Int = 0, - waitForAppearance: Bool = true, - file: StaticString = #filePath, - line: UInt = #line) -> Self { + func quoteMessage( + _ text: String, + messageCellIndex: Int = 0, + waitForAppearance: Bool = true, + file: StaticString = #filePath, + line: UInt = #line + ) -> Self { selectOptionFromContextMenu(option: .reply, forMessageAtIndex: messageCellIndex) - sendMessage(text, - at: messageCellIndex, - waitForAppearance: waitForAppearance, - file: file, - line: line) + sendMessage( + text, + at: messageCellIndex, + waitForAppearance: waitForAppearance, + file: file, + line: line + ) return self } @@ -293,11 +297,13 @@ extension UserRobot { if alsoSendInChannel { threadCheckbox.wait().safeTap() } - sendMessage(text, - at: messageCellIndex, - waitForAppearance: waitForAppearance, - file: file, - line: line) + sendMessage( + text, + at: messageCellIndex, + waitForAppearance: waitForAppearance, + file: file, + line: line + ) return self } @@ -346,6 +352,7 @@ extension UserRobot { @discardableResult func openComposerCommands() -> Self { + // swiftformat:disable:next isEmpty if MessageListPage.ComposerCommands.cells.count == 0 { MessageListPage.Composer.commandButton.wait().safeTap() } @@ -430,7 +437,6 @@ extension UserRobot { // MARK: Debug menu extension UserRobot { - @discardableResult private func tapOnDebugMenu() -> Self { MessageListPage.NavigationBar.debugMenu.safeTap() @@ -471,7 +477,6 @@ extension UserRobot { // MARK: Connectivity extension UserRobot { - /// Toggles the visibility of the connectivity switch control. When set to `.on`, the switch control will be displayed in the navigation bar. @discardableResult func setConnectivitySwitchVisibility(to state: SwitchState) -> Self { @@ -490,7 +495,6 @@ extension UserRobot { // MARK: Config extension UserRobot { - @discardableResult func setIsLocalStorageEnabled(to state: SwitchState) -> Self { setSwitchState(Settings.isLocalStorageEnabled.element, state: state) diff --git a/StreamChatSwiftUITestsAppTests/StreamChatSwiftUITests.swift b/StreamChatSwiftUITestsAppTests/StreamChatSwiftUITests.swift index 33dbeb3fb..ed36a6938 100644 --- a/StreamChatSwiftUITestsAppTests/StreamChatSwiftUITests.swift +++ b/StreamChatSwiftUITestsAppTests/StreamChatSwiftUITests.swift @@ -2,8 +2,8 @@ // Copyright © 2025 Stream.io Inc. All rights reserved. // +import Foundation @_exported import StreamChatTestMockServer @_exported import StreamSwiftTestHelpers -import Foundation final class StreamChatSwiftUITests {} diff --git a/StreamChatSwiftUITestsAppTests/Tests/Attachments_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/Attachments_Tests.swift index b125e52d7..05fd667a9 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/Attachments_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/Attachments_Tests.swift @@ -5,10 +5,11 @@ import XCTest final class Attachments_Tests: StreamTestCase { - override func setUpWithError() throws { - try XCTSkipIf(ProcessInfo().operatingSystemVersion.majorVersion >= 18, - "Attachments tests freeze the test app on iOS > 18") + try XCTSkipIf( + ProcessInfo().operatingSystemVersion.majorVersion >= 18, + "Attachments tests freeze the test app on iOS > 18" + ) try super.setUpWithError() addTags([.coreFeatures]) @@ -78,5 +79,4 @@ final class Attachments_Tests: StreamTestCase { userRobot.assertFile(isPresent: true) } } - } diff --git a/StreamChatSwiftUITestsAppTests/Tests/Base TestCase/StreamTestCase.swift b/StreamChatSwiftUITestsAppTests/Tests/Base TestCase/StreamTestCase.swift index c6bcd4494..17a77ff5a 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/Base TestCase/StreamTestCase.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/Base TestCase/StreamTestCase.swift @@ -8,7 +8,6 @@ import XCTest let app = XCUIApplication() class StreamTestCase: XCTestCase { - let deviceRobot = DeviceRobot(app) var userRobot: UserRobot! var backendRobot: BackendRobot! @@ -45,7 +44,6 @@ class StreamTestCase: XCTestCase { } extension StreamTestCase { - func assertMockServer() { XCTAssertFalse(mockServerCrashed, "Mock server failed on start") } diff --git a/StreamChatSwiftUITestsAppTests/Tests/ChannelList_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/ChannelList_Tests.swift index b4cd7f105..0f2bae593 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/ChannelList_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/ChannelList_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class ChannelList_Tests: StreamTestCase { - let message = "message" override func setUpWithError() throws { @@ -246,7 +245,6 @@ extension ChannelList_Tests { // MARK: - Truncate channel extension ChannelList_Tests { - func test_messageList_and_channelPreview_AreUpdatedWhenChannelTruncatedWithMessage() throws { linkToScenario(withId: 357) diff --git a/StreamChatSwiftUITestsAppTests/Tests/Ephemeral_Messages_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/Ephemeral_Messages_Tests.swift index 453a8cc92..aa74183b7 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/Ephemeral_Messages_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/Ephemeral_Messages_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class Ephemeral_Messages_Tests: StreamTestCase { - override func setUpWithError() throws { try super.setUpWithError() assertMockServer() diff --git a/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus+ChannelList_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus+ChannelList_Tests.swift index af04a93f0..121936b18 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus+ChannelList_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus+ChannelList_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class MessageDeliveryStatus_ChannelList_Tests: StreamTestCase { - let message = "message" var failedMessage: String { "failed \(message)" } @@ -63,7 +62,6 @@ final class MessageDeliveryStatus_ChannelList_Tests: StreamTestCase { userRobot .assertMessageReadCountInChannelPreview(readBy: 0) .assertMessageDeliveryStatusInChannelPreview(.sent) - } } @@ -172,7 +170,6 @@ final class MessageDeliveryStatus_ChannelList_Tests: StreamTestCase { // MARK: Thread Reply extension MessageDeliveryStatus_ChannelList_Tests { - func test_noCheckmarkShownForMessageInPreview_whenThreadReplyIsSent() throws { linkToScenario(withId: 430) diff --git a/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus_Tests.swift index acf04b42d..e33ae7254 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/Message Delivery Status/MessageDeliveryStatus_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class MessageDeliveryStatus_Tests: StreamTestCase { - let message = "message" var pendingMessage: String { "pending \(message)" } var failedMessage: String { "failed \(message)" } @@ -21,6 +20,7 @@ final class MessageDeliveryStatus_Tests: StreamTestCase { } // MARK: Message List + func test_singleCheckmarkShown_whenMessageIsSent() throws { linkToScenario(withId: 397) @@ -71,7 +71,6 @@ final class MessageDeliveryStatus_Tests: StreamTestCase { .login() .setConnectivity(to: .off) .openChannel() - } WHEN("user sends a new message") { userRobot.sendMessage(failedMessage, waitForAppearance: false) @@ -227,8 +226,8 @@ final class MessageDeliveryStatus_Tests: StreamTestCase { // MARK: Thread Reply extension MessageDeliveryStatus_Tests { - // MARK: Thread Previews + func test_singleCheckmarkShown_whenMessageIsSent_andPreviewedInThread() throws { linkToScenario(withId: 405) @@ -534,7 +533,6 @@ extension MessageDeliveryStatus_Tests { // MARK: Disabled Read Events feature extension MessageDeliveryStatus_Tests { - // MARK: Messages func test_deliveryStatusHidden_whenMessageIsSentAndReadEventsIsDisabled() throws { diff --git a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift index 821986de5..8ee85d954 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/MessageList_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class MessageList_Tests: StreamTestCase { - override func setUpWithError() throws { try super.setUpWithError() addTags([.coreFeatures]) @@ -359,7 +358,6 @@ final class MessageList_Tests: StreamTestCase { // MARK: Scroll to bottom extension MessageList_Tests { - func test_messageListScrollsDown_whenMessageListIsScrolledUp_andUserSendsNewMessage() throws { linkToScenario(withId: 359) @@ -511,7 +509,6 @@ extension MessageList_Tests { // MARK: Pagination extension MessageList_Tests { - func test_paginationOnMessageList() throws { linkToScenario(withId: 370) @@ -549,7 +546,6 @@ extension MessageList_Tests { // MARK: Mentions extension MessageList_Tests { - func test_addingCommandHidesLeftButtons() throws { linkToScenario(withId: 372) @@ -610,7 +606,6 @@ extension MessageList_Tests { // MARK: Links preview extension MessageList_Tests { - func test_addMessageWithLinkToUnsplash() { linkToScenario(withId: 375) @@ -683,6 +678,7 @@ extension MessageList_Tests { } // MARK: - Thread replies + extension MessageList_Tests { func test_threadReplyAppearsInThread_whenParticipantAddsThreadReply() throws { linkToScenario(withId: 379) diff --git a/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift index ae8239069..65ca6f65c 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/PushNotification_Tests.swift @@ -6,7 +6,6 @@ import XCTest // Requires running a standalone Sinatra server final class PushNotification_Tests: StreamTestCase { - let sender = "Han Solo" let message = "How are you? 🙂" @@ -30,7 +29,7 @@ final class PushNotification_Tests: StreamTestCase { GIVEN("user goes to channel list") { userRobot .login() - .openChannel() // this is required to let the mock server know + .openChannel() // this is required to let the mock server know .tapOnBackButton() // which channel to use for push notifications } checkHappyPath(message: message, sender: sender) @@ -61,7 +60,7 @@ final class PushNotification_Tests: StreamTestCase { version: "", messageId: "", cid: "" - ) + ) GIVEN("user goes to message list") { userRobot.login().openChannel() @@ -125,9 +124,11 @@ final class PushNotification_Tests: StreamTestCase { mockPushNotification(body: nil) WHEN("participant sends a message (push body param is nil)") { - participantRobot.wait(2).sendMessage("\(message)_0", - withPushNotification: true, - bundleIdForPushNotification: app.bundleId()) + participantRobot.wait(2).sendMessage( + "\(message)_0", + withPushNotification: true, + bundleIdForPushNotification: app.bundleId() + ) } THEN("user does not receive a push notification") { userRobot.assertPushNotificationDoesNotAppear() @@ -135,9 +136,11 @@ final class PushNotification_Tests: StreamTestCase { mockPushNotification(body: "") WHEN("participant sends a message (push body param is empty)") { - participantRobot.sendMessage("\(message)_1", - withPushNotification: true, - bundleIdForPushNotification: app.bundleId()) + participantRobot.sendMessage( + "\(message)_1", + withPushNotification: true, + bundleIdForPushNotification: app.bundleId() + ) } THEN("user does not receive a push notification") { userRobot.assertPushNotificationDoesNotAppear() @@ -145,9 +148,11 @@ final class PushNotification_Tests: StreamTestCase { mockPushNotification(body: 42) WHEN("participant sends a message (push body param contains incorrect type)") { - participantRobot.sendMessage("\(message)_2", - withPushNotification: true, - bundleIdForPushNotification: app.bundleId()) + participantRobot.sendMessage( + "\(message)_2", + withPushNotification: true, + bundleIdForPushNotification: app.bundleId() + ) } THEN("user does not receive a push notification") { userRobot.assertPushNotificationDoesNotAppear() diff --git a/StreamChatSwiftUITestsAppTests/Tests/QuotedReply_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/QuotedReply_Tests.swift index fb935677b..44833c433 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/QuotedReply_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/QuotedReply_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class QuotedReply_Tests: StreamTestCase { - let messageCount = 30 let parentMessage = "1" let quotedMessage = "quoted reply" @@ -107,7 +106,7 @@ final class QuotedReply_Tests: StreamTestCase { } func test_quotedReplyNotInList_whenParticipantAddsQuotedReply_Message() { - linkToScenario(withId: 1702) + linkToScenario(withId: 1702) GIVEN("user opens the channel") { backendRobot.generateChannels(count: 1, messagesCount: messageCount) @@ -133,7 +132,7 @@ final class QuotedReply_Tests: StreamTestCase { } func test_quotedReplyNotInList_whenParticipantAddsQuotedReply_File() { - linkToScenario(withId: 1703) + linkToScenario(withId: 1703) GIVEN("user opens the channel") { backendRobot.generateChannels(count: 1, messagesCount: messageCount) @@ -160,7 +159,7 @@ final class QuotedReply_Tests: StreamTestCase { } func test_quotedReplyNotInList_whenParticipantAddsQuotedReply_Giphy() { - linkToScenario(withId: 1704) + linkToScenario(withId: 1704) GIVEN("user opens the channel") { backendRobot.generateChannels(count: 1, messagesCount: messageCount) @@ -187,7 +186,7 @@ final class QuotedReply_Tests: StreamTestCase { } func test_quotedReplyIsDeletedByParticipant_deletedMessageIsShown() { - linkToScenario(withId: 388) + linkToScenario(withId: 388) GIVEN("user opens the channel") { backendRobot.generateChannels(count: 1, messagesCount: 1) @@ -205,7 +204,7 @@ final class QuotedReply_Tests: StreamTestCase { } func test_quotedReplyIsDeletedByUser_deletedMessageIsShown() { - linkToScenario(withId: 389) + linkToScenario(withId: 389) GIVEN("user opens the channel") { backendRobot.generateChannels(count: 1, messagesCount: 1) @@ -223,7 +222,7 @@ final class QuotedReply_Tests: StreamTestCase { } func test_unreadCount_whenUserSendsInvalidCommand_and_jumpingOnQuotedMessage() throws { - linkToScenario(withId: 1705) + linkToScenario(withId: 1705) let invalidCommand = "invalid command" diff --git a/StreamChatSwiftUITestsAppTests/Tests/Reactions_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/Reactions_Tests.swift index b97de225a..df9422970 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/Reactions_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/Reactions_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class Reactions_Tests: StreamTestCase { - override func setUpWithError() throws { try super.setUpWithError() addTags([.coreFeatures]) diff --git a/StreamChatSwiftUITestsAppTests/Tests/SlowMode_Tests.swift b/StreamChatSwiftUITestsAppTests/Tests/SlowMode_Tests.swift index 132efdf76..8884d9b5d 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/SlowMode_Tests.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/SlowMode_Tests.swift @@ -5,7 +5,6 @@ import XCTest final class SlowMode_Tests: StreamTestCase { - let cooldownDuration = 15 let message = "message" let anotherNewMessage = "Another new message" diff --git a/StreamChatSwiftUITestsAppTests/Tests/StreamTestCase+Tags.swift b/StreamChatSwiftUITestsAppTests/Tests/StreamTestCase+Tags.swift index 55d641b85..2a289319e 100644 --- a/StreamChatSwiftUITestsAppTests/Tests/StreamTestCase+Tags.swift +++ b/StreamChatSwiftUITestsAppTests/Tests/StreamTestCase+Tags.swift @@ -13,6 +13,6 @@ extension StreamTestCase { } func addTags(_ tags: [Tags]) { - addTagsToScenario(tags.map{ $0.rawValue }) + addTagsToScenario(tags.map { $0.rawValue }) } } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index da6f0de56..5866b6bd5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -511,13 +511,9 @@ lane :run_swift_format do |options| Dir.chdir('..') do check_foundation_import strict = options[:strict] ? '--lint' : nil - sources_matrix[:swiftformat].each do |path| - sh("swiftformat #{strict} --config .swiftformat #{path}") - next if path.include?('Tests') - - 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 + sh("swiftformat #{strict} --config .swiftformat .") + sh("swiftlint lint --config .swiftlint.yml --fix --progress --reporter json") unless strict + sh("swiftlint lint --config .swiftlint.yml --strict --progress --reporter json") end end @@ -545,8 +541,7 @@ lane :sources_matrix do ruby: ['fastlane', 'Gemfile', 'Gemfile.lock'], size: ['Sources', xcode_project], sonar: ['Sources'], - public_interface: ['Sources'], - swiftformat: ['Sources', 'DemoAppSwiftUI', 'StreamChatSwiftUITests'] + public_interface: ['Sources'] } end @@ -618,7 +613,7 @@ lane :update_img_shields_sdk_sizes do |options| ) end -def frameworks_sizes +private_lane :frameworks_sizes do root_dir = 'Build/SDKSize' archive_dir = "#{root_dir}/DemoApp.xcarchive" @@ -630,7 +625,9 @@ def frameworks_sizes scheme: 'DemoAppSwiftUI', archive_path: archive_dir, export_method: 'ad-hoc', - export_options: 'fastlane/sdk_size_export_options.plist' + export_options: 'fastlane/sdk_size_export_options.plist', + derived_data_path: derived_data_path, + cloned_source_packages_path: source_packages_path ) # Parse the thinned size of Assets.car from Packaging.log @@ -645,3 +642,20 @@ def frameworks_sizes { StreamChatSwiftUI: stream_chat_swiftui_size_kb.round(0) } end + +lane :size_analyze do + next unless is_check_required(sources: sources_matrix[:size], force_check: @force_check) + + gym( + scheme: 'DemoAppSwiftUI', + configuration: 'Release', + skip_archive: true, + skip_package_ipa: true, + skip_package_pkg: true, + skip_codesigning: true, + derived_data_path: derived_data_path, + cloned_source_packages_path: source_packages_path + ) + + show_detailed_sdk_size(sdk_names: sdk_names, threshold: 42) +end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 37b60ac84..772a05733 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -5,4 +5,5 @@ gem 'fastlane-plugin-versioning' gem 'fastlane-plugin-sonarcloud_metric_kit' gem 'fastlane-plugin-create_xcframework' -gem 'fastlane-plugin-stream_actions', '0.3.90' +gem 'fastlane-plugin-stream_actions', '0.3.101' +gem 'fastlane-plugin-xcsize', '1.1.0' diff --git a/lefthook.yml b/lefthook.yml index 1320df25c..61221d999 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,10 +4,6 @@ pre-commit: - run: swiftlint lint --config .swiftlint.yml --fix --progress --reporter json {staged_files} glob: "*.{swift}" stage_fixed: true - exclude: - - Sources/StreamChatSwiftUI/Generated/** - - Sources/StreamChatSwiftUI/StreamNuke/** - - Sources/StreamChatSwiftUI/StreamSwiftyGif/** skip: - merge - rebase @@ -15,10 +11,6 @@ pre-commit: - run: swiftformat --config .swiftformat {staged_files} glob: "*.{swift}" stage_fixed: true - exclude: - - Sources/StreamChatSwiftUI/Generated/** - - Sources/StreamChatSwiftUI/StreamNuke/** - - Sources/StreamChatSwiftUI/StreamSwiftyGif/** skip: - merge - rebase @@ -27,9 +19,5 @@ pre-push: jobs: - run: swiftlint lint --config .swiftlint.yml --strict --progress --reporter json {push_files} glob: "*.{swift}" - exclude: - - Sources/StreamChatSwiftUI/Generated/** - - Sources/StreamChatSwiftUI/StreamNuke/** - - Sources/StreamChatSwiftUI/StreamSwiftyGif/** skip: - merge-commit