mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 03: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 {
|
func isPeerConnected(_ peerID: String) -> Bool {
|
||||||
// Accept both 16-hex short IDs and 64-hex Noise keys
|
// Accept both 16-hex short IDs and 64-hex Noise keys
|
||||||
let shortID: String = {
|
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 PeerIDUtils.derivePeerID(fromPublicKey: key)
|
||||||
}
|
}
|
||||||
return peerID
|
return peerID
|
||||||
@@ -523,7 +523,7 @@ final class BLEService: NSObject {
|
|||||||
func isPeerReachable(_ peerID: String) -> Bool {
|
func isPeerReachable(_ peerID: String) -> Bool {
|
||||||
// Accept both 16-hex short IDs and 64-hex Noise keys
|
// Accept both 16-hex short IDs and 64-hex Noise keys
|
||||||
let shortID: String = {
|
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 PeerIDUtils.derivePeerID(fromPublicKey: key)
|
||||||
}
|
}
|
||||||
return peerID
|
return peerID
|
||||||
@@ -664,8 +664,7 @@ final class BLEService: NSObject {
|
|||||||
return collectionsQueue.sync {
|
return collectionsQueue.sync {
|
||||||
if let publicKey = peers[peerID]?.noisePublicKey {
|
if let publicKey = peers[peerID]?.noisePublicKey {
|
||||||
// Use the same fingerprinting method as NoiseEncryptionService/UnifiedPeerService (SHA-256 of raw key)
|
// Use the same fingerprinting method as NoiseEncryptionService/UnifiedPeerService (SHA-256 of raw key)
|
||||||
let hash = SHA256.hash(data: publicKey)
|
return publicKey.sha256Fingerprint()
|
||||||
return hash.map { String(format: "%02x", $0) }.joined()
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class FavoritesPersistenceService: ObservableObject {
|
|||||||
/// Falls back to scanning favorites and matching on derived peer ID.
|
/// Falls back to scanning favorites and matching on derived peer ID.
|
||||||
func getFavoriteStatus(forPeerID peerID: String) -> FavoriteRelationship? {
|
func getFavoriteStatus(forPeerID peerID: String) -> FavoriteRelationship? {
|
||||||
// Quick sanity: peerID should be 16 hex chars (8 bytes)
|
// 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 {
|
for (pubkey, rel) in favorites {
|
||||||
let derived = PeerIDUtils.derivePeerID(fromPublicKey: pubkey)
|
let derived = PeerIDUtils.derivePeerID(fromPublicKey: pubkey)
|
||||||
if derived == peerID { return rel }
|
if derived == peerID { return rel }
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ final class NostrTransport: Transport {
|
|||||||
let npub = fav.peerNostrPublicKey {
|
let npub = fav.peerNostrPublicKey {
|
||||||
return npub
|
return npub
|
||||||
}
|
}
|
||||||
if peerID.count == 16,
|
if PeerIDResolver.isShortID(peerID),
|
||||||
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
|
let fav = FavoritesPersistenceService.shared.getFavoriteStatus(forPeerID: peerID),
|
||||||
let npub = fav.peerNostrPublicKey {
|
let npub = fav.peerNostrPublicKey {
|
||||||
return npub
|
return npub
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
private let commandProcessor: CommandProcessor
|
private let commandProcessor: CommandProcessor
|
||||||
private let messageRouter: MessageRouter
|
private let messageRouter: MessageRouter
|
||||||
private let privateChatManager: PrivateChatManager
|
private let privateChatManager: PrivateChatManager
|
||||||
private let readReceiptTracker = ReadReceiptTracker()
|
private let readReceiptTracker: ReadReceiptTracker
|
||||||
private let unifiedPeerService: UnifiedPeerService
|
private let unifiedPeerService: UnifiedPeerService
|
||||||
private let autocompleteService: AutocompleteService
|
private let autocompleteService: AutocompleteService
|
||||||
|
|
||||||
@@ -456,6 +456,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
init() {
|
init() {
|
||||||
|
// Initialize read receipt tracker first
|
||||||
|
self.readReceiptTracker = ReadReceiptTracker()
|
||||||
// Initialize services
|
// Initialize services
|
||||||
self.commandProcessor = CommandProcessor()
|
self.commandProcessor = CommandProcessor()
|
||||||
self.privateChatManager = PrivateChatManager(meshService: meshService, readReceiptTracker: readReceiptTracker)
|
self.privateChatManager = PrivateChatManager(meshService: meshService, readReceiptTracker: readReceiptTracker)
|
||||||
@@ -689,6 +691,195 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
)
|
)
|
||||||
#endif
|
#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
|
// MARK: - Deinitialization
|
||||||
|
|
||||||
@@ -3579,7 +3770,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}()
|
}()
|
||||||
let full = nostrKeyMapping[spid]?.lowercased() ?? bare.lowercased()
|
let full = nostrKeyMapping[spid]?.lowercased() ?? bare.lowercased()
|
||||||
return getNostrPaletteColor(for: full, isDark: isDark)
|
return getNostrPaletteColor(for: full, isDark: isDark)
|
||||||
} else if spid.count == 16 {
|
} else if PeerIDResolver.isShortID(spid) {
|
||||||
// Mesh short ID
|
// Mesh short ID
|
||||||
return getPeerPaletteColor(for: spid, isDark: isDark)
|
return getPeerPaletteColor(for: spid, isDark: isDark)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ struct FingerprintView: View {
|
|||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
// Prefer short mesh ID for session/encryption status
|
// Prefer short mesh ID for session/encryption status
|
||||||
let statusPeerID: String = {
|
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
|
return peerID
|
||||||
}()
|
}()
|
||||||
// Resolve a friendly name
|
// Resolve a friendly name
|
||||||
let peerNickname: String = {
|
let peerNickname: String = {
|
||||||
if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName }
|
if let p = viewModel.getPeer(byID: statusPeerID) { return p.displayName }
|
||||||
if let name = viewModel.meshService.peerNickname(peerID: statusPeerID) { return name }
|
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 }
|
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: data), !fav.peerNickname.isEmpty { return fav.peerNickname }
|
||||||
let fp = data.sha256Fingerprint()
|
let fp = data.sha256Fingerprint()
|
||||||
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
|
if let social = SecureIdentityStateManager.shared.getSocialIdentity(for: fp) {
|
||||||
|
|||||||
Reference in New Issue
Block a user