Skip to content

Commit

Permalink
Merge pull request #54 from gzerad/feature/timingQuizResponses
Browse files Browse the repository at this point in the history
Timing quiz responses
  • Loading branch information
gzerad authored Jan 25, 2024
2 parents d1c62cd + b0e5e0c commit ec1649f
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class PollCreateModel: ObservableObject, Identifiable {
@Published var selectedTimerDuration: String = "5 minutes"
@Published var timerDurationOptions: [String] = ["5 minutes", "15 minutes", "30 minutes", "1 hour"]

var currentPollsSet = Set<PollListModel>()
@Published var currentPolls = [PollListModel]()

var durations = [5, 15, 30, 60]
Expand All @@ -46,6 +47,7 @@ class PollCreateModel: ObservableObject, Identifiable {
self.limitViewResultsToRoles = limitViewResultsToRoles
self.currentRole = currentRole
self.canCreatePolls = self.currentRole.permissions.pollWrite ?? false
setupObserver()
}

func timerDuration() -> Int {
Expand Down Expand Up @@ -115,20 +117,26 @@ class PollCreateModel: ObservableObject, Identifiable {
}

func refreshLocalPolls() {
let allPolls = interactivityCenter.polls.map { PollListModel(poll: $0, resultModel: self.resultModel(poll: $0), createModel: self.createModel(poll: $0)) }

currentPollsSet.formUnion(allPolls)

currentPollsSet.forEach { $0.updateValues() }

let stateOrder = [HMSPollState.started, HMSPollState.created, HMSPollState.stopped]
currentPolls = interactivityCenter.polls.sorted { left, right in
currentPolls = currentPollsSet.sorted { left, right in
if left.state != right.state {
let leftIndex = stateOrder.firstIndex(of: left.state) ?? 0
let rightIndex = stateOrder.firstIndex(of: right.state) ?? 0
return leftIndex < rightIndex
} else if left.state == .started, let leftDate = left.startedAt, let rightDate = right.startedAt {
} else if left.state == .started, let leftDate = left.poll.startedAt, let rightDate = right.poll.startedAt {
return leftDate > rightDate
} else if left.state == .stopped, let leftDate = left.stoppedAt, let rightDate = right.stoppedAt {
} else if left.state == .stopped, let leftDate = left.poll.stoppedAt, let rightDate = right.poll.stoppedAt {
return leftDate > rightDate
}

return false
}.map { PollListModel(poll: $0, resultModel: self.resultModel(poll: $0), createModel: self.createModel(poll: $0)) }
}
}

func refreshPolls() {
Expand All @@ -143,5 +151,17 @@ class PollCreateModel: ObservableObject, Identifiable {
}
}
}

func setupObserver() {
interactivityCenter.addPollUpdateListner { [weak self] updatedPoll, update in
guard let self = self else { return }
switch update {
case .started, .stopped:
self.refreshPolls()
default:
break
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,42 @@
import SwiftUI
import HMSSDK

class PollListModel: ObservableObject, Identifiable {
class PollListModel: ObservableObject, Identifiable, Hashable {
internal init(poll: HMSPoll, resultModel: PollVoteViewModel?, createModel: PollCreateModel?) {
self.resultModel = resultModel
self.createModel = createModel
self.title = poll.title
self.state = poll.state
self.poll = poll

if let startDate = poll.startedAt, poll.duration > 0 {
self.endDate = startDate.addingTimeInterval(TimeInterval(poll.duration))
}
}

var id: String {
poll.pollID
}

var title: String
var state: HMSPollState
@Published var state: HMSPollState
var poll: HMSPoll

var createModel: PollCreateModel?
var resultModel: PollVoteViewModel?
var endDate: Date?

func hash(into hasher: inout Hasher) {
poll.pollID.hash(into: &hasher)
}

func updateValues() {
self.state = poll.state
}

static func == (lhs: PollListModel, rhs: PollListModel) -> Bool {
return lhs.poll.pollID == rhs.poll.pollID
}
}

struct PollListEntryView: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@
import SwiftUI

struct ActionButtonStyle: ButtonStyle {
internal init(isWide: Bool = true) {
internal init(isWide: Bool = true, isDisabled: Bool = false) {
self.isWide = isWide
self.isDisabled = isDisabled
}

var isWide: Bool
var isDisabled: Bool

func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(HMSUIFontTheme().buttonSemibold16)
.foregroundColor(HMSUIColorTheme().onPrimaryHigh)
.foregroundColor(isDisabled ? HMSUIColorTheme().onPrimaryLow : HMSUIColorTheme().onPrimaryHigh)
.frame(maxWidth: isWide ? .infinity : nil, alignment: .center)
.padding(.vertical, 10)
.padding(.horizontal, 24)
.background(HMSUIColorTheme().primaryDefault)
.background(isDisabled ? HMSUIColorTheme().primaryDisabled : HMSUIColorTheme().primaryDefault)
.cornerRadius(8)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,13 @@ struct PollLeaderboardEntryView: View {

var body: some View {
HStack(alignment: .center, spacing: 12) {
if model.place < 4 {
Text("\(model.place)").foregroundColor(HMSUIColorTheme().onPrimaryHigh).font(HMSUIFontTheme().captionSemibold12).padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(
Rectangle()
.cornerRadius(12, corners: .allCorners)
.foregroundColor(placeColor(for: model.place))
.frame(width: 24, height: 24)
).frame(width: 24, height: 24)
} else {
Text("\(model.place)").foregroundColor(HMSUIColorTheme().onSurfaceLow).font(HMSUIFontTheme().captionSemibold12)
}
Text("\(model.place)").foregroundColor(HMSUIColorTheme().onPrimaryHigh).font(HMSUIFontTheme().captionSemibold12).padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(
Rectangle()
.cornerRadius(12, corners: .allCorners)
.foregroundColor(placeColor(for: model.place))
.frame(width: 24, height: 24)
).frame(width: 24, height: 24)

VStack(alignment: .leading) {
Text(model.name).foregroundColor(HMSUIColorTheme().onSurfaceHigh).font(HMSUIFontTheme().subtitle2Semibold14)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class PollLeaderboardViewModel: ObservableObject, Identifiable {

var items = [PollSummaryItemViewModel]()
if summary.averageTime > 0 {
let avgTime = PollSummaryItemViewModel(title: "AVG. TIME TAKEN", subtitle: "\(TimeInterval(summary.averageTime).stringTime)")
let avgTime = PollSummaryItemViewModel(title: "AVG. TIME TAKEN", subtitle: "\(TimeInterval(summary.averageTime / 1000).stringTime)")
items.append(avgTime)
}

Expand All @@ -107,7 +107,7 @@ class PollLeaderboardViewModel: ObservableObject, Identifiable {
guard let userEntry = response.entries.first(where: { $0.peer?.customerUserID == userID }) else { return }
let model = PollLeaderboardEntryViewModel(entry: userEntry, poll: poll)

let rank = PollSummaryItemViewModel(title: "YOUR RANK", subtitle: "\(model.place)/\(summary.totalPeersCount)")
let rank = PollSummaryItemViewModel(title: "YOUR RANK", subtitle: "\(model.place)")

let points = PollSummaryItemViewModel(title: "POINTS", subtitle: "\(userEntry.score)")
let row1 = PollSummaryItemRowViewModel(items: [rank, points])
Expand Down Expand Up @@ -137,7 +137,7 @@ class PollLeaderboardEntryViewModel: Identifiable {
self.name = entry.peer?.userName ?? "Unknown"
self.score = totalScore > 0 ? "\(entry.score)/\(totalScore)" : ""
self.correctAnswers = "\(entry.correctResponses)/\(totalQuestions)"
self.time = entry.duration > 0 ? TimeInterval(entry.duration).stringTime : ""
self.time = entry.duration > 0 ? TimeInterval(entry.duration / 1000).stringTime : ""
self.isNoResponse = entry.totalResponses == 0
self.hasCorrectAnswers = entry.correctResponses > 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftUI

struct PollVoteQuestionView: View {
@ObservedObject var model: PollVoteQuestionViewModel
var onVote: (() -> Void)

var body: some View {
VStack(alignment: .leading, spacing: 16) {
Expand All @@ -20,7 +21,7 @@ struct PollVoteQuestionView: View {
Spacer()
}
VStack(alignment: .leading, spacing: 16) {
if model.poll.category == .poll || model.canVote {
if model.poll.category == .poll || model.canVote || model.poll.state == .started {
ForEach(model.questionOptions) { option in
PollVoteQuestionOptionView(model: option)
}
Expand All @@ -31,23 +32,16 @@ struct PollVoteQuestionView: View {
}
}

if model.canVote {
HStack(spacing: 8) {
Spacer()

if model.canSkip {
Button {

} label: {
Text("Skip")
}.buttonStyle(ActionButtonLowEmphStyle())
}

HStack(spacing: 8) {
Spacer()
if model.canVote {
Button {
model.vote()
onVote()
} label: {
Text(model.poll.category == .poll ? "Vote" : "Answer")
}.buttonStyle(ActionButtonStyle(isWide: false))
}.buttonStyle(ActionButtonStyle(isWide: false, isDisabled: !model.answerSelected)).disabled(!model.answerSelected)
} else if model.poll.state == .started {
Text(model.poll.category == .poll ? "Voted" : "Answered").foregroundColor(HMSUIColorTheme().onSurfaceLow).font(HMSUIFontTheme().buttonSemibold16)
}
}

Expand All @@ -56,3 +50,29 @@ struct PollVoteQuestionView: View {
.stroke(model.borderColor, lineWidth: 1)).clipShape(RoundedRectangle(cornerRadius: 8))
}
}


struct PollVoteQuestionCarouselView: View {
var questions: [PollVoteQuestionViewModel]
@State var questionIndex = 0
@State var startDate = Date()

var body: some View {
let model = questions[questionIndex]

PollVoteQuestionView(model: model, onVote: {
if questionIndex + 1 < questions.count {
questionIndex += 1
}
let interval = Date().timeIntervalSince(startDate)
model.vote(duration: interval)
startDate = Date()
})
.onAppear {
questionIndex = questions.firstIndex(where: { $0.canVote == true }) ?? (questions.count - 1)
}
}
}



Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class PollVoteQuestionViewModel: ObservableObject, Identifiable {
}
}
@Published var canSkip: Bool = false
@Published var answerSelected: Bool = false
@Published var borderColor = HMSUIColorTheme().surfaceBright

var poll: HMSPoll
Expand All @@ -31,6 +32,7 @@ class PollVoteQuestionViewModel: ObservableObject, Identifiable {
}
}
var canViewResponses: Bool
var duration: TimeInterval = 0

internal init(question: HMSPollQuestion, count: Int, poll: HMSPoll, canVote: Bool, canViewResponses: Bool, onVote: @escaping ((PollVoteQuestionViewModel) -> Void)) {
self.question = question
Expand All @@ -43,7 +45,9 @@ class PollVoteQuestionViewModel: ObservableObject, Identifiable {
updateResults()
}

func vote() {
func vote(duration: TimeInterval = 0) {
self.duration = duration

for (index, option) in questionOptions.enumerated() {
if (option.selected) {
question.options?[index].voteCount += 1
Expand All @@ -57,24 +61,34 @@ class PollVoteQuestionViewModel: ObservableObject, Identifiable {
text = question.text
index = question.index
let selection: ((PollVoteQuestionOptionViewModel)->Void) = { [weak self] selectedModel in
guard selectedModel.isSingleChoice, let options = self?.questionOptions else { return }
guard let options = self?.questionOptions else { return }

var hasSelections = false
for optionModel in options {
if optionModel.selected {
hasSelections = true
}

guard selectedModel.isSingleChoice else { continue }

optionModel.selected = optionModel.option.index == selectedModel.option.index
}

self?.answerSelected = hasSelections
}

let singleChoice = question.type == .singleChoice
let selectedIndexes = canVote ? Set<Int>() : question.selectedOptionIndexes
let correctIndexes = canVote ? Set<Int>() : question.correctOptionIndexes

if poll.category == .quiz, question.voted {
if poll.category == .quiz, question.voted, poll.state == .stopped {
let correct = selectedIndexes == correctIndexes
borderColor = correct ? HMSUIColorTheme().alertSuccess : HMSUIColorTheme().alertErrorDefault
} else {
borderColor = HMSUIColorTheme().surfaceBright
}

questionOptions = question.options?.map { PollVoteQuestionOptionViewModel(option: $0, isSingleChoice: singleChoice, canVote: canVote, selected: selectedIndexes.contains($0.index), isCorrect: correctIndexes.contains($0.index), canViewResponses: canViewResponses, onSelectionChange: selection) } ?? []
questionOptions = question.options?.map { PollVoteQuestionOptionViewModel(option: $0, isSingleChoice: singleChoice, canVote: canVote, selected: selectedIndexes.contains($0.index), isCorrect: poll.state == .stopped && correctIndexes.contains($0.index), canViewResponses: canViewResponses, onSelectionChange: selection) } ?? []
}

func updateResults() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,15 @@ struct PollVoteView: View {
PollSummaryView(model: summary).padding(.bottom, 8)
Text("Questions").foregroundColor(HMSUIColorTheme().onPrimaryMedium).font(HMSUIFontTheme().subtitle2Semibold14)
}
ForEach(model.questions) { question in
PollVoteQuestionView(model: question)

if model.poll.category == .quiz, !model.questions.isEmpty, model.poll.state == .started {
PollVoteQuestionCarouselView(questions: model.questions)
} else {
ForEach(model.questions) { question in
PollVoteQuestionView(model: question) {
question.vote()
}
}
}

if model.canEndPoll && model.poll.state != .stopped {
Expand All @@ -65,7 +72,7 @@ struct PollVoteView: View {
HStack {
Spacer()
Button {} label: {
Text("View Results")
Text("View Leaderboard")
}.buttonStyle(ActionButtonStyle(isWide: false)).allowsHitTesting(false)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class PollVoteViewModel: ObservableObject, Identifiable {
@Published var isFetching = false
@Published var questions = [PollVoteQuestionViewModel]()


var id: String {
poll.pollID
}

var voteComplete: Bool {
questions.first(where: { $0.canVote == true }) == nil
}
Expand All @@ -35,7 +40,7 @@ class PollVoteViewModel: ObservableObject, Identifiable {

internal init(poll: HMSPoll, interactivityCenter: HMSInteractivityCenter, currentRole: HMSRole, peerList: [HMSPeer]) {
self.poll = poll
self.canViewResponses = poll.rolesThatCanViewResponses.isEmpty || poll.rolesThatCanViewResponses.contains(currentRole)
self.canViewResponses = (poll.rolesThatCanViewResponses.isEmpty || poll.rolesThatCanViewResponses.contains(currentRole)) && poll.category == .poll
self.state = poll.state
self.interactivityCenter = interactivityCenter
if let startDate = poll.startedAt, poll.duration > 0 {
Expand Down Expand Up @@ -174,7 +179,7 @@ class PollVoteViewModel: ObservableObject, Identifiable {
guard !selectedOptions.isEmpty else { return }

let resultBuilder = HMSPollResponseBuilder(poll: poll)
resultBuilder.addResponse(for: question.question, options: selectedOptions)
resultBuilder.addResponse(for: question.question, options: selectedOptions, duration: Int(question.duration * 1000))

interactivityCenter.add(response: resultBuilder) { _, error in
question.canVote = !question.question.voted
Expand Down

0 comments on commit ec1649f

Please sign in to comment.