Skip to content
Open
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### 🔄 Changed
## StreamChat
### ✅ Added
- Add `hideHistoryBefore` to add members for configuring the history visibility [#3892](https://github.com/GetStream/stream-chat-swift/pull/3892)

# [4.94.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.94.0)
_December 02, 2025_
Expand Down
20 changes: 15 additions & 5 deletions DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,21 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
self.rootViewController.presentAlert(title: "User ID is not valid")
return
}
channelController.addMembers(
[MemberInfo(userId: id, extraData: nil)],
message: "Members added to the channel"
) { error in
if let error = error {
self.rootViewController.presentAlert(
title: "How many days to show?",
message: "Enter the number of days of history to show, 0 for full history",
textFieldPlaceholder: "Days"
) { daysString in
let hideHistoryBefore: Date? = {
guard let daysString, let days = Int(daysString) else { return nil }
return Calendar.current.date(byAdding: .day, value: -days, to: Date())
}()
channelController.addMembers(
[MemberInfo(userId: id, extraData: nil)],
hideHistoryBefore: hideHistoryBefore,
message: "Members added to the channel"
) { error in
guard let error else { return }
self.rootViewController.presentAlert(
title: "Couldn't add user \(id) to channel \(cid)",
message: "\(error)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,17 @@ extension Endpoint {
cid: ChannelId,
members: [MemberInfoRequest],
hideHistory: Bool,
hideHistoryBefore: Date? = nil,
messagePayload: MessageRequestBody? = nil
) -> Endpoint<EmptyResponse> {
var body: [String: AnyEncodable] = [
"add_members": AnyEncodable(members),
"hide_history": AnyEncodable(hideHistory)
"add_members": AnyEncodable(members)
]
if let hideHistoryBefore {
body["hide_history_before"] = AnyEncodable(hideHistoryBefore)
} else {
body["hide_history"] = AnyEncodable(hideHistory)
}
if let messagePayload = messagePayload {
body["message"] = AnyEncodable(messagePayload)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1145,13 +1145,15 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - members: An array of `MemberInfo` objects, each representing a member to be added to the channel.
/// - hideHistory: Hide the history of the channel to the added member. By default, it is false.
/// - hideHistoryBefore: Hide the history of the channel before this date. If both `hideHistoryBefore` and `hideHistory` are set, `hideHistoryBefore` takes precedence.
/// - message: Optional system message sent when adding members.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
public func addMembers(
_ members: [MemberInfo],
hideHistory: Bool = false,
hideHistoryBefore: Date? = nil,
message: String? = nil,
completion: ((Error?) -> Void)? = nil
) {
Expand All @@ -1165,7 +1167,8 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
cid: cid,
members: members,
message: message,
hideHistory: hideHistory
hideHistory: hideHistory,
hideHistoryBefore: hideHistoryBefore
) { error in
self.callback {
completion?(error)
Expand All @@ -1178,18 +1181,21 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameters:
/// - userIds: User ids that will be added to a channel.
/// - hideHistory: Hide the history of the channel to the added member. By default, it is false.
/// - hideHistoryBefore: Hide the history of the channel before this date. If both `hideHistoryBefore` and `hideHistory` are set, `hideHistoryBefore` takes precedence.
/// - message: Optional system message sent when adding members.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
public func addMembers(
userIds: Set<UserId>,
hideHistory: Bool = false,
hideHistoryBefore: Date? = nil,
message: String? = nil,
completion: ((Error?) -> Void)? = nil
) {
addMembers(
userIds.map { .init(userId: $0, extraData: nil) },
hideHistory: hideHistory,
hideHistoryBefore: hideHistoryBefore,
message: message,
completion: completion
)
Expand Down
14 changes: 10 additions & 4 deletions Sources/StreamChat/StateLayer/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,20 +229,23 @@ public class Chat {
/// - members: An array of member data that will be added to the channel.
/// - systemMessage: A system message to be added after adding members.
/// - hideHistory: If true, the previous history is available for added members, otherwise they do not see the history. The default value is false.
/// - hideHistoryBefore: Hide the history of the channel before this date. If both `hideHistoryBefore` and `hideHistory` are set, `hideHistoryBefore` takes precedence.
///
/// - Throws: An error while communicating with the Stream API.
public func addMembers(
_ members: [MemberInfo],
systemMessage: String? = nil,
hideHistory: Bool = false
hideHistory: Bool = false,
hideHistoryBefore: Date? = nil
) async throws {
let currentUserId = client.authenticationRepository.currentUserId
try await channelUpdater.addMembers(
currentUserId: currentUserId,
cid: cid,
members: members,
message: systemMessage,
hideHistory: hideHistory
hideHistory: hideHistory,
hideHistoryBefore: hideHistoryBefore
)
}

Expand All @@ -254,17 +257,20 @@ public class Chat {
/// - members: An array of user ids that will be added to the channel.
/// - systemMessage: A system message to be added after adding members.
/// - hideHistory: If true, the previous history is available for added members, otherwise they do not see the history. The default value is false.
/// - hideHistoryBefore: Hide the history of the channel before this date. If both `hideHistoryBefore` and `hideHistory` are set, `hideHistoryBefore` takes precedence.
///
/// - Throws: An error while communicating with the Stream API.
public func addMembers(
_ members: [UserId],
systemMessage: String? = nil,
hideHistory: Bool = false
hideHistory: Bool = false,
hideHistoryBefore: Date? = nil
) async throws {
try await addMembers(
members.map { .init(userId: $0, extraData: nil) },
systemMessage: systemMessage,
hideHistory: hideHistory
hideHistory: hideHistory,
hideHistoryBefore: hideHistoryBefore
)
}

Expand Down
9 changes: 7 additions & 2 deletions Sources/StreamChat/Workers/ChannelUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -460,13 +460,15 @@ class ChannelUpdater: Worker {
/// - members: The members input data to be added.
/// - message: Optional system message sent when adding a member.
/// - hideHistory: Hide the history of the channel to the added member.
/// - hideHistoryBefore: Hide the history of the channel before this date. If both `hideHistoryBefore` and `hideHistory` are set, `hideHistoryBefore` takes precedence.
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
func addMembers(
currentUserId: UserId? = nil,
cid: ChannelId,
members: [MemberInfo],
message: String? = nil,
hideHistory: Bool,
hideHistoryBefore: Date? = nil,
completion: ((Error?) -> Void)? = nil
) {
let messagePayload = messagePayload(text: message, currentUserId: currentUserId)
Expand All @@ -475,6 +477,7 @@ class ChannelUpdater: Worker {
cid: cid,
members: members.map { MemberInfoRequest(userId: $0.userId, extraData: $0.extraData) },
hideHistory: hideHistory,
hideHistoryBefore: hideHistoryBefore,
messagePayload: messagePayload
)
) {
Expand Down Expand Up @@ -841,15 +844,17 @@ extension ChannelUpdater {
cid: ChannelId,
members: [MemberInfo],
message: String? = nil,
hideHistory: Bool
hideHistory: Bool,
hideHistoryBefore: Date? = nil
) async throws {
try await withCheckedThrowingContinuation { continuation in
addMembers(
currentUserId: currentUserId,
cid: cid,
members: members,
message: message,
hideHistory: hideHistory
hideHistory: hideHistory,
hideHistoryBefore: hideHistoryBefore
) { error in
continuation.resume(with: error)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
@Atomic var addMembers_memberInfos: [MemberInfo]?
@Atomic var addMembers_message: String?
@Atomic var addMembers_hideHistory: Bool?
@Atomic var addMembers_hideHistoryBefore: Date?
@Atomic var addMembers_completion: ((Error?) -> Void)?
@Atomic var addMembers_completion_result: Result<Void, Error>?

Expand Down Expand Up @@ -213,6 +214,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
addMembers_message = nil
addMembers_userIds = nil
addMembers_hideHistory = nil
addMembers_hideHistoryBefore = nil
addMembers_completion = nil
addMembers_completion_result = nil

Expand Down Expand Up @@ -439,6 +441,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
members: [MemberInfo],
message: String?,
hideHistory: Bool,
hideHistoryBefore: Date? = nil,
completion: ((Error?) -> Void)? = nil
) {
addMembers_currentUserId = currentUserId
Expand All @@ -447,6 +450,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
addMembers_memberInfos = members
addMembers_message = message
addMembers_hideHistory = hideHistory
addMembers_hideHistoryBefore = hideHistoryBefore
addMembers_completion = completion
addMembers_completion_result?.invoke(with: completion)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,33 @@ final class ChannelEndpoints_Tests: XCTestCase {
XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
XCTAssertEqual("channels/\(cid.type.rawValue)/\(cid.id)", endpoint.path.value)
}

func test_addMembers_withHideHistoryBefore_buildsCorrectly() {
let cid = ChannelId.unique
let userIds: Set<UserId> = Set([UserId.unique])
let members = userIds.map { MemberInfoRequest(userId: $0, extraData: ["is_premium": true]) }
let hideHistoryBefore = Date()

let expectedEndpoint = Endpoint<EmptyResponse>(
path: .channelUpdate(cid.apiPath),
method: .post,
queryItems: nil,
requiresConnectionId: false,
body: ["add_members": AnyEncodable(members), "hide_history_before": AnyEncodable(hideHistoryBefore)]
)

// Build endpoint
let endpoint: Endpoint<EmptyResponse> = .addMembers(
cid: cid,
members: members,
hideHistory: true,
hideHistoryBefore: hideHistoryBefore
)

// Assert endpoint is built correctly
XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
XCTAssertEqual("channels/\(cid.type.rawValue)/\(cid.id)", endpoint.path.value)
}

func test_removeMembers_buildsCorrectly() {
let cid = ChannelId.unique
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3700,6 +3700,30 @@ final class ChannelController_Tests: XCTestCase {
// Completion should be called with the error
AssertAsync.willBeEqual(completionCalledError as? TestError, testError)
}

func test_addMembers_withHideHistoryBefore_callsChannelUpdater() {
let members: [MemberInfo] = [.init(userId: .unique, extraData: nil)]
let hideHistoryBefore = Date()

// Simulate `addMembers` call with hideHistoryBefore
controller.addMembers(
members,
hideHistory: false,
hideHistoryBefore: hideHistoryBefore
) { [callbackQueueID] error in
AssertTestQueue(withId: callbackQueueID)
XCTAssertNil(error)
}

// Assert hideHistoryBefore is passed to channelUpdater
XCTAssertEqual(env.channelUpdater!.addMembers_cid, channelId)
XCTAssertEqual(env.channelUpdater!.addMembers_memberInfos?.map(\.userId), members.map(\.userId))
XCTAssertEqual(env.channelUpdater!.addMembers_hideHistory, false)
XCTAssertEqual(env.channelUpdater!.addMembers_hideHistoryBefore, hideHistoryBefore)

// Simulate successful update
env.channelUpdater!.addMembers_completion?(nil)
}

// MARK: - Inviting members

Expand Down
39 changes: 39 additions & 0 deletions Tests/StreamChatTests/StateLayer/Chat_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,45 @@ final class Chat_Tests: XCTestCase {
}
}

func test_addMembers_withHideHistoryBefore_whenChannelUpdaterSucceeds_thenAddMembersSucceeds() async throws {
env.channelUpdaterMock.addMembers_completion_result = .success(())
let members: [MemberInfo] = [.init(userId: .unique, extraData: nil), .init(userId: .unique, extraData: nil)]
let hideHistoryBefore = Date()

try await chat.addMembers(
members,
systemMessage: "My system message",
hideHistory: false,
hideHistoryBefore: hideHistoryBefore
)

XCTAssertEqual(channelId, env.channelUpdaterMock.addMembers_cid)
XCTAssertEqual(members.map(\.userId).sorted(), env.channelUpdaterMock.addMembers_userIds?.sorted())
XCTAssertEqual("My system message", env.channelUpdaterMock.addMembers_message)
XCTAssertEqual(false, env.channelUpdaterMock.addMembers_hideHistory)
XCTAssertEqual(hideHistoryBefore, env.channelUpdaterMock.addMembers_hideHistoryBefore)
XCTAssertEqual(currentUserId, env.channelUpdaterMock.addMembers_currentUserId)
}

func test_addMembers_withHideHistoryBefore_takesPrecedenceOverHideHistory() async throws {
env.channelUpdaterMock.addMembers_completion_result = .success(())
let members: [MemberInfo] = [.init(userId: .unique, extraData: nil)]
let hideHistoryBefore = Date()

// Call with both hideHistory and hideHistoryBefore
try await chat.addMembers(
members,
systemMessage: nil,
hideHistory: true,
hideHistoryBefore: hideHistoryBefore
)

// Verify hideHistoryBefore is passed through
XCTAssertEqual(hideHistoryBefore, env.channelUpdaterMock.addMembers_hideHistoryBefore)
// Verify hideHistory is also passed (but hideHistoryBefore takes precedence in the endpoint)
XCTAssertEqual(true, env.channelUpdaterMock.addMembers_hideHistory)
}

func test_removeMembers_whenChannelUpdaterSucceeds_thenRemoveMembersSucceeds() async throws {
env.channelUpdaterMock.removeMembers_completion_result = .success(())
let memberIds: [UserId] = [.unique, .unique]
Expand Down
51 changes: 51 additions & 0 deletions Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,57 @@ final class ChannelUpdater_Tests: XCTestCase {
)
XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint))
}

func test_addMembersWithHideHistoryBefore_makesCorrectAPICall() {
let channelID = ChannelId.unique
let userIds: Set<UserId> = Set([UserId.unique])
let hideHistoryBefore = Date()

// Simulate `addMembers` call with hideHistoryBefore
channelUpdater.addMembers(
cid: channelID,
members: userIds.map { MemberInfo(userId: $0, extraData: nil) },
hideHistory: false,
hideHistoryBefore: hideHistoryBefore
)

// Assert correct endpoint is called
let referenceEndpoint: Endpoint<EmptyResponse> = .addMembers(
cid: channelID,
members: userIds.map { MemberInfoRequest(userId: $0, extraData: nil) },
hideHistory: false,
hideHistoryBefore: hideHistoryBefore
)
XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint))
}

func test_addMembersWithHideHistoryBefore_takesPrecedenceOverHideHistory() {
let channelID = ChannelId.unique
let userIds: Set<UserId> = Set([UserId.unique])
let hideHistoryBefore = Date()

// Simulate `addMembers` call with both hideHistory and hideHistoryBefore
channelUpdater.addMembers(
cid: channelID,
members: userIds.map { MemberInfo(userId: $0, extraData: nil) },
hideHistory: true,
hideHistoryBefore: hideHistoryBefore
)

// Assert correct endpoint is called with hideHistoryBefore (precedence)
let referenceEndpoint: Endpoint<EmptyResponse> = .addMembers(
cid: channelID,
members: userIds.map { MemberInfoRequest(userId: $0, extraData: nil) },
hideHistory: true,
hideHistoryBefore: hideHistoryBefore
)
XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint))

// Verify the body contains hide_history_before and not hide_history
let body = apiClient.request_endpoint?.body?.encodable as? [String: AnyEncodable]
XCTAssertNotNil(body?["hide_history_before"])
XCTAssertNil(body?["hide_history"])
}

func test_addMembers_successfulResponse_isPropagatedToCompletion() {
let channelID = ChannelId.unique
Expand Down
Loading