Document feedback snapshots and poll UI submission contracts

This commit is contained in:
evgeny committed 2026-10-28 11:36:13 +01:00
1 parent 37515df8c1
commit a1ce486e89
2 files changed
+14 -1

No files matched your search

@@ -3,6 +3,9 @@ package com.utun.chat.data
import org.json.JSONArray
import org.json.JSONObject
/* Модели снимка chat_feedback_snapshot и подготовка форм опроса для общего командного API core.
* Сетевой формат/подписи/подсчёт голосов остаются в C; здесь только JSON-разбор и проверка готового ввода UI. */
/** Элемент каталога/снимка реакций: id=0..15 общего каталога, count — участники, mine — выбор текущего узла.
* Участник хранит одну реакцию; нулевой count/mine=false также используются элементами каталога UI. */
data class ReactionState(val id: Int, val emoji: String, val count: Int = 0, val mine: Boolean = false)
@@ -83,7 +86,7 @@ fun parseFeedback(json: JSONObject): FeedbackState {
(0 until values.length()).map { i -> values.getJSONObject(i).let { row ->
val answer = row.getJSONObject("answer")
val node = row.getString("node_id").removePrefix("0x").toLong(16)
require(node > 0) { "Invalid feedback node ID" }
require(node > 0) { "Invalid feedback node ID=$node" }
PollResponse(node,
answer.optJSONArray("choices").choices(), answer.optString("custom"))
} }
@@ -23,6 +23,9 @@ import androidx.compose.ui.unit.dp
import com.utun.chat.R
import com.utun.chat.data.*
/** Форма нового опроса: пункты разделены строками, тексты trim-ятся, лимиты проверяются в UTF-8 байтах.
* Подтверждение вызывает onCreate(question, options, multiple, allowCustom), затем закрывает форму.
* Публикация выполняется core асинхронно; результат/ошибку показывает владелец callback, оптимистического poll здесь нет. */
@Composable
fun CreatePollDialog(onDismiss: () -> Unit, onCreate: (String, List<String>, Boolean, Boolean) -> Unit) {
var question by rememberSaveable { mutableStateOf("") }
@@ -52,6 +55,8 @@ fun CreatePollDialog(onDismiss: () -> Unit, onCreate: (String, List<String>, Boo
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } })
}
/** Опрос из message.feedback: результаты и проценты берутся из core, без локального пересчёта.
* Пока снимок poll отсутствует, показывается placeholder; нажатие с готовым poll открывает выбор/ответы участников. */
@Composable
fun PollMessageBubble(message: Message, onVote: (Message, List<Int>, String) -> Unit, authorName: (Long) -> String,
footer: @Composable () -> Unit = {}, header: @Composable () -> Unit = {}) {
@@ -88,6 +93,9 @@ fun PollMessageBubble(message: Message, onVote: (Message, List<Int>, String) ->
if (showVote && poll != null) PollVoteDialog(message, poll, { showVote = false }, onVote, authorName)
}
/** Локальный draft сохраняется по messageId, новые счётчики poll не перезаписывают редактируемый выбор.
* Single выбирает пункт либо свой текст, multiple позволяет оба. preparePollVote исключает неактивные поля
* и проверяет публикуемые байты; пустой выбор/текст снимает голос. onVote ставит команду, затем диалог закрывается. */
@Composable
private fun PollVoteDialog(message: Message, poll: PollState, onDismiss: () -> Unit,
onVote: (Message, List<Int>, String) -> Unit, authorName: (Long) -> String) {
@@ -135,6 +143,8 @@ private fun PollVoteDialog(message: Message, poll: PollState, onDismiss: () -> U
} })
}
/** Каталог эмодзи и реакции снимка core; нажатие на текущую реакцию переключает её через onReact.
* Один участник выбирает одну реакцию, null очищает выбор; новый mine/count приходит в следующем снимке. */
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MessageReactions(message: Message, catalog: List<ReactionState>, onReact: (Message, Int?) -> Unit) {