Skip to content

Commit

Permalink
#206 Add polls and predictions (#604)
Browse files Browse the repository at this point in the history
* #206 Add polls and predictions: 1st iteration, only showing

* #206 Add polls and predictions: 1st iteration, only showing

* #206 Add polls and predictions: 1st iteration, only showing

* #206 Add polls and predictions: 1st iteration, only showing

* updates

---------

Co-authored-by: crackededed <[email protected]>
  • Loading branch information
devalexx and crackededed authored Jan 18, 2025
1 parent 899efeb commit 91aeacc
Show file tree
Hide file tree
Showing 23 changed files with 770 additions and 0 deletions.
15 changes: 15 additions & 0 deletions app/src/main/java/com/github/andreyasadchy/xtra/model/chat/Poll.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.andreyasadchy.xtra.model.chat

class Poll(
val id: String?,
val title: String?,
val status: String?,
val choices: List<PollChoice>?,
val totalVotes: Int?,
val remainingMilliseconds: Int?,
) {
class PollChoice(
val title: String?,
val totalVotes: Int?,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.github.andreyasadchy.xtra.model.chat

class Prediction(
val id: String?,
val createdAt: Long?,
val outcomes: List<PredictionOutcome>?,
val predictionWindowSeconds: Int?,
val status: String?,
val title: String?,
val winningOutcomeId: String?,
) {
class PredictionOutcome(
val id: String?,
val title: String?,
val totalPoints: Int?,
val totalUsers: Int?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,26 @@ class ChatFragment : BaseNetworkFragment(), LifecycleListener, MessageClickedDia
override fun onRaidClose() {
viewModel.raidClosed = true
}

override fun onPollClose(timeout: Boolean) {
viewModel.pollSecondsLeft.value = null
viewModel.pollTimer?.cancel()
if (timeout) {
viewModel.startPollTimeout { chatView.hidePoll(true) }
} else {
viewModel.pollClosed = true
}
}

override fun onPredictionClose(timeout: Boolean) {
viewModel.predictionSecondsLeft.value = null
viewModel.predictionTimer?.cancel()
if (timeout) {
viewModel.startPredictionTimeout { chatView.hidePrediction(true) }
} else {
viewModel.predictionClosed = true
}
}
})
if (isLoggedIn) {
chatView.setUsername(accountLogin)
Expand Down Expand Up @@ -297,6 +317,74 @@ class ChatFragment : BaseNetworkFragment(), LifecycleListener, MessageClickedDia
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.hidePoll.collectLatest {
if (it) {
chatView.hidePoll()
viewModel.hidePoll.value = false
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.poll.collectLatest {
if (it != null) {
if (!viewModel.pollClosed) {
chatView.notifyPoll(it)
}
viewModel.poll.value = null
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.pollSecondsLeft.collectLatest {
if (it != null) {
chatView.updatePollStatus(it)
if (it <= 0) {
viewModel.pollSecondsLeft.value = null
}
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.hidePrediction.collectLatest {
if (it) {
chatView.hidePrediction()
viewModel.hidePrediction.value = false
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.prediction.collectLatest {
if (it != null) {
if (!viewModel.predictionClosed) {
chatView.notifyPrediction(it)
}
viewModel.prediction.value = null
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.predictionSecondsLeft.collectLatest {
if (it != null) {
chatView.updatePredictionStatus(it)
if (it <= 0) {
viewModel.predictionSecondsLeft.value = null
}
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.playbackMessage.collectLatest {
Expand Down
149 changes: 149 additions & 0 deletions app/src/main/java/com/github/andreyasadchy/xtra/ui/chat/ChatView.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.andreyasadchy.xtra.ui.chat

import android.content.Context
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
Expand Down Expand Up @@ -32,6 +33,8 @@ import com.github.andreyasadchy.xtra.model.chat.Chatter
import com.github.andreyasadchy.xtra.model.chat.CheerEmote
import com.github.andreyasadchy.xtra.model.chat.Emote
import com.github.andreyasadchy.xtra.model.chat.NamePaint
import com.github.andreyasadchy.xtra.model.chat.Poll
import com.github.andreyasadchy.xtra.model.chat.Prediction
import com.github.andreyasadchy.xtra.model.chat.Raid
import com.github.andreyasadchy.xtra.model.chat.RoomState
import com.github.andreyasadchy.xtra.model.chat.StvBadge
Expand All @@ -54,13 +57,16 @@ import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.extensions.LayoutContainer
import java.util.regex.Pattern
import kotlin.math.max
import kotlin.math.roundToInt

class ChatView : ConstraintLayout {

interface ChatViewCallback {
fun send(message: CharSequence, replyId: String?)
fun onRaidClicked(raid: Raid)
fun onRaidClose()
fun onPollClose(timeout: Boolean = false)
fun onPredictionClose(timeout: Boolean = false)
}

private var _binding: ViewChatBinding? = null
Expand Down Expand Up @@ -345,6 +351,149 @@ class ChatView : ConstraintLayout {
callback?.onRaidClose()
}

fun notifyPoll(poll: Poll) {
with(binding) {
when (poll.status) {
"ACTIVE" -> {
pollLayout.visible()
pollTitle.text = context.getString(R.string.poll_title, poll.title)
pollChoices.text = poll.choices?.map {
context.getString(
R.string.poll_choice,
(((it.totalVotes ?: 0).toLong() * 100.0) / max((poll.totalVotes ?: 0), 1)).roundToInt(),
it.totalVotes,
it.title
)
}?.joinToString("\n")
pollStatus.visible()
pollClose.setOnClickListener {
hidePoll()
}
}
"COMPLETED", "TERMINATED" -> {
pollLayout.visible()
pollTitle.text = context.getString(R.string.poll_title, poll.title)
val winningTotal = poll.choices?.maxOfOrNull { it.totalVotes ?: 0 } ?: 0
pollChoices.text = poll.choices?.map {
context.getString(
if (winningTotal == it.totalVotes) {
R.string.poll_choice_winner
} else {
R.string.poll_choice
},
(((it.totalVotes ?: 0).toLong() * 100.0) / max((poll.totalVotes ?: 0), 1)).roundToInt(),
it.totalVotes,
it.title
)
}?.joinToString("\n")
pollStatus.gone()
pollClose.setOnClickListener {
hidePoll()
}
callback?.onPollClose(true)
}
else -> hidePoll()
}
}
}

fun updatePollStatus(secondsLeft: Int) {
binding.pollStatus.text = context.getString(R.string.remaining_time, DateUtils.formatElapsedTime(secondsLeft.toLong()))
}

fun hidePoll(timeout: Boolean = false) {
binding.pollLayout.gone()
if (!timeout) {
callback?.onPollClose()
}
}

fun notifyPrediction(prediction: Prediction) {
with(binding) {
when (prediction.status) {
"ACTIVE" -> {
predictionLayout.visible()
predictionTitle.text = context.getString(R.string.prediction_title, prediction.title)
val totalPoints = prediction.outcomes?.sumOf { it.totalPoints?.toLong() ?: 0 } ?: 0
predictionOutcomes.text = prediction.outcomes?.map {
context.getString(
R.string.prediction_outcome,
(((it.totalPoints ?: 0).toLong() * 100.0) / max(totalPoints, 1)).roundToInt(),
it.totalPoints,
it.totalUsers,
it.title
)
}?.joinToString("\n")
predictionStatus.visible()
predictionClose.setOnClickListener {
hidePrediction()
}
}
"LOCKED" -> {
predictionLayout.visible()
predictionTitle.text = context.getString(R.string.prediction_title, prediction.title)
val totalPoints = prediction.outcomes?.sumOf { it.totalPoints?.toLong() ?: 0 } ?: 0
predictionOutcomes.text = prediction.outcomes?.map {
context.getString(
R.string.prediction_outcome,
(((it.totalPoints ?: 0).toLong() * 100.0) / max(totalPoints, 1)).roundToInt(),
it.totalPoints,
it.totalUsers,
it.title
)
}?.joinToString("\n")
predictionClose.setOnClickListener {
hidePrediction()
}
callback?.onPredictionClose(true)
predictionStatus.visible()
predictionStatus.text = context.getString(R.string.prediction_locked)
}
"CANCELED", "CANCEL_PENDING", "RESOLVED", "RESOLVE_PENDING" -> {
predictionLayout.visible()
predictionTitle.text = context.getString(R.string.prediction_title, prediction.title)
val resolved = prediction.status == "RESOLVED" || prediction.status == "RESOLVE_PENDING"
val totalPoints = prediction.outcomes?.sumOf { it.totalPoints?.toLong() ?: 0 } ?: 0
predictionOutcomes.text = prediction.outcomes?.map {
context.getString(
if (resolved && prediction.winningOutcomeId != null && prediction.winningOutcomeId == it.id) {
R.string.prediction_outcome_winner
} else {
R.string.prediction_outcome
},
(((it.totalPoints ?: 0).toLong() * 100.0) / max(totalPoints, 1)).roundToInt(),
it.totalPoints,
it.totalUsers,
it.title
)
}?.joinToString("\n")
predictionClose.setOnClickListener {
hidePrediction()
}
callback?.onPredictionClose(true)
if (resolved) {
predictionStatus.gone()
} else {
predictionStatus.visible()
predictionStatus.text = context.getString(R.string.prediction_refunded)
}
}
else -> hidePrediction()
}
}
}

fun updatePredictionStatus(secondsLeft: Int) {
binding.predictionStatus.text = context.getString(R.string.remaining_time, DateUtils.formatElapsedTime(secondsLeft.toLong()))
}

fun hidePrediction(timeout: Boolean = false) {
binding.predictionLayout.gone()
if (!timeout) {
callback?.onPredictionClose()
}
}

fun scrollToLastPosition() {
adapter.messages?.let { binding.recyclerView.scrollToPosition(it.lastIndex) }
}
Expand Down
Loading

0 comments on commit 91aeacc

Please sign in to comment.