mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 18:05:19 +00:00
807 lines
21 KiB
Markdown
807 lines
21 KiB
Markdown
# Proper ChatViewModel Re-Architecture Vision
|
|
|
|
**Date:** 2025-10-07
|
|
**Purpose:** Design the RIGHT architecture for ChatViewModel decomposition
|
|
|
|
---
|
|
|
|
## Current State (The Problem)
|
|
|
|
```
|
|
ChatViewModel: 5,355 lines
|
|
Responsibilities: EVERYTHING
|
|
- UI state management
|
|
- Message sending (mesh + Nostr + DMs)
|
|
- Message receiving (mesh + Nostr + DMs)
|
|
- Channel management (mesh ↔ geohash switching)
|
|
- Peer management
|
|
- Verification flows
|
|
- Favorites management
|
|
- Command processing
|
|
- Autocomplete
|
|
- Location handling
|
|
- Emergency panic
|
|
- Nostr integration
|
|
- Bluetooth state
|
|
- Plus ~50 more things
|
|
|
|
Pattern: GOD OBJECT anti-pattern
|
|
```
|
|
|
|
---
|
|
|
|
## Proper Architecture (The Solution)
|
|
|
|
### Core Principle: **Coordinators + Services + Repositories**
|
|
|
|
```
|
|
ChatViewModel (target: < 1,000 lines)
|
|
- ONLY UI state (@Published properties)
|
|
- ONLY thin coordination between layers
|
|
- NO business logic
|
|
- NO direct service calls
|
|
├── Coordinators/ (Business logic orchestration)
|
|
│ ├── MessageCoordinator
|
|
│ ├── ChannelCoordinator
|
|
│ ├── PeerCoordinator
|
|
│ └── VerificationCoordinator
|
|
├── Services/ (Domain logic)
|
|
│ ├── Formatting, Colors, etc.
|
|
│ └── ... (existing)
|
|
└── Repositories/ (Data access)
|
|
├── MessageRepository
|
|
├── PeerRepository
|
|
└── ChannelRepository
|
|
```
|
|
|
|
---
|
|
|
|
## Layer 1: ChatViewModel (< 1,000 lines) - UI State ONLY
|
|
|
|
### Responsibilities
|
|
```swift
|
|
@MainActor
|
|
final class ChatViewModel: ObservableObject {
|
|
// MARK: - Coordinators (inject, don't create)
|
|
private let messageCoordinator: MessageCoordinator
|
|
private let channelCoordinator: ChannelCoordinator
|
|
private let peerCoordinator: PeerCoordinator
|
|
private let verificationCoordinator: VerificationCoordinator
|
|
|
|
// MARK: - UI State (@Published ONLY)
|
|
@Published var messages: [BitchatMessage] = []
|
|
@Published var selectedChannel: ChannelID = .mesh
|
|
@Published var selectedPrivateChatPeer: String? = nil
|
|
@Published var showAutocomplete: Bool = false
|
|
@Published var autocompleteSuggestions: [String] = []
|
|
@Published var connectedPeers: [BitchatPeer] = []
|
|
@Published var showBluetoothAlert: Bool = false
|
|
@Published var bluetoothAlertMessage: String = ""
|
|
// ... ~15-20 @Published properties for UI state
|
|
|
|
// MARK: - User Actions (thin wrappers)
|
|
func sendMessage(_ content: String) {
|
|
messageCoordinator.sendMessage(content)
|
|
}
|
|
|
|
func switchChannel(_ channel: ChannelID) {
|
|
channelCoordinator.switchTo(channel)
|
|
}
|
|
|
|
func startPrivateChat(with peerID: String) {
|
|
messageCoordinator.startPrivateChat(with: peerID)
|
|
}
|
|
|
|
func verifyPeer(_ peerID: String) {
|
|
verificationCoordinator.verify(peerID)
|
|
}
|
|
|
|
// ... ~30-40 thin wrapper functions
|
|
}
|
|
```
|
|
|
|
**Size:** ~800-1,000 lines
|
|
**Purpose:** UI state + thin coordination
|
|
**No business logic whatsoever**
|
|
|
|
---
|
|
|
|
## Layer 2: Coordinators (Business Logic Orchestration)
|
|
|
|
### MessageCoordinator (~500 lines)
|
|
|
|
```swift
|
|
@MainActor
|
|
final class MessageCoordinator {
|
|
// MARK: - Dependencies
|
|
private weak var viewModel: ChatViewModel?
|
|
private let meshService: Transport
|
|
private let nostrService: NostrMessageService
|
|
private let messageRepository: MessageRepository
|
|
private let formatter: MessageFormattingService
|
|
|
|
// MARK: - Public API
|
|
func sendMessage(_ content: String) {
|
|
// 1. Validate
|
|
guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
|
|
|
// 2. Route based on context
|
|
if let peer = viewModel?.selectedPrivateChatPeer {
|
|
sendPrivateMessage(content, to: peer)
|
|
} else {
|
|
sendPublicMessage(content)
|
|
}
|
|
}
|
|
|
|
private func sendPublicMessage(_ content: String) {
|
|
switch viewModel?.selectedChannel {
|
|
case .mesh:
|
|
sendMeshMessage(content)
|
|
case .location(let ch):
|
|
sendGeohashMessage(content, channel: ch)
|
|
case nil:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func sendPrivateMessage(_ content: String, to peerID: String) {
|
|
// Route: Bluetooth if connected, Nostr if mutual favorite, error otherwise
|
|
if meshService.isPeerConnected(PeerID(str: peerID)) {
|
|
sendMeshDM(content, to: peerID)
|
|
} else if isMutualFavorite(peerID) {
|
|
sendNostrDM(content, to: peerID)
|
|
} else {
|
|
viewModel?.showError("Cannot send - peer not connected")
|
|
}
|
|
}
|
|
|
|
func receiveMessage(_ message: BitchatMessage) {
|
|
// 1. Validate (spam filter, blocking, etc.)
|
|
guard shouldAccept(message) else { return }
|
|
|
|
// 2. Process (format, cache, etc.)
|
|
let processed = processMessage(message)
|
|
|
|
// 3. Store
|
|
messageRepository.save(processed)
|
|
|
|
// 4. Notify ViewModel
|
|
viewModel?.updateMessages(messageRepository.getVisible())
|
|
}
|
|
|
|
// ... ~400 more lines of message orchestration
|
|
}
|
|
```
|
|
|
|
**Responsibilities:**
|
|
- Message sending (all transports)
|
|
- Message receiving (all transports)
|
|
- Message routing (mesh vs Nostr vs DM)
|
|
- Message validation
|
|
- Delivery tracking
|
|
- Read receipts
|
|
|
|
**Does NOT:**
|
|
- Hold UI state
|
|
- Know about SwiftUI
|
|
- Access ViewModel properties directly
|
|
|
|
---
|
|
|
|
### ChannelCoordinator (~300 lines)
|
|
|
|
```swift
|
|
@MainActor
|
|
final class ChannelCoordinator {
|
|
// MARK: - Dependencies
|
|
private weak var viewModel: ChatViewModel?
|
|
private let locationService: LocationChannelManager
|
|
private let nostrService: NostrMessageService
|
|
private let channelRepository: ChannelRepository
|
|
|
|
// MARK: - Public API
|
|
func switchTo(_ channel: ChannelID) {
|
|
// 1. Unsubscribe from old channel
|
|
unsubscribeFromCurrent()
|
|
|
|
// 2. Update state
|
|
channelRepository.setActive(channel)
|
|
|
|
// 3. Subscribe to new channel
|
|
subscribe(to: channel)
|
|
|
|
// 4. Load message history
|
|
let messages = channelRepository.getMessages(for: channel)
|
|
viewModel?.updateMessages(messages)
|
|
|
|
// 5. Update participants
|
|
switch channel {
|
|
case .mesh:
|
|
updateMeshParticipants()
|
|
case .location(let ch):
|
|
updateGeohashParticipants(ch.geohash)
|
|
}
|
|
}
|
|
|
|
private func subscribe(to channel: ChannelID) {
|
|
switch channel {
|
|
case .mesh:
|
|
// Mesh is always active
|
|
break
|
|
case .location(let ch):
|
|
subscribeToGeohash(ch.geohash)
|
|
}
|
|
}
|
|
|
|
private func subscribeToGeohash(_ geohash: String) {
|
|
// 1. Derive Nostr identity for this geohash
|
|
guard let identity = try? NostrIdentityBridge.deriveIdentity(forGeohash: geohash) else { return }
|
|
|
|
// 2. Subscribe to Nostr events for this geohash
|
|
nostrService.subscribe(
|
|
geohash: geohash,
|
|
identity: identity,
|
|
onMessage: { [weak self] event in
|
|
self?.handleGeohashMessage(event)
|
|
}
|
|
)
|
|
}
|
|
|
|
// ... ~250 more lines of channel orchestration
|
|
}
|
|
```
|
|
|
|
**Responsibilities:**
|
|
- Channel switching (mesh ↔ geohash)
|
|
- Channel subscriptions (Nostr)
|
|
- Channel-specific message loading
|
|
- Participant tracking per channel
|
|
- Timeline management
|
|
|
|
**Does NOT:**
|
|
- Send messages
|
|
- Handle DMs
|
|
- Know about UI rendering
|
|
|
|
---
|
|
|
|
### PeerCoordinator (~300 lines)
|
|
|
|
```swift
|
|
@MainActor
|
|
final class PeerCoordinator {
|
|
// MARK: - Dependencies
|
|
private weak var viewModel: ChatViewModel?
|
|
private let meshService: Transport
|
|
private let peerRepository: PeerRepository
|
|
private let identityManager: SecureIdentityStateManagerProtocol
|
|
private let favoritesService: FavoritesPersistenceService
|
|
|
|
// MARK: - Public API
|
|
func updatePeerList() {
|
|
// 1. Get mesh peers
|
|
let meshPeers = meshService.currentPeerSnapshots()
|
|
|
|
// 2. Get favorites (including offline)
|
|
let favorites = favoritesService.favorites
|
|
|
|
// 3. Merge into unified peer list
|
|
let unified = peerRepository.unifyPeers(mesh: meshPeers, favorites: favorites)
|
|
|
|
// 4. Update ViewModel
|
|
viewModel?.updatePeers(unified)
|
|
}
|
|
|
|
func toggleFavorite(_ peerID: String) {
|
|
// 1. Get peer's Noise public key
|
|
guard let peer = peerRepository.get(peerID) else { return }
|
|
|
|
// 2. Update favorites
|
|
if peer.isFavorite {
|
|
favoritesService.removeFavorite(peerNoisePublicKey: peer.noisePublicKey)
|
|
} else {
|
|
favoritesService.addFavorite(
|
|
peerNoisePublicKey: peer.noisePublicKey,
|
|
peerNickname: peer.nickname
|
|
)
|
|
}
|
|
|
|
// 3. Refresh peer list
|
|
updatePeerList()
|
|
}
|
|
|
|
func block(_ peerID: String) {
|
|
identityManager.setBlocked(peerID, isBlocked: true)
|
|
peerRepository.remove(peerID)
|
|
viewModel?.updatePeers(peerRepository.getAll())
|
|
}
|
|
|
|
// ... ~250 more lines of peer management
|
|
}
|
|
```
|
|
|
|
**Responsibilities:**
|
|
- Peer list management
|
|
- Favorites handling
|
|
- Blocking/unblocking
|
|
- Peer state updates
|
|
- Connection tracking
|
|
|
|
**Does NOT:**
|
|
- Send/receive messages
|
|
- Handle channels
|
|
- Know about verification
|
|
|
|
---
|
|
|
|
### VerificationCoordinator (~200 lines)
|
|
|
|
```swift
|
|
@MainActor
|
|
final class VerificationCoordinator {
|
|
// MARK: - Dependencies
|
|
private weak var viewModel: ChatViewModel?
|
|
private let meshService: Transport
|
|
private let identityManager: SecureIdentityStateManagerProtocol
|
|
private let verificationService: VerificationService
|
|
|
|
// MARK: - State
|
|
private var pendingVerifications: [String: PendingVerification] = [:]
|
|
|
|
// MARK: - Public API
|
|
func beginQRVerification(with qr: VerificationService.VerificationQR) -> Bool {
|
|
// 1. Find matching peer by Noise key
|
|
guard let peer = findPeer(by: qr.noiseKeyHex) else { return false }
|
|
|
|
// 2. Generate challenge nonce
|
|
let nonce = generateNonce()
|
|
|
|
// 3. Store pending verification
|
|
pendingVerifications[peer.peerID.id] = PendingVerification(
|
|
noiseKeyHex: qr.noiseKeyHex,
|
|
signKeyHex: qr.signKeyHex,
|
|
nonceA: nonce,
|
|
startedAt: Date()
|
|
)
|
|
|
|
// 4. Send challenge
|
|
meshService.sendVerifyChallenge(to: peer.peerID, noiseKeyHex: qr.noiseKeyHex, nonceA: nonce)
|
|
|
|
return true
|
|
}
|
|
|
|
func handleVerifyChallenge(from peerID: String, payload: Data) {
|
|
// 1. Parse TLV
|
|
guard let tlv = verificationService.parseVerifyChallenge(payload) else { return }
|
|
|
|
// 2. Check rate limiting
|
|
guard !isRateLimited(peerID) else { return }
|
|
|
|
// 3. Send response
|
|
meshService.sendVerifyResponse(to: PeerID(str: peerID), noiseKeyHex: tlv.noiseKeyHex, nonceA: tlv.nonceA)
|
|
}
|
|
|
|
func handleVerifyResponse(from peerID: String, payload: Data) {
|
|
// 1. Get pending verification
|
|
guard let pending = pendingVerifications[peerID] else { return }
|
|
|
|
// 2. Parse and verify signature
|
|
guard let resp = verificationService.parseVerifyResponse(payload),
|
|
verificationService.verifySignature(resp, with: pending.signKeyHex, nonce: pending.nonceA) else {
|
|
viewModel?.showError("Verification failed - invalid signature")
|
|
pendingVerifications.removeValue(forKey: peerID)
|
|
return
|
|
}
|
|
|
|
// 3. Mark as verified
|
|
identityManager.setVerified(fingerprint: pending.noiseKeyHex.sha256(), verified: true)
|
|
|
|
// 4. Update ViewModel
|
|
viewModel?.markPeerVerified(peerID)
|
|
|
|
// 5. Clean up
|
|
pendingVerifications.removeValue(forKey: peerID)
|
|
}
|
|
|
|
// ... ~150 more lines
|
|
}
|
|
```
|
|
|
|
**Responsibilities:**
|
|
- QR code verification flow
|
|
- Challenge/response handling
|
|
- Verified state management
|
|
- Rate limiting verification attempts
|
|
|
|
**Does NOT:**
|
|
- Send regular messages
|
|
- Manage peers
|
|
- Handle channels
|
|
|
|
---
|
|
|
|
## Layer 3: Services (Domain Logic)
|
|
|
|
### Already Good (Keep These)
|
|
```
|
|
✅ MessageFormattingService (618 lines) - Message rendering
|
|
✅ ColorPaletteService (328 lines) - Peer colors
|
|
✅ SpamFilterService (222 lines) - Rate limiting
|
|
```
|
|
|
|
### New Services Needed
|
|
|
|
#### NostrMessageService (~400 lines)
|
|
```swift
|
|
final class NostrMessageService {
|
|
// All Nostr-specific message logic
|
|
func sendDM(content: String, to pubkey: String) async throws -> String
|
|
func receiveDM(_ giftWrap: NostrEvent) async throws -> DecryptedMessage
|
|
func sendPublicEvent(content: String, geohash: String) async throws
|
|
func subscribeToChannel(_ geohash: String, handler: @escaping (NostrEvent) -> Void)
|
|
// ... handles ALL Nostr message protocol details
|
|
}
|
|
```
|
|
|
|
#### MeshMessageService (~200 lines)
|
|
```swift
|
|
final class MeshMessageService {
|
|
// All mesh-specific message logic
|
|
func sendPublic(content: String, mentions: [String])
|
|
func sendPrivate(content: String, to peerID: String)
|
|
func receivePublic(_ packet: BitchatPacket)
|
|
func receivePrivate(_ packet: BitchatPacket)
|
|
// ... handles ALL mesh message protocol details
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Layer 4: Repositories (Data Access)
|
|
|
|
### MessageRepository (~200 lines)
|
|
```swift
|
|
@MainActor
|
|
final class MessageRepository {
|
|
// MARK: - Storage
|
|
private var meshTimeline: [BitchatMessage] = []
|
|
private var geoTimelines: [String: [BitchatMessage]] = [:]
|
|
private var privateChats: [String: [BitchatMessage]] = [:]
|
|
|
|
// MARK: - Public API
|
|
func save(_ message: BitchatMessage, to channel: ChannelID)
|
|
func getMessages(for channel: ChannelID) -> [BitchatMessage]
|
|
func getPrivateChat(with peerID: String) -> [BitchatMessage]
|
|
func clearChannel(_ channel: ChannelID)
|
|
|
|
// Handles all message storage, capping, trimming
|
|
// NO business logic, JUST data access
|
|
}
|
|
```
|
|
|
|
### PeerRepository (~150 lines)
|
|
```swift
|
|
@MainActor
|
|
final class PeerRepository {
|
|
// MARK: - Storage
|
|
private var peers: [String: BitchatPeer] = [:]
|
|
private var peerIndex: [String: BitchatPeer] = [:]
|
|
|
|
// MARK: - Public API
|
|
func save(_ peer: BitchatPeer)
|
|
func get(_ peerID: String) -> BitchatPeer?
|
|
func getAll() -> [BitchatPeer]
|
|
func remove(_ peerID: String)
|
|
func unifyPeers(mesh: [MeshPeer], favorites: [Favorite]) -> [BitchatPeer]
|
|
|
|
// Handles all peer storage
|
|
// NO business logic, JUST data access
|
|
}
|
|
```
|
|
|
|
### ChannelRepository (~100 lines)
|
|
```swift
|
|
@MainActor
|
|
final class ChannelRepository {
|
|
// MARK: - Storage
|
|
private var activeChannel: ChannelID = .mesh
|
|
private var subscriptions: [String: String] = [:]
|
|
|
|
// MARK: - Public API
|
|
func setActive(_ channel: ChannelID)
|
|
func getActive() -> ChannelID
|
|
func saveSubscription(id: String, for channel: String)
|
|
func getSubscriptions() -> [String: String]
|
|
|
|
// Handles channel state storage
|
|
// NO business logic, JUST data access
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow Architecture
|
|
|
|
### Sending a Message (Example)
|
|
|
|
```
|
|
User types "hello" in UI
|
|
↓
|
|
1. ContentView calls viewModel.sendMessage("hello")
|
|
↓
|
|
2. ChatViewModel delegates to messageCoordinator.sendMessage("hello")
|
|
↓
|
|
3. MessageCoordinator:
|
|
a. Gets active channel from channelRepository
|
|
b. Determines transport (mesh or Nostr)
|
|
c. Creates BitchatMessage
|
|
d. Saves to messageRepository
|
|
e. Calls appropriate service:
|
|
- MeshMessageService.sendPublic() for mesh
|
|
- NostrMessageService.sendPublicEvent() for geohash
|
|
↓
|
|
4. Service handles protocol details, returns result
|
|
↓
|
|
5. MessageCoordinator updates messageRepository with delivery status
|
|
↓
|
|
6. messageRepository notifies observers
|
|
↓
|
|
7. ChatViewModel @Published properties update
|
|
↓
|
|
8. SwiftUI re-renders
|
|
```
|
|
|
|
**Benefits:**
|
|
- Clear responsibilities at each layer
|
|
- Easy to test (mock repositories)
|
|
- Easy to swap implementations
|
|
- Single Responsibility Principle
|
|
- Testable business logic
|
|
|
|
---
|
|
|
|
## Receiving a Message (Example)
|
|
|
|
```
|
|
Bluetooth packet arrives
|
|
↓
|
|
1. BLEService decrypts, creates BitchatPacket
|
|
↓
|
|
2. BLEService calls delegate: didReceivePublicMessage(message)
|
|
↓
|
|
3. ChatViewModel implements BitchatDelegate:
|
|
func didReceivePublicMessage(_ message: BitchatMessage) {
|
|
messageCoordinator.receiveMessage(message)
|
|
}
|
|
↓
|
|
4. MessageCoordinator:
|
|
a. Validates message (spam filter)
|
|
b. Checks if blocked
|
|
c. Determines target (public/private)
|
|
d. Processes (formatting cached for later)
|
|
e. Saves to messageRepository
|
|
f. Updates delivery status if DM
|
|
↓
|
|
5. messageRepository notifies ChatViewModel
|
|
↓
|
|
6. ChatViewModel updates @Published messages
|
|
↓
|
|
7. SwiftUI re-renders
|
|
```
|
|
|
|
---
|
|
|
|
## Comparison: Current vs Proper
|
|
|
|
### Current Architecture (BAD)
|
|
```
|
|
ChatViewModel (5,355 lines)
|
|
├── Does EVERYTHING
|
|
├── Mixes UI state with business logic
|
|
├── Directly calls services
|
|
├── Holds all data
|
|
├── Impossible to test
|
|
└── Unmaintainable
|
|
|
|
Testing requires:
|
|
- Mock BLEService
|
|
- Mock NostrRelayManager
|
|
- Mock 10+ other services
|
|
- Set up entire app state
|
|
= IMPOSSIBLE in practice
|
|
```
|
|
|
|
### Proper Architecture (GOOD)
|
|
```
|
|
ChatViewModel (< 1,000 lines)
|
|
├── ONLY UI state
|
|
├── Delegates to coordinators
|
|
├── No business logic
|
|
└── Easy to test
|
|
|
|
MessageCoordinator (~500 lines)
|
|
├── Orchestrates message flows
|
|
├── Uses repositories for data
|
|
├── Uses services for logic
|
|
└── Testable with mocked dependencies
|
|
|
|
MessageRepository (~200 lines)
|
|
├── ONLY data access
|
|
├── No business logic
|
|
└── Trivial to test
|
|
|
|
Testing MessageCoordinator:
|
|
- Mock MessageRepository (easy)
|
|
- Mock MeshMessageService (easy)
|
|
- Mock NostrMessageService (easy)
|
|
= Actually testable!
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Strategy
|
|
|
|
### Phase 1: Create Repositories (1 week)
|
|
```
|
|
1. MessageRepository (~200 lines)
|
|
- Extract all message storage from ChatViewModel
|
|
- Move meshTimeline, geoTimelines, privateChats
|
|
|
|
2. PeerRepository (~150 lines)
|
|
- Extract all peer storage
|
|
- Move peers, peerIndex, etc.
|
|
|
|
3. ChannelRepository (~100 lines)
|
|
- Extract channel state
|
|
- Move activeChannel, subscriptions
|
|
|
|
Result: ChatViewModel ~4,900 lines
|
|
No behavior change, just data layer separation
|
|
```
|
|
|
|
### Phase 2: Create Coordinators (2 weeks)
|
|
```
|
|
1. MessageCoordinator (~500 lines)
|
|
- Extract all message sending logic
|
|
- Extract all message receiving logic
|
|
- Extract routing logic
|
|
|
|
2. ChannelCoordinator (~300 lines)
|
|
- Extract channel switching
|
|
- Extract subscription management
|
|
- Extract geohash logic
|
|
|
|
3. PeerCoordinator (~300 lines)
|
|
- Extract peer management
|
|
- Extract favorites logic
|
|
- Extract blocking logic
|
|
|
|
4. VerificationCoordinator (~200 lines)
|
|
- Extract QR verification
|
|
- Extract fingerprint management
|
|
|
|
Result: ChatViewModel ~3,600 lines
|
|
Major architectural improvement
|
|
```
|
|
|
|
### Phase 3: Thin ViewModel (1 week)
|
|
```
|
|
1. Move remaining logic to coordinators
|
|
2. ChatViewModel becomes pure UI state
|
|
3. All @Published properties for UI
|
|
4. Thin wrappers only
|
|
|
|
Result: ChatViewModel ~800-1,000 lines
|
|
Clean architecture achieved
|
|
```
|
|
|
|
**Total timeline: 4-5 weeks of focused work**
|
|
|
|
---
|
|
|
|
## Alternative: Hybrid Approach (Pragmatic)
|
|
|
|
If full re-architecture is too much:
|
|
|
|
### Keep What Works
|
|
```
|
|
✅ MessageFormattingService (good)
|
|
✅ ColorPaletteService (good)
|
|
✅ SpamFilterService (good)
|
|
✅ GeohashParticipantsService (OK)
|
|
❌ Delete DeliveryTrackingService (merge back)
|
|
❌ Delete SystemMessagingService (merge back)
|
|
```
|
|
|
|
### Extract Just 2-3 BIG Coordinators
|
|
```
|
|
1. MessageSendingCoordinator (~450 lines)
|
|
- All sendMessage() variants
|
|
- Routing logic
|
|
- Message creation
|
|
|
|
2. ChannelCoordinator (~300 lines)
|
|
- Channel switching
|
|
- Subscriptions
|
|
- Timeline management
|
|
|
|
3. (Optional) VerificationCoordinator (~200 lines)
|
|
- QR verification flow
|
|
```
|
|
|
|
**Result:** ChatViewModel ~4,100 lines in 2-3 days
|
|
**Benefit:** Major improvement without full re-architecture
|
|
|
|
---
|
|
|
|
## My Recommendation
|
|
|
|
### Option A: Merge PR #1, Plan Full Re-Architecture
|
|
```
|
|
Timeline: 4-5 weeks
|
|
Effort: HIGH
|
|
Risk: MEDIUM
|
|
Benefit: Professional-grade architecture
|
|
|
|
Steps:
|
|
1. Merge PR #1 (get wins now)
|
|
2. Design proper architecture (1 week)
|
|
3. Create repositories (1 week)
|
|
4. Create coordinators (2 weeks)
|
|
5. Thin ViewModel (1 week)
|
|
```
|
|
|
|
### Option B: Extract 1-2 Big Coordinators Now
|
|
```
|
|
Timeline: 1-2 days
|
|
Effort: MEDIUM
|
|
Risk: MEDIUM
|
|
Benefit: Significant improvement
|
|
|
|
Steps:
|
|
1. Extract MessageSendingCoordinator (462 lines) - 3 hours
|
|
2. Extract ChannelCoordinator (300 lines) - 2 hours
|
|
3. Test thoroughly - 1 hour
|
|
|
|
Result: ~4,600 lines, < 5,000 milestone achieved
|
|
```
|
|
|
|
### Option C: Stop Here
|
|
```
|
|
Timeline: Now
|
|
Effort: ZERO
|
|
Risk: ZERO
|
|
Benefit: Consolidate wins
|
|
|
|
PR #1 delivers:
|
|
- 3 good services
|
|
- 100% memory safety
|
|
- Good documentation
|
|
- Solid foundation
|
|
```
|
|
|
|
---
|
|
|
|
## What I Recommend RIGHT NOW
|
|
|
|
**Option C: Stop and merge PR #1.**
|
|
|
|
**Why:**
|
|
- We've worked 6+ hours
|
|
- PR #1 has real value (3 good services + memory fixes)
|
|
- You've learned what works (big extractions) and what doesn't (tiny ones)
|
|
- Proper coordinator extraction needs fresh energy and careful design
|
|
- Better to ship wins now, plan next phase properly
|
|
|
|
**Then:**
|
|
- Take a break
|
|
- Review PR #1 with team
|
|
- Plan proper coordinator architecture separately
|
|
- Execute when fresh with 2-3 focused days
|
|
|
|
---
|
|
|
|
**But if you want to push:** I can extract MessageSendingCoordinator (462 lines) right now in ~2 hours. It would hit < 5,000 milestone.
|
|
|
|
**Your choice?**
|