diff --git a/.github/workflows/full-checks.yml b/.github/workflows/full-checks.yml index e26567227c3..8b1f1ad4d06 100644 --- a/.github/workflows/full-checks.yml +++ b/.github/workflows/full-checks.yml @@ -101,30 +101,6 @@ jobs: - name: Run Stress Tests - Latest iOS (Release) run: bundle exec fastlane stress_test_release device:"iPhone 12" - stress-tests: - name: Stress Test LLC - Latest iOS (Release) - runs-on: macos-11 - steps: - - uses: actions/checkout@v1 - - uses: ./.github/actions/set-build-image-var - - name: Cache RubyGems - uses: actions/cache@v2 - id: rubygem-cache - with: - path: vendor/bundle - key: ${{ runner.os }}-${{ env.ImageVersion }}-gem-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: ${{ runner.os }}-${{ env.ImageVersion }}-gem- - - name: Cache Mint - uses: actions/cache@v2 - id: mint-cache - with: - path: /usr/local/lib/mint - key: ${{ runner.os }}-mint-${{ hashFiles('./Mintfile') }} - restore-keys: ${{ runner.os }}-mint- - - uses: ./.github/actions/bootstrap - - name: Run Stress Tests - Latest iOS (Release) - run: bundle exec fastlane stress_test_release device:"iPhone 12" - stress-tests-ios13: name: Stress Test LLC - iOS 13.5 (Release) runs-on: macos-11 @@ -259,4 +235,4 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} MATRIX_CONTEXT: ${{ toJson(matrix) }} - if: ${{ github.event_name == 'push' && failure() }} \ No newline at end of file + if: ${{ github.event_name == 'push' && failure() }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 761664e6de8..9b35aeaa1bd 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -13,8 +13,34 @@ jobs: runs-on: macos-11 if: github.event.pull_request.merged == true # only merged pull requests must trigger this job steps: - - name: If the release branch contains the version, let's go! + - name: Install Bot SSH Key + uses: webfactory/ssh-agent@v0.4.1 + with: + ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + - uses: actions/checkout@v1 + - name: "Set build image var" + run: echo "ImageVersion=$ImageVersion" >> $GITHUB_ENV + - name: Cache RubyGems + uses: actions/cache@v2 + id: rubygem-cache + with: + path: vendor/bundle + key: ${{ runner.os }}-${{ env.ImageVersion }}-gem-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: ${{ runner.os }}-${{ env.ImageVersion }}-gem- + - name: Cache Mint + uses: actions/cache@v2 + id: mint-cache + with: + path: /usr/local/lib/mint + key: ${{ runner.os }}-mint-${{ hashFiles('./Mintfile') }} + restore-keys: ${{ runner.os }}-mint- + - name: Extract version from branch name (for release branches) if: startsWith(github.event.pull_request.head.ref, 'release/') - BRANCH_NAME="${{ github.event.pull_request.head.ref }}" - VERSION=${BRANCH_NAME#release/} - run: bundle exec fastlane publish_release version:$VERSION + run: | + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + VERSION=${BRANCH_NAME#release/} + echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV + - uses: ./.github/actions/bootstrap + - name: "Starting on the Fastlane Publish Release" + if: startsWith(github.event.pull_request.head.ref, 'release/') + run: bundle exec fastlane publish_release version:${{ env.RELEASE_VERSION }} diff --git a/.gitignore b/.gitignore index 1d983bd7825..22043247980 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,6 @@ docusaurus/.env # Ignore Products folder Products/ + +# Ignore Dependencies folder +Dependencies/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 00cfa2e19ad..00000000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "Dependencies/Nuke"] - path = Dependencies/Nuke - url = git@github.com:kean/Nuke.git -[submodule "Dependencies/SwiftyGif"] - path = Dependencies/SwiftyGif - url = git@github.com:kirualex/SwiftyGif.git -[submodule "Dependencies/Starscream"] - path = Dependencies/Starscream - url = git@github.com:daltoniam/Starscream.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 191627120c5..b955280e787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,24 +3,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). # Upcoming +### 🔄 Changed + +# [4.6.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.6.0) +_December 20, 2021_ + ### ⚠️ Important - Dependencies are no longer exposed (this includes Nuke, SwiftyGif and Starscream). If you were using those dependencies we were exposing, you would need to import them manually. This is due to our newest addition supporting Module Stable XCFrameworks, see more below in the "Added" section. ### 🔄 Changed - Change `ChatMessageLayoutOptions` to a `Set` instead of an `OptionSet` for a more flexible and safer customization [#1651](https://github.com/GetStream/stream-chat-swift/issues/1651) - There is a new `ChatMessageListDateSeparatorView` component that should be used instead of the `ChatMessageListScrollOverlayView` if the goal is customize the styling of the date separator. Read [here](https://getstream.io/chat/docs/sdk/ios/uikit/components/message/#date-separators) for more details. +- `UnknownEvent` is now deprecated, use `UnknownChannelEvent` or `UnknownUserEvent` instead. [#1695](https://github.com/GetStream/stream-chat-swift/pull/1695). +- SwiftyGif now points to [v5.4.2](https://github.com/kirualex/SwiftyGif/releases/tag/5.4.2) that resolves crash related to leaked delegate reference. ### 🐞 Fixed -- Fix `stopTyping` can be called on `TypingEventSender` after calling `startTyping` [#1649](https://github.com/GetStream/stream-chat-swift/issues/1649) -- Reactions no longer cover the text in message bubble [#1666](https://github.com/GetStream/stream-chat-swift/pull/1666) -- Fix `error` type messages rendered as user's messages and interactive [#1672](https://github.com/GetStream/stream-chat-swift/issues/1672) -- Fix `ChannelListController` makes one redundant API call [#1687](https://github.com/GetStream/stream-chat-swift/issues/1687) +- Fix `stopTyping` can be called on `TypingEventSender` after calling `startTyping` [#1649](https://github.com/GetStream/stream-chat-swift/issues/1649). +- Reactions no longer cover the text in message bubble [#1666](https://github.com/GetStream/stream-chat-swift/pull/1666). +- Fix `error` type messages rendered as user's messages and interactive [#1672](https://github.com/GetStream/stream-chat-swift/issues/1672). +- Fix `ChannelListController` makes one redundant API call [#1687](https://github.com/GetStream/stream-chat-swift/issues/1687). +- Safely access indexes of collections [#1692](https://github.com/GetStream/stream-chat-swift/pull/1692). ### ✅ Added -- Add support for pre-built XCFrameworks [#1665](https://github.com/GetStream/stream-chat-swift/pull/1665) -- Added `LogConfig.destinationTypes` for ease of adding new destinations to logger [#1681](https://github.com/GetStream/stream-chat-swift/issues/1681) -- Expose container embedding top & bottom containers by `ChatChannelListItemView` [#1670](https://github.com/GetStream/stream-chat-swift/issues/1670) -- Add Static Message List Date Separators [#1686](https://github.com/GetStream/stream-chat-swift/issues/1686) (You can read this [doc](https://getstream.io/chat/docs/sdk/ios/uikit/components/message/#date-separators) to understand how to configure this feature) +- Add support for pre-built XCFrameworks [#1665](https://github.com/GetStream/stream-chat-swift/pull/1665). +- Added `LogConfig.destinationTypes` for ease of adding new destinations to logger [#1681](https://github.com/GetStream/stream-chat-swift/issues/1681). +- Expose container embedding top & bottom containers by `ChatChannelListItemView` [#1670](https://github.com/GetStream/stream-chat-swift/issues/1670). +- Add Static Message List Date Separators [#1686](https://github.com/GetStream/stream-chat-swift/issues/1686) (You can read this [doc](https://getstream.io/chat/docs/sdk/ios/uikit/components/message/#date-separators) to understand how to configure this feature). +- Adds `UnknownUserEvent` that models custom user event [#1695](https://github.com/GetStream/stream-chat-swift/pull/1695). +- `ChannelQuery.options` and `ChannelListQuery.options` are now public and mutable [#1696](https://github.com/GetStream/stream-chat-swift/issues/1696) +- `ChannelController.startWatching` and `stopWatching` are now `public`. You can explicitly stop watching a channel [#1696](https://github.com/GetStream/stream-chat-swift/issues/1696). # [4.5.2](https://github.com/GetStream/stream-chat-swift/releases/tag/4.5.2) _December 10, 2021_ diff --git a/Dependencies/Nuke b/Dependencies/Nuke deleted file mode 160000 index c0b32b0d715..00000000000 --- a/Dependencies/Nuke +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c0b32b0d7154afceb3fc08b199492f125ff4267e diff --git a/Dependencies/Starscream b/Dependencies/Starscream deleted file mode 160000 index df8d82047f6..00000000000 --- a/Dependencies/Starscream +++ /dev/null @@ -1 +0,0 @@ -Subproject commit df8d82047f6654d8e4b655d1b1525c64e1059d21 diff --git a/Dependencies/SwiftyGif b/Dependencies/SwiftyGif deleted file mode 160000 index 652b344111a..00000000000 --- a/Dependencies/SwiftyGif +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 652b344111a363a53af602b9eef0b52ab1146b42 diff --git a/Makefile b/Makefile index 2ecd93b3f4f..150cfac1ca0 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ update_dependencies: echo "👉 Updating Starscream" make update_starscream version=4.0.4 echo "👉 Updating SwiftyGif" - make update_swiftygif version=5.4.1 + make update_swiftygif version=5.4.2 update_nuke: check_version_parameter ./Scripts/updateDependency.sh $(version) Dependencies/Nuke Sources/StreamChatUI/StreamNuke Sources diff --git a/Package.swift b/Package.swift index 1b3d176d42b..98d03042b71 100644 --- a/Package.swift +++ b/Package.swift @@ -87,15 +87,16 @@ var streamChatSourcesExcluded: [String] { [ "APIClient/HTTPHeader_Tests.swift", "APIClient/Endpoints/GuestEndpoints_Tests.swift", "APIClient/Endpoints/Payloads/CustomDataHashMap_Tests.swift", + "APIClient/Endpoints/Payloads/UnknownUserEvent_Tests.swift", "APIClient/Endpoints/Payloads/DevicePayloads_Tests.swift", "APIClient/Endpoints/Payloads/MessageAttachmentPayload_Tests.swift", "APIClient/Endpoints/Payloads/MutedChannelPayload_Tests.swift", "APIClient/Endpoints/Payloads/CurrentUserPayloads_Tests.swift", "APIClient/Endpoints/Payloads/FlagMessagePayload_Tests.swift", + "APIClient/Endpoints/Payloads/UnknownChannelEvent_Tests.swift", "APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift", "APIClient/Endpoints/Payloads/FlagUserPayload_Tests.swift", "APIClient/Endpoints/Payloads/MessagePayloads_Tests.swift", - "APIClient/Endpoints/Payloads/UnknownEvent_Tests.swift", "APIClient/Endpoints/Payloads/MissingEventsPayload_Tests.swift", "APIClient/Endpoints/Payloads/FileUploadPayload_Tests.swift", "APIClient/Endpoints/Payloads/UserPayloads_Tests.swift", @@ -773,6 +774,7 @@ var streamChatUIFilesExcluded: [String] { [ "Utils/UIViewController+Extensions_Tests.swift", "Utils/ChatChannelNamer_Tests.swift", "Utils/ComponentsProvider_Tests.swift", + "Utils/Array+SafeSubscript_Tests.swift", "Utils/AppearanceProvider_Tests.swift", "Utils/ImageCDN_Tests.swift", "Utils/DateUtils_Tests.swift", diff --git a/Scripts/updateDependency.sh b/Scripts/updateDependency.sh index 4e4d76eda07..36f774fd623 100755 --- a/Scripts/updateDependency.sh +++ b/Scripts/updateDependency.sh @@ -18,7 +18,25 @@ dependency_directory=$2 output_directory=$3 sources_directory=$4 -git submodule update --init +dependency_url="" + +# Nuke +if [[ $dependency_directory == *"Nuke"* ]]; then + dependency_url="git@github.com:kean/Nuke.git" +elif [[ $dependency_directory == *"SwiftyGif"* ]]; then + dependency_url="git@github.com:kirualex/SwiftyGif.git" +elif [[ $dependency_directory == *"Starscream"* ]]; then + dependency_url="git@github.com:daltoniam/Starscream.git" +else + echo "→ Unknown dependency at $dependency_directory" + exit 1 +fi + +if ! [[ -d "$dependency_directory" ]]; then + echo "→ $dependency_directory does not exist in your filesystem. Cloning the repo" + git clone $dependency_url $dependency_directory +fi + cd $dependency_directory ensure_clean_git @@ -30,6 +48,7 @@ ensure_clean_git cd - +echo "→ Copying source files" rm -rf $output_directory mkdir $output_directory cp -r "$dependency_directory/$sources_directory/." $output_directory @@ -40,3 +59,5 @@ do echo "→ Removing $f" rm $f done + +rm -rf $dependency_directory diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownEvent.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownChannelEvent.swift similarity index 86% rename from Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownEvent.swift rename to Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownChannelEvent.swift index f7562edb961..312e5194330 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownEvent.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownChannelEvent.swift @@ -6,7 +6,7 @@ import Foundation /// An event type SDK fallbacks to if incoming event was failed to be /// decoded as a system event. -public struct UnknownEvent: Event, Hashable { +public struct UnknownChannelEvent: Event, Hashable { /// An event type. public let type: EventType @@ -19,13 +19,13 @@ public struct UnknownEvent: Event, Hashable { /// An event creation date. public let createdAt: Date - /// A dictionary with custom fields. - let payload: [String: RawJSON] + /// A dictionary containing the entire event JSON. + public let payload: [String: RawJSON] } // MARK: - Decodable -extension UnknownEvent: Decodable { +extension UnknownChannelEvent: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: EventPayload.CodingKeys.self) @@ -37,14 +37,13 @@ extension UnknownEvent: Decodable { payload: try decoder .singleValueContainer() .decode([String: RawJSON].self) - .removingValues(forKeys: EventPayload.CodingKeys.allCases.map(\.rawValue)) ) } } // MARK: - Payload -public extension UnknownEvent { +public extension UnknownChannelEvent { /// Decodes a payload of the given type from the event. /// /// - Parameter ofType: The type of payload the custom fields should be treated as. diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownEvent_Tests.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownChannelEvent_Tests.swift similarity index 90% rename from Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownEvent_Tests.swift rename to Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownChannelEvent_Tests.swift index e61a004b64b..b9f02b25219 100644 --- a/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownEvent_Tests.swift +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownChannelEvent_Tests.swift @@ -5,7 +5,7 @@ @testable import StreamChat import XCTest -final class UnknownEvent_Tests: XCTestCase { +final class UnknownChannelEvent_Tests: XCTestCase { func test_unkownEvent_decoding() throws { // Create event fields let userId: UserId = .unique @@ -40,16 +40,17 @@ final class UnknownEvent_Tests: XCTestCase { "idea" : "\(ideaPayload.idea)" } """.data(using: .utf8)! - + // Decode unkown event from JSON. - let unknownEvent = try JSONDecoder.default.decode(UnknownEvent.self, from: json) + let unknownEvent = try JSONDecoder.default.decode(UnknownChannelEvent.self, from: json) + let payload = try JSONDecoder.default.decode([String: RawJSON].self, from: json) // Assert all fields are correct. XCTAssertEqual(unknownEvent.cid, cid) XCTAssertEqual(unknownEvent.userId, userId) XCTAssertEqual(unknownEvent.createdAt, createdAt.toDate()) XCTAssertEqual(unknownEvent.type, IdeaEventPayload.eventType) - XCTAssertEqual(unknownEvent.payload, ["idea": .string(ideaPayload.idea)]) + XCTAssertEqual(unknownEvent.payload, payload) } func test_whenAllFieldsArePresentedAndTypeMatches_customPayloadIsDecoded() throws { @@ -57,7 +58,7 @@ final class UnknownEvent_Tests: XCTestCase { let payload = IdeaEventPayload.unique // Create event with `IdeaEventPayload` payload. - let unkownEvent = UnknownEvent( + let unkownEvent = UnknownChannelEvent( type: IdeaEventPayload.eventType, cid: .unique, userId: .unique, @@ -71,7 +72,7 @@ final class UnknownEvent_Tests: XCTestCase { func test_whenFieldsAreMissing_customPayloadIsNotDecoded() throws { // Create event with `IdeaEventPayload` fields missing. - let unkownEvent = UnknownEvent( + let unkownEvent = UnknownChannelEvent( type: IdeaEventPayload.eventType, cid: .unique, userId: .unique, @@ -88,7 +89,7 @@ final class UnknownEvent_Tests: XCTestCase { let randomEventType = EventType(rawValue: .unique) // Create event with `IdeaEventPayload` fields missing. - let unkownEvent = UnknownEvent( + let unkownEvent = UnknownChannelEvent( type: randomEventType, cid: .unique, userId: .unique, diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownUserEvent.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownUserEvent.swift new file mode 100644 index 00000000000..e72b1308066 --- /dev/null +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownUserEvent.swift @@ -0,0 +1,54 @@ +// +// Copyright © 2021 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// An event type SDK fallbacks to if incoming event was failed to be +/// decoded as a system event. +public struct UnknownUserEvent: Event, Hashable { + /// An event type. + public let type: EventType + + /// A user the event is triggered for. + public let userId: UserId + + /// An event creation date. + public let createdAt: Date + + /// A dictionary containing the entire event JSON. + public let payload: [String: RawJSON] +} + +extension UnknownUserEvent: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: EventPayload.CodingKeys.self) + + self.init( + type: try container.decode(EventType.self, forKey: .eventType), + userId: try container.decode(UserPayload.self, forKey: .user).id, + createdAt: try container.decode(Date.self, forKey: .createdAt), + payload: try decoder + .singleValueContainer() + .decode([String: RawJSON].self) + ) + } +} + +// MARK: - Payload + +public extension UnknownUserEvent { + /// Decodes a payload of the given type from the event. + /// + /// - Parameter ofType: The type of payload the custom fields should be treated as. + /// - Returns: A payload of the given type if decoding succeeds and if event type matches the one declared in custom payload type. Otherwise `nil` is returned. + func payload(ofType: T.Type) -> T? { + guard + T.eventType == type, + let payloadData = try? JSONEncoder.default.encode(payload), + let payload = try? JSONDecoder.default.decode(T.self, from: payloadData) + else { return nil } + + return payload + } +} diff --git a/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownUserEvent_Tests.swift b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownUserEvent_Tests.swift new file mode 100644 index 00000000000..3cb9eaac631 --- /dev/null +++ b/Sources/StreamChat/APIClient/Endpoints/Payloads/UnknownUserEvent_Tests.swift @@ -0,0 +1,95 @@ +// +// Copyright © 2021 Stream.io Inc. All rights reserved. +// + +@testable import StreamChat +import XCTest + +final class UnknownUserEvent_Tests: XCTestCase { + func test_unkownEvent_decoding() throws { + // Create event fields + let userId: UserId = .unique + let ideaPayload: IdeaEventPayload = .unique + let createdAt: String = "2020-07-16T15:38:10.289007Z" + + // Create event JSON + let json = """ + { + "user" : { + "id" : "\(userId)", + "banned" : false, + "unread_channels" : 0, + "totalUnreadCount" : 0, + "created_at" : "2019-12-12T15:33:46.488935Z", + "invisible" : false, + "unreadChannels" : 0, + "unread_count" : 0, + "image" : "https://getstream.io/random_svg/?id=broken-waterfall-5&name=Broken+waterfall", + "updated_at" : "2020-07-16T15:38:10.289007Z", + "role" : "user", + "total_unread_count" : 0, + "online" : true, + "name" : "broken-waterfall-5" + }, + "created_at" : "\(createdAt)", + "type" : "\(IdeaEventPayload.eventType.rawValue)", + "idea" : "\(ideaPayload.idea)" + } + """.data(using: .utf8)! + + // Decode unkown event from JSON. + let unknownEvent = try JSONDecoder.default.decode(UnknownUserEvent.self, from: json) + let payload = try JSONDecoder.default.decode([String: RawJSON].self, from: json) + + // Assert all fields are correct. + XCTAssertEqual(unknownEvent.userId, userId) + XCTAssertEqual(unknownEvent.createdAt, createdAt.toDate()) + XCTAssertEqual(unknownEvent.type, IdeaEventPayload.eventType) + XCTAssertEqual(unknownEvent.payload, payload) + } + + func test_whenAllFieldsArePresentedAndTypeMatches_customPayloadIsDecoded() throws { + // Create custom event payload. + let payload = IdeaEventPayload.unique + + // Create event with `IdeaEventPayload` payload. + let unkownEvent = UnknownUserEvent( + type: IdeaEventPayload.eventType, + userId: .unique, + createdAt: .unique, + payload: ["idea": .string(payload.idea)] + ) + + // Assert payload is decoded. + XCTAssertEqual(unkownEvent.payload(ofType: IdeaEventPayload.self), payload) + } + + func test_whenFieldsAreMissing_customPayloadIsNotDecoded() throws { + // Create event with `IdeaEventPayload` fields missing. + let unkownEvent = UnknownUserEvent( + type: IdeaEventPayload.eventType, + userId: .unique, + createdAt: .unique, + payload: [:] + ) + + // Assert payload is not decoded because fields are missing. + XCTAssertNil(unkownEvent.payload(ofType: IdeaEventPayload.self)) + } + + func test_whenAllFieldsArePresentedButTypeDoesNotMatch_customPayloadIsNotDecoded() throws { + // Create random event type. + let randomEventType = EventType(rawValue: .unique) + + // Create event with `IdeaEventPayload` fields missing. + let unkownEvent = UnknownUserEvent( + type: randomEventType, + userId: .unique, + createdAt: .unique, + payload: ["idea": .string(.unique)] + ) + + // Assert payload is not decoded because the type does not match. + XCTAssertNil(unkownEvent.payload(ofType: IdeaEventPayload.self)) + } +} diff --git a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift index 07737b119ef..0e726ea979d 100644 --- a/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift +++ b/Sources/StreamChat/Controllers/ChannelController/ChannelController.swift @@ -1135,11 +1135,9 @@ public extension ChatChannelController { /// /// Please check [documentation](https://getstream.io/chat/docs/android/watch_channel/?language=swift) for more information. /// - /// We keep these functions internal since we're not sure how we should interface this behavior. - /// If you have suggestions, please open a ticket or send us an email at support@getstream.io /// /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - internal func startWatching(completion: ((Error?) -> Void)? = nil) { + func startWatching(completion: ((Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) @@ -1165,11 +1163,11 @@ public extension ChatChannelController { /// /// Please check [documentation](https://getstream.io/chat/docs/android/watch_channel/?language=swift) for more information. /// - /// We keep these functions internal since we're not sure how we should interface this behavior. - /// If you have suggestions, please open a ticket or send us an email at support@getstream.io + /// - Warning: If you're using `ChannelListController`, calling this function can disrupt `ChannelListController`'s functions, + /// such as updating channel data. /// /// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails. - internal func stopWatching(completion: ((Error?) -> Void)? = nil) { + func stopWatching(completion: ((Error?) -> Void)? = nil) { /// Perform action only if channel is already created on backend side and have a valid `cid`. guard let cid = cid, isChannelAlreadyCreated else { channelModificationFailed(completion) diff --git a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift index f5cb3dc2b78..b3182741022 100644 --- a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift +++ b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController.swift @@ -84,7 +84,7 @@ public class ChannelEventsController: EventsController { guard let cid = cid else { return false } let channelEvent = event as? ChannelSpecificEvent - let unknownEvent = event as? UnknownEvent + let unknownEvent = event as? UnknownChannelEvent return channelEvent?.cid == cid || unknownEvent?.cid == cid } diff --git a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController_Tests.swift b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController_Tests.swift index 6e52013e04f..1aaa606eb28 100644 --- a/Sources/StreamChat/Controllers/EventsController/ChannelEventsController_Tests.swift +++ b/Sources/StreamChat/Controllers/EventsController/ChannelEventsController_Tests.swift @@ -239,7 +239,7 @@ final class ChannelEventsController_Tests: XCTestCase { let currentChannelEvent = try ChannelUpdatedEventDTO(from: eventPayload) .toDomainEvent(session: database.viewContext) as! ChannelUpdatedEvent - let currentChannelCustomEvent = UnknownEvent( + let currentChannelCustomEvent = UnknownChannelEvent( type: .init(rawValue: .unique), cid: cid, userId: .unique, @@ -263,7 +263,7 @@ final class ChannelEventsController_Tests: XCTestCase { AssertAsync { Assert.willBeEqual(delegate.events.count, 2) Assert.willBeTrue(delegate.events.first is ChannelUpdatedEvent) - Assert.willBeTrue(delegate.events.last is UnknownEvent) + Assert.willBeTrue(delegate.events.last is UnknownChannelEvent) } } } diff --git a/Sources/StreamChat/Controllers/EventsController/EventsController_Tests.swift b/Sources/StreamChat/Controllers/EventsController/EventsController_Tests.swift index 623ae28a076..1240a518bcd 100644 --- a/Sources/StreamChat/Controllers/EventsController/EventsController_Tests.swift +++ b/Sources/StreamChat/Controllers/EventsController/EventsController_Tests.swift @@ -106,6 +106,41 @@ final class EventsController_Tests: XCTestCase { ) } } + + func test_whenEventsNotificationIsObserved_theUnknownUserEvent_isForwardedToDelegate() { + // Create and set the delegate. + let delegate = EventsControllerDelegateMock(expectedQueueId: callbackQueueID) + controller.delegate = delegate + + // Create `event -> should be processed` mapping. + let event = UnknownUserEvent(type: .userBanned, userId: .unique, createdAt: Date(), payload: [:]) + + let notification = Notification(newEventReceived: event, sender: self) + client.eventNotificationCenter.post(notification) + + // Assert delegate received only events which have passed the filter + AssertAsync.willBeEqual( + delegate.events.compactMap { $0 as? UnknownUserEvent }, [event] + ) + } + + func test_whenEventsNotificationIsObserved_theUnknownChannelEvent_isForwardedToDelegate() throws { + // Create and set the delegate. + let delegate = EventsControllerDelegateMock(expectedQueueId: callbackQueueID) + controller.delegate = delegate + + // Create `event -> should be processed` mapping. + let cid: ChannelId = try .init(cid: "clubid:1234") + let event = UnknownChannelEvent(type: .channelHidden, cid: cid, userId: .unique, createdAt: Date(), payload: [:]) + + let notification = Notification(newEventReceived: event, sender: self) + client.eventNotificationCenter.post(notification) + + // Assert delegate received only events which have passed the filter + AssertAsync.willBeEqual( + delegate.events.compactMap { $0 as? UnknownChannelEvent }, [event] + ) + } } class EventsControllerDelegateMock: QueueAwareDelegate, EventsControllerDelegate { diff --git a/Sources/StreamChat/Deprecations.swift b/Sources/StreamChat/Deprecations.swift index 55987fbad81..f39c212f590 100644 --- a/Sources/StreamChat/Deprecations.swift +++ b/Sources/StreamChat/Deprecations.swift @@ -210,3 +210,6 @@ public typealias NotificationInviteRejected = NotificationInviteRejectedEvent @available(*, deprecated, renamed: "NotificationInviteAcceptedEvent") public typealias NotificationInviteAccepted = NotificationInviteAcceptedEvent + +@available(*, deprecated, renamed: "UnknownChannelEvent") +public typealias UnknownEvent = UnknownChannelEvent diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift index bce415f90a2..fd28264c91d 100644 --- a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift +++ b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift @@ -8,5 +8,5 @@ import Foundation extension SystemEnvironment { /// A Stream Chat version. - public static let version: String = "4.5.2" + public static let version: String = "4.6.0" } diff --git a/Sources/StreamChat/Info.plist b/Sources/StreamChat/Info.plist index 7bacde6766f..a8896df5290 100644 --- a/Sources/StreamChat/Info.plist +++ b/Sources/StreamChat/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.5.2 + 4.6.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/Sources/StreamChat/Query/ChannelListQuery.swift b/Sources/StreamChat/Query/ChannelListQuery.swift index 8a5ab81d166..48bc07d1f9c 100644 --- a/Sources/StreamChat/Query/ChannelListQuery.swift +++ b/Sources/StreamChat/Query/ChannelListQuery.swift @@ -102,7 +102,7 @@ public struct ChannelListQuery: Encodable { /// A number of messages inside each channel. public let messagesLimit: Int /// Query options. - var options: QueryOptions = [.watch] + public var options: QueryOptions = [.watch] /// Init a channels query. /// - Parameters: diff --git a/Sources/StreamChat/Query/ChannelQuery.swift b/Sources/StreamChat/Query/ChannelQuery.swift index 7be2bbd29e6..9fde6b4b3c3 100644 --- a/Sources/StreamChat/Query/ChannelQuery.swift +++ b/Sources/StreamChat/Query/ChannelQuery.swift @@ -24,7 +24,7 @@ public struct ChannelQuery: Encodable { /// A number of watchers for the channel to be retrieved. public let watchersLimit: Int? /// A query options. - var options: QueryOptions = .all + public var options: QueryOptions = .all /// ChannelCreatePayload that is needed only when creating channel let channelPayload: ChannelEditDetailPayload? diff --git a/Sources/StreamChat/Query/QueryOptions.swift b/Sources/StreamChat/Query/QueryOptions.swift index 57d7cea29c3..0c9ae458cec 100644 --- a/Sources/StreamChat/Query/QueryOptions.swift +++ b/Sources/StreamChat/Query/QueryOptions.swift @@ -5,32 +5,32 @@ import Foundation /// Query options. -struct QueryOptions: OptionSet, Encodable { +public struct QueryOptions: OptionSet, Encodable { private enum CodingKeys: String, CodingKey { case state case watch case presence } - let rawValue: Int + public let rawValue: Int /// A query will return a channel state, e.g. messages. - static let state = QueryOptions(rawValue: 1 << 0) + public static let state = QueryOptions(rawValue: 1 << 0) /// Listen for a channel changes in real time, e.g. a new message event. - static let watch = QueryOptions(rawValue: 1 << 1) + public static let watch = QueryOptions(rawValue: 1 << 1) /// Get updates when the user goes offline/online. - static let presence = QueryOptions(rawValue: 1 << 2) + public static let presence = QueryOptions(rawValue: 1 << 2) /// Includes all query options: state, watch and presence. - static let all: QueryOptions = [.state, .watch, .presence] + public static let all: QueryOptions = [.state, .watch, .presence] - init(rawValue: Int) { + public init(rawValue: Int) { self.rawValue = rawValue } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) if contains(.state) { diff --git a/Sources/StreamChat/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift b/Sources/StreamChat/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift index 5eaa7c73a08..58d4b606547 100644 --- a/Sources/StreamChat/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift +++ b/Sources/StreamChat/WebSocketClient/EventMiddlewares/EventDTOConverterMiddleware_Tests.swift @@ -54,7 +54,7 @@ final class EventDTOConverterMiddleware_Tests: XCTestCase { func test_handle_whenNotEventDTOComes_eventIsForwardedAsIs() throws { // Create event - let event = UnknownEvent( + let event = UnknownChannelEvent( type: .reactionNew, cid: .unique, userId: .unique, @@ -66,6 +66,6 @@ final class EventDTOConverterMiddleware_Tests: XCTestCase { let result = middleware.handle(event: event, session: database.viewContext) // Assert - XCTAssertEqual(result as! UnknownEvent, event) + XCTAssertEqual(result as! UnknownChannelEvent, event) } } diff --git a/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift b/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift index e15fc13b24c..15c5cf0a10b 100644 --- a/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift +++ b/Sources/StreamChat/WebSocketClient/Events/EventDecoder.swift @@ -11,8 +11,10 @@ struct EventDecoder { do { let response = try decoder.decode(EventPayload.self, from: data) return try response.event() - } catch is ClientError.UnknownEvent { - return try decoder.decode(UnknownEvent.self, from: data) + } catch is ClientError.UnknownChannelEvent { + return try decoder.decode(UnknownChannelEvent.self, from: data) + } catch is ClientError.UnknownUserEvent { + return try decoder.decode(UnknownUserEvent.self, from: data) } } } diff --git a/Sources/StreamChat/WebSocketClient/Events/EventDecoder_Tests.swift b/Sources/StreamChat/WebSocketClient/Events/EventDecoder_Tests.swift index 6ba73f46a96..539ae8e6cba 100644 --- a/Sources/StreamChat/WebSocketClient/Events/EventDecoder_Tests.swift +++ b/Sources/StreamChat/WebSocketClient/Events/EventDecoder_Tests.swift @@ -37,7 +37,7 @@ final class EventDecoder_Tests: XCTestCase { // MARK: Custom events - func test_decode_whenValidCustomEventPayloadComes_returnsUnkownEvent() throws { + func test_decode_whenValidCustomEventPayloadComes_returnsUnknownChannelEvent() throws { // Create custom event fields let userId: UserId = .unique let cid: ChannelId = .unique @@ -74,8 +74,8 @@ final class EventDecoder_Tests: XCTestCase { // Assert event is decoded. let event = try eventDecoder.decode(from: json) - // Assert `UnknownEvent` event with expected payload is decoded - let unkownEvent = try XCTUnwrap(event as? UnknownEvent) + // Assert `UnknownChannelEvent` event with expected payload is decoded + let unkownEvent = try XCTUnwrap(event as? UnknownChannelEvent) // Assert event has correct fields. XCTAssertEqual(unkownEvent.cid, cid) @@ -84,6 +84,47 @@ final class EventDecoder_Tests: XCTestCase { XCTAssertEqual(unkownEvent.payload(ofType: IdeaEventPayload.self), ideaPayload) } + func test_decode_whenValidCustomEventPayloadComes_returnsUnknownUserEvent() throws { + // Create custom event fields + let userId: UserId = .unique + let ideaPayload: IdeaEventPayload = .unique + let createdAt: String = "2020-07-16T15:38:10.289007Z" + + // Create custom event JSON + let json = """ + { + "user" : { + "id" : "\(userId)", + "banned" : false, + "created_at" : "2019-12-12T15:33:46.488935Z", + "invisible" : false, + "unreadChannels" : 0, + "extra_uid" : 2000, + "unread_count" : 0, + "image" : "https://getstream.io/random_svg/?id=broken-waterfall-5&name=Broken+waterfall", + "updated_at" : "2020-07-16T15:38:10.289007Z", + "role" : "user", + "total_unread_count" : 0, + "online" : true, + "name" : "broken-waterfall-5" + }, + "created_at" : "\(createdAt)", + "type" : "\(IdeaEventPayload.eventType.rawValue)", + "idea" : "\(ideaPayload.idea)" + } + """.data(using: .utf8)! + + // Assert event is decoded. + let event = try eventDecoder.decode(from: json) + // Assert `UnknownUserEvent` event with expected payload is decoded + let unkownEvent = try XCTUnwrap(event as? UnknownUserEvent) + + // Assert event has correct fields. + XCTAssertEqual(unkownEvent.userId, userId) + XCTAssertEqual(unkownEvent.createdAt, createdAt.toDate()) + XCTAssertEqual(unkownEvent.payload(ofType: IdeaEventPayload.self), ideaPayload) + } + func test_decode_whenInvalidCustomEventPayloadComes_throwsDecodingError() { // Create invalid custom channel event JSON let json = """ diff --git a/Sources/StreamChat/WebSocketClient/Events/EventPayload_Tests.swift b/Sources/StreamChat/WebSocketClient/Events/EventPayload_Tests.swift index 7278976694d..1308ce5a10b 100644 --- a/Sources/StreamChat/WebSocketClient/Events/EventPayload_Tests.swift +++ b/Sources/StreamChat/WebSocketClient/Events/EventPayload_Tests.swift @@ -5,25 +5,35 @@ @testable import StreamChat import XCTest -typealias DefaultEventPayload = EventPayload - class EventPayload_Tests: XCTestCase { let eventJSON = XCTestCase.mockData(fromFile: "NotificationAddedToChannel") let eventDecoder = EventDecoder() func test_eventJSON_isSerialized_withDefaultExtraData() throws { - let payload = try JSONDecoder.default.decode(DefaultEventPayload.self, from: eventJSON) + let payload = try JSONDecoder.default.decode(EventPayload.self, from: eventJSON) XCTAssertNotNil(payload.channel) } - func test_event_whenEventPayloadWithTypeCustomType_throwsUnknownEventError() throws { + func test_event_whenTypeIsUnknownAndCIDIsMissing_throwsUnknownUserEventError() throws { // Create event payload with custom event type. - let payload = DefaultEventPayload(eventType: IdeaEventPayload.eventType) + let payload = EventPayload(eventType: IdeaEventPayload.eventType) + + // Try to parse system event from payload + XCTAssertThrowsError(try payload.event()) { error in + // Assert `ClientError.UnknownUserEvent` is thrown + XCTAssertTrue(error is ClientError.UnknownUserEvent) + } + } + + func test_event_whenTypeIsUnknownAndCIDIsPresent_throwsUnknownChannelEventError() throws { + // Create event payload with custom event type and cid + let cid: ChannelId = try .init(cid: "club:123") + let payload = EventPayload(eventType: IdeaEventPayload.eventType, cid: cid) // Try to parse system event from payload XCTAssertThrowsError(try payload.event()) { error in - // Assert `ClientError.UnknownEvent` is thrown - XCTAssertTrue(error is ClientError.UnknownEvent) + // Assert `ClientError.UnknownChannelEvent` is thrown + XCTAssertTrue(error is ClientError.UnknownChannelEvent) } } } diff --git a/Sources/StreamChat/WebSocketClient/Events/EventType.swift b/Sources/StreamChat/WebSocketClient/Events/EventType.swift index 78039793ad8..ba681599898 100644 --- a/Sources/StreamChat/WebSocketClient/Events/EventType.swift +++ b/Sources/StreamChat/WebSocketClient/Events/EventType.swift @@ -154,13 +154,23 @@ extension EventType { case .notificationInviteRejected: return try NotificationInviteRejectedEventDTO(from: response) default: - throw ClientError.UnknownEvent(response.eventType) + if response.cid == nil { + throw ClientError.UnknownUserEvent(response.eventType) + } else { + throw ClientError.UnknownChannelEvent(response.eventType) + } } } } extension ClientError { - class UnknownEvent: ClientError { + class UnknownChannelEvent: ClientError { + init(_ type: EventType) { + super.init("Event with \(type) cannot be decoded as system event.") + } + } + + class UnknownUserEvent: ClientError { init(_ type: EventType) { super.init("Event with \(type) cannot be decoded as system event.") } diff --git a/Sources/StreamChat/Workers/MessageUpdater_Tests.swift b/Sources/StreamChat/Workers/MessageUpdater_Tests.swift index 8ace5b7c465..75e4b54cf3b 100644 --- a/Sources/StreamChat/Workers/MessageUpdater_Tests.swift +++ b/Sources/StreamChat/Workers/MessageUpdater_Tests.swift @@ -234,25 +234,20 @@ final class MessageUpdater_Tests: XCTestCase { // Create a new message in the database try database.createMessage(id: messageId, authorId: currentUserId, localState: state) + let expectation = expectation(description: "deleteMessage") + // Simulate `deleteMessage(messageId:)` call - var completionCalled = false messageUpdater.deleteMessage(messageId: messageId) { error in XCTAssertNil(error) - completionCalled = true + expectation.fulfill() } - + + wait(for: [expectation], timeout: 0.1) let message = try XCTUnwrap(database.viewContext.message(id: messageId)) - - AssertAsync { - // Assert completion is called - Assert.willBeTrue(completionCalled) - // Assert `deletedAt` is set for the message - Assert.willBeTrue(message.deletedAt != nil) - // Assert `type` is set to `.deleted` - Assert.willBeEqual(message.type, MessageType.deleted.rawValue) - // Assert API is not called - Assert.staysTrue(self.apiClient.request_endpoint == nil) - } + + XCTAssertNotNil(message.deletedAt) + XCTAssertEqual(message.type, MessageType.deleted.rawValue) + XCTAssertNil(apiClient.request_endpoint) } } diff --git a/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift b/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift index c50dc5e56f7..b790a0f3fd2 100644 --- a/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift +++ b/Sources/StreamChatUI/ChatChannel/ChatChannelVC.swift @@ -141,8 +141,7 @@ open class ChatChannelVC: } open func chatMessageListVC(_ vc: ChatMessageListVC, messageAt indexPath: IndexPath) -> ChatMessage? { - guard indexPath.item < channelController.messages.count else { return nil } - return channelController.messages[indexPath.item] + channelController.messages[safe: indexPath.item] } open func chatMessageListVC( diff --git a/Sources/StreamChatUI/ChatChannelList/ChatChannelListVC.swift b/Sources/StreamChatUI/ChatChannelList/ChatChannelListVC.swift index f51d555bad4..d5f63f976a3 100644 --- a/Sources/StreamChatUI/ChatChannelList/ChatChannelListVC.swift +++ b/Sources/StreamChatUI/ChatChannelList/ChatChannelListVC.swift @@ -168,12 +168,11 @@ open class ChatChannelListVC: _ViewController, withReuseIdentifier: collectionViewCellReuseIdentifier, for: indexPath ) as! ChatChannelListCollectionViewCell - + + guard let channel = getChannel(at: indexPath) else { return cell } + cell.components = components - cell.itemView.content = .init( - channel: controller.channels[indexPath.row], - currentUserId: controller.client.currentUserId - ) + cell.itemView.content = .init(channel: channel, currentUserId: controller.client.currentUserId) cell.swipeableView.delegate = self cell.swipeableView.indexPath = { [weak cell, weak self] in @@ -197,7 +196,7 @@ open class ChatChannelListVC: _ViewController, } open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let channel = controller.channels[indexPath.row] + guard let channel = getChannel(at: indexPath) else { return } router.showChannel(for: channel.cid) } @@ -265,13 +264,15 @@ open class ChatChannelListVC: _ViewController, /// This function is called when delete button is pressed from action items of a cell. /// - Parameter indexPath: IndexPath of given cell to fetch the content of it. open func deleteButtonPressedForCell(at indexPath: IndexPath) { - router.didTapDeleteButton(for: controller.channels[indexPath.row].cid) + guard let channel = getChannel(at: indexPath) else { return } + router.didTapDeleteButton(for: channel.cid) } /// This function is called when more button is pressed from action items of a cell. /// - Parameter indexPath: IndexPath of given cell to fetch the content of it. open func moreButtonPressedForCell(at indexPath: IndexPath) { - router.didTapMoreButton(for: controller.channels[indexPath.row].cid) + guard let channel = getChannel(at: indexPath) else { return } + router.didTapMoreButton(for: channel.cid) } // MARK: - ChatChannelListControllerDelegate @@ -334,4 +335,10 @@ open class ChatChannelListVC: _ViewController, loadingIndicator.stopAnimating() } } + + private func getChannel(at indexPath: IndexPath) -> ChatChannel? { + let index = indexPath.row + controller.channels.assertIndexIsPresent(index) + return controller.channels[safe: index] + } } diff --git a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift index 96ac0c0a97a..1b1f5fe031e 100644 --- a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift +++ b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptions.swift @@ -16,6 +16,18 @@ extension ChatMessageLayoutOptions: Identifiable { } } +public extension ChatMessageLayoutOptions { + /// Remove multiple message layout options. + mutating func remove(_ options: ChatMessageLayoutOptions) { + self = subtracting(options) + } + + /// Insert multiple message layout options. + mutating func insert(_ options: ChatMessageLayoutOptions) { + options.forEach { self.insert($0) } + } +} + /// Each message layout option is used to define which views will be part of the message cell. /// A different combination of layout options will produce a different cell reuse identifier. public struct ChatMessageLayoutOption: RawRepresentable, Hashable, ExpressibleByStringLiteral { diff --git a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift index f86b37f86d1..049977f84bf 100644 --- a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift +++ b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift @@ -29,7 +29,10 @@ open class ChatMessageLayoutOptionsResolver { appearance: Appearance ) -> ChatMessageLayoutOptions { let messageIndex = messages.index(messages.startIndex, offsetBy: indexPath.item) - let message = messages[messageIndex] + guard let message = messages[safe: messageIndex] else { + indexNotFoundAssertion() + return [] + } let isLastInSequence = isMessageLastInSequence( messageIndexPath: indexPath, @@ -137,13 +140,19 @@ open class ChatMessageLayoutOptionsResolver { messages: AnyRandomAccessCollection ) -> Bool { let messageIndex = messages.index(messages.startIndex, offsetBy: messageIndexPath.item) - let message = messages[messageIndex] + guard let message = messages[safe: messageIndex] else { + indexNotFoundAssertion() + return true + } // The current message is the last message so it's either a standalone or last in sequence. guard messageIndexPath.item > 0 else { return true } let nextMessageIndex = messages.index(before: messageIndex) - let nextMessage = messages[nextMessageIndex] + guard let nextMessage = messages[safe: nextMessageIndex] else { + indexNotFoundAssertion() + return true + } // The message after the current one has different author so the current message // is either a standalone or last in sequence. diff --git a/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift b/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift index fc2f734ddc6..b75b9b949e0 100644 --- a/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift +++ b/Sources/StreamChatUI/ChatMessageList/ChatMessageListVC.swift @@ -327,19 +327,22 @@ open class ChatMessageListVC: cell.messageContentView?.content = message cell.dateSeparatorView.isHidden = true - if isDateSeparatorEnabled, let currentMessage = message { - let currentDay = DateFormatter.messageListDateOverlay.string(from: currentMessage.createdAt) - cell.dateSeparatorView.content = currentDay - - // Only the show the separator if the previous message has a different day - let previousIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) - if let previousMessage = dataSource?.chatMessageListVC(self, messageAt: previousIndexPath) { - let previousDay = DateFormatter.messageListDateOverlay.string(from: previousMessage.createdAt) - cell.dateSeparatorView.isHidden = previousDay == currentDay - } else { - // If previous message doesn't exist show the separator as well - cell.dateSeparatorView.isHidden = false - } + guard isDateSeparatorEnabled, let currentMessage = message else { + return cell + } + + let currentDay = DateFormatter.messageListDateOverlay.string(from: currentMessage.createdAt) + cell.dateSeparatorView.content = currentDay + + // Only show the separator if the previous message is from a different day + // TODO: simplify this and make it simpler to customize the logic (ie. extract this into a open `showDateSeparatorView` method) + let previousIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section) + if let previousMessage = dataSource?.chatMessageListVC(self, messageAt: previousIndexPath) { + let previousDay = DateFormatter.messageListDateOverlay.string(from: previousMessage.createdAt) + cell.dateSeparatorView.isHidden = previousDay == currentDay + } else { + // If previous message doesn't exist show the separator as well + cell.dateSeparatorView.isHidden = false } return cell diff --git a/Sources/StreamChatUI/ChatMessageList/ChatMessageListView.swift b/Sources/StreamChatUI/ChatMessageList/ChatMessageListView.swift index faa940e9c5d..726a1ee080e 100644 --- a/Sources/StreamChatUI/ChatMessageList/ChatMessageListView.swift +++ b/Sources/StreamChatUI/ChatMessageList/ChatMessageListView.swift @@ -182,41 +182,39 @@ open class ChatMessageListView: UITableView, Customizable, ComponentsProvider { return } - if changes.count > 1 { + guard changes.count == 1, let change = changes.first else { reloadData() return } - changes.forEach { - switch $0 { - case let .insert(message, index: index): - UIView.performWithoutAnimation { - self.performBatchUpdates { - self.insertRows(at: [index], with: .none) - } completion: { _ in - guard self.numberOfRows(inSection: index.section) > 1 else { return } - // Update previous row to remove timestamp if needed - // +1 instead of -1 because the message list is inverted - let previousIndex = IndexPath(row: index.row + 1, section: index.section) - self.reloadRows(at: [previousIndex], with: .none) - } + switch change { + case let .insert(message, index: index): + UIView.performWithoutAnimation { + self.performBatchUpdates { + self.insertRows(at: [index], with: .none) + } completion: { _ in + guard self.numberOfRows(inSection: index.section) > index.row + 1 else { return } + // Update previous row to remove timestamp if needed + // +1 instead of -1 because the message list is inverted + let previousIndex = IndexPath(row: index.row + 1, section: index.section) + self.reloadRows(at: [previousIndex], with: .none) } + } - if message.isSentByCurrentUser, index == IndexPath(item: 0, section: 0) { - self.scrollToBottomAction = .init { [weak self] in - self?.scrollToMostRecentMessage() - } + if message.isSentByCurrentUser, index == IndexPath(item: 0, section: 0) { + scrollToBottomAction = .init { [weak self] in + self?.scrollToMostRecentMessage() } + } - case let .move(_, fromIndex: fromIndex, toIndex: toIndex): - self.moveRow(at: fromIndex, to: toIndex) + case let .move(_, fromIndex: fromIndex, toIndex: toIndex): + moveRow(at: fromIndex, to: toIndex) - case let .update(_, index: index): - self.reloadRows(at: [index], with: .automatic) + case let .update(_, index: index): + reloadRows(at: [index], with: .automatic) - case .remove: - self.reloadData() - } + case .remove: + reloadData() } } diff --git a/Sources/StreamChatUI/ChatMessageList/Reactions/ChatMessageReactionAuthorsVC/ChatMessageReactionAuthorsVC.swift b/Sources/StreamChatUI/ChatMessageList/Reactions/ChatMessageReactionAuthorsVC/ChatMessageReactionAuthorsVC.swift index 769c6c13a95..03af977efa7 100644 --- a/Sources/StreamChatUI/ChatMessageList/Reactions/ChatMessageReactionAuthorsVC/ChatMessageReactionAuthorsVC.swift +++ b/Sources/StreamChatUI/ChatMessageList/Reactions/ChatMessageReactionAuthorsVC/ChatMessageReactionAuthorsVC.swift @@ -131,11 +131,8 @@ open class ChatMessageReactionAuthorsVC: ) as! ChatMessageReactionAuthorViewCell let reactions = messageController.reactions - if let currentUserId = messageController?.client.currentUserId { - cell.content = ChatMessageReactionAuthorViewCell.Content( - reaction: reactions[indexPath.item], - currentUserId: currentUserId - ) + if let currentUserId = messageController?.client.currentUserId, let reaction = reactions[safe: indexPath.item] { + cell.content = ChatMessageReactionAuthorViewCell.Content(reaction: reaction, currentUserId: currentUserId) } return cell diff --git a/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift b/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift index b9e23e06167..26a964889c4 100644 --- a/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift +++ b/Sources/StreamChatUI/ChatThread/ChatThreadVC.swift @@ -153,7 +153,11 @@ open class ChatThreadVC: open func chatMessageListVC(_ vc: ChatMessageListVC, messageAt indexPath: IndexPath) -> ChatMessage? { guard indexPath.item < replies.count else { return nil } - return replies[indexPath.item] + guard let reply = replies[safe: indexPath.item] else { + indexNotFoundAssertion() + return nil + } + return reply } open func chatMessageListVC( diff --git a/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift b/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift index 52b4b635e1b..d0dc953fe9b 100644 --- a/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift +++ b/Sources/StreamChatUI/CommonViews/AvatarView/ChatChannelAvatarView.swift @@ -168,12 +168,12 @@ open class ChatChannelAvatarView: _View, ThemeProvider, SwiftUIRepresentable { // The half of the width of the avatar let halfContainerSize = CGSize(width: CGSize.avatarThumbnailSize.width / 2, height: CGSize.avatarThumbnailSize.height) - if images.count == 1 { - combinedImage = images[0] - } else if images.count == 2 { - let leftImage = imageProcessor.crop(image: images[0], to: halfContainerSize) + if images.count == 1, let image = images.first { + combinedImage = image + } else if images.count == 2, let firstImage = images.first, let secondImage = images.last { + let leftImage = imageProcessor.crop(image: firstImage, to: halfContainerSize) ?? appearance.images.userAvatarPlaceholder1 - let rightImage = imageProcessor.crop(image: images[1], to: halfContainerSize) + let rightImage = imageProcessor.crop(image: secondImage, to: halfContainerSize) ?? appearance.images.userAvatarPlaceholder1 combinedImage = imageMerger.merge( images: [ @@ -182,13 +182,16 @@ open class ChatChannelAvatarView: _View, ThemeProvider, SwiftUIRepresentable { ], orientation: .horizontal ) - } else if images.count == 3 { - let leftImage = imageProcessor.crop(image: images[0], to: halfContainerSize) + } else if images.count == 3, + let firstImage = images[safe: 0], + let secondImage = images[safe: 1], + let thirdImage = images[safe: 2] { + let leftImage = imageProcessor.crop(image: firstImage, to: halfContainerSize) let rightCollage = imageMerger.merge( images: [ - images[1], - images[2] + secondImage, + thirdImage ], orientation: .vertical ) @@ -207,11 +210,15 @@ open class ChatChannelAvatarView: _View, ThemeProvider, SwiftUIRepresentable { ], orientation: .horizontal ) - } else if images.count == 4 { + } else if images.count == 4, + let firstImage = images[safe: 0], + let secondImage = images[safe: 1], + let thirdImage = images[safe: 2], + let forthImage = images[safe: 3] { let leftCollage = imageMerger.merge( images: [ - images[0], - images[2] + firstImage, + thirdImage ], orientation: .vertical ) @@ -224,8 +231,8 @@ open class ChatChannelAvatarView: _View, ThemeProvider, SwiftUIRepresentable { let rightCollage = imageMerger.merge( images: [ - images[1], - images[3] + secondImage, + forthImage ], orientation: .vertical ) diff --git a/Sources/StreamChatUI/CommonViews/Suggestions/ChatSuggestionsVC.swift b/Sources/StreamChatUI/CommonViews/Suggestions/ChatSuggestionsVC.swift index b48298f89ad..d72a4e2aed0 100644 --- a/Sources/StreamChatUI/CommonViews/Suggestions/ChatSuggestionsVC.swift +++ b/Sources/StreamChatUI/CommonViews/Suggestions/ChatSuggestionsVC.swift @@ -180,7 +180,12 @@ open class ChatMessageComposerSuggestionsCommandDataSource: NSObject, UICollecti ) as! ChatCommandSuggestionCollectionViewCell cell.components = components - cell.commandView.content = commands[indexPath.row] + guard let command = commands[safe: indexPath.row] else { + indexNotFoundAssertion() + return cell + } + + cell.commandView.content = command return cell } @@ -248,7 +253,10 @@ open class ChatMessageComposerSuggestionsMentionDataSource: NSObject, for: indexPath ) as! ChatMentionSuggestionCollectionViewCell - let user = usersCache[indexPath.row] + guard let user = usersCache[safe: indexPath.row] else { + indexNotFoundAssertion() + return cell + } // We need to make sure we set the components before accessing the mentionView, // so the mentionView is created with the most up-to-dated components. cell.components = components diff --git a/Sources/StreamChatUI/Composer/ComposerVC.swift b/Sources/StreamChatUI/Composer/ComposerVC.swift index 3d11a1338f2..338322ca269 100644 --- a/Sources/StreamChatUI/Composer/ComposerVC.swift +++ b/Sources/StreamChatUI/Composer/ComposerVC.swift @@ -597,7 +597,12 @@ open class ComposerVC: _ViewController, ) suggestionsVC.dataSource = dataSource suggestionsVC.didSelectItemAt = { [weak self] commandIndex in - self?.content.addCommand(commandHints[commandIndex]) + guard let hintCommand = commandHints[safe: commandIndex] else { + indexNotFoundAssertion() + return + } + + self?.content.addCommand(hintCommand) self?.dismissSuggestions() } @@ -660,9 +665,12 @@ open class ComposerVC: _ViewController, guard dataSource.usersCache.count >= userIndex else { return } + guard let user = dataSource.usersCache[safe: userIndex] else { + indexNotFoundAssertion() + return + } let textView = self.composerView.inputMessageView.textView - let user = dataSource.usersCache[userIndex] let text = textView.text as NSString let mentionText = self.mentionText(for: user) let newText = text.replacingCharacters(in: mentionRange, with: mentionText) diff --git a/Sources/StreamChatUI/Deprecations.swift b/Sources/StreamChatUI/Deprecations.swift index f41d2650b57..7a60f02391b 100644 --- a/Sources/StreamChatUI/Deprecations.swift +++ b/Sources/StreamChatUI/Deprecations.swift @@ -182,16 +182,6 @@ public extension ChatMessageLayoutOptions { id } - @available(*, deprecated, message: "use `remove(_ member: ChatMessageLayoutOption)` instead.") - mutating func remove(_ options: ChatMessageLayoutOptions) { - self = subtracting(options) - } - - @available(*, deprecated, message: "use `insert(_ member: ChatMessageLayoutOption)` instead.") - mutating func insert(_ options: ChatMessageLayoutOptions) { - options.forEach { self.insert($0) } - } - @available(*, deprecated, message: "use `subtracting(_ other: Sequence)` instead.") mutating func subtracting(_ option: ChatMessageLayoutOption) { self = subtracting([option]) diff --git a/Sources/StreamChatUI/Gallery/GalleryVC.swift b/Sources/StreamChatUI/Gallery/GalleryVC.swift index e7b8037628b..00810c4d394 100644 --- a/Sources/StreamChatUI/Gallery/GalleryVC.swift +++ b/Sources/StreamChatUI/Gallery/GalleryVC.swift @@ -341,8 +341,10 @@ open class GalleryVC: withReuseIdentifier: reuseIdentifier, for: indexPath ) as! GalleryCollectionViewCell - - cell.content = items[indexPath.item] + + guard let item = getItem(at: indexPath) else { return cell } + + cell.content = item cell.didTapOnce = { [weak self] in self?.handleSingleTapOnCell(at: indexPath) @@ -389,14 +391,15 @@ open class GalleryVC: /// A currently visible gallery item. open var currentItem: AnyChatMessageAttachment { - items[currentItemIndexPath.item] + items.assertIndexIsPresent(currentItemIndexPath.item) + return items[currentItemIndexPath.item] } /// Returns a share item for the gallery item at given index path. /// - Parameter indexPath: An index path. /// - Returns: An item to share. open func shareItem(at indexPath: IndexPath) -> Any? { - let item = items[indexPath.item] + guard let item = getItem(at: indexPath) else { return nil } if let image = item.attachment(payloadType: ImageAttachmentPayload.self) { return image.imageURL @@ -411,8 +414,8 @@ open class GalleryVC: /// - Parameter indexPath: An index path. /// - Returns: A cell reuse identifier. open func cellReuseIdentifierForItem(at indexPath: IndexPath) -> String? { - let item = items[indexPath.item] - + guard let item = getItem(at: indexPath) else { return nil } + switch item.type { case .image: return components.imageAttachmentGalleryCell.reuseId @@ -441,8 +444,9 @@ open class GalleryVC: /// Returns an image view to animate during interactive dismissing. open var imageViewToAnimateWhenDismissing: UIImageView? { let indexPath = currentItemIndexPath - - switch items[indexPath.item].type { + guard let item = getItem(at: indexPath) else { return nil } + + switch item.type { case .image: let cell = attachmentsCollectionView .cellForItem(at: indexPath) as? ImageAttachmentGalleryCell @@ -455,4 +459,10 @@ open class GalleryVC: return nil } } + + private func getItem(at indexPath: IndexPath) -> AnyChatMessageAttachment? { + let index = indexPath.item + items.assertIndexIsPresent(index) + return items[safe: index] + } } diff --git a/Sources/StreamChatUI/Info.plist b/Sources/StreamChatUI/Info.plist index 7bacde6766f..a8896df5290 100644 --- a/Sources/StreamChatUI/Info.plist +++ b/Sources/StreamChatUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 4.5.2 + 4.6.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/Sources/StreamChatUI/Navigation/ChatMessageListRouter.swift b/Sources/StreamChatUI/Navigation/ChatMessageListRouter.swift index 2cc05d295fc..1b2f6324227 100644 --- a/Sources/StreamChatUI/Navigation/ChatMessageListRouter.swift +++ b/Sources/StreamChatUI/Navigation/ChatMessageListRouter.swift @@ -176,7 +176,10 @@ open class ChatMessageListRouter: galleryVC?.imageViewToAnimateWhenDismissing } zoomTransitionController.presentingImageView = { - let id = galleryVC.items[galleryVC.content.currentPage].id + guard let id = galleryVC.items[safe: galleryVC.content.currentPage]?.id else { + indexNotFoundAssertion() + return nil + } return previews.first(where: { $0.attachmentId == id })?.imageView ?? previews.last?.imageView } diff --git a/Sources/StreamChatUI/StreamSwiftyGif/NSImageView+SwiftyGif.swift b/Sources/StreamChatUI/StreamSwiftyGif/NSImageView+SwiftyGif.swift index cd4b9982836..26792b6bbfd 100755 --- a/Sources/StreamChatUI/StreamSwiftyGif/NSImageView+SwiftyGif.swift +++ b/Sources/StreamChatUI/StreamSwiftyGif/NSImageView+SwiftyGif.swift @@ -441,8 +441,8 @@ extension NSImageView { } var delegate: SwiftyGifDelegate? { - get { return (objc_getAssociatedObject(self, _delegateKey!) as? SwiftyGifDelegate) } - set { objc_setAssociatedObject(self, _delegateKey!, newValue, .OBJC_ASSOCIATION_ASSIGN) } + get { return (objc_getAssociatedWeakObject(self, _delegateKey!) as? SwiftyGifDelegate) } + set { objc_setAssociatedWeakObject(self, _delegateKey!, newValue) } } private var haveCache: Bool { diff --git a/Sources/StreamChatUI/StreamSwiftyGif/ObjcAssociatedWeakObject.swift b/Sources/StreamChatUI/StreamSwiftyGif/ObjcAssociatedWeakObject.swift new file mode 100644 index 00000000000..bf99ac2583a --- /dev/null +++ b/Sources/StreamChatUI/StreamSwiftyGif/ObjcAssociatedWeakObject.swift @@ -0,0 +1,18 @@ +// +// ObjcAssociatedWeakObject.swift +// + +import Foundation + +func objc_getAssociatedWeakObject(_ object: AnyObject, _ key: UnsafeRawPointer) -> AnyObject? { + let block: (() -> AnyObject?)? = objc_getAssociatedObject(object, key) as? (() -> AnyObject?) + return block != nil ? block?() : nil +} + +func objc_setAssociatedWeakObject(_ object: AnyObject, _ key: UnsafeRawPointer, _ value: AnyObject?) { + weak var weakValue = value + let block: (() -> AnyObject?)? = { + return weakValue + } + objc_setAssociatedObject(object, key, block, .OBJC_ASSOCIATION_COPY) +} diff --git a/Sources/StreamChatUI/StreamSwiftyGif/UIImageView+SwiftyGif.swift b/Sources/StreamChatUI/StreamSwiftyGif/UIImageView+SwiftyGif.swift index 5381e4f5632..88d6c7d36bf 100755 --- a/Sources/StreamChatUI/StreamSwiftyGif/UIImageView+SwiftyGif.swift +++ b/Sources/StreamChatUI/StreamSwiftyGif/UIImageView+SwiftyGif.swift @@ -436,8 +436,8 @@ extension UIImageView { } var delegate: SwiftyGifDelegate? { - get { return (objc_getAssociatedObject(self, _delegateKey!) as? SwiftyGifDelegate) } - set { objc_setAssociatedObject(self, _delegateKey!, newValue, .OBJC_ASSOCIATION_ASSIGN) } + get { return (objc_getAssociatedWeakObject(self, _delegateKey!) as? SwiftyGifDelegate) } + set { objc_setAssociatedWeakObject(self, _delegateKey!, newValue) } } private var haveCache: Bool { diff --git a/Sources/StreamChatUI/Utils/Array+SafeSubscript.swift b/Sources/StreamChatUI/Utils/Array+SafeSubscript.swift new file mode 100644 index 00000000000..ffc7d4e68ff --- /dev/null +++ b/Sources/StreamChatUI/Utils/Array+SafeSubscript.swift @@ -0,0 +1,38 @@ +// +// Copyright © 2021 Stream.io Inc. All rights reserved. +// + +import Foundation +import StreamChat + +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } + + /// Triggers indexNotFoundAssertion if the index is not present in the collection. + /// Mostly used in places where returning optional would be a breaking change + func assertIndexIsPresent( + _ index: Index, + functionName: StaticString = #function, + fileName: StaticString = #file, + lineNumber: UInt = #line + ) { + guard !indices.contains(index) else { return } + indexNotFoundAssertion(functionName: functionName, fileName: fileName, lineNumber: lineNumber) + } +} + +func indexNotFoundAssertion( + functionName: StaticString = #function, + fileName: StaticString = #file, + lineNumber: UInt = #line +) { + log.assertionFailure( + "Accessing an index that is not present in the data source", + functionName: functionName, + fileName: fileName, + lineNumber: lineNumber + ) +} diff --git a/Sources/StreamChatUI/Utils/Array+SafeSubscript_Tests.swift b/Sources/StreamChatUI/Utils/Array+SafeSubscript_Tests.swift new file mode 100644 index 00000000000..0d06bbbd7f4 --- /dev/null +++ b/Sources/StreamChatUI/Utils/Array+SafeSubscript_Tests.swift @@ -0,0 +1,22 @@ +// +// Copyright © 2021 Stream.io Inc. All rights reserved. +// + +import Foundation +@testable import StreamChatUI +import XCTest + +final class Array_SafeSubscript_Tests: XCTestCase { + func test_safeSubscript_returnsObject_whenIndexIsPresent() { + let collection = (0..<3).map { "index\($0)" } + XCTAssertEqual(collection[safe: 0], "index0") + XCTAssertEqual(collection[safe: 1], "index1") + XCTAssertEqual(collection[safe: 2], "index2") + } + + func test_safeSubscript_returnsNil_whenIndexNotPresent() { + let collection = (0..<3).map { "index\($0)" } + XCTAssertNil(collection[safe: 3]) + XCTAssertNil(collection[safe: 84]) + } +} diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec index 51c6d048c69..09a326ec3e8 100644 --- a/StreamChat-XCFramework.podspec +++ b/StreamChat-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "StreamChat-XCFramework" - spec.version = "4.5.0" + spec.version = "4.6.0" spec.summary = "StreamChat iOS Client" spec.description = "stream-chat-swift is the official Swift client for Stream Chat, a service for building chat applications." diff --git a/StreamChat.podspec b/StreamChat.podspec index c6d5259801a..c387c27cc95 100644 --- a/StreamChat.podspec +++ b/StreamChat.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "StreamChat" - spec.version = "4.5.2" + spec.version = "4.6.0" spec.summary = "StreamChat iOS Client" spec.description = "stream-chat-swift is the official Swift client for Stream Chat, a service for building chat applications." diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index ab256b65c09..ac8d6abd875 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -545,8 +545,8 @@ 84A1D2EE26AAFDEE00014712 /* TestCustomEventPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1D2ED26AAFDEE00014712 /* TestCustomEventPayload.swift */; }; 84A1D2F026AB10DB00014712 /* EventDecoder_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1D2EF26AB10DB00014712 /* EventDecoder_Tests.swift */; }; 84A1D2F426AB221E00014712 /* ChannelEventsController_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1D2F326AB221E00014712 /* ChannelEventsController_Tests.swift */; }; - 84A1D2F626AB357900014712 /* UnknownEvent_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1D2F526AB357900014712 /* UnknownEvent_Tests.swift */; }; - 84A43CAF26A9A25000302763 /* UnknownEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A43CAE26A9A25000302763 /* UnknownEvent.swift */; }; + 84A1D2F626AB357900014712 /* UnknownChannelEvent_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1D2F526AB357900014712 /* UnknownChannelEvent_Tests.swift */; }; + 84A43CAF26A9A25000302763 /* UnknownChannelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A43CAE26A9A25000302763 /* UnknownChannelEvent.swift */; }; 84A43CB326A9A54700302763 /* EventSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A43CB226A9A54700302763 /* EventSender.swift */; }; 84AA4E3626F264610056A684 /* EventDTOConverterMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA4E3526F264610056A684 /* EventDTOConverterMiddleware.swift */; }; 84ABB015269F0A84003A4585 /* EventsController+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ABB014269F0A84003A4585 /* EventsController+Combine.swift */; }; @@ -772,10 +772,14 @@ 8AE335A924FCF999002B6677 /* InternetConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE335A624FCF999002B6677 /* InternetConnection.swift */; }; 8AE335AA24FCF99E002B6677 /* InternetConnection_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE335A424FCF999002B6677 /* InternetConnection_Tests.swift */; }; 8AE8950D24D8660800E852EC /* PingPongEmojiFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE8950B24D85FF200E852EC /* PingPongEmojiFormatter.swift */; }; + A30C3F20276B428F00DA5968 /* UnknownUserEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30C3F1F276B428F00DA5968 /* UnknownUserEvent.swift */; }; + A30C3F22276B4F8800DA5968 /* UnknownUserEvent_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30C3F21276B4F8800DA5968 /* UnknownUserEvent_Tests.swift */; }; A35757C72613081B00DC914C /* ComposerKeyboardHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A35757C62613081B00DC914C /* ComposerKeyboardHandler.swift */; }; A39A8AE7263825F4003453D9 /* ChatMessageLayoutOptionsResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A39A8AE6263825F4003453D9 /* ChatMessageLayoutOptionsResolver.swift */; }; A3BB3FFF261DA74D00365496 /* ContainerStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BB3FFE261DA74D00365496 /* ContainerStackView.swift */; }; A3C0D774261CA25700A8A1A2 /* ContainerStackView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C0D773261CA25700A8A1A2 /* ContainerStackView_Tests.swift */; }; + A3EA3328276C904700C84A52 /* ObjcAssociatedWeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3EA3327276C904700C84A52 /* ObjcAssociatedWeakObject.swift */; }; + A3EA3329276C904700C84A52 /* ObjcAssociatedWeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3EA3327276C904700C84A52 /* ObjcAssociatedWeakObject.swift */; }; AC1E16FF269C70530040548B /* String+Extensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDB5412269C6F2A007CD465 /* String+Extensions_Tests.swift */; }; AC73783A26A6AF1C002ED7B4 /* AttachmentPayloadLinkWithoutImagePreview.json in Resources */ = {isa = PBXBuildFile; fileRef = AC73783926A6AF1C002ED7B4 /* AttachmentPayloadLinkWithoutImagePreview.json */; }; AC908384268B115F00ACFB8E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC908383268B115F00ACFB8E /* AppDelegate.swift */; }; @@ -968,7 +972,7 @@ C121E841274544AE00023E4C /* FileUploadPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88381E64258258C20047A6A3 /* FileUploadPayload.swift */; }; C121E842274544AE00023E4C /* MutedChannelPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DA57DF2631E80D00FA8C53 /* MutedChannelPayload.swift */; }; C121E843274544AE00023E4C /* RawJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B54C3E25C80E940041B357 /* RawJSON.swift */; }; - C121E844274544AE00023E4C /* UnknownEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A43CAE26A9A25000302763 /* UnknownEvent.swift */; }; + C121E844274544AE00023E4C /* UnknownChannelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A43CAE26A9A25000302763 /* UnknownChannelEvent.swift */; }; C121E845274544AE00023E4C /* GuestUserTokenRequestPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A0D64A524E57A520017A3C0 /* GuestUserTokenRequestPayload.swift */; }; C121E846274544AE00023E4C /* MissingEventsRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6ED5F7725027907005D7327 /* MissingEventsRequestBody.swift */; }; C121E847274544AE00023E4C /* ChannelMemberBanRequestPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888E8C4D252B4B1C00195E03 /* ChannelMemberBanRequestPayload.swift */; }; @@ -1384,8 +1388,11 @@ C121EC612746AC8C00023E4C /* StreamChatUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 790881FD25432B7200896F03 /* StreamChatUI.framework */; platformFilter = ios; }; C121EC622746AC8C00023E4C /* StreamChatUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 790881FD25432B7200896F03 /* StreamChatUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C121EC662746AD0E00023E4C /* StreamChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 799C941B247D2F80001F1104 /* StreamChat.framework */; }; + C1320E0A276B2E0F00A06B35 /* Array+SafeSubscript_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1320E07276B2E0800A06B35 /* Array+SafeSubscript_Tests.swift */; }; C139335F275E7BD800225E7A /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = C139335E275E7BD800225E7A /* Nuke */; }; C1393361275F5D1E00225E7A /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = C1393360275F5D1E00225E7A /* Nuke */; }; + C171041E2768C34E008FB3F2 /* Array+SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = C171041D2768C34E008FB3F2 /* Array+SafeSubscript.swift */; }; + C171041F2768C34E008FB3F2 /* Array+SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = C171041D2768C34E008FB3F2 /* Array+SafeSubscript.swift */; }; C1BE72732732CA62006EB51E /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = C1BE72722732CA62006EB51E /* Nuke */; }; C1FC2F6727416E150062530F /* ResumableData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1BE728A2732CB7B006EB51E /* ResumableData.swift */; }; C1FC2F6827416E150062530F /* ImagePipelineCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1BE72822732CB7B006EB51E /* ImagePipelineCache.swift */; }; @@ -2502,8 +2509,8 @@ 84A1D2ED26AAFDEE00014712 /* TestCustomEventPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCustomEventPayload.swift; sourceTree = ""; }; 84A1D2EF26AB10DB00014712 /* EventDecoder_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDecoder_Tests.swift; sourceTree = ""; }; 84A1D2F326AB221E00014712 /* ChannelEventsController_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEventsController_Tests.swift; sourceTree = ""; }; - 84A1D2F526AB357900014712 /* UnknownEvent_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownEvent_Tests.swift; sourceTree = ""; }; - 84A43CAE26A9A25000302763 /* UnknownEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownEvent.swift; sourceTree = ""; }; + 84A1D2F526AB357900014712 /* UnknownChannelEvent_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownChannelEvent_Tests.swift; sourceTree = ""; }; + 84A43CAE26A9A25000302763 /* UnknownChannelEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownChannelEvent.swift; sourceTree = ""; }; 84A43CB226A9A54700302763 /* EventSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSender.swift; sourceTree = ""; }; 84AA4E3526F264610056A684 /* EventDTOConverterMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDTOConverterMiddleware.swift; sourceTree = ""; }; 84ABB014269F0A84003A4585 /* EventsController+Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EventsController+Combine.swift"; sourceTree = ""; }; @@ -2729,12 +2736,15 @@ 8AE335A524FCF999002B6677 /* Reachability_Vendor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reachability_Vendor.swift; sourceTree = ""; }; 8AE335A624FCF999002B6677 /* InternetConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternetConnection.swift; sourceTree = ""; }; 8AE8950B24D85FF200E852EC /* PingPongEmojiFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PingPongEmojiFormatter.swift; sourceTree = ""; }; + A30C3F1F276B428F00DA5968 /* UnknownUserEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownUserEvent.swift; sourceTree = ""; }; + A30C3F21276B4F8800DA5968 /* UnknownUserEvent_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownUserEvent_Tests.swift; sourceTree = ""; }; A30DEC98260B47DE0066E8CE /* TitleContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleContainerView.swift; sourceTree = ""; }; A35757C62613081B00DC914C /* ComposerKeyboardHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposerKeyboardHandler.swift; sourceTree = ""; }; A396B752260CCE7400D8D15B /* TitleContainerView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleContainerView_Tests.swift; sourceTree = ""; }; A39A8AE6263825F4003453D9 /* ChatMessageLayoutOptionsResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageLayoutOptionsResolver.swift; sourceTree = ""; }; A3BB3FFE261DA74D00365496 /* ContainerStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerStackView.swift; sourceTree = ""; }; A3C0D773261CA25700A8A1A2 /* ContainerStackView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerStackView_Tests.swift; sourceTree = ""; }; + A3EA3327276C904700C84A52 /* ObjcAssociatedWeakObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcAssociatedWeakObject.swift; sourceTree = ""; }; AC73783926A6AF1C002ED7B4 /* AttachmentPayloadLinkWithoutImagePreview.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = AttachmentPayloadLinkWithoutImagePreview.json; sourceTree = ""; }; AC908381268B115F00ACFB8E /* YouTube.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = YouTube.app; sourceTree = BUILT_PRODUCTS_DIR; }; AC908383268B115F00ACFB8E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -2858,6 +2868,7 @@ BDEB9416268211EC00928AF1 /* ChatMessageListUnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageListUnreadCountView.swift; sourceTree = ""; }; C121E758274543D000023E4C /* libStreamChat.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libStreamChat.a; sourceTree = BUILT_PRODUCTS_DIR; }; C121EA2F2746A19400023E4C /* libStreamChatUI.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libStreamChatUI.a; sourceTree = BUILT_PRODUCTS_DIR; }; + C1320E07276B2E0800A06B35 /* Array+SafeSubscript_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SafeSubscript_Tests.swift"; sourceTree = ""; }; C13C74D2273932D200F93B34 /* UIImageView+SwiftyGif.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+SwiftyGif.swift"; sourceTree = ""; }; C13C74D3273932D200F93B34 /* NSImage+SwiftyGif.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImage+SwiftyGif.swift"; sourceTree = ""; }; C13C74D4273932D200F93B34 /* UIImage+SwiftyGif.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+SwiftyGif.swift"; sourceTree = ""; }; @@ -2883,6 +2894,7 @@ C13C7508273936DA00F93B34 /* Engine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Engine.swift; sourceTree = ""; }; C13C7509273936DA00F93B34 /* NativeEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeEngine.swift; sourceTree = ""; }; C13C750A273936DA00F93B34 /* WSEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WSEngine.swift; sourceTree = ""; }; + C171041D2768C34E008FB3F2 /* Array+SafeSubscript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SafeSubscript.swift"; sourceTree = ""; }; C1BE72762732CB7B006EB51E /* ImageViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewExtensions.swift; sourceTree = ""; }; C1BE72772732CB7B006EB51E /* FetchImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchImage.swift; sourceTree = ""; }; C1BE72792732CB7B006EB51E /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; @@ -3889,9 +3901,11 @@ 88DA57E92631E82B00FA8C53 /* MutedChannelPayload_Tests.swift */, 22B54C3E25C80E940041B357 /* RawJSON.swift */, 22CAFA7525CAE278005935D9 /* RawJSON_Tests.swift */, - 84A43CAE26A9A25000302763 /* UnknownEvent.swift */, - 84A1D2F526AB357900014712 /* UnknownEvent_Tests.swift */, + 84A43CAE26A9A25000302763 /* UnknownChannelEvent.swift */, + 84A1D2F526AB357900014712 /* UnknownChannelEvent_Tests.swift */, 430156DB26B1862C0006E7EA /* CustomDataHashMap_Tests.swift */, + A30C3F1F276B428F00DA5968 /* UnknownUserEvent.swift */, + A30C3F21276B4F8800DA5968 /* UnknownUserEvent_Tests.swift */, ); path = Payloads; sourceTree = ""; @@ -4855,6 +4869,8 @@ 640FE0F526A57903006CE703 /* CollectionUpdatesMapper_Tests.swift */, ACD502A826BC0C670029FB7D /* ImageMerger.swift */, ACCA772D26C568D8007AE2ED /* NukeImageProcessor.swift */, + C171041D2768C34E008FB3F2 /* Array+SafeSubscript.swift */, + C1320E07276B2E0800A06B35 /* Array+SafeSubscript_Tests.swift */, ); path = Utils; sourceTree = ""; @@ -5521,11 +5537,12 @@ C13C74D1273932D200F93B34 /* StreamSwiftyGif */ = { isa = PBXGroup; children = ( - C13C74D2273932D200F93B34 /* UIImageView+SwiftyGif.swift */, C13C74D3273932D200F93B34 /* NSImage+SwiftyGif.swift */, - C13C74D4273932D200F93B34 /* UIImage+SwiftyGif.swift */, - C13C74D5273932D200F93B34 /* SwiftyGifManager.swift */, C13C74D6273932D200F93B34 /* NSImageView+SwiftyGif.swift */, + A3EA3327276C904700C84A52 /* ObjcAssociatedWeakObject.swift */, + C13C74D5273932D200F93B34 /* SwiftyGifManager.swift */, + C13C74D4273932D200F93B34 /* UIImage+SwiftyGif.swift */, + C13C74D2273932D200F93B34 /* UIImageView+SwiftyGif.swift */, ); path = StreamSwiftyGif; sourceTree = ""; @@ -6909,6 +6926,7 @@ 224FF6812562F2E900725DD1 /* ChatChannelUnreadCountView.swift in Sources */, C1FC2F7827416E150062530F /* ImageDecoding.swift in Sources */, C1FC2F8227416E150062530F /* ImagePipelineTask.swift in Sources */, + A3EA3328276C904700C84A52 /* ObjcAssociatedWeakObject.swift in Sources */, F6E5E3472627A372007FA51F /* CGRect+Extensions.swift in Sources */, C1FC2F7E27416E150062530F /* ImageProcessing.swift in Sources */, E7DF7E2425C2C67E00AE9D21 /* ChatAvatarView.swift in Sources */, @@ -6945,6 +6963,7 @@ AD447443263AC6A10030E583 /* ChatMentionSuggestionView.swift in Sources */, E7166CBA25BED29200B03B07 /* Appearance+Fonts.swift in Sources */, E7073A6325DD67B3003896B9 /* UILabel+Extensions.swift in Sources */, + C171041E2768C34E008FB3F2 /* Array+SafeSubscript.swift in Sources */, 843F0BC526775D2D00B342CB /* VideoLoading.swift in Sources */, 88CABC4625933EE70061BB67 /* ChatReactionPickerBubbleView.swift in Sources */, F838F6A92636D3C30025E1F5 /* ZoomTransitionController.swift in Sources */, @@ -7075,6 +7094,7 @@ ADFCCD7525C9D4D10024505D /* AssertSnapshot.swift in Sources */, F838F6CE263713090025E1F5 /* GalleryVC_Tests.swift in Sources */, 8897305E265D046D00F83739 /* ChatMessageLayoutOptionsResolver_Tests.swift in Sources */, + C1320E0A276B2E0F00A06B35 /* Array+SafeSubscript_Tests.swift in Sources */, F86615D9264940A80026814A /* ChatMessageGalleryView_Tests.swift in Sources */, 79B4F14A25D3F1120063FFB5 /* TemporaryData.swift in Sources */, 8893FF16265FC60B00DD62BE /* ChatMessageErrorIndicator_Tests.swift in Sources */, @@ -7402,13 +7422,14 @@ C1FC2F9B2742579D0062530F /* FrameCollector.swift in Sources */, 8A0C3BBC24C0947400CAFD19 /* UserEvents.swift in Sources */, DA4EE5B2252B67F500CB26D4 /* UserListController+SwiftUI.swift in Sources */, + A30C3F20276B428F00DA5968 /* UnknownUserEvent.swift in Sources */, 79280F7A248918FD00CDEB89 /* StarscreamWebSocketEngine.swift in Sources */, 7963BD6926B0208900281F8C /* ChatMessageAudioAttachment.swift in Sources */, 697C6F90260CFA37000E9023 /* Deprecations.swift in Sources */, 7964F3A4249A0ACF002A09EC /* ChannelListQueryDTO.swift in Sources */, 79280F3F2484E3BA00CDEB89 /* ClientError.swift in Sources */, 884C61222594A449008B70DC /* AttachmentActionRequestBody.swift in Sources */, - 84A43CAF26A9A25000302763 /* UnknownEvent.swift in Sources */, + 84A43CAF26A9A25000302763 /* UnknownChannelEvent.swift in Sources */, DAFAD6A124DC476A0043ED06 /* Result+Extensions.swift in Sources */, C1FC2F982742579D0062530F /* TCPTransport.swift in Sources */, AD7DFC3625D2FA8100DD9DA3 /* CurrentUserUpdater.swift in Sources */, @@ -7496,6 +7517,7 @@ F6CCA24F2512491B004C1859 /* UserTypingStateUpdaterMiddleware_Tests.swift in Sources */, DA8407332526003D005A0F62 /* UserListUpdater_Tests.swift in Sources */, F69E7F7D24ED7562000F5252 /* CurrentUserController_Tests.swift in Sources */, + A30C3F22276B4F8800DA5968 /* UnknownUserEvent_Tests.swift in Sources */, 7922F30A24DAE3B600C364BC /* EntityDatabaseObserver_Tests.swift in Sources */, DA84074025260CA3005A0F62 /* UserListController_Tests.swift in Sources */, 84A1D2EE26AAFDEE00014712 /* TestCustomEventPayload.swift in Sources */, @@ -7630,7 +7652,7 @@ 843C53AB269370A900C7D8EA /* ImageAttachmentPayload_Tests.swift in Sources */, 792FCB4D24A3D56D000290C7 /* DatabaseSession_Tests.swift in Sources */, 430156F226B4523A0006E7EA /* WebSocketConnectPayload_Tests.swift in Sources */, - 84A1D2F626AB357900014712 /* UnknownEvent_Tests.swift in Sources */, + 84A1D2F626AB357900014712 /* UnknownChannelEvent_Tests.swift in Sources */, F62BE7852506309B00D13B86 /* MissingEventsPayload_Tests.swift in Sources */, 7922F30824DACF1F00C364BC /* TestManagedObject.swift in Sources */, 792B805424D95FC700C2963E /* RandomDispatchQueue.swift in Sources */, @@ -7904,7 +7926,7 @@ C121E841274544AE00023E4C /* FileUploadPayload.swift in Sources */, C121E842274544AE00023E4C /* MutedChannelPayload.swift in Sources */, C121E843274544AE00023E4C /* RawJSON.swift in Sources */, - C121E844274544AE00023E4C /* UnknownEvent.swift in Sources */, + C121E844274544AE00023E4C /* UnknownChannelEvent.swift in Sources */, C121E845274544AE00023E4C /* GuestUserTokenRequestPayload.swift in Sources */, C121E846274544AE00023E4C /* MissingEventsRequestBody.swift in Sources */, C121E847274544AE00023E4C /* ChannelMemberBanRequestPayload.swift in Sources */, @@ -8228,6 +8250,7 @@ C121EBD82746A1EA00023E4C /* ChatMessageReactionsView.swift in Sources */, C121EBD92746A1EA00023E4C /* ChatMessageReactionsVC.swift in Sources */, C121EBDA2746A1EA00023E4C /* ChatReactionPickerBubbleView.swift in Sources */, + A3EA3329276C904700C84A52 /* ObjcAssociatedWeakObject.swift in Sources */, C121EBDB2746A1EA00023E4C /* ChatMessageDefaultReactionsBubbleView.swift in Sources */, C121EBDC2746A1EA00023E4C /* ChatMessageReactionsBubbleTail.swift in Sources */, C121EBDD2746A1EA00023E4C /* ChatMessageReactionItemView.swift in Sources */, @@ -8264,6 +8287,7 @@ C121EBFB2746A1EB00023E4C /* ChatMessageListVCDataSource.swift in Sources */, C121EBFC2746A1EB00023E4C /* ChatMessageListVCDelegate.swift in Sources */, C121EBFD2746A1EB00023E4C /* ChatMessageListView.swift in Sources */, + C171041F2768C34E008FB3F2 /* Array+SafeSubscript.swift in Sources */, C121EBFE2746A1EB00023E4C /* ChatMessageListScrollOverlayView.swift in Sources */, C121EBFF2746A1EB00023E4C /* ChatMessageListUnreadCountView.swift in Sources */, C121EC002746A1EB00023E4C /* ScrollToLatestMessageButton.swift in Sources */, @@ -8747,9 +8771,9 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = EHV7XZLAHA; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -8765,6 +8789,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.stream.StreamChatUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; @@ -8777,9 +8802,9 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = EHV7XZLAHA; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -8796,6 +8821,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.stream.StreamChatUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; @@ -8808,9 +8834,9 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = EHV7XZLAHA; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -8827,6 +8853,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = io.stream.StreamChatUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; @@ -9192,9 +9219,9 @@ ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = EHV7XZLAHA; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -9211,6 +9238,8 @@ MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.StreamChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_VERSION = 5.0; @@ -9225,9 +9254,9 @@ ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = EHV7XZLAHA; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -9244,6 +9273,8 @@ MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.StreamChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_VERSION = 5.0; @@ -9620,9 +9651,9 @@ ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = EHV7XZLAHA; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -9640,6 +9671,8 @@ MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.StreamChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_VERSION = 5.0; diff --git a/StreamChatArtifacts.json b/StreamChatArtifacts.json index 9e26dfeeb6e..82f499d3678 100644 --- a/StreamChatArtifacts.json +++ b/StreamChatArtifacts.json @@ -1 +1 @@ -{} \ No newline at end of file +{"4.6.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.6.0/StreamChat-All.zip"} \ No newline at end of file diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec index 1106ad5e2e9..b020bb6cc32 100644 --- a/StreamChatUI-XCFramework.podspec +++ b/StreamChatUI-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "StreamChatUI-XCFramework" - spec.version = "4.5.0" + spec.version = "4.6.0" spec.summary = "StreamChat UI Components" spec.description = "StreamChatUI SDK offers flexible UI components able to display data provided by StreamChat SDK." diff --git a/StreamChatUI.podspec b/StreamChatUI.podspec index d335b7fa6b6..36d7caeb2cd 100644 --- a/StreamChatUI.podspec +++ b/StreamChatUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "StreamChatUI" - spec.version = "4.5.2" + spec.version = "4.6.0" spec.summary = "StreamChat UI Components" spec.description = "StreamChatUI SDK offers flexible UI components able to display data provided by StreamChat SDK." diff --git a/docusaurus/docs/iOS/swiftui/components/attachments.md b/docusaurus/docs/iOS/swiftui/components/attachments.md index 7d6c192d089..a7501c371e2 100644 --- a/docusaurus/docs/iOS/swiftui/components/attachments.md +++ b/docusaurus/docs/iOS/swiftui/components/attachments.md @@ -57,11 +57,13 @@ class CustomFactory: ViewFactory { func makeMessageTextView( for message: ChatMessage, isFirst: Bool, - availableWidth: CGFloat + availableWidth: CGFloat, + scrolledId: Binding ) -> some View { CustomMessageTextView( message: message, - isFirst: isFirst + isFirst: isFirst, + scrolledId: scrolledId ) } @@ -146,7 +148,8 @@ Next, in our `CustomFactory`, we need to return the new view we have created abo func makeCustomAttachmentViewType( for message: ChatMessage, isFirst: Bool, - availableWidth: CGFloat + availableWidth: CGFloat, + scrolledId: Binding ) -> some View { CustomAttachmentView( message: message, diff --git a/docusaurus/docs/iOS/swiftui/components/inline-replies.md b/docusaurus/docs/iOS/swiftui/components/inline-replies.md new file mode 100644 index 00000000000..9d8cb392d87 --- /dev/null +++ b/docusaurus/docs/iOS/swiftui/components/inline-replies.md @@ -0,0 +1,47 @@ +--- +title: Message inline replies +--- + +## Inline Replies Overview + +The SwiftUI SDK has support for inline message replies. These replies are invoked either by swiping left on a message, or via long pressing a message and selecting the "Reply" action. When a message is quoted, it appears in the message composer, as an indication which message the user is replying to. + +## Customizing the Quoted Message + +When a message appears as quoted in the composer, you can customize two parts. First, there's a header at the top of the composer, which by default has a title and a button to remove the quoted message from the composer. To swap this view with your own implementation, you need to implement the `makeQuotedMessageHeaderView` method in the `ViewFactory`, which passes a binding of the quoted chat message. + +```swift +public func makeQuotedMessageHeaderView( + quotedMessage: Binding +) -> some View { + CustomQuotedMessageHeaderView(quotedMessage: quotedMessage) +} +``` + +The second customization that can be done is to swap the preview of the message shown in the composer. The default implementation is able to present several different attachments, such as text, image, video, gifs, links and files. The UI consists of small image and either the message text (if present), or a description for the attachment. + +In order to swap this UI, you need to implement the `makeQuotedMessageComposerView` method in the `ViewFactory`. The method passes the quoted message as a parameter. + +```swift +public func makeQuotedMessageComposerView( + quotedMessage: ChatMessage +) -> some View { + QuotedMessageViewContainer( + quotedMessage: quotedMessage, + fillAvailableSpace: true, + forceLeftToRight: true, + scrolledId: .constant(nil) + ) +} +``` + + +Finally, we need to inject the your `CustomFactory` in our view hierarchy. + +```swift +var body: some Scene { + WindowGroup { + ChatChannelListView(viewFactory: CustomFactory.shared) + } +} +``` \ No newline at end of file diff --git a/docusaurus/docs/iOS/swiftui/components/message-reactions.md b/docusaurus/docs/iOS/swiftui/components/message-reactions.md index cc847673ab0..9014146b778 100644 --- a/docusaurus/docs/iOS/swiftui/components/message-reactions.md +++ b/docusaurus/docs/iOS/swiftui/components/message-reactions.md @@ -42,17 +42,17 @@ The reactions overlay view (shown on long press of a message), also provides acc public func supportedMessageActions( for message: ChatMessage, channel: ChatChannel, - onDismiss: @escaping () -> Void, + onFinish: @escaping (MessageActionInfo) -> Void, onError: @escaping (Error) -> Void ) -> [MessageAction] { MessageAction.defaultActions( - factory: self, - for: message, - channel: channel, - chatClient: chatClient, - onDismiss: onDismiss, - onError: onError - ) + factory: self, + for: message, + channel: channel, + chatClient: chatClient, + onFinish: onFinish, + onError: onError + ) } extension MessageAction { @@ -61,23 +61,24 @@ extension MessageAction { for message: ChatMessage, channel: ChatChannel, chatClient: ChatClient, - onDismiss: @escaping () -> Void, + onFinish: @escaping (MessageActionInfo) -> Void, onError: @escaping (Error) -> Void ) -> [MessageAction] { var messageActions = [MessageAction]() + let replyAction = replyAction( + for: message, + channel: channel, + onFinish: onFinish + ) + messageActions.append(replyAction) + if !message.isPartOfThread { - var replyThread = MessageAction( - title: L10n.Message.Actions.threadReply, - iconName: "icn_thread_reply", - action: onDismiss, - confirmationPopup: nil, - isDestructive: false + let replyThread = threadReplyAction( + factory: factory, + for: message, + channel: channel ) - - let destination = factory.makeMessageThreadDestination() - replyThread.navigationDestination = AnyView(destination(channel, message)) - messageActions.append(replyThread) } @@ -86,7 +87,7 @@ extension MessageAction { for: message, channel: channel, chatClient: chatClient, - onDismiss: onDismiss, + onFinish: onFinish, onError: onError ) @@ -96,7 +97,7 @@ extension MessageAction { for: message, channel: channel, chatClient: chatClient, - onDismiss: onDismiss, + onFinish: onFinish, onError: onError ) @@ -106,11 +107,54 @@ extension MessageAction { return messageActions } + // MARK: - private + + private static func replyAction( + for message: ChatMessage, + channel: ChatChannel, + onFinish: @escaping (MessageActionInfo) -> Void + ) -> MessageAction { + let replyAction = MessageAction( + title: L10n.Message.Actions.inlineReply, + iconName: "icn_inline_reply", + action: { + onFinish( + MessageActionInfo( + message: message, + identifier: "inlineReply" + ) + ) + }, + confirmationPopup: nil, + isDestructive: false + ) + + return replyAction + } + + private static func threadReplyAction( + factory: Factory, + for message: ChatMessage, + channel: ChatChannel + ) -> MessageAction { + var replyThread = MessageAction( + title: L10n.Message.Actions.threadReply, + iconName: "icn_thread_reply", + action: {}, + confirmationPopup: nil, + isDestructive: false + ) + + let destination = factory.makeMessageThreadDestination() + replyThread.navigationDestination = AnyView(destination(channel, message)) + return replyThread + } + private static func deleteMessageAction( for message: ChatMessage, channel: ChatChannel, chatClient: ChatClient, - onDismiss: @escaping () -> Void, + onFinish: @escaping (MessageActionInfo) -> Void, onError: @escaping (Error) -> Void ) -> MessageAction { let messageController = chatClient.messageController( @@ -123,7 +167,12 @@ extension MessageAction { if let error = error { onError(error) } else { - onDismiss() + onFinish( + MessageActionInfo( + message: message, + identifier: "delete" + ) + ) } } } @@ -149,7 +198,7 @@ extension MessageAction { for message: ChatMessage, channel: ChatChannel, chatClient: ChatClient, - onDismiss: @escaping () -> Void, + onFinish: @escaping (MessageActionInfo) -> Void, onError: @escaping (Error) -> Void ) -> MessageAction { let messageController = chatClient.messageController( @@ -162,7 +211,12 @@ extension MessageAction { if let error = error { onError(error) } else { - onDismiss() + onFinish( + MessageActionInfo( + message: message, + identifier: "flag" + ) + ) } } } @@ -183,24 +237,25 @@ extension MessageAction { return flagMessage } +} ``` Alternatively, you can swap the whole `MessageActionsView` with your own implementation, by implementing the `makeMessageActionsView` method in the `ViewFactory`. ```swift public func makeMessageActionsView( - for message: ChatMessage, - channel: ChatChannel, - onDismiss: @escaping () -> Void, - onError: @escaping (Error) -> Void + for message: ChatMessage, + channel: ChatChannel, + onFinish: @escaping (MessageActionInfo) -> Void, + onError: @escaping (Error) -> Void ) -> some View { let messageActions = supportedMessageActions( for: message, channel: channel, - onDismiss: onDismiss, + onFinish: onFinish, onError: onError ) - + return MessageActionsView(messageActions: messageActions) } ``` @@ -209,17 +264,19 @@ Additionally, you can swap the whole `ReactionsOverlayView` with your own implem ```swift public func makeReactionsOverlayView( - channel: ChatChannel, - currentSnapshot: UIImage, - messageDisplayInfo: MessageDisplayInfo, - onBackgroundTap: @escaping () -> Void + channel: ChatChannel, + currentSnapshot: UIImage, + messageDisplayInfo: MessageDisplayInfo, + onBackgroundTap: @escaping () -> Void, + onActionExecuted: @escaping (MessageActionInfo) -> Void ) -> some View { ReactionsOverlayView( factory: self, channel: channel, currentSnapshot: currentSnapshot, messageDisplayInfo: messageDisplayInfo, - onBackgroundTap: onBackgroundTap + onBackgroundTap: onBackgroundTap, + onActionExecuted: onActionExecuted ) } ``` \ No newline at end of file diff --git a/docusaurus/sidebars-ios.json b/docusaurus/sidebars-ios.json index 2948c1c46d0..805a1ba3a54 100644 --- a/docusaurus/sidebars-ios.json +++ b/docusaurus/sidebars-ios.json @@ -64,7 +64,8 @@ "swiftui/components/attachments", "swiftui/components/message-composer", "swiftui/components/message-reactions", - "swiftui/components/message-threads" + "swiftui/components/message-threads", + "swiftui/components/inline-replies" ] } ] diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c5e8bee683c..1a8e05d8fce 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -31,9 +31,6 @@ end desc "Start a new release" lane :release do |options| - # Ensure we are on the main branch - ensure_git_branch(branch: 'main') - # Ensure we have a clean git status ensure_git_status_clean unless options[:no_ensure_clean] @@ -68,7 +65,7 @@ lane :release do |options| changes = touch_changelog(release_version: version_number) # Make sure the podspecs actually build before pushing - # Disabled now since `StreamChatUI` pod lints it against `StreamChat`s latest version instead of `main` branch + # Disabled now since `StreamChatUI` pod lints it against `StreamChat`s latest version instead of `develop` branch #pod_lib_lint(podspec: "StreamChat.podspec", allow_warnings: true) #pod_lib_lint(podspec: "StreamChatUI.podspec", allow_warnings: true) @@ -113,6 +110,31 @@ lane :publish_release do |options| version = options[:version] + # Update Frameworks (WIP) + # update_frameworks + + # Create Github Release + github_release = set_github_release( + repository_name: "GetStream/stream-chat-swift", + api_token: ENV["GITHUB_TOKEN"], + name: version, + tag_name: version, + description: changes + # upload_assets: ["Products/StreamChat.zip", "Products/StreamChatUI.zip", "Products/StreamChat-All.zip"] + ) + push_pods(sync: options[:sync]) + + # Set tag + sh("git tag #{version}") + + # Push tag to remote + push_to_git_remote(tags: true) + + UI.success("Github release was created, please visit #{github_release["url"]} to see it") +end + +desc "Update XCFrameworks and submit to the SPM repository" +lane :update_frameworks do |options| # Make xcframeworks sh("cd .. && make frameworks") @@ -121,12 +143,13 @@ lane :publish_release do |options| streamChatUIChecksum = sh("swift package compute-checksum ../Products/StreamChatUI.zip").strip # Update SPM Repo - sh("git clone git@github.com:GetStream/stream-chat-swift-spm.git ../StreamSPM") - file = File.open("../StreamSPM/Package.swift") + sh("git clone git@github.com:GetStream/stream-chat-swift-spm.git ../../StreamSPM") + sh("cd ../../StreamSPM") + file = File.open("Package.swift") file_data = file.read # [WIP] We can improve using regex here - fileLines = File.readlines("../StreamSPM/Package.swift") + fileLines = File.readlines("Package.swift") # We are unfortunately having to dig into a file and manually change the lines required fileLines.each_with_index do |line, index| @@ -149,58 +172,54 @@ lane :publish_release do |options| end # Write the new changes - File.open("../StreamSPM/Package.swift", "w") { |file| file << file_data } + File.open("./Package.swift", "w") { |file| file << file_data } # Update the repo - sh("cd ../StreamSPM && git add -A") + sh("git add -A") sh("git commit -m 'Bump #{version}'") sh("git tag #{version}") sh("git push origin main --force --tags") # Clean Up - sh("cd ../") - sh("rm -r StreamSPM") - - # Create Github Release - github_release = set_github_release( - repository_name: "GetStream/stream-chat-swift", - api_token: ENV["GITHUB_TOKEN"], - name: version, - tag_name: version, - description: changes, - upload_assets: ["Products/StreamChat.zip", "Products/StreamChatUI.zip", "Products/StreamChat-All.zip"] - ) - # The & operator makes sure truthy values are converted to bool - # and falsy (false and nil) values are converted to bool false - push_pods(sync: options[:sync] & true) - - # Set tag - sh("git tag #{version}") - - # Push tag to remote - push_to_git_remote(tags: true) - - UI.success("Github release was created, please visit #{github_release["url"]} to see it") + sh("rm -r ../../StreamSPM") end desc "Pushes the StreamChat and StreamChatUI SDK podspecs to Cocoapods trunk" lane :push_pods do |options| - # First pod release will not have any problems - pod_push(path: "StreamChat.podspec", allow_warnings: true) - def release_ui(sync) + def release_pod(sync:, podspec:, dry:) begin - pod_push(path: "StreamChatUI.podspec", allow_warnings: true, synchronous: sync) - rescue - puts "pod_push failed. Waiting a minute until retry for trunk to get updated..." + UI.message "Starting to push podspec: #{podspec}" + + if dry == false + pod_push(path: podspec, allow_warnings: true, synchronous: sync) + end + rescue => exception + UI.message exception + UI.message "pod_push failed for #{podspec}. Waiting a minute until retry for trunk to get updated..." sleep(60) # sleep for a minute, wait until trunk gets updates - release_ui(sync) + release_pod(sync: sync, podspec: podspec, dry: dry) end end - puts "Sleeping for 2 minutes for trunk to get updated..." - sleep(60 * 2) - release_ui(options[:sync]) + # The == 1 comparison makes sure truthy values are converted to bool + # and falsy (false and nil) values are converted to bool false + + # When sync is false, pod trunk push is run asynchronously + sync = options[:sync] == 1 + + # When dry option is specified - `dry:1`, the lane is executed in test/safe mode + # that won't cause pod_push action to fire. + dry = options[:dry] == 1 + + ["StreamChat.podspec", "StreamChatUI.podspec"].each do |podspec| + release_pod(sync: sync, podspec: podspec, dry: dry) + end + + ["StreamChat-XCFramework.podspec", "StreamChatUI-XCFramework.podspec"].each do |podspec| + release_pod(sync: sync, podspec: podspec, dry: dry) + end + end lane :set_SDK_version do |options|