From fe5cffe4d7c7d3097bd5bd928d9ec3b84280b794 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Fri, 10 Jan 2025 17:00:17 +0000 Subject: [PATCH 1/3] Fix reactions users view not paginating results --- .../Reactions/ReactionsUsersView.swift | 78 +++++++++++++++---- .../ReactionsUsersView_Tests.swift | 45 +++++++---- 2 files changed, 94 insertions(+), 29 deletions(-) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift index bbbe9976b..f665c483f 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift @@ -7,46 +7,48 @@ import SwiftUI /// View displaying users who have reacted to a message. struct ReactionsUsersView: View { - + @StateObject private var viewModel: ReactionsUsersViewModel + @Injected(\.fonts) private var fonts @Injected(\.colors) private var colors - var message: ChatMessage var maxHeight: CGFloat private static let columnCount = 4 private static let itemSize: CGFloat = 64 private let columns = Array( - repeating: - GridItem( - .adaptive(minimum: itemSize), - alignment: .top - ), + repeating: GridItem(.adaptive(minimum: itemSize), alignment: .top), count: columnCount ) + + init(message: ChatMessage, maxHeight: CGFloat) { + self.maxHeight = maxHeight + _viewModel = StateObject(wrappedValue: ReactionsUsersViewModel(message: message)) + } - private var reactions: [ChatMessageReaction] { - Array(message.latestReactions) + init(viewModel: ReactionsUsersViewModel, maxHeight: CGFloat) { + self.maxHeight = maxHeight + _viewModel = StateObject(wrappedValue: viewModel) } var body: some View { HStack { - if message.isRightAligned { + if viewModel.isRightAligned { Spacer() } VStack(alignment: .center) { - Text(L10n.Reaction.Authors.numberOfReactions(reactions.count)) + Text(L10n.Reaction.Authors.numberOfReactions(viewModel.totalReactionsCount)) .foregroundColor(Color(colors.text)) .font(fonts.title3) .fontWeight(.bold) .padding() - if reactions.count > Self.columnCount { + if viewModel.reactions.count > Self.columnCount { ScrollView { LazyVGrid(columns: columns, alignment: .center, spacing: 8) { - ForEach(reactions) { reaction in + ForEach(viewModel.reactions) { reaction in ReactionUserView( reaction: reaction, imageSize: Self.itemSize @@ -57,7 +59,7 @@ struct ReactionsUsersView: View { .frame(maxHeight: maxHeight) } else { HStack(alignment: .top, spacing: 0) { - ForEach(reactions) { reaction in + ForEach(viewModel.reactions) { reaction in ReactionUserView( reaction: reaction, imageSize: Self.itemSize @@ -70,7 +72,7 @@ struct ReactionsUsersView: View { .background(Color(colors.background)) .cornerRadius(16) - if !message.isRightAligned { + if !viewModel.isRightAligned { Spacer() } } @@ -78,6 +80,52 @@ struct ReactionsUsersView: View { } } +class ReactionsUsersViewModel: ObservableObject, ChatMessageControllerDelegate { + @Published var reactions: [ChatMessageReaction] = [] + + var totalReactionsCount: Int { + messageController?.message?.totalReactionsCount ?? 0 + } + + var isRightAligned: Bool { + messageController?.message?.isRightAligned == true + } + + private var isLoading = false + private let messageController: ChatMessageController? + + init(message: ChatMessage) { + if let cid = message.cid { + messageController = InjectedValues[\.chatClient].messageController( + cid: cid, + messageId: message.id + ) + } else { + messageController = nil + } + messageController?.delegate = self + loadMoreReactions() + } + + func loadMoreReactions() { + guard let messageController = self.messageController else { + return + } + guard !isLoading && messageController.hasLoadedAllReactions == false else { + return + } + + isLoading = true + messageController.loadNextReactions { [weak self] _ in + self?.isLoading = false + } + } + + func messageController(_ controller: ChatMessageController, didChangeReactions reactions: [ChatMessageReaction]) { + self.reactions = reactions + } +} + extension ChatMessageReaction: Identifiable { public var id: String { diff --git a/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsUsersView_Tests.swift b/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsUsersView_Tests.swift index f02eb8e2c..d48ce0aff 100644 --- a/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsUsersView_Tests.swift +++ b/StreamChatSwiftUITests/Tests/ChatChannel/ReactionsUsersView_Tests.swift @@ -23,16 +23,13 @@ class ReactionsUsersView_Tests: StreamChatTestCase { author: author, extraData: [:] ) - let message = ChatMessage.mock( - id: .unique, - cid: .unique, - text: "test", - author: .mock(id: .unique), - latestReactions: [reaction] + let mockViewModel = MockReactionUsersViewModel( + reactions: [reaction], + totalReactionsCount: 1 ) // When - let view = ReactionsUsersView(message: message, maxHeight: 140) + let view = ReactionsUsersView(viewModel: mockViewModel, maxHeight: 140) .frame(width: 250) // Then @@ -56,19 +53,39 @@ class ReactionsUsersView_Tests: StreamChatTestCase { reactions.insert(reaction) } - let message = ChatMessage.mock( - id: .unique, - cid: .unique, - text: "test", - author: .mock(id: .unique), - latestReactions: reactions + let mockViewModel = MockReactionUsersViewModel( + reactions: Array(reactions), + totalReactionsCount: 8 ) // When - let view = ReactionsUsersView(message: message, maxHeight: 280) + let view = ReactionsUsersView(viewModel: mockViewModel, maxHeight: 280) .frame(width: defaultScreenSize.width, height: 320) // Then assertSnapshot(matching: view, as: .image(perceptualPrecision: precision)) } } + +class MockReactionUsersViewModel: ReactionsUsersViewModel { + init( + reactions: [ChatMessageReaction] = [], + totalReactionsCount: Int = 0, + isRightAligned: Bool = false + ) { + super.init(message: .mock()) + self.reactions = reactions + mockedIsRightAligned = isRightAligned + mockedTotalReactionsCount = totalReactionsCount + } + + var mockedTotalReactionsCount: Int = 0 + override var totalReactionsCount: Int { + mockedTotalReactionsCount + } + + var mockedIsRightAligned: Bool = false + override var isRightAligned: Bool { + mockedIsRightAligned + } +} From d83eaaca36d202007bb4ba47776b4cfa7a46da49 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Fri, 10 Jan 2025 17:40:16 +0000 Subject: [PATCH 2/3] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc7a6167..48fa18239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Use bright color for typing indicator animation in dark mode [#702](https://github.com/GetStream/stream-chat-swiftui/pull/702) - Refresh quoted message preview when the quoted message is deleted [#705](https://github.com/GetStream/stream-chat-swiftui/pull/705) - Fix composer command view not Themable [#710](https://github.com/GetStream/stream-chat-swiftui/pull/710) +- Fix reactions users view not paginating results [#712](https://github.com/GetStream/stream-chat-swiftui/pull/712) # [4.69.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.69.0) _December 18, 2024_ From e46eadd5712a8da4055d427f252c5bb7406fe002 Mon Sep 17 00:00:00 2001 From: Nuno Vieira Date: Mon, 13 Jan 2025 15:55:03 +0000 Subject: [PATCH 3/3] Create seperate file for the view model --- .../Reactions/ReactionsUsersView.swift | 46 ---------------- .../Reactions/ReactionsUsersViewModel.swift | 52 +++++++++++++++++++ StreamChatSwiftUI.xcodeproj/project.pbxproj | 4 ++ 3 files changed, 56 insertions(+), 46 deletions(-) create mode 100644 Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersViewModel.swift diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift index f665c483f..538e27b96 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift @@ -80,52 +80,6 @@ struct ReactionsUsersView: View { } } -class ReactionsUsersViewModel: ObservableObject, ChatMessageControllerDelegate { - @Published var reactions: [ChatMessageReaction] = [] - - var totalReactionsCount: Int { - messageController?.message?.totalReactionsCount ?? 0 - } - - var isRightAligned: Bool { - messageController?.message?.isRightAligned == true - } - - private var isLoading = false - private let messageController: ChatMessageController? - - init(message: ChatMessage) { - if let cid = message.cid { - messageController = InjectedValues[\.chatClient].messageController( - cid: cid, - messageId: message.id - ) - } else { - messageController = nil - } - messageController?.delegate = self - loadMoreReactions() - } - - func loadMoreReactions() { - guard let messageController = self.messageController else { - return - } - guard !isLoading && messageController.hasLoadedAllReactions == false else { - return - } - - isLoading = true - messageController.loadNextReactions { [weak self] _ in - self?.isLoading = false - } - } - - func messageController(_ controller: ChatMessageController, didChangeReactions reactions: [ChatMessageReaction]) { - self.reactions = reactions - } -} - extension ChatMessageReaction: Identifiable { public var id: String { diff --git a/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersViewModel.swift new file mode 100644 index 000000000..ae9674e9b --- /dev/null +++ b/Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersViewModel.swift @@ -0,0 +1,52 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import StreamChat +import SwiftUI + +class ReactionsUsersViewModel: ObservableObject, ChatMessageControllerDelegate { + @Published var reactions: [ChatMessageReaction] = [] + + var totalReactionsCount: Int { + messageController?.message?.totalReactionsCount ?? 0 + } + + var isRightAligned: Bool { + messageController?.message?.isRightAligned == true + } + + private var isLoading = false + private let messageController: ChatMessageController? + + init(message: ChatMessage) { + if let cid = message.cid { + messageController = InjectedValues[\.chatClient].messageController( + cid: cid, + messageId: message.id + ) + } else { + messageController = nil + } + messageController?.delegate = self + loadMoreReactions() + } + + func loadMoreReactions() { + guard let messageController = self.messageController else { + return + } + guard !isLoading && messageController.hasLoadedAllReactions == false else { + return + } + + isLoading = true + messageController.loadNextReactions { [weak self] _ in + self?.isLoading = false + } + } + + func messageController(_ controller: ChatMessageController, didChangeReactions reactions: [ChatMessageReaction]) { + self.reactions = reactions + } +} diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index aa9ba665c..fab5d0b38 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -515,6 +515,7 @@ AD3AB65C2CB730090014D4D7 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65B2CB730090014D4D7 /* Shimmer.swift */; }; AD3AB65E2CB731360014D4D7 /* ChatThreadListLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */; }; AD3AB6602CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */; }; + AD6B7E052D356E8800ADEF39 /* ReactionsUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */; }; ADE0F55E2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */; }; ADE0F5602CB846EC0053B8B9 /* FloatingBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */; }; ADE0F5622CB8556F0053B8B9 /* ChatThreadListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */; }; @@ -1107,6 +1108,7 @@ AD3AB65B2CB730090014D4D7 /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = ""; }; AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListLoadingView.swift; sourceTree = ""; }; AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListHeaderViewModifier.swift; sourceTree = ""; }; + AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsUsersViewModel.swift; sourceTree = ""; }; ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListErrorBannerView.swift; sourceTree = ""; }; ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingBannerViewModifier.swift; sourceTree = ""; }; ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListFooterView.swift; sourceTree = ""; }; @@ -1824,6 +1826,7 @@ 8465FD212746A95600AF091E /* ReactionsOverlayViewModel.swift */, 8465FD252746A95600AF091E /* ReactionsHelperViews.swift */, 84E6EC26279B0C930017207B /* ReactionsUsersView.swift */, + AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */, 846D6563279FF0800094B36E /* ReactionUserView.swift */, ); path = Reactions; @@ -2717,6 +2720,7 @@ 842383E427678A4D00888CFC /* QuotedMessageView.swift in Sources */, 84289BE328071C7200282ABE /* ChatChannelInfoViewModel.swift in Sources */, 4F6D83512C1079A00098C298 /* AlertBannerViewModifier.swift in Sources */, + AD6B7E052D356E8800ADEF39 /* ReactionsUsersViewModel.swift in Sources */, 8465FD932746A95700AF091E /* PhotoAttachmentPickerView.swift in Sources */, 841B64C82774BA770016FF3B /* CommandsHandler.swift in Sources */, 8465FDC42746A95700AF091E /* ChatChannelListScreen.swift in Sources */,