feat(chat-input): привести lock flow записи ГС к Telegram (геометрия и анимации)
This commit is contained in:
173
Architecture.md
173
Architecture.md
@@ -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/*`.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 который лучше работает со
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user