Skip to content

Commit 79ecf3e

Browse files
authored
Channel Partial Update (#2681)
* Add partialChannelUpdate to ChannelEndpoints * Expose partialChannelUpdate in ChannelController * Update CHANGELOG * Update ChannelUpdater tests * Update ChannelController tests
1 parent ce33d9f commit 79ecf3e

File tree

8 files changed

+248
-2
lines changed

8 files changed

+248
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
## StreamChat
77
### ✅ Added
88
- Expose Extra Data for Giphy Attachment Payloads [#2678](https://github.com/GetStream/stream-chat-swift/pull/2678)
9+
- Add support for partial Channel update [#2681](https://github.com/GetStream/stream-chat-swift/pull/2681)
10+
911
### 🐞 Fixed
1012
- Rescue messages that are stuck in `.sending` state [#2676](https://github.com/GetStream/stream-chat-swift/pull/2676)
1113
- Fix not being able to resend failed attachments [#2680](https://github.com/GetStream/stream-chat-swift/pull/2680)

Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ extension Endpoint {
4444
)
4545
}
4646

47+
static func partialChannelUpdate(updates: ChannelEditDetailPayload, unsetProperties: [String]) -> Endpoint<EmptyResponse> {
48+
let body: [String: AnyEncodable] = [
49+
"set": AnyEncodable(updates),
50+
"unset": AnyEncodable(unsetProperties)
51+
]
52+
53+
return .init(
54+
path: .channelUpdate(updates.apiPath),
55+
method: .patch,
56+
queryItems: nil,
57+
requiresConnectionId: false,
58+
body: body
59+
)
60+
}
61+
4762
static func muteChannel(cid: ChannelId, mute: Bool) -> Endpoint<EmptyResponse> {
4863
.init(
4964
path: .muteChannel(mute),

Sources/StreamChat/Controllers/ChannelController/ChannelController.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,50 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
262262
}
263263
}
264264

265+
/// Updates channel information with provided data, and removes unneeded properties.
266+
///
267+
/// - Parameters:
268+
/// - team: New team.
269+
/// - members: New members.
270+
/// - invites: New invites.
271+
/// - extraData: New `ExtraData`.
272+
/// - unsetProperties: Properties from the channel that are going to be cleared/unset.
273+
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
274+
/// If request fails, the completion will be called with an error.
275+
///
276+
public func partialChannelUpdate(
277+
name: String? = nil,
278+
imageURL: URL? = nil,
279+
team: String? = nil,
280+
members: Set<UserId> = [],
281+
invites: Set<UserId> = [],
282+
extraData: [String: RawJSON] = [:],
283+
unsetProperties: [String] = [],
284+
completion: ((Error?) -> Void)? = nil
285+
) {
286+
/// Perform action only if channel is already created on backend side and have a valid `cid`.
287+
guard let cid = cid, isChannelAlreadyCreated else {
288+
channelModificationFailed(completion)
289+
return
290+
}
291+
292+
let payload: ChannelEditDetailPayload = .init(
293+
cid: cid,
294+
name: name,
295+
imageURL: imageURL,
296+
team: team,
297+
members: members,
298+
invites: invites,
299+
extraData: extraData
300+
)
301+
302+
updater.partialChannelUpdate(updates: payload, unsetProperties: unsetProperties) { error in
303+
self.callback {
304+
completion?(error)
305+
}
306+
}
307+
}
308+
265309
/// Mutes the channel this controller manages.
266310
///
267311
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.

Sources/StreamChat/Workers/ChannelUpdater.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,29 @@ class ChannelUpdater: Worker {
9999

100100
/// Updates specific channel with new data.
101101
/// - Parameters:
102-
/// - channelPayload: New channel data..
102+
/// - channelPayload: New channel data.
103103
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
104104
func updateChannel(channelPayload: ChannelEditDetailPayload, completion: ((Error?) -> Void)? = nil) {
105105
apiClient.request(endpoint: .updateChannel(channelPayload: channelPayload)) {
106106
completion?($0.error)
107107
}
108108
}
109109

110+
/// Updates specific channel with provided data, and removes unneeded properties.
111+
/// - Parameters:
112+
/// - updates: Updated channel data. Only non-nil data will be updated.
113+
/// - unsetProperties: Properties from the channel that are going to be cleared/unset.
114+
/// - completion: Called when the API call is finished. Called with `Error` if the remote update fails.
115+
func partialChannelUpdate(
116+
updates: ChannelEditDetailPayload,
117+
unsetProperties: [String],
118+
completion: ((Error?) -> Void)? = nil
119+
) {
120+
apiClient.request(endpoint: .partialChannelUpdate(updates: updates, unsetProperties: unsetProperties)) {
121+
completion?($0.error)
122+
}
123+
}
124+
110125
/// Mutes/unmutes the specific channel.
111126
/// - Parameters:
112127
/// - cid: The channel identifier.

TestTools/StreamChatTestTools/Mocks/StreamChat/Workers/ChannelUpdater_Mock.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ final class ChannelUpdater_Mock: ChannelUpdater {
1515
@Atomic var updateChannel_payload: ChannelEditDetailPayload?
1616
@Atomic var updateChannel_completion: ((Error?) -> Void)?
1717

18+
@Atomic var partialChannelUpdate_updates: ChannelEditDetailPayload?
19+
@Atomic var partialChannelUpdate_unsetProperties: [String]?
20+
@Atomic var partialChannelUpdate_completion: ((Error?) -> Void)?
21+
1822
@Atomic var muteChannel_cid: ChannelId?
1923
@Atomic var muteChannel_mute: Bool?
2024
@Atomic var muteChannel_completion: ((Error?) -> Void)?
@@ -221,6 +225,12 @@ final class ChannelUpdater_Mock: ChannelUpdater {
221225
updateChannel_completion = completion
222226
}
223227

228+
override func partialChannelUpdate(updates: ChannelEditDetailPayload, unsetProperties: [String], completion: ((Error?) -> Void)? = nil) {
229+
partialChannelUpdate_updates = updates
230+
partialChannelUpdate_unsetProperties = unsetProperties
231+
partialChannelUpdate_completion = completion
232+
}
233+
224234
override func muteChannel(cid: ChannelId, mute: Bool, completion: ((Error?) -> Void)? = nil) {
225235
muteChannel_cid = cid
226236
muteChannel_mute = mute

Tests/StreamChatTests/APIClient/Endpoints/ChannelEndpoints_Tests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,28 @@ final class ChannelEndpoints_Tests: XCTestCase {
9595
// Assert endpoint is built correctly
9696
XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
9797
XCTAssertEqual("channels/\(channelPayload.apiPath)", endpoint.path.value)
98+
XCTAssertEqual(expectedEndpoint.method, endpoint.method)
99+
}
100+
101+
func test_partialChannelUpdate_buildsCorrectly() {
102+
let channelPayload: ChannelEditDetailPayload = .unique
103+
let unsetProperties = ["user", "clash"]
104+
105+
let expectedEndpoint = Endpoint<EmptyResponse>(
106+
path: .channelUpdate(channelPayload.apiPath),
107+
method: .patch,
108+
queryItems: nil,
109+
requiresConnectionId: false,
110+
body: ["set": AnyEncodable(channelPayload), "unset": AnyEncodable(unsetProperties)]
111+
)
112+
113+
// Build endpoint
114+
let endpoint: Endpoint<EmptyResponse> = .partialChannelUpdate(updates: channelPayload, unsetProperties: unsetProperties)
115+
116+
// Assert endpoint is built correctly
117+
XCTAssertEqual(AnyEndpoint(expectedEndpoint), AnyEndpoint(endpoint))
118+
XCTAssertEqual("channels/\(channelPayload.apiPath)", endpoint.path.value)
119+
XCTAssertEqual(expectedEndpoint.method, endpoint.method)
98120
}
99121

100122
func test_deleteChannel_buildsCorrectly() {

Tests/StreamChatTests/Controllers/ChannelController/ChannelController_Tests.swift

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1581,7 +1581,7 @@ final class ChannelController_Tests: XCTestCase {
15811581
}
15821582
XCTAssert(error is ClientError.ChannelNotCreatedYet)
15831583

1584-
// Simulate succsesfull backend channel creation
1584+
// Simulate successful backend channel creation
15851585
env.channelUpdater!.update_onChannelCreated?(query.cid!)
15861586

15871587
// Simulate `updateChannel` call and assert no error is returned
@@ -1641,6 +1641,97 @@ final class ChannelController_Tests: XCTestCase {
16411641
AssertAsync.willBeEqual(completionCalledError as? TestError, testError)
16421642
}
16431643

1644+
// MARK: - Channel partial update
1645+
1646+
func test_partialChannelUpdate_failsForNewChannels() throws {
1647+
// Create `ChannelController` for new channel
1648+
let query = ChannelQuery(channelPayload: .unique)
1649+
setupControllerForNewChannel(query: query)
1650+
1651+
var receivedError: Error?
1652+
let expectation = self.expectation(description: "partialChannelUpdate completes")
1653+
controller.partialChannelUpdate { [callbackQueueID] error in
1654+
AssertTestQueue(withId: callbackQueueID)
1655+
receivedError = error
1656+
expectation.fulfill()
1657+
}
1658+
1659+
waitForExpectations(timeout: defaultTimeout)
1660+
XCTAssert(receivedError is ClientError.ChannelNotCreatedYet)
1661+
}
1662+
1663+
func test_partialChannelUpdate_propagatesSuccess() throws {
1664+
let name = "Jason Bourne"
1665+
let imageURL: URL? = nil
1666+
let team = "team-one"
1667+
let members: Set<UserId> = ["member1", "member2"]
1668+
let invites: Set<UserId> = ["invite1"]
1669+
let extraData: [String: RawJSON] = ["scope": "test"]
1670+
let unsetProperties: [String] = ["user.id", "channel_store"]
1671+
1672+
var receivedError: Error?
1673+
let expectation = self.expectation(description: "partialChannelUpdate completes")
1674+
controller.partialChannelUpdate(
1675+
name: name,
1676+
imageURL: imageURL,
1677+
team: team,
1678+
members: members,
1679+
invites: invites,
1680+
extraData: extraData,
1681+
unsetProperties: unsetProperties
1682+
) { [callbackQueueID] error in
1683+
AssertTestQueue(withId: callbackQueueID)
1684+
receivedError = error
1685+
expectation.fulfill()
1686+
}
1687+
1688+
let updater = try XCTUnwrap(env.channelUpdater)
1689+
1690+
// Simulate successful update
1691+
updater.partialChannelUpdate_completion?(nil)
1692+
updater.partialChannelUpdate_completion = nil
1693+
1694+
waitForExpectations(timeout: defaultTimeout)
1695+
1696+
XCTAssertEqual(updater.partialChannelUpdate_updates?.name, name)
1697+
XCTAssertEqual(updater.partialChannelUpdate_updates?.imageURL, imageURL)
1698+
XCTAssertEqual(updater.partialChannelUpdate_updates?.team, team)
1699+
XCTAssertEqual(updater.partialChannelUpdate_updates?.members, members)
1700+
XCTAssertEqual(updater.partialChannelUpdate_updates?.invites, invites)
1701+
XCTAssertEqual(updater.partialChannelUpdate_updates?.extraData, extraData)
1702+
XCTAssertEqual(updater.partialChannelUpdate_unsetProperties, unsetProperties)
1703+
XCTAssertNil(receivedError)
1704+
}
1705+
1706+
func test_partialChannelUpdate_propagatesError() throws {
1707+
var receivedError: Error?
1708+
let expectation = self.expectation(description: "partialChannelUpdate completes")
1709+
controller.partialChannelUpdate(
1710+
name: .unique,
1711+
imageURL: .unique(),
1712+
team: .unique,
1713+
members: [],
1714+
invites: [],
1715+
extraData: [:],
1716+
unsetProperties: []
1717+
) { [callbackQueueID] error in
1718+
AssertTestQueue(withId: callbackQueueID)
1719+
receivedError = error
1720+
expectation.fulfill()
1721+
}
1722+
1723+
let updater = try XCTUnwrap(env.channelUpdater)
1724+
1725+
// Simulate failed update
1726+
let testError = TestError()
1727+
updater.partialChannelUpdate_completion?(testError)
1728+
updater.partialChannelUpdate_completion = nil
1729+
1730+
waitForExpectations(timeout: defaultTimeout)
1731+
1732+
XCTAssertEqual(receivedError, testError)
1733+
}
1734+
16441735
// MARK: - Muting channel
16451736

16461737
func test_muteChannel_failsForNewChannels() throws {

Tests/StreamChatTests/Workers/ChannelUpdater_Tests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,53 @@ final class ChannelUpdater_Tests: XCTestCase {
708708
AssertAsync.willBeEqual(completionCalledError as? TestError, error)
709709
}
710710

711+
// MARK: - Partial channel update
712+
713+
func test_partialChannelUpdate_makesCorrectAPICall() {
714+
let updates: ChannelEditDetailPayload = .unique
715+
let unsetProperties: [String] = ["user.id", "channel_store"]
716+
717+
// Simulate `partialChannelUpdate(updates:unsetProperties:completion:)` call
718+
channelUpdater.partialChannelUpdate(updates: updates, unsetProperties: unsetProperties)
719+
720+
// Assert correct endpoint is called
721+
let referenceEndpoint: Endpoint<EmptyResponse> = .partialChannelUpdate(updates: updates, unsetProperties: unsetProperties)
722+
XCTAssertEqual(apiClient.request_endpoint, AnyEndpoint(referenceEndpoint))
723+
}
724+
725+
func test_partialChannelUpdate_successfulResponse_isPropagatedToCompletion() {
726+
// Simulate `partialChannelUpdate(updates:unsetProperties:completion:)` call
727+
var receivedError: Error?
728+
let expectation = self.expectation(description: "partialChannelUpdate completion")
729+
channelUpdater.partialChannelUpdate(updates: .unique, unsetProperties: []) { error in
730+
receivedError = error
731+
expectation.fulfill()
732+
}
733+
734+
// Simulate API response with success
735+
apiClient.test_simulateResponse(Result<EmptyResponse, Error>.success(.init()))
736+
waitForExpectations(timeout: defaultTimeout)
737+
738+
XCTAssertNil(receivedError)
739+
}
740+
741+
func test_partialChannelUpdate_errorResponse_isPropagatedToCompletion() {
742+
// Simulate `partialChannelUpdate(updates:unsetProperties:completion:)` call
743+
var receivedError: Error?
744+
let expectation = self.expectation(description: "partialChannelUpdate completion")
745+
channelUpdater.partialChannelUpdate(updates: .unique, unsetProperties: []) { error in
746+
receivedError = error
747+
expectation.fulfill()
748+
}
749+
750+
// Simulate API response with failure
751+
let error = TestError()
752+
apiClient.test_simulateResponse(Result<EmptyResponse, Error>.failure(error))
753+
waitForExpectations(timeout: defaultTimeout)
754+
755+
XCTAssertEqual(receivedError, error)
756+
}
757+
711758
// MARK: - Mute channel
712759

713760
func test_muteChannel_makesCorrectAPICall() {

0 commit comments

Comments
 (0)