import Combine import Foundation @MainActor final class LocationChannelsModel: ObservableObject { @Published private(set) var permissionState: LocationChannelManager.PermissionState @Published private(set) var availableChannels: [GeohashChannel] @Published private(set) var selectedChannel: ChannelID @Published private(set) var teleported: Bool @Published private(set) var bookmarks: [String] @Published private(set) var bookmarkNames: [String: String] @Published private(set) var locationNames: [GeohashChannelLevel: String] @Published private(set) var userTorEnabled: Bool @Published private(set) var gatewayEnabled: Bool private let manager: LocationChannelManager private let network: NetworkActivationService private let gateway: GatewayService init( manager: LocationChannelManager? = nil, network: NetworkActivationService? = nil, gateway: GatewayService? = nil ) { let manager = manager ?? .shared let network = network ?? .shared let gateway = gateway ?? .shared self.manager = manager self.network = network self.gateway = gateway self.gatewayEnabled = gateway.isEnabled self.permissionState = manager.permissionState self.availableChannels = manager.availableChannels self.selectedChannel = manager.selectedChannel self.teleported = manager.teleported self.bookmarks = manager.bookmarks self.bookmarkNames = manager.bookmarkNames self.locationNames = manager.locationNames self.userTorEnabled = network.userTorEnabled bind() } var currentBuildingGeohash: String? { availableChannels.first(where: { $0.level == .building })?.geohash } func isSelected(_ channel: GeohashChannel) -> Bool { guard case .location(let selected) = selectedChannel else { return false } return selected == channel } func isBookmarked(_ geohash: String) -> Bool { manager.isBookmarked(geohash) } func enableLocationChannels() { manager.enableLocationChannels() } func refreshChannels() { manager.refreshChannels() } func enableAndRefresh() { manager.enableLocationChannels() manager.refreshChannels() } func beginLiveRefresh() { manager.beginLiveRefresh() } func endLiveRefresh() { manager.endLiveRefresh() } func select(_ channel: ChannelID) { manager.select(channel) } func markTeleported(for geohash: String, _ flag: Bool) { manager.markTeleported(for: geohash, flag) } func toggleBookmark(_ geohash: String) { manager.toggleBookmark(geohash) } func resolveBookmarkNameIfNeeded(for geohash: String) { manager.resolveBookmarkNameIfNeeded(for: geohash) } func locationName(for level: GeohashChannelLevel) -> String? { locationNames[level] } func setUserTorEnabled(_ enabled: Bool) { network.setUserTorEnabled(enabled) } func refreshMeshChannelsIfNeeded() { guard case .mesh = selectedChannel, permissionState == .authorized, availableChannels.isEmpty else { return } refreshChannels() } func openLocationChannel(for geohash: String) { let normalized = geohash.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz") guard (2...12).contains(normalized.count), normalized.allSatisfy({ allowed.contains($0) }) else { return } let channel = GeohashChannel(level: level(forLength: normalized.count), geohash: normalized) let isRegional = availableChannels.contains { $0.geohash == normalized } if !isRegional && !availableChannels.isEmpty { markTeleported(for: normalized, true) } select(.location(channel)) } func teleport(to geohash: String) { let normalized = geohash.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() let channel = GeohashChannel(level: level(forLength: normalized.count), geohash: normalized) markTeleported(for: normalized, true) select(.location(channel)) } private func bind() { manager.$permissionState .receive(on: DispatchQueue.main) .assign(to: &$permissionState) manager.$availableChannels .receive(on: DispatchQueue.main) .assign(to: &$availableChannels) manager.$selectedChannel .receive(on: DispatchQueue.main) .assign(to: &$selectedChannel) manager.$teleported .receive(on: DispatchQueue.main) .assign(to: &$teleported) manager.$bookmarks .receive(on: DispatchQueue.main) .assign(to: &$bookmarks) manager.$bookmarkNames .receive(on: DispatchQueue.main) .assign(to: &$bookmarkNames) manager.$locationNames .receive(on: DispatchQueue.main) .assign(to: &$locationNames) network.$userTorEnabled .receive(on: DispatchQueue.main) .assign(to: &$userTorEnabled) gateway.$isEnabled .receive(on: DispatchQueue.main) .assign(to: &$gatewayEnabled) } private func level(forLength length: Int) -> GeohashChannelLevel { switch length { case 0...2: return .region case 3...4: return .province case 5: return .city case 6: return .neighborhood case 7: return .block case 8...12: return .building default: return .block } } }