Skip to content

Commit f566ec1

Browse files
authored
Fix reactions users view not paginating results (#712)
* Fix reactions users view not paginating results * Update CHANGELOG.md * Create seperate file for the view model
1 parent ff23f48 commit f566ec1

File tree

5 files changed

+105
-29
lines changed

5 files changed

+105
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99
- Use bright color for typing indicator animation in dark mode [#702](https://github.com/GetStream/stream-chat-swiftui/pull/702)
1010
- Refresh quoted message preview when the quoted message is deleted [#705](https://github.com/GetStream/stream-chat-swiftui/pull/705)
1111
- Fix composer command view not Themable [#710](https://github.com/GetStream/stream-chat-swiftui/pull/710)
12+
- Fix reactions users view not paginating results [#712](https://github.com/GetStream/stream-chat-swiftui/pull/712)
1213

1314
# [4.69.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.69.0)
1415
_December 18, 2024_

Sources/StreamChatSwiftUI/ChatChannel/Reactions/ReactionsUsersView.swift

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,48 @@ import SwiftUI
77

88
/// View displaying users who have reacted to a message.
99
struct ReactionsUsersView: View {
10-
10+
@StateObject private var viewModel: ReactionsUsersViewModel
11+
1112
@Injected(\.fonts) private var fonts
1213
@Injected(\.colors) private var colors
1314

14-
var message: ChatMessage
1515
var maxHeight: CGFloat
1616

1717
private static let columnCount = 4
1818
private static let itemSize: CGFloat = 64
1919

2020
private let columns = Array(
21-
repeating:
22-
GridItem(
23-
.adaptive(minimum: itemSize),
24-
alignment: .top
25-
),
21+
repeating: GridItem(.adaptive(minimum: itemSize), alignment: .top),
2622
count: columnCount
2723
)
24+
25+
init(message: ChatMessage, maxHeight: CGFloat) {
26+
self.maxHeight = maxHeight
27+
_viewModel = StateObject(wrappedValue: ReactionsUsersViewModel(message: message))
28+
}
2829

29-
private var reactions: [ChatMessageReaction] {
30-
Array(message.latestReactions)
30+
init(viewModel: ReactionsUsersViewModel, maxHeight: CGFloat) {
31+
self.maxHeight = maxHeight
32+
_viewModel = StateObject(wrappedValue: viewModel)
3133
}
3234

3335
var body: some View {
3436
HStack {
35-
if message.isRightAligned {
37+
if viewModel.isRightAligned {
3638
Spacer()
3739
}
3840

3941
VStack(alignment: .center) {
40-
Text(L10n.Reaction.Authors.numberOfReactions(reactions.count))
42+
Text(L10n.Reaction.Authors.numberOfReactions(viewModel.totalReactionsCount))
4143
.foregroundColor(Color(colors.text))
4244
.font(fonts.title3)
4345
.fontWeight(.bold)
4446
.padding()
4547

46-
if reactions.count > Self.columnCount {
48+
if viewModel.reactions.count > Self.columnCount {
4749
ScrollView {
4850
LazyVGrid(columns: columns, alignment: .center, spacing: 8) {
49-
ForEach(reactions) { reaction in
51+
ForEach(viewModel.reactions) { reaction in
5052
ReactionUserView(
5153
reaction: reaction,
5254
imageSize: Self.itemSize
@@ -57,7 +59,7 @@ struct ReactionsUsersView: View {
5759
.frame(maxHeight: maxHeight)
5860
} else {
5961
HStack(alignment: .top, spacing: 0) {
60-
ForEach(reactions) { reaction in
62+
ForEach(viewModel.reactions) { reaction in
6163
ReactionUserView(
6264
reaction: reaction,
6365
imageSize: Self.itemSize
@@ -70,7 +72,7 @@ struct ReactionsUsersView: View {
7072
.background(Color(colors.background))
7173
.cornerRadius(16)
7274

73-
if !message.isRightAligned {
75+
if !viewModel.isRightAligned {
7476
Spacer()
7577
}
7678
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
import SwiftUI
7+
8+
class ReactionsUsersViewModel: ObservableObject, ChatMessageControllerDelegate {
9+
@Published var reactions: [ChatMessageReaction] = []
10+
11+
var totalReactionsCount: Int {
12+
messageController?.message?.totalReactionsCount ?? 0
13+
}
14+
15+
var isRightAligned: Bool {
16+
messageController?.message?.isRightAligned == true
17+
}
18+
19+
private var isLoading = false
20+
private let messageController: ChatMessageController?
21+
22+
init(message: ChatMessage) {
23+
if let cid = message.cid {
24+
messageController = InjectedValues[\.chatClient].messageController(
25+
cid: cid,
26+
messageId: message.id
27+
)
28+
} else {
29+
messageController = nil
30+
}
31+
messageController?.delegate = self
32+
loadMoreReactions()
33+
}
34+
35+
func loadMoreReactions() {
36+
guard let messageController = self.messageController else {
37+
return
38+
}
39+
guard !isLoading && messageController.hasLoadedAllReactions == false else {
40+
return
41+
}
42+
43+
isLoading = true
44+
messageController.loadNextReactions { [weak self] _ in
45+
self?.isLoading = false
46+
}
47+
}
48+
49+
func messageController(_ controller: ChatMessageController, didChangeReactions reactions: [ChatMessageReaction]) {
50+
self.reactions = reactions
51+
}
52+
}

StreamChatSwiftUI.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@
515515
AD3AB65C2CB730090014D4D7 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65B2CB730090014D4D7 /* Shimmer.swift */; };
516516
AD3AB65E2CB731360014D4D7 /* ChatThreadListLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */; };
517517
AD3AB6602CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */; };
518+
AD6B7E052D356E8800ADEF39 /* ReactionsUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */; };
518519
ADE0F55E2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */; };
519520
ADE0F5602CB846EC0053B8B9 /* FloatingBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */; };
520521
ADE0F5622CB8556F0053B8B9 /* ChatThreadListFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */; };
@@ -1107,6 +1108,7 @@
11071108
AD3AB65B2CB730090014D4D7 /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = "<group>"; };
11081109
AD3AB65D2CB731360014D4D7 /* ChatThreadListLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListLoadingView.swift; sourceTree = "<group>"; };
11091110
AD3AB65F2CB7403C0014D4D7 /* ChatThreadListHeaderViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListHeaderViewModifier.swift; sourceTree = "<group>"; };
1111+
AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsUsersViewModel.swift; sourceTree = "<group>"; };
11101112
ADE0F55D2CB838420053B8B9 /* ChatThreadListErrorBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListErrorBannerView.swift; sourceTree = "<group>"; };
11111113
ADE0F55F2CB846EC0053B8B9 /* FloatingBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingBannerViewModifier.swift; sourceTree = "<group>"; };
11121114
ADE0F5612CB8556F0053B8B9 /* ChatThreadListFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThreadListFooterView.swift; sourceTree = "<group>"; };
@@ -1824,6 +1826,7 @@
18241826
8465FD212746A95600AF091E /* ReactionsOverlayViewModel.swift */,
18251827
8465FD252746A95600AF091E /* ReactionsHelperViews.swift */,
18261828
84E6EC26279B0C930017207B /* ReactionsUsersView.swift */,
1829+
AD6B7E042D356E8800ADEF39 /* ReactionsUsersViewModel.swift */,
18271830
846D6563279FF0800094B36E /* ReactionUserView.swift */,
18281831
);
18291832
path = Reactions;
@@ -2717,6 +2720,7 @@
27172720
842383E427678A4D00888CFC /* QuotedMessageView.swift in Sources */,
27182721
84289BE328071C7200282ABE /* ChatChannelInfoViewModel.swift in Sources */,
27192722
4F6D83512C1079A00098C298 /* AlertBannerViewModifier.swift in Sources */,
2723+
AD6B7E052D356E8800ADEF39 /* ReactionsUsersViewModel.swift in Sources */,
27202724
8465FD932746A95700AF091E /* PhotoAttachmentPickerView.swift in Sources */,
27212725
841B64C82774BA770016FF3B /* CommandsHandler.swift in Sources */,
27222726
8465FDC42746A95700AF091E /* ChatChannelListScreen.swift in Sources */,

StreamChatSwiftUITests/Tests/ChatChannel/ReactionsUsersView_Tests.swift

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,13 @@ class ReactionsUsersView_Tests: StreamChatTestCase {
2323
author: author,
2424
extraData: [:]
2525
)
26-
let message = ChatMessage.mock(
27-
id: .unique,
28-
cid: .unique,
29-
text: "test",
30-
author: .mock(id: .unique),
31-
latestReactions: [reaction]
26+
let mockViewModel = MockReactionUsersViewModel(
27+
reactions: [reaction],
28+
totalReactionsCount: 1
3229
)
3330

3431
// When
35-
let view = ReactionsUsersView(message: message, maxHeight: 140)
32+
let view = ReactionsUsersView(viewModel: mockViewModel, maxHeight: 140)
3633
.frame(width: 250)
3734

3835
// Then
@@ -56,19 +53,39 @@ class ReactionsUsersView_Tests: StreamChatTestCase {
5653
reactions.insert(reaction)
5754
}
5855

59-
let message = ChatMessage.mock(
60-
id: .unique,
61-
cid: .unique,
62-
text: "test",
63-
author: .mock(id: .unique),
64-
latestReactions: reactions
56+
let mockViewModel = MockReactionUsersViewModel(
57+
reactions: Array(reactions),
58+
totalReactionsCount: 8
6559
)
6660

6761
// When
68-
let view = ReactionsUsersView(message: message, maxHeight: 280)
62+
let view = ReactionsUsersView(viewModel: mockViewModel, maxHeight: 280)
6963
.frame(width: defaultScreenSize.width, height: 320)
7064

7165
// Then
7266
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
7367
}
7468
}
69+
70+
class MockReactionUsersViewModel: ReactionsUsersViewModel {
71+
init(
72+
reactions: [ChatMessageReaction] = [],
73+
totalReactionsCount: Int = 0,
74+
isRightAligned: Bool = false
75+
) {
76+
super.init(message: .mock())
77+
self.reactions = reactions
78+
mockedIsRightAligned = isRightAligned
79+
mockedTotalReactionsCount = totalReactionsCount
80+
}
81+
82+
var mockedTotalReactionsCount: Int = 0
83+
override var totalReactionsCount: Int {
84+
mockedTotalReactionsCount
85+
}
86+
87+
var mockedIsRightAligned: Bool = false
88+
override var isRightAligned: Bool {
89+
mockedIsRightAligned
90+
}
91+
}

0 commit comments

Comments
 (0)