mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 16:45:22 +00:00
Cleanup: remove dead code, normalize fingerprints, modernize share extension, trim test noise, and drop ‘preparing to share…’ message (#520)
* Remove dead code and artifacts: drop PeerManager, unused views/types; delete LegacyTestProtocolTypes; update .gitignore; purge TestResult.xcresult and build.log * Tests: gate verbose prints under DEBUG; ChatViewModel: remove legacy fingerprint helper and rely on UnifiedPeerService * Share Extension: migrate to UIKit + UTTypes; drop Social/SLComposeServiceViewController * Remove 'preparing to share …' system message; send shared content immediately * Inline comment cleanup: drop legacy 'removed' breadcrumbs across protocols, services, view model, and views --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -71,3 +71,7 @@ __pycache__/
|
|||||||
# Local build results
|
# Local build results
|
||||||
.Result*/
|
.Result*/
|
||||||
.Result*.xcresult/
|
.Result*.xcresult/
|
||||||
|
TestResult.xcresult/
|
||||||
|
*.xcresult/
|
||||||
|
build.log
|
||||||
|
*.log
|
||||||
|
|||||||
@@ -109,20 +109,8 @@ struct BitchatApp: App {
|
|||||||
userDefaults.removeObject(forKey: "sharedContentDate")
|
userDefaults.removeObject(forKey: "sharedContentDate")
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
|
|
||||||
// Show notification about shared content
|
// Send the shared content immediately on the main queue
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
// Add system message about sharing
|
|
||||||
let systemMessage = BitchatMessage(
|
|
||||||
sender: "system",
|
|
||||||
content: "preparing to share \(contentType)...",
|
|
||||||
timestamp: Date(),
|
|
||||||
isRelay: false
|
|
||||||
)
|
|
||||||
self.chatViewModel.messages.append(systemMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the shared content after a short delay
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
||||||
if contentType == "url" {
|
if contentType == "url" {
|
||||||
// Try to parse as JSON first
|
// Try to parse as JSON first
|
||||||
if let data = sharedContent.data(using: .utf8),
|
if let data = sharedContent.data(using: .utf8),
|
||||||
|
|||||||
@@ -173,56 +173,7 @@ struct PendingActions {
|
|||||||
var setPetname: String?
|
var setPetname: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Privacy Settings
|
//
|
||||||
|
|
||||||
struct PrivacySettings: Codable {
|
|
||||||
// Level 1: Maximum privacy (default)
|
|
||||||
var persistIdentityCache = false
|
|
||||||
var showLastSeen = false
|
|
||||||
|
|
||||||
// Level 2: Convenience
|
|
||||||
var autoAcceptKnownFingerprints = false
|
|
||||||
var rememberNicknameHistory = false
|
|
||||||
|
|
||||||
// Level 3: Social
|
|
||||||
var shareTrustNetworkHints = false // "3 mutual contacts trust this person"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Conflict Resolution
|
|
||||||
|
|
||||||
/// Strategies for resolving identity conflicts in the decentralized network.
|
|
||||||
/// Handles cases where multiple peers claim the same nickname or when
|
|
||||||
/// identity mappings become ambiguous due to network partitions.
|
|
||||||
enum ConflictResolution {
|
|
||||||
case acceptNew(petname: String) // "John (2)"
|
|
||||||
case rejectNew
|
|
||||||
case blockFingerprint(String)
|
|
||||||
case alertUser(message: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UI State
|
|
||||||
|
|
||||||
struct PeerUIState {
|
|
||||||
let peerID: String
|
|
||||||
let nickname: String
|
|
||||||
var identityState: IdentityState
|
|
||||||
var connectionQuality: ConnectionQuality
|
|
||||||
|
|
||||||
enum IdentityState {
|
|
||||||
case unknown // Gray - No identity info
|
|
||||||
case unverifiedKnown(String) // Blue - Handshake done, matches cache
|
|
||||||
case verified(String) // Green - Cryptographically verified
|
|
||||||
case conflict(String, String) // Red - Nickname doesn't match fingerprint
|
|
||||||
case pending // Yellow - Handshake in progress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ConnectionQuality {
|
|
||||||
case excellent
|
|
||||||
case good
|
|
||||||
case poor
|
|
||||||
case disconnected
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Migration Support
|
// MARK: - Migration Support
|
||||||
// Removed LegacyFavorite - no longer needed
|
//
|
||||||
|
|||||||
@@ -89,219 +89,4 @@ struct BitchatPeer: Identifiable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Peer Manager
|
//
|
||||||
|
|
||||||
/// Manages the collection of peers and their states
|
|
||||||
@MainActor
|
|
||||||
class PeerManager: ObservableObject {
|
|
||||||
@Published var peers: [BitchatPeer] = []
|
|
||||||
@Published var favorites: [BitchatPeer] = []
|
|
||||||
@Published var mutualFavorites: [BitchatPeer] = []
|
|
||||||
|
|
||||||
private let meshService: Transport
|
|
||||||
private let favoritesService = FavoritesPersistenceService.shared
|
|
||||||
|
|
||||||
init(meshService: Transport) {
|
|
||||||
self.meshService = meshService
|
|
||||||
updatePeers()
|
|
||||||
|
|
||||||
// Listen for updates
|
|
||||||
NotificationCenter.default.addObserver(
|
|
||||||
self,
|
|
||||||
selector: #selector(handleFavoriteChanged),
|
|
||||||
name: .favoriteStatusChanged,
|
|
||||||
object: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func handleFavoriteChanged() {
|
|
||||||
SecureLogger.log("⭐ Favorite status changed notification received, updating peers",
|
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
updatePeers()
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
NotificationCenter.default.removeObserver(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePeers() {
|
|
||||||
// Reduce log verbosity - only log when count changes
|
|
||||||
let previousCount = peers.count
|
|
||||||
|
|
||||||
// Get current mesh peers
|
|
||||||
let meshPeers = meshService.getPeerNicknames()
|
|
||||||
|
|
||||||
// Build peer list
|
|
||||||
var allPeers: [BitchatPeer] = []
|
|
||||||
var connectedNicknames: Set<String> = []
|
|
||||||
var addedPeerIDs: Set<String> = []
|
|
||||||
|
|
||||||
// Add connected mesh peers (only if actually connected or relay connected)
|
|
||||||
for (peerID, nickname) in meshPeers {
|
|
||||||
guard let noiseKey = Data(hexString: peerID) else { continue }
|
|
||||||
|
|
||||||
// Safety check: Never add our own peer ID
|
|
||||||
if peerID == meshService.myPeerID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this peer is actually connected
|
|
||||||
let isConnected = meshService.isPeerConnected(peerID)
|
|
||||||
|
|
||||||
// Skip disconnected peers unless they're favorites (handled later)
|
|
||||||
if !isConnected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isConnected {
|
|
||||||
connectedNicknames.insert(nickname)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track that we've added this peer ID
|
|
||||||
addedPeerIDs.insert(peerID)
|
|
||||||
|
|
||||||
var peer = BitchatPeer(
|
|
||||||
id: peerID,
|
|
||||||
noisePublicKey: noiseKey,
|
|
||||||
nickname: nickname,
|
|
||||||
isConnected: isConnected
|
|
||||||
)
|
|
||||||
// Set favorite status - check both by current noise key and by nickname
|
|
||||||
if let favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey) {
|
|
||||||
peer.favoriteStatus = favoriteStatus
|
|
||||||
peer.nostrPublicKey = favoriteStatus.peerNostrPublicKey
|
|
||||||
} else {
|
|
||||||
// Check if we have a favorite for this nickname (peer may have reconnected with new ID)
|
|
||||||
let favoriteByNickname = favoritesService.favorites.values.first { $0.peerNickname == nickname }
|
|
||||||
if let favorite = favoriteByNickname {
|
|
||||||
SecureLogger.log("🔄 Found favorite for '\(nickname)' by nickname, updating noise key",
|
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
// Update the favorite's noise key to match the current connection
|
|
||||||
favoritesService.updateNoisePublicKey(from: favorite.peerNoisePublicKey, to: noiseKey, peerNickname: nickname)
|
|
||||||
// Get the updated favorite with the new key
|
|
||||||
peer.favoriteStatus = favoritesService.getFavoriteStatus(for: noiseKey)
|
|
||||||
peer.nostrPublicKey = peer.favoriteStatus?.peerNostrPublicKey ?? favorite.peerNostrPublicKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allPeers.append(peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add offline favorites (only those not currently connected AND that we actively favorite)
|
|
||||||
|
|
||||||
for (favoriteKey, favorite) in favoritesService.favorites {
|
|
||||||
let favoriteID = favorite.peerNoisePublicKey.hexEncodedString()
|
|
||||||
|
|
||||||
// Skip if this peer is already connected (by nickname)
|
|
||||||
if connectedNicknames.contains(favorite.peerNickname) {
|
|
||||||
// Skipping favorite - already connected
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if we already added a peer with this ID (prevents duplicates)
|
|
||||||
if addedPeerIDs.contains(favoriteID) {
|
|
||||||
// Skipping favorite - peer ID already added
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only add peers that WE favorite (not just ones who favorite us)
|
|
||||||
if !favorite.isFavorite {
|
|
||||||
// Skipping - we don't favorite them
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this favorite as an offline peer
|
|
||||||
SecureLogger.log(" - Adding offline favorite '\(favorite.peerNickname)' (key: \(favoriteKey.hexEncodedString()), ID: \(favoriteID), mutual: \(favorite.isMutual))",
|
|
||||||
category: SecureLogger.session, level: .info)
|
|
||||||
|
|
||||||
var peer = BitchatPeer(
|
|
||||||
id: favoriteID,
|
|
||||||
noisePublicKey: favorite.peerNoisePublicKey,
|
|
||||||
nickname: favorite.peerNickname,
|
|
||||||
isConnected: false
|
|
||||||
)
|
|
||||||
// Set favorite status
|
|
||||||
peer.favoriteStatus = favorite
|
|
||||||
peer.nostrPublicKey = favorite.peerNostrPublicKey
|
|
||||||
addedPeerIDs.insert(favoriteID) // Track that we've added this ID
|
|
||||||
allPeers.append(peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out "Unknown" peers unless they are favorites or have a favorite relationship
|
|
||||||
allPeers = allPeers.filter { peer in
|
|
||||||
!(peer.displayName == "Unknown" && peer.favoriteStatus == nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort: Connected first, then favorites, then alphabetical
|
|
||||||
allPeers.sort { lhs, rhs in
|
|
||||||
// Direct connections first
|
|
||||||
if lhs.isConnected != rhs.isConnected {
|
|
||||||
return lhs.isConnected
|
|
||||||
}
|
|
||||||
// Then favorites
|
|
||||||
if lhs.isFavorite != rhs.isFavorite {
|
|
||||||
return lhs.isFavorite
|
|
||||||
}
|
|
||||||
// Finally alphabetical
|
|
||||||
return lhs.displayName < rhs.displayName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single pass to compute all subsets and counts
|
|
||||||
var favorites: [BitchatPeer] = []
|
|
||||||
var mutualFavorites: [BitchatPeer] = []
|
|
||||||
var connectedCount = 0
|
|
||||||
var offlineCount = 0
|
|
||||||
|
|
||||||
for peer in allPeers {
|
|
||||||
if peer.isFavorite {
|
|
||||||
favorites.append(peer)
|
|
||||||
}
|
|
||||||
if peer.isMutualFavorite {
|
|
||||||
mutualFavorites.append(peer)
|
|
||||||
}
|
|
||||||
if peer.isConnected {
|
|
||||||
connectedCount += 1
|
|
||||||
} else {
|
|
||||||
offlineCount += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final safety check: ensure no duplicate IDs
|
|
||||||
var finalPeers: [BitchatPeer] = []
|
|
||||||
var seenIDs: Set<String> = []
|
|
||||||
for peer in allPeers {
|
|
||||||
if !seenIDs.contains(peer.id) {
|
|
||||||
seenIDs.insert(peer.id)
|
|
||||||
finalPeers.append(peer)
|
|
||||||
} else {
|
|
||||||
SecureLogger.log("⚠️ Removing duplicate peer ID in final check: \(peer.id) (\(peer.displayName))",
|
|
||||||
category: SecureLogger.session, level: .warning)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.peers = finalPeers
|
|
||||||
self.favorites = favorites
|
|
||||||
self.mutualFavorites = mutualFavorites
|
|
||||||
|
|
||||||
// Log peer list summary sparingly at debug level
|
|
||||||
if favoritesService.favorites.count > 0 {
|
|
||||||
SecureLogger.log("📊 Peer list update: \(allPeers.count) total (\(connectedCount) connected, \(offlineCount) offline), \(favorites.count) favorites, \(mutualFavorites.count) mutual",
|
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
} else if previousCount != allPeers.count {
|
|
||||||
SecureLogger.log("✅ Updated peer list: \(allPeers.count) total peers",
|
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toggleFavorite(_ peer: BitchatPeer) {
|
|
||||||
if peer.isFavorite {
|
|
||||||
favoritesService.removeFavorite(peerNoisePublicKey: peer.noisePublicKey)
|
|
||||||
} else {
|
|
||||||
favoritesService.addFavorite(
|
|
||||||
peerNoisePublicKey: peer.noisePublicKey,
|
|
||||||
peerNostrPublicKey: peer.nostrPublicKey,
|
|
||||||
peerNickname: peer.nickname
|
|
||||||
)
|
|
||||||
}
|
|
||||||
updatePeers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ struct NostrProtocol {
|
|||||||
|
|
||||||
// Log with explicit UTC and local time for debugging
|
// Log with explicit UTC and local time for debugging
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
// Removed unnecessary date formatting operations
|
//
|
||||||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
||||||
|
|
||||||
|
|||||||
@@ -181,8 +181,7 @@ enum LazyHandshakeState {
|
|||||||
case failed(Error) // Handshake failed
|
case failed(Error) // Handshake failed
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Special Recipients (removed)
|
//
|
||||||
// Previously defined broadcast identifiers were unused; removed for simplicity.
|
|
||||||
|
|
||||||
// MARK: - Core Protocol Structures
|
// MARK: - Core Protocol Structures
|
||||||
|
|
||||||
@@ -268,8 +267,7 @@ struct BitchatPacket: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Delivery Acknowledgments (removed)
|
//
|
||||||
// Legacy DeliveryAck structures are no longer used; delivery status flows via Noise payloads.
|
|
||||||
|
|
||||||
// MARK: - Read Receipts
|
// MARK: - Read Receipts
|
||||||
|
|
||||||
@@ -361,7 +359,7 @@ struct ReadReceipt: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// PeerIdentityBinding removed (unused).
|
//
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Delivery Status
|
// MARK: - Delivery Status
|
||||||
|
|||||||
@@ -730,7 +730,7 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed unused getPeers(): use getPeerNicknames() from Transport
|
//
|
||||||
|
|
||||||
// MARK: - Private Message Handling
|
// MARK: - Private Message Handling
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class CommandProcessor {
|
|||||||
case "/unfav":
|
case "/unfav":
|
||||||
if inGeoPublic || inGeoDM { return .error(message: "favorites are only for mesh peers in #mesh") }
|
if inGeoPublic || inGeoDM { return .error(message: "favorites are only for mesh peers in #mesh") }
|
||||||
return handleFavorite(args, add: false)
|
return handleFavorite(args, add: false)
|
||||||
// /help removed
|
//
|
||||||
case "/help", "/h":
|
case "/help", "/h":
|
||||||
return .error(message: "unknown command: \(cmd)")
|
return .error(message: "unknown command: \(cmd)")
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Missing properties that were removed during refactoring
|
//
|
||||||
private var peerIDToPublicKeyFingerprint: [String: String] = [:]
|
private var peerIDToPublicKeyFingerprint: [String: String] = [:]
|
||||||
private var selectedPrivateChatFingerprint: String? = nil
|
private var selectedPrivateChatFingerprint: String? = nil
|
||||||
// Map stable short peer IDs (16-hex) to full Noise public key hex (64-hex) for session continuity
|
// Map stable short peer IDs (16-hex) to full Noise public key hex (64-hex) for session continuity
|
||||||
@@ -1561,7 +1561,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
// Presence announcement removed; we will tag actual chat events instead
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Geohash Participants
|
// MARK: - Geohash Participants
|
||||||
@@ -1714,7 +1714,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
let toAdd = desired.subtracting(current)
|
let toAdd = desired.subtracting(current)
|
||||||
let toRemove = current.subtracting(desired)
|
let toRemove = current.subtracting(desired)
|
||||||
|
|
||||||
// Unsubscribe removed
|
//
|
||||||
for (subID, gh) in geoSamplingSubs where toRemove.contains(gh) {
|
for (subID, gh) in geoSamplingSubs where toRemove.contains(gh) {
|
||||||
NostrRelayManager.shared.unsubscribe(id: subID)
|
NostrRelayManager.shared.unsubscribe(id: subID)
|
||||||
geoSamplingSubs.removeValue(forKey: subID)
|
geoSamplingSubs.removeValue(forKey: subID)
|
||||||
@@ -3617,26 +3617,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return unifiedPeerService.getFingerprint(for: peerID)
|
return unifiedPeerService.getFingerprint(for: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getFingerprint_old(for peerID: String) -> String? {
|
//
|
||||||
// Remove debug logging to prevent console spam during view updates
|
|
||||||
|
|
||||||
// First try to get fingerprint from mesh service's peer ID rotation mapping
|
|
||||||
if let fingerprint = meshService.getFingerprint(for: peerID) {
|
|
||||||
return fingerprint
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check noise service (direct Noise session fingerprint)
|
|
||||||
if let fingerprint = meshService.getNoiseService().getPeerFingerprint(peerID) {
|
|
||||||
return fingerprint
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: check local mapping
|
|
||||||
if let fingerprint = peerIDToPublicKeyFingerprint[peerID] {
|
|
||||||
return fingerprint
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to resolve nickname for a peer ID through various sources
|
// Helper to resolve nickname for a peer ID through various sources
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -4034,7 +4016,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
messageRouter.flushOutbox(for: peerID)
|
messageRouter.flushOutbox(for: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection messages removed to reduce chat noise
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
func didDisconnectFromPeer(_ peerID: String) {
|
func didDisconnectFromPeer(_ peerID: String) {
|
||||||
@@ -4101,7 +4083,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnection messages removed to reduce chat noise
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
func didUpdatePeerList(_ peers: [String]) {
|
func didUpdatePeerList(_ peers: [String]) {
|
||||||
@@ -4414,7 +4396,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// MARK: - Simplified Nostr Integration (Inlined from MessageRouter)
|
// MARK: - Simplified Nostr Integration (Inlined from MessageRouter)
|
||||||
|
|
||||||
// Removed inlined Nostr send helpers in favor of MessageRouter
|
//
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func setupNostrMessageHandling() {
|
private func setupNostrMessageHandling() {
|
||||||
@@ -4745,7 +4727,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
return Data(base64Encoded: str)
|
return Data(base64Encoded: str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed local TLV decoder; using PrivateMessagePacket.decode from Protocols
|
//
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func handleFavoriteNotificationFromMesh(_ content: String, from peerID: String, senderNickname: String) {
|
private func handleFavoriteNotificationFromMesh(_ content: String, from peerID: String, senderNickname: String) {
|
||||||
|
|||||||
@@ -13,19 +13,9 @@ import UIKit
|
|||||||
|
|
||||||
// MARK: - Supporting Types
|
// MARK: - Supporting Types
|
||||||
|
|
||||||
// Pre-computed peer data for performance
|
//
|
||||||
struct PeerDisplayData: Identifiable {
|
|
||||||
let id: String
|
|
||||||
let displayName: String
|
|
||||||
let isFavorite: Bool
|
|
||||||
let isMe: Bool
|
|
||||||
let hasUnreadMessages: Bool
|
|
||||||
let encryptionStatus: EncryptionStatus
|
|
||||||
let connectionState: BitchatPeer.ConnectionState
|
|
||||||
let isMutualFavorite: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// (Link previews removed; URLs are now clickable inline)
|
//
|
||||||
|
|
||||||
// MARK: - Main Content View
|
// MARK: - Main Content View
|
||||||
|
|
||||||
@@ -1382,23 +1372,7 @@ private struct PaymentChipView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper view for rendering message content (plain, no hashtag/mention formatting)
|
//
|
||||||
struct MessageContentView: View {
|
|
||||||
let message: BitchatMessage
|
|
||||||
let viewModel: ChatViewModel
|
|
||||||
let colorScheme: ColorScheme
|
|
||||||
let isMentioned: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Text(message.content)
|
|
||||||
.font(.system(size: 14, design: .monospaced))
|
|
||||||
.fontWeight(isMentioned ? .bold : .regular)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
|
||||||
|
|
||||||
// buildTextSegments removed: content is rendered plain.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delivery status indicator view
|
// Delivery status indicator view
|
||||||
struct DeliveryStatusView: View {
|
struct DeliveryStatusView: View {
|
||||||
|
|||||||
@@ -7,160 +7,160 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Social
|
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
class ShareViewController: SLComposeServiceViewController {
|
/// Modern share extension using UIKit + UTTypes.
|
||||||
|
/// Avoids deprecated Social framework and SLComposeServiceViewController.
|
||||||
|
final class ShareViewController: UIViewController {
|
||||||
|
private let statusLabel: UILabel = {
|
||||||
|
let l = UILabel()
|
||||||
|
l.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
l.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||||
|
l.textAlignment = .center
|
||||||
|
l.numberOfLines = 0
|
||||||
|
l.textColor = .label
|
||||||
|
return l
|
||||||
|
}()
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
// Set placeholder text
|
view.backgroundColor = .systemBackground
|
||||||
placeholder = "Share to bitchat..."
|
view.addSubview(statusLabel)
|
||||||
// Set character limit (optional)
|
NSLayoutConstraint.activate([
|
||||||
charactersRemaining = 500
|
statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
statusLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.leadingAnchor),
|
||||||
|
statusLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.layoutMarginsGuide.trailingAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
processShare()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func isContentValid() -> Bool {
|
// MARK: - Processing
|
||||||
// Validate that we have text content or attachments
|
private func processShare() {
|
||||||
if let text = contentText, !text.isEmpty {
|
guard let ctx = self.extensionContext,
|
||||||
return true
|
let item = ctx.inputItems.first as? NSExtensionItem else {
|
||||||
}
|
finishWithMessage("Nothing to share")
|
||||||
// Check if we have attachments
|
|
||||||
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
|
|
||||||
let attachments = item.attachments,
|
|
||||||
!attachments.isEmpty {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didSelectPost() {
|
|
||||||
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else {
|
|
||||||
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try content from attributed text first (Safari often passes URL here)
|
||||||
|
if let url = detectURL(in: item.attributedContentText?.string ?? "") {
|
||||||
|
saveAndFinish(url: url, title: item.attributedTitle?.string)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get the page title from the compose view or extension item
|
// Scan attachments for URL/text
|
||||||
let pageTitle = self.contentText ?? extensionItem.attributedContentText?.string ?? extensionItem.attributedTitle?.string
|
let providers = item.attachments ?? []
|
||||||
|
if providers.isEmpty {
|
||||||
|
// Fallback: use attributed title as plain text
|
||||||
|
if let title = item.attributedTitle?.string, !title.isEmpty {
|
||||||
|
saveAndFinish(text: title)
|
||||||
|
} else {
|
||||||
|
finishWithMessage("No shareable content")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var foundURL: URL? = nil
|
// Load URL or text asynchronously
|
||||||
let group = DispatchGroup()
|
loadFirstURL(from: providers) { [weak self] url in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let url = url {
|
||||||
|
self.saveAndFinish(url: url, title: item.attributedTitle?.string)
|
||||||
|
} else {
|
||||||
|
self.loadFirstPlainText(from: providers) { text in
|
||||||
|
if let t = text, !t.isEmpty {
|
||||||
|
// Treat as URL if parseable http(s), else plain text
|
||||||
|
if let u = URL(string: t), ["http","https"].contains(u.scheme?.lowercased() ?? "") {
|
||||||
|
self.saveAndFinish(url: u, title: item.attributedTitle?.string)
|
||||||
|
} else {
|
||||||
|
self.saveAndFinish(text: t)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.finishWithMessage("No shareable content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IMPORTANT: Check if the NSExtensionItem itself has a URL
|
private func detectURL(in text: String) -> URL? {
|
||||||
// Safari often provides the URL as an attributedString with a link
|
guard !text.isEmpty else { return nil }
|
||||||
if let attributedText = extensionItem.attributedContentText {
|
|
||||||
let text = attributedText.string
|
|
||||||
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||||
let matches = detector?.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
|
let range = NSRange(location: 0, length: (text as NSString).length)
|
||||||
if let firstMatch = matches?.first, let url = firstMatch.url {
|
let match = detector?.matches(in: text, options: [], range: range).first
|
||||||
foundURL = url
|
return match?.url
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only check attachments if we haven't found a URL yet
|
private func loadFirstURL(from providers: [NSItemProvider], completion: @escaping (URL?) -> Void) {
|
||||||
if foundURL == nil {
|
let identifiers = [UTType.url.identifier, "public.url", "public.file-url"]
|
||||||
for (_, itemProvider) in (extensionItem.attachments ?? []).enumerated() {
|
let grp = DispatchGroup()
|
||||||
|
var found: URL?
|
||||||
|
|
||||||
// Try multiple URL type identifiers that Safari might use
|
for p in providers where found == nil {
|
||||||
let urlTypes = [
|
for id in identifiers where p.hasItemConformingToTypeIdentifier(id) {
|
||||||
UTType.url.identifier,
|
grp.enter()
|
||||||
"public.url",
|
p.loadItem(forTypeIdentifier: id, options: nil) { item, _ in
|
||||||
"public.file-url"
|
defer { grp.leave() }
|
||||||
]
|
if let u = item as? URL { found = u; return }
|
||||||
|
if let s = item as? String, let u = URL(string: s) { found = u; return }
|
||||||
for urlType in urlTypes {
|
if let d = item as? Data, let s = String(data: d, encoding: .utf8), let u = URL(string: s) { found = u; return }
|
||||||
if itemProvider.hasItemConformingToTypeIdentifier(urlType) {
|
}
|
||||||
group.enter()
|
break
|
||||||
itemProvider.loadItem(forTypeIdentifier: urlType, options: nil) { (item, error) in
|
|
||||||
defer { group.leave() }
|
|
||||||
|
|
||||||
if let url = item as? URL {
|
|
||||||
foundURL = url
|
|
||||||
} else if let data = item as? Data,
|
|
||||||
let urlString = String(data: data, encoding: .utf8),
|
|
||||||
let url = URL(string: urlString) {
|
|
||||||
foundURL = url
|
|
||||||
} else if let string = item as? String,
|
|
||||||
let url = URL(string: string) {
|
|
||||||
foundURL = url
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break // Found a URL type, no need to check other types
|
grp.notify(queue: .main) { completion(found) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check for plain text that might be a URL
|
private func loadFirstPlainText(from providers: [NSItemProvider], completion: @escaping (String?) -> Void) {
|
||||||
if foundURL == nil && itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
|
let id = UTType.plainText.identifier
|
||||||
group.enter()
|
let grp = DispatchGroup()
|
||||||
itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { (item, error) in
|
var text: String?
|
||||||
defer { group.leave() }
|
for p in providers where p.hasItemConformingToTypeIdentifier(id) {
|
||||||
|
grp.enter()
|
||||||
|
p.loadItem(forTypeIdentifier: id, options: nil) { item, _ in
|
||||||
|
defer { grp.leave() }
|
||||||
|
if let s = item as? String { text = s }
|
||||||
|
else if let d = item as? Data, let s = String(data: d, encoding: .utf8) { text = s }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
grp.notify(queue: .main) { completion(text) }
|
||||||
|
}
|
||||||
|
|
||||||
if let text = item as? String {
|
// MARK: - Save + Finish
|
||||||
// Check if the text is actually a URL
|
private func saveAndFinish(url: URL, title: String?) {
|
||||||
if let url = URL(string: text),
|
let payload: [String: String] = [
|
||||||
(url.scheme == "http" || url.scheme == "https") {
|
|
||||||
foundURL = url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // End of if foundURL == nil
|
|
||||||
|
|
||||||
// Process after all checks complete
|
|
||||||
group.notify(queue: .main) { [weak self] in
|
|
||||||
if let url = foundURL {
|
|
||||||
// We have a URL! Create the JSON data
|
|
||||||
let urlData: [String: String] = [
|
|
||||||
"url": url.absoluteString,
|
"url": url.absoluteString,
|
||||||
"title": pageTitle ?? url.host ?? "Shared Link"
|
"title": title ?? url.host ?? "Shared Link"
|
||||||
]
|
]
|
||||||
|
if let json = try? JSONSerialization.data(withJSONObject: payload),
|
||||||
|
let s = String(data: json, encoding: .utf8) {
|
||||||
if let jsonData = try? JSONSerialization.data(withJSONObject: urlData),
|
saveToSharedDefaults(content: s, type: "url")
|
||||||
let jsonString = String(data: jsonData, encoding: .utf8) {
|
finishWithMessage("✓ Shared link to bitchat")
|
||||||
self?.saveToSharedDefaults(content: jsonString, type: "url")
|
} else {
|
||||||
}
|
finishWithMessage("Failed to encode link")
|
||||||
} else if let title = pageTitle, !title.isEmpty {
|
|
||||||
// No URL found, just share the text
|
|
||||||
self?.saveToSharedDefaults(content: title, type: "text")
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func configurationItems() -> [Any]! {
|
private func saveAndFinish(text: String) {
|
||||||
// No configuration items needed
|
saveToSharedDefaults(content: text, type: "text")
|
||||||
return []
|
finishWithMessage("✓ Shared text to bitchat")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helper Methods
|
|
||||||
|
|
||||||
private func saveToSharedDefaults(content: String, type: String) {
|
private func saveToSharedDefaults(content: String, type: String) {
|
||||||
// Use app groups to share data between extension and main app
|
guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else { return }
|
||||||
guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userDefaults.set(content, forKey: "sharedContent")
|
userDefaults.set(content, forKey: "sharedContent")
|
||||||
userDefaults.set(type, forKey: "sharedContentType")
|
userDefaults.set(type, forKey: "sharedContentType")
|
||||||
userDefaults.set(Date(), forKey: "sharedContentDate")
|
userDefaults.set(Date(), forKey: "sharedContentDate")
|
||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
|
|
||||||
|
|
||||||
// Force open the main app
|
|
||||||
self.openMainApp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openMainApp() {
|
private func finishWithMessage(_ msg: String) {
|
||||||
// Share extensions cannot directly open the containing app
|
statusLabel.text = msg
|
||||||
// The app will check for shared content when it becomes active
|
// Complete shortly after showing status
|
||||||
// Show success feedback to user
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
DispatchQueue.main.async {
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
self.textView.text = "✓ Shared to bitchat"
|
|
||||||
self.textView.isEditable = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,10 @@ final class NostrProtocolTests: XCTestCase {
|
|||||||
let sender = try NostrIdentity.generate()
|
let sender = try NostrIdentity.generate()
|
||||||
let recipient = try NostrIdentity.generate()
|
let recipient = try NostrIdentity.generate()
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
print("Sender pubkey: \(sender.publicKeyHex)")
|
print("Sender pubkey: \(sender.publicKeyHex)")
|
||||||
print("Recipient pubkey: \(recipient.publicKeyHex)")
|
print("Recipient pubkey: \(recipient.publicKeyHex)")
|
||||||
|
#endif
|
||||||
|
|
||||||
// Create a test message
|
// Create a test message
|
||||||
let originalContent = "Hello from NIP-17 test!"
|
let originalContent = "Hello from NIP-17 test!"
|
||||||
@@ -34,8 +36,10 @@ final class NostrProtocolTests: XCTestCase {
|
|||||||
senderIdentity: sender
|
senderIdentity: sender
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
print("Gift wrap created with ID: \(giftWrap.id)")
|
print("Gift wrap created with ID: \(giftWrap.id)")
|
||||||
print("Gift wrap pubkey: \(giftWrap.pubkey)")
|
print("Gift wrap pubkey: \(giftWrap.pubkey)")
|
||||||
|
#endif
|
||||||
|
|
||||||
// Decrypt the gift wrap
|
// Decrypt the gift wrap
|
||||||
let (decryptedContent, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
|
let (decryptedContent, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
|
||||||
@@ -52,7 +56,9 @@ final class NostrProtocolTests: XCTestCase {
|
|||||||
let timeDiff = abs(messageDate.timeIntervalSinceNow)
|
let timeDiff = abs(messageDate.timeIntervalSinceNow)
|
||||||
XCTAssertLessThan(timeDiff, 60, "Message timestamp should be recent")
|
XCTAssertLessThan(timeDiff, 60, "Message timestamp should be recent")
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
print("✅ Successfully decrypted message: '\(decryptedContent)' from \(senderPubkey) at \(messageDate)")
|
print("✅ Successfully decrypted message: '\(decryptedContent)' from \(senderPubkey) at \(messageDate)")
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGiftWrapUsesUniqueEphemeralKeys() throws {
|
func testGiftWrapUsesUniqueEphemeralKeys() throws {
|
||||||
@@ -75,8 +81,10 @@ final class NostrProtocolTests: XCTestCase {
|
|||||||
|
|
||||||
// Gift wrap pubkeys should be different (unique ephemeral keys)
|
// Gift wrap pubkeys should be different (unique ephemeral keys)
|
||||||
XCTAssertNotEqual(message1.pubkey, message2.pubkey)
|
XCTAssertNotEqual(message1.pubkey, message2.pubkey)
|
||||||
|
#if DEBUG
|
||||||
print("Message 1 gift wrap pubkey: \(message1.pubkey)")
|
print("Message 1 gift wrap pubkey: \(message1.pubkey)")
|
||||||
print("Message 2 gift wrap pubkey: \(message2.pubkey)")
|
print("Message 2 gift wrap pubkey: \(message2.pubkey)")
|
||||||
|
#endif
|
||||||
|
|
||||||
// Both should decrypt successfully
|
// Both should decrypt successfully
|
||||||
let (content1, _, _) = try NostrProtocol.decryptPrivateMessage(
|
let (content1, _, _) = try NostrProtocol.decryptPrivateMessage(
|
||||||
@@ -109,7 +117,9 @@ final class NostrProtocolTests: XCTestCase {
|
|||||||
giftWrap: giftWrap,
|
giftWrap: giftWrap,
|
||||||
recipientIdentity: wrongRecipient
|
recipientIdentity: wrongRecipient
|
||||||
)) { error in
|
)) { error in
|
||||||
|
#if DEBUG
|
||||||
print("Expected error when decrypting with wrong key: \(error)")
|
print("Expected error when decrypting with wrong key: \(error)")
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
//
|
|
||||||
// LegacyTestProtocolTypes.swift
|
|
||||||
// bitchatTests
|
|
||||||
//
|
|
||||||
// Minimal legacy protocol types used only by tests to simulate old flows.
|
|
||||||
// These are not part of production code anymore.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct ProtocolNack {
|
|
||||||
let originalPacketID: String
|
|
||||||
let nackID: String
|
|
||||||
let senderID: String
|
|
||||||
let receiverID: String
|
|
||||||
let packetType: UInt8
|
|
||||||
let reason: String
|
|
||||||
let errorCode: UInt8
|
|
||||||
|
|
||||||
enum ErrorCode: UInt8 {
|
|
||||||
case unknown = 0
|
|
||||||
case decryptionFailed = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
init(originalPacketID: String, senderID: String, receiverID: String, packetType: UInt8, reason: String, errorCode: ErrorCode = .unknown) {
|
|
||||||
self.originalPacketID = originalPacketID
|
|
||||||
self.nackID = UUID().uuidString
|
|
||||||
self.senderID = senderID
|
|
||||||
self.receiverID = receiverID
|
|
||||||
self.packetType = packetType
|
|
||||||
self.reason = reason
|
|
||||||
self.errorCode = errorCode.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func toBinaryData() -> Data {
|
|
||||||
// Tests don't parse the payload; return a compact encoding for completeness
|
|
||||||
var data = Data()
|
|
||||||
data.appendUUID(originalPacketID)
|
|
||||||
data.appendUUID(nackID)
|
|
||||||
data.append(UInt8(packetType))
|
|
||||||
data.append(UInt8(errorCode))
|
|
||||||
data.appendString(reason)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
Command line invocation:
|
|
||||||
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -project bitchat.xcodeproj -scheme bitchat -configuration Debug -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES
|
|
||||||
|
|
||||||
Build settings from command line:
|
|
||||||
CODE_SIGNING_ALLOWED = NO
|
|
||||||
ONLY_ACTIVE_ARCH = YES
|
|
||||||
SDKROOT = iphonesimulator18.5
|
|
||||||
|
|
||||||
Resolve Package Graph
|
|
||||||
/Users/jack/Library/org.swift.swiftpm/configuration is not accessible or not writable, disabling user-level cache features./Users/jack/Library/org.swift.swiftpm/security is not accessible or not writable, disabling user-level cache features./Users/jack/Library/Caches/org.swift.swiftpm is not accessible or not writable, disabling user-level cache features.
|
|
||||||
Package: swift-secp256k1
|
|
||||||
|
|
||||||
fatalError
|
|
||||||
Reference in New Issue
Block a user