Skip to content

Commit 4f2f073

Browse files
Implemented read indicators
2 parents 8a1dc23 + 7ad5ba6 commit 4f2f073

16 files changed

+335
-81
lines changed

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageContainerView.swift

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct MessageContainerView<Factory: ViewFactory>: View {
1212
@Injected(\.fonts) private var fonts
1313
@Injected(\.colors) private var colors
1414
@Injected(\.images) private var images
15+
@Injected(\.chatClient) private var chatClient
1516

1617
var factory: Factory
1718
let channel: ChatChannel
@@ -137,7 +138,15 @@ struct MessageContainerView<Factory: ViewFactory>: View {
137138
}
138139

139140
if showsAllInfo && !message.isDeleted {
140-
if isInGroup && !message.isSentByCurrentUser {
141+
if message.isSentByCurrentUser {
142+
HStack(spacing: 4) {
143+
factory.makeMessageReadIndicatorView(
144+
channel: channel,
145+
message: message
146+
)
147+
MessageDateView(message: message)
148+
}
149+
} else if isInGroup {
141150
MessageAuthorAndDateView(message: message)
142151
} else {
143152
MessageDateView(message: message)
@@ -199,51 +208,6 @@ struct MessageContainerView<Factory: ViewFactory>: View {
199208
}
200209
}
201210

202-
struct MessageAuthorAndDateView: View {
203-
@Injected(\.fonts) private var fonts
204-
@Injected(\.colors) private var colors
205-
206-
var message: ChatMessage
207-
208-
var body: some View {
209-
HStack {
210-
Text(message.author.name ?? "")
211-
.font(fonts.footnoteBold)
212-
.foregroundColor(Color(colors.textLowEmphasis))
213-
MessageDateView(message: message)
214-
Spacer()
215-
}
216-
}
217-
}
218-
219-
struct MessageDateView: View {
220-
@Injected(\.utils) private var utils
221-
@Injected(\.fonts) private var fonts
222-
@Injected(\.colors) private var colors
223-
224-
private var dateFormatter: DateFormatter {
225-
utils.dateFormatter
226-
}
227-
228-
var message: ChatMessage
229-
230-
var body: some View {
231-
Text(dateFormatter.string(from: message.createdAt))
232-
.font(fonts.footnote)
233-
.foregroundColor(Color(colors.textLowEmphasis))
234-
}
235-
}
236-
237-
struct MessageSpacer: View {
238-
var spacerWidth: CGFloat?
239-
240-
var body: some View {
241-
Spacer()
242-
.frame(minWidth: spacerWidth)
243-
.layoutPriority(-1)
244-
}
245-
}
246-
247211
public struct MessageDisplayInfo {
248212
let message: ChatMessage
249213
let frame: CGRect
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// Copyright © 2022 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
import SwiftUI
7+
8+
struct MessageAuthorAndDateView: View {
9+
@Injected(\.fonts) private var fonts
10+
@Injected(\.colors) private var colors
11+
12+
var message: ChatMessage
13+
14+
var body: some View {
15+
HStack {
16+
Text(message.author.name ?? "")
17+
.font(fonts.footnoteBold)
18+
.foregroundColor(Color(colors.textLowEmphasis))
19+
MessageDateView(message: message)
20+
Spacer()
21+
}
22+
}
23+
}
24+
25+
struct MessageDateView: View {
26+
@Injected(\.utils) private var utils
27+
@Injected(\.fonts) private var fonts
28+
@Injected(\.colors) private var colors
29+
30+
private var dateFormatter: DateFormatter {
31+
utils.dateFormatter
32+
}
33+
34+
var message: ChatMessage
35+
36+
var body: some View {
37+
Text(dateFormatter.string(from: message.createdAt))
38+
.font(fonts.footnote)
39+
.foregroundColor(Color(colors.textLowEmphasis))
40+
}
41+
}
42+
43+
struct MessageReadIndicatorView: View {
44+
@Injected(\.images) private var images
45+
@Injected(\.fonts) private var fonts
46+
@Injected(\.colors) private var colors
47+
48+
var readUsers: [ChatUser]
49+
var showReadCount: Bool
50+
51+
var body: some View {
52+
HStack(spacing: 2) {
53+
if showReadCount && !readUsers.isEmpty {
54+
Text("\(readUsers.count)")
55+
.font(fonts.footnoteBold)
56+
.foregroundColor(colors.tintColor)
57+
}
58+
Image(
59+
uiImage: !readUsers.isEmpty ? images.readByAll : images.messageSent
60+
)
61+
.customizable()
62+
.foregroundColor(!readUsers.isEmpty ? colors.tintColor : Color(colors.textLowEmphasis))
63+
.frame(height: 16)
64+
}
65+
}
66+
}
67+
68+
struct MessageSpacer: View {
69+
var spacerWidth: CGFloat?
70+
71+
var body: some View {
72+
Spacer()
73+
.frame(minWidth: spacerWidth)
74+
.layoutPriority(-1)
75+
}
76+
}

Sources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelExtensions.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import StreamChat
77

88
extension ChatChannel {
99

10+
/// Returns the online info text for a channel.
11+
/// - Parameters:
12+
/// - currentUserId: the id of the current user.
13+
/// - Returns: the online info text string.
1014
func onlineInfoText(currentUserId: String) -> String {
1115
if isDirectMessageChannel {
1216
guard let member = lastActiveMembers
@@ -28,12 +32,20 @@ extension ChatChannel {
2832
return L10n.Message.Title.group(memberCount, watcherCount)
2933
}
3034

35+
/// Returns the currently typing users, without the current user.
36+
/// - Parameters:
37+
/// - currentUserId: the id of the current user.
38+
/// - Returns: Array of users that are currently typing.
3139
func currentlyTypingUsersFiltered(currentUserId: UserId?) -> [ChatUser] {
3240
currentlyTypingUsers.filter { user in
3341
user.id != currentUserId
3442
}
3543
}
3644

45+
/// Returns the typing indicator string.
46+
/// - Parameters:
47+
/// - currentUserId: the id of the current user.
48+
/// - Returns: the typing indicator string.
3749
func typingIndicatorString(currentUserId: UserId?) -> String {
3850
let typingUsers = currentlyTypingUsersFiltered(currentUserId: currentUserId)
3951
if let user = typingUsers.first(where: { user in user.name != nil }), let name = user.name {
@@ -44,6 +56,22 @@ extension ChatChannel {
4456
}
4557
}
4658

59+
/// Returns users that have read the channel's latest message.
60+
/// - Parameters:
61+
/// - currentUserId: the id of the current user.
62+
/// - message: the current message.
63+
/// - Returns: The list of users that read the channel.
64+
public func readUsers(currentUserId: UserId?, message: ChatMessage?) -> [ChatUser] {
65+
guard let message = message else {
66+
return []
67+
}
68+
let readUsers = reads.filter {
69+
$0.lastReadAt > message.createdAt &&
70+
$0.user.id != currentUserId
71+
}.map(\.user)
72+
return readUsers
73+
}
74+
4775
private var lastSeenDateFormatter: (Date) -> String? {
4876
DateUtils.timeAgo
4977
}

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelList.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ extension ChatChannel: Identifiable {
9393
}
9494

9595
public var id: String {
96-
"\(cid.id)-\(lastMessageAt ?? createdAt)-\(lastActiveMembersCount)-\(mutedString)-\(unreadCount.messages)-\(typingUsersString)"
96+
"\(cid.id)-\(lastMessageAt ?? createdAt)-\(lastActiveMembersCount)-\(mutedString)-\(unreadCount.messages)-\(typingUsersString)-\(readUsersId)"
97+
}
98+
99+
private var readUsersId: String {
100+
"\(readUsers(currentUserId: nil, message: latestMessages.first).count)"
97101
}
98102

99103
private var lastActiveMembersCount: Int {

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,45 +33,39 @@ public struct ChatChannelListItem: View {
3333
)
3434

3535
VStack(alignment: .leading, spacing: 4) {
36-
Text(channelName)
37-
.lineLimit(1)
38-
.font(fonts.bodyBold)
39-
.foregroundColor(Color(colors.text))
40-
if let image = image {
41-
HStack(spacing: 4) {
42-
Image(uiImage: image)
43-
.customizable()
44-
.frame(maxHeight: 12)
45-
.foregroundColor(Color(colors.subtitleText))
46-
SubtitleText(text: subtitleText)
47-
Spacer()
36+
HStack {
37+
titleView
38+
39+
Spacer()
40+
41+
if channel.unreadCount != .noUnread {
42+
UnreadIndicatorView(
43+
unreadCount: channel.unreadCount.messages
44+
)
4845
}
49-
} else {
46+
}
47+
48+
HStack {
49+
subtitleView
50+
51+
Spacer()
52+
5053
HStack(spacing: 4) {
51-
if !channel.currentlyTypingUsersFiltered(
52-
currentUserId: chatClient.currentUserId
53-
).isEmpty {
54-
TypingIndicatorView()
54+
if let message = channel.latestMessages.first,
55+
message.isSentByCurrentUser,
56+
!message.isDeleted {
57+
MessageReadIndicatorView(
58+
readUsers: channel.readUsers(
59+
currentUserId: chatClient.currentUserId,
60+
message: channel.latestMessages.first
61+
),
62+
showReadCount: false
63+
)
5564
}
56-
SubtitleText(text: subtitleText)
57-
Spacer()
65+
SubtitleText(text: timestampText)
5866
}
5967
}
6068
}
61-
62-
Spacer()
63-
64-
VStack(alignment: .trailing, spacing: 4) {
65-
if channel.unreadCount == .noUnread {
66-
Spacer()
67-
} else {
68-
UnreadIndicatorView(
69-
unreadCount: channel.unreadCount.messages
70-
)
71-
}
72-
73-
SubtitleText(text: timestampText)
74-
}
7569
}
7670
.padding(.all, 8)
7771
}
@@ -81,6 +75,32 @@ public struct ChatChannelListItem: View {
8175
.id("\(channel.id)-base")
8276
}
8377

78+
private var titleView: some View {
79+
Text(channelName)
80+
.lineLimit(1)
81+
.font(fonts.bodyBold)
82+
.foregroundColor(Color(colors.text))
83+
}
84+
85+
private var subtitleView: some View {
86+
HStack(spacing: 4) {
87+
if let image = image {
88+
Image(uiImage: image)
89+
.customizable()
90+
.frame(maxHeight: 12)
91+
.foregroundColor(Color(colors.subtitleText))
92+
} else {
93+
if !channel.currentlyTypingUsersFiltered(
94+
currentUserId: chatClient.currentUserId
95+
).isEmpty {
96+
TypingIndicatorView()
97+
}
98+
}
99+
SubtitleText(text: subtitleText)
100+
Spacer()
101+
}
102+
}
103+
84104
private var subtitleText: String {
85105
if channel.isMuted {
86106
return L10n.Channel.Item.muted

Sources/StreamChatSwiftUI/DefaultViewFactory.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,21 @@ extension ViewFactory {
553553
handleCommand: handleCommand
554554
)
555555
}
556+
557+
public func makeMessageReadIndicatorView(
558+
channel: ChatChannel,
559+
message: ChatMessage
560+
) -> some View {
561+
let readUsers = channel.readUsers(
562+
currentUserId: chatClient.currentUserId,
563+
message: message
564+
)
565+
let showReadCount = channel.memberCount > 2
566+
return MessageReadIndicatorView(
567+
readUsers: readUsers,
568+
showReadCount: showReadCount
569+
)
570+
}
556571
}
557572

558573
/// Default class conforming to `ViewFactory`, used throughout the SDK.

Sources/StreamChatSwiftUI/ViewFactory.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,4 +541,15 @@ public protocol ViewFactory: AnyObject {
541541
suggestions: [String: Any],
542542
handleCommand: @escaping ([String: Any]) -> Void
543543
) -> CommandsContainerViewType
544+
545+
associatedtype MessageReadIndicatorViewType: View
546+
/// Creates the message read indicator view.
547+
/// - Parameters:
548+
/// - channel: the channel where the message was sent.
549+
/// - message: the sent message.
550+
/// - Returns: view shown in the message read indicator slot.
551+
func makeMessageReadIndicatorView(
552+
channel: ChatChannel,
553+
message: ChatMessage
554+
) -> MessageReadIndicatorViewType
544555
}

0 commit comments

Comments
 (0)