2 changed files with 152 additions and 3 deletions
@ -0,0 +1,141 @@ |
|||||||
|
package com.utun.chat.ui.components |
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable |
||||||
|
import androidx.compose.foundation.layout.* |
||||||
|
import androidx.compose.foundation.rememberScrollState |
||||||
|
import androidx.compose.foundation.verticalScroll |
||||||
|
import androidx.compose.material3.* |
||||||
|
import androidx.compose.runtime.* |
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.res.stringResource |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
import com.utun.chat.R |
||||||
|
import com.utun.chat.data.* |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun CreatePollDialog(onDismiss: () -> Unit, onCreate: (String, List<String>, Boolean, Boolean) -> Unit) { |
||||||
|
var question by rememberSaveable { mutableStateOf("") } |
||||||
|
var variants by rememberSaveable { mutableStateOf("") } |
||||||
|
var multiple by rememberSaveable { mutableStateOf(false) } |
||||||
|
var custom by rememberSaveable { mutableStateOf(false) } |
||||||
|
val options = variants.lines().map { it.trim() }.filter { it.isNotEmpty() } |
||||||
|
val bytes = 6 + question.trim().toByteArray().size + options.sumOf { 2 + it.toByteArray().size } |
||||||
|
val valid = question.isNotBlank() && question.trim().toByteArray().size <= 512 && options.size in 2..32 && |
||||||
|
options.distinct().size == options.size && options.all { it.toByteArray().size <= 256 } && bytes <= 2048 |
||||||
|
AlertDialog(onDismissRequest = onDismiss, title = { Text(stringResource(R.string.poll_create)) }, |
||||||
|
text = { Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) { |
||||||
|
OutlinedTextField(question, { question = it }, label = { Text(stringResource(R.string.poll_question)) }) |
||||||
|
OutlinedTextField(variants, { variants = it }, label = { Text(stringResource(R.string.poll_options_hint)) }, minLines = 3) |
||||||
|
Row(Modifier.clickable { multiple = false }) { |
||||||
|
RadioButton(!multiple, onClick = { multiple = false }); Text(stringResource(R.string.poll_single)) |
||||||
|
} |
||||||
|
Row(Modifier.clickable { multiple = true }) { |
||||||
|
RadioButton(multiple, onClick = { multiple = true }); Text(stringResource(R.string.poll_multiple)) |
||||||
|
} |
||||||
|
Row(Modifier.clickable { custom = !custom }) { |
||||||
|
Checkbox(custom, { custom = it }); Text(stringResource(R.string.poll_allow_custom)) |
||||||
|
} |
||||||
|
if (!valid) Text(stringResource(R.string.poll_invalid_input), style = MaterialTheme.typography.bodySmall) |
||||||
|
} }, |
||||||
|
confirmButton = { TextButton(enabled = valid, onClick = { onCreate(question.trim(), options, multiple, custom); onDismiss() }) { |
||||||
|
Text(stringResource(R.string.poll_create)) |
||||||
|
} }, |
||||||
|
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }) |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun PollMessageBubble(message: Message, onVote: (Message, List<Int>, String) -> Unit, authorName: (Long) -> String) { |
||||||
|
var showVote by rememberSaveable(message.messageId) { mutableStateOf(false) } |
||||||
|
val poll = message.feedback?.poll |
||||||
|
Card(onClick = { if (poll != null) showVote = true }, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), |
||||||
|
colors = CardDefaults.cardColors(containerColor = if (message.isOutgoing) MaterialTheme.colorScheme.primaryContainer |
||||||
|
else MaterialTheme.colorScheme.surfaceVariant)) { |
||||||
|
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { |
||||||
|
if (!message.isOutgoing) Text(message.author, style = MaterialTheme.typography.labelMedium) |
||||||
|
if (poll == null) Text(stringResource(R.string.poll_pending)) |
||||||
|
else { |
||||||
|
Text(poll.question, style = MaterialTheme.typography.titleMedium) |
||||||
|
Text(stringResource(if (poll.multiple) R.string.poll_multiple else R.string.poll_single), |
||||||
|
style = MaterialTheme.typography.labelSmall) |
||||||
|
for (option in poll.options) { |
||||||
|
Text(option.text) |
||||||
|
LinearProgressIndicator(progress = { (option.percent / 100).toFloat().coerceIn(0f, 1f) }, |
||||||
|
modifier = Modifier.fillMaxWidth()) |
||||||
|
Text(stringResource(R.string.poll_option_result, option.count, option.percent), |
||||||
|
style = MaterialTheme.typography.labelSmall) |
||||||
|
} |
||||||
|
Text(stringResource(R.string.poll_voters, poll.voters)) |
||||||
|
TextButton(onClick = { showVote = true }) { Text(stringResource(R.string.poll_vote_results)) } |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (showVote && poll != null) PollVoteDialog(message, poll, { showVote = false }, onVote, authorName) |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
private fun PollVoteDialog(message: Message, poll: PollState, onDismiss: () -> Unit, |
||||||
|
onVote: (Message, List<Int>, String) -> Unit, authorName: (Long) -> String) { |
||||||
|
var selected by rememberSaveable(message.messageId) { mutableStateOf(ArrayList(poll.myChoices)) } |
||||||
|
var custom by rememberSaveable(message.messageId) { mutableStateOf(poll.myCustom) } |
||||||
|
var customSelected by rememberSaveable(message.messageId) { mutableStateOf(!poll.multiple && custom.isNotEmpty()) } |
||||||
|
val valid = custom.toByteArray().size <= 1024 && (!customSelected || custom.isNotBlank()) |
||||||
|
AlertDialog(onDismissRequest = onDismiss, title = { Text(poll.question) }, |
||||||
|
text = { Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) { |
||||||
|
for (option in poll.options) { |
||||||
|
val choose = { |
||||||
|
customSelected = false |
||||||
|
selected = if (!poll.multiple) arrayListOf(option.id) |
||||||
|
else ArrayList(if (option.id in selected) selected.filter { it != option.id } else selected + option.id) |
||||||
|
} |
||||||
|
Row(Modifier.fillMaxWidth().clickable(onClick = choose)) { |
||||||
|
if (poll.multiple) Checkbox(option.id in selected, onCheckedChange = { choose() }) |
||||||
|
else RadioButton(!customSelected && option.id in selected, onClick = choose) |
||||||
|
Text(option.text) |
||||||
|
} |
||||||
|
} |
||||||
|
if (poll.allowCustom) { |
||||||
|
if (!poll.multiple) Row(Modifier.clickable { customSelected = true; selected = arrayListOf() }) { |
||||||
|
RadioButton(customSelected, onClick = { customSelected = true; selected = arrayListOf() }) |
||||||
|
Text(stringResource(R.string.poll_own_answer)) |
||||||
|
} |
||||||
|
OutlinedTextField(custom, { custom = it }, enabled = poll.multiple || customSelected, isError = !valid, |
||||||
|
label = { Text(stringResource(R.string.poll_own_answer)) }) |
||||||
|
if (!valid) Text(stringResource(R.string.poll_answer_error), color = MaterialTheme.colorScheme.error) |
||||||
|
} |
||||||
|
Text(stringResource(R.string.poll_voters, poll.voters)) |
||||||
|
for (answer in poll.answers) { |
||||||
|
val parts = answer.choices.mapNotNull { id -> poll.options.find { it.id == id }?.text } + |
||||||
|
listOfNotNull(answer.custom.takeIf { it.isNotEmpty() }) |
||||||
|
Text("${authorName(answer.nodeId)}: ${parts.joinToString(", ")}", style = MaterialTheme.typography.bodySmall) |
||||||
|
} |
||||||
|
} }, |
||||||
|
confirmButton = { TextButton(enabled = valid, onClick = { |
||||||
|
onVote(message, selected, if (poll.allowCustom && (poll.multiple || customSelected)) custom.trim() else ""); onDismiss() |
||||||
|
}) { Text(stringResource(R.string.poll_save)) } }, |
||||||
|
dismissButton = { Row { |
||||||
|
TextButton(onClick = { onVote(message, emptyList(), ""); onDismiss() }) { Text(stringResource(R.string.poll_withdraw)) } |
||||||
|
TextButton(onClick = onDismiss) { Text(stringResource(R.string.close)) } |
||||||
|
} }) |
||||||
|
} |
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class) |
||||||
|
@Composable |
||||||
|
fun MessageReactions(message: Message, catalog: List<ReactionState>, onReact: (Message, Int?) -> Unit) { |
||||||
|
var picker by rememberSaveable(message.messageId) { mutableStateOf(false) } |
||||||
|
FlowRow(Modifier.padding(horizontal = 8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { |
||||||
|
for (reaction in message.feedback?.reactions.orEmpty()) { |
||||||
|
FilterChip(selected = reaction.mine, onClick = { onReact(message, reaction.id) }, |
||||||
|
label = { Text("${reaction.emoji} ${reaction.count}") }) |
||||||
|
} |
||||||
|
TextButton(onClick = { picker = true }) { Text(stringResource(R.string.poll_react)) } |
||||||
|
} |
||||||
|
if (picker) AlertDialog(onDismissRequest = { picker = false }, title = { Text(stringResource(R.string.poll_react)) }, |
||||||
|
text = { FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { |
||||||
|
for (reaction in catalog) TextButton(onClick = { onReact(message, reaction.id); picker = false }) { Text(reaction.emoji) } |
||||||
|
} }, |
||||||
|
confirmButton = { TextButton(onClick = { onReact(message, null); picker = false }) { |
||||||
|
Text(stringResource(R.string.poll_clear_reactions)) |
||||||
|
} }, |
||||||
|
dismissButton = { TextButton(onClick = { picker = false }) { Text(stringResource(R.string.close)) } }) |
||||||
|
} |
||||||
Loading…
Reference in new issue