Files
bitchat/plans/proper-architecture-vision.md
T

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?**