Skip to content

Commit 23fce24

Browse files
Mark unread and jump to unread (#406)
1 parent 2f3fa25 commit 23fce24

26 files changed

+311
-45
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6-
### 🔄 Changed
6+
### ✅ Added
7+
- Mark message as unread
8+
- Jump to first unread message
9+
- Factory method to swap jump to unread button
710

811
# [4.44.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.44.0)
912
_December 01, 2023_

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ protocol ChannelDataSource: AnyObject {
4141

4242
/// Determines whether all new messages have been fetched.
4343
var hasLoadedAllNextMessages: Bool { get }
44+
45+
/// Returns the first unread message id.
46+
var firstUnreadMessageId: String? { get }
4447

4548
/// Loads the previous messages.
4649
/// - Parameters:
@@ -89,6 +92,10 @@ class ChatChannelDataSource: ChannelDataSource, ChatChannelControllerDelegate {
8992
var hasLoadedAllNextMessages: Bool {
9093
controller.hasLoadedAllNextMessages
9194
}
95+
96+
var firstUnreadMessageId: String? {
97+
controller.firstUnreadMessageId
98+
}
9299

93100
init(controller: ChatChannelController) {
94101
self.controller = controller
@@ -160,6 +167,10 @@ class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate
160167
var hasLoadedAllNextMessages: Bool {
161168
messageController.hasLoadedAllNextReplies
162169
}
170+
171+
var firstUnreadMessageId: String? {
172+
channelController.firstUnreadMessageId
173+
}
163174

164175
init(
165176
channelController: ChatChannelController,

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public struct ChatChannelView<Factory: ViewFactory>: View, KeyboardReadable {
5757
shouldShowTypingIndicator: viewModel.shouldShowTypingIndicator,
5858
scrollPosition: $viewModel.scrollPosition,
5959
loadingNextMessages: viewModel.loadingNextMessages,
60+
firstUnreadMessageId: $viewModel.firstUnreadMessageId,
6061
onMessageAppear: viewModel.handleMessageAppear(index:scrollDirection:),
6162
onScrollToBottom: viewModel.scrollToLastMessage,
6263
onLongPress: { displayInfo in

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
1717
private var cancellables = Set<AnyCancellable>()
1818
private var lastRefreshThreshold = 200
1919
private let refreshThreshold = 200
20-
private let newerMessagesLimit: Int = {
20+
private static let newerMessagesLimit: Int = {
2121
if #available(iOS 17, *) {
2222
// On iOS 17 we can maintain the scroll position.
2323
return 25
@@ -35,6 +35,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
3535

3636
private var isActive = true
3737
private var readsString = ""
38+
private var canMarkRead = true
3839

3940
private let messageListDateOverlay: DateFormatter = DateFormatter.messageListDateOverlay
4041

@@ -47,6 +48,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
4748
private var disableDateIndicator = false
4849
private var channelName = ""
4950
private var onlineIndicatorShown = false
51+
private var lastReadMessageId: String?
5052
private let throttler = Throttler(interval: 3, broadcastLatestEvent: true)
5153

5254
public var channelController: ChatChannelController
@@ -108,6 +110,13 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
108110
@Published public var shouldShowTypingIndicator = false
109111
@Published public var scrollPosition: String?
110112
@Published public private(set) var loadingNextMessages: Bool = false
113+
@Published public var firstUnreadMessageId: String? {
114+
didSet {
115+
if oldValue != nil && firstUnreadMessageId == nil && (channel?.unreadCount.messages ?? 0) > 0 {
116+
channelController.markRead()
117+
}
118+
}
119+
}
111120

112121
public var channel: ChatChannel? {
113122
channelController.channel
@@ -179,6 +188,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
179188

180189
channelName = channel?.name ?? ""
181190
checkHeaderType()
191+
checkUnreadCount()
182192
}
183193

184194
@objc
@@ -198,7 +208,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
198208
@objc
199209
private func applicationWillEnterForeground() {
200210
guard let first = messages.first else { return }
201-
maybeSendReadEvent(for: first)
211+
if canMarkRead {
212+
sendReadEventIfNeeded(for: first)
213+
}
202214
}
203215

204216
public func scrollToLastMessage() {
@@ -212,6 +224,24 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
212224
}
213225

214226
public func jumpToMessage(messageId: String) -> Bool {
227+
if messageId == .unknownMessageId {
228+
if firstUnreadMessageId == nil, let lastReadMessageId {
229+
channelDataSource.loadPageAroundMessageId(lastReadMessageId) { [weak self] error in
230+
if error != nil {
231+
log.error("Error loading messages around message \(messageId)")
232+
return
233+
}
234+
if let firstUnread = self?.channelDataSource.firstUnreadMessageId,
235+
let message = self?.channelController.dataStore.message(id: firstUnread) {
236+
self?.firstUnreadMessageId = message.messageId
237+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
238+
self?.scrolledId = message.messageId
239+
}
240+
}
241+
}
242+
}
243+
return false
244+
}
215245
if messageId == messages.first?.messageId {
216246
scrolledId = nil
217247
return true
@@ -221,9 +251,12 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
221251
return true
222252
}
223253
let alreadyLoaded = messages.map(\.id).contains(baseId)
224-
if alreadyLoaded {
225-
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
226-
self.scrolledId = nil
254+
if alreadyLoaded && baseId != messageId {
255+
if scrolledId == nil {
256+
scrolledId = messageId
257+
}
258+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
259+
self?.scrolledId = nil
227260
}
228261
return true
229262
} else {
@@ -246,8 +279,12 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
246279
log.error("Error loading messages around message \(messageId)")
247280
return
248281
}
282+
var toJumpId = messageId
283+
if toJumpId == baseId, let message = self?.channelController.dataStore.message(id: toJumpId) {
284+
toJumpId = message.messageId
285+
}
249286
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
250-
self?.scrolledId = messageId
287+
self?.scrolledId = toJumpId
251288
self?.loadingMessagesAround = false
252289
}
253290
}
@@ -267,13 +304,16 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
267304
} else {
268305
checkForNewerMessages(index: index)
269306
}
307+
if let firstUnreadMessageId, firstUnreadMessageId.contains(message.id) {
308+
canMarkRead = true
309+
}
270310
if utils.messageListConfig.dateIndicatorPlacement == .overlay {
271311
save(lastDate: message.createdAt)
272312
}
273313
if index == 0 {
274314
let isActive = UIApplication.shared.applicationState == .active
275-
if isActive {
276-
maybeSendReadEvent(for: message)
315+
if isActive && canMarkRead {
316+
sendReadEventIfNeeded(for: message)
277317
}
278318
}
279319
}
@@ -350,11 +390,15 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
350390
}
351391
}
352392

353-
maybeRefreshMessageList()
393+
refreshMessageListIfNeeded()
354394

355395
if !showScrollToLatestButton && scrolledId == nil && !loadingNextMessages {
356396
updateScrolledIdToNewestMessage()
357397
}
398+
399+
if lastReadMessageId != nil && firstUnreadMessageId == nil {
400+
firstUnreadMessageId = channelDataSource.firstUnreadMessageId
401+
}
358402
}
359403

360404
func dataSource(
@@ -382,6 +426,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
382426
public func onViewAppear() {
383427
setActive()
384428
messages = channelDataSource.messages
429+
firstUnreadMessageId = channelDataSource.firstUnreadMessageId
385430
checkNameChange()
386431
}
387432

@@ -427,7 +472,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
427472
scrollPosition = messages[index].messageId
428473
}
429474

430-
channelDataSource.loadNextMessages(limit: newerMessagesLimit) { [weak self] _ in
475+
channelDataSource.loadNextMessages(limit: Self.newerMessagesLimit) { [weak self] _ in
431476
guard let self = self else { return }
432477
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
433478
self.loadingNextMessages = false
@@ -452,16 +497,19 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
452497
)
453498
}
454499

455-
private func maybeSendReadEvent(for message: ChatMessage) {
500+
private func sendReadEventIfNeeded(for message: ChatMessage) {
456501
if message.id != lastMessageRead {
457502
lastMessageRead = message.id
458503
throttler.throttle { [weak self] in
459504
self?.channelController.markRead()
505+
DispatchQueue.main.async {
506+
self?.firstUnreadMessageId = nil
507+
}
460508
}
461509
}
462510
}
463511

464-
private func maybeRefreshMessageList() {
512+
private func refreshMessageListIfNeeded() {
465513
let count = messages.count
466514
if count > lastRefreshThreshold {
467515
lastRefreshThreshold = lastRefreshThreshold + refreshThreshold
@@ -552,6 +600,19 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
552600
}
553601
}
554602

603+
private func checkUnreadCount() {
604+
guard !isMessageThread else { return }
605+
if channelController.channel?.unreadCount.messages ?? 0 > 0 {
606+
if channelController.firstUnreadMessageId != nil {
607+
firstUnreadMessageId = channelController.firstUnreadMessageId
608+
canMarkRead = false
609+
} else if channelController.lastReadMessageId != nil {
610+
lastReadMessageId = channelController.lastReadMessageId
611+
canMarkRead = false
612+
}
613+
}
614+
}
615+
555616
private func handleDateChange() {
556617
guard showScrollToLatestButton == true, let currentDate = currentDate else {
557618
currentDateString = nil
@@ -630,6 +691,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
630691
if messageController == nil {
631692
utils.channelControllerFactory.clearCurrentController()
632693
ImageCache.shared.trim(toCost: utils.messageListConfig.cacheSizeOnChatDismiss)
694+
if !channelDataSource.hasLoadedAllNextMessages {
695+
channelDataSource.loadFirstPage { _ in }
696+
}
633697
}
634698
}
635699
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Copyright © 2023 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
import SwiftUI
7+
8+
struct JumpToUnreadButton: View {
9+
10+
@Injected(\.colors) var colors
11+
12+
var unreadCount: Int
13+
var onTap: () -> Void
14+
var onClose: () -> Void
15+
16+
var body: some View {
17+
HStack {
18+
Button {
19+
onTap()
20+
} label: {
21+
Text(L10n.Message.Unread.count(unreadCount))
22+
.font(.caption)
23+
}
24+
Button {
25+
onClose()
26+
} label: {
27+
Image(systemName: "xmark")
28+
.font(.caption.weight(.bold))
29+
}
30+
}
31+
.padding(.all, 10)
32+
.foregroundColor(.white)
33+
.background(Color(colors.textLowEmphasis))
34+
.cornerRadius(16)
35+
}
36+
}

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public struct MessageListConfig {
2424
cacheSizeOnChatDismiss: Int = 1024 * 1024 * 100,
2525
iPadSplitViewEnabled: Bool = true,
2626
scrollingAnchor: UnitPoint = .bottom,
27-
showNewMessagesSeparator: Bool = false,
27+
showNewMessagesSeparator: Bool = true,
2828
handleTabBarVisibility: Bool = true,
2929
messageListAlignment: MessageListAlignment = .standard
3030
) {

0 commit comments

Comments
 (0)