Extract MessageFormattingEngine from ChatViewModel

Refactor message formatting logic into a dedicated, testable class:

- Create MessageFormattingEngine with Patterns enum (precompiled regexes)
- Create MessageFormattingContext protocol for dependency injection
- Extract extractMentions(), containsCashuToken(), and pattern matchers
- Add 28 comprehensive tests for formatting utilities
- Update ChatViewModel to conform to MessageFormattingContext

This prepares for further message formatting extraction and makes
regex patterns and utility functions testable in isolation.
This commit is contained in:
jack
2025-11-24 11:44:59 -10:00
parent ff7e5a2c2c
commit b6dd31b285
3 changed files with 735 additions and 1 deletions
+41 -1
View File
@@ -92,7 +92,7 @@ import UniformTypeIdentifiers
/// Manages the application state and business logic for BitChat.
/// Acts as the primary coordinator between UI components and backend services,
/// implementing the BitchatDelegate protocol to handle network events.
final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProvider, GeohashParticipantContext {
final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProvider, GeohashParticipantContext, MessageFormattingContext {
// Precompiled regexes and detectors reused across formatting
private enum Regexes {
static let hashtag: NSRegularExpression = {
@@ -4193,6 +4193,46 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
return Color(peerSeed: message.sender.lowercased(), isDark: isDark)
}
// MARK: - MessageFormattingContext Protocol
@MainActor
func isSelfMessage(_ message: BitchatMessage) -> Bool {
if let spid = message.senderPeerID {
// In geohash channels, compare against our per-geohash nostr short ID
if case .location(let ch) = activeChannel, spid.isGeoChat {
let myGeo: NostrIdentity? = {
if let cached = cachedGeohashIdentity, cached.geohash == ch.geohash {
return cached.identity
}
// Derive and cache
if let identity = try? idBridge.deriveIdentity(forGeohash: ch.geohash) {
cachedGeohashIdentity = (ch.geohash, identity)
return identity
}
return nil
}()
if let myGeo {
return spid == PeerID(nostr: myGeo.publicKeyHex)
}
}
return spid == meshService.myPeerID
}
// Fallback by nickname
if message.sender == nickname { return true }
if message.sender.hasPrefix(nickname + "#") { return true }
return false
}
@MainActor
func senderColor(for message: BitchatMessage, isDark: Bool) -> Color {
return peerColor(for: message, isDark: isDark)
}
@MainActor
func peerURL(for peerID: PeerID) -> URL? {
return URL(string: "bitchat://user/\(peerID.toPercentEncoded())")
}
// Public helpers for views to color peers consistently in lists
@MainActor
func colorForNostrPubkey(_ pubkeyHexLowercased: String, isDark: Bool) -> Color {