diff --git a/.github/workflows/xcmetrics.yml b/.github/workflows/sdk-performance-metrics.yml
similarity index 94%
rename from .github/workflows/xcmetrics.yml
rename to .github/workflows/sdk-performance-metrics.yml
index 49476662126..e9ac86a6076 100644
--- a/.github/workflows/xcmetrics.yml
+++ b/.github/workflows/sdk-performance-metrics.yml
@@ -1,4 +1,4 @@
-name: Performance Benchmarks
+name: SDK Performance
on:
schedule:
@@ -16,8 +16,8 @@ env:
HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
jobs:
- xcmetrics:
- name: XCMetrics
+ performance:
+ name: Metrics
runs-on: macos-14
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_GITHUB_TOKEN }}'
@@ -39,7 +39,7 @@ jobs:
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
INSTALL_GCLOUD: true
- - name: Run Performance Metrics
+ - name: Run XCMetrics
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
run: bundle exec fastlane xcmetrics
timeout-minutes: 120
diff --git a/.github/workflows/sdk-size-metrics.yml b/.github/workflows/sdk-size-metrics.yml
new file mode 100644
index 00000000000..e88d63c6738
--- /dev/null
+++ b/.github/workflows/sdk-size-metrics.yml
@@ -0,0 +1,43 @@
+name: SDK Size
+
+on:
+ pull_request:
+
+ workflow_dispatch:
+
+ push:
+ branches:
+ - develop
+
+env:
+ HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
+
+jobs:
+ sdk_size:
+ name: Metrics
+ runs-on: macos-14
+ env:
+ GITHUB_TOKEN: '${{ secrets.CI_BOT_GITHUB_TOKEN }}'
+ steps:
+ - name: Install Bot SSH Key
+ uses: webfactory/ssh-agent@v0.7.0
+ with:
+ ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }}
+
+ - uses: actions/checkout@v3.1.0
+
+ - uses: ./.github/actions/bootstrap
+
+ - name: Get branch name
+ id: get_branch_name
+ run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
+
+ - name: Run SDK Size Metrics
+ run: bundle exec fastlane show_frameworks_sizes
+ timeout-minutes: 30
+ env:
+ BRANCH_NAME: ${{ steps.get_branch_name.outputs.branch }}
+ GITHUB_PR_NUM: ${{ github.event.pull_request.number }}
+ GITHUB_EVENT_NAME: ${{ github.event_name }}
+ MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+ APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml
index 490899d6c84..44e7a7b905e 100644
--- a/.github/workflows/smoke-checks.yml
+++ b/.github/workflows/smoke-checks.yml
@@ -193,6 +193,7 @@ jobs:
env:
ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_PR_NUM: ${{ github.event.number }}
GITHUB_EVENT: ${{ toJson(github.event) }}
- id: get_launch_id
run: echo "launch_id=${{env.LAUNCH_ID}}" >> $GITHUB_OUTPUT
diff --git a/.gitignore b/.gitignore
index 972569a5fcc..84ae32ab7e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,12 +54,6 @@ playground.xcworkspace
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
Pods/
-# Carthage
-#
-# Add this line if you want to avoid checking in source code from Carthage dependencies.
-Carthage/
-!Sample/Carthage/
-
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
@@ -77,13 +71,12 @@ fastlane/allurectl
fastlane/xcresults
fastlane/recordings
fastlane/performance
+fastlane/metrics
StreamChatCore.framework.coverage.txt
StreamChatCoreTests.xctest.coverage.txt
vendor/bundle/
.bundle/
.swiftpm
-Example/Carthage/.env
-Example/Carthage/fastlane/report.xml
Sample/Cocoapods/Podfile.lock
docusaurus/.env
reports/
diff --git a/.swiftlint.yml b/.swiftlint.yml
index 2817534709e..679c72cdd85 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -12,12 +12,12 @@ excluded:
- UISDKdocumentation
- Tests
- TestTools
- - Carthage
- Pods
- .build
- spm_cache
- vendor/bundle
- .ruby-lsp
+ - derived_data
disabled_rules:
- large_tuple
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23672e5054b..ec55282a178 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### π Changed
+# [4.61.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.61.0)
+_July 30, 2024_
+
+## StreamChat
+### β‘ Performance
+- Improve performance of `ChatChannel` database model conversions more than 7 times [#3325](https://github.com/GetStream/stream-chat-swift/pull/3325)
+- Improve performance of `ChatChannel` and `ChatMessage` equality checks [#3335](https://github.com/GetStream/stream-chat-swift/pull/3335)
+### β
Added
+- Expose `MissingConnectionId` + `InvalidURL` + `InvalidJSON` Errors [#3332](https://github.com/GetStream/stream-chat-swift/pull/3332)
+- Add support for `.hasUnread` filter key to `ChannelListQuery` [#3340](https://github.com/GetStream/stream-chat-swift/pull/3340)
+### π Fixed
+- Fix a rare issue with incorrect message order when sending multiple messages while offline [#3316](https://github.com/GetStream/stream-chat-swift/issues/3316)
+- Fix sorting channel list by unread count [#3340](https://github.com/GetStream/stream-chat-swift/pull/3340)
+
+## StreamChatUI
+### π Fixed
+- Fix message search not showing results for new search terms [#3345](https://github.com/GetStream/stream-chat-swift/pull/3345)
+
# [4.60.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.60.0)
_July 18, 2024_
@@ -20,13 +38,11 @@ _July 18, 2024_
- Increase QoS for `Throttler` and `Debouncer` to `utility` [#3297](https://github.com/GetStream/stream-chat-swift/issues/3297)
- Improve reliability of accessing data in controllers' completion handlers [#3305](https://github.com/GetStream/stream-chat-swift/issues/3305)
-## StreamChatUI
-### π Fixed
-- Fix Channel List not hiding error state view when data is available [#3303](https://github.com/GetStream/stream-chat-swift/pull/3303)
-
## StreamChatUI
### β
Added
- Add support for enabling message list view animations [#3314](https://github.com/GetStream/stream-chat-swift/pull/3314)
+### π Fixed
+- Fix Channel List not hiding error state view when data is available [#3303](https://github.com/GetStream/stream-chat-swift/pull/3303)
# [4.59.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.59.0)
_July 10, 2024_
diff --git a/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift b/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
index 116ed847418..a35b339d629 100644
--- a/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
+++ b/DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
@@ -41,6 +41,11 @@ final class DemoChatChannelListVC: ChatChannelListVC {
.equal(.hidden, to: true)
]))
+ lazy var unreadChannelsQuery: ChannelListQuery = .init(filter: .and([
+ .containMembers(userIds: [currentUserId]),
+ .hasUnread
+ ]), sort: [.init(key: .unreadCount, isAscending: false)])
+
lazy var mutedChannelsQuery: ChannelListQuery = .init(filter: .and([
.containMembers(userIds: [currentUserId]),
.equal(.muted, to: true)
@@ -113,6 +118,15 @@ final class DemoChatChannelListVC: ChatChannelListVC {
}
)
+ let unreadChannelsAction = UIAlertAction(
+ title: "Unread Channels",
+ style: .default,
+ handler: { [weak self] _ in
+ self?.title = "Unread Channels"
+ self?.setUnreadChannelsQuery()
+ }
+ )
+
let coolChannelsAction = UIAlertAction(
title: "Cool Channels",
style: .default,
@@ -133,7 +147,13 @@ final class DemoChatChannelListVC: ChatChannelListVC {
presentAlert(
title: "Filter Channels",
- actions: [defaultChannelsAction, hiddenChannelsAction, mutedChannelsAction, coolChannelsAction],
+ actions: [
+ defaultChannelsAction,
+ unreadChannelsAction,
+ hiddenChannelsAction,
+ mutedChannelsAction,
+ coolChannelsAction
+ ],
preferredStyle: .actionSheet,
sourceView: filterChannelsButton
)
@@ -143,6 +163,10 @@ final class DemoChatChannelListVC: ChatChannelListVC {
replaceQuery(hiddenChannelsQuery)
}
+ func setUnreadChannelsQuery() {
+ replaceQuery(unreadChannelsQuery)
+ }
+
func setMutedChannelsQuery() {
replaceQuery(mutedChannelsQuery)
}
diff --git a/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift b/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
index 961581b92cc..f59bf755a34 100644
--- a/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
+++ b/DemoApp/StreamChat/StreamChatWrapper+DemoApp.swift
@@ -14,7 +14,7 @@ extension StreamChatWrapper {
}
// Set the log level
- LogConfig.level = .warning
+ LogConfig.level = StreamRuntimeCheck.logLevel ?? .warning
LogConfig.formatters = [
PrefixLogFormatter(prefixes: [.info: "βΉοΈ", .debug: "π ", .warning: "β οΈ", .error: "π¨"])
]
diff --git a/DemoApp/StreamRuntimeCheck+StreamInternal.swift b/DemoApp/StreamRuntimeCheck+StreamInternal.swift
index 5da00f0062c..32de841931d 100644
--- a/DemoApp/StreamRuntimeCheck+StreamInternal.swift
+++ b/DemoApp/StreamRuntimeCheck+StreamInternal.swift
@@ -9,4 +9,10 @@ extension StreamRuntimeCheck {
static var isStreamInternalConfiguration: Bool {
ProcessInfo.processInfo.environment["STREAM_DEV"] != nil
}
+
+ static var logLevel: LogLevel? {
+ guard let value = ProcessInfo.processInfo.environment["STREAM_LOG_LEVEL"] else { return nil }
+ guard let intValue = Int(value) else { return nil }
+ return LogLevel(rawValue: intValue)
+ }
}
diff --git a/Documentation.docc/Documentation.md b/Documentation.docc/Documentation.md
index 371c67f9a38..478da115da8 100644
--- a/Documentation.docc/Documentation.md
+++ b/Documentation.docc/Documentation.md
@@ -17,7 +17,7 @@ This is the official iOS SDK for [Stream Chat](https://getstream.io/chat/sdk/ios
* [iOS/Swift Chat Tutorial](https://getstream.io/tutorials/ios-chat/): Learn how to use the SDK by following our simple tutorial.
* [Register](https://getstream.io/chat/trial/): Register to get an API key for Stream Chat.
-* [Installation](https://getstream.io/chat/docs/sdk/ios/basics/integration): Learn more about how to install the SDK using CocoaPods, SPM or Carthage.
+* [Installation](https://getstream.io/chat/docs/sdk/ios/basics/integration): Learn more about how to install the SDK using SPM or CocoaPods.
* Do you want to use Module Stable XCFrameworks? [Check this out](https://getstream.io/chat/docs/sdk/ios/basics/integration#xcframeworks)
* [Documentation](https://getstream.io/chat/docs/sdk/ios/): An extensive documentation is available to help with you integration.
* [SwiftUI](https://github.com/GetStream/stream-chat-swiftui): Check our SwiftUI SDK if you are developing with SwiftUI.
diff --git a/README.md b/README.md
index 20c36cc7c4f..a972e18dbfb 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,6 @@
-
@@ -14,8 +13,8 @@
-
-
+
+
This is the official iOS SDK for [Stream Chat](https://getstream.io/chat/sdk/ios/), a service for building chat and messaging applications. This library includes both a low-level SDK and a set of reusable UI components.
@@ -49,7 +48,7 @@ The **StreamChatSwiftUI SDK** is our UI SDK for SwiftUI components. If your appl
- [iOS/Swift Chat Tutorial](https://getstream.io/tutorials/ios-chat/): Learn how to use the SDK by following our simple tutorial with UIKit (or [SwiftUI](https://getstream.io/tutorials/swiftui-chat/)).
- [Register](https://getstream.io/chat/trial/): Register to get an API key for Stream Chat.
-- [Installation](https://getstream.io/chat/docs/sdk/ios/basics/integration): Learn more about how to install the SDK using CocoaPods, SPM or Carthage.
+- [Installation](https://getstream.io/chat/docs/sdk/ios/basics/integration): Learn more about how to install the SDK using SPM or CocoaPods.
- Do you want to use Module Stable XCFrameworks? [Check this out](https://getstream.io/chat/docs/sdk/ios/basics/integration#xcframeworks)
- [Documentation](https://getstream.io/chat/docs/sdk/ios/): An extensive documentation is available to help with you integration.
- [SwiftUI](https://github.com/GetStream/stream-chat-swiftui): Check our SwiftUI SDK if you are developing with SwiftUI.
diff --git a/Scripts/carthage.sh b/Scripts/carthage.sh
deleted file mode 100644
index 4e004da4203..00000000000
--- a/Scripts/carthage.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-# carthage.sh
-# Usage example: ./carthage.sh build --platform iOS
-
-set -euo pipefail
-
-xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX)
-trap 'rm -f "$xcconfig"' INT TERM HUP EXIT
-
-# For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise
-# the build will fail on lipo due to duplicate architectures.
-
-CURRENT_XCODE_VERSION=$(xcodebuild -version | grep "Build version" | cut -d' ' -f3)
-echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$CURRENT_XCODE_VERSION = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig
-
-echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> $xcconfig
-echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig
-
-export XCODE_XCCONFIG_FILE="$xcconfig"
-carthage "$@"
\ No newline at end of file
diff --git a/Sources/StreamChat/APIClient/RequestEncoder.swift b/Sources/StreamChat/APIClient/RequestEncoder.swift
index 96b815f4042..2c7a6d767c4 100644
--- a/Sources/StreamChat/APIClient/RequestEncoder.swift
+++ b/Sources/StreamChat/APIClient/RequestEncoder.swift
@@ -303,7 +303,7 @@ protocol ConnectionDetailsProviderDelegate: AnyObject {
func provideToken(timeout: TimeInterval, completion: @escaping (Result) -> Void)
}
-extension ClientError {
+public extension ClientError {
final class InvalidURL: ClientError {}
final class InvalidJSON: ClientError {}
final class MissingConnectionId: ClientError {}
diff --git a/Sources/StreamChat/ChatClient.swift b/Sources/StreamChat/ChatClient.swift
index 3a91fe5ec46..c38b82bd560 100644
--- a/Sources/StreamChat/ChatClient.swift
+++ b/Sources/StreamChat/ChatClient.swift
@@ -660,6 +660,7 @@ extension ChatClient: ConnectionStateDelegate {
}
)
connectionRecoveryHandler?.webSocketClient(client, didUpdateConnectionState: state)
+ try? backgroundWorker(of: MessageSender.self).didUpdateConnectionState(state)
}
}
@@ -674,6 +675,21 @@ extension ChatClient: ConnectionDetailsProviderDelegate {
}
}
+extension ChatClient {
+ func backgroundWorker(of type: T.Type) throws -> T {
+ if let worker = backgroundWorkers.compactMap({ $0 as? T }).first {
+ return worker
+ }
+ if currentUserId == nil {
+ throw ClientError.CurrentUserDoesNotExist()
+ }
+ if !config.isClientInActiveMode {
+ throw ClientError.ClientIsNotInActiveMode()
+ }
+ throw ClientError("Background worker of type \(T.self) is not set up")
+ }
+}
+
extension ClientError {
public final class MissingLocalStorageURL: ClientError {
override public var localizedDescription: String { "The URL provided in ChatClientConfig is `nil`." }
diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
index c2817b14c1b..a46fdcfc1f0 100644
--- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift
@@ -61,6 +61,7 @@ class ChannelDTO: NSManagedObject {
@NSManaged var messages: Set
@NSManaged var pinnedMessages: Set
@NSManaged var reads: Set
+ @NSManaged var currentUserUnreadMessagesCount: Int32
@NSManaged var watchers: Set
@NSManaged var memberListQueries: Set
@NSManaged var previewMessage: MessageDTO?
@@ -75,6 +76,16 @@ class ChannelDTO: NSManagedObject {
return
}
+ // Update the unreadMessagesCount for the current user.
+ // At the moment this computed property is used for `hasUnread` automatic channel list filtering.
+ if let currentUserId = managedObjectContext?.currentUser?.user.id {
+ let currentUserUnread = reads.first(where: { $0.user.id == currentUserId })
+ let newUnreadCount = currentUserUnread?.unreadMessageCount ?? 0
+ if newUnreadCount != currentUserUnreadMessagesCount {
+ currentUserUnreadMessagesCount = newUnreadCount
+ }
+ }
+
// Change to the `truncatedAt` value have effect on messages, we need to mark them dirty manually
// to triggers related FRC updates
if changedValues().keys.contains("truncatedAt") {
@@ -434,70 +445,75 @@ extension ChatChannel {
)
extraData = [:]
}
-
+
+ let sortedMessageDTOs = dto.messages.sorted(by: { $0.createdAt.bridgeDate > $1.createdAt.bridgeDate })
let reads: [ChatChannelRead] = try dto.reads.map { try $0.asModel() }
-
let unreadCount: ChannelUnreadCount = {
- guard let currentUser = context.currentUser else {
+ guard let currentUserDTO = context.currentUser else {
return .noUnread
}
-
- let currentUserRead = reads.first(where: { $0.user.id == currentUser.user.id })
-
+ let currentUserRead = reads.first(where: { $0.user.id == currentUserDTO.user.id })
let allUnreadMessages = currentUserRead?.unreadMessagesCount ?? 0
-
- // Fetch count of all mentioned messages after last read
- // (this is not 100% accurate but it's the best we have)
- let unreadMentionsRequest = NSFetchRequest(entityName: MessageDTO.entityName)
- unreadMentionsRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
- MessageDTO.channelMessagesPredicate(
- for: dto.cid,
- deletedMessagesVisibility: context.deletedMessagesVisibility ?? .visibleForCurrentUser,
- shouldShowShadowedMessages: context.shouldShowShadowedMessages ?? false
- ),
- NSPredicate(format: "createdAt > %@", currentUserRead?.lastReadAt.bridgeDate ?? DBDate(timeIntervalSince1970: 0)),
- NSPredicate(format: "%@ IN mentionedUsers", currentUser.user)
- ])
-
- do {
- return ChannelUnreadCount(
- messages: allUnreadMessages,
- mentions: try context.count(for: unreadMentionsRequest)
- )
- } catch {
- log.error("Failed to fetch unread counts for channel `\(cid)`. Error: \(error)")
+ // Therefore, no unread messages with mentions and we can skip the fetch
+ if allUnreadMessages == 0 {
return .noUnread
}
+ let unreadMentionsCount = sortedMessageDTOs
+ .prefix(allUnreadMessages)
+ .filter { $0.mentionedUsers.contains(currentUserDTO.user) }
+ .count
+ return ChannelUnreadCount(
+ messages: allUnreadMessages,
+ mentions: unreadMentionsCount
+ )
}()
- let messages: [ChatMessage] = {
- MessageDTO
- .load(
- for: dto.cid,
- limit: dto.managedObjectContext?.localCachingSettings?.chatChannel.latestMessagesLimit ?? 25,
- deletedMessagesVisibility: dto.managedObjectContext?.deletedMessagesVisibility ?? .visibleForCurrentUser,
- shouldShowShadowedMessages: dto.managedObjectContext?.shouldShowShadowedMessages ?? false,
- context: context
- )
+ let latestMessages: [ChatMessage] = {
+ var messages = sortedMessageDTOs
+ .prefix(dto.managedObjectContext?.localCachingSettings?.chatChannel.latestMessagesLimit ?? 25)
.compactMap { try? $0.relationshipAsModel(depth: depth) }
+ if let oldest = dto.oldestMessageAt?.bridgeDate {
+ messages = messages.filter { $0.createdAt >= oldest }
+ }
+ if let truncated = dto.truncatedAt?.bridgeDate {
+ messages = messages.filter { $0.createdAt >= truncated }
+ }
+ return messages
}()
let latestMessageFromUser: ChatMessage? = {
- guard let currentUser = context.currentUser else { return nil }
-
- return try? MessageDTO
- .loadLastMessage(
- from: currentUser.user.id,
- in: dto.cid,
- context: context
- )?
+ guard let currentUserId = context.currentUser?.user.id else { return nil }
+ return try? sortedMessageDTOs
+ .first(where: { messageDTO in
+ guard messageDTO.user.id == currentUserId else { return false }
+ guard messageDTO.localMessageState == nil else { return false }
+ return messageDTO.type != MessageType.ephemeral.rawValue
+ })?
.relationshipAsModel(depth: depth)
}()
-
- let watchers = UserDTO.loadLastActiveWatchers(cid: cid, context: context)
+
+ let watchers = dto.watchers
+ .sorted { lhs, rhs in
+ let lhsActivity = lhs.lastActivityAt?.bridgeDate ?? .distantPast
+ let rhsActivity = rhs.lastActivityAt?.bridgeDate ?? .distantPast
+ if lhsActivity == rhsActivity {
+ return lhs.id > rhs.id
+ }
+ return lhsActivity > rhsActivity
+ }
+ .prefix(context.localCachingSettings?.chatChannel.lastActiveWatchersLimit ?? 100)
.compactMap { try? $0.asModel() }
- let members = MemberDTO.loadLastActiveMembers(cid: cid, context: context)
+ let members = dto.members
+ .sorted { lhs, rhs in
+ let lhsActivity = lhs.user.lastActivityAt?.bridgeDate ?? .distantPast
+ let rhsActivity = rhs.user.lastActivityAt?.bridgeDate ?? .distantPast
+ if lhsActivity == rhsActivity {
+ return lhs.id > rhs.id
+ }
+ return lhsActivity > rhsActivity
+ }
+ .prefix(context.localCachingSettings?.chatChannel.lastActiveMembersLimit ?? 100)
.compactMap { try? $0.asModel() }
let muteDetails: MuteDetails? = {
@@ -539,7 +555,7 @@ extension ChatChannel {
reads: reads,
cooldownDuration: Int(dto.cooldownDuration),
extraData: extraData,
- latestMessages: messages,
+ latestMessages: latestMessages,
lastMessageFromCurrentUser: latestMessageFromUser,
pinnedMessages: pinnedMessages,
muteDetails: muteDetails,
diff --git a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
index f9f58da5d09..d869a6e8c4f 100644
--- a/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MemberModelDTO.swift
@@ -92,17 +92,6 @@ extension MemberDTO {
new.id = memberId
return new
}
-
- static func loadLastActiveMembers(cid: ChannelId, context: NSManagedObjectContext) -> [MemberDTO] {
- let request = NSFetchRequest(entityName: MemberDTO.entityName)
- request.predicate = NSPredicate(format: "channel.cid == %@", cid.rawValue)
- request.sortDescriptors = [
- ChannelMemberListSortingKey.lastActiveSortDescriptor,
- ChannelMemberListSortingKey.defaultSortDescriptor
- ]
- request.fetchLimit = context.localCachingSettings?.chatChannel.lastActiveMembersLimit ?? 100
- return load(by: request, context: context)
- }
}
extension NSManagedObjectContext {
diff --git a/Sources/StreamChat/Database/DTOs/MessageDTO.swift b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
index 853b6ec1ef1..81b05497ff1 100644
--- a/Sources/StreamChat/Database/DTOs/MessageDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/MessageDTO.swift
@@ -516,19 +516,6 @@ class MessageDTO: NSManagedObject {
return (try? context.count(for: request)) ?? 0
}
- static func loadLastMessage(from userId: String, in cid: String, context: NSManagedObjectContext) -> MessageDTO? {
- let request = NSFetchRequest(entityName: entityName)
- request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
- channelPredicate(with: cid),
- .init(format: "user.id == %@", userId),
- .init(format: "type != %@", MessageType.ephemeral.rawValue),
- messageSentPredicate()
- ])
- request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.createdAt, ascending: false)]
- request.fetchLimit = 1
- return load(by: request, context: context).first
- }
-
static func loadSendingMessages(context: NSManagedObjectContext) -> [MessageDTO] {
let request = NSFetchRequest(entityName: MessageDTO.entityName)
request.sortDescriptors = [NSSortDescriptor(keyPath: \MessageDTO.locallyCreatedAt, ascending: false)]
@@ -1309,21 +1296,28 @@ private extension ChatMessage {
if let currentUser = context.currentUser {
isSentByCurrentUser = currentUser.user.id == dto.user.id
- currentUserReactions = Set(
- MessageReactionDTO
- .loadReactions(ids: dto.ownReactions, context: context)
- .compactMap { try? $0.asModel() }
- )
+ if !dto.ownReactions.isEmpty {
+ currentUserReactions = Set(
+ MessageReactionDTO
+ .loadReactions(ids: dto.ownReactions, context: context)
+ .compactMap { try? $0.asModel() }
+ )
+ } else {
+ currentUserReactions = []
+ }
} else {
isSentByCurrentUser = false
currentUserReactions = []
}
- latestReactions = Set(
- MessageReactionDTO
- .loadReactions(ids: dto.latestReactions, context: context)
- .compactMap { try? $0.asModel() }
- )
+ latestReactions = {
+ guard !dto.latestReactions.isEmpty else { return Set() }
+ return Set(
+ MessageReactionDTO
+ .loadReactions(ids: dto.latestReactions, context: context)
+ .compactMap { try? $0.asModel() }
+ )
+ }()
threadParticipants = dto.threadParticipants.array
.compactMap { $0 as? UserDTO }
@@ -1337,8 +1331,10 @@ private extension ChatMessage {
.sorted { $0.id.index < $1.id.index }
latestReplies = {
- guard !dto.replies.isEmpty else { return [] }
- return MessageDTO.loadReplies(for: dto.id, limit: 5, context: context)
+ guard dto.replyCount > 0 else { return [] }
+ return dto.replies
+ .sorted(by: { $0.createdAt.bridgeDate > $1.createdAt.bridgeDate })
+ .prefix(5)
.compactMap { try? ChatMessage(fromDTO: $0, depth: depth) }
}()
diff --git a/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift b/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift
index 16531e3eb07..1c6fe0dc214 100644
--- a/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/QueuedRequestDTO.swift
@@ -39,6 +39,10 @@ class QueuedRequestDTO: NSManagedObject {
}
extension NSManagedObjectContext: QueuedRequestDatabaseSession {
+ func allQueuedRequests() -> [QueuedRequestDTO] {
+ QueuedRequestDTO.loadAllPendingRequests(context: self)
+ }
+
func deleteQueuedRequest(id: String) {
guard let request = QueuedRequestDTO.load(id: id, context: self) else { return }
delete(request)
diff --git a/Sources/StreamChat/Database/DTOs/UserDTO.swift b/Sources/StreamChat/Database/DTOs/UserDTO.swift
index 2d6430c30ad..483fe21e457 100644
--- a/Sources/StreamChat/Database/DTOs/UserDTO.swift
+++ b/Sources/StreamChat/Database/DTOs/UserDTO.swift
@@ -115,17 +115,6 @@ extension UserDTO {
new.teams = []
return new
}
-
- static func loadLastActiveWatchers(cid: ChannelId, context: NSManagedObjectContext) -> [UserDTO] {
- let request = NSFetchRequest(entityName: UserDTO.entityName)
- request.sortDescriptors = [
- UserListSortingKey.lastActiveSortDescriptor,
- UserListSortingKey.defaultSortDescriptor
- ]
- request.predicate = NSPredicate(format: "ANY watchedChannels.cid == %@", cid.rawValue)
- request.fetchLimit = context.localCachingSettings?.chatChannel.lastActiveWatchersLimit ?? 100
- return load(by: request, context: context)
- }
}
extension NSManagedObjectContext: UserDatabaseSession {
diff --git a/Sources/StreamChat/Database/DatabaseSession.swift b/Sources/StreamChat/Database/DatabaseSession.swift
index efbeb3994a0..6fcc6401993 100644
--- a/Sources/StreamChat/Database/DatabaseSession.swift
+++ b/Sources/StreamChat/Database/DatabaseSession.swift
@@ -393,6 +393,7 @@ protocol AttachmentDatabaseSession {
}
protocol QueuedRequestDatabaseSession {
+ func allQueuedRequests() -> [QueuedRequestDTO]
func deleteQueuedRequest(id: String)
}
diff --git a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
index 2cea52d961d..dd35fc9f1f1 100644
--- a/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
+++ b/Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
@@ -37,6 +37,7 @@
+
diff --git a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
index 79375f4c08f..683e9527056 100644
--- a/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
+++ b/Sources/StreamChat/Generated/SystemEnvironment+Version.swift
@@ -7,5 +7,5 @@ import Foundation
extension SystemEnvironment {
/// A Stream Chat version.
- public static let version: String = "4.60.0"
+ public static let version: String = "4.61.0"
}
diff --git a/Sources/StreamChat/Info.plist b/Sources/StreamChat/Info.plist
index 5b6e9a4e712..892b781b6c3 100644
--- a/Sources/StreamChat/Info.plist
+++ b/Sources/StreamChat/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.60.0
+ 4.61.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/Sources/StreamChat/Models/Channel.swift b/Sources/StreamChat/Models/Channel.swift
index 9b797b2bac0..a89243367a2 100644
--- a/Sources/StreamChat/Models/Channel.swift
+++ b/Sources/StreamChat/Models/Channel.swift
@@ -237,27 +237,28 @@ extension ChatChannel: AnyChannel {}
extension ChatChannel: Hashable {
public static func == (lhs: ChatChannel, rhs: ChatChannel) -> Bool {
- lhs.cid == rhs.cid &&
- lhs.updatedAt == rhs.updatedAt &&
- lhs.cooldownDuration == rhs.cooldownDuration &&
- lhs.createdAt == rhs.createdAt &&
- lhs.createdBy == rhs.createdBy &&
- lhs.deletedAt == rhs.deletedAt &&
- lhs.extraData == rhs.extraData &&
- lhs.imageURL == rhs.imageURL &&
- lhs.isFrozen == rhs.isFrozen &&
- lhs.isHidden == rhs.isHidden &&
- lhs.lastMessageAt == rhs.lastMessageAt &&
- lhs.memberCount == rhs.memberCount &&
- lhs.membership == rhs.membership &&
- lhs.muteDetails == rhs.muteDetails &&
- lhs.name == rhs.name &&
- lhs.ownCapabilities == rhs.ownCapabilities &&
- lhs.previewMessage == rhs.previewMessage &&
- lhs.reads == rhs.reads &&
- lhs.team == rhs.team &&
- lhs.truncatedAt == rhs.truncatedAt &&
- lhs.watcherCount == rhs.watcherCount
+ guard lhs.cid == rhs.cid else { return false }
+ guard lhs.updatedAt == rhs.updatedAt else { return false }
+ guard lhs.lastMessageAt == rhs.lastMessageAt else { return false }
+ guard lhs.muteDetails == rhs.muteDetails else { return false }
+ guard lhs.reads == rhs.reads else { return false }
+ guard lhs.previewMessage == rhs.previewMessage else { return false }
+ guard lhs.name == rhs.name else { return false }
+ guard lhs.watcherCount == rhs.watcherCount else { return false }
+ guard lhs.createdAt == rhs.createdAt else { return false }
+ guard lhs.cooldownDuration == rhs.cooldownDuration else { return false }
+ guard lhs.createdBy == rhs.createdBy else { return false }
+ guard lhs.deletedAt == rhs.deletedAt else { return false }
+ guard lhs.extraData == rhs.extraData else { return false }
+ guard lhs.imageURL == rhs.imageURL else { return false }
+ guard lhs.isFrozen == rhs.isFrozen else { return false }
+ guard lhs.isHidden == rhs.isHidden else { return false }
+ guard lhs.memberCount == rhs.memberCount else { return false }
+ guard lhs.membership == rhs.membership else { return false }
+ guard lhs.team == rhs.team else { return false }
+ guard lhs.truncatedAt == rhs.truncatedAt else { return false }
+ guard lhs.ownCapabilities == rhs.ownCapabilities else { return false }
+ return true
}
public func hash(into hasher: inout Hasher) {
diff --git a/Sources/StreamChat/Models/ChatMessage.swift b/Sources/StreamChat/Models/ChatMessage.swift
index 6b86bf28227..948155d79a9 100644
--- a/Sources/StreamChat/Models/ChatMessage.swift
+++ b/Sources/StreamChat/Models/ChatMessage.swift
@@ -359,29 +359,30 @@ public extension ChatMessage {
extension ChatMessage: Hashable {
public static func == (lhs: Self, rhs: Self) -> Bool {
- lhs.id == rhs.id &&
- lhs.updatedAt == rhs.updatedAt &&
- lhs.allAttachments == rhs.allAttachments &&
- lhs.arguments == rhs.arguments &&
- lhs.author == rhs.author &&
- lhs.command == rhs.command &&
- lhs.currentUserReactionsCount == rhs.currentUserReactionsCount &&
- lhs.extraData == rhs.extraData &&
- lhs.isFlaggedByCurrentUser == rhs.isFlaggedByCurrentUser &&
- lhs.isShadowed == rhs.isShadowed &&
- lhs.localState == rhs.localState &&
- lhs.parentMessageId == rhs.parentMessageId &&
- lhs.quotedMessage == rhs.quotedMessage &&
- lhs.reactionCounts == rhs.reactionCounts &&
- lhs.reactionGroups == rhs.reactionGroups &&
- lhs.reactionScores == rhs.reactionScores &&
- lhs.readByCount == rhs.readByCount &&
- lhs.replyCount == rhs.replyCount &&
- lhs.showReplyInChannel == rhs.showReplyInChannel &&
- lhs.text == rhs.text &&
- lhs.threadParticipantsCount == rhs.threadParticipantsCount &&
- lhs.translations == rhs.translations &&
- lhs.type == rhs.type
+ guard lhs.id == rhs.id else { return false }
+ guard lhs.localState == rhs.localState else { return false }
+ guard lhs.updatedAt == rhs.updatedAt else { return false }
+ guard lhs.allAttachments == rhs.allAttachments else { return false }
+ guard lhs.author == rhs.author else { return false }
+ guard lhs.currentUserReactionsCount == rhs.currentUserReactionsCount else { return false }
+ guard lhs.text == rhs.text else { return false }
+ guard lhs.parentMessageId == rhs.parentMessageId else { return false }
+ guard lhs.reactionCounts == rhs.reactionCounts else { return false }
+ guard lhs.reactionGroups == rhs.reactionGroups else { return false }
+ guard lhs.reactionScores == rhs.reactionScores else { return false }
+ guard lhs.readByCount == rhs.readByCount else { return false }
+ guard lhs.replyCount == rhs.replyCount else { return false }
+ guard lhs.showReplyInChannel == rhs.showReplyInChannel else { return false }
+ guard lhs.threadParticipantsCount == rhs.threadParticipantsCount else { return false }
+ guard lhs.arguments == rhs.arguments else { return false }
+ guard lhs.command == rhs.command else { return false }
+ guard lhs.extraData == rhs.extraData else { return false }
+ guard lhs.isFlaggedByCurrentUser == rhs.isFlaggedByCurrentUser else { return false }
+ guard lhs.isShadowed == rhs.isShadowed else { return false }
+ guard lhs.quotedMessage == rhs.quotedMessage else { return false }
+ guard lhs.translations == rhs.translations else { return false }
+ guard lhs.type == rhs.type else { return false }
+ return true
}
public func hash(into hasher: inout Hasher) {
diff --git a/Sources/StreamChat/Query/ChannelListQuery.swift b/Sources/StreamChat/Query/ChannelListQuery.swift
index f77560b8422..54b989fa6c3 100644
--- a/Sources/StreamChat/Query/ChannelListQuery.swift
+++ b/Sources/StreamChat/Query/ChannelListQuery.swift
@@ -25,6 +25,11 @@ public extension Filter where Scope: AnyChannelListFilterScope {
static var noTeam: Filter {
.equal(.team, to: nil)
}
+
+ /// Filter for fetching only the unread channels.
+ static var hasUnread: Filter {
+ .equal(.hasUnread, to: true)
+ }
}
extension Filter where Scope: AnyChannelListFilterScope {
@@ -162,6 +167,28 @@ public extension FilterKey where Scope: AnyChannelListFilterScope {
static var lastUpdatedAt: FilterKey { .init(rawValue: "last_updated", keyPathString: #keyPath(ChannelDTO.lastMessageAt)) }
}
+/// Internal filter queries for the channel list.
+/// These ones are helpers that should be used by an higher-level filter.
+internal extension FilterKey where Scope: AnyChannelListFilterScope {
+ /// Filter for fetching only the unread channels.
+ /// Supported operators: `equal`, and only `true` is supported.
+ static var hasUnread: FilterKey {
+ .init(
+ rawValue: "has_unread",
+ keyPathString: nil,
+ predicateMapper: { op, hasUnread in
+ let key = #keyPath(ChannelDTO.currentUserUnreadMessagesCount)
+ switch op {
+ case .equal:
+ return NSPredicate(format: hasUnread ? "\(key) > 0" : "\(key) <= 0")
+ default:
+ return nil
+ }
+ }
+ )
+ }
+}
+
/// A query is used for querying specific channels from backend.
/// You can specify filter, sorting, pagination, limit for fetched messages in channel and other options.
public struct ChannelListQuery: Encodable {
diff --git a/Sources/StreamChat/Query/Filter.swift b/Sources/StreamChat/Query/Filter.swift
index 65f55ad6e43..6c3d02ff4a1 100644
--- a/Sources/StreamChat/Query/Filter.swift
+++ b/Sources/StreamChat/Query/Filter.swift
@@ -263,7 +263,7 @@ public struct FilterKey: ExpressibleBySt
init(
rawValue value: String,
- keyPathString: String,
+ keyPathString: String?,
valueMapper: TypedValueMapper? = nil,
isCollectionFilter: Bool = false,
predicateMapper: TypedPredicateMapper? = nil
diff --git a/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift b/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift
index 28d190c15c9..a4d52d1e390 100644
--- a/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift
+++ b/Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift
@@ -49,8 +49,11 @@ public struct ChannelListSortingKey: SortingKey, Equatable {
remoteKey: ChannelCodingKeys.cid.rawValue
)
- /// Sort channels by unread state. When using this sorting key, every unread channel weighs the same,
- /// so they're sorted by `updatedAt`
+ /// Sort channels by unread state.
+ ///
+ /// When using this sorting key, every unread channel weighs the same, so they're sorted by `updatedAt`.
+ ///
+ /// **Note:** If you want to sort by number of unreads, you should use the `unreadCount` sorting key.
public static let hasUnread = Self(
keyPath: \.hasUnread,
localKey: nil,
@@ -60,7 +63,7 @@ public struct ChannelListSortingKey: SortingKey, Equatable {
/// Sort channels by their unread count.
public static let unreadCount = Self(
keyPath: \.unreadCount,
- localKey: nil,
+ localKey: #keyPath(ChannelDTO.currentUserUnreadMessagesCount),
remoteKey: "unread_count"
)
diff --git a/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift b/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift
index 1125b4a01e3..9ccfb8ade38 100644
--- a/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift
+++ b/Sources/StreamChat/Query/Sorting/ChannelMemberListSortingKey.swift
@@ -33,11 +33,6 @@ extension ChannelMemberListSortingKey {
return .init(keyPath: dateKeyPath, ascending: false)
}()
- static let lastActiveSortDescriptor: NSSortDescriptor = {
- let dateKeyPath: KeyPath = \MemberDTO.user.lastActivityAt
- return .init(keyPath: dateKeyPath, ascending: false)
- }()
-
func sortDescriptor(isAscending: Bool) -> NSSortDescriptor {
.init(key: rawValue, ascending: isAscending)
}
diff --git a/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift b/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift
index 4237ccdea03..9b618c5a13c 100644
--- a/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift
+++ b/Sources/StreamChat/Query/Sorting/UserListSortingKey.swift
@@ -39,11 +39,6 @@ extension UserListSortingKey {
return .init(keyPath: stringKeyPath, ascending: false)
}()
- static let lastActiveSortDescriptor: NSSortDescriptor = {
- let dateKeyPath: KeyPath = \UserDTO.lastActivityAt
- return .init(keyPath: dateKeyPath, ascending: false)
- }()
-
func sortDescriptor(isAscending: Bool) -> NSSortDescriptor? {
.init(key: rawValue, ascending: isAscending)
}
diff --git a/Sources/StreamChat/Repositories/MessageRepository.swift b/Sources/StreamChat/Repositories/MessageRepository.swift
index 77593ead67d..60f9046a047 100644
--- a/Sources/StreamChat/Repositories/MessageRepository.swift
+++ b/Sources/StreamChat/Repositories/MessageRepository.swift
@@ -88,6 +88,47 @@ class MessageRepository {
})
}
}
+
+ /// Marks the message's local status to failed and adds it to the offline retry which sends the message when connection comes back.
+ func scheduleOfflineRetry(for messageId: MessageId, completion: @escaping (Result) -> Void) {
+ var dataEndpoint: DataEndpoint!
+ var messageModel: ChatMessage!
+ database.write { session in
+ guard let dto = session.message(id: messageId) else {
+ throw MessageRepositoryError.messageDoesNotExist
+ }
+ guard let channelDTO = dto.channel, let cid = try? ChannelId(cid: channelDTO.cid) else {
+ throw MessageRepositoryError.messageDoesNotHaveValidChannel
+ }
+
+ // Send the message to offline handling
+ let requestBody = dto.asRequestBody() as MessageRequestBody
+ let endpoint: Endpoint = .sendMessage(
+ cid: cid,
+ messagePayload: requestBody,
+ skipPush: dto.skipPush,
+ skipEnrichUrl: dto.skipEnrichUrl
+ )
+ dataEndpoint = endpoint.withDataResponse
+
+ // Mark it as failed
+ dto.localMessageState = .sendingFailed
+ messageModel = try dto.asModel()
+ } completion: { [weak self] writeError in
+ if let writeError {
+ switch writeError {
+ case let repositoryError as MessageRepositoryError:
+ completion(.failure(repositoryError))
+ default:
+ completion(.failure(.failedToSendMessage(writeError)))
+ }
+ return
+ }
+ // Offline repository will send it when connection comes back on, until then we show the message as failed
+ self?.apiClient.queueOfflineRequest?(dataEndpoint.withDataResponse)
+ completion(.success(messageModel))
+ }
+ }
func saveSuccessfullySentMessage(
cid: ChannelId,
@@ -126,11 +167,12 @@ class MessageRepository {
// error code for duplicated messages.
let isDuplicatedMessageError = errorPayload.code == 4 && errorPayload.message.contains("already exists")
if isDuplicatedMessageError {
- database.write {
+ database.write({
let messageDTO = $0.message(id: messageId)
messageDTO?.markMessageAsSent()
+ }, completion: { _ in
completion(.failure(.failedToSendMessage(error)))
- }
+ })
return
}
}
diff --git a/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift b/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift
index 4a5755f2c63..62adbb24e41 100644
--- a/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift
+++ b/Sources/StreamChat/Repositories/OfflineRequestsRepository.swift
@@ -2,6 +2,7 @@
// Copyright Β© 2024 Stream.io Inc. All rights reserved.
//
+import CoreData
import Foundation
typealias QueueOfflineRequestBlock = (DataEndpoint) -> Void
@@ -51,24 +52,100 @@ class OfflineRequestsRepository {
/// - If the request fails with a connection error -> The request is kept to be executed once the connection is back (we are not putting it back at the queue to make sure we respect the order)
/// - If the request fails with any other error -> We are dismissing the request, and removing it from the queue
func runQueuedRequests(completion: @escaping () -> Void) {
- let readContext = database.backgroundReadOnlyContext
- readContext.perform { [weak self] in
- let requests = QueuedRequestDTO.loadAllPendingRequests(context: readContext).map {
- ($0.id, $0.endpoint, $0.date as Date)
+ database.read { session in
+ let dtos = session.allQueuedRequests()
+ var requests = [Request]()
+ requests.reserveCapacity(dtos.count)
+ var requestIdsToDelete = Set()
+ let currentDate = Date()
+
+ for dto in dtos {
+ let id = dto.id
+ let endpointData = dto.endpoint
+ let date = dto.date.bridgeDate
+
+ // Is valid
+ guard let endpoint = try? JSONDecoder.stream.decode(DataEndpoint.self, from: endpointData) else {
+ log.error("Could not decode queued request \(id)", subsystems: .offlineSupport)
+ requestIdsToDelete.insert(dto.id)
+ continue
+ }
+
+ // Is expired
+ let hoursQueued = currentDate.timeIntervalSince(date) / Constants.secondsInHour
+ let shouldBeDiscarded = hoursQueued > Double(self.maxHoursThreshold)
+ guard endpoint.shouldBeQueuedOffline && !shouldBeDiscarded else {
+ log.error("Queued request for /\(endpoint.path.value) should not be queued", subsystems: .offlineSupport)
+ requestIdsToDelete.insert(dto.id)
+ continue
+ }
+ requests.append(Request(id: id, date: date, endpoint: endpoint))
+ }
+
+ // Out of valid requests, merge send message requests for the same id
+ let sendMessageIdGroups = Dictionary(grouping: requests, by: { $0.sendMessageId })
+ var mergedRequests = [Request]()
+ mergedRequests.reserveCapacity(requests.count)
+ for request in requests {
+ if let sendMessageId = request.sendMessageId {
+ // Is it already merged into another
+ if requestIdsToDelete.contains(request.id) {
+ continue
+ }
+ if let duplicates = sendMessageIdGroups[sendMessageId], duplicates.count >= 2 {
+ // Coalesce send message requests in a way that we use the latest endpoint data
+ // because the message could have changed when there was a manual retry
+ let sortedDuplicates = duplicates.sorted(by: { $0.date < $1.date })
+ let earliest = sortedDuplicates.first!
+ let latest = sortedDuplicates.last!
+ mergedRequests.append(Request(id: earliest.id, date: earliest.date, endpoint: latest.endpoint))
+ // All the others should be deleted
+ requestIdsToDelete.formUnion(duplicates.dropFirst().map(\.id))
+ } else {
+ mergedRequests.append(request)
+ }
+ } else {
+ mergedRequests.append(request)
+ }
}
- DispatchQueue.main.async {
- self?.executeRequests(requests, completion: completion)
+ log.info("\(mergedRequests.count) pending offline requests (coalesced = \(requests.count - mergedRequests.count)", subsystems: .offlineSupport)
+ return (requests: mergedRequests, deleteIds: requestIdsToDelete)
+ } completion: { [weak self] result in
+ switch result {
+ case .success(let pair):
+ self?.deleteRequests(with: pair.deleteIds, completion: {
+ self?.retryQueue.async {
+ self?.executeRequests(pair.requests, completion: completion)
+ }
+ })
+ case .failure(let error):
+ log.error("Failed to read queued requests with error \(error.localizedDescription)", subsystems: .offlineSupport)
+ completion()
}
}
}
-
- private func executeRequests(_ requests: [(String, Data, Date)], completion: @escaping () -> Void) {
- log.info("\(requests.count) pending offline requests", subsystems: .offlineSupport)
-
+
+ private func deleteRequests(with ids: Set, completion: @escaping () -> Void) {
+ guard !ids.isEmpty else {
+ completion()
+ return
+ }
+ database.write { session in
+ for id in ids {
+ session.deleteQueuedRequest(id: id)
+ }
+ } completion: { _ in
+ completion()
+ }
+ }
+
+ private func executeRequests(_ requests: [Request], completion: @escaping () -> Void) {
let database = self.database
- let currentDate = Date()
let group = DispatchGroup()
- for (id, endpoint, date) in requests {
+ for request in requests {
+ let id = request.id
+ let endpoint = request.endpoint
+
group.enter()
let leave = {
group.leave()
@@ -79,21 +156,6 @@ class OfflineRequestsRepository {
}, completion: { _ in leave() })
}
- guard let endpoint = try? JSONDecoder.stream.decode(DataEndpoint.self, from: endpoint) else {
- log.error("Could not decode queued request \(id)", subsystems: .offlineSupport)
- deleteQueuedRequestAndComplete()
- continue
- }
-
- let hoursQueued = currentDate.timeIntervalSince(date) / Constants.secondsInHour
- let shouldBeDiscarded = hoursQueued > Double(maxHoursThreshold)
-
- guard endpoint.shouldBeQueuedOffline && !shouldBeDiscarded else {
- log.error("Queued request for /\(endpoint.path.value) should not be queued", subsystems: .offlineSupport)
- deleteQueuedRequestAndComplete()
- continue
- }
-
log.info("Executing queued offline request for /\(endpoint.path)", subsystems: .offlineSupport)
apiClient.recoveryRequest(endpoint: endpoint) { [weak self] result in
log.info("Completed queued offline request /\(endpoint.path)", subsystems: .offlineSupport)
@@ -177,8 +239,36 @@ class OfflineRequestsRepository {
database.write { _ in
QueuedRequestDTO.createRequest(date: date, endpoint: data, context: database.writableContext)
log.info("Queued request for /\(endpoint.path)", subsystems: .offlineSupport)
+ } completion: { _ in
completion?()
}
}
}
}
+
+private extension OfflineRequestsRepository {
+ struct Request {
+ let id: String
+ let date: Date
+ let endpoint: DataEndpoint
+ let sendMessageId: MessageId?
+
+ init(id: String, date: Date, endpoint: DataEndpoint) {
+ self.id = id
+ self.date = date
+ self.endpoint = endpoint
+
+ sendMessageId = {
+ switch endpoint.path {
+ case .sendMessage:
+ guard let bodyData = endpoint.body as? Data else { return nil }
+ guard let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any] else { return nil }
+ guard let message = json["message"] as? [String: Any] else { return nil }
+ return message["id"] as? String
+ default:
+ return nil
+ }
+ }()
+ }
+ }
+}
diff --git a/Sources/StreamChat/Repositories/SyncRepository.swift b/Sources/StreamChat/Repositories/SyncRepository.swift
index 19ad3d932c9..328d424bb67 100644
--- a/Sources/StreamChat/Repositories/SyncRepository.swift
+++ b/Sources/StreamChat/Repositories/SyncRepository.swift
@@ -132,6 +132,11 @@ class SyncRepository {
// Enter recovery mode so no other requests are triggered.
apiClient.enterRecoveryMode()
+ // Run offline actions requests as the first thing
+ if config.isLocalStorageEnabled {
+ operations.append(ExecutePendingOfflineActions(offlineRequestsRepository: offlineRequestsRepository))
+ }
+
// Get the existing channelIds
let activeChannelIds = activeChannelControllers.allObjects.compactMap(\.cid)
operations.append(GetChannelIdsOperation(database: database, context: context, activeChannelIds: activeChannelIds))
@@ -176,11 +181,6 @@ class SyncRepository {
// 4. Clean up unwanted channels
operations.append(DeleteUnwantedChannelsOperation(database: database, context: context))
- // 5. Run offline actions requests
- if config.isLocalStorageEnabled {
- operations.append(ExecutePendingOfflineActions(offlineRequestsRepository: offlineRequestsRepository))
- }
-
operations.append(BlockOperation(block: { [weak self] in
log.info("Finished recovering offline state", subsystems: .offlineSupport)
DispatchQueue.main.async {
diff --git a/Sources/StreamChat/StateLayer/Chat.swift b/Sources/StreamChat/StateLayer/Chat.swift
index 90c79f413dc..71a6c51f18c 100644
--- a/Sources/StreamChat/StateLayer/Chat.swift
+++ b/Sources/StreamChat/StateLayer/Chat.swift
@@ -1418,20 +1418,3 @@ extension Chat {
) -> TypingEventsSender = TypingEventsSender.init
}
}
-
-// MARK: - Chat Client
-
-private extension ChatClient {
- func backgroundWorker(of type: T.Type) throws -> T {
- if let worker = backgroundWorkers.compactMap({ $0 as? T }).first {
- return worker
- }
- if currentUserId == nil {
- throw ClientError.CurrentUserDoesNotExist()
- }
- if !config.isClientInActiveMode {
- throw ClientError.ClientIsNotInActiveMode()
- }
- throw ClientError("Background worker of type \(T.self) is not set up")
- }
-}
diff --git a/Sources/StreamChat/Workers/Background/MessageSender.swift b/Sources/StreamChat/Workers/Background/MessageSender.swift
index e600e2b8cae..af6d12fbb6b 100644
--- a/Sources/StreamChat/Workers/Background/MessageSender.swift
+++ b/Sources/StreamChat/Workers/Background/MessageSender.swift
@@ -14,10 +14,7 @@ import Foundation
/// 3. When the message is being sent, its local state is changed to `.sending`
/// 4. If the operation is successful, the local state of the message is changed to `nil`. If the operation fails, the local
/// state of is changed to `sendingFailed`.
-///
-// TODO:
-/// - Message send retry
-/// - Start sending messages when connection status changes (offline -> online)
+/// 5. When connection errors happen, all the queued messages are sent to offline retry which retries them one by one.
///
class MessageSender: Worker {
/// Because we need to be sure messages for every channel are sent in the correct order, we create a sending queue for
@@ -117,6 +114,15 @@ class MessageSender: Worker {
}
}
}
+
+ func didUpdateConnectionState(_ state: WebSocketConnectionState) {
+ guard state.isConnected else { return }
+ sendingDispatchQueue.async { [weak self] in
+ self?.sendingQueueByCid.forEach { _, messageQueue in
+ messageQueue.webSocketConnected()
+ }
+ }
+ }
}
// MARK: - Chat State Layer
@@ -170,6 +176,7 @@ private class MessageSendingQueue {
/// We use Set because the message Id is the main identifier. Thanks to this, it's possible to schedule message for sending
/// multiple times without having to worry about that.
@Atomic private(set) var requests: Set = []
+ @Atomic private var isWaitingForConnection = false
/// Schedules sending of the message. All already scheduled messages with `createdLocallyAt` older than these ones will
/// be sent first.
@@ -184,37 +191,57 @@ private class MessageSendingQueue {
sendNextMessage()
}
}
+
+ func webSocketConnected() {
+ guard isWaitingForConnection else { return }
+ isWaitingForConnection = false
+ log.debug("Message sender resumed sending messages after establishing internet connection")
+ sendNextMessage()
+ }
+ private var sortedQueuedRequests: [SendRequest] {
+ requests.sorted(by: { $0.createdLocallyAt < $1.createdLocallyAt })
+ }
+
/// Gets the oldest message from the queue and tries to send it.
private func sendNextMessage() {
dispatchQueue.async { [weak self] in
- // Sort the messages and send the oldest one
- // If this proves to be a bottleneck in the future, we might
- // switch to using a custom `OrderedSet`
- guard let request = self?.requests.sorted(by: { $0.createdLocallyAt < $1.createdLocallyAt }).first else { return }
-
- self?.messageRepository.sendMessage(with: request.messageId) { [weak self] result in
- guard let self else { return }
- self.removeRequestAndContinue(request)
- if let error = result.error {
- switch error {
- case .messageDoesNotExist,
- .messageNotPendingSend,
- .messageDoesNotHaveValidChannel:
- let event = NewMessageErrorEvent(messageId: request.messageId, error: error)
- self.eventsNotificationCenter.process(event)
- case let .failedToSendMessage(error):
- let event = NewMessageErrorEvent(messageId: request.messageId, error: error)
- self.eventsNotificationCenter.process(event)
- }
+ guard let self else { return }
+ guard let request = self.sortedQueuedRequests.first else { return }
+
+ if self.isWaitingForConnection {
+ self.messageRepository.scheduleOfflineRetry(for: request.messageId) { [weak self] _ in
+ self?._requests.mutate { $0.remove(request) }
+ self?.sendNextMessage()
+ }
+ } else {
+ self.messageRepository.sendMessage(with: request.messageId) { [weak self] result in
+ self?.handleSendMessageResult(request, result: result)
}
- self.delegate?.messageSendingQueue(self, didProcess: request.messageId, result: result)
}
}
}
-
- private func removeRequestAndContinue(_ request: SendRequest) {
+
+ private func handleSendMessageResult(_ request: SendRequest, result: Result) {
_requests.mutate { $0.remove(request) }
+
+ if let repositoryError = result.error {
+ switch repositoryError {
+ case .messageDoesNotExist, .messageNotPendingSend, .messageDoesNotHaveValidChannel:
+ let event = NewMessageErrorEvent(messageId: request.messageId, error: repositoryError)
+ eventsNotificationCenter.process(event)
+ case .failedToSendMessage(let clientError):
+ let event = NewMessageErrorEvent(messageId: request.messageId, error: clientError)
+ eventsNotificationCenter.process(event)
+
+ if ClientError.isEphemeral(error: clientError) {
+ // We hit a connection error, therefore all the remaining and upcoming requests should be scheduled for keeping the order
+ isWaitingForConnection = true
+ log.debug("Message sender started waiting for connection and forwarding messages to offline requests queue")
+ }
+ }
+ }
+ delegate?.messageSendingQueue(self, didProcess: request.messageId, result: result)
sendNextMessage()
}
}
diff --git a/Sources/StreamChatUI/ChatChannelList/Search/ChatChannelSearchVC.swift b/Sources/StreamChatUI/ChatChannelList/Search/ChatChannelSearchVC.swift
index 27848e4e6dc..377eedfb838 100644
--- a/Sources/StreamChatUI/ChatChannelList/Search/ChatChannelSearchVC.swift
+++ b/Sources/StreamChatUI/ChatChannelList/Search/ChatChannelSearchVC.swift
@@ -15,7 +15,7 @@ open class ChatChannelSearchVC: ChatChannelListSearchVC {
// MARK: - ChatChannelListSearchVC Abstract Implementations
override open var hasEmptyResults: Bool {
- channels.isEmpty
+ controller.channels.isEmpty
}
override open func loadSearchResults(with text: String) {
diff --git a/Sources/StreamChatUI/ChatChannelList/Search/ChatMessageSearchVC.swift b/Sources/StreamChatUI/ChatChannelList/Search/ChatMessageSearchVC.swift
index ef7cbcddc95..74bedb3f1ea 100644
--- a/Sources/StreamChatUI/ChatChannelList/Search/ChatMessageSearchVC.swift
+++ b/Sources/StreamChatUI/ChatChannelList/Search/ChatMessageSearchVC.swift
@@ -31,7 +31,7 @@ open class ChatMessageSearchVC: ChatChannelListSearchVC, ChatMessageSearchContro
// MARK: - ChatChannelListSearchVC Abstract Implementations
override open var hasEmptyResults: Bool {
- messages.isEmpty
+ messageSearchController.messages.isEmpty
}
override open func loadSearchResults(with text: String) {
diff --git a/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift b/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift
index 4d8f98f6c8b..343605ec1c1 100644
--- a/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift
+++ b/Sources/StreamChatUI/ChatThreadList/ChatThreadListItemView.swift
@@ -242,7 +242,6 @@ open class ChatThreadListItemView: _View, ThemeProvider {
}
let thread = content.thread
- let channel = thread.channel
let latestReply = thread.latestReplies.last
let unreadReplies = thread.reads.first(where: { $0.user.id == content.currentUserId })?.unreadMessagesCount ?? 0
diff --git a/Sources/StreamChatUI/Info.plist b/Sources/StreamChatUI/Info.plist
index 5b6e9a4e712..892b781b6c3 100644
--- a/Sources/StreamChatUI/Info.plist
+++ b/Sources/StreamChatUI/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 4.60.0
+ 4.61.0
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec
index 648426905a8..57d68969502 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.60.0"
+ spec.version = "4.61.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 c66dd1828a5..91e8b571a3f 100644
--- a/StreamChat.podspec
+++ b/StreamChat.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChat"
- spec.version = "4.60.0"
+ spec.version = "4.61.0"
spec.summary = "StreamChat iOS Chat 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 dae7223ce03..8183f46e980 100644
--- a/StreamChat.xcodeproj/project.pbxproj
+++ b/StreamChat.xcodeproj/project.pbxproj
@@ -13882,6 +13882,8 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = StreamChatUITestsApp/StreamChatUITestsApp.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
@@ -13985,6 +13987,8 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+ CODE_SIGN_IDENTITY = "Apple Development";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
diff --git a/StreamChatArtifacts.json b/StreamChatArtifacts.json
index 5f082992027..f79e8530749 100644
--- a/StreamChatArtifacts.json
+++ b/StreamChatArtifacts.json
@@ -1 +1 @@
-{"4.7.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.7.0/StreamChat-All.zip","4.8.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.8.0/StreamChat-All.zip","4.9.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.9.0/StreamChat-All.zip","4.10.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.0/StreamChat-All.zip","4.10.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.1/StreamChat-All.zip","4.11.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.11.0/StreamChat-All.zip","4.12.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.12.0/StreamChat-All.zip","4.13.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.0/StreamChat-All.zip","4.13.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.1/StreamChat-All.zip","4.14.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.14.0/StreamChat-All.zip","4.15.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.0/StreamChat-All.zip","4.15.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.1/StreamChat-All.zip","4.16.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.16.0/StreamChat-All.zip","4.17.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.17.0/StreamChat-All.zip","4.18.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.18.0/StreamChat-All.zip","4.19.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.19.0/StreamChat-All.zip","4.20.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.20.0/StreamChat-All.zip","4.21.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.0/StreamChat-All.zip","4.21.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.1/StreamChat-All.zip","4.21.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.2/StreamChat-All.zip","4.22.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.22.0/StreamChat-All.zip","4.23.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.23.0/StreamChat-All.zip","4.24.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.0/StreamChat-All.zip","4.24.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.1/StreamChat-All.zip","4.25.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.0/StreamChat-All.zip","4.25.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.1/StreamChat-All.zip","4.26.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.26.0/StreamChat-All.zip","4.27.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.0/StreamChat-All.zip","4.27.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.1/StreamChat-All.zip","4.28.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.28.0/StreamChat-All.zip","4.29.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.29.0/StreamChat-All.zip","4.30.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.30.0/StreamChat-All.zip","4.31.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.31.0/StreamChat-All.zip","4.32.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.32.0/StreamChat-All.zip","4.33.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.33.0/StreamChat-All.zip","4.34.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.34.0/StreamChat-All.zip","4.35.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.0/StreamChat-All.zip","4.35.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.1/StreamChat-All.zip","4.35.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.2/StreamChat-All.zip","4.36.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.36.0/StreamChat-All.zip","4.37.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.0/StreamChat-All.zip","4.37.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.1/StreamChat-All.zip","4.38.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.38.0/StreamChat-All.zip","4.39.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.39.0/StreamChat-All.zip","4.40.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.40.0/StreamChat-All.zip","4.41.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.41.0/StreamChat-All.zip","4.42.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.42.0/StreamChat-All.zip","4.43.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.43.0/StreamChat-All.zip","4.44.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.44.0/StreamChat-All.zip","4.45.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.45.0/StreamChat-All.zip","4.46.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.46.0/StreamChat-All.zip","4.47.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.0/StreamChat-All.zip","4.47.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.1/StreamChat-All.zip","4.48.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.0/StreamChat-All.zip","4.48.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.1/StreamChat-All.zip","4.49.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.49.0/StreamChat-All.zip","4.50.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.50.0/StreamChat-All.zip","4.51.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.51.0/StreamChat-All.zip","4.52.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.52.0/StreamChat-All.zip","4.53.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.53.0/StreamChat-All.zip","4.54.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.54.0/StreamChat-All.zip","4.55.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.55.0/StreamChat-All.zip","4.56.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.0/StreamChat-All.zip","4.56.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.1/StreamChat-All.zip","4.57.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.57.0/StreamChat-All.zip","4.58.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.58.0/StreamChat-All.zip","4.59.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.59.0/StreamChat-All.zip","4.60.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.60.0/StreamChat-All.zip"}
\ No newline at end of file
+{"4.7.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.7.0/StreamChat-All.zip","4.8.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.8.0/StreamChat-All.zip","4.9.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.9.0/StreamChat-All.zip","4.10.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.0/StreamChat-All.zip","4.10.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.10.1/StreamChat-All.zip","4.11.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.11.0/StreamChat-All.zip","4.12.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.12.0/StreamChat-All.zip","4.13.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.0/StreamChat-All.zip","4.13.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.13.1/StreamChat-All.zip","4.14.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.14.0/StreamChat-All.zip","4.15.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.0/StreamChat-All.zip","4.15.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.15.1/StreamChat-All.zip","4.16.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.16.0/StreamChat-All.zip","4.17.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.17.0/StreamChat-All.zip","4.18.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.18.0/StreamChat-All.zip","4.19.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.19.0/StreamChat-All.zip","4.20.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.20.0/StreamChat-All.zip","4.21.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.0/StreamChat-All.zip","4.21.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.1/StreamChat-All.zip","4.21.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.21.2/StreamChat-All.zip","4.22.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.22.0/StreamChat-All.zip","4.23.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.23.0/StreamChat-All.zip","4.24.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.0/StreamChat-All.zip","4.24.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.24.1/StreamChat-All.zip","4.25.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.0/StreamChat-All.zip","4.25.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.25.1/StreamChat-All.zip","4.26.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.26.0/StreamChat-All.zip","4.27.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.0/StreamChat-All.zip","4.27.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.27.1/StreamChat-All.zip","4.28.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.28.0/StreamChat-All.zip","4.29.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.29.0/StreamChat-All.zip","4.30.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.30.0/StreamChat-All.zip","4.31.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.31.0/StreamChat-All.zip","4.32.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.32.0/StreamChat-All.zip","4.33.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.33.0/StreamChat-All.zip","4.34.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.34.0/StreamChat-All.zip","4.35.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.0/StreamChat-All.zip","4.35.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.1/StreamChat-All.zip","4.35.2":"https://github.com/GetStream/stream-chat-swift/releases/download/4.35.2/StreamChat-All.zip","4.36.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.36.0/StreamChat-All.zip","4.37.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.0/StreamChat-All.zip","4.37.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.37.1/StreamChat-All.zip","4.38.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.38.0/StreamChat-All.zip","4.39.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.39.0/StreamChat-All.zip","4.40.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.40.0/StreamChat-All.zip","4.41.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.41.0/StreamChat-All.zip","4.42.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.42.0/StreamChat-All.zip","4.43.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.43.0/StreamChat-All.zip","4.44.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.44.0/StreamChat-All.zip","4.45.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.45.0/StreamChat-All.zip","4.46.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.46.0/StreamChat-All.zip","4.47.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.0/StreamChat-All.zip","4.47.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.47.1/StreamChat-All.zip","4.48.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.0/StreamChat-All.zip","4.48.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.48.1/StreamChat-All.zip","4.49.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.49.0/StreamChat-All.zip","4.50.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.50.0/StreamChat-All.zip","4.51.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.51.0/StreamChat-All.zip","4.52.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.52.0/StreamChat-All.zip","4.53.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.53.0/StreamChat-All.zip","4.54.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.54.0/StreamChat-All.zip","4.55.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.55.0/StreamChat-All.zip","4.56.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.0/StreamChat-All.zip","4.56.1":"https://github.com/GetStream/stream-chat-swift/releases/download/4.56.1/StreamChat-All.zip","4.57.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.57.0/StreamChat-All.zip","4.58.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.58.0/StreamChat-All.zip","4.59.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.59.0/StreamChat-All.zip","4.60.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.60.0/StreamChat-All.zip","4.61.0":"https://github.com/GetStream/stream-chat-swift/releases/download/4.61.0/StreamChat-All.zip"}
\ No newline at end of file
diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec
index b109b23ba6c..fc6ed3b63e2 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.60.0"
+ spec.version = "4.61.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 b28f9ffc368..45fb816fb73 100644
--- a/StreamChatUI.podspec
+++ b/StreamChatUI.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = "StreamChatUI"
- spec.version = "4.60.0"
+ spec.version = "4.61.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/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
index e3a35438ce3..9a04f23457f 100644
--- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
+++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
@@ -360,6 +360,10 @@ class DatabaseSession_Mock: DatabaseSession {
func saveQuery(query: MessageSearchQuery) -> MessageSearchQueryDTO {
underlyingSession.saveQuery(query: query)
}
+
+ func allQueuedRequests() -> [QueuedRequestDTO] {
+ underlyingSession.allQueuedRequests()
+ }
func deleteQueuedRequest(id: String) {
underlyingSession.deleteQueuedRequest(id: id)
diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift
index a2a7bd4f843..dd6bd131282 100644
--- a/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift
+++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/APIClient_Spy.swift
@@ -23,6 +23,7 @@ final class APIClient_Spy: APIClient, Spy {
/// The last endpoint `recoveryRequest` function was called with.
@Atomic var recoveryRequest_endpoint: AnyEndpoint?
@Atomic var recoveryRequest_completion: Any?
+ @Atomic var recoveryRequest_results: [Any] = []
@Atomic var recoveryRequest_allRecordedCalls: [(endpoint: AnyEndpoint, completion: Any?)] = []
/// The last endpoint `unmanagedRequest` function was called with.
@@ -53,6 +54,7 @@ final class APIClient_Spy: APIClient, Spy {
request_completion = nil
request_results = []
request_expectation = .init()
+ recoveryRequest_results = []
recoveryRequest_expectation = .init()
uploadRequest_expectation = .init()
@@ -104,6 +106,10 @@ final class APIClient_Spy: APIClient, Spy {
func test_mockResponseResult(_ responseResult: Result) {
request_results.append(responseResult)
}
+
+ func test_mockRecoveryResponseResult(_ responseResult: Result) {
+ recoveryRequest_results.append(responseResult)
+ }
func test_mockUnmanagedResponseResult(_ responseResult: Result) {
unmanagedRequest_result = responseResult
@@ -129,6 +135,10 @@ final class APIClient_Spy: APIClient, Spy {
completion: @escaping (Result) -> Void
) where Response: Decodable {
recoveryRequest_endpoint = AnyEndpoint(endpoint)
+ if let resultIndex = recoveryRequest_results.firstIndex(where: { $0 is Result }) {
+ let result = recoveryRequest_results.remove(at: resultIndex)
+ completion(result as! Result)
+ }
recoveryRequest_completion = completion
_recoveryRequest_allRecordedCalls.mutate { $0.append((recoveryRequest_endpoint!, recoveryRequest_completion!)) }
}
@@ -166,6 +176,7 @@ final class APIClient_Spy: APIClient, Spy {
@discardableResult
func waitForRequest(timeout: Double = defaultTimeout) -> AnyEndpoint? {
XCTWaiter().wait(for: [request_expectation], timeout: timeout)
+ request_expectation = XCTestExpectation()
return request_endpoint
}
diff --git a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
index 4b2b53ddac7..331ef311d39 100644
--- a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
+++ b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
@@ -1020,53 +1020,6 @@ final class ChannelListController_Tests: XCTestCase {
// MARK: Predicates
- private func assertFilterPredicate(
- _ filter: @autoclosure () -> Filter,
- channelsInDB: @escaping @autoclosure () -> [ChannelPayload],
- expectedResult: @autoclosure () -> [ChannelId],
- file: StaticString = #file,
- line: UInt = #line
- ) throws {
- /// Ensure that isChannelAutomaticFilteringEnabled is enabled
- var config = ChatClientConfig(apiKeyString: .unique)
- config.isChannelAutomaticFilteringEnabled = true
- client = ChatClient.mock(config: config)
-
- let query = ChannelListQuery(
- filter: filter()
- )
- controller = ChatChannelListController(
- query: query,
- client: client,
- environment: env.environment
- )
- controllerCallbackQueueID = UUID()
- controller.callbackQueue = .testQueue(withId: controllerCallbackQueueID)
-
- // Simulate `synchronize` call
- controller.synchronize()
- waitForInitialChannelsUpdate()
-
- XCTAssertEqual(controller.channels.map(\.cid), [], file: file, line: line)
-
- // Simulate changes in the DB:
- _ = try waitFor {
- writeAndWaitForChannelsUpdates({ [query] session in
- try channelsInDB().forEach { payload in
- try session.saveChannel(payload: payload, query: query, cache: nil)
- }
- }, completion: $0)
- }
-
- // Assert the resulting value is updated
- XCTAssertEqual(
- controller.channels.map(\.cid.rawValue).sorted(),
- expectedResult().map(\.rawValue).sorted(),
- file: file,
- line: line
- )
- }
-
func test_filterPredicate_equal_containsExpectedItems() throws {
let cid = ChannelId.unique
@@ -1598,6 +1551,53 @@ final class ChannelListController_Tests: XCTestCase {
)
}
+ func test_filterPredicate_hasUnread_returnsExpectedResults() throws {
+ let cid1 = ChannelId.unique
+ let cid2 = ChannelId.unique
+ let currentUserId = UserId.unique
+
+ try assertFilterPredicate(
+ .hasUnread,
+ sort: [.init(key: .unreadCount, isAscending: false)],
+ currentUserId: currentUserId,
+ channelsInDB: [
+ .dummy(
+ channel: .dummy(cid: cid1),
+ channelReads: [
+ .init(
+ user: .dummy(userId: currentUserId),
+ lastReadAt: .unique,
+ lastReadMessageId: nil,
+ unreadMessagesCount: 3
+ )
+ ]
+ ),
+ .dummy(channel: .dummy(team: .unique), channelReads: [
+ .init(
+ user: .dummy(userId: .unique),
+ lastReadAt: .unique,
+ lastReadMessageId: nil,
+ unreadMessagesCount: 10
+ )
+ ]),
+ .dummy(channel: .dummy(team: .unique)),
+ .dummy(channel: .dummy(team: .unique)),
+ .dummy(
+ channel: .dummy(cid: cid2),
+ channelReads: [
+ .init(
+ user: .dummy(userId: currentUserId),
+ lastReadAt: .unique,
+ lastReadMessageId: nil,
+ unreadMessagesCount: 20
+ )
+ ]
+ )
+ ],
+ expectedResult: [cid2, cid1]
+ )
+ }
+
func test_filterPredicate_muted_returnsExpectedResults() throws {
let cid1 = ChannelId.unique
let userId = memberId
@@ -1671,6 +1671,64 @@ final class ChannelListController_Tests: XCTestCase {
// MARK: - Private Helpers
+ private func assertFilterPredicate(
+ _ filter: @autoclosure () -> Filter,
+ sort: [Sorting] = [],
+ currentUserId: UserId? = nil,
+ channelsInDB: @escaping @autoclosure () -> [ChannelPayload],
+ expectedResult: @autoclosure () -> [ChannelId],
+ file: StaticString = #file,
+ line: UInt = #line
+ ) throws {
+ /// Ensure that isChannelAutomaticFilteringEnabled is enabled
+ var config = ChatClientConfig(apiKeyString: .unique)
+ config.isChannelAutomaticFilteringEnabled = true
+ client = ChatClient.mock(config: config)
+
+ if let currentUserId {
+ try database.writeSynchronously { session in
+ try session.saveCurrentUser(
+ payload: .dummy(userId: currentUserId, role: .admin)
+ )
+ }
+ }
+
+ let query = ChannelListQuery(
+ filter: filter(),
+ sort: sort
+ )
+ controller = ChatChannelListController(
+ query: query,
+ client: client,
+ environment: env.environment
+ )
+ controllerCallbackQueueID = UUID()
+ controller.callbackQueue = .testQueue(withId: controllerCallbackQueueID)
+
+ // Simulate `synchronize` call
+ controller.synchronize()
+ waitForInitialChannelsUpdate()
+
+ XCTAssertEqual(controller.channels.map(\.cid), [], file: file, line: line)
+
+ // Simulate changes in the DB:
+ _ = try waitFor {
+ writeAndWaitForChannelsUpdates({ [query] session in
+ try channelsInDB().forEach { payload in
+ try session.saveChannel(payload: payload, query: query, cache: nil)
+ }
+ }, completion: $0)
+ }
+
+ // Assert the resulting value is updated
+ XCTAssertEqual(
+ controller.channels.map(\.cid.rawValue).sorted(),
+ expectedResult().map(\.rawValue).sorted(),
+ file: file,
+ line: line
+ )
+ }
+
private func makeAddedChannelEvent(with channel: ChatChannel) -> NotificationAddedToChannelEvent {
NotificationAddedToChannelEvent(
channel: channel,
diff --git a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
index 3721b639345..d685b17d7e5 100644
--- a/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
+++ b/Tests/StreamChatTests/Database/DTOs/ChannelDTO_Tests.swift
@@ -1322,7 +1322,7 @@ final class ChannelDTO_Tests: XCTestCase {
XCTAssertEqual(channel.unreadCount.messages, 0)
}
- func test_asModel_populatesLatestMessage() throws {
+ func test_asModel_populatesLatestMessage_withoutFilteringDeletedMessages() throws {
// GIVEN
database = DatabaseContainer_Spy(
kind: .inMemory,
@@ -1411,7 +1411,7 @@ final class ChannelDTO_Tests: XCTestCase {
// THEN
XCTAssertEqual(
Set(channel.latestMessages.map(\.id)),
- Set([message1.id, deletedMessageFromCurrentUser.id, shadowedMessageFromAnotherUser.id])
+ Set([message1.id, deletedMessageFromCurrentUser.id, deletedMessageFromAnotherUser.id])
)
}
diff --git a/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift b/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift
index c057be14d01..7fb07efe9df 100644
--- a/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift
+++ b/Tests/StreamChatTests/Database/DatabaseContainer_Tests.swift
@@ -265,6 +265,7 @@ final class DatabaseContainer_Tests: XCTestCase {
modelName: "TestDataModel",
bundle: .testTools
)
+ database?.shouldCleanUpTempDBFiles = false
// Insert a new object
try database!.writeSynchronously {
diff --git a/Tests/StreamChatTests/Query/Sorting/ChannelListSortingKey_Tests.swift b/Tests/StreamChatTests/Query/Sorting/ChannelListSortingKey_Tests.swift
index 63b04d921a9..738231a0014 100644
--- a/Tests/StreamChatTests/Query/Sorting/ChannelListSortingKey_Tests.swift
+++ b/Tests/StreamChatTests/Query/Sorting/ChannelListSortingKey_Tests.swift
@@ -77,9 +77,13 @@ final class ChannelListSortingKey_Tests: XCTestCase {
XCTAssertEqual(key.remoteKey, "has_unread")
XCTAssertTrue(key.requiresRuntimeSorting)
case .unreadCount:
- XCTAssertNil(key.sortDescriptor(isAscending: true))
+ XCTAssertNotNil(key.sortDescriptor(isAscending: true))
XCTAssertEqual(key.remoteKey, "unread_count")
- XCTAssertTrue(key.requiresRuntimeSorting)
+ XCTAssertFalse(key.requiresRuntimeSorting)
+ XCTAssertEqual(
+ key.localKey,
+ NSExpression(forKeyPath: \ChannelDTO.currentUserUnreadMessagesCount).keyPath
+ )
default:
XCTFail()
}
diff --git a/Tests/StreamChatTests/Repositories/OfflineRequestsRepository_Tests.swift b/Tests/StreamChatTests/Repositories/OfflineRequestsRepository_Tests.swift
index 250cec361c1..deb7085a411 100644
--- a/Tests/StreamChatTests/Repositories/OfflineRequestsRepository_Tests.swift
+++ b/Tests/StreamChatTests/Repositories/OfflineRequestsRepository_Tests.swift
@@ -315,20 +315,34 @@ final class OfflineRequestsRepository_Tests: XCTestCase {
XCTAssertEqual(pendingRequests.count, 0)
}
- private func createSendMessageRequests(count: Int, date: Date = Date()) throws {
+ private func createSendMessageRequests(count: Int, date: Date = Date().addingTimeInterval(-3600)) throws {
try (1...count).forEach {
- let id = "request\($0)"
- try self.createRequest(
- id: id,
- path: .sendMessage(.init(type: .messaging, id: id)),
- body: ["some\($0)": 123],
- date: date
+ try createSendMessageRequest(
+ requestIdNumber: $0,
+ messageIdNumber: $0,
+ date: date.addingTimeInterval(TimeInterval($0))
)
}
let allRequests = QueuedRequestDTO.loadAllPendingRequests(context: database.viewContext)
XCTAssertEqual(allRequests.count, count)
}
+
+ private func createSendMessageRequest(requestIdNumber: Int, messageIdNumber: Int, date: Date) throws {
+ let id = "request\(requestIdNumber)"
+ let messageId = "message\(messageIdNumber)"
+ let requestBody = MessageRequestBody(id: messageId, user: .dummy(userId: .unique), text: .unique, extraData: [:])
+ let endpoint: Endpoint = .sendMessage(
+ cid: .init(type: .messaging, id: id),
+ messagePayload: requestBody,
+ skipPush: false,
+ skipEnrichUrl: false
+ )
+ let endpointData: Data = try JSONEncoder.stream.encode(endpoint.withDataResponse)
+ try database.writeSynchronously { _ in
+ QueuedRequestDTO.createRequest(id: id, date: date, endpoint: endpointData, context: self.database.writableContext)
+ }
+ }
private func createRequest(id: String, path: EndpointPath, body: Encodable? = nil, date: Date = Date()) throws {
let endpoint = Endpoint(
@@ -382,4 +396,30 @@ final class OfflineRequestsRepository_Tests: XCTestCase {
waitForExpectations(timeout: defaultTimeout, handler: nil)
XCTAssertCall("write(_:completion:)", on: database, times: 1)
}
+
+ func test_queueOfflineRequestsMultipleTimesThenDuplicateSendMessageRequestsAreCoalesced() throws {
+ try createSendMessageRequests(count: 5) // 1...5
+ // Duplicate second and forth
+ try createSendMessageRequest(requestIdNumber: 6, messageIdNumber: 2, date: Date())
+ try createSendMessageRequest(requestIdNumber: 7, messageIdNumber: 4, date: Date())
+
+ // 5 successful responses, 2 should never end up here because these should be coalesced
+ for _ in 0..<5 {
+ apiClient.test_mockRecoveryResponseResult(Result.success(Data()))
+ }
+
+ let expectation = XCTestExpectation(description: "Run")
+ repository.runQueuedRequests {
+ expectation.fulfill()
+ }
+
+ // When failure happens then request merging did not work
+ wait(for: [expectation], timeout: defaultTimeout)
+
+ // Validate that all the requests are cleaned up
+ try database.readSynchronously { session in
+ let requests = session.allQueuedRequests()
+ XCTAssertEqual(0, requests.count)
+ }
+ }
}
diff --git a/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift b/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift
index 96583263b8f..ff5bb560111 100644
--- a/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift
+++ b/Tests/StreamChatTests/Workers/Background/MessageSender_Tests.swift
@@ -404,6 +404,56 @@ final class MessageSender_Tests: XCTestCase {
AssertAsync.willBeTrue(eventsNotificationCenter.mock_processCalledWithEvents.first is NewMessageErrorEvent)
XCTAssertCall("sendMessage(with:completion:)", on: messageRepository, times: 1)
}
+
+ func test_senderSendsMessages_forwardsPendingMessagesToOfflineHandlingOnConnectionError() throws {
+ // Sender with non-mock message repository
+ let nonMockMessageRepository = MessageRepository(database: database, apiClient: apiClient)
+ sender = MessageSender(
+ messageRepository: nonMockMessageRepository,
+ eventsNotificationCenter: eventsNotificationCenter,
+ database: database,
+ apiClient: apiClient
+ )
+ var queueOfflineRequestCounter = 0
+ let offlineQueuingExpectation = XCTestExpectation(description: "2, 3, 4, 5 queued")
+ apiClient.queueOfflineRequest = { _ in
+ queueOfflineRequestCounter += 1
+ guard queueOfflineRequestCounter == 4 else { return }
+ offlineQueuingExpectation.fulfill()
+ }
+
+ // At the end of test all 5 are expected to finish successfully
+ let messageIds = (1...5).map { "\($0)" }
+
+ try database.writeSynchronously { session in
+ for id in messageIds {
+ try self.createMessage(id: id, in: session)
+ }
+ }
+
+ // First: success
+ apiClient.waitForRequest()
+ try resumeAPIRequestAndWaitForLocalStateChange(messageId: "1", success: true)
+
+ // Second: connection error
+ apiClient.waitForRequest()
+ try resumeAPIRequestAndWaitForLocalStateChange(messageId: "2", success: false)
+
+ // We use mocked API client which does not do the automatic forwarding, therefore we simulate it here
+ apiClient.queueOfflineRequest?(DataEndpoint(path: .sendMessage(cid), method: .post))
+
+ // Since connection error was received, all the remaining queued messages are sent directly to offline repository
+ wait(for: [offlineQueuingExpectation], timeout: defaultTimeout)
+
+ // Verify states (one successful, others failing)
+ try database.readSynchronously { session in
+ let localMessageStates = messageIds.map { session.message(id: $0)?.localMessageState }
+ let expected: [LocalMessageState?] = [nil, .sendingFailed, .sendingFailed, .sendingFailed, .sendingFailed]
+ XCTAssertEqual(expected, localMessageStates)
+ }
+
+ // Offline repository is now responsible of sending the requests
+ }
// MARK: - Life cycle tests
@@ -456,6 +506,38 @@ final class MessageSender_Tests: XCTestCase {
// Then
wait(for: [sessionMock.rescueMessagesExpectation], timeout: defaultTimeout)
}
+
+ // MARK: -
+
+ @discardableResult func createMessage(id: MessageId, in session: DatabaseSession) throws -> MessageId {
+ let dto = try session.createNewMessage(
+ in: cid,
+ messageId: id,
+ text: "\(id)",
+ pinning: nil,
+ quotedMessageId: nil,
+ skipPush: false,
+ skipEnrichUrl: false
+ )
+ dto.localMessageState = .pendingSend
+ return dto.id
+ }
+
+ private func resumeAPIRequestAndWaitForLocalStateChange(messageId: MessageId, success: Bool) throws {
+ let localStateExpectation = XCTestExpectation(description: "\(messageId) - local state change")
+ database.didWrite = {
+ // Extra delay for allowing MessageSender to run the MessageRepository's completion
+ DispatchQueue.main.async {
+ localStateExpectation.fulfill()
+ }
+ }
+ if success {
+ apiClient.test_simulateResponse(.success(MessagePayload.Boxed(message: .dummy(messageId: messageId, text: "processed", cid: cid))))
+ } else {
+ apiClient.test_simulateResponse(Result.failure(NSError(domain: NSURLErrorDomain, code: NSURLErrorNetworkConnectionLost)))
+ }
+ wait(for: [localStateExpectation], timeout: defaultTimeout)
+ }
}
private class DatabaseSessionRescueListener: DatabaseSession_Mock {
diff --git a/docusaurus/docs/iOS/assets/carthage-drag.png b/docusaurus/docs/iOS/assets/carthage-drag.png
deleted file mode 100644
index 8e344c58e69..00000000000
Binary files a/docusaurus/docs/iOS/assets/carthage-drag.png and /dev/null differ
diff --git a/docusaurus/docs/iOS/assets/carthage-embed-and-sign.png b/docusaurus/docs/iOS/assets/carthage-embed-and-sign.png
deleted file mode 100644
index 694a46993d8..00000000000
Binary files a/docusaurus/docs/iOS/assets/carthage-embed-and-sign.png and /dev/null differ
diff --git a/docusaurus/docs/iOS/basics/integration.md b/docusaurus/docs/iOS/basics/integration.md
index 4ea723c39a0..858119c5f9b 100644
--- a/docusaurus/docs/iOS/basics/integration.md
+++ b/docusaurus/docs/iOS/basics/integration.md
@@ -8,7 +8,6 @@ To integrate Stream Chat in your app, you can use one of the following dependenc
- [**Swift Package Manager**](#swift-package-manager)
- [**CocoaPods**](#cocoapods)
-- [**Carthage**](#carthage)
We also provide pre-built XCFramework support, read more [here](#xcframeworks).
@@ -114,70 +113,6 @@ With our workspace now containing our Pods project with dependencies, as well as
_More information about CocoaPods [can be found here](https://cocoapods.org/)._
-### Carthage
-
-If you are using **Swift 5.7 / Xcode 14** or above, the recommended method for Carthage is to use **pre-built XCFrameworks**.
-
-:::note
-Our SwiftUI components library is not yet available using Carthage, please use Swift Package Manager or CocoaPods.
-:::
-
-β€ Using pre-built XCFrameworks
-
-
-:::caution
-Our XCFrameworks are built with **Swift 5.7**. In order to use them you need **Xcode 14** or above
-:::
-
-You can learn more about [our Module Stable XCFrameworks here](#xcframeworks)
-
-- For the LLC (**StreamChat**) use:
- - `binary "https://raw.githubusercontent.com/GetStream/stream-chat-swift/main/StreamChatArtifacts.json" ~> 4.6`
-- For the UIKit components (**StreamChatUI**, which depends on **StreamChat**) use:
- - `binary "https://raw.githubusercontent.com/GetStream/stream-chat-swift/main/StreamChatArtifacts.json" ~> 4.6`
-
-Now that weβve modified our Cartfile, letβs go ahead and install the project dependencies via the terminal with one simple command:
-
-```bash
-carthage update --use-xcframeworks
-```
-
-The previous command will download pre-built XCFrameworks. You now need to add those to your project. Keep reading.
-
-
-
-
-β€ Building from source (OSS)
-
-
-In your project's `Cartfile`, add one of these options
-
-- For the LLC (**StreamChat**) use:
- - `github "getstream/stream-chat-swift" ~> 4.6.0`
-- For the UIKit components (**StreamChatUI**, which depends on **StreamChat**) use:
- - `github "getstream/stream-chat-swift" ~> 4.6.0`
-
-Now that weβve modified our Cartfile, letβs go ahead and install the project dependencies via the terminal with one simple command:
-
-```bash
-carthage update --use-xcframeworks --no-use-binaries --platform iOS
-```
-
-The previous command will create pre-built XCFrameworks built from our source code (This might take a while β±). You now need to add those to your project. Keep reading.
-
-
-
-
-Open the `Carthage/Build` folder that has been created in the root of your project, and drag and drop the frameworks you want to use. Those should be added to the "Frameworks, Libraries, and Embedded Content" section under General settings:
-
-![Screenshot shows XCFrameworks being dragged into Xcode](../assets/carthage-drag.png)
-
-Make sure you select **Embed & Sign** under "Embed" options if you are adding Stream libraries to an app target. If not, use **Do Not Embed**
-
-![Screenshot shows Embed and Sign being the option selected](../assets/carthage-embed-and-sign.png)
-
-_More information about Carthage [can be found here](https://github.com/Carthage/Carthage)._
-
## XCFrameworks
In an effort to have [**Module Stability**](https://www.swift.org/blog/library-evolution/), we have started distributing **pre-built XCFrameworks** starting ***4.6.0***
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 33e4ba42467..d433459d75e 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -13,12 +13,19 @@ github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-swift'
stress_tests_cycles = 50
derived_data_path = 'derived_data'
source_packages_path = 'spm_cache'
-performance_path = "performance/#{github_repo.split('/').last}.json"
+metrics_git = 'git@github.com:GetStream/apple-internal-metrics.git'
+xcmetrics_path = "metrics/#{github_repo.split('/').last}-xcmetrics.json"
+sdk_size_path = "metrics/#{github_repo.split('/').last}-size.json"
buildcache_xcargs = 'CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++'
testlab_bucket = 'gs://test-lab-af3rt9m4yh360-mqm1zzm767nhc'
is_localhost = !is_ci
@force_check = false
+warning_status = 'π‘' # Warning if a branch is #{max_tolerance} less performant than the benchmark
+fail_status = 'π΄' # Failure if a branch is more than #{max_tolerance} less performant than the benchmark
+success_status = 'π’' # Success if a branch is more performant or equals to the benchmark
+outstanding_status = 'π' # Outstanding performance
+
before_all do |lane|
if is_ci
setup_ci
@@ -103,6 +110,8 @@ lane :publish_release do |options|
)
update_spm(version: options[:version])
+
+ merge_main_to_develop
end
lane :merge_release_to_main do
@@ -123,11 +132,18 @@ lane :merge_release_to_main do
end
lane :merge_main_to_develop do
- ensure_git_status_clean
- sh('git checkout main && git pull')
- sh('git checkout develop && git pull')
+ if is_ci
+ sh('git reset --hard')
+ else
+ ensure_git_status_clean
+ end
+
+ sh('git checkout main')
+ sh('git pull origin main')
+ sh('git checkout develop')
+ sh('git pull origin develop')
+ sh('git log develop..main')
sh('git merge main')
- UI.user_error!('Not pushing changes') unless prompt(text: 'Will push changes. All looking good?', boolean: true)
sh('git push')
end
@@ -136,7 +152,7 @@ lane :compress_frameworks do
Dir.chdir('..') do
FileUtils.cp('LICENSE', 'Products/LICENSE')
Dir.chdir('Products') do
- ['StreamChat', 'StreamChatUI'].each do |framework|
+ sdk_names.each do |framework|
sh("zip -r #{framework} ./#{framework}.xcframework ./LICENSE")
sh("swift package compute-checksum #{framework}.zip")
end
@@ -366,7 +382,7 @@ end
lane :xcmetrics do |options|
next unless is_check_required(sources: sources_matrix[:xcmetrics], force_check: @force_check)
- ['test_output/', 'performance/', "../#{derived_data_path}/Build/Products"].each { |dir| FileUtils.remove_dir(dir, force: true) }
+ ['test_output/', 'metrics/', "../#{derived_data_path}/Build/Products"].each { |dir| FileUtils.remove_dir(dir, force: true) }
match_me
@@ -399,28 +415,36 @@ lane :xcmetrics do |options|
xcodebuild_output = File.read('xcodebuild_output.log')
end
- sh("git clone git@github.com:GetStream/stream-swift-performance-benchmarks.git #{File.dirname(performance_path)}")
- branch_performance = xcmetrics_log_parser(log: xcodebuild_output)
- performance_benchmarks = JSON.parse(File.read(performance_path))
+ sh("git clone #{metrics_git} #{File.dirname(xcmetrics_path)}")
+ performance_benchmarks = JSON.parse(File.read(xcmetrics_path))
expected_performance = performance_benchmarks['benchmark']
+ actual_performance = xcmetrics_log_parser(log: xcodebuild_output)
- markdown_table = "## StreamChat XCMetrics\n| `target` | `metric` | `benchmark` | `branch` | `performance` | `status` |\n| - | - | - | - | - | - |\n"
+ table_header = '## SDK Performance'
+ markdown_table = "#{table_header}\n| `target` | `metric` | `benchmark` | `branch` | `performance` | `status` |\n| - | - | - | - | - | - |\n"
['testMessageListScrollTime', 'testChannelListScrollTime'].each do |test_name|
index = 0
['hitches_total_duration', 'duration', 'hitch_time_ratio', 'frame_rate', 'number_of_hitches'].each do |metric|
is_frame_rate = metric == 'frame_rate'
benchmark_value = expected_performance[test_name][metric]['value']
- branch_value = branch_performance[test_name][metric]['value']
- value_extension = branch_performance[test_name][metric]['ext']
+ branch_value = actual_performance[test_name][metric]['value']
+ value_extension = actual_performance[test_name][metric]['ext']
+ max_tolerance = benchmark_value * 0.1 # Default Xcode Max Tolerance is 10%
- max_stddev = benchmark_value * 0.1 # Default Xcode Max STDDEV is 10%
- warning_status = 'π‘' # Warning if a branch is 10% less performant than the benchmark
- fail_status = 'π΄' # Failure if a branch is more than 10% less performant than the benchmark
- success_status = 'π’' # Success if a branch is more performant or equals to the benchmark
+ benchmark_value_avoids_zero_division = benchmark_value == 0 ? 1 : benchmark_value
+ diff = is_frame_rate ? branch_value - benchmark_value : benchmark_value - branch_value
+ diff = (diff * 100.0 / benchmark_value_avoids_zero_division).round(2)
+ diff_emoji = if diff > 0
+ 'πΌ'
+ elsif diff.zero?
+ 'π°'
+ else
+ 'π½'
+ end
status_emoji =
if is_frame_rate
- if branch_value < benchmark_value && branch_value > benchmark_value - max_stddev
+ if branch_value < benchmark_value && branch_value > benchmark_value - max_tolerance
warning_status
elsif branch_value < benchmark_value
fail_status
@@ -428,7 +452,7 @@ lane :xcmetrics do |options|
success_status
end
else
- if branch_value > benchmark_value && branch_value < benchmark_value + max_stddev
+ if branch_value > benchmark_value && branch_value < benchmark_value + max_tolerance
warning_status
elsif branch_value > benchmark_value
fail_status
@@ -437,26 +461,13 @@ lane :xcmetrics do |options|
end
end
- benchmark_value_avoids_zero_division = benchmark_value == 0 ? 1 : benchmark_value
- diff = is_frame_rate ? branch_value - benchmark_value : benchmark_value - branch_value
- diff = (diff * 100.0 / benchmark_value_avoids_zero_division).round(2)
-
- diff_emoji =
- if diff > 0
- 'πΌ'
- elsif diff.zero?
- 'π°'
- else
- 'π½'
- end
-
title = metric.to_s.gsub('_', ' ').capitalize
target = index.zero? ? test_name.match(/(?<=test)(.*?)(?=ScrollTime)/).to_s : ''
index += 1
markdown_table << "| #{target} | #{title} | #{benchmark_value} #{value_extension} | #{branch_value} #{value_extension} | #{diff}% #{diff_emoji} | #{status_emoji} |\n"
FastlaneCore::PrintTable.print_values(
- title: "β³ #{title} β³",
+ title: title,
config: {
benchmark: "#{benchmark_value} #{value_extension}",
branch: "#{branch_value} #{value_extension}",
@@ -469,27 +480,9 @@ lane :xcmetrics do |options|
UI.user_error!("See Firebase error above βοΈ") unless firebase_error.to_s.empty?
- if is_ci
- pr_comment_required = !ENV['GITHUB_PR_NUM'].to_s.empty?
- performance_benchmarks[current_branch] = branch_performance
- UI.message("Performance benchmarks: #{performance_benchmarks}")
- File.write(performance_path, JSON.pretty_generate(performance_benchmarks))
-
- Dir.chdir(File.dirname(performance_path)) do
- if sh('git status -s', log: false).to_s.empty?
- pr_comment_required = false
- UI.important('No changes in performance benchmarks. Skipping commit and comment.')
- else
- sh('git add -A')
- sh("git commit -m 'Update #{github_repo.split('/').last}.json: #{current_branch}'")
- sh('git push')
- end
- end
-
- sh("gh pr comment #{ENV.fetch('GITHUB_PR_NUM')} -b '#{markdown_table}'") if pr_comment_required
- end
+ create_pr_comment(pr_num: ENV.fetch('GITHUB_PR_NUM'), text: markdown_table, edit_last_comment_with_text: table_header)
- UI.user_error!('Performance benchmark failed.') if markdown_table.include?('π΄')
+ UI.user_error!("#{table_header} benchmark failed.") if markdown_table.include?(fail_status)
end
private_lane :xcmetrics_log_parser do |options|
@@ -826,6 +819,7 @@ lane :sources_matrix do
sample_apps: ['Sources', 'Examples', 'DemoApp', xcode_project],
integration: ['Sources', 'Integration', xcode_project],
ruby: ['fastlane', 'Gemfile', 'Gemfile.lock'],
+ size: ['Sources', xcode_project],
xcmetrics: ['Sources']
}
end
@@ -842,25 +836,76 @@ end
desc 'Show current frameworks size'
lane :show_frameworks_sizes do |options|
- options[:sizes] ||= frameworks_sizes
+ next unless is_check_required(sources: sources_matrix[:size], force_check: @force_check)
+
+ ['metrics/'].each { |dir| FileUtils.remove_dir(dir, force: true) }
+
+ sh("git clone #{metrics_git} #{File.dirname(sdk_size_path)}")
+ is_release = current_branch.include?('release/')
+ benchmark_config = JSON.parse(File.read(sdk_size_path))
+ benchmark_key = is_release ? 'release' : 'develop'
+ benchmark_sizes = benchmark_config[benchmark_key]
+ branch_sizes = options[:sizes] || frameworks_sizes
+
+ table_header = '## SDK Size'
+ markdown_table = "#{table_header}\n| `title` | `#{is_release ? 'previous release' : 'develop'}` | `#{is_release ? 'current release' : 'branch'}` | `diff` | `status` |\n| - | - | - | - | - |\n"
+ sdk_names.each do |title|
+ benchmark_value = benchmark_sizes[title]
+ branch_value = branch_sizes[title.to_sym]
+ max_tolerance = 0.5 # Max Tolerance is 0.5MB
+ fine_tolerance = 0.25 # Fine Tolerance is 0.25MB
+
+ diff = (branch_value - benchmark_value).round(2)
+
+ status_emoji =
+ if diff < 0
+ outstanding_status
+ elsif diff >= max_tolerance
+ fail_status
+ elsif diff >= fine_tolerance
+ warning_status
+ else
+ success_status
+ end
- UI.success("StreamChat: #{options[:sizes][:stream_chat]}MB")
- UI.success("StreamChatUI: #{options[:sizes][:stream_chat_ui]}MB")
- UI.success("Total Size: #{options[:sizes][:total]}MB")
+ markdown_table << "|#{title}|#{benchmark_value}MB|#{branch_value}MB|#{diff}MB|#{status_emoji}|\n"
+ end
+
+ FastlaneCore::PrintTable.print_values(title: 'Benchmark', config: benchmark_sizes)
+ FastlaneCore::PrintTable.print_values(title: 'SDK Size', config: branch_sizes)
+
+ if is_ci
+ if is_release || ENV['GITHUB_EVENT_NAME'].to_s == 'push'
+ benchmark_config[benchmark_key] = branch_sizes
+ File.write(sdk_size_path, JSON.pretty_generate(benchmark_config))
+ Dir.chdir(File.dirname(sdk_size_path)) do
+ if sh('git status -s', log: false).to_s.empty?
+ UI.important('No changes in SDK sizes benchmarks.')
+ else
+ sh('git add -A')
+ sh("git commit -m 'Update #{sdk_size_path}'")
+ sh('git push')
+ end
+ end
+ end
+
+ create_pr_comment(pr_num: ENV.fetch('GITHUB_PR_NUM'), text: markdown_table, edit_last_comment_with_text: table_header)
+ end
+
+ UI.user_error!("#{table_header} benchmark failed.") if markdown_table.include?(fail_status)
end
desc 'Update img shields SDK size labels'
lane :update_img_shields_sdk_sizes do
sizes = frameworks_sizes
- show_frameworks_sizes(sizes: sizes)
# Read the file into a string
readme_path = '../README.md'
readme_content = File.read(readme_path)
# Define the new value for the badge
- stream_chat_size = "#{sizes[:stream_chat]}MB"
- stream_chat_ui_size = "#{sizes[:stream_chat_ui]}MB"
+ stream_chat_size = "#{sizes[:StreamChat]}MB"
+ stream_chat_ui_size = "#{sizes[:StreamChatUI]}MB"
# Replace the value in the badge URL
readme_content.gsub!(%r{(https://img.shields.io/badge/StreamChat-)(.*?)(-blue)}, "\\1#{stream_chat_size}\\3")
@@ -891,6 +936,14 @@ private_lane :create_pr do |options|
)
end
+private_lane :create_pr_comment do |options|
+ if is_ci && !options[:pr_num].to_s.empty?
+ last_comment = sh("gh pr view #{options[:pr_num]} --json comments --jq '.comments | map(select(.author.login == \"Stream-iOS-Bot\")) | last'")
+ edit_last_comment = last_comment.include?(options[:edit_last_comment_with_text]) ? '--edit-last' : ''
+ sh("gh pr comment #{options[:pr_num]} #{edit_last_comment} -b '#{options[:text]}'")
+ end
+end
+
private_lane :current_branch do
github_pr_branch_name = ENV['BRANCH_NAME'].to_s
github_ref_branch_name = ENV['GITHUB_REF'].to_s.sub('refs/heads/', '')
@@ -944,6 +997,8 @@ def frameworks_sizes
# Cleanup the previous builds
FileUtils.rm_rf("../#{root_dir}/")
+ match_me
+
gym(
scheme: 'DemoApp',
archive_path: archive_dir,
@@ -961,13 +1016,13 @@ def frameworks_sizes
stream_chat_size = File.size("#{frameworks_path}/StreamChat.framework/StreamChat")
stream_chat_ui_size = File.size("#{frameworks_path}/StreamChatUI.framework/StreamChatUI")
- stream_chat_size_mb = (stream_chat_size.to_f / 1024 / 1024).round(1)
- stream_chat_ui_size_mb = ((stream_chat_ui_size + assets_thinned_size).to_f / 1024 / 1024).round(1)
- total_size_mb = (stream_chat_size_mb + stream_chat_ui_size_mb).round(1)
+ stream_chat_size_mb = (stream_chat_size.to_f / 1024 / 1024).round(2)
+ stream_chat_ui_size_mb = ((stream_chat_ui_size + assets_thinned_size).to_f / 1024 / 1024).round(2)
+ total_size_mb = (stream_chat_size_mb + stream_chat_ui_size_mb).round(2)
{
- stream_chat: stream_chat_size_mb,
- stream_chat_ui: stream_chat_ui_size_mb,
- total: total_size_mb
+ StreamChat: stream_chat_size_mb,
+ StreamChatUI: stream_chat_ui_size_mb,
+ Total: total_size_mb
}
end