Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/cron-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ jobs:
strategy:
matrix:
include:
- ios: 18.3
xcode: 16.3
- ios: 18.5
xcode: 16.4
os: macos-15
device: "iPhone 16 Pro"
setup_runtime: false
Expand Down Expand Up @@ -94,8 +94,8 @@ jobs:
strategy:
matrix:
include:
- ios: 18.3
xcode: 16.3
- ios: 18.5
xcode: 16.4
os: macos-15
device: "iPhone 16 Pro"
setup_runtime: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/smoke-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ concurrency:

env:
HOMEBREW_NO_INSTALL_CLEANUP: 1 # Disable cleanup for homebrew, we don't need it on CI
IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.3)"
IOS_SIMULATOR_DEVICE: "iPhone 16 Pro (18.5)"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_NUM: ${{ github.event.pull_request.number }}

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ buildcache
App Thinning Size Report.txt
app-thinning.plist
*.dmg
*.pkg*

# gcloud
google-cloud-sdk
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### 🔄 Changed

# [4.86.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.86.0)
_August 21, 2025_

## StreamChat
### 🐞 Fixed
- Fix `LivestreamChannelController` not reconnecting when connection is dropped [#3782](https://github.com/GetStream/stream-chat-swift/pull/3782)
- Fix `StreamAudioRecorder` not overridable because of init method [#3783](https://github.com/GetStream/stream-chat-swift/pull/3783)
- Fix channel list query filtering by both blocked and non-blocked channels [#3785](https://github.com/GetStream/stream-chat-swift/pull/3785)
- Fix `LivestreamChannelController.synchronize()` not working if client not connected [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
- Fix membership updates in `LivestreamChannelController` [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
- Fix deleted messages updates in `LivestreamChannelController` [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
- Fix `channel.pinnedMessages` not updated when pinning a message in `LivestreamChannelController` [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)
- Fix `LivestreamChannelController` not watching the channel automatically when the current user joins the channel [#3787](https://github.com/GetStream/stream-chat-swift/pull/3787)

# [4.85.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.85.0)
_August 13, 2025_

Expand Down
22 changes: 22 additions & 0 deletions DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ final class DemoChatChannelListVC: ChatChannelListVC {
.equal(.hidden, to: true)
]))

lazy var allBlockedChannelsQuery: ChannelListQuery = .init(filter: .and([
.containMembers(userIds: [currentUserId]),
.or([
.equal(.blocked, to: false),
.equal(.blocked, to: true)
])
]))

lazy var unreadCountChannelsQuery: ChannelListQuery = .init(
filter: .and([
.containMembers(userIds: [currentUserId]),
Expand Down Expand Up @@ -145,6 +153,15 @@ final class DemoChatChannelListVC: ChatChannelListVC {
}
)

let allBlockedChannelsAction = UIAlertAction(
title: "Blocked and Non-Blocked Channels",
style: .default,
handler: { [weak self] _ in
self?.title = "All Blocked Channels"
self?.setAllBlockedChannelsQuery()
}
)

let unreadCountChannelsAction = UIAlertAction(
title: "Unread Count Channels",
style: .default,
Expand Down Expand Up @@ -212,6 +229,7 @@ final class DemoChatChannelListVC: ChatChannelListVC {
unreadCountChannelsAction,
hasUnreadChannelsAction,
hiddenChannelsAction,
allBlockedChannelsAction,
mutedChannelsAction,
coolChannelsAction,
pinnedChannelsAction,
Expand All @@ -227,6 +245,10 @@ final class DemoChatChannelListVC: ChatChannelListVC {
replaceQuery(hiddenChannelsQuery)
}

func setAllBlockedChannelsQuery() {
replaceQuery(allBlockedChannelsQuery)
}

func setUnreadCountChannelsQuery() {
replaceQuery(unreadCountChannelsQuery)
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<a href="https://sonarcloud.io/summary/new_code?id=GetStream_stream-chat-swift"><img src="https://sonarcloud.io/api/project_badges/measure?project=GetStream_stream-chat-swift&metric=coverage" /></a>
</p>
<p align="center">
<img id="stream-chat-label" alt="StreamChat" src="https://img.shields.io/badge/StreamChat-8.04%20MB-blue"/>
<img id="stream-chat-label" alt="StreamChat" src="https://img.shields.io/badge/StreamChat-8.08%20MB-blue"/>
<img id="stream-chat-ui-label" alt="StreamChatUI" src="https://img.shields.io/badge/StreamChatUI-4.86%20MB-blue"/>
</p>

Expand Down
39 changes: 24 additions & 15 deletions Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,25 +145,34 @@ open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegat
self.init(configuration: .default)
}

/// Initialises a new instance of StreamAudioRecorder
/// Initializes a new `StreamAudioRecorder`.
///
/// - Parameters:
/// - audioSessionConfigurator: The configurator to use to interact with `AVAudioSession`
/// - audioRecorderSettings: The settings that will be used any time a new `AVAudioRecorder` is instantiated
/// - audioFileName: The name of the file that will be used by every `AVAudioRecorder` instance to store in progress recordings.
/// - audioRecorderBaseStorageURL: The path in where we would like to store temporary and finalised recording files.
/// - audioRecorderMeterNormaliser: The normaliser that will be used to transform `AVAudioRecorder's` updated meter values.
public convenience init(
configuration: Configuration
/// - configuration: Configuration for the recorder.
/// - audioSessionConfigurator: The object used to interact with `AVAudioSession`. Defaults to `StreamAudioSessionConfigurator()`.
public init(
configuration: Configuration,
audioSessionConfigurator: AudioSessionConfiguring = StreamAudioSessionConfigurator()
) {
self.init(
configuration: configuration,
audioSessionConfigurator: StreamAudioSessionConfigurator(),
audioRecorderMeterNormaliser: AudioValuePercentageNormaliser(),
appStateObserver: StreamAppStateObserver(),
audioRecorderAVProvider: AVAudioRecorder.init
)
self.audioSessionConfigurator = audioSessionConfigurator
self.configuration = configuration
audioRecorderMeterNormaliser = AudioValuePercentageNormaliser()
appStateObserver = StreamAppStateObserver()
audioRecorderAVProvider = AVAudioRecorder.init
multicastDelegate = .init()

super.init()

setUp()
}

/// Initialises a new instance of StreamAudioRecorder
/// - Parameters:
/// - configuration: Configuration for the recorder.
/// - audioSessionConfigurator: The object used to interact with `AVAudioSession`.
/// - audioRecorderMeterNormaliser: Transforms meter values emitted by `AVAudioRecorder` into a normalised range for UI/logic.
/// - appStateObserver: Observes application lifecycle events that the recorder reacts to.
/// - audioRecorderAVProvider: Factory closure used to construct `AVAudioRecorder` instances for a given file URL and settings dictionary.
internal init(
configuration: Configuration,
audioSessionConfigurator: AudioSessionConfiguring,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,56 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
messages = channel.latestMessages
}

client.syncRepository.startTrackingLivestreamController(self)

updateChannelData(
channelQuery: channelQuery,
completion: completion
)
}

/// Start watching a channel
///
/// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails.
public func startWatching(isInRecoveryMode: Bool, completion: ((Error?) -> Void)? = nil) {
guard let cid = cid else {
let error = ClientError.ChannelNotCreatedYet()
callback {
completion?(error)
}
return
}

client.syncRepository.startTrackingLivestreamController(self)

updater.startWatching(cid: cid, isInRecoveryMode: isInRecoveryMode) { error in
self.callback {
completion?(error)
}
}
}

/// Stop watching a channel
///
/// - Parameter completion: Called when the API call is finished. Called with `Error` if the remote update fails.
public func stopWatching(completion: ((Error?) -> Void)? = nil) {
guard let cid = cid else {
let error = ClientError.ChannelNotCreatedYet()
callback {
completion?(error)
}
return
}

client.syncRepository.stopTrackingLivestreamController(self)

updater.stopWatching(cid: cid) { error in
self.callback {
completion?(error)
}
}
}

/// Loads previous (older) messages from backend.
/// - Parameters:
/// - messageId: ID of the last fetched message. You will get messages `older` than the provided ID.
Expand Down Expand Up @@ -788,13 +832,6 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel

switch result {
case .success(let payload):
// If it is the first page, save channel to the DB to make sure manual event handling
// can fetch the channel from the DB.
if channelQuery.pagination == nil {
client.databaseContainer.write { session in
try session.saveChannel(payload: payload)
}
}
self.handleChannelPayload(payload, channelQuery: channelQuery)
completion?(nil)

Expand All @@ -807,7 +844,11 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
}
}

apiClient.request(endpoint: endpoint, completion: requestCompletion)
updater.update(
channelQuery: channelQuery,
isInRecoveryMode: false,
completion: requestCompletion
)
}

private func handleChannelPayload(_ payload: ChannelPayload, channelQuery: ChannelQuery) {
Expand Down Expand Up @@ -892,7 +933,10 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
handleDeletedMessage(messageDeletedEvent.message)
return
}
handleUpdatedMessage(messageDeletedEvent.message)
let deletedMessage = messageDeletedEvent.message.changing(
deletedAt: messageDeletedEvent.createdAt
)
handleUpdatedMessage(deletedMessage)

case let newMessageErrorEvent as NewMessageErrorEvent:
guard let message = messages.first(where: { $0.id == newMessageErrorEvent.messageId }) else {
Expand All @@ -913,15 +957,73 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
case let channelUpdatedEvent as ChannelUpdatedEvent:
handleChannelUpdated(channelUpdatedEvent)

case is MemberAddedEvent,
is MemberRemovedEvent,
is MemberUpdatedEvent,
is NotificationAddedToChannelEvent,
is NotificationRemovedFromChannelEvent,
is NotificationInvitedEvent,
is NotificationInviteAcceptedEvent,
is NotificationInviteRejectedEvent:
updateChannelFromDataStore()
case let notificationAddedToChannelEvent as NotificationAddedToChannelEvent:
var members = Set(channel?.lastActiveMembers ?? [])
members.insert(notificationAddedToChannelEvent.member)
let memberCount = channel?.memberCount ?? 0
channel = channel?.changing(
members: Array(members),
membership: notificationAddedToChannelEvent.member,
memberCount: memberCount + 1
)
startWatching(isInRecoveryMode: false)

case let notificationRemovedFromChannelEvent as NotificationRemovedFromChannelEvent:
var members = channel?.lastActiveMembers ?? []
members.removeAll(where: { $0.id == notificationRemovedFromChannelEvent.user.id })
let memberCount = channel?.memberCount ?? 0

channel = channel?.changing(members: members, memberCount: memberCount - 1)
channel?.membership = nil

case let memberAddedEvent as MemberAddedEvent:
var members = Set(channel?.lastActiveMembers ?? [])
members.insert(memberAddedEvent.member)
let memberCount = channel?.memberCount ?? 0

var membership: ChatChannelMember?
if memberAddedEvent.member.id == currentUserId {
membership = memberAddedEvent.member
}
channel = channel?.changing(
members: Array(members),
membership: membership,
memberCount: memberCount + 1
)

case let memberRemovedEvent as MemberRemovedEvent:
var members = channel?.lastActiveMembers ?? []
members.removeAll(where: { $0.id == memberRemovedEvent.user.id })
let memberCount = channel?.memberCount ?? 0

var membership: ChatChannelMember? = channel?.membership
if memberRemovedEvent.user.id == currentUserId {
membership = nil
}
channel = channel?.changing(members: members, memberCount: memberCount - 1)
channel?.membership = membership

case let userWatchingEvent as UserWatchingEvent:
var watchers = channel?.lastActiveWatchers ?? []
if userWatchingEvent.isStarted {
watchers.append(userWatchingEvent.user)
} else {
watchers.removeAll(where: { $0.id == userWatchingEvent.user.id })
}
channel = channel?.changing(watchers: watchers, watcherCount: userWatchingEvent.watcherCount)

case let memberUpdatedEvent as MemberUpdatedEvent:
var members = channel?.lastActiveMembers ?? []
if let index = members.firstIndex(where: { $0.id == memberUpdatedEvent.member.id }) {
members[index] = memberUpdatedEvent.member
}

var membership: ChatChannelMember? = channel?.membership
if memberUpdatedEvent.member.id == currentUserId {
membership = memberUpdatedEvent.member
}

channel = channel?.changing(members: members, membership: membership)

case is UserBannedEvent,
is UserUnbannedEvent:
Expand Down Expand Up @@ -964,7 +1066,18 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel

private func handleUpdatedMessage(_ updatedMessage: ChatMessage) {
if let index = messages.firstIndex(where: { $0.id == updatedMessage.id }) {
let existingMessage = messages[index]
messages[index] = updatedMessage

if existingMessage.isPinned != updatedMessage.isPinned {
var pinnedMessages = channel?.pinnedMessages ?? []
if updatedMessage.isPinned {
pinnedMessages.append(updatedMessage)
} else {
pinnedMessages.removeAll(where: { $0.id == existingMessage.id })
}
channel = channel?.changing(pinnedMessages: pinnedMessages)
}
}
}

Expand Down Expand Up @@ -993,7 +1106,31 @@ public class LivestreamChannelController: DataStoreProvider, EventsControllerDel
}

private func handleChannelUpdated(_ event: ChannelUpdatedEvent) {
channel = event.channel
channel = channel?.changing(
name: event.channel.name,
imageURL: event.channel.imageURL,
lastMessageAt: event.channel.lastMessageAt,
createdAt: event.channel.createdAt,
deletedAt: event.channel.deletedAt,
updatedAt: event.channel.updatedAt,
truncatedAt: event.channel.truncatedAt,
isHidden: event.channel.isHidden,
createdBy: event.channel.createdBy,
config: event.channel.config,
ownCapabilities: event.channel.ownCapabilities,
isFrozen: event.channel.isFrozen,
isDisabled: event.channel.isDisabled,
isBlocked: event.channel.isBlocked,
reads: event.channel.reads,
members: event.channel.lastActiveMembers,
membership: event.channel.membership,
memberCount: event.channel.memberCount,
watchers: event.channel.lastActiveWatchers,
watcherCount: event.channel.watcherCount,
team: event.channel.team,
cooldownDuration: event.channel.cooldownDuration,
extraData: event.channel.extraData
)
}

// For events that do not have the channel data, and still
Expand Down
Loading
Loading