Оптимизация приложения
This commit is contained in:
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Rosetta versioning — bump here on each release
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
val rosettaVersionName = "1.3.2"
|
||||
val rosettaVersionCode = 34 // Increment on each release
|
||||
val rosettaVersionName = "1.3.3"
|
||||
val rosettaVersionCode = 35 // Increment on each release
|
||||
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
||||
|
||||
android {
|
||||
|
||||
@@ -456,6 +456,7 @@ class GroupRepository private constructor(context: Context) {
|
||||
messageId = UUID.randomUUID().toString().replace("-", "").take(32),
|
||||
plainMessage = encryptedPlainMessage,
|
||||
attachments = "[]",
|
||||
primaryAttachmentType = -1,
|
||||
dialogKey = dialogPublicKey
|
||||
)
|
||||
)
|
||||
|
||||
@@ -207,6 +207,7 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
messageId = messageId,
|
||||
plainMessage = encryptedPlainMessage,
|
||||
attachments = "[]",
|
||||
primaryAttachmentType = -1,
|
||||
dialogKey = dialogKey
|
||||
)
|
||||
)
|
||||
@@ -266,6 +267,7 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
messageId = messageId,
|
||||
plainMessage = encryptedPlainMessage,
|
||||
attachments = "[]",
|
||||
primaryAttachmentType = -1,
|
||||
dialogKey = dialogKey
|
||||
)
|
||||
)
|
||||
@@ -528,6 +530,8 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
messageId = messageId,
|
||||
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
|
||||
attachments = attachmentsJson,
|
||||
primaryAttachmentType =
|
||||
resolvePrimaryAttachmentType(attachments),
|
||||
replyToMessageId = replyToMessageId,
|
||||
dialogKey = dialogKey
|
||||
)
|
||||
@@ -860,6 +864,8 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
messageId = messageId, // 🔥 Используем сгенерированный messageId!
|
||||
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
|
||||
attachments = attachmentsJson,
|
||||
primaryAttachmentType =
|
||||
resolvePrimaryAttachmentType(packet.attachments),
|
||||
dialogKey = dialogKey
|
||||
)
|
||||
|
||||
@@ -1638,6 +1644,11 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
return jsonArray.toString()
|
||||
}
|
||||
|
||||
private fun resolvePrimaryAttachmentType(attachments: List<MessageAttachment>): Int {
|
||||
if (attachments.isEmpty()) return -1
|
||||
return attachments.first().type.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 📸 Обработка AVATAR attachments - сохранение аватара отправителя в кэш Как в desktop: при
|
||||
* получении attachment с типом AVATAR - сохраняем в avatar_cache
|
||||
|
||||
@@ -17,10 +17,11 @@ object ReleaseNotes {
|
||||
val RELEASE_NOTICE = """
|
||||
Update v$VERSION_PLACEHOLDER
|
||||
|
||||
Защищенные звонки и диагностика E2EE
|
||||
- Обновлен custom WebRTC для Android и исправлена совместимость аудио E2EE с Desktop
|
||||
- Улучшены diagnostics для шифрования звонков (детализация ENC/DEC в crash reports)
|
||||
- В Crash Reports добавлена кнопка копирования полного лога одним действием
|
||||
Оптимизация E2EE и списка чатов
|
||||
- В release отключена frame-диагностика E2EE (детальные frame-логи только в debug)
|
||||
- Упрощен ChatsListScreen: убрано дублирование collectAsState и вынесены route-компоненты
|
||||
- Ускорены выборки по вложениям: добавлен denormalized attachment type + индекс в БД
|
||||
- Добавлена миграция БД с backfill типа вложения для старых сообщений
|
||||
""".trimIndent()
|
||||
|
||||
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", "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(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
@@ -99,6 +100,8 @@ data class MessageEntity(
|
||||
@ColumnInfo(name = "plain_message")
|
||||
val plainMessage: String, // 🔒 Зашифрованный текст (encryptWithPassword) для хранения в БД
|
||||
@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")
|
||||
val replyToMessageId: String? = null, // ID цитируемого сообщения
|
||||
@ColumnInfo(name = "dialog_key") val dialogKey: String // Ключ диалога для быстрой выборки
|
||||
@@ -545,8 +548,10 @@ interface MessageDao {
|
||||
"""
|
||||
SELECT * FROM messages
|
||||
WHERE account = :account
|
||||
AND attachments != '[]'
|
||||
AND attachments LIKE '%"type":0%'
|
||||
AND (
|
||||
primary_attachment_type = 0
|
||||
OR (primary_attachment_type = -1 AND attachments != '[]' AND attachments LIKE '%"type":0%')
|
||||
)
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
@@ -561,8 +566,10 @@ interface MessageDao {
|
||||
"""
|
||||
SELECT * FROM messages
|
||||
WHERE account = :account
|
||||
AND attachments != '[]'
|
||||
AND attachments LIKE '%"type":2%'
|
||||
AND (
|
||||
primary_attachment_type = 2
|
||||
OR (primary_attachment_type = -1 AND attachments != '[]' AND attachments LIKE '%"type":2%')
|
||||
)
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
@@ -593,8 +600,14 @@ interface MessageDao {
|
||||
ELSE m.from_public_key
|
||||
END
|
||||
WHERE m.account = :account
|
||||
AND m.attachments != '[]'
|
||||
AND (m.attachments LIKE '%"type":4%' OR m.attachments LIKE '%"type": 4%')
|
||||
AND (
|
||||
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
|
||||
LIMIT :limit
|
||||
"""
|
||||
@@ -611,8 +624,14 @@ interface MessageDao {
|
||||
END AS peer_key
|
||||
FROM messages
|
||||
WHERE account = :account
|
||||
AND attachments != '[]'
|
||||
AND (attachments LIKE '%"type":4%' OR attachments LIKE '%"type": 4%')
|
||||
AND (
|
||||
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>
|
||||
@@ -622,8 +641,14 @@ interface MessageDao {
|
||||
"""
|
||||
DELETE FROM messages
|
||||
WHERE account = :account
|
||||
AND attachments != '[]'
|
||||
AND (attachments LIKE '%"type":4%' OR attachments LIKE '%"type": 4%')
|
||||
AND (
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
AccountSyncTimeEntity::class,
|
||||
GroupEntity::class,
|
||||
PinnedMessageEntity::class],
|
||||
version = 14,
|
||||
version = 15,
|
||||
exportSchema = false
|
||||
)
|
||||
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 {
|
||||
return INSTANCE
|
||||
?: synchronized(this) {
|
||||
@@ -224,7 +254,8 @@ abstract class RosettaDatabase : RoomDatabase() {
|
||||
MIGRATION_10_11,
|
||||
MIGRATION_11_12,
|
||||
MIGRATION_12_13,
|
||||
MIGRATION_13_14
|
||||
MIGRATION_13_14,
|
||||
MIGRATION_14_15
|
||||
)
|
||||
.fallbackToDestructiveMigration() // Для разработки - только
|
||||
// если миграция не
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.rosetta.messenger.network
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.util.Log
|
||||
import com.rosetta.messenger.BuildConfig
|
||||
import com.rosetta.messenger.data.MessageRepository
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
@@ -872,13 +873,15 @@ object CallManager {
|
||||
}
|
||||
sharedKeyBytes = keyBytes.copyOf(32)
|
||||
breadcrumb("KE: shared key fp=${sharedKeyBytes?.fingerprintHex(8)}")
|
||||
// Open native diagnostics file for frame-level logging
|
||||
try {
|
||||
val dir = java.io.File(appContext!!.filesDir, "crash_reports")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath
|
||||
XChaCha20E2EE.nativeOpenDiagFile(diagPath)
|
||||
} catch (_: Throwable) {}
|
||||
// Frame-level diagnostics are enabled only for debug builds.
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
val dir = java.io.File(appContext!!.filesDir, "crash_reports")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath
|
||||
XChaCha20E2EE.nativeOpenDiagFile(diagPath)
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
// If sender track already exists, bind encryptor now.
|
||||
val existingSender =
|
||||
pendingAudioSenderForE2ee
|
||||
|
||||
@@ -4641,6 +4641,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
encryptedPlainMessage, // 🔒 Зашифрованный текст для хранения в
|
||||
// БД
|
||||
attachments = attachmentsJson,
|
||||
primaryAttachmentType =
|
||||
resolvePrimaryAttachmentTypeFromJson(attachmentsJson),
|
||||
replyToMessageId = null,
|
||||
dialogKey = dialogKey
|
||||
)
|
||||
@@ -4649,6 +4651,18 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
} 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() {
|
||||
_opponentTyping.value = true
|
||||
// Отменяем предыдущий таймер, чтобы избежать race condition
|
||||
|
||||
@@ -666,6 +666,7 @@ fun ChatsListScreen(
|
||||
|
||||
// Requests count for badge on hamburger & sidebar
|
||||
val topLevelChatsState by chatsViewModel.chatsState.collectAsState()
|
||||
val topLevelIsLoading by chatsViewModel.isLoading.collectAsState()
|
||||
val topLevelRequestsCount = topLevelChatsState.requestsCount
|
||||
|
||||
// Dev console dialog - commented out for now
|
||||
@@ -1934,8 +1935,8 @@ fun ChatsListScreen(
|
||||
// Это предотвращает "дергание" UI когда dialogs и requests
|
||||
// обновляются
|
||||
// независимо
|
||||
val chatsState by chatsViewModel.chatsState.collectAsState()
|
||||
val isLoading by chatsViewModel.isLoading.collectAsState()
|
||||
val chatsState = topLevelChatsState
|
||||
val isLoading = topLevelIsLoading
|
||||
val requests = chatsState.requests
|
||||
val requestsCount = chatsState.requestsCount
|
||||
|
||||
@@ -2061,64 +2062,15 @@ fun ChatsListScreen(
|
||||
label = "CallsTransition"
|
||||
) { isCallsScreen ->
|
||||
if (isCallsScreen) {
|
||||
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) {
|
||||
setInlineCallsVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
CallsHistoryScreen(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountPublicKey = accountPublicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
onOpenChat = onUserSelect,
|
||||
onStartCall = onStartCall,
|
||||
onStartNewCall = onSearchClick,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
CallsRouteContent(
|
||||
isDarkTheme = isDarkTheme,
|
||||
accountPublicKey = accountPublicKey,
|
||||
avatarRepository = avatarRepository,
|
||||
onUserSelect = onUserSelect,
|
||||
onStartCall = onStartCall,
|
||||
onStartNewCall = onSearchClick,
|
||||
onBack = { setInlineCallsVisible(false) }
|
||||
)
|
||||
} else {
|
||||
// 🎬 Animated content transition between main list and
|
||||
// requests
|
||||
@@ -2154,110 +2106,42 @@ fun ChatsListScreen(
|
||||
label = "RequestsTransition"
|
||||
) { isRequestsScreen ->
|
||||
if (isRequestsScreen) {
|
||||
// 📬 Show Requests Screen with swipe-back
|
||||
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(
|
||||
RequestsRouteContent(
|
||||
requests = requests,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onBack = {
|
||||
setInlineRequestsVisible(
|
||||
false
|
||||
)
|
||||
},
|
||||
onRequestClick = { request ->
|
||||
val user =
|
||||
chatsViewModel
|
||||
.dialogToSearchUser(
|
||||
request
|
||||
)
|
||||
onUserSelect(user)
|
||||
},
|
||||
avatarRepository =
|
||||
avatarRepository,
|
||||
avatarRepository = avatarRepository,
|
||||
blockedUsers = blockedUsers,
|
||||
pinnedChats = pinnedChats,
|
||||
isDrawerOpen =
|
||||
drawerState.isOpen ||
|
||||
drawerState
|
||||
.isAnimationRunning,
|
||||
onTogglePin = { opponentKey ->
|
||||
onTogglePin(opponentKey)
|
||||
},
|
||||
drawerState.isAnimationRunning,
|
||||
onTogglePin = onTogglePin,
|
||||
onDeleteDialog = { opponentKey ->
|
||||
scope.launch {
|
||||
chatsViewModel
|
||||
.deleteDialog(
|
||||
opponentKey
|
||||
)
|
||||
chatsViewModel.deleteDialog(opponentKey)
|
||||
}
|
||||
},
|
||||
onBlockUser = { opponentKey ->
|
||||
scope.launch {
|
||||
chatsViewModel
|
||||
.blockUser(
|
||||
opponentKey
|
||||
)
|
||||
chatsViewModel.blockUser(opponentKey)
|
||||
}
|
||||
},
|
||||
onUnblockUser = { opponentKey ->
|
||||
scope.launch {
|
||||
chatsViewModel
|
||||
.unblockUser(
|
||||
opponentKey
|
||||
)
|
||||
chatsViewModel.unblockUser(opponentKey)
|
||||
}
|
||||
},
|
||||
onRequestClick = { request ->
|
||||
val user =
|
||||
chatsViewModel.dialogToSearchUser(
|
||||
request
|
||||
)
|
||||
onUserSelect(user)
|
||||
},
|
||||
onBack = {
|
||||
setInlineRequestsVisible(false)
|
||||
}
|
||||
)
|
||||
} // Close Box wrapper
|
||||
} else if (showSkeleton) {
|
||||
ChatsListSkeleton(isDarkTheme = isDarkTheme)
|
||||
} 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 */
|
||||
@Composable
|
||||
fun RequestsSection(
|
||||
|
||||
Reference in New Issue
Block a user