Refactor and optimize various components

- Updated RosettaFirebaseMessagingService to use IO dispatcher for blocking calls.
- Enhanced AvatarRepository with LRU caching and improved coroutine handling for avatar loading.
- Implemented timeout for websocket connection in UnlockScreen.
- Added selection mode functionality in ChatsListScreen with haptic feedback and improved UI for chat actions.
- Improved animated dots in AttachmentComponents for a smoother visual effect.
- Refactored image downloading and caching logic in ChatDetailComponents to streamline the process.
- Optimized SwipeBackContainer to simplify gesture handling.
- Adjusted swipe back behavior in OtherProfileScreen based on image viewer state.
This commit is contained in:
2026-02-12 15:38:30 +05:00
parent 263d00b783
commit ea537ccce1
16 changed files with 775 additions and 1370 deletions

View File

@@ -1,6 +1,7 @@
package com.rosetta.messenger.network
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -29,7 +30,9 @@ data class TransportState(
*/
object TransportManager {
private const val TAG = "TransportManager"
private const val MAX_RETRIES = 3
private const val INITIAL_BACKOFF_MS = 1000L
// Fallback transport server (CDN)
private const val FALLBACK_TRANSPORT_SERVER = "https://cdn.rosetta-im.com"
@@ -67,6 +70,24 @@ object TransportManager {
return server
}
/**
* Retry с exponential backoff: 1с, 2с, 4с
*/
private suspend fun <T> withRetry(block: suspend () -> T): T {
var lastException: Exception? = null
repeat(MAX_RETRIES) { attempt ->
try {
return block()
} catch (e: IOException) {
lastException = e
if (attempt < MAX_RETRIES - 1) {
delay(INITIAL_BACKOFF_MS shl attempt) // 1s, 2s, 4s
}
}
}
throw lastException!!
}
/**
* Запросить адрес транспортного сервера с сервера протокола
*/
@@ -83,80 +104,81 @@ object TransportManager {
*/
suspend fun uploadFile(id: String, content: String): String = withContext(Dispatchers.IO) {
val server = getActiveServer()
// Добавляем в список загрузок
_uploading.value = _uploading.value + TransportState(id, 0)
try {
// 🔥 КРИТИЧНО: Преобразуем строку в байты (как desktop делает new Blob([content]))
val contentBytes = content.toByteArray(Charsets.UTF_8)
val totalSize = contentBytes.size.toLong()
// 🔥 RequestBody с отслеживанием прогресса загрузки
val progressRequestBody = object : RequestBody() {
override fun contentType() = "application/octet-stream".toMediaType()
override fun contentLength() = totalSize
override fun writeTo(sink: okio.BufferedSink) {
val source = okio.Buffer().write(contentBytes)
var uploaded = 0L
val bufferSize = 8 * 1024L // 8 KB chunks
while (true) {
val read = source.read(sink.buffer, bufferSize)
if (read == -1L) break
uploaded += read
sink.flush()
// Обновляем прогресс
val progress = ((uploaded * 100) / totalSize).toInt()
_uploading.value = _uploading.value.map {
if (it.id == id) it.copy(progress = progress) else it
withRetry {
// 🔥 КРИТИЧНО: Преобразуем строку в байты (как desktop делает new Blob([content]))
val contentBytes = content.toByteArray(Charsets.UTF_8)
val totalSize = contentBytes.size.toLong()
// 🔥 RequestBody с отслеживанием прогресса загрузки
val progressRequestBody = object : RequestBody() {
override fun contentType() = "application/octet-stream".toMediaType()
override fun contentLength() = totalSize
override fun writeTo(sink: okio.BufferedSink) {
val source = okio.Buffer().write(contentBytes)
var uploaded = 0L
val bufferSize = 8 * 1024L // 8 KB chunks
while (true) {
val read = source.read(sink.buffer, bufferSize)
if (read == -1L) break
uploaded += read
sink.flush()
// Обновляем прогресс
val progress = ((uploaded * 100) / totalSize).toInt()
_uploading.value = _uploading.value.map {
if (it.id == id) it.copy(progress = progress) else it
}
}
}
}
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", id, progressRequestBody)
.build()
val request = Request.Builder()
.url("$server/u")
.post(requestBody)
.build()
val response = suspendCoroutine<Response> { cont ->
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
cont.resume(response)
}
})
}
if (!response.isSuccessful) {
throw IOException("Upload failed: ${response.code}")
}
val responseBody = response.body?.string()
?: throw IOException("Empty response")
// Parse JSON response to get tag
val tag = org.json.JSONObject(responseBody).getString("t")
// Обновляем прогресс до 100%
_uploading.value = _uploading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
tag
}
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", id, progressRequestBody)
.build()
val request = Request.Builder()
.url("$server/u")
.post(requestBody)
.build()
val response = suspendCoroutine<Response> { cont ->
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
cont.resume(response)
}
})
}
if (!response.isSuccessful) {
val errorBody = response.body?.string() ?: "No error body"
throw IOException("Upload failed: ${response.code}")
}
val responseBody = response.body?.string()
?: throw IOException("Empty response")
// Parse JSON response to get tag
val tag = org.json.JSONObject(responseBody).getString("t")
// Обновляем прогресс до 100%
_uploading.value = _uploading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
tag
} finally {
// Удаляем из списка загрузок
_uploading.value = _uploading.value.filter { it.id != id }
@@ -171,43 +193,43 @@ object TransportManager {
*/
suspend fun downloadFile(id: String, tag: String): String = withContext(Dispatchers.IO) {
val server = getActiveServer()
// Добавляем в список скачиваний
_downloading.value = _downloading.value + TransportState(id, 0)
try {
val request = Request.Builder()
.url("$server/d/$tag")
.get()
.build()
val response = suspendCoroutine<Response> { cont ->
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
cont.resume(response)
}
})
withRetry {
val request = Request.Builder()
.url("$server/d/$tag")
.get()
.build()
val response = suspendCoroutine<Response> { cont ->
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
cont.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
cont.resume(response)
}
})
}
if (!response.isSuccessful) {
throw IOException("Download failed: ${response.code}")
}
val content = response.body?.string()
?: throw IOException("Empty response")
// Обновляем прогресс до 100%
_downloading.value = _downloading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
content
}
if (!response.isSuccessful) {
throw IOException("Download failed: ${response.code}")
}
val content = response.body?.string()
?: throw IOException("Empty response")
// Обновляем прогресс до 100%
_downloading.value = _downloading.value.map {
if (it.id == id) it.copy(progress = 100) else it
}
content
} finally {
// Удаляем из списка скачиваний
_downloading.value = _downloading.value.filter { it.id != id }