Extract DeliveryTrackingService and SystemMessagingService (-37 lines)

Continues god object decomposition with 2 more focused services.

WHAT WAS EXTRACTED
==================
1. DeliveryTrackingService (73 lines)
   - updateMessageDeliveryStatus() logic
   - Prevents status downgrades (read → delivered)
   - Updates messages in both public and private chats
   - Triggers UI notifications

2. SystemMessagingService (42 lines)
   - createSystemMessage() factory methods
   - Consistent system message creation
   - Timestamp management

From ChatViewModel (37 net lines reduced):
- Delivery status update logic (54 lines) → thin wrapper
- System message creation (inline code) → delegates to service
- Message routing logic stays in ViewModel (needs channel access)

NEW SERVICES
===========
1. bitchat/Services/DeliveryTrackingService.swift (73 lines)

API:
- updateStatus(messageID:status:messages:privateChats:notifyChange:)

Features:
- Prevents delivery status downgrades
- Updates across all message stores
- Clean separation of tracking logic

2. bitchat/Services/SystemMessagingService.swift (42 lines)

API:
- createSystemMessage(content:timestamp:)
- createSystemMessage(content:timestamp:isRelay:originalSender:)

Features:
- Consistent system message factory
- Reusable message creation
- Timestamp management

INTEGRATION
===========
ChatViewModel changes:
- Added deliveryTracking service
- Added systemMessaging service
- didReceiveReadReceipt() → delegates to deliveryTracking
- didUpdateMessageDeliveryStatus() → delegates to deliveryTracking
- updateMessageDeliveryStatus() → thin wrapper over deliveryTracking
- addSystemMessage() → uses systemMessaging.createSystemMessage()
- addMeshOnlySystemMessage() → uses systemMessaging.createSystemMessage()
- addPublicSystemMessage() → uses systemMessaging.createSystemMessage()

IMPACT
======
ChatViewModel: 5,394 → 5,357 lines (-0.7% this commit, -13.5% cumulative)
Services added: +115 lines (2 new services)
Tests:  All 23 passing
Build:  Clean (6.21s)

Cumulative god object reduction: -838 lines (13.5% total)
Services extracted: 6 total
  1. SpamFilterService (222)
  2. ColorPaletteService (328)
  3. MessageFormattingService (618)
  4. GeohashParticipantsService (180)
  5. DeliveryTrackingService (73)
  6. SystemMessagingService (42)
Total service lines: 1,463

Progress toward next milestone:
  Current: 5,357 lines
  Target: < 5,000 lines
  Remaining: 357 lines to extract

TEST RESULTS
============
✔ All 23 tests passing
✔ Build completes successfully
✔ Zero regressions
✔ Delivery tracking still works correctly
✔ System messages still display properly
This commit is contained in:
jack
2025-10-07 22:21:27 +01:00
committed by Islam
parent 52c51c9be6
commit fb003ba25e
3 changed files with 143 additions and 65 deletions
@@ -0,0 +1,73 @@
//
// DeliveryTrackingService.swift
// bitchat
//
// Service for tracking message delivery and read status
// This is free and unencumbered software released into the public domain.
//
import BitLogger
import Foundation
/// Service that manages delivery status updates for messages
/// Prevents status downgrades (e.g., read delivered) and maintains consistency
final class DeliveryTrackingService {
// MARK: - Public API
/// Update delivery status for a message, preventing downgrades
/// - Parameters:
/// - messageID: The message ID to update
/// - status: The new delivery status
/// - messages: Array of public messages (inout for mutation)
/// - privateChats: Dictionary of private chats (inout for mutation)
/// - notifyChange: Closure to trigger UI update
func updateStatus(
messageID: String,
status: DeliveryStatus,
messages: inout [BitchatMessage],
privateChats: inout [String: [BitchatMessage]],
notifyChange: @escaping () -> Void
) {
// Update in main messages
if let index = messages.firstIndex(where: { $0.id == messageID }) {
let currentStatus = messages[index].deliveryStatus
if !shouldSkipUpdate(currentStatus: currentStatus, newStatus: status) {
messages[index].deliveryStatus = status
}
}
// Update in private chats
for (peerID, chatMessages) in privateChats {
guard let index = chatMessages.firstIndex(where: { $0.id == messageID }) else { continue }
let currentStatus = chatMessages[index].deliveryStatus
guard !shouldSkipUpdate(currentStatus: currentStatus, newStatus: status) else { continue }
// Update delivery status
privateChats[peerID]?[index].deliveryStatus = status
}
// Trigger UI update
DispatchQueue.main.async {
notifyChange()
}
}
// MARK: - Private Helpers
/// Check if we should skip a status update to prevent downgrades
private func shouldSkipUpdate(currentStatus: DeliveryStatus?, newStatus: DeliveryStatus) -> Bool {
guard let current = currentStatus else { return false }
// Don't downgrade from read to delivered or sent
switch (current, newStatus) {
case (.read, .delivered):
return true
case (.read, .sent):
return true
default:
return false
}
}
}
@@ -0,0 +1,42 @@
//
// SystemMessagingService.swift
// bitchat
//
// Service for creating and managing system messages
// This is free and unencumbered software released into the public domain.
//
import BitLogger
import Foundation
/// Service that creates and routes system messages to appropriate channels
final class SystemMessagingService {
// MARK: - Message Creation
/// Create a basic system message
func createSystemMessage(content: String, timestamp: Date = Date()) -> BitchatMessage {
return BitchatMessage(
sender: "system",
content: content,
timestamp: timestamp,
isRelay: false
)
}
/// Create a system message with custom properties
func createSystemMessage(
content: String,
timestamp: Date = Date(),
isRelay: Bool = false,
originalSender: String? = nil
) -> BitchatMessage {
return BitchatMessage(
sender: "system",
content: content,
timestamp: timestamp,
isRelay: isRelay,
originalSender: originalSender
)
}
}
+28 -65
View File
@@ -123,6 +123,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
// Computed property for backward compatibility // Computed property for backward compatibility
var geohashPeople: [GeoPerson] { geohashParticipantsService.geohashPeople } var geohashPeople: [GeoPerson] { geohashParticipantsService.geohashPeople }
// MARK: - System Messaging
/// Service for creating system messages
private let systemMessaging = SystemMessagingService()
// MARK: - Delivery Tracking
/// Service for tracking message delivery status
private let deliveryTracking = DeliveryTrackingService()
// MARK: - Published Properties // MARK: - Published Properties
@Published var messages: [BitchatMessage] = [] @Published var messages: [BitchatMessage] = []
@@ -4089,68 +4099,31 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
return identityManager.isFavorite(fingerprint: fingerprint) return identityManager.isFavorite(fingerprint: fingerprint)
} }
// MARK: - Delivery Tracking // MARK: - Delivery Tracking (Delegated to DeliveryTrackingService)
func didReceiveReadReceipt(_ receipt: ReadReceipt) { func didReceiveReadReceipt(_ receipt: ReadReceipt) {
// Find the message and update its read status
updateMessageDeliveryStatus(receipt.originalMessageID, status: .read(by: receipt.readerNickname, at: receipt.timestamp)) updateMessageDeliveryStatus(receipt.originalMessageID, status: .read(by: receipt.readerNickname, at: receipt.timestamp))
} }
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) { func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
updateMessageDeliveryStatus(messageID, status: status) updateMessageDeliveryStatus(messageID, status: status)
} }
private func updateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) { private func updateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
deliveryTracking.updateStatus(
// Helper function to check if we should skip this update messageID: messageID,
func shouldSkipUpdate(currentStatus: DeliveryStatus?, newStatus: DeliveryStatus) -> Bool { status: status,
guard let current = currentStatus else { return false } messages: &messages,
privateChats: &privateChats,
// Don't downgrade from read to delivered notifyChange: { [weak self] in
switch (current, newStatus) { self?.objectWillChange.send()
case (.read, .delivered):
return true
case (.read, .sent):
return true
default:
return false
} }
}
// Update in main messages
if let index = messages.firstIndex(where: { $0.id == messageID }) {
let currentStatus = messages[index].deliveryStatus
if !shouldSkipUpdate(currentStatus: currentStatus, newStatus: status) {
messages[index].deliveryStatus = status
}
}
// Update in private chats
for (peerID, chatMessages) in privateChats {
guard let index = chatMessages.firstIndex(where: { $0.id == messageID }) else { continue }
let currentStatus = chatMessages[index].deliveryStatus
guard !shouldSkipUpdate(currentStatus: currentStatus, newStatus: status) else { continue }
// Update delivery status directly (BitchatMessage is a class/reference type)
privateChats[peerID]?[index].deliveryStatus = status
}
// Trigger UI update for delivery status change
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send()
}
}
// MARK: - Helper for System Messages
private func addSystemMessage(_ content: String, timestamp: Date = Date()) {
let systemMessage = BitchatMessage(
sender: "system",
content: content,
timestamp: timestamp,
isRelay: false
) )
}
// MARK: - Helper for System Messages (Using SystemMessagingService)
private func addSystemMessage(_ content: String, timestamp: Date = Date()) {
let systemMessage = systemMessaging.createSystemMessage(content: content, timestamp: timestamp)
messages.append(systemMessage) messages.append(systemMessage)
} }
@@ -4158,12 +4131,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
/// If mesh is currently active, also append to the visible `messages`. /// If mesh is currently active, also append to the visible `messages`.
@MainActor @MainActor
private func addMeshOnlySystemMessage(_ content: String) { private func addMeshOnlySystemMessage(_ content: String) {
let systemMessage = BitchatMessage( let systemMessage = systemMessaging.createSystemMessage(content: content)
sender: "system",
content: content,
timestamp: Date(),
isRelay: false
)
// Persist to mesh timeline // Persist to mesh timeline
meshTimeline.append(systemMessage) meshTimeline.append(systemMessage)
trimMeshTimelineIfNeeded() trimMeshTimelineIfNeeded()
@@ -4178,12 +4146,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
/// Also persists the message into the active channel's backing store so it survives timeline rebinds. /// Also persists the message into the active channel's backing store so it survives timeline rebinds.
@MainActor @MainActor
func addPublicSystemMessage(_ content: String) { func addPublicSystemMessage(_ content: String) {
let systemMessage = BitchatMessage( let systemMessage = systemMessaging.createSystemMessage(content: content)
sender: "system",
content: content,
timestamp: Date(),
isRelay: false
)
// Append to current visible messages // Append to current visible messages
messages.append(systemMessage) messages.append(systemMessage)
// Persist into the backing store for the active channel to survive rebinds // Persist into the backing store for the active channel to survive rebinds