From 647fe05dbce210d7e6406144d9bafe65a9eaa0dc Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Tue, 28 May 2024 11:15:24 +0300 Subject: [PATCH] Use green color for highest vote and add tests --- .../Polls/PollAttachmentView.swift | 4 +- .../Polls/PollAttachmentViewModel.swift | 27 +++-- StreamChatSwiftUI.xcodeproj/project.pbxproj | 22 +++- .../Infrastructure/Mocks/Poll_Mock.swift | 108 ++++++++++++++++++ .../Polls/PollAttachmentViewModel_Tests.swift | 64 +++++++++++ 5 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 StreamChatSwiftUITests/Tests/ChatChannel/MessageList/Polls/PollAttachmentViewModel_Tests.swift diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift index d4f9a2d9..6da9debb 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentView.swift @@ -237,6 +237,7 @@ struct PollOptionView: View { if let maxVotes { PollVotesIndicatorView( + mostVotes: viewModel.hasMostVotes(for: option), optionVotes: optionVotes ?? 0, maxVotes: maxVotes ) @@ -250,6 +251,7 @@ struct PollVotesIndicatorView: View { @Injected(\.colors) var colors + let mostVotes: Bool var optionVotes: Int var maxVotes: Int @@ -263,7 +265,7 @@ struct PollVotesIndicatorView: View { .frame(width: reader.size.width, height: height) RoundedRectangle(cornerRadius: 8) - .fill(colors.tintColor) + .fill(mostVotes ? Color(colors.alternativeActiveTint) : colors.tintColor) .frame(width: reader.size.width * ratio, height: height) } .frame(height: height) diff --git a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentViewModel.swift b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentViewModel.swift index de634a81..04be14e0 100644 --- a/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentViewModel.swift +++ b/Sources/StreamChatSwiftUI/ChatChannel/MessageList/Polls/PollAttachmentViewModel.swift @@ -33,7 +33,7 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate { private let createdByCurrentUser: Bool var showEndVoteButton: Bool { - //TODO: check why createdBy is set to nil. + // TODO: check why createdBy is set to nil. !poll.isClosed && createdByCurrentUser } @@ -48,18 +48,29 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate { init(message: ChatMessage, poll: Poll) { self.message = message self.poll = poll - self.createdByCurrentUser = poll.createdBy?.id == InjectedValues[\.chatClient].currentUserId - self.pollController = InjectedValues[\.chatClient].pollController( + createdByCurrentUser = poll.createdBy?.id == InjectedValues[\.chatClient].currentUserId + pollController = InjectedValues[\.chatClient].pollController( messageId: message.id, pollId: poll.id ) pollController.delegate = self - pollController.synchronize { [weak self] error in + pollController.synchronize { [weak self] _ in guard let self else { return } self.currentUserVotes = Array(self.pollController.ownVotes) } } + /// Returns true if the specified option has more votes than any other option. + func hasMostVotes(for option: PollOption) -> Bool { + guard let allCounts = poll.voteCountsByOption else { return false } + guard let optionVoteCount = allCounts[option.id], optionVoteCount > 0 else { return false } + guard let highestVotePerOption = allCounts.values.max() else { return false } + guard optionVoteCount == highestVotePerOption else { return false } + // Check if only one option has highest number for votes + let optionsByVoteCounts = Dictionary(grouping: allCounts, by: { $0.value }) + return optionsByVoteCounts[optionVoteCount]?.count == 1 + } + func castPollVote(for option: PollOption) { pollController.castPollVote( answerText: nil, @@ -74,7 +85,7 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate { func add(comment: String) { pollController.castPollVote(answerText: comment, optionId: nil) { [weak self] error in DispatchQueue.main.async { - self?.commentText = "" + self?.commentText = "" } if let error { log.error("Error casting a vote \(error.localizedDescription)") @@ -102,7 +113,7 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate { } func optionVotedByCurrentUser(_ option: PollOption) -> Bool { - return currentUserVote(for: option) != nil + currentUserVote(for: option) != nil } func suggest(option: String) { @@ -113,7 +124,7 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate { } } - //MARK: - PollControllerDelegate + // MARK: - PollControllerDelegate func pollController(_ pollController: PollController, didUpdatePoll poll: EntityChange) { self.poll = poll.item @@ -123,7 +134,7 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate { _ pollController: PollController, didUpdateCurrentUserVotes votes: [ListChange] ) { - self.currentUserVotes = Array(pollController.ownVotes) + currentUserVotes = Array(pollController.ownVotes) } // MARK: - private diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index b8c7fd48..de5e4d5a 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 402C54482B6AAC0100672BFB /* StreamChatSwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8465FBB52746873A00AF091E /* StreamChatSwiftUI.framework */; }; 402C54492B6AAC0100672BFB /* StreamChatSwiftUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8465FBB52746873A00AF091E /* StreamChatSwiftUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F198FDD2C0480EC00148F49 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */; }; + 4F198FE12C05BABF00148F49 /* PollAttachmentViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F198FE02C05BABF00148F49 /* PollAttachmentViewModel_Tests.swift */; }; 4F7DD9A02BFC7C6100599AA6 /* ChatClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */; }; 4F7DD9A22BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */; }; 4FEAB3182BFF71F70057E511 /* SwiftUI+UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEAB3172BFF71F70057E511 /* SwiftUI+UIAlertController.swift */; }; @@ -565,6 +566,7 @@ /* Begin PBXFileReference section */ 4A65451E274BA170003C5FA8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = ""; }; + 4F198FE02C05BABF00148F49 /* PollAttachmentViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollAttachmentViewModel_Tests.swift; sourceTree = ""; }; 4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatClient+Extensions.swift"; sourceTree = ""; }; 4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatClientExtensions_Tests.swift; sourceTree = ""; }; 4FEAB3172BFF71F70057E511 /* SwiftUI+UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+UIAlertController.swift"; sourceTree = ""; }; @@ -1107,6 +1109,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4F198FDE2C05BA7D00148F49 /* MessageList */ = { + isa = PBXGroup; + children = ( + 4F198FDF2C05BA8C00148F49 /* Polls */, + ); + path = MessageList; + sourceTree = ""; + }; + 4F198FDF2C05BA8C00148F49 /* Polls */ = { + isa = PBXGroup; + children = ( + 4F198FE02C05BABF00148F49 /* PollAttachmentViewModel_Tests.swift */, + ); + path = Polls; + sourceTree = ""; + }; 82A1814528FD69E8005F9D43 /* Message Delivery Status */ = { isa = PBXGroup; children = ( @@ -2000,6 +2018,7 @@ 84C94D472758BDB2007FE2B9 /* ChatChannel */ = { isa = PBXGroup; children = ( + 4F198FDE2C05BA7D00148F49 /* MessageList */, 846B15F22817E7440017F7A1 /* ChannelInfo */, 8423C340277CB5C70092DCF1 /* Suggestions */, 84C94D482758BE1C007FE2B9 /* ChatChannelViewModel_Tests.swift */, @@ -2035,8 +2054,8 @@ 84E1D8252976B3F100060491 /* MessageViewMultiRowReactions_Tests.swift */, 84AA6EA92987EE51005732EF /* MessageListViewNewMessages_Tests.swift */, 8469592E29BB235400134EA0 /* LazyImageExtensions_Tests.swift */, - 84779C742AEBBACD000A6A68 /* BottomReactionsView_Tests.swift */, 84204BF92C04D55300AE522B /* PollAttachmentView_Tests.swift */, + 84779C742AEBBACD000A6A68 /* BottomReactionsView_Tests.swift */, 84204BFD2C052BD600AE522B /* CreatePollView_Tests.swift */, ); path = ChatChannel; @@ -2805,6 +2824,7 @@ 842F036D288E93BF00496D49 /* ChatMessage_AdjustedText_Tests.swift in Sources */, 84507C98281AC40F0081DDC2 /* AddUsersViewModel_Tests.swift in Sources */, 84C94D1727578BF3007FE2B9 /* XCTAssertEqual+Difference.swift in Sources */, + 4F198FE12C05BABF00148F49 /* PollAttachmentViewModel_Tests.swift in Sources */, 84B2B5CC28195C9300479CEE /* PinnedMessagesViewModel_Tests.swift in Sources */, 84E1D8262976B3F100060491 /* MessageViewMultiRowReactions_Tests.swift in Sources */, 84782785284A4DB500D2EE11 /* ChatClient_Mock.swift in Sources */, diff --git a/StreamChatSwiftUITests/Infrastructure/Mocks/Poll_Mock.swift b/StreamChatSwiftUITests/Infrastructure/Mocks/Poll_Mock.swift index d7639c3c..4935bcf0 100644 --- a/StreamChatSwiftUITests/Infrastructure/Mocks/Poll_Mock.swift +++ b/StreamChatSwiftUITests/Infrastructure/Mocks/Poll_Mock.swift @@ -54,4 +54,112 @@ extension Poll { ) return poll } + + static func mock( + pollId: String = .unique, + allowAnswers: Bool = true, + allowUserSuggestedOptions: Bool = true, + enforceUniqueVote: Bool = false, + isClosed: Bool = false, + options: [PollOption] = [] + ) -> Poll { + let voteCountsByOption = Dictionary(grouping: options, by: { $0.id }) + .mapValues { options in + options + .map(\.latestVotes.count) + .reduce(0, +) + } + return Poll( + allowAnswers: allowAnswers, + allowUserSuggestedOptions: allowUserSuggestedOptions, + answersCount: allowAnswers ? 1 : 0, + createdAt: Date(), + pollDescription: "Test", + enforceUniqueVote: enforceUniqueVote, + id: pollId, + name: "Test poll", + updatedAt: Date(), + voteCount: voteCountsByOption.values.reduce(0, +), + extraData: [:], + voteCountsByOption: voteCountsByOption, + isClosed: isClosed, + maxVotesAllowed: nil, + votingVisibility: .public, + createdBy: .mock(id: "test", name: "test"), + latestAnswers: [], + options: options, + latestVotesByOption: options + ) + } +} + +extension PollOption { + static func mock( + id: String = .unique, + text: String = .unique, + latestVotes: [PollVote] = [] + ) -> PollOption { + PollOption( + id: id, + text: text, + latestVotes: latestVotes, + extraData: nil + ) + } +} + +extension PollVote { + static func mock( + id: String = .unique, + createdAt: Date = .unique, + updatedAt: Date = .unique, + pollId: String, + optionId: String?, + isAnswer: Bool = false, + answerText: String? = nil, + user: ChatUser? = nil + ) -> PollVote { + PollVote( + id: .unique, + createdAt: Date(), + updatedAt: Date(), + pollId: pollId, + optionId: optionId, + isAnswer: isAnswer, + answerText: answerText, + user: user + ) + } +} + +extension Poll { + static func mock(optionCount: Int, voteCountForOption: (Int) -> Int) -> Poll { + let pollId = String.unique + let options = (0..