diff --git a/Architecture.md b/Architecture.md index ddc5f27..d41b9c7 100644 --- a/Architecture.md +++ b/Architecture.md @@ -1,17 +1,18 @@ # Rosetta Android — Architecture -> Документ описывает **текущую** архитектуру `rosetta-android` (ветка `dev`) по коду, без идеализаций. +> Документ отражает текущее состояние `rosetta-android` (ветка `dev`) по коду на 2026-04-18. -## 1. Архитектурный стиль +## 1. Архитектурный профиль -Приложение построено как **layered + feature-oriented** архитектура: -- UI на Jetpack Compose (`MainActivity` + `ui/*`). -- Бизнес-оркестрация в singleton-сервисах (`ProtocolManager`, `CallManager`, `TransportManager`, `UpdateManager`). -- Data слой через репозитории (`MessageRepository`, `GroupRepository`, `AvatarRepository`, `AccountManager`). -- Persistence через Room (`RosettaDatabase`). -- Crypto изолирован в `crypto/*`. +Приложение сейчас устроено как layered + service-oriented архитектура: +- UI: `MainActivity` + Compose-экраны + ViewModel. +- DI: Hilt (`@HiltAndroidApp`, `@AndroidEntryPoint`, модули в `di/AppContainer.kt`). +- Runtime orchestration: `ProtocolManager`, `CallManager`, `TransportManager`, `UpdateManager`. +- Session/Identity runtime state: `SessionStore`, `SessionReducer`, `IdentityStore`. +- Data: `MessageRepository`, `GroupRepository`, `AccountManager`, `PreferencesManager`. +- Persistence: Room (`RosettaDatabase`) + DataStore/SharedPreferences. -DI-контейнера (Hilt/Koin) сейчас нет: зависимости поднимаются через `object`, `getInstance(...)`, singleton-инициализацию. +`ProtocolManager` остается крупным runtime orchestrator-объектом, но критичные зоны уже вынесены в отдельные сервисы. --- @@ -19,136 +20,220 @@ DI-контейнера (Hilt/Koin) сейчас нет: зависимости ```mermaid flowchart TB - subgraph UI["UI Layer (Compose + ViewModel)"] - A1["MainActivity"] - A2["ui/chats/*"] - A3["ui/auth/*"] - A4["ui/settings/*"] + subgraph ENTRY["Android Entry Points"] + E1["RosettaApplication"] + E2["MainActivity"] + E3["RosettaFirebaseMessagingService"] + E4["IncomingCallActivity / CallForegroundService"] end - subgraph SVC["Service Layer (Singleton orchestrators)"] - B1["ProtocolManager"] - B2["CallManager"] - B3["TransportManager"] - B4["UpdateManager"] - B5["RosettaFirebaseMessagingService"] + subgraph DI["Hilt Singleton Graph"] + D1["ProtocolGateway"] + D2["SessionCoordinator"] + D3["IdentityGateway"] + D4["AccountManager / PreferencesManager"] + D5["MessageRepository / GroupRepository"] + D6["UiEntryPoint + UiDependencyAccess"] end - subgraph DATA["Data Layer"] - C1["MessageRepository"] - C2["GroupRepository"] - C3["AvatarRepository"] - C4["AccountManager / PreferencesManager"] - C5["DraftManager / ForwardManager"] + subgraph SESSION["Session / Identity Runtime"] + S1["SessionStore"] + S2["SessionReducer"] + S3["IdentityStore"] + S4["AppSessionCoordinator"] end - subgraph DB["Persistence Layer"] - D1["Room: RosettaDatabase"] - D2["DAO: message/dialog/group/etc"] + subgraph NET["Network Runtime"] + N1["ProtocolManager"] + N2["Protocol"] + N3["PacketSubscriptionRegistry"] + N4["ReadyPacketGate"] end - subgraph NET["Network Layer"] - E1["Protocol (WebSocket)"] - E2["Packet* codec"] - E3["OkHttp HTTP (transport/update)"] - E4["WebRTC"] + subgraph DATA["Data + Persistence"] + R1["MessageRepository"] + R2["GroupRepository"] + R3["Room: RosettaDatabase"] end - subgraph CRYPTO["Crypto Layer"] - F1["CryptoManager"] - F2["MessageCrypto"] - F3["XChaCha20E2EE (calls)"] - end - - UI --> SVC - UI --> DATA - SVC --> DATA - SVC --> NET - DATA --> DB - DATA --> CRYPTO - SVC --> CRYPTO + ENTRY --> DI + DI --> SESSION + DI --> NET + DI --> DATA + SESSION --> NET + DATA --> NET + DATA --> R3 + NET --> N2 ``` --- -## 3. Главные модули и ответственность +## 3. DI и composition root -### 3.1 `MainActivity` (composition root) +### 3.1 Hilt +- `RosettaApplication` помечен `@HiltAndroidApp`. +- Entry points уровня Android-компонентов: `MainActivity`, `IncomingCallActivity`, `CallForegroundService`, `RosettaFirebaseMessagingService`. +- Основные модули: +- `AppDataModule`: `AccountManager`, `PreferencesManager`. +- `AppGatewayModule`: биндинги `ProtocolGateway`, `SessionCoordinator`, `IdentityGateway`. -`MainActivity` — главный orchestration entrypoint приложения: -- поднимает `ProtocolManager.initialize(...)`, `CallManager.initialize(...)`; -- управляет auth-гейтингом, онбордингом и основным nav-stack (`Screen`); -- привязывает текущий аккаунт к runtime-сервисам; -- триггерит fast reconnect на `onResume`; -- следит за разрешениями (уведомления, fullscreen intent и т.д.). - -### 3.2 `RosettaApplication` - -На старте процесса инициализирует глобальные подсистемы: -- crash reporting, -- draft manager, -- transport manager, -- update manager. - -### 3.3 Репозитории - -#### `MessageRepository` -Ключевой data-центр для чатов: -- инициализация аккаунт-контекста (`publicKey/privateKey`); -- отправка сообщений (optimistic insert + сетевой send); -- обработка входящих `PacketMessage`/`PacketDelivery`/`PacketRead`; -- обновление `dialogs`, `messages`, `message_search_index`; -- синк timestamp (`accounts_sync_times`); -- шина событий для UI (`newMessageEvents`, `deliveryStatusEvents`). - -#### `GroupRepository` -- хранение и операции по группам, -- интеграция с `PacketGroup*`. - -#### `AvatarRepository` -- кэш/история аватаров (Room + file storage), -- реактивная выдача аватаров через `Flow`. - -#### `AccountManager` -- хранение аккаунтов в DataStore, -- last logged public/private hash в SharedPreferences для быстрых синхронных чтений. +### 3.2 UI bridge для не-Hilt классов +Часть UI/VM пока получает зависимости через `UiDependencyAccess` -> `UiEntryPoint`. +Это transitional-слой, чтобы не тащить singleton `getInstance(...)` в UI и постепенно перейти на чистый constructor injection. --- -## 4. Сетевой стек +## 4. Session lifecycle: единый source of truth -## 4.1 `Protocol` (низкий уровень) +### 4.1 Модель состояния +`SessionState`: +- `LoggedOut` +- `AuthInProgress(publicKey?, reason)` +- `Ready(account, reason)` -`Protocol` отвечает за: -- WebSocket lifecycle (`DISCONNECTED/CONNECTING/CONNECTED/HANDSHAKING/DEVICE_VERIFICATION_REQUIRED/AUTHENTICATED`), -- heartbeat, -- reconnect/backoff, -- packet encode/decode, -- `waitPacket/unwaitPacket` обработчики, -- queue pre-handshake пакетов. +### 4.2 Модель событий +`SessionAction`: +- `LoggedOut` +- `AuthInProgress` +- `Ready` +- `SyncFromCachedAccount` -Ключевые механизмы устойчивости: -- `lifecycleMutex` для serialized lifecycle операций, -- `connectionGeneration` для игнора stale callbacks старых сокетов, -- guard `isConnecting` от параллельного `connect()`. +### 4.3 Контур изменения состояния +- Только `SessionStore` владеет `MutableStateFlow`. +- Только `SessionReducer` вычисляет next-state. +- `SessionCoordinator`/`AppSessionCoordinator` больше не мутируют состояние напрямую, а делают `dispatch(action)`. +- `SessionStore.dispatch(...)` синхронно обновляет `IdentityStore` для консистентности account/profile/auth-runtime. -## 4.2 `ProtocolManager` (верхний уровень) +```mermaid +flowchart LR + A["AuthFlow / MainActivity / Unlock / SetPassword"] --> B["SessionCoordinator.dispatch(action)"] + B --> C["SessionStore.dispatch(action)"] + C --> D["SessionReducer.reduce(current, action)"] + D --> E["StateFlow"] + C --> F["IdentityStore sync"] +``` -`ProtocolManager` — orchestration-слой над `Protocol`: -- единая точка для UI/Data слоёв (`send`, `authenticate`, `reconnect`, `waitPacket` и т.д.); -- bootstrap после auth (sync, own-profile resolve, push subscribe); -- маршрутизация входящих packet-ов в репозитории/подсистемы; -- call signaling bridge для `CallManager`; -- управление typed caches (`SearchUser`, user info cache). +### 4.4 State machine -Новый runtime-дизайн connection orchestration: -- `ProtocolConnectionModels.kt` — `ConnectionLifecycleState`, `ConnectionEvent`, bootstrap context; -- `ProtocolConnectionSupervisor.kt` — actor/event-loop для serialized событий; -- `ReadyPacketGate.kt` — очередь пакетов до состояния `READY` (TTL + max size). +```mermaid +stateDiagram-v2 + [*] --> LoggedOut + LoggedOut --> AuthInProgress: dispatch(AuthInProgress) + AuthInProgress --> Ready: dispatch(Ready) + AuthInProgress --> LoggedOut: dispatch(LoggedOut) + Ready --> LoggedOut: dispatch(LoggedOut) + Ready --> Ready: dispatch(SyncFromCachedAccount(account)) +``` --- -## 5. Lifecycle состояния соединения (верхний уровень) +## 5. Network orchestration после декомпозиции + +`ProtocolManager` теперь делегирует отдельные зоны ответственности: +- `ConnectionOrchestrator`: connect/reconnect/authenticate + network-aware поведение. +- `BootstrapCoordinator`: пересчет lifecycle (`AUTHENTICATED`/`BOOTSTRAPPING`/`READY`) и работа с `ReadyPacketGate`. +- `PacketRouter`: user/search cache + resolve/search continuation routing. +- `OwnProfileSyncService`: применение собственного профиля из search и синхронизация `IdentityStore`. +- `RetryQueueService`: retry очереди отправки `PacketMessage`. +- `PacketSubscriptionRegistry`: централизованные подписки на пакеты и fan-out. + +```mermaid +flowchart TB + PM["ProtocolManager"] --> CO["ConnectionOrchestrator"] + PM --> BC["BootstrapCoordinator"] + PM --> PR["PacketRouter"] + PM --> OPS["OwnProfileSyncService"] + PM --> RQ["RetryQueueService"] + PM --> PSR["PacketSubscriptionRegistry"] + PM --> SUP["ProtocolConnectionSupervisor"] + PM --> RPG["ReadyPacketGate"] + PM --> P["Protocol (WebSocket + packet codec)"] +``` + +--- + +## 6. Централизация packet-subscriptions + +Проблема дублирующихся low-level подписок закрыта через `PacketSubscriptionRegistry`: +- На каждый `packetId` создается один bus и один bridge на `Protocol.waitPacket(...)`. +- Дальше packet fan-out идет в: +- callback API (`waitPacket/unwaitPacket`), +- `SharedFlow` (`packetFlow(packetId)`). + +```mermaid +sequenceDiagram + participant Feature as Feature/Service + participant PM as ProtocolManager + participant REG as PacketSubscriptionRegistry + participant P as Protocol + + Feature->>PM: waitPacket(0x03, callback) + PM->>REG: addCallback(0x03, callback) + REG->>P: waitPacket(0x03, protocolBridge) [once per packetId] + + P-->>REG: Packet(0x03) + REG-->>Feature: callback(packet) + REG-->>Feature: packetFlow(0x03).emit(packet) +``` + +--- + +## 7. Отправка сообщений: use-cases + retry + +Из `ChatViewModel` выделены use-cases: +- `SendTextMessageUseCase` +- `SendMediaMessageUseCase` +- `SendForwardUseCase` + +Текущий поток: +1. ViewModel готовит command и шифрованный payload. +2. UseCase собирает `PacketMessage`. +3. UseCase вызывает `protocolGateway.sendMessageWithRetry(packet)`. +4. `ProtocolManager` регистрирует пакет в `RetryQueueService` и отправляет в сеть. +5. Если lifecycle еще не `READY`, пакет попадает в `ReadyPacketGate` и flush после `READY`. + +```mermaid +flowchart LR + VM["ChatViewModel"] --> UC["Send*UseCase"] + UC --> GW["ProtocolGateway.sendMessageWithRetry"] + GW --> PM["ProtocolManager"] + PM --> RQ["RetryQueueService"] + PM --> RG["ReadyPacketGate"] + PM --> P["Protocol.sendPacket"] +``` + +--- + +## 8. Auth/bootstrap: фактический runtime flow + +```mermaid +sequenceDiagram + participant UI as Auth UI (SetPassword/Unlock) + participant SC as SessionCoordinatorImpl + participant SS as SessionStore + participant PG as ProtocolGateway + participant PM as ProtocolManager + participant AM as AccountManager + + UI->>SC: bootstrapAuthenticatedSession(account, reason) + SC->>SS: dispatch(AuthInProgress) + SC->>PG: initializeAccount(public, private) + SC->>PG: connect() + SC->>PG: authenticate(public, privateHash) + SC->>PG: reconnectNowIfNeeded(...) + SC->>AM: setCurrentAccount(public) + SC->>SS: dispatch(Ready) + + PM-->>PM: HANDSHAKE -> AUTHENTICATED -> BOOTSTRAPPING + PM-->>PM: SyncCompleted + OwnProfileResolved + PM-->>PM: connectionLifecycleState = READY +``` + +Важно: `SessionState.Ready` (app-session готова) и `connectionLifecycleState = READY` (сеть готова) — это разные state-модели. + +--- + +## 9. Состояния соединения (network lifecycle) `ProtocolManager.connectionLifecycleState`: - `DISCONNECTED` @@ -159,12 +244,6 @@ flowchart TB - `READY` - `DEVICE_VERIFICATION_REQUIRED` -Переход в `READY` происходит только когда одновременно выполнено: -- аккаунт инициализирован, -- протокол аутентифицирован, -- sync завершён, -- own-profile резолвнут (или истёк fallback timeout). - ```mermaid stateDiagram-v2 [*] --> DISCONNECTED @@ -180,203 +259,33 @@ stateDiagram-v2 --- -## 6. Поток auth / session bootstrap +## 10. Ключевые файлы новой архитектуры -```mermaid -sequenceDiagram - participant UI as AuthFlow/MainActivity - participant PM as ProtocolManager - participant P as Protocol - participant MR as MessageRepository - - UI->>PM: initializeAccount(public, private) - UI->>PM: connect() - UI->>PM: authenticate(public, privateHash) - PM->>P: connect + startHandshake - P-->>PM: AUTHENTICATED - PM->>PM: onAuthenticated() - PM->>PM: subscribePushTokenIfAvailable() - PM->>PM: requestSynchronize() - PM->>MR: initialize(...) - PM-->>PM: SyncCompleted + OwnProfileResolved - PM-->>UI: connectionLifecycleState = READY -``` - -Критично: отправка пользовательских пакетов до `READY` не теряется, а попадает в `ReadyPacketGate`. - ---- - -## 7. Поток сообщений (send/receive/delivery) - -## 7.1 Отправка -1. UI (`ChatViewModel`) вызывает отправку через `MessageRepository.sendMessage(...)`. -2. Репозиторий делает optimistic insert в `messages` (`WAITING`). -3. Формируется `PacketMessage`. -4. Отправка идёт в `ProtocolManager.send(...)`. -5. Если состояние не `READY`, пакет ставится в `ReadyPacketGate`. -6. После `READY` очередь сбрасывается в `Protocol.sendPacket(...)`. - -## 7.2 Подтверждение доставки -1. Сервер присылает `PacketDelivery (0x08)`. -2. `ProtocolManager` маршрутизирует в `MessageRepository.handleDelivery(...)`. -3. Репозиторий обновляет статус в Room. -4. UI получает апдейт через `Flow`/события. - -## 7.3 Входящие -1. `PacketMessage (0x06)` приходит в `Protocol`. -2. `ProtocolManager` dispatch → `MessageRepository.handleIncomingMessage(...)`. -3. Сообщение сохраняется в Room, обновляется `dialogs` и индексы поиска. -4. UI реактивно перерисовывается. - ---- - -## 8. Sync-пайплайн - -Используется пакет `PacketSync (0x19)` и режимы: -- `BATCH_START` -- поток синк-пакетов (`MESSAGE/READ/DELIVERY/...`) -- `BATCH_END` -- `NOT_NEEDED` - -Особенности: -- есть последовательная очередь inbound задач для сохранения порядка обработки; -- after-sync hooks: retry waiting messages, request missing user info; -- sync timestamp хранится в `accounts_sync_times`. - ---- - -## 9. Calls (WebRTC + signaling) - -`CallManager` использует: -- signaling пакеты: `PacketSignalPeer (0x1A)`, `PacketWebRTC (0x1B)`; -- ICE: `PacketIceServers (0x1C)`; -- WebRTC stack (`PeerConnectionFactory`, `PeerConnection`, audio track); -- E2EE голоса через `XChaCha20E2EE` обвязки sender/receiver. - -Основные фазы звонка: -- `IDLE` → `INCOMING`/`OUTGOING` → `CONNECTING` → `ACTIVE` → `IDLE`. - -```mermaid -stateDiagram-v2 - [*] --> IDLE - IDLE --> OUTGOING - IDLE --> INCOMING - OUTGOING --> CONNECTING - INCOMING --> CONNECTING - CONNECTING --> ACTIVE - ACTIVE --> IDLE - OUTGOING --> IDLE - INCOMING --> IDLE -``` - ---- - -## 10. Transport (вложения) - -`TransportManager`: -- получает transport server через `PacketRequestTransport (0x0F)` (desktop parity); -- upload/download через OkHttp; -- resumable download (HTTP Range); -- трекает состояния прогресса через `StateFlow` (`uploading`, `downloading`); -- поддерживает cancel/pause/resume через `FileDownloadManager`. - ---- - -## 11. Push Notifications - -`RosettaFirebaseMessagingService`: -- обрабатывает `onNewToken` и подписку токена через `ProtocolManager`; -- dedup пушей; -- маршрутизация типов (`personal_message`, `group_message`, `call`, `read`); -- очистка уведомлений по read events; -- wake-up reconnect при silent push. - ---- - -## 12. Обновления (SDU) - -`UpdateManager`: -1. запрашивает update server через `PacketRequestUpdate (0x0A)`; -2. ходит на SDU HTTP endpoint; -3. скачивает APK через `DownloadManager`; -4. ведёт update state machine (`Idle/Checking/UpdateAvailable/Downloading/ReadyToInstall/Error`). - ---- - -## 13. Persistence (Room) - -`RosettaDatabase` (version `17`) включает: -- `messages` — сообщения, -- `dialogs` — диалоги + денормализованные поля для быстрых списков, -- `message_search_index` — локальный индекс поиска, -- `groups` — группы, -- `pinned_messages` — закрепы, -- `avatar_cache` — аватары, -- `blacklist` — blacklist, -- `accounts_sync_times` — sync cursor, -- `encrypted_accounts` — аккаунты (legacy Room account storage). - -Есть длинная цепочка миграций (4→17) с оптимизациями под производительность и денормализацию. - ---- - -## 14. Crypto - -- `CryptoManager`: -- seed phrase / keypair, -- PBKDF2-derived key caching, -- encrypt/decrypt для локального хранения. - -- `MessageCrypto`: -- message-level XChaCha20-Poly1305, -- ECDH/AES-обмен ключом для payload, -- attachment decrypt logic. - -Crypto и network связаны через `MessageRepository`/`ProtocolManager`. - ---- - -## 15. Наблюдаемость и диагностика - -- wire/protocol логи через `ProtocolManager.addLog(...)` + trace file; -- debug logs доступны в UI; -- отдельные диагностические логи для звонков (`CallManager` breadcrumbs). - ---- - -## 16. Текущие архитектурные сильные стороны - -- Реактивная модель состояния (`StateFlow`) на большинстве критических путей. -- Сильная декомпозиция packet протокола (`Packet*`). -- Наличие ready-gate и serialized supervisor снижает race-condition в соединении. -- Room + денормализация ускоряют списки чатов/поиск. - ---- - -## 17. Текущие архитектурные риски - -- `MainActivity` остаётся очень крупным composition root. -- `ProtocolManager` и `MessageRepository` всё ещё крупные “god objects”. -- Отсутствие DI усложняет управляемость зависимостей/тестируемость. -- Часть жизненного цикла связана через runtime singleton state, что повышает риск регрессий при эволюции. - ---- - -## 18. Карта ключевых файлов - -- `app/src/main/java/com/rosetta/messenger/MainActivity.kt` -- `app/src/main/java/com/rosetta/messenger/RosettaApplication.kt` -- `app/src/main/java/com/rosetta/messenger/network/Protocol.kt` +- `app/src/main/java/com/rosetta/messenger/di/AppContainer.kt` +- `app/src/main/java/com/rosetta/messenger/di/UiEntryPoint.kt` +- `app/src/main/java/com/rosetta/messenger/session/AppSessionCoordinator.kt` +- `app/src/main/java/com/rosetta/messenger/session/SessionStore.kt` +- `app/src/main/java/com/rosetta/messenger/session/SessionReducer.kt` +- `app/src/main/java/com/rosetta/messenger/session/SessionAction.kt` +- `app/src/main/java/com/rosetta/messenger/session/IdentityStore.kt` - `app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt` +- `app/src/main/java/com/rosetta/messenger/network/PacketSubscriptionRegistry.kt` - `app/src/main/java/com/rosetta/messenger/network/ProtocolConnectionModels.kt` - `app/src/main/java/com/rosetta/messenger/network/ProtocolConnectionSupervisor.kt` - `app/src/main/java/com/rosetta/messenger/network/ReadyPacketGate.kt` -- `app/src/main/java/com/rosetta/messenger/network/CallManager.kt` -- `app/src/main/java/com/rosetta/messenger/network/TransportManager.kt` -- `app/src/main/java/com/rosetta/messenger/push/RosettaFirebaseMessagingService.kt` -- `app/src/main/java/com/rosetta/messenger/update/UpdateManager.kt` -- `app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt` -- `app/src/main/java/com/rosetta/messenger/database/RosettaDatabase.kt` -- `app/src/main/java/com/rosetta/messenger/crypto/CryptoManager.kt` -- `app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt` +- `app/src/main/java/com/rosetta/messenger/network/connection/ConnectionOrchestrator.kt` +- `app/src/main/java/com/rosetta/messenger/network/connection/BootstrapCoordinator.kt` +- `app/src/main/java/com/rosetta/messenger/network/connection/PacketRouter.kt` +- `app/src/main/java/com/rosetta/messenger/network/connection/OwnProfileSyncService.kt` +- `app/src/main/java/com/rosetta/messenger/network/connection/RetryQueueService.kt` +- `app/src/main/java/com/rosetta/messenger/ui/chats/usecase/SendTextMessageUseCase.kt` +- `app/src/main/java/com/rosetta/messenger/ui/chats/usecase/SendMediaMessageUseCase.kt` +- `app/src/main/java/com/rosetta/messenger/ui/chats/usecase/SendForwardUseCase.kt` +--- + +## 11. Что осталось как технический долг + +- `ProtocolManager` все еще содержит много cross-cutting логики и требует дальнейшей декомпозиции. +- Не весь UI перешел на constructor injection (`UiDependencyAccess` пока нужен для части ViewModel). +- Часть data-слоя напрямую знает о network singleton (`ProtocolManager`) и требует окончательного разрыва через интерфейсы. diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt index 214a795..cac34b1 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt @@ -2657,7 +2657,7 @@ fun MessageInputBar( contentAlignment = Alignment.Center ) { Icon( - imageVector = if (recordMode == RecordMode.VOICE) TablerIcons.Microphone else TablerIcons.Video, + imageVector = if (recordMode == RecordMode.VOICE) ThinMicrophoneIcon else TablerIcons.Video, contentDescription = null, tint = Color.White, modifier = Modifier.size(19.dp) @@ -2995,7 +2995,7 @@ fun MessageInputBar( contentAlignment = Alignment.Center ) { Icon( - imageVector = if (recordMode == RecordMode.VOICE) TablerIcons.Microphone else TablerIcons.Video, + imageVector = if (recordMode == RecordMode.VOICE) ThinMicrophoneIcon else TablerIcons.Video, contentDescription = "Record message", tint = Color(0xFF8E8E93).copy(alpha = 0.6f), modifier = Modifier.size(24.dp) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/utils/ChatDetailUtils.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/utils/ChatDetailUtils.kt index 9a4cb13..600a412 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/utils/ChatDetailUtils.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/utils/ChatDetailUtils.kt @@ -116,3 +116,61 @@ val TelegramSendIcon: ImageVector horizontalLineToRelative(16f) } }.build() + +/** Thin microphone icon matching the visual weight of Telegram-style input icons */ +val ThinMicrophoneIcon: ImageVector + get() = ImageVector.Builder( + name = "ThinMicrophone", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + // Microphone capsule + path( + fill = null, + stroke = SolidColor(Color.White), + strokeLineWidth = 1.5f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(9f, 5f) + arcToRelative(3f, 3f, 0f, isMoreThanHalf = false, isPositiveArc = true, dx1 = 6f, dy1 = 0f) + verticalLineToRelative(5f) + arcToRelative(3f, 3f, 0f, isMoreThanHalf = false, isPositiveArc = true, dx1 = -6f, dy1 = 0f) + close() + } + // U-shaped curve below capsule + path( + fill = null, + stroke = SolidColor(Color.White), + strokeLineWidth = 1.5f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(5f, 10f) + arcToRelative(7f, 7f, 0f, isMoreThanHalf = false, isPositiveArc = false, dx1 = 14f, dy1 = 0f) + } + // Vertical stem + path( + fill = null, + stroke = SolidColor(Color.White), + strokeLineWidth = 1.5f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(12f, 17f) + verticalLineToRelative(4f) + } + // Horizontal base + path( + fill = null, + stroke = SolidColor(Color.White), + strokeLineWidth = 1.5f, + strokeLineCap = StrokeCap.Round, + strokeLineJoin = StrokeJoin.Round + ) { + moveTo(8f, 21f) + horizontalLineToRelative(8f) + } + }.build()