Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/actions/bootstrap/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: 'Run bootstrap.sh'
runs:
using: "composite"
steps:
- run: echo "IMAGE=${ImageOS}-${ImageVersion}" >> $GITHUB_ENV
- run: echo "IMAGE=${ImageOS}" >> $GITHUB_ENV
shell: bash
- run: echo "$HOME/.mint/bin" >> $GITHUB_PATH
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cron-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ jobs:
- uses: actions/[email protected]
- uses: ./.github/actions/bootstrap
- run: bundle exec fastlane rubocop
- run: ./Scripts/run-linter.sh
- run: bundle exec fastlane run_swift_format strict:true
- run: bundle exec fastlane pod_lint

slack:
Expand Down
12 changes: 4 additions & 8 deletions .github/workflows/smoke-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,11 @@ jobs:
with:
fetch-depth: 100
- uses: ./.github/actions/bootstrap
- name: Run Danger
run: bundle exec fastlane lint_pr
- name: Run Fastlane Linting
run: bundle exec fastlane rubocop
- name: Run SwiftFormat Linting
run: ./Scripts/run-linter.sh
- name: Run Podspec Linting
- run: bundle exec fastlane lint_pr
- run: bundle exec fastlane rubocop
- run: bundle exec fastlane run_swift_format strict:true
- run: bundle exec fastlane pod_lint
if: startsWith(github.event.pull_request.head.ref, 'release/')
run: bundle exec fastlane pod_lint

build-old-xcode:
name: Build LLC + UI (Xcode 15)
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ jobs:
steps:
- uses: actions/[email protected]

- uses: ./.github/actions/bootstrap
env:
INSTALL_SONAR: true
SKIP_MINT_BOOTSTRAP: true

- uses: actions/github-script@v6
id: get_pr_number
with:
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### 🔄 Changed

# [4.82.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.82.0)
_July 16, 2025_

## StreamChat
### 🐞 Fixed
- Fix channel unread count cleared when a thread is marked as read [#3710](https://github.com/GetStream/stream-chat-swift/pull/3710)
- Fix channel mute local state not updated after mute action callback [#3718](https://github.com/GetStream/stream-chat-swift/pull/3718)

# [4.81.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.81.0)
_July 03, 2025_

Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'danger', group: :danger_dependencies
gem 'fastlane', group: :fastlane_dependencies
gem 'json'
gem 'lefthook'
gem 'rubocop', '1.38', group: :rubocop_dependencies
gem 'sinatra', group: :sinatra_dependencies
gem 'slather'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ GEM
rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
lefthook (1.11.16)
logger (1.7.0)
method_source (1.1.0)
mini_magick (4.13.2)
Expand Down Expand Up @@ -444,6 +445,7 @@ DEPENDENCIES
fastlane-plugin-versioning
faye-websocket
json
lefthook
plist
puma
rackup
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-7.87%20MB-blue"/>
<img id="stream-chat-label" alt="StreamChat" src="https://img.shields.io/badge/StreamChat-7.88%20MB-blue"/>
<img id="stream-chat-ui-label" alt="StreamChatUI" src="https://img.shields.io/badge/StreamChatUI-4.84%20MB-blue"/>
</p>

Expand Down
14 changes: 5 additions & 9 deletions Scripts/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,11 @@ trap "echo ; echo ❌ The Bootstrap script failed to finish without error. See t

source ./Githubfile

puts "Create git/hooks folder if needed"
mkdir -p .git/hooks

# Symlink hooks folder to .git/hooks folder
puts "Create symlink for pre-commit hooks"
# Symlink needs to be ../../hooks and not ./hooks because git evaluates them in .git/hooks
ln -sf ../../hooks/pre-commit.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
chmod +x ./hooks/git-format-staged
if [ "${GITHUB_ACTIONS:-}" != "true" ]; then
puts "Set up git hooks"
bundle install
bundle exec lefthook install
fi

if [ "${SKIP_MINT_BOOTSTRAP:-}" != true ]; then
puts "Bootstrap Mint dependencies"
Expand Down
16 changes: 0 additions & 16 deletions Scripts/run-linter.sh

This file was deleted.

21 changes: 19 additions & 2 deletions Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,32 @@ extension Endpoint {
)
}

static func muteChannel(cid: ChannelId, mute: Bool, expiration: Int? = nil) -> Endpoint<EmptyResponse> {
static func muteChannel(
cid: ChannelId,
expiration: Int? = nil
) -> Endpoint<MutedChannelPayloadResponse> {
var body: [String: AnyEncodable] = ["channel_cid": AnyEncodable(cid)]

if let expiration = expiration {
body["expiration"] = AnyEncodable(expiration)
}

return .init(
path: .muteChannel(mute),
path: .muteChannel(true),
method: .post,
queryItems: nil,
requiresConnectionId: true,
body: body
)
}

static func unmuteChannel(
cid: ChannelId
) -> Endpoint<EmptyResponse> {
var body: [String: AnyEncodable] = ["channel_cid": AnyEncodable(cid)]

return .init(
path: .muteChannel(false),
method: .post,
queryItems: nil,
requiresConnectionId: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ extension Endpoint {

struct MessagePartialUpdateRequest: Encodable {
var set: SetProperties?
var unset: [String]? = nil
var unset: [String]?
var skipEnrichUrl: Bool?
var userId: String?
var user: UserRequestBody?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

import Foundation

struct MutedChannelPayloadResponse: Decodable {
private enum CodingKeys: String, CodingKey {
case channelMute = "channel_mute"
}

let channelMute: MutedChannelPayload
}

/// An object describing the incoming muted-channel JSON payload.
struct MutedChannelPayload: Decodable {
private enum CodingKeys: String, CodingKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
return
}

updater.muteChannel(cid: cid, mute: true, expiration: expiration) { error in
updater.muteChannel(cid: cid, expiration: expiration) { error in
self.callback {
completion?(error)
}
Expand All @@ -367,7 +367,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
return
}

updater.muteChannel(cid: cid, mute: false) { error in
updater.unmuteChannel(cid: cid) { error in
self.callback {
completion?(error)
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/StreamChat/Database/DTOs/ChannelMuteDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,8 @@ extension NSManagedObjectContext {

return dto
}

func delete(mute: ChannelMuteDTO) {
delete(mute)
}
}
7 changes: 7 additions & 0 deletions Sources/StreamChat/Database/DTOs/MessageDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,8 @@ extension NSManagedObjectContext: MessageDatabaseSession {

return message
}

// swiftlint:disable function_body_length

/// Saves a message into the local DB.
/// - Parameters:
Expand Down Expand Up @@ -1151,6 +1153,8 @@ extension NSManagedObjectContext: MessageDatabaseSession {
return dto
}

// swiftlint:enable function_body_length

func saveMessages(
messagesPayload: MessageListPayload,
for cid: ChannelId?,
Expand Down Expand Up @@ -1741,6 +1745,7 @@ extension MessageDTO {
}

private extension ChatMessage {
// swiftlint:disable function_body_length
init(fromDTO dto: MessageDTO, depth: Int) throws {
guard StreamRuntimeCheck._canFetchRelationship(currentDepth: depth) else {
throw RecursionLimitError()
Expand Down Expand Up @@ -1911,6 +1916,8 @@ private extension ChatMessage {

self = message
}

// swiftlint:enable function_body_length
}

extension ClientError {
Expand Down
3 changes: 3 additions & 0 deletions Sources/StreamChat/Database/DatabaseSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ protocol MessageDatabaseSession {
/// - Parameter message: The DTO to be deleted
func delete(message: MessageDTO)

/// Deletes a mute.
func delete(mute: ChannelMuteDTO)

/// Fetches `MessageReactionDTO` for the given `messageId`, `userId`, and `type` from the DB.
/// Returns `nil` if there is no matching `MessageReactionDTO`.
func reaction(messageId: MessageId, userId: UserId, type: MessageReactionType) -> MessageReactionDTO?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import Foundation

extension SystemEnvironment {
/// A Stream Chat version.
public static let version: String = "4.81.0"
public static let version: String = "4.82.0"
}
2 changes: 1 addition & 1 deletion Sources/StreamChat/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>4.81.0</string>
<string>4.82.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
Expand Down
11 changes: 7 additions & 4 deletions Sources/StreamChat/Models/ChatMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public struct ChatMessage {

/// The original language of the message.
public let originalLanguage: TranslationLanguage?

/// The moderation details in case the message was moderated.
public let moderationDetails: MessageModerationDetails?

Expand All @@ -177,7 +177,7 @@ public struct ChatMessage {
///
/// - Note: For the message authored by other channel members this field always returns `0`.
public var readByCount: Int { readBy.count }

/// Optional poll that is part of the message.
public let poll: Poll?

Expand Down Expand Up @@ -369,7 +369,7 @@ public struct ChatMessage {
extraData: extraData
)
}

/// Returns a new `ChatMessage` with the provided data replaced.
///
/// If the provided data is `nil`, it will overwrite the existing value with `nil`.
Expand Down Expand Up @@ -541,12 +541,13 @@ public extension ChatMessage {
if let localState = self.localState {
return localState.isLocalOnly
}

return type == .ephemeral || type == .error
}
}

extension ChatMessage: Hashable {
// swiftlint:disable cyclomatic_complexity
public static func == (lhs: Self, rhs: Self) -> Bool {
guard lhs.id == rhs.id else { return false }
guard lhs.localState == rhs.localState else { return false }
Expand Down Expand Up @@ -578,6 +579,8 @@ extension ChatMessage: Hashable {
return true
}

// swiftlint:enable cyclomatic_complexity

public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
Expand Down
11 changes: 6 additions & 5 deletions Sources/StreamChat/Repositories/MessageRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ 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<ChatMessage, MessageRepositoryError>) -> Void) {
var dataEndpoint: DataEndpoint!
Expand All @@ -110,7 +110,7 @@ class MessageRepository {
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<MessagePayload.Boxed> = .sendMessage(
Expand All @@ -120,7 +120,7 @@ class MessageRepository {
skipEnrichUrl: dto.skipEnrichUrl
)
dataEndpoint = endpoint.withDataResponse

// Mark it as failed
dto.localMessageState = .sendingFailed
messageModel = try dto.asModel()
Expand Down Expand Up @@ -314,7 +314,8 @@ class MessageRepository {
syncOwnReactions: true,
skipDraftUpdate: false,
cache: nil
).asModel()
)
.asModel()
if !store {
// Force load attachments before discarding changes
_ = message?.attachmentCounts
Expand Down Expand Up @@ -448,7 +449,7 @@ extension MessageRepository {
.map { try $0.asModel() }
}
}

/// Fetches replies from the database with a date range.
func replies(from fromDate: Date, to toDate: Date, in message: MessageId) async throws -> [ChatMessage] {
try await database.read { session in
Expand Down
4 changes: 2 additions & 2 deletions Sources/StreamChat/StateLayer/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1140,14 +1140,14 @@ public class Chat {
///
/// - Throws: An error while communicating with the Stream API.
public func mute(expiration: Int? = nil) async throws {
try await channelUpdater.muteChannel(true, cid: cid, expiration: expiration)
try await channelUpdater.muteChannel(cid: cid, expiration: expiration)
}

/// Unmutes the channel which enables push notifications and unread count changes for new messages.
///
/// - Throws: An error while communicating with the Stream API.
public func unmute() async throws {
try await channelUpdater.muteChannel(false, cid: cid)
try await channelUpdater.unmuteChannel(cid: cid)
}

/// Hide the channel which removes if from the query channel requests for that user until a new message is added.
Expand Down
Loading
Loading