media: рефакторинг загрузки блоков — конечный автомат, фейловер, watchdog простоя

- media_download: состояние блока (IDLE/CONN/REQ/RECV/WAIT/DONE/FAILED),
  кандидаты-держатели (MD_MAX_HOLDERS_PER_BLOCK), фейловер + ретраи (MEDIA_DL_MAX_ATTEMPTS),
  watchdog простоя (MEDIA_DL_STALL_TIMEOUT_TB) вместо фейкового времени
- media_delivery: настраиваемые dl_stall_timeout_tb / dl_max_attempts
- chat_msg: пометка ошибки скачивания в local_attrs ("st":"er") для UI
- tests: новый test_media_download_timeout (watchdog/фейловер), рефакторинг
  test_media_delivery_download/full
- chatgui-android: состояние 'error' у вложений/видео (иконка + tap to retry)
This commit is contained in:
evgeny committed 2026-09-08 06:53:48 +03:00
1 parent 089bbe5189
commit f28d3e91f7
11 files changed
+1200 -746

No files matched your search

@@ -274,6 +274,7 @@ class ChatRepository {
val localAttrs = obj.optString("localAttrs", "")
if (filePath.isNotEmpty()) downloadState = "downloaded"
else if (localAttrs.contains("\"st\":\"fl\"")) downloadState = "downloaded"
else if (localAttrs.contains("\"st\":\"er\"")) downloadState = "error"
var waveform = emptyList<Float>()
var voiceDurationMs = 0
@@ -208,11 +208,13 @@ private fun FileBubble(message: Message, textColor: Color, onDownload: (Message)
val stateIcon = when {
state == "downloaded" -> "\u2705"
state == "downloading" -> "\u23F3"
state == "error" -> "\u26A0\uFE0F"
else -> "\u2B07\uFE0F"
}
val stateText = when {
state == "downloaded" -> "Tap to save"
state == "downloading" -> "Downloading..."
state == "error" -> "Download failed, tap to retry"
else -> "Tap to download"
}
@@ -220,7 +222,7 @@ private fun FileBubble(message: Message, textColor: Color, onDownload: (Message)
modifier = Modifier.clickable {
when {
state == "downloaded" -> onOpen(message)
state.isEmpty() -> onDownload(message)
else -> onDownload(message)
}
}
) {
@@ -272,6 +274,7 @@ private fun VideoBubble(message: Message, textColor: Color,
onPlay: (Message) -> Unit = {}) {
val downloading = message.downloadState == "downloading"
val downloaded = message.filePath.isNotEmpty()
val isError = message.downloadState == "error"
var thumb by remember(message.filePath) { mutableStateOf<Bitmap?>(null) }
var durMs by remember(message.filePath) { mutableStateOf(0) }
@@ -302,7 +305,7 @@ private fun VideoBubble(message: Message, textColor: Color,
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.size(44.dp).background(Color(0x88000000), CircleShape), contentAlignment = Alignment.Center) {
Text(
when { downloading -> "\u23F3"; !downloaded -> "\u2B07"; else -> "\u25B6" },
when { downloading -> "\u23F3"; isError -> "\u267B"; !downloaded -> "\u2B07"; else -> "\u25B6" },
fontSize = 18.sp, color = Color.White
)
}
@@ -323,6 +326,7 @@ private fun VideoBubble(message: Message, textColor: Color,
Text(
"${formatFileSize(message.fileSize)} · " + when {
downloading -> "Downloading..."
isError -> "Failed, tap to retry"
!downloaded -> "Tap to download"
else -> "Tap to play"
},