Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CIS-877] User typing events #1254

Merged
merged 7 commits into from
Jul 9, 2021
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

- Changed Channel from `currentlyTypingMembers: Set<ChatChannelMember>` to `currentlyTypingUsers: Set<ChatUser>` to show all typing users (not only channel members; eg: watching users) [#1254](https://github.com/GetStream/stream-chat-swift/pull/1254)

### ⚠️ Breaking Changes from `4.0-beta.6`
- The `ChatSuggestionsViewController` was renamed to `ChatSuggestionsVC` to follow the same pattern across the codebase. [#1195](https://github.com/GetStream/stream-chat-swift/pull/1195)

Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ var streamChatSourcesExcluded: [String] { [
"WebSocketClient/EventMiddlewares/UserWatchingEventMiddleware_Tests.swift",
"WebSocketClient/EventMiddlewares/TypingStartCleanupMiddleware_Tests.swift",
"WebSocketClient/EventMiddlewares/EventMiddleware_Mock.swift",
"WebSocketClient/EventMiddlewares/ChannelMemberTypingStateUpdaterMiddleware_Tests.swift",
"WebSocketClient/EventMiddlewares/UserTypingStateUpdaterMiddleware_Tests.swift",
"WebSocketClient/EventMiddlewares/ChannelVisibilityEventMiddleware_Tests.swift",
"WebSocketClient/EventMiddlewares/EventMiddleware_Tests.swift",
"WebSocketClient/EventMiddlewares/EventDataProcessorMiddleware_Tests.swift",
Expand Down
8 changes: 4 additions & 4 deletions Sample/Extensions/Channel+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ func createMemberInfoString(for channel: ChatChannel) -> String {
"\(channel.memberCount) members, \(channel.watcherCount) online"
}

/// Creates formatted string for currently typing members from `currentlyTypingMembers` property of `ChatChannel` if any.
/// Creates formatted string for currently typing users from `currentlyTypingUsers` property of `ChatChannel` if any.
///
/// Example result: ` "Nick is typing..."` or`"Nick, Maria are typing..."`
func createTypingMemberString(for channel: ChatChannel?) -> String? {
guard let members = channel?.currentlyTypingMembers, !members.isEmpty else { return nil }
let names = members.map { $0.name ?? $0.id }.sorted()
func createTypingUserString(for channel: ChatChannel?) -> String? {
guard let users = channel?.currentlyTypingUsers, !users.isEmpty else { return nil }
let names = users.map { $0.name ?? $0.id }.sorted()
return names.joined(separator: ", ") + " \(names.count == 1 ? "is" : "are") typing..."
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ class CombineSimpleChannelsViewController: UITableViewController {
let channel = channelListController.channels[indexPath.row]

let subtitle: String
if let typingMembersInfo = createTypingMemberString(for: channel) {
subtitle = typingMembersInfo
if let typingUsersInfo = createTypingUserString(for: channel) {
subtitle = typingUsersInfo
} else if let latestMessage = channel.latestMessages.first {
let author = latestMessage.author.name ?? latestMessage.author.id.description
subtitle = "\(author): \(latestMessage.text)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class CombineSimpleChatViewController: UITableViewController, UITextViewDe

///
/// This subscription updates the view controller's `title` and its `navigationItem.prompt` to display the count of channel
/// members and the count of online members or typing members if any.
/// members and the count of online members or typing users if any.
/// When the channel is deleted, this view controller is dismissed.
///
let updatedChannel = channelController
Expand Down Expand Up @@ -88,7 +88,7 @@ final class CombineSimpleChatViewController: UITableViewController, UITextViewDe
.store(in: &cancellables)

updatedChannel
.map { createTypingMemberString(for: $0) ?? createMemberInfoString(for: $0) }
.map { createTypingUserString(for: $0) ?? createMemberInfoString(for: $0) }
.assign(to: \.navigationItem.prompt, on: self)
.store(in: &cancellables)

Expand Down Expand Up @@ -123,15 +123,15 @@ final class CombineSimpleChatViewController: UITableViewController, UITextViewDe
.store(in: &cancellables)

///
/// This subscription updates UI with typing members after receiving changes from `typingMembersPublisher`.
/// This subscription updates UI with typing users after receiving changes from `typingUsersPublisher`.
///
channelController
.typingMembersPublisher
.typingUsersPublisher
.sink { [weak self] _ in
self?.title = self?.channelController.channel
.flatMap { createChannelTitle(for: $0, self?.channelController.client.currentUserId) }
self?.navigationItem.prompt = self?.channelController.channel.flatMap {
createTypingMemberString(for: $0) ?? createMemberInfoString(for: $0)
createTypingUserString(for: $0) ?? createMemberInfoString(for: $0)
}
}
.store(in: &cancellables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ class SimpleChannelsViewController: UITableViewController, ChatChannelListContro
let channel = channelListController.channels[indexPath.row]

let subtitle: String
if let typingMembersInfo = createTypingMemberString(for: channel) {
subtitle = typingMembersInfo
if let typingUsersInfo = createTypingUserString(for: channel) {
subtitle = typingUsersInfo
} else if let latestMessage = channel.latestMessages.first {
let author = latestMessage.author.name ?? latestMessage.author.id.description
subtitle = "\(author): \(latestMessage.text)"
Expand Down
8 changes: 4 additions & 4 deletions Sample/Samples/SimpleChat/SimpleChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ final class SimpleChatViewController: UITableViewController, ChatChannelControll
}

///
/// # didChangeTypingMembers
/// # didChangeTypingUsers
///
/// The method below receives a set of `Member` that are currently typing.
/// The method below receives a set of `User` that are currently typing.
///
func channelController(
_ channelController: ChatChannelController,
didChangeTypingMembers typingMembers: Set<ChatChannelMember>
didChangeTypingUsers typingUsers: Set<ChatUser>
) {
updateNavigationTitleAndPrompt()
}
Expand Down Expand Up @@ -507,7 +507,7 @@ extension SimpleChatViewController {
func updateNavigationTitleAndPrompt() {
title = channelController.channel.flatMap { createChannelTitle(for: $0, channelController.client.currentUserId) }
navigationItem.prompt = channelController.channel.flatMap {
createTypingMemberString(for: $0) ?? createMemberInfoString(for: $0)
createTypingUserString(for: $0) ?? createMemberInfoString(for: $0)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sample/Samples/SwiftUISimpleChat/ChannelListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ struct ChannelListView: View {
private func channelDetails(for index: Int) -> Text {
let channel = self.channel(index)

if let typingMembersInfo = createTypingMemberString(for: channel) {
return Text(typingMembersInfo)
if let typingUsersInfo = createTypingUserString(for: channel) {
return Text(typingUsersInfo)
} else if let latestMessage = channel.latestMessages.first {
let author = latestMessage.author.name ?? latestMessage.author.id.description
return Text("\(author): \(latestMessage.text)")
Expand Down
2 changes: 1 addition & 1 deletion Sample/Samples/SwiftUISimpleChat/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct ChatView: View {
/// Set title to channel's name.
.navigationBarTitle(
Text(
createTypingMemberString(for: channel.channel) ??
createTypingUserString(for: channel.channel) ??
createChannelTitle(for: channel.channel, channel.controller.client.currentUserId)
),
displayMode: .inline
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public class _ChatClient<ExtraData: ExtraDataTypes> {
emitEvent: { [weak center] in center?.process($0) }
),
ChannelReadUpdaterMiddleware<ExtraData>(),
ChannelMemberTypingStateUpdaterMiddleware<ExtraData>(),
UserTypingStateUpdaterMiddleware<ExtraData>(),
MessageReactionsMiddleware<ExtraData>(),
ChannelTruncatedEventMiddleware<ExtraData>(),
MemberEventMiddleware<ExtraData>(),
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/ChatClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ class ChatClient_Tests: StressTestCase {
XCTAssert(middlewares.contains(where: { $0 is ChannelReadUpdaterMiddleware<NoExtraData> }))
// Assert `ChannelMemberTypingStateUpdaterMiddleware` exists
let typingStateUpdaterMiddlewareIndex = middlewares
.firstIndex { $0 is ChannelMemberTypingStateUpdaterMiddleware<NoExtraData> }
.firstIndex { $0 is UserTypingStateUpdaterMiddleware<NoExtraData> }
XCTAssertNotNil(typingStateUpdaterMiddlewareIndex)
// Assert `ChannelMemberTypingStateUpdaterMiddleware` goes after `TypingStartCleanupMiddleware`
XCTAssertTrue(typingStateUpdaterMiddlewareIndex! > typingStartCleanupMiddlewareIndex!)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ extension _ChatChannelController {
basePublishers.memberEvent.keepAlive(self)
}

/// A publisher emitting a new value every time typing members change.
public var typingMembersPublisher: AnyPublisher<Set<_ChatChannelMember<ExtraData.User>>, Never> {
basePublishers.typingMembers.keepAlive(self)
/// A publisher emitting a new value every time typing users change.
public var typingUsersPublisher: AnyPublisher<Set<_ChatUser<ExtraData.User>>, Never> {
basePublishers.typingUsers.keepAlive(self)
}

/// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose
Expand All @@ -51,8 +51,8 @@ extension _ChatChannelController {
/// A backing subject for `memberEventPublisher`.
let memberEvent: PassthroughSubject<MemberEvent, Never> = .init()

/// A backing subject for `typingMembersPublisher`.
let typingMembers: PassthroughSubject<Set<_ChatChannelMember<ExtraData.User>>, Never> = .init()
/// A backing subject for `typingUsersPublisher`.
let typingUsers: PassthroughSubject<Set<_ChatUser<ExtraData.User>>, Never> = .init()

init(controller: _ChatChannelController<ExtraData>) {
self.controller = controller
Expand Down Expand Up @@ -89,8 +89,8 @@ extension _ChatChannelController.BasePublishers: _ChatChannelControllerDelegate

func channelController(
_ channelController: _ChatChannelController<ExtraData>,
didChangeTypingMembers typingMembers: Set<_ChatChannelMember<ExtraData.User>>
didChangeTypingUsers typingUsers: Set<_ChatUser<ExtraData.User>>
) {
self.typingMembers.send(typingMembers)
self.typingUsers.send(typingUsers)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,48 +112,39 @@ class ChannelController_Combine_Tests: iOS13TestCase {
XCTAssertEqual(recording.output as! [TestMemberEvent], [memberEvent])
}

func test_typingMembersPublisher() {
func test_typingUsersPublisher() {
// Setup Recording publishers
var recording = Record<Set<ChatChannelMember>, Never>.Recording()
var recording = Record<Set<ChatUser>, Never>.Recording()

// Setup the chain
channelController
.typingMembersPublisher
.typingUsersPublisher
.sink(receiveValue: { recording.receive($0) })
.store(in: &cancellables)

// Keep only the weak reference to the controller. The existing publisher should keep it alive.
weak var controller: ChannelControllerMock? = channelController
channelController = nil

let typingMember = ChatChannelMember(
let typingUser = ChatUser(
id: .unique,
name: .unique,
imageURL: .unique(),
isOnline: true,
isBanned: false,
isFlaggedByCurrentUser: false,
userRole: .user,
userCreatedAt: .unique,
userUpdatedAt: .unique,
createdAt: .unique,
updatedAt: .unique,
lastActiveAt: .unique,
teams: [],
extraData: .defaultValue,
memberRole: .member,
memberCreatedAt: .unique,
memberUpdatedAt: .unique,
isInvited: false,
inviteAcceptedAt: nil,
inviteRejectedAt: nil,
isBannedFromChannel: true,
banExpiresAt: .unique,
isShadowBannedFromChannel: true
extraData: .defaultValue
)

controller?.delegateCallback {
$0.channelController(controller!, didChangeTypingMembers: [typingMember])
$0.channelController(controller!, didChangeTypingUsers: [typingUser])
}

XCTAssertEqual(recording.output, [[typingMember]])
XCTAssertEqual(recording.output, [[typingUser]])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ extension _ChatChannelController {
/// The current state of the Controller.
@Published public private(set) var state: DataController.State

/// The typing members related to the channel.
@Published public private(set) var typingMembers: Set<_ChatChannelMember<ExtraData.User>> = []
/// The typing users related to the channel.
@Published public private(set) var typingUsers: Set<_ChatUser<ExtraData.User>> = []

/// Creates a new `ObservableObject` wrapper with the provided controller instance.
init(controller: _ChatChannelController<ExtraData>) {
Expand All @@ -36,7 +36,7 @@ extension _ChatChannelController {

channel = controller.channel
messages = controller.messages
typingMembers = controller.channel?.currentlyTypingMembers ?? []
typingUsers = controller.channel?.currentlyTypingUsers ?? []
}
}
}
Expand All @@ -63,8 +63,8 @@ extension _ChatChannelController.ObservableObject: _ChatChannelControllerDelegat

public func channelController(
_ channelController: _ChatChannelController<ExtraData>,
didChangeTypingMembers typingMembers: Set<_ChatChannelMember<ExtraData.User>>
didChangeTypingUsers typingUsers: Set<_ChatUser<ExtraData.User>>
) {
self.typingMembers = typingMembers
self.typingUsers = typingUsers
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,42 +80,33 @@ class ChannelController_SwiftUI_Tests: iOS13TestCase {
AssertAsync.willBeEqual(observableObject.state, newState)
}

func test_observableObject_reactsToDelegateTypingMembersChangeCallback() {
func test_observableObject_reactsToDelegateTypingUsersChangeCallback() {
let observableObject = channelController.observableObject

let typingMember = ChatChannelMember(
let typingUser = ChatUser(
id: .unique,
name: .unique,
imageURL: .unique(),
isOnline: true,
isBanned: false,
isFlaggedByCurrentUser: false,
userRole: .user,
userCreatedAt: .unique,
userUpdatedAt: .unique,
createdAt: .unique,
updatedAt: .unique,
lastActiveAt: .unique,
teams: [],
extraData: .defaultValue,
memberRole: .member,
memberCreatedAt: .unique,
memberUpdatedAt: .unique,
isInvited: false,
inviteAcceptedAt: nil,
inviteRejectedAt: nil,
isBannedFromChannel: true,
banExpiresAt: .unique,
isShadowBannedFromChannel: true
extraData: .defaultValue
)

// Simulate typing members change
// Simulate typing users change
channelController.delegateCallback {
$0.channelController(
self.channelController,
didChangeTypingMembers: [typingMember]
didChangeTypingUsers: [typingUser]
)
}

AssertAsync.willBeEqual(observableObject.typingMembers, [typingMember])
AssertAsync.willBeEqual(observableObject.typingUsers, [typingUser])
}
}

Expand Down
Loading