From be8793148e879cacb48c16f5c5104886371d8e86 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 8 Oct 2025 08:10:56 +0000 Subject: [PATCH 01/16] Update release version to snapshot --- .../StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index b97f1f3b..8f174aa1 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-SNAPSHOT" } From 2b12759aca3831d9c3bfeda7a63713ba9a6af606 Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 8 Oct 2025 13:25:28 +0300 Subject: [PATCH 02/16] Adjust linter rules and linted paths (#1002) * Remove trailing_whitespace from SwiftLint rules due to multi-line string issues * Lint everything * Rename deprecated rule --------- Co-authored-by: Alexey Alter-Pesotskiy --- .swiftformat | 4 +- .swiftlint.yml | 5 +- Scripts/GenerateSPMFileLists.swift | 4 +- StreamChatSwiftUITestsApp/AppDelegate.swift | 20 +- .../CustomChannelHeader.swift | 2 - .../InternetConnectionMonitor_Mock.swift | 6 +- StreamChatSwiftUITestsApp/StartPage.swift | 8 +- .../StreamChatSwiftUITestsAppApp.swift | 2 - .../StreamChatWrapper.swift | 12 +- .../UserCredentials.swift | 14 +- .../Pages/ChannelListPage.swift | 4 +- .../Pages/MessageListPage.swift | 16 +- .../Pages/SpringBoard.swift | 6 +- .../Pages/ThreadPage.swift | 2 - .../Robots/UserRobot+Asserts.swift | 311 ++++++++++-------- .../Robots/UserRobot.swift | 56 ++-- .../StreamChatSwiftUITests.swift | 2 +- .../Tests/Attachments_Tests.swift | 8 +- .../Tests/Base TestCase/StreamTestCase.swift | 2 - .../Tests/ChannelList_Tests.swift | 2 - .../Tests/Ephemeral_Messages_Tests.swift | 1 - ...sageDeliveryStatus+ChannelList_Tests.swift | 3 - .../MessageDeliveryStatus_Tests.swift | 6 +- .../Tests/MessageList_Tests.swift | 6 +- .../Tests/PushNotification_Tests.swift | 29 +- .../Tests/QuotedReply_Tests.swift | 13 +- .../Tests/Reactions_Tests.swift | 1 - .../Tests/SlowMode_Tests.swift | 1 - .../Tests/StreamTestCase+Tags.swift | 2 +- fastlane/Fastfile | 13 +- lefthook.yml | 12 - 31 files changed, 293 insertions(+), 280 deletions(-) diff --git a/.swiftformat b/.swiftformat index 09b4ef18..d07296f9 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 6a5da8f1..4020346b 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/Scripts/GenerateSPMFileLists.swift b/Scripts/GenerateSPMFileLists.swift index e49785bc..2debc37b 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/StreamChatSwiftUITestsApp/AppDelegate.swift b/StreamChatSwiftUITestsApp/AppDelegate.swift index d3d6a560..7a9bfa7c 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 a3d98f60..c3269c9d 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 ec1e8026..cb5a87b6 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 280b967c..69e61f7d 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 0b6fc9e6..0fccfeff 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 a18853d6..78c4e217 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 df293db5..64c21621 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 f926db28..4cf37f9b 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 314d04d4..1eac12e4 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 ea756894..e94aecf4 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 9a6daecb..eca7e35b 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 637cdb19..3ab1f0b8 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 9f2d112f..f03af913 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 33dbeb3f..ed36a693 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 b125e52d..05fd667a 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 c6bcd449..17a77ff5 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 b4cd7f10..0f2bae59 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 453a8cc9..aa74183b 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 af04a93f..121936b1 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 acf04b42..e33ae725 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 821986de..8ee85d95 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 ae823906..65ca6f65 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 fb935677..44833c43 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 b97de225..df942297 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 132efdf7..8884d9b5 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 55d641b8..2a289319 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 da6f0de5..4b637fc2 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 diff --git a/lefthook.yml b/lefthook.yml index 1320df25..61221d99 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 From fd63af17961829ad3be3eeca59abc515837627cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 11:58:36 +0100 Subject: [PATCH 03/16] Bump rack from 3.2.0 to 3.2.2 (#1003) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 622ff78c..3d620371 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -316,7 +316,7 @@ GEM puma (6.6.1) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.0) + rack (3.2.2) rack-protection (4.1.1) base64 (>= 0.1.0) logger (>= 1.6.0) From 2b342de9b425c357ac3252b6be97925d3175bb58 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Oct 2025 07:06:36 +0100 Subject: [PATCH 04/16] [CI] Implement detailed size metrics (#1008) --- .github/workflows/sdk-size-metrics.yml | 8 +++++-- .gitignore | 1 + Gemfile.lock | 9 ++++++-- StreamChatSwiftUI.xcodeproj/project.pbxproj | 6 ++++++ fastlane/Fastfile | 23 +++++++++++++++++++-- fastlane/Pluginfile | 3 ++- 6 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml index a05407b1..e71245a9 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 45ae8d6e..06ee34d3 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/Gemfile.lock b/Gemfile.lock index 3d620371..81a35f6c 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.100) 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) @@ -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.100) fastlane-plugin-versioning + fastlane-plugin-xcsize (= 1.1.0) json lefthook plist diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index a87a019f..cedd29ef 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", diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4b637fc2..5866b6bd 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -613,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" @@ -625,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 @@ -640,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 37b60ac8..a6bc5621 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.100' +gem 'fastlane-plugin-xcsize', '1.1.0' From badf0218ff5b110d123c00877546d1d75821105f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:11:17 +0300 Subject: [PATCH 05/16] Bump sinatra from 4.1.1 to 4.2.0 (#1010) Bumps [sinatra](https://github.com/sinatra/sinatra) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/sinatra/sinatra/blob/main/CHANGELOG.md) - [Commits](https://github.com/sinatra/sinatra/compare/v4.1.1...v4.2.0) --- updated-dependencies: - dependency-name: sinatra dependency-version: 4.2.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 81a35f6c..398646b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -285,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) @@ -318,8 +318,8 @@ GEM puma (6.6.1) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.2) - 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) @@ -374,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) From dbe21bc06abbffd15993ab55e95d00454d98af8c Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Oct 2025 11:34:15 +0100 Subject: [PATCH 06/16] [CI] Bump fastlane plugin version to avoid PR comments when there are no size changes (#1011) --- Gemfile.lock | 4 ++-- fastlane/Pluginfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 398646b1..300196b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,7 +208,7 @@ GEM fastlane pry fastlane-plugin-sonarcloud_metric_kit (0.2.1) - fastlane-plugin-stream_actions (0.3.100) + fastlane-plugin-stream_actions (0.3.101) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.7.1) fastlane-plugin-xcsize (1.1.0) @@ -430,7 +430,7 @@ DEPENDENCIES fastlane-plugin-create_xcframework fastlane-plugin-lizard fastlane-plugin-sonarcloud_metric_kit - fastlane-plugin-stream_actions (= 0.3.100) + fastlane-plugin-stream_actions (= 0.3.101) fastlane-plugin-versioning fastlane-plugin-xcsize (= 1.1.0) json diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index a6bc5621..772a0573 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -5,5 +5,5 @@ gem 'fastlane-plugin-versioning' gem 'fastlane-plugin-sonarcloud_metric_kit' gem 'fastlane-plugin-create_xcframework' -gem 'fastlane-plugin-stream_actions', '0.3.100' +gem 'fastlane-plugin-stream_actions', '0.3.101' gem 'fastlane-plugin-xcsize', '1.1.0' From 1af66ba2bac9f9703b0022681385898644c99800 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Mon, 13 Oct 2025 16:59:31 +0100 Subject: [PATCH 07/16] [CI] Update docc implementation condition in Package.swift (#1012) --- .spi.yml | 1 - Package.swift | 2 -- 2 files changed, 3 deletions(-) diff --git a/.spi.yml b/.spi.yml index c5dc1a7c..c9a92ca1 100644 --- a/.spi.yml +++ b/.spi.yml @@ -4,4 +4,3 @@ builder: - platform: ios documentation_targets: [StreamChatSwiftUI] scheme: StreamChatSwiftUI - swift_version: 5.9 diff --git a/Package.swift b/Package.swift index 0e96f688..472e6295 100644 --- a/Package.swift +++ b/Package.swift @@ -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 From 222f93dcf44257853ced15fce3fc2a09faefc26d Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Fri, 17 Oct 2025 11:38:14 +0100 Subject: [PATCH 08/16] Fix composer not being locked after the channel was frozen (#1015) --- CHANGELOG.md | 3 + .../Composer/AttachmentPickerTypeView.swift | 22 +++-- .../Composer/MessageComposerView.swift | 12 ++- .../Composer/MessageComposerViewModel.swift | 5 + .../Composer/TrailingComposerView.swift | 4 +- .../StreamChatSwiftUI/Generated/L10n.swift | 2 + .../Resources/en.lproj/Localizable.strings | 1 + .../ChatChannel/ChatChannelTestHelpers.swift | 2 +- .../MessageComposerView_Tests.swift | 93 +++++++++++++++++- ...test_composerInputView_frozenChannel.1.png | Bin 0 -> 21051 bytes ...st_leadingComposerView_frozenChannel.1.png | Bin 0 -> 3918 bytes ...st_messageComposerView_frozenChannel.1.png | Bin 0 -> 21096 bytes ...t_trailingComposerView_frozenChannel.1.png | Bin 0 -> 4175 bytes 13 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_composerInputView_frozenChannel.1.png create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_leadingComposerView_frozenChannel.1.png create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_messageComposerView_frozenChannel.1.png create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/__Snapshots__/MessageComposerView_Tests/test_trailingComposerView_frozenChannel.1.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 139f0c9d..3389424a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### 🐞 Fixed +- Fix composer not being locked after the channel was frozen [#1015](https://github.com/GetStream/stream-chat-swiftui/pull/1015) + ### 🔄 Changed # [4.90.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.90.0) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift index 9e0c0994..a021e153 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 eb598176..1d5a6add 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift @@ -375,8 +375,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 +435,12 @@ public struct ComposerInputView: View, KeyboardReadable { private var isInCooldown: Bool { cooldownDuration > 0 } + + private var isChannelFrozen: Bool { + !viewModel.isSendMessageEnabled + } + + private var isInputDisabled: Bool { + isInCooldown || isChannelFrozen + } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift index 8c2afd6c..48e1b105 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 817f3171..7bd1621f 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/Generated/L10n.swift b/Sources/StreamChatSwiftUI/Generated/L10n.swift index 79a7dddf..9c4dcf11 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/Resources/en.lproj/Localizable.strings b/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings index 7ac2913f..619e24b3 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/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelTestHelpers.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelTestHelpers.swift index 8b818304..05926d9a 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 905da904..7fe459f5 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( 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 0000000000000000000000000000000000000000..c0474044c10a63ccbaf45f6d2f95f577c5d4aaf5 GIT binary patch literal 21051 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
    %#pT!nU=lT**S3)=V!_UDn+ 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..86c39cbb38848c22e7be3fa44948c7c588d56e97 GIT binary patch literal 3918 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 Dk{T)1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9f366ec2d3fac492e3d6cecbc78c06bb0fc1f00f GIT binary patch literal 21096 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 Date: Fri, 17 Oct 2025 21:41:30 +0300 Subject: [PATCH 09/16] Fix messages not appearing when presenting ChatChannelView with UIKit on iOS 26.1 (#1007) --- .../ChatChannel/ChatChannelView.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index e01c6bb0..5a6e50b3 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift @@ -183,13 +183,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 @@ -221,9 +214,3 @@ public struct ChatChannelView: View, KeyboardReadable { return bottomPadding } } - -extension PresentationMode: Equatable { - public static func == (lhs: PresentationMode, rhs: PresentationMode) -> Bool { - lhs.isPresented == rhs.isPresented - } -} From 47d57c16458f74e824ddb4a2ea97e6ccc23b7631 Mon Sep 17 00:00:00 2001 From: "Sven A. Schmidt" Date: Mon, 20 Oct 2025 12:08:02 +0200 Subject: [PATCH 10/16] Pin doc generation to Swift 6.1 (#1019) --- .spi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.spi.yml b/.spi.yml index c9a92ca1..201ffcb2 100644 --- a/.spi.yml +++ b/.spi.yml @@ -4,3 +4,4 @@ builder: - platform: ios documentation_targets: [StreamChatSwiftUI] scheme: StreamChatSwiftUI + swift_version: '6.1' From a0b9f51fcb92ec294fc54eee07cf607f56e96ddd Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 21 Oct 2025 11:06:03 +0100 Subject: [PATCH 11/16] [Feature] Add the `makeAttachmentTextView` method to ViewFactory (#1023) --- CHANGELOG.md | 3 +++ .../VoiceRecordingContainerView.swift | 2 +- .../MessageList/ImageAttachmentView.swift | 10 ++++++---- .../ChatChannel/MessageList/LinkAttachmentView.swift | 2 +- .../ChatChannel/MessageList/MessageView.swift | 12 +++++++++++- .../MessageList/VideoAttachmentView.swift | 2 +- Sources/StreamChatSwiftUI/DefaultViewFactory.swift | 6 ++++++ Sources/StreamChatSwiftUI/ViewFactory.swift | 8 ++++++++ .../Tests/Utils/ViewFactory_Tests.swift | 11 +++++++++++ 9 files changed, 48 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3389424a..6135f62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### ✅ Added +- Add the `makeAttachmentTextView` method to ViewFactory [#1013](https://github.com/GetStream/stream-chat-swiftui/pull/1013) + ### 🐞 Fixed - Fix composer not being locked after the channel was frozen [#1015](https://github.com/GetStream/stream-chat-swiftui/pull/1015) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AsyncVoiceMessages/VoiceRecordingContainerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/AsyncVoiceMessages/VoiceRecordingContainerView.swift index 5c4854ac..032941ed 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 7a0aa781..3ebeb57e 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 47208901..2e1dab23 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/MessageView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageView.swift index b1c32f09..433b4873 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/VideoAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/VideoAttachmentView.swift index 7cc4e217..ba458830 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/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index b238a265..3d881999 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/ViewFactory.swift b/Sources/StreamChatSwiftUI/ViewFactory.swift index 9c746573..0439b8d1 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/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift b/StreamChatSwiftUITests/Tests/Utils/ViewFactory_Tests.swift index a2974a46..0b4e693f 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 { From 07d4e0e7e723ab7b20718ff7eb31d95af5640954 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Tue, 21 Oct 2025 16:19:14 +0100 Subject: [PATCH 12/16] Allow dismissing commands overlay and attachments picker on message list tap (#1024) * Hide commands overlay when keyboard disappears * Allow hiding keyboard attachments picker when tapping the message list * Update CHANGELOG.md * Use the same implementation strategy for the commands overlay * Rename the notifications names to be more consistent with the rest of the codebase * Allow the attachments picker by default as well * Add test coverage to the view --- CHANGELOG.md | 5 +- .../ChatChannel/ChatChannelView.swift | 16 +- .../Composer/MessageComposerView.swift | 23 +++ .../MessageList/MessageListConfig.swift | 16 +- .../MessageList/MessageListView.swift | 1 - .../Utils/KeyboardHandling.swift | 2 +- .../TestTools/ViewFrameUtils.swift | 11 ++ .../MessageComposerView_Tests.swift | 160 ++++++++++++++++++ 8 files changed, 227 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6135f62e..3287dc09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### ✅ 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) -### 🔄 Changed - # [4.90.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.90.0) _October 08, 2025_ diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift index 5a6e50b3..47ff4277 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, @@ -213,4 +218,13 @@ public struct ChatChannelView: View, KeyboardReadable { let bottomPadding = topVC()?.view.safeAreaInsets.bottom ?? 0 return bottomPadding } + + private func hideComposerCommandsAndAttachmentsPicker() { + NotificationCenter.default.post( + name: .attachmentPickerHiddenNotification, object: nil + ) + NotificationCenter.default.post( + name: .commandsOverlayHiddenNotification, object: nil + ) + } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift index 1d5a6add..c4712a21 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) } } @@ -444,3 +457,13 @@ public struct ComposerInputView: View, KeyboardReadable { 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/MessageList/MessageListConfig.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift index e0dba4bc..b03d8550 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 0696eeb7..2bfe8a96 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/Utils/KeyboardHandling.swift b/Sources/StreamChatSwiftUI/Utils/KeyboardHandling.swift index ec483dbf..5a21e9dc 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/StreamChatSwiftUITests/Infrastructure/TestTools/ViewFrameUtils.swift b/StreamChatSwiftUITests/Infrastructure/TestTools/ViewFrameUtils.swift index c9186bbe..77a7130d 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/MessageComposerView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift index 7fe459f5..34663602 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift @@ -894,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 { From 8903e7786c7490d430f8e1c9675b6eb13301d9a4 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Wed, 22 Oct 2025 13:16:58 +0100 Subject: [PATCH 13/16] Fix `PollOptionAllVotesView` not updated on poll cast events (#1025) * Fix PollOptionAllVotesView not updated on poll events * Update CHANGELOG.md --- CHANGELOG.md | 1 + .../Polls/PollOptionAllVotesViewModel.swift | 10 +++++++--- StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3287dc09..040bfcba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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) # [4.90.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.90.0) _October 08, 2025_ diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollOptionAllVotesViewModel.swift index aeba9bf4..fb85a6a5 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/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index cedd29ef..b31d6525 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3945,8 +3945,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.90.0; + branch = develop; + kind = branch; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { From 1a93343a47c5e9d99be231ccf3ca7f3f51a35266 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Wed, 22 Oct 2025 17:09:34 +0100 Subject: [PATCH 14/16] Fix action sheet not showing when discarding Poll creation on iOS 26 (#1027) * Fix action sheet not showing when discarding Poll creation on iOS 26 * Update CHANGELOG.md --- CHANGELOG.md | 1 + .../ChatChannel/Polls/CreatePollView.swift | 22 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 040bfcba..812865e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🐞 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/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Polls/CreatePollView.swift index be7a180e..8064166d 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 } From 3d63d97c1c80b348951b6ed7807d70417bee0a4e Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Wed, 22 Oct 2025 17:13:08 +0100 Subject: [PATCH 15/16] Update StreamChat dependency to 4.91.0 (#1026) --- Package.swift | 2 +- StreamChatSwiftUI-XCFramework.podspec | 2 +- StreamChatSwiftUI.podspec | 2 +- StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 472e6295..aa0f4fef 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( diff --git a/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index 75dfae9b..623e5c3c 100644 --- a/StreamChatSwiftUI-XCFramework.podspec +++ b/StreamChatSwiftUI-XCFramework.podspec @@ -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 0d1ce08e..87bfc22a 100644 --- a/StreamChatSwiftUI.podspec +++ b/StreamChatSwiftUI.podspec @@ -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 b31d6525..904fe0c4 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -3945,8 +3945,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { - branch = develop; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 4.91.0; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { From 5aa483aa9ba7c4d566086bf57ba9e2cdd896b817 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 22 Oct 2025 16:34:15 +0000 Subject: [PATCH 16/16] Bump 4.91.0 --- CHANGELOG.md | 5 +++++ README.md | 2 +- .../Generated/SystemEnvironment+Version.swift | 2 +- Sources/StreamChatSwiftUI/Info.plist | 2 +- StreamChatSwiftUI-XCFramework.podspec | 2 +- StreamChatSwiftUI.podspec | 2 +- StreamChatSwiftUIArtifacts.json | 2 +- 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812865e0..5512f5ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### 🔄 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) diff --git a/README.md b/README.md index de473ce8..8f326406 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

    - StreamChatSwiftUI + StreamChatSwiftUI

    ## SwiftUI StreamChat SDK diff --git a/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift b/Sources/StreamChatSwiftUI/Generated/SystemEnvironment+Version.swift index 8f174aa1..a5c3c79c 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.91.0-SNAPSHOT" + public static let version: String = "4.91.0" } diff --git a/Sources/StreamChatSwiftUI/Info.plist b/Sources/StreamChatSwiftUI/Info.plist index eca9a0a5..268afdcc 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/StreamChatSwiftUI-XCFramework.podspec b/StreamChatSwiftUI-XCFramework.podspec index 623e5c3c..6d88ce8d 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.' diff --git a/StreamChatSwiftUI.podspec b/StreamChatSwiftUI.podspec index 87bfc22a..e8cd2f44 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.' diff --git a/StreamChatSwiftUIArtifacts.json b/StreamChatSwiftUIArtifacts.json index 212e8b2e..84875efd 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