Skip to content

Commit

Permalink
Use green color for highest vote and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
laevandus committed May 28, 2024
1 parent 2be40ee commit 647fe05
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ struct PollOptionView: View {

if let maxVotes {
PollVotesIndicatorView(
mostVotes: viewModel.hasMostVotes(for: option),
optionVotes: optionVotes ?? 0,
maxVotes: maxVotes
)
Expand All @@ -250,6 +251,7 @@ struct PollVotesIndicatorView: View {

@Injected(\.colors) var colors

let mostVotes: Bool
var optionVotes: Int
var maxVotes: Int

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

Expand All @@ -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,
Expand All @@ -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)")
Expand Down Expand Up @@ -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) {
Expand All @@ -113,7 +124,7 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate {
}
}

//MARK: - PollControllerDelegate
// MARK: - PollControllerDelegate

func pollController(_ pollController: PollController, didUpdatePoll poll: EntityChange<Poll>) {
self.poll = poll.item
Expand All @@ -123,7 +134,7 @@ class PollAttachmentViewModel: ObservableObject, PollControllerDelegate {
_ pollController: PollController,
didUpdateCurrentUserVotes votes: [ListChange<PollVote>]
) {
self.currentUserVotes = Array(pollController.ownVotes)
currentUserVotes = Array(pollController.ownVotes)
}

// MARK: - private
Expand Down
22 changes: 21 additions & 1 deletion StreamChatSwiftUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -565,6 +566,7 @@
/* Begin PBXFileReference section */
4A65451E274BA170003C5FA8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
4F198FE02C05BABF00148F49 /* PollAttachmentViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollAttachmentViewModel_Tests.swift; sourceTree = "<group>"; };
4F7DD99F2BFC7C6100599AA6 /* ChatClient+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatClient+Extensions.swift"; sourceTree = "<group>"; };
4F7DD9A12BFCB2EF00599AA6 /* ChatClientExtensions_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatClientExtensions_Tests.swift; sourceTree = "<group>"; };
4FEAB3172BFF71F70057E511 /* SwiftUI+UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+UIAlertController.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1107,6 +1109,22 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
4F198FDE2C05BA7D00148F49 /* MessageList */ = {
isa = PBXGroup;
children = (
4F198FDF2C05BA8C00148F49 /* Polls */,
);
path = MessageList;
sourceTree = "<group>";
};
4F198FDF2C05BA8C00148F49 /* Polls */ = {
isa = PBXGroup;
children = (
4F198FE02C05BABF00148F49 /* PollAttachmentViewModel_Tests.swift */,
);
path = Polls;
sourceTree = "<group>";
};
82A1814528FD69E8005F9D43 /* Message Delivery Status */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2000,6 +2018,7 @@
84C94D472758BDB2007FE2B9 /* ChatChannel */ = {
isa = PBXGroup;
children = (
4F198FDE2C05BA7D00148F49 /* MessageList */,
846B15F22817E7440017F7A1 /* ChannelInfo */,
8423C340277CB5C70092DCF1 /* Suggestions */,
84C94D482758BE1C007FE2B9 /* ChatChannelViewModel_Tests.swift */,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */,
Expand Down
108 changes: 108 additions & 0 deletions StreamChatSwiftUITests/Infrastructure/Mocks/Poll_Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<optionCount)
.map { optionIndex in
let optionId = String(format: "option_%03d", optionIndex)
let votes = (0..<voteCountForOption(optionIndex))
.map { voteIndex in
PollVote.mock(
id: String(format: "vote_%03d", voteIndex),
createdAt: Date(timeIntervalSinceReferenceDate: TimeInterval(voteIndex)),
updatedAt: Date(timeIntervalSinceReferenceDate: TimeInterval(voteIndex) + 0.5),
pollId: pollId,
optionId: optionId,
isAnswer: false,
answerText: nil,
user: nil
)
}
return PollOption.mock(
id: optionId,
text: String(format: "option_text_%03d", optionIndex),
latestVotes: votes
)
}
return Poll.mock(
pollId: pollId,
options: options
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

@testable import StreamChat
@testable import StreamChatSwiftUI
import SwiftUI
import XCTest

final class PollAttachmentViewModel_Tests: StreamChatTestCase {
func test_hasMostVotes_whenWinningVote() {
// Given
let poll = Poll.mock(optionCount: 3, voteCountForOption: { optionIndex in
switch optionIndex {
case 0: return 2
case 1: return 3
case 2: return 1
default: return 0
}
})
let message = ChatMessage.mock(
id: .unique,
cid: .unique,
text: "",
author: .mock(id: .unique),
poll: poll
)

// When
let viewModel = PollAttachmentViewModel(message: message, poll: poll)

// Then
XCTAssertEqual(viewModel.hasMostVotes(for: poll.options[0]), false)
XCTAssertEqual(viewModel.hasMostVotes(for: poll.options[1]), true)
XCTAssertEqual(viewModel.hasMostVotes(for: poll.options[2]), false)
}

func test_hasMostVotes_whenEqualHighestVotes() {
// Given
let poll = Poll.mock(optionCount: 3, voteCountForOption: { optionIndex in
switch optionIndex {
case 0: return 2
case 1: return 3
case 2: return 3
default: return 0
}
})
let message = ChatMessage.mock(
id: .unique,
cid: .unique,
text: "",
author: .mock(id: .unique),
poll: poll
)

// When
let viewModel = PollAttachmentViewModel(message: message, poll: poll)

// Then
XCTAssertEqual(false, viewModel.hasMostVotes(for: poll.options[0]))
XCTAssertEqual(false, viewModel.hasMostVotes(for: poll.options[1]))
XCTAssertEqual(false, viewModel.hasMostVotes(for: poll.options[2]))
}
}

0 comments on commit 647fe05

Please sign in to comment.