Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -70,14 +72,60 @@ struct ReactionsUsersView: View {
.background(Color(colors.background))
.cornerRadius(16)

if !message.isRightAligned {
if !viewModel.isRightAligned {
Spacer()
}
}
.accessibilityIdentifier("ReactionsUsersView")
}
}

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Loading