Оптимизация приложения
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
|
## 1.3.3
|
||||||
|
|
||||||
|
### E2EE, чаты и производительность
|
||||||
|
- В release-сборке отключена frame-диагностика E2EE (детальный frame dump теперь только в debug).
|
||||||
|
- В `ChatsListScreen` убран двойной `collectAsState(chatsState)` и вынесены route-блоки в подкомпоненты (`CallsRouteContent`, `RequestsRouteContent`, общий `SwipeBackContainer`).
|
||||||
|
- Добавлена денормализация `primary_attachment_type` в таблице `messages` + индекс `(account, primary_attachment_type, timestamp)`.
|
||||||
|
- Обновлена миграция БД `14 -> 15`: добавление колонки, индекс и backfill значения типа вложения для уже сохраненных сообщений.
|
||||||
|
- Поисковые и call-history запросы переведены на `primary_attachment_type` с fallback на legacy `attachments LIKE` для старых записей.
|
||||||
|
|
||||||
## 1.2.3
|
## 1.2.3
|
||||||
|
|
||||||
### Групповые чаты и медиа
|
### Групповые чаты и медиа
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Rosetta versioning — bump here on each release
|
// Rosetta versioning — bump here on each release
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val rosettaVersionName = "1.3.2"
|
val rosettaVersionName = "1.3.3"
|
||||||
val rosettaVersionCode = 34 // Increment on each release
|
val rosettaVersionCode = 35 // Increment on each release
|
||||||
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ class GroupRepository private constructor(context: Context) {
|
|||||||
messageId = UUID.randomUUID().toString().replace("-", "").take(32),
|
messageId = UUID.randomUUID().toString().replace("-", "").take(32),
|
||||||
plainMessage = encryptedPlainMessage,
|
plainMessage = encryptedPlainMessage,
|
||||||
attachments = "[]",
|
attachments = "[]",
|
||||||
|
primaryAttachmentType = -1,
|
||||||
dialogKey = dialogPublicKey
|
dialogKey = dialogPublicKey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
messageId = messageId,
|
messageId = messageId,
|
||||||
plainMessage = encryptedPlainMessage,
|
plainMessage = encryptedPlainMessage,
|
||||||
attachments = "[]",
|
attachments = "[]",
|
||||||
|
primaryAttachmentType = -1,
|
||||||
dialogKey = dialogKey
|
dialogKey = dialogKey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -266,6 +267,7 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
messageId = messageId,
|
messageId = messageId,
|
||||||
plainMessage = encryptedPlainMessage,
|
plainMessage = encryptedPlainMessage,
|
||||||
attachments = "[]",
|
attachments = "[]",
|
||||||
|
primaryAttachmentType = -1,
|
||||||
dialogKey = dialogKey
|
dialogKey = dialogKey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -528,6 +530,8 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
messageId = messageId,
|
messageId = messageId,
|
||||||
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
|
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
|
||||||
attachments = attachmentsJson,
|
attachments = attachmentsJson,
|
||||||
|
primaryAttachmentType =
|
||||||
|
resolvePrimaryAttachmentType(attachments),
|
||||||
replyToMessageId = replyToMessageId,
|
replyToMessageId = replyToMessageId,
|
||||||
dialogKey = dialogKey
|
dialogKey = dialogKey
|
||||||
)
|
)
|
||||||
@@ -860,6 +864,8 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
messageId = messageId, // 🔥 Используем сгенерированный messageId!
|
messageId = messageId, // 🔥 Используем сгенерированный messageId!
|
||||||
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
|
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
|
||||||
attachments = attachmentsJson,
|
attachments = attachmentsJson,
|
||||||
|
primaryAttachmentType =
|
||||||
|
resolvePrimaryAttachmentType(packet.attachments),
|
||||||
dialogKey = dialogKey
|
dialogKey = dialogKey
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1638,6 +1644,11 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
return jsonArray.toString()
|
return jsonArray.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolvePrimaryAttachmentType(attachments: List<MessageAttachment>): Int {
|
||||||
|
if (attachments.isEmpty()) return -1
|
||||||
|
return attachments.first().type.value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 📸 Обработка AVATAR attachments - сохранение аватара отправителя в кэш Как в desktop: при
|
* 📸 Обработка AVATAR attachments - сохранение аватара отправителя в кэш Как в desktop: при
|
||||||
* получении attachment с типом AVATAR - сохраняем в avatar_cache
|
* получении attachment с типом AVATAR - сохраняем в avatar_cache
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ object ReleaseNotes {
|
|||||||
val RELEASE_NOTICE = """
|
val RELEASE_NOTICE = """
|
||||||
Update v$VERSION_PLACEHOLDER
|
Update v$VERSION_PLACEHOLDER
|
||||||
|
|
||||||
Защищенные звонки и диагностика E2EE
|
Оптимизация E2EE и списка чатов
|
||||||
- Обновлен custom WebRTC для Android и исправлена совместимость аудио E2EE с Desktop
|
- В release отключена frame-диагностика E2EE (детальные frame-логи только в debug)
|
||||||
- Улучшены diagnostics для шифрования звонков (детализация ENC/DEC в crash reports)
|
- Упрощен ChatsListScreen: убрано дублирование collectAsState и вынесены route-компоненты
|
||||||
- В Crash Reports добавлена кнопка копирования полного лога одним действием
|
- Ускорены выборки по вложениям: добавлен denormalized attachment type + индекс в БД
|
||||||
|
- Добавлена миграция БД с backfill типа вложения для старых сообщений
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
fun getNotice(version: String): String =
|
fun getNotice(version: String): String =
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ data class LastMessageStatus(
|
|||||||
[
|
[
|
||||||
Index(value = ["account", "from_public_key", "to_public_key", "timestamp"]),
|
Index(value = ["account", "from_public_key", "to_public_key", "timestamp"]),
|
||||||
Index(value = ["account", "message_id"], unique = true),
|
Index(value = ["account", "message_id"], unique = true),
|
||||||
Index(value = ["account", "dialog_key", "timestamp"])]
|
Index(value = ["account", "dialog_key", "timestamp"]),
|
||||||
|
Index(value = ["account", "primary_attachment_type", "timestamp"])]
|
||||||
)
|
)
|
||||||
data class MessageEntity(
|
data class MessageEntity(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
@@ -99,6 +100,8 @@ data class MessageEntity(
|
|||||||
@ColumnInfo(name = "plain_message")
|
@ColumnInfo(name = "plain_message")
|
||||||
val plainMessage: String, // 🔒 Зашифрованный текст (encryptWithPassword) для хранения в БД
|
val plainMessage: String, // 🔒 Зашифрованный текст (encryptWithPassword) для хранения в БД
|
||||||
@ColumnInfo(name = "attachments") val attachments: String = "[]", // JSON массив вложений
|
@ColumnInfo(name = "attachments") val attachments: String = "[]", // JSON массив вложений
|
||||||
|
@ColumnInfo(name = "primary_attachment_type", defaultValue = "-1")
|
||||||
|
val primaryAttachmentType: Int = -1, // Денормализованный тип 1-го вложения (-1 если нет)
|
||||||
@ColumnInfo(name = "reply_to_message_id")
|
@ColumnInfo(name = "reply_to_message_id")
|
||||||
val replyToMessageId: String? = null, // ID цитируемого сообщения
|
val replyToMessageId: String? = null, // ID цитируемого сообщения
|
||||||
@ColumnInfo(name = "dialog_key") val dialogKey: String // Ключ диалога для быстрой выборки
|
@ColumnInfo(name = "dialog_key") val dialogKey: String // Ключ диалога для быстрой выборки
|
||||||
@@ -545,8 +548,10 @@ interface MessageDao {
|
|||||||
"""
|
"""
|
||||||
SELECT * FROM messages
|
SELECT * FROM messages
|
||||||
WHERE account = :account
|
WHERE account = :account
|
||||||
AND attachments != '[]'
|
AND (
|
||||||
AND attachments LIKE '%"type":0%'
|
primary_attachment_type = 0
|
||||||
|
OR (primary_attachment_type = -1 AND attachments != '[]' AND attachments LIKE '%"type":0%')
|
||||||
|
)
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT :limit OFFSET :offset
|
LIMIT :limit OFFSET :offset
|
||||||
"""
|
"""
|
||||||
@@ -561,8 +566,10 @@ interface MessageDao {
|
|||||||
"""
|
"""
|
||||||
SELECT * FROM messages
|
SELECT * FROM messages
|
||||||
WHERE account = :account
|
WHERE account = :account
|
||||||
AND attachments != '[]'
|
AND (
|
||||||
AND attachments LIKE '%"type":2%'
|
primary_attachment_type = 2
|
||||||
|
OR (primary_attachment_type = -1 AND attachments != '[]' AND attachments LIKE '%"type":2%')
|
||||||
|
)
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT :limit OFFSET :offset
|
LIMIT :limit OFFSET :offset
|
||||||
"""
|
"""
|
||||||
@@ -593,8 +600,14 @@ interface MessageDao {
|
|||||||
ELSE m.from_public_key
|
ELSE m.from_public_key
|
||||||
END
|
END
|
||||||
WHERE m.account = :account
|
WHERE m.account = :account
|
||||||
AND m.attachments != '[]'
|
AND (
|
||||||
AND (m.attachments LIKE '%"type":4%' OR m.attachments LIKE '%"type": 4%')
|
m.primary_attachment_type = 4
|
||||||
|
OR (
|
||||||
|
m.primary_attachment_type = -1
|
||||||
|
AND m.attachments != '[]'
|
||||||
|
AND (m.attachments LIKE '%"type":4%' OR m.attachments LIKE '%"type": 4%')
|
||||||
|
)
|
||||||
|
)
|
||||||
ORDER BY m.timestamp DESC, m.message_id DESC
|
ORDER BY m.timestamp DESC, m.message_id DESC
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
"""
|
"""
|
||||||
@@ -611,8 +624,14 @@ interface MessageDao {
|
|||||||
END AS peer_key
|
END AS peer_key
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE account = :account
|
WHERE account = :account
|
||||||
AND attachments != '[]'
|
AND (
|
||||||
AND (attachments LIKE '%"type":4%' OR attachments LIKE '%"type": 4%')
|
primary_attachment_type = 4
|
||||||
|
OR (
|
||||||
|
primary_attachment_type = -1
|
||||||
|
AND attachments != '[]'
|
||||||
|
AND (attachments LIKE '%"type":4%' OR attachments LIKE '%"type": 4%')
|
||||||
|
)
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
suspend fun getCallHistoryPeers(account: String): List<String>
|
suspend fun getCallHistoryPeers(account: String): List<String>
|
||||||
@@ -622,8 +641,14 @@ interface MessageDao {
|
|||||||
"""
|
"""
|
||||||
DELETE FROM messages
|
DELETE FROM messages
|
||||||
WHERE account = :account
|
WHERE account = :account
|
||||||
AND attachments != '[]'
|
AND (
|
||||||
AND (attachments LIKE '%"type":4%' OR attachments LIKE '%"type": 4%')
|
primary_attachment_type = 4
|
||||||
|
OR (
|
||||||
|
primary_attachment_type = -1
|
||||||
|
AND attachments != '[]'
|
||||||
|
AND (attachments LIKE '%"type":4%' OR attachments LIKE '%"type": 4%')
|
||||||
|
)
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
suspend fun deleteAllCallMessages(account: String): Int
|
suspend fun deleteAllCallMessages(account: String): Int
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
AccountSyncTimeEntity::class,
|
AccountSyncTimeEntity::class,
|
||||||
GroupEntity::class,
|
GroupEntity::class,
|
||||||
PinnedMessageEntity::class],
|
PinnedMessageEntity::class],
|
||||||
version = 14,
|
version = 15,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class RosettaDatabase : RoomDatabase() {
|
abstract class RosettaDatabase : RoomDatabase() {
|
||||||
@@ -202,6 +202,36 @@ abstract class RosettaDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🧱 МИГРАЦИЯ 14->15: Денормализованный тип вложения для ускорения фильтров (media/files/calls)
|
||||||
|
*/
|
||||||
|
private val MIGRATION_14_15 =
|
||||||
|
object : Migration(14, 15) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL(
|
||||||
|
"ALTER TABLE messages ADD COLUMN primary_attachment_type INTEGER NOT NULL DEFAULT -1"
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE INDEX IF NOT EXISTS index_messages_account_primary_attachment_type_timestamp ON messages (account, primary_attachment_type, timestamp)"
|
||||||
|
)
|
||||||
|
// Best-effort backfill для уже сохраненных сообщений.
|
||||||
|
database.execSQL(
|
||||||
|
"""
|
||||||
|
UPDATE messages
|
||||||
|
SET primary_attachment_type = CASE
|
||||||
|
WHEN attachments IS NULL OR TRIM(attachments) = '' OR TRIM(attachments) = '[]' THEN -1
|
||||||
|
WHEN attachments LIKE '%"type":0%' OR attachments LIKE '%"type": 0%' THEN 0
|
||||||
|
WHEN attachments LIKE '%"type":1%' OR attachments LIKE '%"type": 1%' THEN 1
|
||||||
|
WHEN attachments LIKE '%"type":2%' OR attachments LIKE '%"type": 2%' THEN 2
|
||||||
|
WHEN attachments LIKE '%"type":3%' OR attachments LIKE '%"type": 3%' THEN 3
|
||||||
|
WHEN attachments LIKE '%"type":4%' OR attachments LIKE '%"type": 4%' THEN 4
|
||||||
|
ELSE -1
|
||||||
|
END
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getDatabase(context: Context): RosettaDatabase {
|
fun getDatabase(context: Context): RosettaDatabase {
|
||||||
return INSTANCE
|
return INSTANCE
|
||||||
?: synchronized(this) {
|
?: synchronized(this) {
|
||||||
@@ -224,7 +254,8 @@ abstract class RosettaDatabase : RoomDatabase() {
|
|||||||
MIGRATION_10_11,
|
MIGRATION_10_11,
|
||||||
MIGRATION_11_12,
|
MIGRATION_11_12,
|
||||||
MIGRATION_12_13,
|
MIGRATION_12_13,
|
||||||
MIGRATION_13_14
|
MIGRATION_13_14,
|
||||||
|
MIGRATION_14_15
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration() // Для разработки - только
|
.fallbackToDestructiveMigration() // Для разработки - только
|
||||||
// если миграция не
|
// если миграция не
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.rosetta.messenger.network
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.rosetta.messenger.BuildConfig
|
||||||
import com.rosetta.messenger.data.MessageRepository
|
import com.rosetta.messenger.data.MessageRepository
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@@ -872,13 +873,15 @@ object CallManager {
|
|||||||
}
|
}
|
||||||
sharedKeyBytes = keyBytes.copyOf(32)
|
sharedKeyBytes = keyBytes.copyOf(32)
|
||||||
breadcrumb("KE: shared key fp=${sharedKeyBytes?.fingerprintHex(8)}")
|
breadcrumb("KE: shared key fp=${sharedKeyBytes?.fingerprintHex(8)}")
|
||||||
// Open native diagnostics file for frame-level logging
|
// Frame-level diagnostics are enabled only for debug builds.
|
||||||
try {
|
if (BuildConfig.DEBUG) {
|
||||||
val dir = java.io.File(appContext!!.filesDir, "crash_reports")
|
try {
|
||||||
if (!dir.exists()) dir.mkdirs()
|
val dir = java.io.File(appContext!!.filesDir, "crash_reports")
|
||||||
val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath
|
if (!dir.exists()) dir.mkdirs()
|
||||||
XChaCha20E2EE.nativeOpenDiagFile(diagPath)
|
val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath
|
||||||
} catch (_: Throwable) {}
|
XChaCha20E2EE.nativeOpenDiagFile(diagPath)
|
||||||
|
} catch (_: Throwable) {}
|
||||||
|
}
|
||||||
// If sender track already exists, bind encryptor now.
|
// If sender track already exists, bind encryptor now.
|
||||||
val existingSender =
|
val existingSender =
|
||||||
pendingAudioSenderForE2ee
|
pendingAudioSenderForE2ee
|
||||||
|
|||||||
@@ -4641,6 +4641,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
encryptedPlainMessage, // 🔒 Зашифрованный текст для хранения в
|
encryptedPlainMessage, // 🔒 Зашифрованный текст для хранения в
|
||||||
// БД
|
// БД
|
||||||
attachments = attachmentsJson,
|
attachments = attachmentsJson,
|
||||||
|
primaryAttachmentType =
|
||||||
|
resolvePrimaryAttachmentTypeFromJson(attachmentsJson),
|
||||||
replyToMessageId = null,
|
replyToMessageId = null,
|
||||||
dialogKey = dialogKey
|
dialogKey = dialogKey
|
||||||
)
|
)
|
||||||
@@ -4649,6 +4651,18 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolvePrimaryAttachmentTypeFromJson(attachmentsJson: String): Int {
|
||||||
|
if (attachmentsJson.isBlank() || attachmentsJson == "[]") return -1
|
||||||
|
return try {
|
||||||
|
val array = JSONArray(attachmentsJson)
|
||||||
|
if (array.length() == 0) return -1
|
||||||
|
val first = array.optJSONObject(0) ?: return -1
|
||||||
|
first.optInt("type", -1)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showTypingIndicator() {
|
private fun showTypingIndicator() {
|
||||||
_opponentTyping.value = true
|
_opponentTyping.value = true
|
||||||
// Отменяем предыдущий таймер, чтобы избежать race condition
|
// Отменяем предыдущий таймер, чтобы избежать race condition
|
||||||
|
|||||||
@@ -666,6 +666,7 @@ fun ChatsListScreen(
|
|||||||
|
|
||||||
// Requests count for badge on hamburger & sidebar
|
// Requests count for badge on hamburger & sidebar
|
||||||
val topLevelChatsState by chatsViewModel.chatsState.collectAsState()
|
val topLevelChatsState by chatsViewModel.chatsState.collectAsState()
|
||||||
|
val topLevelIsLoading by chatsViewModel.isLoading.collectAsState()
|
||||||
val topLevelRequestsCount = topLevelChatsState.requestsCount
|
val topLevelRequestsCount = topLevelChatsState.requestsCount
|
||||||
|
|
||||||
// Dev console dialog - commented out for now
|
// Dev console dialog - commented out for now
|
||||||
@@ -1934,8 +1935,8 @@ fun ChatsListScreen(
|
|||||||
// Это предотвращает "дергание" UI когда dialogs и requests
|
// Это предотвращает "дергание" UI когда dialogs и requests
|
||||||
// обновляются
|
// обновляются
|
||||||
// независимо
|
// независимо
|
||||||
val chatsState by chatsViewModel.chatsState.collectAsState()
|
val chatsState = topLevelChatsState
|
||||||
val isLoading by chatsViewModel.isLoading.collectAsState()
|
val isLoading = topLevelIsLoading
|
||||||
val requests = chatsState.requests
|
val requests = chatsState.requests
|
||||||
val requestsCount = chatsState.requestsCount
|
val requestsCount = chatsState.requestsCount
|
||||||
|
|
||||||
@@ -2061,64 +2062,15 @@ fun ChatsListScreen(
|
|||||||
label = "CallsTransition"
|
label = "CallsTransition"
|
||||||
) { isCallsScreen ->
|
) { isCallsScreen ->
|
||||||
if (isCallsScreen) {
|
if (isCallsScreen) {
|
||||||
Box(
|
CallsRouteContent(
|
||||||
modifier = Modifier
|
isDarkTheme = isDarkTheme,
|
||||||
.fillMaxSize()
|
accountPublicKey = accountPublicKey,
|
||||||
.pointerInput(Unit) {
|
avatarRepository = avatarRepository,
|
||||||
val velocityTracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
|
onUserSelect = onUserSelect,
|
||||||
awaitEachGesture {
|
onStartCall = onStartCall,
|
||||||
val down = awaitFirstDown(requireUnconsumed = false)
|
onStartNewCall = onSearchClick,
|
||||||
velocityTracker.resetTracking()
|
onBack = { setInlineCallsVisible(false) }
|
||||||
velocityTracker.addPosition(down.uptimeMillis, down.position)
|
)
|
||||||
var totalDragX = 0f
|
|
||||||
var totalDragY = 0f
|
|
||||||
var claimed = false
|
|
||||||
val touchSlop = viewConfiguration.touchSlop * 0.6f
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val event = awaitPointerEvent()
|
|
||||||
val change = event.changes.firstOrNull { it.id == down.id } ?: break
|
|
||||||
if (change.changedToUpIgnoreConsumed()) break
|
|
||||||
|
|
||||||
val delta = change.positionChange()
|
|
||||||
totalDragX += delta.x
|
|
||||||
totalDragY += delta.y
|
|
||||||
velocityTracker.addPosition(change.uptimeMillis, change.position)
|
|
||||||
|
|
||||||
if (!claimed) {
|
|
||||||
val distance = kotlin.math.sqrt(totalDragX * totalDragX + totalDragY * totalDragY)
|
|
||||||
if (distance < touchSlop) continue
|
|
||||||
if (totalDragX > 0 && kotlin.math.abs(totalDragX) > kotlin.math.abs(totalDragY) * 1.2f) {
|
|
||||||
claimed = true
|
|
||||||
change.consume()
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
change.consume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (claimed) {
|
|
||||||
val velocityX = velocityTracker.calculateVelocity().x
|
|
||||||
val screenWidth = size.width.toFloat()
|
|
||||||
if (totalDragX > screenWidth * 0.08f || velocityX > 200f) {
|
|
||||||
setInlineCallsVisible(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
CallsHistoryScreen(
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
accountPublicKey = accountPublicKey,
|
|
||||||
avatarRepository = avatarRepository,
|
|
||||||
onOpenChat = onUserSelect,
|
|
||||||
onStartCall = onStartCall,
|
|
||||||
onStartNewCall = onSearchClick,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 🎬 Animated content transition between main list and
|
// 🎬 Animated content transition between main list and
|
||||||
// requests
|
// requests
|
||||||
@@ -2154,110 +2106,42 @@ fun ChatsListScreen(
|
|||||||
label = "RequestsTransition"
|
label = "RequestsTransition"
|
||||||
) { isRequestsScreen ->
|
) { isRequestsScreen ->
|
||||||
if (isRequestsScreen) {
|
if (isRequestsScreen) {
|
||||||
// 📬 Show Requests Screen with swipe-back
|
RequestsRouteContent(
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
val velocityTracker = androidx.compose.ui.input.pointer.util.VelocityTracker()
|
|
||||||
awaitEachGesture {
|
|
||||||
val down = awaitFirstDown(requireUnconsumed = false)
|
|
||||||
velocityTracker.resetTracking()
|
|
||||||
velocityTracker.addPosition(down.uptimeMillis, down.position)
|
|
||||||
var totalDragX = 0f
|
|
||||||
var totalDragY = 0f
|
|
||||||
var claimed = false
|
|
||||||
val touchSlop = viewConfiguration.touchSlop * 0.6f
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val event = awaitPointerEvent()
|
|
||||||
val change = event.changes.firstOrNull { it.id == down.id } ?: break
|
|
||||||
if (change.changedToUpIgnoreConsumed()) break
|
|
||||||
|
|
||||||
val delta = change.positionChange()
|
|
||||||
totalDragX += delta.x
|
|
||||||
totalDragY += delta.y
|
|
||||||
velocityTracker.addPosition(change.uptimeMillis, change.position)
|
|
||||||
|
|
||||||
if (!claimed) {
|
|
||||||
val distance = kotlin.math.sqrt(totalDragX * totalDragX + totalDragY * totalDragY)
|
|
||||||
if (distance < touchSlop) continue
|
|
||||||
if (totalDragX > 0 && kotlin.math.abs(totalDragX) > kotlin.math.abs(totalDragY) * 1.2f) {
|
|
||||||
claimed = true
|
|
||||||
change.consume()
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
change.consume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (claimed) {
|
|
||||||
val velocityX = velocityTracker.calculateVelocity().x
|
|
||||||
val screenWidth = size.width.toFloat()
|
|
||||||
if (totalDragX > screenWidth * 0.08f || velocityX > 200f) {
|
|
||||||
setInlineRequestsVisible(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
RequestsScreen(
|
|
||||||
requests = requests,
|
requests = requests,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onBack = {
|
avatarRepository = avatarRepository,
|
||||||
setInlineRequestsVisible(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onRequestClick = { request ->
|
|
||||||
val user =
|
|
||||||
chatsViewModel
|
|
||||||
.dialogToSearchUser(
|
|
||||||
request
|
|
||||||
)
|
|
||||||
onUserSelect(user)
|
|
||||||
},
|
|
||||||
avatarRepository =
|
|
||||||
avatarRepository,
|
|
||||||
blockedUsers = blockedUsers,
|
blockedUsers = blockedUsers,
|
||||||
pinnedChats = pinnedChats,
|
pinnedChats = pinnedChats,
|
||||||
isDrawerOpen =
|
isDrawerOpen =
|
||||||
drawerState.isOpen ||
|
drawerState.isOpen ||
|
||||||
drawerState
|
drawerState.isAnimationRunning,
|
||||||
.isAnimationRunning,
|
onTogglePin = onTogglePin,
|
||||||
onTogglePin = { opponentKey ->
|
|
||||||
onTogglePin(opponentKey)
|
|
||||||
},
|
|
||||||
onDeleteDialog = { opponentKey ->
|
onDeleteDialog = { opponentKey ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chatsViewModel
|
chatsViewModel.deleteDialog(opponentKey)
|
||||||
.deleteDialog(
|
|
||||||
opponentKey
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBlockUser = { opponentKey ->
|
onBlockUser = { opponentKey ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chatsViewModel
|
chatsViewModel.blockUser(opponentKey)
|
||||||
.blockUser(
|
|
||||||
opponentKey
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUnblockUser = { opponentKey ->
|
onUnblockUser = { opponentKey ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
chatsViewModel
|
chatsViewModel.unblockUser(opponentKey)
|
||||||
.unblockUser(
|
|
||||||
opponentKey
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onRequestClick = { request ->
|
||||||
|
val user =
|
||||||
|
chatsViewModel.dialogToSearchUser(
|
||||||
|
request
|
||||||
|
)
|
||||||
|
onUserSelect(user)
|
||||||
|
},
|
||||||
|
onBack = {
|
||||||
|
setInlineRequestsVisible(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} // Close Box wrapper
|
|
||||||
} else if (showSkeleton) {
|
} else if (showSkeleton) {
|
||||||
ChatsListSkeleton(isDarkTheme = isDarkTheme)
|
ChatsListSkeleton(isDarkTheme = isDarkTheme)
|
||||||
} else if (isLoading && chatsState.isEmpty) {
|
} else if (isLoading && chatsState.isEmpty) {
|
||||||
@@ -4919,6 +4803,135 @@ fun TypingIndicatorSmall() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SwipeBackContainer(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable BoxScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
modifier.fillMaxSize().pointerInput(onBack) {
|
||||||
|
val velocityTracker = VelocityTracker()
|
||||||
|
awaitEachGesture {
|
||||||
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
velocityTracker.resetTracking()
|
||||||
|
velocityTracker.addPosition(down.uptimeMillis, down.position)
|
||||||
|
var totalDragX = 0f
|
||||||
|
var totalDragY = 0f
|
||||||
|
var claimed = false
|
||||||
|
val touchSlop = viewConfiguration.touchSlop * 0.6f
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
val change =
|
||||||
|
event.changes.firstOrNull { it.id == down.id }
|
||||||
|
?: break
|
||||||
|
if (change.changedToUpIgnoreConsumed()) break
|
||||||
|
|
||||||
|
val delta = change.positionChange()
|
||||||
|
totalDragX += delta.x
|
||||||
|
totalDragY += delta.y
|
||||||
|
velocityTracker.addPosition(
|
||||||
|
change.uptimeMillis,
|
||||||
|
change.position
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!claimed) {
|
||||||
|
val distance =
|
||||||
|
kotlin.math.sqrt(
|
||||||
|
totalDragX * totalDragX +
|
||||||
|
totalDragY * totalDragY
|
||||||
|
)
|
||||||
|
if (distance < touchSlop) continue
|
||||||
|
if (
|
||||||
|
totalDragX > 0 &&
|
||||||
|
kotlin.math.abs(totalDragX) >
|
||||||
|
kotlin.math.abs(totalDragY) * 1.2f
|
||||||
|
) {
|
||||||
|
claimed = true
|
||||||
|
change.consume()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimed) {
|
||||||
|
val velocityX = velocityTracker.calculateVelocity().x
|
||||||
|
val screenWidth = size.width.toFloat()
|
||||||
|
if (
|
||||||
|
totalDragX > screenWidth * 0.08f ||
|
||||||
|
velocityX > 200f
|
||||||
|
) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CallsRouteContent(
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
accountPublicKey: String,
|
||||||
|
avatarRepository: AvatarRepository?,
|
||||||
|
onUserSelect: (com.rosetta.messenger.network.SearchUser) -> Unit,
|
||||||
|
onStartCall: (com.rosetta.messenger.network.SearchUser) -> Unit,
|
||||||
|
onStartNewCall: () -> Unit,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
SwipeBackContainer(onBack = onBack) {
|
||||||
|
CallsHistoryScreen(
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
accountPublicKey = accountPublicKey,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
onOpenChat = onUserSelect,
|
||||||
|
onStartCall = onStartCall,
|
||||||
|
onStartNewCall = onStartNewCall,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RequestsRouteContent(
|
||||||
|
requests: List<DialogUiModel>,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
avatarRepository: AvatarRepository?,
|
||||||
|
blockedUsers: Set<String>,
|
||||||
|
pinnedChats: Set<String>,
|
||||||
|
isDrawerOpen: Boolean,
|
||||||
|
onTogglePin: (String) -> Unit,
|
||||||
|
onDeleteDialog: (String) -> Unit,
|
||||||
|
onBlockUser: (String) -> Unit,
|
||||||
|
onUnblockUser: (String) -> Unit,
|
||||||
|
onRequestClick: (DialogUiModel) -> Unit,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
SwipeBackContainer(onBack = onBack) {
|
||||||
|
RequestsScreen(
|
||||||
|
requests = requests,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
onBack = onBack,
|
||||||
|
onRequestClick = onRequestClick,
|
||||||
|
avatarRepository = avatarRepository,
|
||||||
|
blockedUsers = blockedUsers,
|
||||||
|
pinnedChats = pinnedChats,
|
||||||
|
isDrawerOpen = isDrawerOpen,
|
||||||
|
onTogglePin = onTogglePin,
|
||||||
|
onDeleteDialog = onDeleteDialog,
|
||||||
|
onBlockUser = onBlockUser,
|
||||||
|
onUnblockUser = onUnblockUser
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 📬 Секция Requests — Telegram Archived Chats style */
|
/** 📬 Секция Requests — Telegram Archived Chats style */
|
||||||
@Composable
|
@Composable
|
||||||
fun RequestsSection(
|
fun RequestsSection(
|
||||||
|
|||||||
Reference in New Issue
Block a user