feat(chat-input): привести lock flow записи ГС к Telegram (геометрия и анимации)
This commit is contained in:
173
Architecture.md
173
Architecture.md
@@ -32,8 +32,7 @@ flowchart TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph DI["Hilt Singleton Graph"]
|
subgraph DI["Hilt Singleton Graph"]
|
||||||
D1["ProtocolGateway"]
|
D1["ProtocolGateway -> ProtocolRuntime"]
|
||||||
D1A["ProtocolRuntime"]
|
|
||||||
D2["SessionCoordinator"]
|
D2["SessionCoordinator"]
|
||||||
D3["IdentityGateway"]
|
D3["IdentityGateway"]
|
||||||
D4["AccountManager / PreferencesManager"]
|
D4["AccountManager / PreferencesManager"]
|
||||||
@@ -42,54 +41,57 @@ flowchart TB
|
|||||||
|
|
||||||
subgraph CHAT_UI["Chat UI Orchestration"]
|
subgraph CHAT_UI["Chat UI Orchestration"]
|
||||||
C1["ChatDetailScreen / ChatsListScreen"]
|
C1["ChatDetailScreen / ChatsListScreen"]
|
||||||
C2["ChatViewModel (host)"]
|
C2["ChatViewModel (host-state)"]
|
||||||
C3["Messages/Voice/Attachments/Typing ViewModel"]
|
C3["Feature VM: Messages/Voice/Attachments/Typing"]
|
||||||
C4["Messages/Forward/Attachments Coordinator"]
|
C4["Coordinators: Messages/Forward/Attachments"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph CHAT_DOMAIN["Chat Domain UseCases"]
|
subgraph CHAT_DOMAIN["Chat Domain UseCases"]
|
||||||
U1["SendText / SendMedia / SendForward"]
|
U1["SendText / SendMedia / SendForward"]
|
||||||
U2["SendVoice / SendTyping / SendReadReceipt"]
|
U2["SendVoice / SendTyping / SendReadReceipt"]
|
||||||
U3["CreateAttachment / EncryptAndUpload"]
|
U3["CreateAttachment / EncryptAndUpload / VideoCircle"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph SESSION["Session / Identity Runtime"]
|
subgraph SESSION["Session / Identity Runtime"]
|
||||||
S1["SessionStore"]
|
S1["SessionStore / SessionReducer"]
|
||||||
S2["SessionReducer"]
|
S2["IdentityStore / AppSessionCoordinator"]
|
||||||
S3["IdentityStore"]
|
|
||||||
S4["AppSessionCoordinator"]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph NET["Network Runtime"]
|
subgraph NET["Network Runtime"]
|
||||||
N0["ProtocolRuntime"]
|
N0["ProtocolRuntime"]
|
||||||
N1C["RuntimeComposition"]
|
N1["RuntimeComposition (wiring only)"]
|
||||||
N1A["ProtocolManager (compat facade)"]
|
N2["RuntimeConnectionControlFacade"]
|
||||||
N2["Protocol"]
|
N3["RuntimeDirectoryFacade"]
|
||||||
N3["PacketSubscriptionRegistry"]
|
N4["RuntimePacketIoFacade"]
|
||||||
N4["ReadyPacketGate"]
|
N5["Assemblies: Transport / Messaging / State / Routing"]
|
||||||
|
N6["ProtocolInstanceManager -> Protocol"]
|
||||||
|
N7["ProtocolManager (legacy compat)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph DATA["Data + Persistence"]
|
subgraph DATA["Data + Persistence"]
|
||||||
R1["MessageRepository"]
|
R1["MessageRepository / GroupRepository"]
|
||||||
R2["GroupRepository"]
|
R2["Room: RosettaDatabase"]
|
||||||
R3["Room: RosettaDatabase"]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
ENTRY --> DI
|
ENTRY --> DI
|
||||||
DI --> SESSION
|
DI --> SESSION
|
||||||
DI --> NET
|
|
||||||
DI --> DATA
|
DI --> DATA
|
||||||
DI --> CHAT_UI
|
DI --> CHAT_UI
|
||||||
|
DI --> N0
|
||||||
CHAT_UI --> CHAT_DOMAIN
|
CHAT_UI --> CHAT_DOMAIN
|
||||||
CHAT_UI --> DATA
|
CHAT_UI --> R1
|
||||||
CHAT_DOMAIN --> D1
|
CHAT_DOMAIN --> D1
|
||||||
D1 --> D1A
|
D1 --> N0
|
||||||
D1A --> N1C
|
N0 --> N1
|
||||||
N1A --> N1C
|
N1 --> N2
|
||||||
SESSION --> NET
|
N1 --> N3
|
||||||
DATA --> NET
|
N1 --> N4
|
||||||
DATA --> R3
|
N1 --> N5
|
||||||
N1C --> N2
|
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 не используется.
|
На hot-path `ProtocolRuntime` берет runtime API (`RuntimeConnectionControlFacade`/`RuntimeDirectoryFacade`/`RuntimePacketIoFacade`) напрямую из `RuntimeComposition`, поэтому лишний proxy-hop через публичные методы composition не используется.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
PR["ProtocolRuntime (ProtocolGateway impl)"] --> RC["RuntimeComposition"]
|
PR["ProtocolRuntime (ProtocolGateway impl)"] --> RC["RuntimeComposition"]
|
||||||
RC --> RCC["RuntimeConnectionControlFacade"]
|
RC --> RCC["RuntimeConnectionControlFacade"]
|
||||||
RC --> RDF["RuntimeDirectoryFacade"]
|
RC --> RDF["RuntimeDirectoryFacade"]
|
||||||
RC --> RPF["RuntimePacketIoFacade"]
|
RC --> RPF["RuntimePacketIoFacade"]
|
||||||
RC --> CO["ConnectionOrchestrator"]
|
|
||||||
RC --> PIM["ProtocolInstanceManager"]
|
RC --> RTA["RuntimeTransportAssembly"]
|
||||||
RC --> RLSM["RuntimeLifecycleStateMachine"]
|
RC --> RMA["RuntimeMessagingAssembly"]
|
||||||
RC --> RIC["RuntimeInitializationCoordinator"]
|
RC --> RSA["RuntimeStateAssembly"]
|
||||||
RC --> PLSS["ProtocolLifecycleStateStoreImpl"]
|
RC --> RRA["RuntimeRoutingAssembly"]
|
||||||
RC --> OPFT["OwnProfileFallbackTimerService"]
|
|
||||||
RC --> ARS["AuthRestoreService"]
|
RTA --> PIM["ProtocolInstanceManager"]
|
||||||
RC --> RSC["RuntimeShutdownCoordinator"]
|
RTA --> PSF["PacketSubscriptionFacade"]
|
||||||
RC --> CER["ConnectionEventRouter"]
|
RTA --> NCF["NetworkConnectivityFacade"]
|
||||||
RC --> NCF["NetworkConnectivityFacade"]
|
|
||||||
RC --> PLC["ProtocolLifecycleCoordinator"]
|
RMA --> SC["SyncCoordinator"]
|
||||||
RC --> PAC["ProtocolAccountSessionCoordinator"]
|
RMA --> PROUTER["PacketRouter"]
|
||||||
RC --> RPDC["ReadyPacketDispatchCoordinator"]
|
RMA --> OMPS["OutgoingMessagePipelineService"]
|
||||||
RC --> PABC["ProtocolPostAuthBootstrapCoordinator"]
|
RMA --> CSB["CallSignalBridge"]
|
||||||
RC --> BC["BootstrapCoordinator"]
|
RMA --> IPR["InboundPacketHandlerRegistrar"]
|
||||||
RC --> SC["SyncCoordinator"]
|
|
||||||
RC --> PT["PresenceTypingService"]
|
RSA --> RLSM["RuntimeLifecycleStateMachine"]
|
||||||
RC --> PR["PacketRouter"]
|
RSA --> BC["BootstrapCoordinator"]
|
||||||
RC --> OPS["OwnProfileSyncService"]
|
RSA --> RPG["ReadyPacketGate"]
|
||||||
RC --> RQ["RetryQueueService"]
|
RSA --> PLSS["ProtocolLifecycleStateStoreImpl"]
|
||||||
RC --> ABC["AuthBootstrapCoordinator"]
|
|
||||||
RC --> NRW["NetworkReconnectWatcher"]
|
RRA --> SUP["ProtocolConnectionSupervisor"]
|
||||||
RC --> DVS["DeviceVerificationService"]
|
RRA --> CER["ConnectionEventRouter"]
|
||||||
RC --> CSB["CallSignalBridge"]
|
|
||||||
RC --> PSF["PacketSubscriptionFacade"]
|
CER --> CO["ConnectionOrchestrator"]
|
||||||
RC --> PSR["PacketSubscriptionRegistry"]
|
CER --> PLC["ProtocolLifecycleCoordinator"]
|
||||||
RC --> IPR["InboundPacketHandlerRegistrar"]
|
CER --> PAC["ProtocolAccountSessionCoordinator"]
|
||||||
RC --> IQ["InboundTaskQueueService"]
|
CER --> RPDC["ReadyPacketDispatchCoordinator"]
|
||||||
RC --> SUP["ProtocolConnectionSupervisor"]
|
|
||||||
RC --> RPG["ReadyPacketGate"]
|
|
||||||
PIM --> P["Protocol (WebSocket + packet codec)"]
|
PIM --> P["Protocol (WebSocket + packet codec)"]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -267,12 +268,16 @@ stateDiagram-v2
|
|||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Feature as Feature/Service
|
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 REG as PacketSubscriptionRegistry
|
||||||
participant P as Protocol
|
participant P as Protocol
|
||||||
|
|
||||||
Feature->>PM: waitPacket(0x03, callback)
|
Feature->>PR: waitPacket(0x03, callback)
|
||||||
PM->>REG: addCallback(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]
|
REG->>P: waitPacket(0x03, protocolBridge) [once per packetId]
|
||||||
|
|
||||||
P-->>REG: Packet(0x03)
|
P-->>REG: Packet(0x03)
|
||||||
@@ -319,12 +324,14 @@ flowchart LR
|
|||||||
CVM --> COORD["Messages/Forward/Attachments Coordinator"]
|
CVM --> COORD["Messages/Forward/Attachments Coordinator"]
|
||||||
CVM --> UC["domain/chats/usecase/*"]
|
CVM --> UC["domain/chats/usecase/*"]
|
||||||
COORD --> UC
|
COORD --> UC
|
||||||
UC --> GW["ProtocolGateway.sendMessageWithRetry"]
|
UC --> GW["ProtocolGateway.send / sendMessageWithRetry"]
|
||||||
GW --> PR["ProtocolRuntime"]
|
GW --> PR["ProtocolRuntime"]
|
||||||
PR --> RC["RuntimeComposition"]
|
PR --> RPF["RuntimePacketIoFacade"]
|
||||||
RC --> RQ["RetryQueueService"]
|
RPF --> OMP["OutgoingMessagePipelineService"]
|
||||||
RC --> RG["ReadyPacketGate"]
|
OMP --> RQ["RetryQueueService"]
|
||||||
RC --> P["Protocol.sendPacket"]
|
OMP --> RR["RuntimeRoutingAssembly"]
|
||||||
|
RR --> RG["ReadyPacketGate / ReadyPacketDispatchCoordinator"]
|
||||||
|
RG --> P["Protocol.sendPacket"]
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.2 Декомпозиция ChatViewModel (host + feature/coordinator слой)
|
### 7.2 Декомпозиция ChatViewModel (host + feature/coordinator слой)
|
||||||
@@ -351,17 +358,19 @@ flowchart TB
|
|||||||
CD --> TVM["TypingViewModel"]
|
CD --> TVM["TypingViewModel"]
|
||||||
CD --> VVM["VoiceRecordingViewModel"]
|
CD --> VVM["VoiceRecordingViewModel"]
|
||||||
CD --> AVM["AttachmentsViewModel"]
|
CD --> AVM["AttachmentsViewModel"]
|
||||||
MVM --> CVM["ChatViewModel"]
|
MVM --> CVM["ChatViewModel (host-state)"]
|
||||||
TVM --> CVM
|
TVM --> CVM
|
||||||
VVM --> CVM
|
VVM --> CVM
|
||||||
AVM --> CVM
|
AVM --> CVM
|
||||||
CVM --> MCO["MessagesCoordinator"]
|
CVM --> MCO["MessagesCoordinator"]
|
||||||
CVM --> FCO["ForwardCoordinator"]
|
CVM --> FCO["ForwardCoordinator"]
|
||||||
CVM --> ACO["AttachmentsCoordinator"]
|
CVM --> ACO["AttachmentsCoordinator"]
|
||||||
|
AVM --> AFCO["AttachmentsFeatureCoordinator"]
|
||||||
CVM --> U["domain/chats/usecase/*"]
|
CVM --> U["domain/chats/usecase/*"]
|
||||||
MCO --> U
|
MCO --> U
|
||||||
FCO --> U
|
FCO --> U
|
||||||
ACO --> U
|
ACO --> U
|
||||||
|
AFCO --> U
|
||||||
```
|
```
|
||||||
|
|
||||||
Важно: после вынесения `MessagesCoordinator`, `ForwardCoordinator` и `AttachmentsCoordinator` `ChatViewModel` выступает как host-state и bridge для feature/coordinator подсистем.
|
Важно: после вынесения `MessagesCoordinator`, `ForwardCoordinator` и `AttachmentsCoordinator` `ChatViewModel` выступает как host-state и bridge для feature/coordinator подсистем.
|
||||||
@@ -388,7 +397,10 @@ sequenceDiagram
|
|||||||
participant SC as SessionCoordinatorImpl
|
participant SC as SessionCoordinatorImpl
|
||||||
participant SS as SessionStore
|
participant SS as SessionStore
|
||||||
participant PG as ProtocolGateway
|
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
|
participant AM as AccountManager
|
||||||
|
|
||||||
UI->>SC: bootstrapAuthenticatedSession(account, reason)
|
UI->>SC: bootstrapAuthenticatedSession(account, reason)
|
||||||
@@ -400,9 +412,12 @@ sequenceDiagram
|
|||||||
SC->>AM: setCurrentAccount(public)
|
SC->>AM: setCurrentAccount(public)
|
||||||
SC->>SS: dispatch(Ready)
|
SC->>SS: dispatch(Ready)
|
||||||
|
|
||||||
RC-->>RC: HANDSHAKE -> AUTHENTICATED -> BOOTSTRAPPING
|
PG->>PR: runtime API calls
|
||||||
RC-->>RC: SyncCompleted + OwnProfileResolved
|
PR->>RCC: connection/auth commands
|
||||||
RC-->>RC: connectionLifecycleState = READY
|
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-модели.
|
Важно: `SessionState.Ready` (app-session готова) и `connectionLifecycleState = READY` (сеть готова) — это разные state-модели.
|
||||||
@@ -429,6 +444,9 @@ stateDiagram-v2
|
|||||||
HANDSHAKING --> AUTHENTICATED
|
HANDSHAKING --> AUTHENTICATED
|
||||||
AUTHENTICATED --> BOOTSTRAPPING
|
AUTHENTICATED --> BOOTSTRAPPING
|
||||||
BOOTSTRAPPING --> READY
|
BOOTSTRAPPING --> READY
|
||||||
|
READY --> HANDSHAKING
|
||||||
|
AUTHENTICATED --> DISCONNECTED
|
||||||
|
BOOTSTRAPPING --> DISCONNECTED
|
||||||
READY --> DISCONNECTED
|
READY --> DISCONNECTED
|
||||||
DEVICE_VERIFICATION_REQUIRED --> CONNECTING
|
DEVICE_VERIFICATION_REQUIRED --> CONNECTING
|
||||||
```
|
```
|
||||||
@@ -519,13 +537,12 @@ stateDiagram-v2
|
|||||||
## 11. Что осталось как технический долг
|
## 11. Что осталось как технический долг
|
||||||
|
|
||||||
Актуальные открытые хвосты:
|
Актуальные открытые хвосты:
|
||||||
- `RuntimeComposition` остается composition-root, но уже существенно сжат (около 501 строки) после выноса `RuntimeTransportAssembly`, `RuntimeMessagingAssembly`, `RuntimeStateAssembly`, `RuntimeRoutingAssembly` и удаления публичных proxy-методов; следующий шаг — перенос части lifecycle/orchestration helper-кода в отдельные domain-oriented service/adapters.
|
- `RuntimeComposition` остается composition-root (около 501 строки): публичные proxy-методы уже убраны, но внутри все еще смешаны wiring и часть helper-логики (`setupStateMonitoring`, event-bridge, log helpers). Следующий шаг: вынести эти helper-блоки в отдельные adapters/services.
|
||||||
- `ProtocolRuntime` и `ProtocolRuntimePort` все еще имеют широкий proxy-surface; нужен audit методов и дальнейшее сужение публичного API по use-case группам.
|
- `ProtocolRuntime` + `ProtocolRuntimePort` все еще имеют широкий API surface (connection + directory + packet IO + call signaling + debug). Нужен audit и сужение публичных контрактов по use-case группам.
|
||||||
- `ChatViewModel` остается крупным host-классом (state + bridge/proxy API к feature/coordinator/use-case слоям).
|
- `ChatViewModel` остается очень крупным host-классом (около 4391 строки) с большим bridge/proxy surface к feature/coordinator/use-case слоям.
|
||||||
- High-level media сценарии теперь в `AttachmentsFeatureCoordinator`, но остаются крупными и требуют дальнейшей декомпозиции на более узкие coordinator/service/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*`).
|
||||||
- сейчас в `app/src/test` всего 7 unit-тестов (в основном crypto/data/helpers), в `app/src/androidTest` — 1 тест;
|
- В runtime все еще несколько точек входа (`ProtocolRuntime`, `ProtocolRuntimeAccess`, `ProtocolManager` legacy), что повышает cognitive load; целевой шаг — дальнейшее сокращение legacy/static call-sites.
|
||||||
- не покрыты network runtime/coordinator слои (`RuntimeComposition`, `ConnectionEventRouter`, `ProtocolLifecycle*`, `ReadyPacketDispatchCoordinator`) и chat orchestration (`Messages/Forward/Attachments*`).
|
|
||||||
|
|
||||||
Уже закрыто и больше не считается техдолгом:
|
Уже закрыто и больше не считается техдолгом:
|
||||||
- `UiDependencyAccess.get(...)` удален из `ui/*`.
|
- `UiDependencyAccess.get(...)` удален из `ui/*`.
|
||||||
|
|||||||
@@ -27,10 +27,8 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".RosettaApplication"
|
android:name=".RosettaApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.rosetta.messenger.data
|
package com.rosetta.messenger.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.rosetta.messenger.BuildConfig
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
import com.rosetta.messenger.crypto.CryptoManager
|
||||||
import com.rosetta.messenger.crypto.MessageCrypto
|
import com.rosetta.messenger.crypto.MessageCrypto
|
||||||
import com.rosetta.messenger.database.*
|
import com.rosetta.messenger.database.*
|
||||||
@@ -266,7 +267,7 @@ class MessageRepository @Inject constructor(
|
|||||||
try {
|
try {
|
||||||
CryptoManager.encryptWithPassword(messageText, privateKey)
|
CryptoManager.encryptWithPassword(messageText, privateKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("ReleaseNotes", "❌ encryptWithPassword failed", e)
|
if (BuildConfig.DEBUG) android.util.Log.e("ReleaseNotes", "❌ encryptWithPassword failed", e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,12 +352,12 @@ class MessageRepository @Inject constructor(
|
|||||||
suspend fun checkAndSendVersionUpdateMessage() {
|
suspend fun checkAndSendVersionUpdateMessage() {
|
||||||
val account = currentAccount
|
val account = currentAccount
|
||||||
if (account == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
val privateKey = currentPrivateKey
|
val privateKey = currentPrivateKey
|
||||||
if (privateKey == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
val prefs = context.getSharedPreferences("rosetta_system_${account}", Context.MODE_PRIVATE)
|
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 currentVersion = com.rosetta.messenger.BuildConfig.VERSION_NAME
|
||||||
val currentKey = "${currentVersion}_${ReleaseNotes.noticeHash}"
|
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) {
|
if (lastNoticeKey != currentKey) {
|
||||||
// Delete the previous message for this version (if any)
|
// Delete the previous message for this version (if any)
|
||||||
@@ -375,15 +376,15 @@ class MessageRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val messageId = addUpdateSystemMessage(ReleaseNotes.getNotice(currentVersion))
|
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) {
|
if (messageId != null) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString("lastNoticeKey", currentKey)
|
.putString("lastNoticeKey", currentKey)
|
||||||
.putString("lastNoticeMessageId_$currentVersion", messageId)
|
.putString("lastNoticeMessageId_$currentVersion", messageId)
|
||||||
.apply()
|
.apply()
|
||||||
android.util.Log.d("ReleaseNotes", "✅ Update message saved successfully")
|
if (BuildConfig.DEBUG) android.util.Log.d("ReleaseNotes", "✅ Update message saved successfully")
|
||||||
} else {
|
} 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)
|
MessageCrypto.decryptIncoming(packet.content, packet.chachaKey, privateKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Fallback: если дешифровка не удалась (напр. CALL с encrypted empty content)
|
// 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 ->
|
syncedOpponentsWithWrongStatus.forEach { opponentKey ->
|
||||||
runCatching { dialogDao.updateDialogFromMessages(account, opponentKey) }
|
runCatching { dialogDao.updateDialogFromMessages(account, opponentKey) }
|
||||||
}
|
}
|
||||||
android.util.Log.i(
|
if (BuildConfig.DEBUG) android.util.Log.i(
|
||||||
"MessageRepository",
|
"MessageRepository",
|
||||||
"✅ Normalized $normalizedSyncedCount synced own messages to DELIVERED"
|
"✅ Normalized $normalizedSyncedCount synced own messages to DELIVERED"
|
||||||
)
|
)
|
||||||
@@ -1335,14 +1336,14 @@ class MessageRepository @Inject constructor(
|
|||||||
// Mark expired messages as ERROR (older than 80 seconds)
|
// Mark expired messages as ERROR (older than 80 seconds)
|
||||||
val expiredCount = messageDao.markExpiredWaitingAsError(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
|
val expiredCount = messageDao.markExpiredWaitingAsError(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
|
||||||
if (expiredCount > 0) {
|
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)
|
// Get remaining WAITING messages (younger than 80s)
|
||||||
val waitingMessages = messageDao.getWaitingMessages(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
|
val waitingMessages = messageDao.getWaitingMessages(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
|
||||||
if (waitingMessages.isEmpty()) return
|
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) {
|
for (entity in waitingMessages) {
|
||||||
// Skip saved messages (should not happen, but guard)
|
// Skip saved messages (should not happen, but guard)
|
||||||
@@ -1366,7 +1367,7 @@ class MessageRepository @Inject constructor(
|
|||||||
privateKey
|
privateKey
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} 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
|
// iOS parity: use retry mechanism for reconnect-resent messages too
|
||||||
protocolClient.sendMessageWithRetry(packet)
|
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) {
|
} 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
|
// Mark as ERROR if retry fails
|
||||||
messageDao.updateDeliveryStatus(account, entity.messageId, DeliveryStatus.ERROR.value)
|
messageDao.updateDeliveryStatus(account, entity.messageId, DeliveryStatus.ERROR.value)
|
||||||
val dialogKey = getDialogKey(entity.toPublicKey)
|
val dialogKey = getDialogKey(entity.toPublicKey)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.graphics.BitmapFactory
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.rosetta.messenger.BuildConfig
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
@@ -136,7 +137,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
/** Вызывается когда получено push-уведомление */
|
/** Вызывается когда получено push-уведомление */
|
||||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
super.onMessageReceived(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 data = remoteMessage.data
|
||||||
val notificationTitle = remoteMessage.notification?.title?.trim().orEmpty()
|
val notificationTitle = remoteMessage.notification?.title?.trim().orEmpty()
|
||||||
@@ -153,7 +154,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
val hasNotificationContent = notificationTitle.isNotBlank() || notificationBody.isNotBlank()
|
val hasNotificationContent = notificationTitle.isNotBlank() || notificationBody.isNotBlank()
|
||||||
|
|
||||||
if (!hasDataContent && !hasNotificationContent) {
|
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
|
// Still trigger reconnect if WebSocket is disconnected
|
||||||
protocolGateway.reconnectNowIfNeeded("silent_push")
|
protocolGateway.reconnectNowIfNeeded("silent_push")
|
||||||
return
|
return
|
||||||
@@ -226,14 +227,14 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
isReadEvent -> {
|
isReadEvent -> {
|
||||||
val keysToClear = collectReadDialogKeys(data, dialogKey, senderPublicKey)
|
val keysToClear = collectReadDialogKeys(data, dialogKey, senderPublicKey)
|
||||||
if (keysToClear.isEmpty()) {
|
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 {
|
} else {
|
||||||
keysToClear.forEach { key ->
|
keysToClear.forEach { key ->
|
||||||
cancelNotificationForChat(applicationContext, key)
|
cancelNotificationForChat(applicationContext, key)
|
||||||
}
|
}
|
||||||
val titleHints = collectReadTitleHints(data, keysToClear)
|
val titleHints = collectReadTitleHints(data, keysToClear)
|
||||||
cancelMatchingActiveNotifications(keysToClear, titleHints)
|
cancelMatchingActiveNotifications(keysToClear, titleHints)
|
||||||
Log.d(
|
if (BuildConfig.DEBUG) Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"READ push cleared notifications for keys=$keysToClear titles=$titleHints"
|
"READ push cleared notifications for keys=$keysToClear titles=$titleHints"
|
||||||
)
|
)
|
||||||
@@ -317,11 +318,11 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val lastTs = lastNotifTimestamps[dedupKey]
|
val lastTs = lastNotifTimestamps[dedupKey]
|
||||||
if (lastTs != null && now - lastTs < DEDUP_WINDOW_MS) {
|
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
|
return // duplicate push — skip
|
||||||
}
|
}
|
||||||
lastNotifTimestamps[dedupKey] = now
|
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()
|
val senderKey = senderPublicKey?.trim().orEmpty()
|
||||||
if (senderKey.isNotEmpty() && isDialogMuted(senderKey)) {
|
if (senderKey.isNotEmpty() && isDialogMuted(senderKey)) {
|
||||||
return
|
return
|
||||||
@@ -508,7 +509,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pushCallLog(msg: String) {
|
private fun pushCallLog(msg: String) {
|
||||||
Log.d(TAG, msg)
|
if (BuildConfig.DEBUG) Log.d(TAG, msg)
|
||||||
try {
|
try {
|
||||||
val dir = java.io.File(applicationContext.filesDir, "crash_reports")
|
val dir = java.io.File(applicationContext.filesDir, "crash_reports")
|
||||||
if (!dir.exists()) dir.mkdirs()
|
if (!dir.exists()) dir.mkdirs()
|
||||||
@@ -534,7 +535,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
pushCallLog("wakeProtocolFromPush: authRestore=$restored account=${account.take(8)}…")
|
pushCallLog("wakeProtocolFromPush: authRestore=$restored account=${account.take(8)}…")
|
||||||
protocolGateway.reconnectNowIfNeeded("push_$reason")
|
protocolGateway.reconnectNowIfNeeded("push_$reason")
|
||||||
}.onFailure { error ->
|
}.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) {
|
if (matchesDeterministicId || matchesDialogKey || matchesHint) {
|
||||||
manager.cancel(sbn.tag, sbn.id)
|
manager.cancel(sbn.tag, sbn.id)
|
||||||
Log.d(
|
if (BuildConfig.DEBUG) Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"READ push fallback cancel id=${sbn.id} tag=${sbn.tag} " +
|
"READ push fallback cancel id=${sbn.id} tag=${sbn.tag} " +
|
||||||
"channel=${notification.channelId} title='$title' " +
|
"channel=${notification.channelId} title='$title' " +
|
||||||
@@ -726,7 +727,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { error ->
|
}.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,
|
text = callUi.subtitle,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color =
|
color =
|
||||||
if (callUi.isError) {
|
if (callUi.isError && isOutgoing) {
|
||||||
|
Color.White.copy(alpha = 0.72f)
|
||||||
|
} else if (callUi.isError) {
|
||||||
Color(0xFFE55A5A)
|
Color(0xFFE55A5A)
|
||||||
} else if (isOutgoing) {
|
} else if (isOutgoing) {
|
||||||
Color.White.copy(alpha = 0.72f)
|
Color.White.copy(alpha = 0.72f)
|
||||||
|
|||||||
@@ -563,7 +563,6 @@ fun MessageBubble(
|
|||||||
Modifier.fillMaxWidth().pointerInput(isSystemSafeChat, textSelectionHelper?.isActive, isVoiceWaveGestureActive) {
|
Modifier.fillMaxWidth().pointerInput(isSystemSafeChat, textSelectionHelper?.isActive, isVoiceWaveGestureActive) {
|
||||||
if (isSystemSafeChat) return@pointerInput
|
if (isSystemSafeChat) return@pointerInput
|
||||||
if (textSelectionHelper?.isActive == true) return@pointerInput
|
if (textSelectionHelper?.isActive == true) return@pointerInput
|
||||||
if (hasVoiceAttachmentForGesture) return@pointerInput
|
|
||||||
if (isVoiceWaveGestureActive) return@pointerInput
|
if (isVoiceWaveGestureActive) return@pointerInput
|
||||||
// 🔥 Простой горизонтальный свайп для reply
|
// 🔥 Простой горизонтальный свайп для reply
|
||||||
// Используем detectHorizontalDragGestures который лучше работает со
|
// Используем detectHorizontalDragGestures который лучше работает со
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user