feat: Update color handling for typing indicator and verified badge in dark theme

This commit is contained in:
2026-02-25 18:35:43 +05:00
parent 0cbd2b84c3
commit 31d100e9ac
6 changed files with 106 additions and 18 deletions

View File

@@ -3,6 +3,7 @@ package com.rosetta.messenger
import android.app.Application
import com.airbnb.lottie.L
import com.rosetta.messenger.data.DraftManager
import com.rosetta.messenger.network.TransportManager
import com.rosetta.messenger.update.UpdateManager
import com.rosetta.messenger.utils.CrashReportManager
@@ -27,6 +28,9 @@ class RosettaApplication : Application() {
// Инициализируем менеджер черновиков
DraftManager.init(this)
// Инициализируем менеджер транспорта файлов (streaming download)
TransportManager.init(this)
// Инициализируем менеджер обновлений (SDU)
UpdateManager.init()

View File

@@ -1,5 +1,6 @@
package com.rosetta.messenger.network
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
@@ -32,6 +34,7 @@ object TransportManager {
private const val INITIAL_BACKOFF_MS = 1000L
private var transportServer: String? = null
private var appContext: Context? = null
private val _uploading = MutableStateFlow<List<TransportState>>(emptyList())
val uploading: StateFlow<List<TransportState>> = _uploading.asStateFlow()
@@ -45,6 +48,13 @@ object TransportManager {
.writeTimeout(5, TimeUnit.MINUTES) // 5 минут для больших файлов
.build()
/**
* Инициализация с контекстом приложения (вызывать из Application.onCreate)
*/
fun init(context: Context) {
appContext = context.applicationContext
}
/**
* Установить адрес транспортного сервера
*/
@@ -83,8 +93,8 @@ object TransportManager {
repeat(MAX_RETRIES) { attempt ->
try {
return block()
} catch (e: IOException) {
lastException = e
} catch (e: Exception) {
lastException = if (e is IOException) e else IOException("Download error: ${e.message}", e)
if (attempt < MAX_RETRIES - 1) {
delay(INITIAL_BACKOFF_MS shl attempt) // 1s, 2s, 4s
}
@@ -194,13 +204,17 @@ object TransportManager {
}
/**
* Скачать файл с транспортного сервера
* Скачать файл с транспортного сервера — СТРИМИНГ на диск.
* Файл скачивается чанками по 64KB во временный файл, потом читается как String.
* Это предотвращает OutOfMemoryError для больших файлов (30MB+).
*
* @param id Уникальный ID файла (для трекинга)
* @param tag Tag файла на сервере
* @return Содержимое файла
*/
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
val server = getActiveServer()
ProtocolManager.addLog("📥 Download start: id=${id.take(8)}, tag=${tag.take(10)}, server=$server")
// Добавляем в список скачиваний
_downloading.value = _downloading.value + TransportState(id, 0)
@@ -228,16 +242,65 @@ object TransportManager {
throw IOException("Download failed: ${response.code}")
}
val content = response.body?.string()
?: throw IOException("Empty response")
val body = response.body ?: throw IOException("Empty response body")
val contentLength = body.contentLength()
// Обновляем прогресс до 100%
_downloading.value = _downloading.value.map {
if (it.id == id) it.copy(progress = 100) else it
// Для маленьких файлов (<2MB) — читаем в память напрямую (быстрее)
if (contentLength in 1..2_000_000) {
val content = body.string()
_downloading.value = _downloading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
ProtocolManager.addLog("✅ Download OK (mem): id=${id.take(8)}, size=${content.length}")
return@withRetry content
}
content
// Для больших файлов — стримим на диск чанками
val cacheDir = appContext?.cacheDir ?: throw IOException("No app context for streaming download")
val tempFile = File(cacheDir, "dl_${id.take(16)}_${System.currentTimeMillis()}.tmp")
try {
var totalRead = 0L
val buffer = ByteArray(64 * 1024) // 64KB chunks
body.byteStream().use { inputStream ->
tempFile.outputStream().use { outputStream ->
while (true) {
val bytesRead = inputStream.read(buffer)
if (bytesRead == -1) break
outputStream.write(buffer, 0, bytesRead)
totalRead += bytesRead
// Обновляем прогресс
if (contentLength > 0) {
val progress = ((totalRead * 100) / contentLength).toInt().coerceIn(0, 99)
_downloading.value = _downloading.value.map {
if (it.id == id) it.copy(progress = progress) else it
}
}
}
}
}
// Читаем результат из файла
val content = tempFile.readText(Charsets.UTF_8)
_downloading.value = _downloading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
ProtocolManager.addLog("✅ Download OK (stream): id=${id.take(8)}, size=$totalRead")
content
} finally {
tempFile.delete()
}
}
} catch (e: Exception) {
ProtocolManager.addLog(
"❌ Download failed: id=${id.take(8)}, reason=${e.javaClass.simpleName}: ${e.message ?: "unknown"}"
)
throw e
} finally {
// Удаляем из списка скачиваний
_downloading.value = _downloading.value.filter { it.id != id }

View File

@@ -1142,13 +1142,10 @@ fun ChatDetailScreen(
isSavedMessages ->
Color.White.copy(alpha = 0.7f)
isOnline ->
Color(
0xFF7FE084
) // Зелёный когда онлайн (светлый на синем фоне)
if (isDarkTheme) Color(0xFF7FE084)
else Color.White.copy(alpha = 0.7f)
else ->
Color.White.copy(alpha = 0.7f) // Белый полупрозрачный
// для
// offline
Color.White.copy(alpha = 0.7f)
},
maxLines = 1
)

View File

@@ -217,7 +217,7 @@ fun DateHeader(dateText: String, secondaryTextColor: Color) {
*/
@Composable
fun TypingIndicator(isDarkTheme: Boolean) {
val typingColor = Color(0xFF54A9EB)
val typingColor = if (isDarkTheme) Color(0xFF54A9EB) else Color.White.copy(alpha = 0.7f)
val infiniteTransition = rememberInfiniteTransition(label = "typing")
// Each dot animates through a 0→1→0 cycle, staggered by 150 ms

View File

@@ -1026,7 +1026,30 @@ fun extractImagesFromMessages(
)
} ?: emptyList()
mainImages + replyImages
// Also include images from forwarded messages
val forwardedImages = message.forwardedMessages.flatMap { fwd ->
fwd.attachments
.filter { it.type == AttachmentType.IMAGE }
.map { attachment ->
val fwdSenderKey = fwd.senderPublicKey.ifEmpty {
if (fwd.isFromMe) currentPublicKey else opponentPublicKey
}
ViewableImage(
attachmentId = attachment.id,
preview = attachment.preview,
blob = attachment.blob,
chachaKey = message.chachaKey,
senderPublicKey = fwdSenderKey,
senderName = fwd.senderName,
timestamp = message.timestamp,
width = attachment.width,
height = attachment.height,
caption = fwd.text
)
}
}
mainImages + replyImages + forwardedImages
}
.filter { seenIds.add(it.attachmentId) }
.sortedBy { it.timestamp }

View File

@@ -1935,7 +1935,8 @@ private fun CollapsingOtherProfileHeader(
VerifiedBadge(
verified = if (verified > 0) verified else 1,
size = (nameFontSize.value * 0.8f).toInt(),
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
badgeTint = if (isDarkTheme) null else Color.White
)
}
}