diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift index c0841664..b3f43877 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelDataSource.swift @@ -41,6 +41,8 @@ protocol ChannelDataSource: AnyObject { /// Determines whether all new messages have been fetched. var hasLoadedAllNextMessages: Bool { get } + + var firstUnreadMessageId: String? { get } /// Loads the previous messages. /// - Parameters: @@ -89,6 +91,10 @@ class ChatChannelDataSource: ChannelDataSource, ChatChannelControllerDelegate { var hasLoadedAllNextMessages: Bool { controller.hasLoadedAllNextMessages } + + var firstUnreadMessageId: String? { + controller.firstUnreadMessageId + } init(controller: ChatChannelController) { self.controller = controller @@ -160,6 +166,10 @@ class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate var hasLoadedAllNextMessages: Bool { messageController.hasLoadedAllNextReplies } + + var firstUnreadMessageId: String? { + channelController.firstUnreadMessageId + } init( channelController: ChatChannelController, diff --git a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift index 3adeb0ca..46e8044d 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift @@ -188,15 +188,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { channelName = channel?.name ?? "" checkHeaderType() - if channelController.channel?.unreadCount.messages ?? 0 > 0 { - if channelController.firstUnreadMessageId != nil { - firstUnreadMessageId = channelController.firstUnreadMessageId - canMarkRead = false - } else if channelController.lastReadMessageId != nil { - lastReadMessageId = channelController.lastReadMessageId - canMarkRead = false - } - } + checkUnreadCount() } @objc @@ -216,7 +208,9 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { @objc private func applicationWillEnterForeground() { guard let first = messages.first else { return } - maybeSendReadEvent(for: first) + if canMarkRead { + maybeSendReadEvent(for: first) + } } public func scrollToLastMessage() { @@ -237,8 +231,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { log.error("Error loading messages around message \(messageId)") return } - //TODO: change to data source. - if let firstUnread = self?.channelController.firstUnreadMessageId, + if let firstUnread = self?.channelDataSource.firstUnreadMessageId, let message = self?.channelController.dataStore.message(id: firstUnread) { self?.firstUnreadMessageId = message.messageId DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -404,8 +397,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } if lastMessageRead != nil && firstUnreadMessageId == nil { - //TODO: data source - self.firstUnreadMessageId = channelController.firstUnreadMessageId + self.firstUnreadMessageId = channelDataSource.firstUnreadMessageId } } @@ -434,6 +426,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { public func onViewAppear() { setActive() messages = channelDataSource.messages + firstUnreadMessageId = channelDataSource.firstUnreadMessageId checkNameChange() } @@ -607,6 +600,19 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource { } } + private func checkUnreadCount() { + guard !isMessageThread else { return } + if channelController.channel?.unreadCount.messages ?? 0 > 0 { + if channelController.firstUnreadMessageId != nil { + firstUnreadMessageId = channelController.firstUnreadMessageId + canMarkRead = false + } else if channelController.lastReadMessageId != nil { + lastReadMessageId = channelController.lastReadMessageId + canMarkRead = false + } + } + } + private func handleDateChange() { guard showScrollToLatestButton == true, let currentDate = currentDate else { currentDateString = nil diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/JumpToUnreadButton.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/JumpToUnreadButton.swift index cf6c03e6..c3a070c8 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/JumpToUnreadButton.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/JumpToUnreadButton.swift @@ -5,9 +5,10 @@ import StreamChat import SwiftUI -//TODO: improve this struct JumpToUnreadButton: View { + @Injected(\.colors) var colors + var unreadCount: Int var onTap: () -> () var onClose: () -> () @@ -17,18 +18,19 @@ struct JumpToUnreadButton: View { Button { onTap() } label: { - Text("\(unreadCount) unread ") + Text(L10n.Message.Unread.count(unreadCount)) .font(.caption) } Button { onClose() } label: { Image(systemName: "xmark") + .font(.caption.weight(.bold)) } } - .padding() + .padding(.all, 10) .foregroundColor(.white) - .background(Color.gray) + .background(Color(colors.textLowEmphasis)) .cornerRadius(16) } } diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift index d8c62f45..0d628172 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift @@ -289,15 +289,16 @@ public struct MessageListView: View, KeyboardReadable { } }) .overlay( - (channel.unreadCount.messages > 0 && !unreadMessagesBannerShown) ? VStack { - JumpToUnreadButton(unreadCount: channel.unreadCount.messages) { + (channel.unreadCount.messages > 0 && !unreadMessagesBannerShown) ? + factory.makeJumpToUnreadButton( + channel: channel, + onJumpToMessage: { _ = onJumpToMessage?(firstUnreadMessageId ?? "unknown") - } onClose: { + }, + onClose: { firstUnreadMessageId = nil } - - Spacer() - } : nil + ) : nil ) .modifier(factory.makeMessageListContainerModifier()) .modifier(HideKeyboardOnTapGesture(shouldAdd: keyboardShown)) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift index 2a049619..f3617e35 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/MessageActions/DefaultMessageActions.swift @@ -128,15 +128,17 @@ extension MessageAction { messageActions.append(deleteAction) } else { - let markUnreadAction = markAsUnreadAction( - for: message, - channel: channel, - chatClient: chatClient, - onFinish: onFinish, - onError: onError - ) - - messageActions.append(markUnreadAction) + if !message.isPartOfThread { + let markUnreadAction = markAsUnreadAction( + for: message, + channel: channel, + chatClient: chatClient, + onFinish: onFinish, + onError: onError + ) + + messageActions.append(markUnreadAction) + } let flagAction = flagMessageAction( for: message, diff --git a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift index 2dbd1f40..a9cbebdd 100644 --- a/Sources/StreamChatSwiftUI/DefaultViewFactory.swift +++ b/Sources/StreamChatSwiftUI/DefaultViewFactory.swift @@ -876,6 +876,23 @@ extension ViewFactory { count: count ) } + + public func makeJumpToUnreadButton( + channel: ChatChannel, + onJumpToMessage: @escaping () -> (), + onClose: @escaping () -> () + ) -> some View { + VStack { + JumpToUnreadButton( + unreadCount: channel.unreadCount.messages, + onTap: onJumpToMessage, + onClose: onClose + ) + .padding(.all, 8) + + Spacer() + } + } } /// Default class conforming to `ViewFactory`, used throughout the SDK. diff --git a/Sources/StreamChatSwiftUI/Generated/L10n.swift b/Sources/StreamChatSwiftUI/Generated/L10n.swift index da42f2a1..5d57494d 100644 --- a/Sources/StreamChatSwiftUI/Generated/L10n.swift +++ b/Sources/StreamChatSwiftUI/Generated/L10n.swift @@ -366,6 +366,12 @@ internal enum L10n { /// Online internal static var online: String { L10n.tr("Localizable", "message.title.online") } } + internal enum Unread { + /// Plural format key: "%#@unread@" + internal static func count(_ p1: Int) -> String { + return L10n.tr("Localizable", "message.unread.count", p1) + } + } } internal enum MessageList { diff --git a/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.stringsdict b/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.stringsdict index 1e438017..9ba6adad 100644 --- a/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.stringsdict +++ b/Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.stringsdict @@ -20,6 +20,24 @@ %d Message Reactions + message.unread.count + + NSStringLocalizedFormatKey + %#@unread@ + unread + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + %d unread + one + %d unread + other + %d unread + + message.search.number-of-results NSStringLocalizedFormatKey diff --git a/Sources/StreamChatSwiftUI/ViewFactory.swift b/Sources/StreamChatSwiftUI/ViewFactory.swift index 3a348088..a2b57c7b 100644 --- a/Sources/StreamChatSwiftUI/ViewFactory.swift +++ b/Sources/StreamChatSwiftUI/ViewFactory.swift @@ -870,4 +870,17 @@ public protocol ViewFactory: AnyObject { newMessagesStartId: Binding, count: Int ) -> NewMessagesIndicatorViewType + + associatedtype JumpToUnreadButtonType: View + /// Creates a jump to unread button. + /// - Parameters: + /// - channel: the current channel. + /// - onJumpToMessage: called when jump to message is tapped. + /// - onClose: called when the jump to unread should be dismissed. + /// - Returns: view shown in the jump to unread slot. + func makeJumpToUnreadButton( + channel: ChatChannel, + onJumpToMessage: @escaping () -> (), + onClose: @escaping () -> () + ) -> JumpToUnreadButtonType }