mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 20:05:19 +00:00
refactor(init): add dependency-injected initializer for ChatViewModel to enable composition/testing
Provide an alternate initializer that accepts meshService, router, privateChatManager, unifiedPeerService, commandProcessor, and an optional ReadReceiptTracker. Keeps default init intact; both perform the same wiring and startup behavior.
This commit is contained in:
@@ -512,7 +512,7 @@ final class BLEService: NSObject {
|
||||
func isPeerConnected(_ peerID: String) -> Bool {
|
||||
// Accept both 16-hex short IDs and 64-hex Noise keys
|
||||
let shortID: String = {
|
||||
if peerID.count == 64, let key = Data(hexString: peerID) {
|
||||
if PeerIDResolver.isNoiseKeyHex(peerID), let key = Data(hexString: peerID) {
|
||||
return PeerIDUtils.derivePeerID(fromPublicKey: key)
|
||||
}
|
||||
return peerID
|
||||
@@ -523,7 +523,7 @@ final class BLEService: NSObject {
|
||||
func isPeerReachable(_ peerID: String) -> Bool {
|
||||
// Accept both 16-hex short IDs and 64-hex Noise keys
|
||||
let shortID: String = {
|
||||
if peerID.count == 64, let key = Data(hexString: peerID) {
|
||||
if PeerIDResolver.isNoiseKeyHex(peerID), let key = Data(hexString: peerID) {
|
||||
return PeerIDUtils.derivePeerID(fromPublicKey: key)
|
||||
}
|
||||
return peerID
|
||||
@@ -664,8 +664,7 @@ final class BLEService: NSObject {
|
||||
return collectionsQueue.sync {
|
||||
if let publicKey = peers[peerID]?.noisePublicKey {
|
||||
// Use the same fingerprinting method as NoiseEncryptionService/UnifiedPeerService (SHA-256 of raw key)
|
||||
let hash = SHA256.hash(data: publicKey)
|
||||
return hash.map { String(format: "%02x", $0) }.joined()
|
||||
return publicKey.sha256Fingerprint()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ class FavoritesPersistenceService: ObservableObject {
|
||||
/// Falls back to scanning favorites and matching on derived peer ID.
|
||||
func getFavoriteStatus(forPeerID peerID: String) -> FavoriteRelationship? {
|
||||
// Quick sanity: peerID should be 16 hex chars (8 bytes)
|
||||
guard peerID.count == 16 else { return nil }
|
||||
guard PeerIDResolver.isShortID(peerID) else { return nil }
|
||||
for (pubkey, rel) in favorites {
|
||||
let derived = PeerIDUtils.derivePeerID(fromPublicKey: pubkey)
|
||||
if derived == peerID { return rel }
|
||||
|
||||
@@ -167,7 +167,7 @@ final class NostrTransport: Transport {
|
||||
let npub = fav.peerNostrPublicKey {
|
||||
return npub
|
||||
}
|
||||
if peerID.count == 16,
|
||||
if PeerIDResolver.isShortID(peerID),
|
||||
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
|
||||
let npub = fav.peerNostrPublicKey {
|
||||
return npub
|
||||
|
||||
@@ -245,7 +245,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
private let commandProcessor: CommandProcessor
|
||||
private let messageRouter: MessageRouter
|
||||
private let privateChatManager: PrivateChatManager
|
||||
private let readReceiptTracker = ReadReceiptTracker()
|
||||
private let readReceiptTracker: ReadReceiptTracker
|
||||
private let unifiedPeerService: UnifiedPeerService
|
||||
private let autocompleteService: AutocompleteService
|
||||
|
||||
@@ -456,6 +456,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
|
||||
@MainActor
|
||||
init() {
|
||||
// Initialize read receipt tracker first
|
||||
self.readReceiptTracker = ReadReceiptTracker()
|
||||
// Initialize services
|
||||
self.commandProcessor = CommandProcessor()
|
||||
self.privateChatManager = PrivateChatManager(meshService: meshService, readReceiptTracker: readReceiptTracker)
|
||||
@@ -689,6 +691,195 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Dependency-injected initializer (for testing and composition)
|
||||
@MainActor
|
||||
init(meshService: Transport,
|
||||
commandProcessor: CommandProcessor,
|
||||
privateChatManager: PrivateChatManager,
|
||||
unifiedPeerService: UnifiedPeerService,
|
||||
messageRouter: MessageRouter,
|
||||
readReceiptTracker: ReadReceiptTracker = ReadReceiptTracker()) {
|
||||
// Assign dependencies
|
||||
self.meshService = meshService
|
||||
self.commandProcessor = commandProcessor
|
||||
self.privateChatManager = privateChatManager
|
||||
self.unifiedPeerService = unifiedPeerService
|
||||
self.messageRouter = messageRouter
|
||||
self.readReceiptTracker = readReceiptTracker
|
||||
|
||||
// Wire up dependencies
|
||||
self.commandProcessor.chatViewModel = self
|
||||
self.privateChatManager.messageRouter = self.messageRouter
|
||||
self.commandProcessor.meshService = meshService
|
||||
|
||||
// Subscribe to privateChatManager changes to trigger UI updates
|
||||
privateChatManager.objectWillChange
|
||||
.sink { [weak self] _ in
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
loadNickname()
|
||||
loadVerifiedFingerprints()
|
||||
meshService.delegate = self
|
||||
|
||||
// Log fingerprint after a delay to ensure encryption service is ready
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiStartupInitialDelaySeconds) { [weak self] in
|
||||
if let self = self {
|
||||
_ = self.getMyFingerprint()
|
||||
}
|
||||
}
|
||||
|
||||
// Set nickname before starting services
|
||||
meshService.setNickname(nickname)
|
||||
|
||||
// Start mesh service immediately
|
||||
meshService.startServices()
|
||||
|
||||
// Initialize Nostr services
|
||||
Task { @MainActor in
|
||||
nostrRelayManager = NostrRelayManager.shared
|
||||
SecureLogger.log("Initializing Nostr relay connections", category: SecureLogger.session, level: .debug)
|
||||
nostrRelayManager?.connect()
|
||||
|
||||
// Small delay to ensure read receipts are fully loaded
|
||||
try? await Task.sleep(nanoseconds: TransportConfig.uiStartupShortSleepNs)
|
||||
|
||||
// Set up Nostr message handling directly
|
||||
setupNostrMessageHandling()
|
||||
|
||||
// Attempt to flush any queued outbox after Nostr comes online
|
||||
messageRouter.flushAllOutbox()
|
||||
|
||||
// End startup phase after 2 seconds
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: UInt64(TransportConfig.uiStartupPhaseDurationSeconds * 1_000_000_000))
|
||||
self.isStartupPhase = false
|
||||
}
|
||||
|
||||
// Bind unified peer service's peer list to our published property
|
||||
let cancellable = unifiedPeerService.$peers
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] peers in
|
||||
// Update peers on main thread for UI consistency
|
||||
guard let self = self else { return }
|
||||
Task { @MainActor in
|
||||
self.allPeers = peers
|
||||
// Build a quick index for getPeer(by:)
|
||||
var uniquePeers: [String: BitchatPeer] = [:]
|
||||
for peer in peers {
|
||||
if uniquePeers[peer.id] == nil {
|
||||
uniquePeers[peer.id] = peer
|
||||
}
|
||||
}
|
||||
self.peerIndex = uniquePeers
|
||||
if self.selectedPrivateChatFingerprint != nil {
|
||||
self.updatePrivateChatPeerIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cancellables.insert(cancellable)
|
||||
|
||||
// Resubscribe geohash on relay reconnect
|
||||
if let relayMgr = self.nostrRelayManager {
|
||||
relayMgr.$isConnected
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] connected in
|
||||
guard let self = self else { return }
|
||||
if connected {
|
||||
Task { @MainActor in
|
||||
self.resubscribeCurrentGeohash()
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up Noise encryption callbacks
|
||||
setupNoiseCallbacks()
|
||||
|
||||
// Observe location channel selection
|
||||
LocationChannelManager.shared.$selectedChannel
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] channel in
|
||||
guard let self = self else { return }
|
||||
Task { @MainActor in
|
||||
self.switchLocationChannel(to: channel)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
// Initialize with current selection
|
||||
Task { @MainActor in
|
||||
self.switchLocationChannel(to: LocationChannelManager.shared.selectedChannel)
|
||||
}
|
||||
|
||||
// Request notification permission
|
||||
NotificationService.shared.requestAuthorization()
|
||||
|
||||
// Listen for favorite status changes
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleFavoriteStatusChanged),
|
||||
name: .favoriteStatusChanged,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Listen for peer status updates to refresh UI
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handlePeerStatusUpdate),
|
||||
name: Notification.Name("peerStatusUpdated"),
|
||||
object: nil
|
||||
)
|
||||
|
||||
#if os(macOS)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appDidBecomeActive),
|
||||
name: NSApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appWillResignActive),
|
||||
name: NSApplication.willResignActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appWillTerminate),
|
||||
name: NSApplication.willTerminateNotification,
|
||||
object: nil
|
||||
)
|
||||
#else
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appDidBecomeActive),
|
||||
name: UIApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appWillResignActive),
|
||||
name: UIApplication.willResignActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(appWillTerminate),
|
||||
name: UIApplication.willTerminateNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(userDidTakeScreenshot),
|
||||
name: UIApplication.userDidTakeScreenshotNotification,
|
||||
object: nil
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
@@ -3579,7 +3770,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
}()
|
||||
let full = nostrKeyMapping[spid]?.lowercased() ?? bare.lowercased()
|
||||
return getNostrPaletteColor(for: full, isDark: isDark)
|
||||
} else if spid.count == 16 {
|
||||
} else if PeerIDResolver.isShortID(spid) {
|
||||
// Mesh short ID
|
||||
return getPeerPaletteColor(for: spid, isDark: isDark)
|
||||
} else {
|
||||
|
||||
@@ -43,14 +43,14 @@ struct FingerprintView: View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Prefer short mesh ID for session/encryption status
|
||||
let statusPeerID: String = {
|
||||
if peerID.count == 64, let short = viewModel.getShortIDForNoiseKey(peerID) { return short }
|
||||
if PeerIDResolver.isNoiseKeyHex(peerID), let short = viewModel.getShortIDForNoiseKey(peerID) { return short }
|
||||
return peerID
|
||||
}()
|
||||
// Resolve a friendly name
|
||||
let peerNickname: String = {
|
||||
if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName }
|
||||
if let name = viewModel.meshService.peerNickname(peerID: statusPeerID) { return name }
|
||||
if peerID.count == 64, let data = Data(hexString: peerID) {
|
||||
if PeerIDResolver.isNoiseKeyHex(peerID), let data = Data(hexString: peerID) {
|
||||
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
|
||||
let fp = data.sha256Fingerprint()
|
||||
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
|
||||
|
||||
Reference in New Issue
Block a user