Skip to content

Commit

Permalink
Replace shared current user observer used for reading privacy settings (
Browse files Browse the repository at this point in the history
  • Loading branch information
laevandus authored Nov 21, 2024
1 parent 4d098f5 commit 98745b0
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 107 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix connection not resuming after guest user goes to background [#3483](https://github.com/GetStream/stream-chat-swift/pull/3483)
- Fix empty channel list if the channel list filter contains OR statement with only custom filtering keys [#3482](https://github.com/GetStream/stream-chat-swift/pull/3482)
- Fix rare crashes when accessing the current user object [#3500](https://github.com/GetStream/stream-chat-swift/pull/3500)
### ⚡ Performance
- Avoid creating `CurrentChatUserController` for reading user privacy settings which is more expensive than just reading the data from the local database [#3502](https://github.com/GetStream/stream-chat-swift/pull/3502)

# [4.66.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.66.0)
_November 05, 2024_
Expand Down Expand Up @@ -188,7 +190,7 @@ _July 10, 2024_
- Add support for user blocking [#3223](https://github.com/GetStream/stream-chat-swift/pull/3223)
- [Threads v2] Add support for Threads v2 [#3229](https://github.com/GetStream/stream-chat-swift/pull/3229)
- Add `ChatThreadListController` to fetch current user threads
- Add `ChatMessageController.markThreadRead()`
- Add `ChatMessageController.markThreadRead()`
- Add `ChatMessageController.markThreadUnread()`
- Add `ChatMessageController.updateThread()`
- Add `ChatMessageController.loadThread()`
Expand Down
22 changes: 0 additions & 22 deletions Sources/StreamChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ public class ChatClient {

/// The notification center used to send and receive notifications about incoming events.
private(set) var eventNotificationCenter: EventNotificationCenter

private var _sharedCurrentUserController: CurrentChatUserController?
private let queue = DispatchQueue(label: "io.getstream.chat-client")

/// The registry that contains all the attachment payloads associated with their attachment types.
/// For the meantime this is a static property to avoid breaking changes. On v5, this can be changed.
Expand Down Expand Up @@ -483,7 +480,6 @@ public class ChatClient {
/// Disconnects the chat client from the chat servers and removes all the local data related.
public func logout(completion: @escaping () -> Void) {
authenticationRepository.logOutUser()
resetSharedCurrentUserController()

// Stop tracking active components
syncRepository.removeAllTracked()
Expand Down Expand Up @@ -621,24 +617,6 @@ public class ChatClient {
completion?($0)
}
}

/// A shared user controller for an easy access to the current user.
var sharedCurrentUserController: CurrentChatUserController {
queue.sync {
if let controller = _sharedCurrentUserController {
return controller
}
let controller = currentUserController()
_sharedCurrentUserController = controller
return controller
}
}

func resetSharedCurrentUserController() {
queue.async {
self._sharedCurrentUserController = nil
}
}
}

extension ChatClient: AuthenticationRepositoryDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,16 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP

/// A boolean value indicating if it should send typing events.
/// It is `true` if the channel typing events are enabled as well as the user privacy settings.
internal var shouldSendTypingEvents: Bool {
/// Ignore if user typing indicators privacy settings are disabled. By default, they are enabled.
let currentUserPrivacySettings = client.sharedCurrentUserController.currentUser?.privacySettings
let isTypingIndicatorsForCurrentUserEnabled = currentUserPrivacySettings?.typingIndicators?.enabled ?? true
let isChannelTypingEventsEnabled = channel?.canSendTypingEvents ?? true
return isTypingIndicatorsForCurrentUserEnabled && isChannelTypingEventsEnabled
func shouldSendTypingEvents(completion: @escaping (Bool) -> Void) {
guard channel?.canSendTypingEvents ?? true else {
completion(false)
return
}
eventSender.database.read { session in
session.currentUser?.isTypingIndicatorsEnabled ?? true
} completion: { result in
completion(result.value ?? false)
}
}

/// Set the delegate of `ChannelController` to observe the changes in the system.
Expand Down Expand Up @@ -611,23 +615,19 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameter completion: a completion block with an error if the request was failed.
///
public func sendKeystrokeEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) {
/// Ignore if app-level typing events or user-level typing events are not enabled.
guard shouldSendTypingEvents else {
callback {
completion?(nil)
}
return
}

/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion?($0) }
return
}

eventSender.keystroke(in: cid, parentMessageId: parentMessageId) { error in
self.callback {
completion?(error)
/// Ignore if app-level typing events or user-level typing events are not enabled.
shouldSendTypingEvents { isEnabled in
guard isEnabled else {
self.callback { completion?(nil) }
return
}
self.eventSender.keystroke(in: cid, parentMessageId: parentMessageId) { error in
self.callback { completion?(error) }
}
}
}
Expand All @@ -641,21 +641,19 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameter completion: a completion block with an error if the request was failed.
///
public func sendStartTypingEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) {
/// Ignore if app-level typing events or user-level typing events are not enabled.
guard shouldSendTypingEvents else {
channelFeatureDisabled(feature: "typing events", completion: completion)
return
}

/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion?($0) }
return
}

eventSender.startTyping(in: cid, parentMessageId: parentMessageId) { error in
self.callback {
completion?(error)

shouldSendTypingEvents { isEnabled in
guard isEnabled else {
self.channelFeatureDisabled(feature: "typing events", completion: completion)
return
}
self.eventSender.startTyping(in: cid, parentMessageId: parentMessageId) { error in
self.callback { completion?(error) }
}
}
}
Expand All @@ -669,21 +667,18 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
/// - Parameter completion: a completion block with an error if the request was failed.
///
public func sendStopTypingEvent(parentMessageId: MessageId? = nil, completion: ((Error?) -> Void)? = nil) {
/// Ignore if app-level typing events or user-level typing events are not enabled.
guard shouldSendTypingEvents else {
channelFeatureDisabled(feature: "typing events", completion: completion)
return
}

/// Perform action only if channel is already created on backend side and have a valid `cid`.
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { completion?($0) }
return
}

eventSender.stopTyping(in: cid, parentMessageId: parentMessageId) { error in
self.callback {
completion?(error)
shouldSendTypingEvents { isEnabled in
guard isEnabled else {
self.channelFeatureDisabled(feature: "typing events", completion: completion)
return
}
self.eventSender.stopTyping(in: cid, parentMessageId: parentMessageId) { error in
self.callback { completion?(error) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ final class ChatClient_Mock: ChatClient {
// MARK: - Clean Up

func cleanUp() {
resetSharedCurrentUserController()

(apiClient as? APIClient_Spy)?.cleanUp()

fetchCurrentUserIdFromDatabase_called = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,63 @@

import Foundation
@testable import StreamChat
import XCTest

/// Mock implementation of ChannelUpdater
final class TypingEventsSender_Mock: TypingEventsSender {
@Atomic var keystroke_cid: ChannelId?
@Atomic var keystroke_parentMessageId: MessageId?
@Atomic var keystroke_completion: ((Error?) -> Void)?
@Atomic var keystroke_completion_expectation = XCTestExpectation()

@Atomic var startTyping_cid: ChannelId?
@Atomic var startTyping_parentMessageId: MessageId?
@Atomic var startTyping_completion: ((Error?) -> Void)?
@Atomic var startTyping_completion_expectation = XCTestExpectation()

@Atomic var stopTyping_cid: ChannelId?
@Atomic var stopTyping_parentMessageId: MessageId?
@Atomic var stopTyping_completion: ((Error?) -> Void)?
@Atomic var stopTyping_completion_result: Result<Void, Error>?

@Atomic var stopTyping_completion_expectation = XCTestExpectation()

override func keystroke(in cid: ChannelId, parentMessageId: MessageId?, completion: ((Error?) -> Void)? = nil) {
keystroke_cid = cid
keystroke_parentMessageId = parentMessageId
keystroke_completion = completion
keystroke_completion_expectation.fulfill()
}

override func startTyping(in cid: ChannelId, parentMessageId: MessageId?, completion: ((Error?) -> Void)? = nil) {
startTyping_cid = cid
startTyping_parentMessageId = parentMessageId
startTyping_completion = completion
startTyping_completion_expectation.fulfill()
}

override func stopTyping(in cid: ChannelId, parentMessageId: MessageId?, completion: ((Error?) -> Void)? = nil) {
stopTyping_cid = cid
stopTyping_parentMessageId = parentMessageId
stopTyping_completion = completion
stopTyping_completion_result?.invoke(with: completion)
stopTyping_completion_expectation.fulfill()
}

func cleanUp() {
keystroke_cid = nil
keystroke_parentMessageId = nil
keystroke_completion = nil
keystroke_completion_expectation = XCTestExpectation()

startTyping_cid = nil
startTyping_parentMessageId = nil
startTyping_completion = nil
startTyping_completion_expectation = XCTestExpectation()

stopTyping_cid = nil
stopTyping_parentMessageId = nil
stopTyping_completion = nil
stopTyping_completion_result = nil
stopTyping_completion_expectation = XCTestExpectation()
}
}
37 changes: 0 additions & 37 deletions Tests/StreamChatTests/ChatClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -835,43 +835,6 @@ final class ChatClient_Tests: XCTestCase {

XCTAssertEqual(streamHeader, SystemEnvironment.xStreamClientHeader)
}

// MARK: - User Session

func test_sharedCurrentUserController() {
let client = ChatClient_Mock.mock()
let controller1 = client.sharedCurrentUserController
let controller2 = client.sharedCurrentUserController
XCTAssertTrue(controller1 === controller2, "Shared instance should be returned")
}

func test_sharedCurrentUserController_whenConnectAndLogout_thenNewInstance() throws {
let client = ChatClient_Mock(config: inMemoryStorageConfig, environment: testEnv.environment)
let userInfo = UserInfo(id: "id1")
// Connect
let expectation = XCTestExpectation(description: "Connect")
client.mockAuthenticationRepository.connectUserResult = .success(())
try client.mockDatabaseContainer.createCurrentUser(id: userInfo.id)
client.connectUser(userInfo: userInfo, token: .unique()) { error in
guard error == nil else { return }
expectation.fulfill()
}
wait(for: [expectation], timeout: defaultTimeout)
let controller1 = client.sharedCurrentUserController
let controller2 = client.sharedCurrentUserController
XCTAssertTrue(controller1 === controller2, "Shared instance should be returned")

// Logout
let expectation2 = XCTestExpectation(description: "Logout")
let connectionRepositoryMock = try XCTUnwrap(client.connectionRepository as? ConnectionRepository_Mock)
connectionRepositoryMock.disconnectResult = .success(())
client.logout {
expectation2.fulfill()
}
wait(for: [expectation2], timeout: defaultTimeout)
let controller3 = client.sharedCurrentUserController
XCTAssertFalse(controller1 === controller3, "New instance should be returned")
}
}

final class TestWorker: Worker {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3034,6 +3034,7 @@ final class ChannelController_Tests: XCTestCase {
controller = nil

// Check keystroke cid and parentMessageId.
wait(for: [env.eventSender!.keystroke_completion_expectation], timeout: defaultTimeout)
XCTAssertEqual(env.eventSender!.keystroke_cid, channelId)
XCTAssertEqual(env.eventSender!.keystroke_parentMessageId, parentMessageId)

Expand Down Expand Up @@ -3103,8 +3104,9 @@ final class ChannelController_Tests: XCTestCase {
// (Try to) deallocate the controller
// by not keeping any references to it
controller = nil

// Check `startTyping` cid and parentMessageId.
wait(for: [env.eventSender!.startTyping_completion_expectation], timeout: defaultTimeout)
XCTAssertEqual(env.eventSender!.startTyping_cid, channelId)
XCTAssertEqual(env.eventSender!.startTyping_parentMessageId, parentMessageId)

Expand Down Expand Up @@ -3176,6 +3178,7 @@ final class ChannelController_Tests: XCTestCase {
controller = nil

// Check `stopTyping` cid and parentMessageId.
wait(for: [env.eventSender!.stopTyping_completion_expectation], timeout: defaultTimeout)
XCTAssertEqual(env.eventSender!.stopTyping_cid, channelId)
XCTAssertEqual(env.eventSender!.stopTyping_parentMessageId, parentMessageId)

Expand Down Expand Up @@ -5180,7 +5183,8 @@ final class ChannelController_Tests: XCTestCase {
try $0.saveCurrentUser(payload: .dummy(userId: userId, privacySettings: nil))
}

XCTAssertEqual(controller.shouldSendTypingEvents, true)
let isEnabled = try waitFor { controller.shouldSendTypingEvents(completion: $0) }
XCTAssertEqual(isEnabled, true)
}

func test_shouldSendTypingEvents_whenChannelEnabled_whenUserEnabled() throws {
Expand All @@ -5200,7 +5204,8 @@ final class ChannelController_Tests: XCTestCase {
)))
}

XCTAssertEqual(controller.shouldSendTypingEvents, true)
let isEnabled = try waitFor { controller.shouldSendTypingEvents(completion: $0) }
XCTAssertEqual(isEnabled, true)
}

func test_shouldSendTypingEvents_whenChannelDisabled_whenUserEnabled() throws {
Expand All @@ -5219,7 +5224,9 @@ final class ChannelController_Tests: XCTestCase {
cid: self.channelId, ownCapabilities: []
)))
}
XCTAssertEqual(controller.shouldSendTypingEvents, false)

let isEnabled = try waitFor { controller.shouldSendTypingEvents(completion: $0) }
XCTAssertEqual(isEnabled, false)
}

func test_shouldSendTypingEvents_whenChannelEnabled_whenUserDisabled() throws {
Expand All @@ -5239,7 +5246,8 @@ final class ChannelController_Tests: XCTestCase {
)))
}

XCTAssertEqual(controller.shouldSendTypingEvents, false)
let isEnabled = try waitFor { controller.shouldSendTypingEvents(completion: $0) }
XCTAssertEqual(isEnabled, false)
}

func test_shouldSendTypingEvents_whenChannelDisabled_whenUserDisabled() throws {
Expand All @@ -5259,7 +5267,8 @@ final class ChannelController_Tests: XCTestCase {
)))
}

XCTAssertEqual(controller.shouldSendTypingEvents, false)
let isEnabled = try waitFor { controller.shouldSendTypingEvents(completion: $0) }
XCTAssertEqual(isEnabled, false)
}

// MARK: deinit
Expand Down

0 comments on commit 98745b0

Please sign in to comment.