From a1ce486e89a3bbf009ac9edad784f6ccd09451d8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 10:59:14 +0100 Subject: [PATCH] Document feedback snapshots and poll UI submission contracts --- .../app/src/main/java/com/utun/chat/data/Feedback.kt | 5 ++++- .../main/java/com/utun/chat/ui/components/PollUi.kt | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/Feedback.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/Feedback.kt index 9d823bba..dd9e5b59 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/Feedback.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/Feedback.kt @@ -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")) } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt index c3caacee..61dc9c4c 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/PollUi.kt @@ -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, Boolean, Boolean) -> Unit) { var question by rememberSaveable { mutableStateOf("") } @@ -52,6 +55,8 @@ fun CreatePollDialog(onDismiss: () -> Unit, onCreate: (String, List, Boo dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } }) } +/** Опрос из message.feedback: результаты и проценты берутся из core, без локального пересчёта. + * Пока снимок poll отсутствует, показывается placeholder; нажатие с готовым poll открывает выбор/ответы участников. */ @Composable fun PollMessageBubble(message: Message, onVote: (Message, List, String) -> Unit, authorName: (Long) -> String, footer: @Composable () -> Unit = {}, header: @Composable () -> Unit = {}) { @@ -88,6 +93,9 @@ fun PollMessageBubble(message: Message, onVote: (Message, List, 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, 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, onReact: (Message, Int?) -> Unit) {