feat(chat-input): привести lock flow записи ГС к Telegram (геометрия и анимации)

This commit is contained in:
2026-04-19 21:37:55 +05:00
parent 5e6d66b762
commit b32d8ed061
7 changed files with 741 additions and 325 deletions

View File

@@ -27,10 +27,8 @@
<application
android:name=".RosettaApplication"
android:allowBackup="true"
android:allowBackup="false"
android:enableOnBackInvokedCallback="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"

View File

@@ -1,6 +1,7 @@
package com.rosetta.messenger.data
import android.content.Context
import com.rosetta.messenger.BuildConfig
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.crypto.MessageCrypto
import com.rosetta.messenger.database.*
@@ -266,7 +267,7 @@ class MessageRepository @Inject constructor(
try {
CryptoManager.encryptWithPassword(messageText, privateKey)
} catch (e: Exception) {
android.util.Log.e("ReleaseNotes", "❌ encryptWithPassword failed", e)
if (BuildConfig.DEBUG) android.util.Log.e("ReleaseNotes", "❌ encryptWithPassword failed", e)
return null
}
@@ -351,12 +352,12 @@ class MessageRepository @Inject constructor(
suspend fun checkAndSendVersionUpdateMessage() {
val account = currentAccount
if (account == null) {
android.util.Log.w("ReleaseNotes", "❌ currentAccount is null, skipping update message")
if (BuildConfig.DEBUG) android.util.Log.w("ReleaseNotes", "❌ currentAccount is null, skipping update message")
return
}
val privateKey = currentPrivateKey
if (privateKey == null) {
android.util.Log.w("ReleaseNotes", "❌ currentPrivateKey is null, skipping update message")
if (BuildConfig.DEBUG) android.util.Log.w("ReleaseNotes", "❌ currentPrivateKey is null, skipping update message")
return
}
val prefs = context.getSharedPreferences("rosetta_system_${account}", Context.MODE_PRIVATE)
@@ -364,7 +365,7 @@ class MessageRepository @Inject constructor(
val currentVersion = com.rosetta.messenger.BuildConfig.VERSION_NAME
val currentKey = "${currentVersion}_${ReleaseNotes.noticeHash}"
android.util.Log.d("ReleaseNotes", "checkUpdate: version=$currentVersion, lastKey=$lastNoticeKey, currentKey=$currentKey, match=${lastNoticeKey == currentKey}")
if (BuildConfig.DEBUG) android.util.Log.d("ReleaseNotes", "checkUpdate: version=$currentVersion, lastKey=$lastNoticeKey, currentKey=$currentKey, match=${lastNoticeKey == currentKey}")
if (lastNoticeKey != currentKey) {
// Delete the previous message for this version (if any)
@@ -375,15 +376,15 @@ class MessageRepository @Inject constructor(
}
val messageId = addUpdateSystemMessage(ReleaseNotes.getNotice(currentVersion))
android.util.Log.d("ReleaseNotes", "addUpdateSystemMessage result: messageId=$messageId")
if (BuildConfig.DEBUG) android.util.Log.d("ReleaseNotes", "addUpdateSystemMessage result: messageId=$messageId")
if (messageId != null) {
prefs.edit()
.putString("lastNoticeKey", currentKey)
.putString("lastNoticeMessageId_$currentVersion", messageId)
.apply()
android.util.Log.d("ReleaseNotes", "✅ Update message saved successfully")
if (BuildConfig.DEBUG) android.util.Log.d("ReleaseNotes", "✅ Update message saved successfully")
} else {
android.util.Log.e("ReleaseNotes", "❌ Failed to create update message")
if (BuildConfig.DEBUG) android.util.Log.e("ReleaseNotes", "❌ Failed to create update message")
}
}
}
@@ -881,7 +882,7 @@ class MessageRepository @Inject constructor(
MessageCrypto.decryptIncoming(packet.content, packet.chachaKey, privateKey)
} catch (e: Exception) {
// Fallback: если дешифровка не удалась (напр. CALL с encrypted empty content)
android.util.Log.w("MessageRepository", "Decryption fallback for ${messageId.take(8)}: ${e.message}")
if (BuildConfig.DEBUG) android.util.Log.w("MessageRepository", "Decryption fallback for ${messageId.take(8)}: ${e.message}")
""
}
}
@@ -1326,7 +1327,7 @@ class MessageRepository @Inject constructor(
syncedOpponentsWithWrongStatus.forEach { opponentKey ->
runCatching { dialogDao.updateDialogFromMessages(account, opponentKey) }
}
android.util.Log.i(
if (BuildConfig.DEBUG) android.util.Log.i(
"MessageRepository",
"✅ Normalized $normalizedSyncedCount synced own messages to DELIVERED"
)
@@ -1335,14 +1336,14 @@ class MessageRepository @Inject constructor(
// Mark expired messages as ERROR (older than 80 seconds)
val expiredCount = messageDao.markExpiredWaitingAsError(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
if (expiredCount > 0) {
android.util.Log.w("MessageRepository", "⚠️ Marked $expiredCount expired WAITING messages as ERROR")
if (BuildConfig.DEBUG) android.util.Log.w("MessageRepository", "⚠️ Marked $expiredCount expired WAITING messages as ERROR")
}
// Get remaining WAITING messages (younger than 80s)
val waitingMessages = messageDao.getWaitingMessages(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
if (waitingMessages.isEmpty()) return
android.util.Log.i("MessageRepository", "🔄 Retrying ${waitingMessages.size} WAITING messages")
if (BuildConfig.DEBUG) android.util.Log.i("MessageRepository", "🔄 Retrying ${waitingMessages.size} WAITING messages")
for (entity in waitingMessages) {
// Skip saved messages (should not happen, but guard)
@@ -1366,7 +1367,7 @@ class MessageRepository @Inject constructor(
privateKey
)
} catch (e: Exception) {
android.util.Log.w("MessageRepository", "⚠️ Cannot regenerate aesChachaKey for ${entity.messageId.take(8)}, sending without it")
if (BuildConfig.DEBUG) android.util.Log.w("MessageRepository", "⚠️ Cannot regenerate aesChachaKey for ${entity.messageId.take(8)}, sending without it")
""
}
}
@@ -1393,9 +1394,9 @@ class MessageRepository @Inject constructor(
// iOS parity: use retry mechanism for reconnect-resent messages too
protocolClient.sendMessageWithRetry(packet)
android.util.Log.d("MessageRepository", "🔄 Resent WAITING message: ${entity.messageId.take(8)}")
if (BuildConfig.DEBUG) android.util.Log.d("MessageRepository", "🔄 Resent WAITING message: ${entity.messageId.take(8)}")
} catch (e: Exception) {
android.util.Log.e("MessageRepository", "❌ Failed to retry message ${entity.messageId.take(8)}: ${e.message}")
if (BuildConfig.DEBUG) android.util.Log.e("MessageRepository", "❌ Failed to retry message ${entity.messageId.take(8)}: ${e.message}")
// Mark as ERROR if retry fails
messageDao.updateDeliveryStatus(account, entity.messageId, DeliveryStatus.ERROR.value)
val dialogKey = getDialogKey(entity.toPublicKey)

View File

@@ -10,6 +10,7 @@ import android.graphics.BitmapFactory
import android.os.Build
import android.util.Base64
import android.util.Log
import com.rosetta.messenger.BuildConfig
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import com.google.firebase.messaging.FirebaseMessagingService
@@ -136,7 +137,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
/** Вызывается когда получено push-уведомление */
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
Log.d(TAG, "\u2709\ufe0f onMessageReceived: messageId=${remoteMessage.messageId} from=${remoteMessage.from} data=${remoteMessage.data} notif=${remoteMessage.notification?.body}")
if (BuildConfig.DEBUG) Log.d(TAG, "\u2709\ufe0f onMessageReceived: messageId=${remoteMessage.messageId} from=${remoteMessage.from} data=${remoteMessage.data} notif=${remoteMessage.notification?.body}")
val data = remoteMessage.data
val notificationTitle = remoteMessage.notification?.title?.trim().orEmpty()
@@ -153,7 +154,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
val hasNotificationContent = notificationTitle.isNotBlank() || notificationBody.isNotBlank()
if (!hasDataContent && !hasNotificationContent) {
Log.d(TAG, "Silent/empty push ignored (iOS wake-up push)")
if (BuildConfig.DEBUG) Log.d(TAG, "Silent/empty push ignored (iOS wake-up push)")
// Still trigger reconnect if WebSocket is disconnected
protocolGateway.reconnectNowIfNeeded("silent_push")
return
@@ -226,14 +227,14 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
isReadEvent -> {
val keysToClear = collectReadDialogKeys(data, dialogKey, senderPublicKey)
if (keysToClear.isEmpty()) {
Log.d(TAG, "READ push received but no dialog key in payload: $data")
if (BuildConfig.DEBUG) Log.d(TAG, "READ push received but no dialog key in payload: $data")
} else {
keysToClear.forEach { key ->
cancelNotificationForChat(applicationContext, key)
}
val titleHints = collectReadTitleHints(data, keysToClear)
cancelMatchingActiveNotifications(keysToClear, titleHints)
Log.d(
if (BuildConfig.DEBUG) Log.d(
TAG,
"READ push cleared notifications for keys=$keysToClear titles=$titleHints"
)
@@ -317,11 +318,11 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
val now = System.currentTimeMillis()
val lastTs = lastNotifTimestamps[dedupKey]
if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) {
Log.d(TAG, "\ud83d\udeab Dedup BLOCKED notification for key=$dedupKey, delta=${now - lastTs}ms")
if (BuildConfig.DEBUG) Log.d(TAG, "\ud83d\udeab Dedup BLOCKED notification for key=$dedupKey, delta=${now - lastTs}ms")
return // duplicate push — skip
}
lastNotifTimestamps[dedupKey] = now
Log.d(TAG, "\u2705 Showing notification for key=$dedupKey")
if (BuildConfig.DEBUG) Log.d(TAG, "\u2705 Showing notification for key=$dedupKey")
val senderKey = senderPublicKey?.trim().orEmpty()
if (senderKey.isNotEmpty() && isDialogMuted(senderKey)) {
return
@@ -508,7 +509,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
}
private fun pushCallLog(msg: String) {
Log.d(TAG, msg)
if (BuildConfig.DEBUG) Log.d(TAG, msg)
try {
val dir = java.io.File(applicationContext.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
@@ -534,7 +535,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
pushCallLog("wakeProtocolFromPush: authRestore=$restored account=${account.take(8)}")
protocolGateway.reconnectNowIfNeeded("push_$reason")
}.onFailure { error ->
Log.w(TAG, "wakeProtocolFromPush failed: ${error.message}")
if (BuildConfig.DEBUG) Log.w(TAG, "wakeProtocolFromPush failed: ${error.message}")
}
}
@@ -717,7 +718,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
if (matchesDeterministicId || matchesDialogKey || matchesHint) {
manager.cancel(sbn.tag, sbn.id)
Log.d(
if (BuildConfig.DEBUG) Log.d(
TAG,
"READ push fallback cancel id=${sbn.id} tag=${sbn.tag} " +
"channel=${notification.channelId} title='$title' " +
@@ -726,7 +727,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
}
}
}.onFailure { error ->
Log.w(TAG, "cancelMatchingActiveNotifications failed: ${error.message}")
if (BuildConfig.DEBUG) Log.w(TAG, "cancelMatchingActiveNotifications failed: ${error.message}")
}
}

View File

@@ -2471,7 +2471,9 @@ fun CallAttachment(
text = callUi.subtitle,
fontSize = 12.sp,
color =
if (callUi.isError) {
if (callUi.isError && isOutgoing) {
Color.White.copy(alpha = 0.72f)
} else if (callUi.isError) {
Color(0xFFE55A5A)
} else if (isOutgoing) {
Color.White.copy(alpha = 0.72f)

View File

@@ -563,7 +563,6 @@ fun MessageBubble(
Modifier.fillMaxWidth().pointerInput(isSystemSafeChat, textSelectionHelper?.isActive, isVoiceWaveGestureActive) {
if (isSystemSafeChat) return@pointerInput
if (textSelectionHelper?.isActive == true) return@pointerInput
if (hasVoiceAttachmentForGesture) return@pointerInput
if (isVoiceWaveGestureActive) return@pointerInput
// 🔥 Простой горизонтальный свайп для reply
// Используем detectHorizontalDragGestures который лучше работает со