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

@@ -32,8 +32,7 @@ flowchart TB
end
subgraph DI["Hilt Singleton Graph"]
D1["ProtocolGateway"]
D1A["ProtocolRuntime"]
D1["ProtocolGateway -> ProtocolRuntime"]
D2["SessionCoordinator"]
D3["IdentityGateway"]
D4["AccountManager / PreferencesManager"]
@@ -42,54 +41,57 @@ flowchart TB
subgraph CHAT_UI["Chat UI Orchestration"]
C1["ChatDetailScreen / ChatsListScreen"]
C2["ChatViewModel (host)"]
C3["Messages/Voice/Attachments/Typing ViewModel"]
C4["Messages/Forward/Attachments Coordinator"]
C2["ChatViewModel (host-state)"]
C3["Feature VM: Messages/Voice/Attachments/Typing"]
C4["Coordinators: Messages/Forward/Attachments"]
end
subgraph CHAT_DOMAIN["Chat Domain UseCases"]
U1["SendText / SendMedia / SendForward"]
U2["SendVoice / SendTyping / SendReadReceipt"]
U3["CreateAttachment / EncryptAndUpload"]
U3["CreateAttachment / EncryptAndUpload / VideoCircle"]
end
subgraph SESSION["Session / Identity Runtime"]
S1["SessionStore"]
S2["SessionReducer"]
S3["IdentityStore"]
S4["AppSessionCoordinator"]
S1["SessionStore / SessionReducer"]
S2["IdentityStore / AppSessionCoordinator"]
end
subgraph NET["Network Runtime"]
N0["ProtocolRuntime"]
N1C["RuntimeComposition"]
N1A["ProtocolManager (compat facade)"]
N2["Protocol"]
N3["PacketSubscriptionRegistry"]
N4["ReadyPacketGate"]
N1["RuntimeComposition (wiring only)"]
N2["RuntimeConnectionControlFacade"]
N3["RuntimeDirectoryFacade"]
N4["RuntimePacketIoFacade"]
N5["Assemblies: Transport / Messaging / State / Routing"]
N6["ProtocolInstanceManager -> Protocol"]
N7["ProtocolManager (legacy compat)"]
end
subgraph DATA["Data + Persistence"]
R1["MessageRepository"]
R2["GroupRepository"]
R3["Room: RosettaDatabase"]
R1["MessageRepository / GroupRepository"]
R2["Room: RosettaDatabase"]
end
ENTRY --> DI
DI --> SESSION
DI --> NET
DI --> DATA
DI --> CHAT_UI
DI --> N0
CHAT_UI --> CHAT_DOMAIN
CHAT_UI --> DATA
CHAT_UI --> R1
CHAT_DOMAIN --> D1
D1 --> D1A
D1A --> N1C
N1A --> N1C
SESSION --> NET
DATA --> NET
DATA --> R3
N1C --> N2
D1 --> N0
N0 --> N1
N1 --> N2
N1 --> N3
N1 --> N4
N1 --> N5
N5 --> N6
N7 --> N0
SESSION --> N0
R1 --> N0
R1 --> R2
```
---
@@ -216,41 +218,40 @@ stateDiagram-v2
На hot-path `ProtocolRuntime` берет runtime API (`RuntimeConnectionControlFacade`/`RuntimeDirectoryFacade`/`RuntimePacketIoFacade`) напрямую из `RuntimeComposition`, поэтому лишний proxy-hop через публичные методы composition не используется.
```mermaid
flowchart TB
flowchart TB
PR["ProtocolRuntime (ProtocolGateway impl)"] --> RC["RuntimeComposition"]
RC --> RCC["RuntimeConnectionControlFacade"]
RC --> RDF["RuntimeDirectoryFacade"]
RC --> RPF["RuntimePacketIoFacade"]
RC --> CO["ConnectionOrchestrator"]
RC --> PIM["ProtocolInstanceManager"]
RC --> RLSM["RuntimeLifecycleStateMachine"]
RC --> RIC["RuntimeInitializationCoordinator"]
RC --> PLSS["ProtocolLifecycleStateStoreImpl"]
RC --> OPFT["OwnProfileFallbackTimerService"]
RC --> ARS["AuthRestoreService"]
RC --> RSC["RuntimeShutdownCoordinator"]
RC --> CER["ConnectionEventRouter"]
RC --> NCF["NetworkConnectivityFacade"]
RC --> PLC["ProtocolLifecycleCoordinator"]
RC --> PAC["ProtocolAccountSessionCoordinator"]
RC --> RPDC["ReadyPacketDispatchCoordinator"]
RC --> PABC["ProtocolPostAuthBootstrapCoordinator"]
RC --> BC["BootstrapCoordinator"]
RC --> SC["SyncCoordinator"]
RC --> PT["PresenceTypingService"]
RC --> PR["PacketRouter"]
RC --> OPS["OwnProfileSyncService"]
RC --> RQ["RetryQueueService"]
RC --> ABC["AuthBootstrapCoordinator"]
RC --> NRW["NetworkReconnectWatcher"]
RC --> DVS["DeviceVerificationService"]
RC --> CSB["CallSignalBridge"]
RC --> PSF["PacketSubscriptionFacade"]
RC --> PSR["PacketSubscriptionRegistry"]
RC --> IPR["InboundPacketHandlerRegistrar"]
RC --> IQ["InboundTaskQueueService"]
RC --> SUP["ProtocolConnectionSupervisor"]
RC --> RPG["ReadyPacketGate"]
RC --> RTA["RuntimeTransportAssembly"]
RC --> RMA["RuntimeMessagingAssembly"]
RC --> RSA["RuntimeStateAssembly"]
RC --> RRA["RuntimeRoutingAssembly"]
RTA --> PIM["ProtocolInstanceManager"]
RTA --> PSF["PacketSubscriptionFacade"]
RTA --> NCF["NetworkConnectivityFacade"]
RMA --> SC["SyncCoordinator"]
RMA --> PROUTER["PacketRouter"]
RMA --> OMPS["OutgoingMessagePipelineService"]
RMA --> CSB["CallSignalBridge"]
RMA --> IPR["InboundPacketHandlerRegistrar"]
RSA --> RLSM["RuntimeLifecycleStateMachine"]
RSA --> BC["BootstrapCoordinator"]
RSA --> RPG["ReadyPacketGate"]
RSA --> PLSS["ProtocolLifecycleStateStoreImpl"]
RRA --> SUP["ProtocolConnectionSupervisor"]
RRA --> CER["ConnectionEventRouter"]
CER --> CO["ConnectionOrchestrator"]
CER --> PLC["ProtocolLifecycleCoordinator"]
CER --> PAC["ProtocolAccountSessionCoordinator"]
CER --> RPDC["ReadyPacketDispatchCoordinator"]
PIM --> P["Protocol (WebSocket + packet codec)"]
```
@@ -267,12 +268,16 @@ stateDiagram-v2
```mermaid
sequenceDiagram
participant Feature as Feature/Service
participant PM as Runtime API (Core/Facade)
participant PR as ProtocolRuntime
participant RPF as RuntimePacketIoFacade
participant PSF as PacketSubscriptionFacade
participant REG as PacketSubscriptionRegistry
participant P as Protocol
Feature->>PM: waitPacket(0x03, callback)
PM->>REG: addCallback(0x03, callback)
Feature->>PR: waitPacket(0x03, callback)
PR->>RPF: waitPacket(0x03, callback)
RPF->>PSF: waitPacket(0x03, callback)
PSF->>REG: addCallback(0x03, callback)
REG->>P: waitPacket(0x03, protocolBridge) [once per packetId]
P-->>REG: Packet(0x03)
@@ -319,12 +324,14 @@ flowchart LR
CVM --> COORD["Messages/Forward/Attachments Coordinator"]
CVM --> UC["domain/chats/usecase/*"]
COORD --> UC
UC --> GW["ProtocolGateway.sendMessageWithRetry"]
UC --> GW["ProtocolGateway.send / sendMessageWithRetry"]
GW --> PR["ProtocolRuntime"]
PR --> RC["RuntimeComposition"]
RC --> RQ["RetryQueueService"]
RC --> RG["ReadyPacketGate"]
RC --> P["Protocol.sendPacket"]
PR --> RPF["RuntimePacketIoFacade"]
RPF --> OMP["OutgoingMessagePipelineService"]
OMP --> RQ["RetryQueueService"]
OMP --> RR["RuntimeRoutingAssembly"]
RR --> RG["ReadyPacketGate / ReadyPacketDispatchCoordinator"]
RG --> P["Protocol.sendPacket"]
```
### 7.2 Декомпозиция ChatViewModel (host + feature/coordinator слой)
@@ -351,17 +358,19 @@ flowchart TB
CD --> TVM["TypingViewModel"]
CD --> VVM["VoiceRecordingViewModel"]
CD --> AVM["AttachmentsViewModel"]
MVM --> CVM["ChatViewModel"]
MVM --> CVM["ChatViewModel (host-state)"]
TVM --> CVM
VVM --> CVM
AVM --> CVM
CVM --> MCO["MessagesCoordinator"]
CVM --> FCO["ForwardCoordinator"]
CVM --> ACO["AttachmentsCoordinator"]
AVM --> AFCO["AttachmentsFeatureCoordinator"]
CVM --> U["domain/chats/usecase/*"]
MCO --> U
FCO --> U
ACO --> U
AFCO --> U
```
Важно: после вынесения `MessagesCoordinator`, `ForwardCoordinator` и `AttachmentsCoordinator` `ChatViewModel` выступает как host-state и bridge для feature/coordinator подсистем.
@@ -388,7 +397,10 @@ sequenceDiagram
participant SC as SessionCoordinatorImpl
participant SS as SessionStore
participant PG as ProtocolGateway
participant RC as RuntimeComposition
participant PR as ProtocolRuntime
participant RCC as RuntimeConnectionControlFacade
participant RRA as RuntimeRoutingAssembly
participant RSA as RuntimeStateAssembly
participant AM as AccountManager
UI->>SC: bootstrapAuthenticatedSession(account, reason)
@@ -400,9 +412,12 @@ sequenceDiagram
SC->>AM: setCurrentAccount(public)
SC->>SS: dispatch(Ready)
RC-->>RC: HANDSHAKE -> AUTHENTICATED -> BOOTSTRAPPING
RC-->>RC: SyncCompleted + OwnProfileResolved
RC-->>RC: connectionLifecycleState = READY
PG->>PR: runtime API calls
PR->>RCC: connection/auth commands
RCC->>RRA: post(ConnectionEvent.*)
RRA-->>RRA: Supervisor + Router route events
RRA-->>RSA: apply lifecycle transitions
RSA-->>RSA: AUTHENTICATED -> BOOTSTRAPPING -> READY
```
Важно: `SessionState.Ready` (app-session готова) и `connectionLifecycleState = READY` (сеть готова) — это разные state-модели.
@@ -429,6 +444,9 @@ stateDiagram-v2
HANDSHAKING --> AUTHENTICATED
AUTHENTICATED --> BOOTSTRAPPING
BOOTSTRAPPING --> READY
READY --> HANDSHAKING
AUTHENTICATED --> DISCONNECTED
BOOTSTRAPPING --> DISCONNECTED
READY --> DISCONNECTED
DEVICE_VERIFICATION_REQUIRED --> CONNECTING
```
@@ -519,13 +537,12 @@ stateDiagram-v2
## 11. Что осталось как технический долг
Актуальные открытые хвосты:
- `RuntimeComposition` остается composition-root, но уже существенно сжат (около 501 строки) после выноса `RuntimeTransportAssembly`, `RuntimeMessagingAssembly`, `RuntimeStateAssembly`, `RuntimeRoutingAssembly` и удаления публичных proxy-методов; следующий шаг — перенос части lifecycle/orchestration helper-кода в отдельные domain-oriented service/adapters.
- `ProtocolRuntime` и `ProtocolRuntimePort` все еще имеют широкий proxy-surface; нужен audit методов и дальнейшее сужение публичного API по use-case группам.
- `ChatViewModel` остается крупным host-классом (state + bridge/proxy API к feature/coordinator/use-case слоям).
- High-level media сценарии теперь в `AttachmentsFeatureCoordinator`, но остаются крупными и требуют дальнейшей декомпозиции на более узкие coordinator/service/use-case блоки.
- Тестовое покрытие архитектурных слоев все еще недостаточно:
- сейчас в `app/src/test` всего 7 unit-тестов (в основном crypto/data/helpers), в `app/src/androidTest` — 1 тест;
- не покрыты network runtime/coordinator слои (`RuntimeComposition`, `ConnectionEventRouter`, `ProtocolLifecycle*`, `ReadyPacketDispatchCoordinator`) и chat orchestration (`Messages/Forward/Attachments*`).
- `RuntimeComposition` остается composition-root (около 501 строки): публичные proxy-методы уже убраны, но внутри все еще смешаны wiring и часть helper-логики (`setupStateMonitoring`, event-bridge, log helpers). Следующий шаг: вынести эти helper-блоки в отдельные adapters/services.
- `ProtocolRuntime` + `ProtocolRuntimePort` все еще имеют широкий API surface (connection + directory + packet IO + call signaling + debug). Нужен audit и сужение публичных контрактов по use-case группам.
- `ChatViewModel` остается очень крупным host-классом (около 4391 строки) с большим bridge/proxy surface к feature/coordinator/use-case слоям.
- `AttachmentsFeatureCoordinator` остается крупным (около 761 строки): high-level media сценарии стоит резать на более узкие upload/transform/packet-assembly сервисы.
- Тестовое покрытие архитектурно-критичных слоев недостаточно: `app/src/test` = 7, `app/src/androidTest` = 1; не покрыты runtime-routing/lifecycle компоненты (`RuntimeRoutingAssembly`, `ConnectionEventRouter`, `ProtocolLifecycle*`, `ReadyPacketDispatchCoordinator`) и chat coordinators (`Messages/Forward/Attachments*`).
- В runtime все еще несколько точек входа (`ProtocolRuntime`, `ProtocolRuntimeAccess`, `ProtocolManager` legacy), что повышает cognitive load; целевой шаг — дальнейшее сокращение legacy/static call-sites.
Уже закрыто и больше не считается техдолгом:
- `UiDependencyAccess.get(...)` удален из `ui/*`.

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 который лучше работает со