Full Code of permissionlesstech/bitchat for AI

main b6e45e3228e7 cached
254 files
3.1 MB
827.1k tokens
22 symbols
1 requests
Download .txt
Showing preview only (3,304K chars total). Download the full file or copy to clipboard to get everything.
Repository: permissionlesstech/bitchat
Branch: main
Commit: b6e45e3228e7
Files: 254
Total size: 3.1 MB

Directory structure:
gitextract_fcb18bnp/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── fetch_georelays.yml
│       └── swift-tests.yml
├── .gitignore
├── BRING_THE_NOISE.md
├── Configs/
│   ├── Debug.xcconfig
│   ├── Local.xcconfig.example
│   └── Release.xcconfig
├── Justfile
├── LICENSE
├── PRIVACY_POLICY.md
├── Package.resolved
├── Package.swift
├── README.md
├── WHITEPAPER.md
├── bitchat/
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── AppIcon.appiconset/
│   │   │   └── Contents.json
│   │   ├── AppIconDebug.appiconset/
│   │   │   └── Contents.json
│   │   └── Contents.json
│   ├── BitchatApp.swift
│   ├── Features/
│   │   ├── media/
│   │   │   └── ImageUtils.swift
│   │   └── voice/
│   │       ├── VoiceNotePlaybackController.swift
│   │       ├── VoiceRecorder.swift
│   │       └── Waveform.swift
│   ├── Identity/
│   │   ├── IdentityModels.swift
│   │   └── SecureIdentityStateManager.swift
│   ├── Info.plist
│   ├── LaunchScreen.storyboard
│   ├── Localizable.xcstrings
│   ├── Models/
│   │   ├── BitchatMessage.swift
│   │   ├── BitchatPacket.swift
│   │   ├── BitchatPeer.swift
│   │   ├── CommandInfo.swift
│   │   ├── MessagePadding.swift
│   │   ├── NoisePayload.swift
│   │   ├── PeerID.swift
│   │   ├── ReadReceipt.swift
│   │   └── RequestSyncPacket.swift
│   ├── Noise/
│   │   ├── NoiseProtocol.swift
│   │   ├── NoiseRateLimiter.swift
│   │   ├── NoiseSecurityConstants.swift
│   │   ├── NoiseSecurityError.swift
│   │   ├── NoiseSecurityValidator.swift
│   │   ├── NoiseSession.swift
│   │   ├── NoiseSessionError.swift
│   │   ├── NoiseSessionManager.swift
│   │   ├── NoiseSessionState.swift
│   │   └── SecureNoiseSession.swift
│   ├── Nostr/
│   │   ├── Bech32.swift
│   │   ├── GeoRelayDirectory.swift
│   │   ├── NostrEmbeddedBitChat.swift
│   │   ├── NostrIdentity.swift
│   │   ├── NostrIdentityBridge.swift
│   │   ├── NostrProtocol.swift
│   │   ├── NostrRelayManager.swift
│   │   └── XChaCha20Poly1305Compat.swift
│   ├── Protocols/
│   │   ├── BinaryEncodingUtils.swift
│   │   ├── BinaryProtocol.swift
│   │   ├── BitchatFilePacket.swift
│   │   ├── BitchatProtocol.swift
│   │   ├── Geohash.swift
│   │   ├── LocationChannel.swift
│   │   └── Packets.swift
│   ├── Services/
│   │   ├── AutocompleteService.swift
│   │   ├── BLE/
│   │   │   ├── BLEService.swift
│   │   │   └── MimeType.swift
│   │   ├── CommandProcessor.swift
│   │   ├── FavoritesPersistenceService.swift
│   │   ├── GeohashParticipantTracker.swift
│   │   ├── GeohashPresenceService.swift
│   │   ├── KeychainManager.swift
│   │   ├── LocationNotesManager.swift
│   │   ├── LocationStateManager.swift
│   │   ├── MeshTopologyTracker.swift
│   │   ├── MessageDeduplicationService.swift
│   │   ├── MessageFormattingEngine.swift
│   │   ├── MessageRouter.swift
│   │   ├── NetworkActivationService.swift
│   │   ├── NoiseEncryptionService.swift
│   │   ├── NostrTransport.swift
│   │   ├── NotificationService.swift
│   │   ├── NotificationStreamAssembler.swift
│   │   ├── PrivateChatManager.swift
│   │   ├── RelayController.swift
│   │   ├── TransferProgressManager.swift
│   │   ├── Transport.swift
│   │   ├── TransportConfig.swift
│   │   ├── UnifiedPeerService.swift
│   │   └── VerificationService.swift
│   ├── Sync/
│   │   ├── GCSFilter.swift
│   │   ├── GossipSyncManager.swift
│   │   ├── PacketIdUtil.swift
│   │   ├── RequestSyncManager.swift
│   │   └── SyncTypeFlags.swift
│   ├── Utils/
│   │   ├── Color+Peer.swift
│   │   ├── CompressionUtil.swift
│   │   ├── Data+SHA256.swift
│   │   ├── FileTransferLimits.swift
│   │   ├── Font+Bitchat.swift
│   │   ├── InputValidator.swift
│   │   ├── MessageDeduplicator.swift
│   │   ├── PeerDisplayNameResolver.swift
│   │   ├── String+DJB2.swift
│   │   └── String+Nickname.swift
│   ├── ViewModels/
│   │   ├── ChatViewModel.swift
│   │   ├── Extensions/
│   │   │   ├── ChatViewModel+Nostr.swift
│   │   │   ├── ChatViewModel+PrivateChat.swift
│   │   │   ├── ChatViewModel+Tor.swift
│   │   │   └── README.md
│   │   ├── GeoChannelCoordinator.swift
│   │   ├── MessageRateLimiter.swift
│   │   ├── MinimalDistancePalette.swift
│   │   ├── PublicMessagePipeline.swift
│   │   └── PublicTimelineStore.swift
│   ├── Views/
│   │   ├── AppInfoView.swift
│   │   ├── Components/
│   │   │   ├── CommandSuggestionsView.swift
│   │   │   ├── DeliveryStatusView.swift
│   │   │   ├── PaymentChipView.swift
│   │   │   └── TextMessageView.swift
│   │   ├── ContentView.swift
│   │   ├── FingerprintView.swift
│   │   ├── GeohashPeopleList.swift
│   │   ├── LocationChannelsSheet.swift
│   │   ├── LocationNotesView.swift
│   │   ├── Media/
│   │   │   ├── BlockRevealImageView.swift
│   │   │   ├── VoiceNoteView.swift
│   │   │   └── WaveformView.swift
│   │   ├── MeshPeerList.swift
│   │   ├── MessageTextHelpers.swift
│   │   └── VerificationViews.swift
│   ├── _PreviewHelpers/
│   │   ├── BitchatMessage+Preview.swift
│   │   └── PreviewKeychainManager.swift
│   ├── bitchat-macOS.entitlements
│   └── bitchat.entitlements
├── bitchat.xcodeproj/
│   ├── project.pbxproj
│   └── xcshareddata/
│       └── xcschemes/
│           ├── bitchat (iOS).xcscheme
│           └── bitchat (macOS).xcscheme
├── bitchatShareExtension/
│   ├── Info.plist
│   ├── Localization/
│   │   └── Localizable.xcstrings
│   ├── ShareViewController.swift
│   └── bitchatShareExtension.entitlements
├── bitchatTests/
│   ├── BLEServiceCoreTests.swift
│   ├── BLEServiceTests.swift
│   ├── BitchatPeerTests.swift
│   ├── ChatViewModelDeliveryStatusTests.swift
│   ├── ChatViewModelExtensionsTests.swift
│   ├── ChatViewModelRefactoringTests.swift
│   ├── ChatViewModelTests.swift
│   ├── ChatViewModelTorTests.swift
│   ├── CommandProcessorTests.swift
│   ├── EndToEnd/
│   │   ├── PrivateChatE2ETests.swift
│   │   └── PublicChatE2ETests.swift
│   ├── Features/
│   │   └── ImageUtilsTests.swift
│   ├── FontBitchatTests.swift
│   ├── Fragmentation/
│   │   └── FragmentationTests.swift
│   ├── GCSFilterTests.swift
│   ├── GeohashBookmarksStoreTests.swift
│   ├── GeohashParticipantTrackerTests.swift
│   ├── GeohashPresenceTests.swift
│   ├── GossipSyncManagerTests.swift
│   ├── Info.plist
│   ├── InputValidatorTests.swift
│   ├── Integration/
│   │   ├── IntegrationTests.swift
│   │   └── TestNetworkHelper.swift
│   ├── KeychainErrorHandlingTests.swift
│   ├── Localization/
│   │   └── PrimaryLocalizationKeys.json
│   ├── LocationChannelsTests.swift
│   ├── LocationNotesManagerTests.swift
│   ├── MessageDeduplicationServiceTests.swift
│   ├── MessageFormattingEngineTests.swift
│   ├── MimeTypeTests.swift
│   ├── Mocks/
│   │   ├── MockBLEBus.swift
│   │   ├── MockBLEService.swift
│   │   ├── MockIdentityManager.swift
│   │   ├── MockKeychain.swift
│   │   └── MockTransport.swift
│   ├── Noise/
│   │   ├── NoiseCoverageTests.swift
│   │   ├── NoiseProtocolTests.swift
│   │   ├── NoiseRateLimiterTests.swift
│   │   └── NoiseTestVectors.json
│   ├── Nostr/
│   │   └── GeoRelayDirectoryTests.swift
│   ├── NostrProtocolTests.swift
│   ├── NotificationBlockingTests.swift
│   ├── NotificationStreamAssemblerTests.swift
│   ├── PreviewKeychainManagerTests.swift
│   ├── Protocol/
│   │   ├── BinaryProtocolPaddingTests.swift
│   │   └── BinaryProtocolTests.swift
│   ├── ProtocolContractTests.swift
│   ├── Protocols/
│   │   ├── BinaryEncodingUtilsTests.swift
│   │   ├── BitchatFilePacketTests.swift
│   │   ├── LocationChannelTests.swift
│   │   └── PacketsTests.swift
│   ├── PublicMessagePipelineTests.swift
│   ├── PublicTimelineStoreTests.swift
│   ├── README.md
│   ├── ReadReceiptTests.swift
│   ├── Services/
│   │   ├── AutocompleteServiceTests.swift
│   │   ├── FavoritesPersistenceServiceTests.swift
│   │   ├── GeohashPresenceServiceTests.swift
│   │   ├── LocationStateManagerTests.swift
│   │   ├── MeshTopologyTrackerTests.swift
│   │   ├── MessageRouterTests.swift
│   │   ├── NetworkActivationServiceTests.swift
│   │   ├── NoiseEncryptionServiceTests.swift
│   │   ├── NostrRelayManagerTests.swift
│   │   ├── NostrTransportTests.swift
│   │   ├── NotificationServiceTests.swift
│   │   ├── PrivateChatManagerTests.swift
│   │   ├── RelayControllerTests.swift
│   │   ├── SecureIdentityStateManagerTests.swift
│   │   ├── TransferProgressManagerTests.swift
│   │   ├── UnifiedPeerServiceTests.swift
│   │   └── VerificationServiceTests.swift
│   ├── SubscriptionRateLimitTests.swift
│   ├── Sync/
│   │   └── RequestSyncManagerTests.swift
│   ├── TestUtilities/
│   │   ├── TestConstants.swift
│   │   └── TestHelpers.swift
│   ├── Utils/
│   │   ├── HexStringTests.swift
│   │   └── PeerIDTests.swift
│   ├── ViewSmokeTests.swift
│   └── XChaCha20Poly1305CompatTests.swift
├── docs/
│   ├── GeohashPresenceSpec.md
│   ├── REQUEST_SYNC_MANAGER.md
│   ├── SOURCE_ROUTING.md
│   ├── TOR-INTEGRATION.md
│   └── privacy-assessment.md
├── localPackages/
│   ├── Arti/
│   │   ├── .gitignore
│   │   ├── Cargo.toml
│   │   ├── Frameworks/
│   │   │   ├── arti.xcframework/
│   │   │   │   ├── Info.plist
│   │   │   │   ├── ios-arm64/
│   │   │   │   │   ├── Headers/
│   │   │   │   │   │   └── arti.h
│   │   │   │   │   └── libarti_bitchat.a
│   │   │   │   ├── ios-arm64-simulator/
│   │   │   │   │   ├── Headers/
│   │   │   │   │   │   └── arti.h
│   │   │   │   │   └── libarti_bitchat.a
│   │   │   │   └── macos-arm64/
│   │   │   │       ├── Headers/
│   │   │   │       │   └── arti.h
│   │   │   │       └── libarti_bitchat.a
│   │   │   └── include/
│   │   │       └── arti.h
│   │   ├── Package.swift
│   │   ├── Sources/
│   │   │   ├── C/
│   │   │   │   ├── arti_shim.c
│   │   │   │   └── include/
│   │   │   │       ├── arti.h
│   │   │   │       └── module.modulemap
│   │   │   ├── TorManager.swift
│   │   │   ├── TorNotifications.swift
│   │   │   └── TorURLSession.swift
│   │   ├── arti-bitchat/
│   │   │   ├── Cargo.toml
│   │   │   ├── cbindgen.toml
│   │   │   └── src/
│   │   │       ├── lib.rs
│   │   │       └── socks.rs
│   │   └── build-ios.sh
│   └── BitLogger/
│       ├── Package.swift
│       ├── Sources/
│       │   ├── OSLog+Categories.swift
│       │   ├── SecureLogger.swift
│       │   └── String+Sanitization.swift
│       └── Tests/
│           └── StringSanitizationTests.swift
└── relays/
    └── online_relays_gps.csv

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# Prevent Github Languages stats skewing:

# Binaries and assets
**/*.xcframework/** linguist-vendored
**/*.xcassets/** linguist-vendored

# Generated files
**/*.pbxproj linguist-generated
**/*.storyboard linguist-generated
Package.resolved linguist-generated

# Downloaded CSVs
relays/online_relays_gps.csv linguist-vendored

# Docs
**/*.md linguist-documentation

# Configs
Configs/*.xcconfig linguist-documentation
**/*.plist linguist-documentation


================================================
FILE: .github/workflows/fetch_georelays.yml
================================================
name: Fetch GeoRelays Data

on:
  schedule:
    - cron: '0 6 * * 0'
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  update-relay-data:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          fetch-depth: 0

      - name: Fetch GeoRelays
        run: |
          wget -q https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv
          mv nostr_relays.csv ./relays/online_relays_gps.csv

      - name: Check for changes
        id: git-check
        run: |
          git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT
    
      - name: Commit and push changes
        if: steps.git-check.outputs.changes == 'true'
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add relays/online_relays_gps.csv
          git commit -m "Automated update of relay data - $(date -u)"
          git push
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

================================================
FILE: .github/workflows/swift-tests.yml
================================================
name: Build & Test

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  test:
    name: Run Swift Tests
    runs-on: macos-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Set up Swift
        uses: swift-actions/setup-swift@v2

      - name: Build the package
        run: swift build

      - name: Run Tests
        run: swift test --parallel


================================================
FILE: .gitignore
================================================
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## implementation plans
plans/

## AI
CLAUDE.md
AGENTS.md

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
.DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Gcc Patch
/*.gcno

## macOS
.DS_Store

## SPM
.swiftpm
.build/

## CocoaPods
Pods/

## Carthage
Carthage/Build/

## fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

## Code Injection
iOSInjectionProject/

## Xcode project
*.xcodeproj/project.xcworkspace/
## Xcode User settings
xcuserdata/

## Python
__pycache__/
*.py[cod]
*$py.class

## Temporary files
*.tmp
*.temp

## Cache
.cache/

# Local build results
.Result*/
.Result*.xcresult/
TestResult.xcresult/
*.xcresult/
build.log
*.log

# Local configs
Local.xcconfig


================================================
FILE: BRING_THE_NOISE.md
================================================
# Bringing the Noise: Secure Communication in BitChat

## Overview

BitChat implements the Noise Protocol Framework for end-to-end encryption, providing forward secrecy, identity hiding, and cryptographic authentication. This document details our Swift implementation and its integration with BitChat's decentralized mesh network.

## The Noise Protocol Framework

### Why Noise?

The Noise Protocol Framework offers:
- **Forward Secrecy**: Past messages remain secure even if keys are compromised
- **Identity Hiding**: Peer identities are encrypted during handshake
- **Simplicity**: Clean, auditable protocol with minimal complexity
- **Performance**: Efficient for resource-constrained mobile devices
- **Flexibility**: Supports various handshake patterns

### The XX Pattern

BitChat uses the Noise XX pattern:
```
XX:
  -> e
  <- e, ee, s, es
  -> s, se
```

This three-message pattern provides:
- Mutual authentication
- Identity encryption (identities revealed only after initial key exchange)
- Resistance to key-compromise impersonation

## Implementation Architecture

### Core Components

#### NoiseEncryptionService
The main service managing all Noise operations:
```swift
final class NoiseEncryptionService {
    private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
    private let sessionManager: NoiseSessionManager
    private let channelEncryption = NoiseChannelEncryption()
}
```

#### NoiseSession
Individual session state for each peer:
```swift
final class NoiseSession {
    private var handshakeState: NoiseHandshakeState?
    private var sendCipher: NoiseCipherState?
    private var receiveCipher: NoiseCipherState?
    private let remoteStaticKey: Curve25519.KeyAgreement.PublicKey?
}
```

#### NoiseSessionManager
Thread-safe session management:
```swift
final class NoiseSessionManager {
    private var sessions: [String: NoiseSession] = [:]
    private let sessionsQueue = DispatchQueue(label: "noise.sessions", attributes: .concurrent)
}
```

### Handshake Flow

1. **Initiator sends ephemeral key**
   ```swift
   let ephemeralKey = Curve25519.KeyAgreement.PrivateKey()
   let message = ephemeralKey.publicKey.rawRepresentation
   ```

2. **Responder sends ephemeral + encrypted static**
   ```swift
   // Generate ephemeral, perform DH, encrypt static key
   let encryptedStatic = encrypt(staticKey, using: sharedSecret)
   ```

3. **Initiator sends encrypted static**
   ```swift
   // Complete handshake, derive session keys
   let (sendKey, recvKey) = deriveSessionKeys(transcript)
   ```

### Session Management

Sessions are managed with automatic cleanup and rekey support:

```swift
// Session lookup by peer ID
func getSession(for peerID: String) -> NoiseSession?

// Automatic session removal on disconnect
func removeSession(for peerID: String)

// Rekey detection
func getSessionsNeedingRekey() -> [(String, Bool)]
```

## Integration with BitChat

### Peer ID Rotation

Noise sessions persist across peer ID rotations through fingerprint mapping:

```swift
// Identity announcement after handshake
struct NoiseIdentityAnnouncement {
    let peerID: String
    let publicKey: Data
    let nickname: String
    let previousPeerID: String?
    let signature: Data
}
```

### Message Encryption

All messages are encrypted using established Noise sessions:

```swift
// Encrypt message
let encrypted = try noiseService.encrypt(messageData, for: peerID)

// Decrypt message  
let decrypted = try noiseService.decrypt(encryptedData, from: peerID)
```

## Security Properties

### Forward Secrecy
- Ephemeral keys are generated for each handshake
- Past sessions cannot be decrypted with current keys
- Automatic rekey after 1 hour or 10,000 messages

### Authentication
- Static keys provide long-term identity
- Handshake ensures mutual authentication
- MAC tags prevent message tampering

### Privacy
- Peer identities encrypted during handshake
- Metadata minimization through padding
- No persistent session identifiers

## Implementation Details

### Cryptographic Primitives
- **DH**: X25519 (Curve25519)
- **Cipher**: ChaChaPoly (AEAD)
- **Hash**: SHA-256
- **KDF**: HKDF-SHA256

### Error Handling
```swift
enum NoiseError: Error {
    case handshakeFailed
    case invalidMessage
    case sessionNotEstablished
    case decryptionFailed
}
```

## Performance Optimizations

### Connection Pooling
- Reuse established sessions
- Lazy handshake initiation
- Session caching with TTL

### Message Batching
- Combine small messages
- Reduce encryption overhead
- Optimize for BLE MTU

### Memory Management
- Bounded session cache
- Automatic cleanup of stale sessions
- Efficient key rotation

## Protocol Version Negotiation

BitChat implements protocol version negotiation to ensure compatibility between different client versions:

### Version Negotiation Flow
1. **Version Hello**: Upon connection, peers exchange supported protocol versions
2. **Version Agreement**: Peers agree on the highest common version
3. **Graceful Fallback**: Legacy peers without version negotiation assume protocol v1

### Message Types
```swift
case versionHello = 0x20    // Announce supported versions
case versionAck = 0x21      // Acknowledge and agree on version
```

### Backward Compatibility
- Peers that don't send version negotiation messages are assumed to support v1
- Future protocol versions can be added to `ProtocolVersion.supportedVersions`
- Incompatible peers receive a rejection message and disconnect gracefully

## Future Enhancements

### Post-Quantum Readiness
- Hybrid handshake patterns
- Kyber integration plans
- Graceful algorithm migration

### Advanced Features
- Multi-device support
- Session backup/restore
- Group messaging primitives

## Conclusion

BitChat's Noise implementation provides encryption while maintaining the simplicity and performance required for a peer-to-peer messaging application. The protocol's elegant design ensures that people's communications remain private, authenticated, and forward-secure without sacrificing usability.


================================================
FILE: Configs/Debug.xcconfig
================================================
#include "Release.xcconfig"

// Optional include of local configs
#include? "Local.xcconfig"


================================================
FILE: Configs/Local.xcconfig.example
================================================
// Your Apple Developer Team ID - https://stackoverflow.com/a/18727947
DEVELOPMENT_TEAM = ABC123

// Unique bundle id to be able to register and run locally
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.$(DEVELOPMENT_TEAM)


================================================
FILE: Configs/Release.xcconfig
================================================
MARKETING_VERSION = 1.5.1
CURRENT_PROJECT_VERSION = 1

IPHONEOS_DEPLOYMENT_TARGET = 16.0
MACOSX_DEPLOYMENT_TARGET = 13.0
SWIFT_VERSION = 5.0

DEVELOPMENT_TEAM = L3N5LHJD5Y
CODE_SIGN_STYLE = Automatic

PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat


================================================
FILE: Justfile
================================================
# BitChat macOS Build Justfile
# Handles temporary modifications needed to build and run on macOS

# Default recipe - shows available commands
default:
    @echo "BitChat macOS Build Commands:"
    @echo "  just run     - Build and run the macOS app"
    @echo "  just build   - Build the macOS app only"
    @echo "  just clean   - Clean build artifacts and restore original files"
    @echo "  just check   - Check prerequisites"
    @echo ""
    @echo "Original files are preserved - modifications are temporary for builds only"

# Check prerequisites
check:
    @echo "Checking prerequisites..."
    @command -v xcodebuild >/dev/null 2>&1 || (echo "❌ xcodebuild not found. Install Xcode from App Store" && exit 1)
    @xcode-select -p | grep -q "Xcode.app" || (echo "❌ Full Xcode required, not just command line tools. Install from App Store and run:\n   sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" && exit 1)
    @test -d "/Applications/Xcode.app" || (echo "❌ Xcode.app not found in Applications folder. Install from App Store" && exit 1)
    @xcodebuild -version >/dev/null 2>&1 || (echo "❌ Xcode not properly configured. Try:\n   sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" && exit 1)
    @security find-identity -v -p codesigning | grep -q "Apple Development\|Developer ID" || (echo "⚠️  No Developer ID found - code signing may fail" && exit 0)
    @echo "✅ All prerequisites met"

# Backup original files
backup:
    @echo "Backing up original project configuration..."
    @if [ -f bitchat.xcodeproj/project.pbxproj ]; then cp bitchat.xcodeproj/project.pbxproj bitchat.xcodeproj/project.pbxproj.backup; fi
    @if [ -f bitchat/Info.plist ]; then cp bitchat/Info.plist bitchat/Info.plist.backup; fi

# Restore original files
restore:
    @echo "Restoring original project configuration..."
    @if [ -f project.yml.backup ]; then mv project.yml.backup project.yml; fi
    @# Restore iOS-specific files
    @if [ -f bitchat/LaunchScreen.storyboard.ios ]; then mv bitchat/LaunchScreen.storyboard.ios bitchat/LaunchScreen.storyboard; fi
    @# Use git to restore all modified files except Justfile
    @git checkout -- project.yml bitchat.xcodeproj/project.pbxproj bitchat/Info.plist 2>/dev/null || echo "⚠️  Could not restore some files with git"
    @# Remove any backup files
    @rm -f bitchat.xcodeproj/project.pbxproj.backup bitchat/Info.plist.backup 2>/dev/null || true

# Apply macOS-specific modifications
patch-for-macos: backup
    @echo "Temporarily hiding iOS-specific files for macOS build..."
    @# Move iOS-specific files out of the way temporarily
    @if [ -f bitchat/LaunchScreen.storyboard ]; then mv bitchat/LaunchScreen.storyboard bitchat/LaunchScreen.storyboard.ios; fi

# Build the macOS app
build: #check generate
    @echo "Building BitChat for macOS..."
    @xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build

# Run the macOS app
run: build
    @echo "Launching BitChat..."
    @find ~/Library/Developer/Xcode/DerivedData -name "bitchat.app" -path "*/Debug/*" -not -path "*/Index.noindex/*" | head -1 | xargs -I {} open "{}"

# Clean build artifacts and restore original files
clean: restore
    @echo "Cleaning build artifacts..."
    @rm -rf ~/Library/Developer/Xcode/DerivedData/bitchat-* 2>/dev/null || true
    @# Only remove the generated project if we have a backup, otherwise use git
    @if [ -f bitchat.xcodeproj/project.pbxproj.backup ]; then \
        rm -rf bitchat.xcodeproj; \
    else \
        git checkout -- bitchat.xcodeproj/project.pbxproj 2>/dev/null || echo "⚠️  Could not restore project.pbxproj"; \
    fi
    @rm -f project-macos.yml 2>/dev/null || true
    @echo "✅ Cleaned and restored original files"

# Quick run without cleaning (for development)
dev-run: check
    @echo "Quick development build..."
    @xcodebuild -project bitchat.xcodeproj -scheme "bitchat_macOS" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build
    @find ~/Library/Developer/Xcode/DerivedData -name "bitchat.app" -path "*/Debug/*" -not -path "*/Index.noindex/*" | head -1 | xargs -I {} open "{}"

# Show app info
info:
    @echo "BitChat - Decentralized Mesh Messaging"
    @echo "======================================"
    @echo "• Native macOS SwiftUI app"
    @echo "• Bluetooth LE mesh networking"
    @echo "• End-to-end encryption"
    @echo "• No internet required"
    @echo "• Works offline with nearby devices"
    @echo ""
    @echo "Requirements:"
    @echo "• macOS 13.0+ (Ventura)"
    @echo "• Bluetooth LE capable Mac"
    @echo "• Physical device (no simulator support)"
    @echo ""
    @echo "Usage:"
    @echo "• Set nickname and start chatting"
    @echo "• Use /join #channel for group chats"
    @echo "• Use /msg @user for private messages"
    @echo "• Triple-tap logo for emergency wipe"

# Force clean everything (nuclear option)
nuke:
    @echo "🧨 Nuclear clean - removing all build artifacts and backups..."
    @rm -rf ~/Library/Developer/Xcode/DerivedData/bitchat-* 2>/dev/null || true
    @rm -rf bitchat.xcodeproj 2>/dev/null || true
    @rm -f bitchat.xcodeproj/project.pbxproj.backup 2>/dev/null || true
    @rm -f bitchat/Info.plist.backup 2>/dev/null || true
    @# Restore iOS-specific files if they were moved
    @if [ -f bitchat/LaunchScreen.storyboard.ios ]; then mv bitchat/LaunchScreen.storyboard.ios bitchat/LaunchScreen.storyboard; fi
    @git checkout bitchat.xcodeproj/project.pbxproj bitchat/Info.plist 2>/dev/null || echo "⚠️  Not a git repo or no changes to restore"
    @echo "✅ Nuclear clean complete"


================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <https://unlicense.org>

================================================
FILE: PRIVACY_POLICY.md
================================================
# bitchat Privacy Policy

*Last updated: January 2025*

## Our Commitment

bitchat is designed with privacy as its foundation. We believe private communication is a fundamental human right. This policy explains how bitchat protects your privacy.

## Summary

- **No personal data collection** - We don't collect names, emails, or phone numbers
- **No servers** - Everything happens on your device and through peer-to-peer connections
- **No tracking** - We have no analytics, telemetry, or user tracking
- **Open source** - You can verify these claims by reading our code

## What Information bitchat Stores

### On Your Device Only

1. **Identity Key** 
   - A cryptographic key generated on first launch
   - Stored locally in your device's secure storage
   - Allows you to maintain "favorite" relationships across app restarts
   - Never leaves your device

2. **Nickname**
   - The display name you choose (or auto-generated)
   - Stored only on your device
   - Shared with peers you communicate with

3. **Message History** (if enabled)
   - When room owners enable retention, messages are saved locally
   - Stored encrypted on your device
   - You can delete this at any time

4. **Favorite Peers**
   - Public keys of peers you mark as favorites
   - Stored only on your device
   - Allows you to recognize these peers in future sessions

### Temporary Session Data

During each session, bitchat temporarily maintains:
- Active peer connections (forgotten when app closes)
- Routing information for message delivery
- Cached messages for offline peers (12 hours max)

## What Information is Shared

### With Other bitchat Users

When you use bitchat, nearby peers can see:
- Your chosen nickname
- Your ephemeral public key (changes each session)
- Messages you send to public rooms or directly to them
- Your approximate Bluetooth signal strength (for connection quality)

### With Room Members

When you join a password-protected room:
- Your messages are visible to others with the password
- Your nickname appears in the member list
- Room owners can see you've joined

## What We DON'T Do

bitchat **never**:
- Collects personal information
- Tracks your location
- Stores data on servers
- Shares data with third parties
- Uses analytics or telemetry
- Creates user profiles
- Requires registration

## Encryption

All private messages use end-to-end encryption:
- **X25519** for key exchange
- **AES-256-GCM** for message encryption
- **Ed25519** for digital signatures
- **Argon2id** for password-protected rooms

## Your Rights

You have complete control:
- **Delete Everything**: Triple-tap the logo to instantly wipe all data
- **Leave Anytime**: Close the app and your presence disappears
- **No Account**: Nothing to delete from servers because there are none
- **Portability**: Your data never leaves your device unless you export it

## Bluetooth & Permissions

bitchat requires Bluetooth permission to function:
- Used only for peer-to-peer communication
- No location data is accessed or stored
- Bluetooth is not used for tracking
- You can revoke this permission at any time in system settings

## Children's Privacy

bitchat does not knowingly collect information from children. The app has no age verification because it collects no personal information from anyone.

## Data Retention

- **Messages**: Deleted from memory when app closes (unless room retention is enabled)
- **Identity Key**: Persists until you delete the app
- **Favorites**: Persist until you remove them or delete the app
- **Everything Else**: Exists only during active sessions

## Security Measures

- All communication is encrypted
- No data transmitted to servers (there are none)
- Open source code for public audit
- Regular security updates
- Cryptographic signatures prevent tampering

## Changes to This Policy

If we update this policy:
- The "Last updated" date will change
- The updated policy will be included in the app
- No retroactive changes can affect data (since we don't collect any)

## Contact

bitchat is an open source project. For privacy questions:
- View our source code: [https://github.com/permissionlesstech/bitchat/tree/main](https://github.com/permissionlesstech/bitchat/tree/main)
- Open an issue on GitHub
- Join the discussion in public rooms

## Philosophy

Privacy isn't just a feature—it's the entire point. bitchat proves that modern communication doesn't require surrendering your privacy. No accounts, no servers, no surveillance. Just people talking freely.

---

*This policy is released into the public domain under The Unlicense, just like bitchat itself.*


================================================
FILE: Package.resolved
================================================
{
  "pins" : [
    {
      "identity" : "swift-secp256k1",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/21-DOT-DEV/swift-secp256k1",
      "state" : {
        "revision" : "8c62aba8a3011c9bcea232e5ee007fb0b34a15e2",
        "version" : "0.21.1"
      }
    }
  ],
  "version" : 2
}


================================================
FILE: Package.swift
================================================
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
    name: "bitchat",
    defaultLocalization: "en",
    platforms: [
        .iOS(.v16),
        .macOS(.v13)
    ],
    products: [
        .executable(
            name: "bitchat",
            targets: ["bitchat"]
        ),
    ],
    dependencies:[
        .package(path: "localPackages/Arti"),
        .package(path: "localPackages/BitLogger"),
        .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1")
    ],
    targets: [
        .executableTarget(
            name: "bitchat",
            dependencies: [
                .product(name: "P256K", package: "swift-secp256k1"),
                .product(name: "BitLogger", package: "BitLogger"),
                .product(name: "Tor", package: "Arti")
            ],
            path: "bitchat",
            exclude: [
                "Info.plist",
                "Assets.xcassets",
                "bitchat.entitlements",
                "bitchat-macOS.entitlements",
                "LaunchScreen.storyboard",
                "ViewModels/Extensions/README.md"
            ],
            resources: [
                .process("Localizable.xcstrings")
            ]
        ),
        .testTarget(
            name: "bitchatTests",
            dependencies: ["bitchat"],
            path: "bitchatTests",
            exclude: [
                "Info.plist",
                "README.md"
            ],
            resources: [
                .process("Localization"),
                .process("Noise")
            ]
        )
    ]
)


================================================
FILE: README.md
================================================
<img width="256" height="256" alt="icon_128x128@2x" src="https://github.com/user-attachments/assets/90133f83-b4f6-41c6-aab9-25d0859d2a47" />

## bitchat

A decentralized peer-to-peer messaging app with dual transport architecture: local Bluetooth mesh networks for offline communication and internet-based Nostr protocol for global reach. No accounts, no phone numbers, no central servers. It's the side-groupchat.

[bitchat.free](http://bitchat.free)

📲 [App Store](https://apps.apple.com/us/app/bitchat-mesh/id6748219622)

## License

This project is released into the public domain. See the [LICENSE](LICENSE) file for details.

## Features

- **Dual Transport Architecture**: Bluetooth mesh for offline + Nostr protocol for internet-based messaging
- **Location-Based Channels**: Geographic chat rooms using geohash coordinates over global Nostr relays
- **Intelligent Message Routing**: Automatically chooses best transport (Bluetooth → Nostr fallback)
- **Decentralized Mesh Network**: Automatic peer discovery and multi-hop message relay over Bluetooth LE
- **Privacy First**: No accounts, no phone numbers, no persistent identifiers
- **Private Message End-to-End Encryption**: [Noise Protocol](https://noiseprotocol.org) for mesh, NIP-17 for Nostr
- **IRC-Style Commands**: Familiar `/slap`, `/msg`, `/who` style interface
- **Universal App**: Native support for iOS and macOS
- **Emergency Wipe**: Triple-tap to instantly clear all data
- **Performance Optimizations**: LZ4 message compression, adaptive battery modes, and optimized networking

## [Technical Architecture](https://deepwiki.com/permissionlesstech/bitchat)

BitChat uses a **hybrid messaging architecture** with two complementary transport layers:

### Bluetooth Mesh Network (Offline)

- **Local Communication**: Direct peer-to-peer within Bluetooth range
- **Multi-hop Relay**: Messages route through nearby devices (max 7 hops)
- **No Internet Required**: Works completely offline in disaster scenarios
- **Noise Protocol Encryption**: End-to-end encryption with forward secrecy
- **Binary Protocol**: Compact packet format optimized for Bluetooth LE constraints
- **Automatic Discovery**: Peer discovery and connection management
- **Adaptive Power**: Battery-optimized duty cycling

### Nostr Protocol (Internet)

- **Global Reach**: Connect with users worldwide via internet relays
- **Location Channels**: Geographic chat rooms using geohash coordinates
- **290+ Relay Network**: Distributed across the globe for reliability
- **NIP-17 Encryption**: Gift-wrapped private messages for internet privacy
- **Ephemeral Keys**: Fresh cryptographic identity per geohash area

### Channel Types

#### `mesh #bluetooth`

- **Transport**: Bluetooth Low Energy mesh network
- **Scope**: Local devices within multi-hop range
- **Internet**: Not required
- **Use Case**: Offline communication, protests, disasters, remote areas

#### Location Channels (`block #dr5rsj7`, `neighborhood #dr5rs`, `country #dr`)

- **Transport**: Nostr protocol over internet
- **Scope**: Geographic areas defined by geohash precision
  - `block` (7 chars): City block level
  - `neighborhood` (6 chars): District/neighborhood
  - `city` (5 chars): City level
  - `province` (4 chars): State/province
  - `region` (2 chars): Country/large region
- **Internet**: Required (connects to Nostr relays)
- **Use Case**: Location-based community chat, local events, regional discussions

### Direct Message Routing

Private messages use **intelligent transport selection**:

1. **Bluetooth First** (preferred when available)

   - Direct connection with established Noise session
   - Fastest and most private option

2. **Nostr Fallback** (when Bluetooth unavailable)

   - Uses recipient's Nostr public key
   - NIP-17 gift-wrapping for privacy
   - Routes through global relay network

3. **Smart Queuing** (when neither available)
   - Messages queued until transport becomes available
   - Automatic delivery when connection established

For detailed protocol documentation, see the [Technical Whitepaper](WHITEPAPER.md).

## Setup

### Option 1: Using Xcode

   ```bash
   cd bitchat
   open bitchat.xcodeproj
   ```

   To run on a device there're a few steps to prepare the code:
   - Clone the local configs: `cp Configs/Local.xcconfig.example Configs/Local.xcconfig`
   - Add your Developer Team ID into the newly created `Configs/Local.xcconfig`
      - Bundle ID would be set to `chat.bitchat.<team_id>` (unless you set to something else)
   - Entitlements need to be updated manually (TODO: Automate):
      - Search and replace `group.chat.bitchat` with `group.<your_bundle_id>` (e.g. `group.chat.bitchat.ABC123`)

### Option 2: Using `just`

   ```bash
   brew install just
   ```

Want to try this on macos: `just run` will set it up and run from source.
Run `just clean` afterwards to restore things to original state for mobile app building and development.

## Localization

- Base app resources live under `bitchat/Localization/Base.lproj/`. Add new copy to `Localizable.strings` and plural rules to `Localizable.stringsdict`.
- Share extension strings are separate in `bitchatShareExtension/Localization/Base.lproj/Localizable.strings`.
- Prefer keys that describe intent (`app_info.features.offline.title`) and reuse existing ones where possible.
- Run `xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGNING_ALLOWED=NO build` to compile-check any localization updates.


================================================
FILE: WHITEPAPER.md
================================================
# BitChat Protocol Whitepaper

**Version 1.1**

**Date: July 25, 2025**

---

## Abstract

BitChat is a decentralized, peer-to-peer messaging application designed for secure, private, and censorship-resistant communication over ephemeral, ad-hoc networks. This whitepaper details the BitChat Protocol Stack, a layered architecture that combines a modern cryptographic foundation with a flexible application protocol. At its core, BitChat leverages the Noise Protocol Framework (specifically, the `XX` pattern) to establish mutually authenticated, end-to-end encrypted sessions between peers. This document provides a technical specification of the identity management, session lifecycle, message framing, and security considerations that underpin the BitChat network.

---

## 1. Introduction

In an era of centralized communication platforms, BitChat offers a resilient alternative by operating without central servers. It is designed for scenarios where internet connectivity is unavailable or untrustworthy, such as protests, natural disasters, or remote areas. Communication occurs directly between devices over transports like Bluetooth Low Energy (BLE).

The design goals of the BitChat Protocol are:

*   **Confidentiality:** All communication must be unreadable to third parties.
*   **Authentication:** Users must be able to verify the identity of their correspondents.
*   **Integrity:** Messages cannot be tampered with in transit.
*   **Forward Secrecy:** The compromise of long-term identity keys must not compromise past session keys.
*   **Deniability:** It should be difficult to cryptographically prove that a specific user sent a particular message.
*   **Resilience:** The protocol must function reliably in lossy, low-bandwidth environments.

This paper specifies the technical details of the protocol designed to meet these goals.

---

## 2. Protocol Stack

The BitChat Protocol is a four-layer stack. This layered approach separates concerns, allowing for modularity and future extensibility.

```mermaid
graph TD
    A[Application Layer] --> B[Session Layer];
    B --> C[Encryption Layer];
    C --> D[Transport Layer];

    subgraph "BitChat Application"
        A
    end

    subgraph "Message Framing & State"
        B
    end

    subgraph "Noise Protocol Framework"
        C
    end

    subgraph "BLE, Wi-Fi Direct, etc."
        D
    end

    style A fill:#cde4ff
    style B fill:#b5d8ff
    style C fill:#9ac2ff
    style D fill:#7eadff
```

*   **Application Layer:** Defines the structure of user-facing messages (`BitchatMessage`), acknowledgments (`DeliveryAck`), and other application-level data.
*   **Session Layer:** Manages the overall communication packet (`BitchatPacket`). This includes routing information (TTL), message typing, fragmentation, and serialization into a compact binary format.
*   **Encryption Layer:** Establishes and manages secure channels using the Noise Protocol Framework. It is responsible for the cryptographic handshake, session management, and transport message encryption/decryption.
*   **Transport Layer:** The underlying physical medium used for data transmission, such as Bluetooth Low Energy (BLE). This layer is abstracted away from the core protocol.

---

## 3. Identity and Key Management

A peer's identity in BitChat is defined by two persistent cryptographic key pairs, which are generated on first launch and stored securely in the device's Keychain.

1.  **Noise Static Key Pair (`Curve25519`):** This is the long-term identity key used for the Noise Protocol handshake. The public part of this key is shared with peers to establish secure sessions.
2.  **Signing Key Pair (`Ed25519`):** This key is used to sign announcements and other protocol messages where non-repudiation is required, such as binding a public key to a nickname.

### 3.1. Fingerprint

A user's unique, verifiable fingerprint is the **SHA-256 hash** of their **Noise static public key**. This provides a user-friendly and secure way to verify an identity out-of-band (e.g., by reading it aloud or scanning a QR code).

`Fingerprint = SHA256(StaticPublicKey_Curve25519)`

### 3.2. Identity Management

The `SecureIdentityStateManager` class is responsible for managing all cryptographic identity material and social metadata (petnames, trust levels, etc.). It uses an in-memory cache for performance and persists this cache to the Keychain after encrypting it with a separate AES-GCM key.

---

## 4. The Social Trust Layer

Beyond cryptographic identity, BitChat incorporates a social trust layer, allowing users to manage their relationships with peers. This functionality is handled by the `SecureIdentityStateManager`.

### 4.1. Peer Verification

While the Noise handshake cryptographically authenticates a peer's key, it doesn't confirm the real-world identity of the person holding the device. To solve this, users can perform out-of-band (OOB) verification by comparing fingerprints. Once a user confirms that a peer's fingerprint matches the one they expect, they can mark that peer as "verified". This status is stored locally and displayed in the UI, providing a strong assurance of identity for future conversations.

### 4.2. Favorites and Blocking

To improve the user experience and provide control over interactions, the protocol supports:
*   **Favorites:** Users can mark trusted or frequently contacted peers as "favorites". This is a local designation that can be used by the application to prioritize notifications or display peers more prominently.
*   **Blocking:** Users can block peers. When a peer is blocked, the application will discard any incoming packets from that peer's fingerprint at the earliest possible stage, effectively silencing them without notifying the blocked peer.

---

## 5. The Noise Protocol Layer

BitChat implements the Noise Protocol Framework to provide strong, authenticated end-to-end encryption.

### 5.1. Protocol Name

The specific Noise protocol implemented is:

**`Noise_XX_25519_ChaChaPoly_SHA256`**

*   **`XX` Pattern:** This handshake pattern provides mutual authentication and forward secrecy. It does not require either party to know the other's static public key before the handshake begins. The keys are exchanged and authenticated during the three-part handshake. This is ideal for a decentralized P2P environment.
*   **`25519`:** The Diffie-Hellman function used is Curve25519.
*   **`ChaChaPoly`:** The AEAD (Authenticated Encryption with Associated Data) cipher is ChaCha20-Poly1305.
*   **`SHA256`:** The hash function used for all cryptographic hashing operations is SHA-256.

### 5.2. The `XX` Handshake

The `XX` handshake consists of three messages exchanged between an Initiator and a Responder to establish a shared secret and derive transport encryption keys.

```mermaid
sequenceDiagram
    participant I as Initiator
    participant R as Responder

    Note over I, R: Pre-computation: h = SHA256(protocol_name)

    I->>R: -> e
    Note right of I: I generates ephemeral key `e_i`.<br/>h = SHA256(h + e_i.pub)

    R->>I: <- e, ee, s, es
    Note left of R: R generates ephemeral key `e_r`.<br/>h = SHA256(h + e_r.pub)<br/>MixKey(DH(e_i, e_r))<br/>R sends static key `s_r`, encrypted.<br/>h = SHA256(h + ciphertext)<br/>MixKey(DH(e_i, s_r))

    I->>R: -> s, se
    Note right of I: I decrypts and verifies `s_r`.<br/>I sends static key `s_i`, encrypted.<br/>h = SHA256(h + ciphertext)<br/>MixKey(DH(s_i, e_r))

    Note over I, R: Handshake complete. Transport keys derived.
```

**Handshake Flow:**

1.  **Initiator -> Responder:** The initiator generates a new ephemeral key pair (`e_i`) and sends the public part to the responder.
2.  **Responder -> Initiator:** The responder receives the initiator's ephemeral public key. It then generates its own ephemeral key pair (`e_r`), performs a DH exchange with the initiator's ephemeral key (`ee`), sends its own static public key (`s_r`) encrypted with the resulting symmetric key, and performs another DH exchange between the initiator's ephemeral key and its own static key (`es`).
3.  **Initiator -> Responder:** The initiator receives the responder's message, decrypts the responder's static key, and authenticates it. The initiator then sends its own static key (`s_i`) encrypted and performs a final DH exchange between its static key and the responder's ephemeral key (`se`).

Upon completion, both parties share a set of symmetric keys for bidirectional transport message encryption. The final handshake hash is used for channel binding.

### 5.3. Session Management

The `NoiseSessionManager` class manages all active Noise sessions. It handles:
*   Creating sessions for new peers.
*   Coordinating the handshake process to prevent race conditions.
*   Storing the resulting transport ciphers (`sendCipher`, `receiveCipher`).
*   Periodically checking if sessions need to be re-keyed for enhanced security.

---

## 6. The BitChat Session and Application Protocol

Once a Noise session is established, peers exchange `BitchatPacket` structures, which are encrypted as the payload of Noise transport messages.

### 6.1. Binary Packet Format (`BitchatPacket`)

To minimize bandwidth, `BitchatPacket`s are serialized into a compact binary format. The structure is designed to be fixed-size where possible to resist traffic analysis.

| Field           | Size (bytes) | Description                                                                                             |
|-----------------|--------------|---------------------------------------------------------------------------------------------------------|
| **Header**      | **13**       | **Fixed-size header**                                                                                   |
| Version         | 1            | Protocol version (currently `1`).                                                                       |
| Type            | 1            | Message type (e.g., `message`, `deliveryAck`, `noiseHandshakeInit`). See `MessageType` enum.            |
| TTL             | 1            | Time-To-Live for mesh network routing. Decremented at each hop.                                         |
| Timestamp       | 8            | `UInt64` millisecond timestamp of packet creation.                                                      |
| Flags           | 1            | Bitmask for optional fields (`hasRecipient`, `hasSignature`, `isCompressed`).                           |
| Payload Length  | 2            | `UInt16` length of the payload field.                                                                   |
| **Variable**    | **...**      | **Variable-size fields**                                                                                |
| Sender ID       | 8            | 8-byte truncated peer ID of the sender.                                                                 |
| Recipient ID    | 8 (optional) | 8-byte truncated peer ID of the recipient. Present if `hasRecipient` flag is set. Broadcast if `0xFF..FF`. |
| Payload         | Variable     | The actual content of the packet, as defined by the `Type` field.                                       |
| Signature       | 64 (optional)| `Ed25519` signature of the packet. Present if `hasSignature` flag is set.                               |

**Padding:** All packets are padded to the next standard block size (256, 512, 1024, or 2048 bytes) using a PKCS#7-style scheme to obscure the true message length from network observers.

```mermaid
---
config:
  theme: dark
---
---
title: "BitchatPacket"
---
packet
+8: "Version"
+8: "Type"
+8: "TTL"
+64: "Timestamp"
+8: "Flags"
+16: "Payload Length"
+64: "Sender ID"
+64: "Recipient ID (optional)"
+48: "Payload (variable)"
+64: "Signature (optional)"
```
_A representation of the sizes of the fields in `BitchatPacket`_

### 6.2. Application Message Format (`BitchatMessage`)

For packets of type `message`, the payload is a binary-serialized `BitchatMessage` containing the chat content.

| Field               | Size (bytes) | Description                                                              |
|---------------------|--------------|--------------------------------------------------------------------------|
| Flags               | 1            | Bitmask for optional fields (`isRelay`, `isPrivate`, `hasOriginalSender`). |
| Timestamp           | 8            | `UInt64` millisecond timestamp of message creation.                      |
| ID                  | 1 + len      | `UUID` string for the message.                                           |
| Sender              | 1 + len      | Nickname of the sender.                                                  |
| Content             | 2 + len      | The UTF-8 encoded message content.                                       |
| Original Sender     | 1 + len (opt)| Nickname of the original sender if the message is a relay.               |
| Recipient Nickname  | 1 + len (opt)| Nickname of the recipient for private messages.                          |

```mermaid
---
config:
  theme: dark
---
---
title: "BitchatMessage"
---
packet
+8: "Flags"
+64: "Timestamp"
+24: "ID (variable)"
+32: "Sender (variable)"
+32: "Content (variable)"
+32: "Original Sender (variable) (optional)"
+32: "Recipient Nickname (variable) (optional)"
```
_A representation of the sizes of the fields in `BitchatMessage`_

---

## 7. Message Routing and Propagation

BitChat operates as a decentralized mesh network, meaning there are no central servers to route messages. Packets are propagated through the network from peer to peer. The protocol supports several modes of message delivery.

### 7.1. Direct Connection

This is the simplest case. If Peer A and Peer B are directly connected, they can exchange packets after establishing a mutually authenticated Noise session. All packets are encrypted using the transport ciphers derived from the handshake.

### 7.2. Efficient Gossip with Bloom Filters

To send messages to peers that are not directly connected, BitChat employs a "flooding" or "gossip" protocol. When a peer receives a packet that is not destined for it, it acts as a relay. To prevent infinite routing loops and minimize memory usage, the protocol uses an `OptimizedBloomFilter` to track recently seen packet IDs.

The logic is as follows:

1.  A peer receives a packet.
2.  It checks the Bloom filter to see if the packet's ID has likely been seen before. If so, the packet is discarded. Bloom filters can have false positives (though they are rare), but they guarantee no false negatives. This means that while some packets may be incorrectly discarded due to false positives, the gossip protocol's redundancy ensures these packets will eventually be received through subsequent exchanges with other peers.
3.  If the packet is new, its ID is added to the Bloom filter.
4.  The peer decrements the packet's Time-To-Live (TTL) field.
5.  If the TTL is greater than zero, the peer re-broadcasts the packet to all of its connected peers, *except* for the peer from which it received the packet.

This mechanism allows packets to "flood" through the network efficiently, maximizing the chance of reaching their destination while using minimal resources to prevent loops.

### 7.3. Time-To-Live (TTL)

Every `BitchatPacket` contains an 8-bit TTL field. This value is set by the originating peer and is decremented by one at each relay hop. If a peer receives a packet and decrements its TTL to 0, it will process the packet (if it is the recipient) but will not relay it further. This is a crucial mechanism to prevent packets from circulating endlessly in the mesh.

### 7.4. Private vs. Broadcast Messages

The routing logic respects the confidentiality of private messages:

*   **Private Messages:** A packet with a specific `recipientID` is a private message. Relay nodes forward the entire, encrypted Noise message without being able to access the inner `BitchatPacket` or its payload. Only the final recipient, who shares the correct Noise session keys with the sender, can decrypt the packet.
*   **Broadcast Messages:** A packet with the special broadcast `recipientID` (`0xFFFFFFFFFFFFFFFF`) is intended for all peers. Any peer that receives and decrypts a broadcast message will process its content. It will still be relayed according to the flooding algorithm to ensure it reaches the entire network.

### 7.5. Message Reliability and Lifecycle

To function in unreliable, lossy networks, the protocol includes features to track the lifecycle of a message and ensure its delivery.

*   **Delivery Acknowledgments (`DeliveryAck`):** When a private message reaches its final destination, the recipient's device sends a `DeliveryAck` packet back to the original sender. This acknowledgment contains the ID of the original message.
*   **Read Receipts (`ReadReceipt`):** After a message is displayed on the recipient's screen, the application can send a `ReadReceipt`, also containing the original message ID, to inform the sender that the message has been seen.
*   **Message Retry Service:** Senders maintain a `MessageRetryService` which tracks outgoing messages. If a `DeliveryAck` is not received for a message within a certain time window, the service will automatically re-send the message, creating a more resilient user experience.

### 7.6. Fragmentation

Transport layers like BLE have a Maximum Transmission Unit (MTU) that limits the size of a single packet. To handle messages larger than this limit, BitChat implements a fragmentation protocol.

*   **`fragmentStart`:** A packet with this type marks the beginning of a fragmented message. It contains metadata about the total size and number of fragments.
*   **`fragmentContinue`:** These packets carry the intermediate chunks of the message data.
*   **`fragmentEnd`:** This packet carries the final chunk of the message and signals the receiver to begin reassembly.

Receiving peers collect all fragments and reassemble them in the correct order before passing the complete message up to the application layer.

---

## 8. Security Considerations

*   **Replay Attacks:** The Noise transport messages include a nonce that is incremented for each message. The `NoiseCipherState` implements a sliding window replay protection mechanism to detect and discard replayed or out-of-order messages.
*   **Denial of Service:** The `NoiseRateLimiter` is implemented to prevent resource exhaustion from rapid, repeated handshake attempts from a single peer.
*   **Key-Compromise Impersonation:** The `XX` pattern authenticates both parties, preventing an attacker from impersonating one party to the other.
*   **Identity Binding:** While the Noise handshake authenticates the cryptographic keys, binding those keys to a human-readable nickname is handled at the application layer. Users must verify fingerprints out-of-band to prevent man-in-the-middle attacks.
*   **Traffic Analysis:** The use of fixed-size padding for all packets helps to obscure the exact nature and content of the communication, making it harder for a network-level adversary to infer information based on message size.

---

## 9. Conclusion

The BitChat Protocol provides a robust and secure foundation for decentralized, peer-to-peer communication. By layering a flexible application protocol on top of the well-regarded Noise Protocol Framework, it achieves strong confidentiality, authentication, and forward secrecy. The use of a compact binary format and thoughtful security considerations like rate limiting and traffic analysis resistance make it suitable for use in challenging network environments.


================================================
FILE: bitchat/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
  "colors" : [
    {
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0.000",
          "green" : "1.000",
          "red" : "0.000"
        }
      },
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

================================================
FILE: bitchat/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "icon_1024x1024.png",
      "idiom" : "universal",
      "platform" : "ios",
      "size" : "1024x1024"
    },
    {
      "filename" : "icon_16x16.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "16x16"
    },
    {
      "filename" : "icon_16x16@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "16x16"
    },
    {
      "filename" : "icon_32x32.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "32x32"
    },
    {
      "filename" : "icon_32x32@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "32x32"
    },
    {
      "filename" : "icon_128x128.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "128x128"
    },
    {
      "filename" : "icon_128x128@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "128x128"
    },
    {
      "filename" : "icon_256x256.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "256x256"
    },
    {
      "filename" : "icon_256x256@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "256x256"
    },
    {
      "filename" : "icon_512x512.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "512x512"
    },
    {
      "filename" : "icon_512x512@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "512x512"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: bitchat/Assets.xcassets/AppIconDebug.appiconset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "image-1024.png",
      "idiom" : "universal",
      "platform" : "ios",
      "size" : "1024x1024"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "idiom" : "universal",
      "platform" : "ios",
      "size" : "1024x1024"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "tinted"
        }
      ],
      "idiom" : "universal",
      "platform" : "ios",
      "size" : "1024x1024"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: bitchat/Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: bitchat/BitchatApp.swift
================================================
//
// BitchatApp.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//

import Tor
import SwiftUI
import UserNotifications

@main
struct BitchatApp: App {
    static let bundleID = Bundle.main.bundleIdentifier ?? "chat.bitchat"
    static let groupID = "group.\(bundleID)"
    
    @StateObject private var chatViewModel: ChatViewModel
    #if os(iOS)
    @Environment(\.scenePhase) var scenePhase
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    // Skip the very first .active-triggered Tor restart on cold launch
    @State private var didHandleInitialActive: Bool = false
    @State private var didEnterBackground: Bool = false
    #elseif os(macOS)
    @NSApplicationDelegateAdaptor(MacAppDelegate.self) var appDelegate
    #endif
    
    private let idBridge = NostrIdentityBridge()
    
    init() {
        let keychain = KeychainManager()
        let idBridge = self.idBridge
        _chatViewModel = StateObject(
            wrappedValue: ChatViewModel(
                keychain: keychain,
                idBridge: idBridge,
                identityManager: SecureIdentityStateManager(keychain)
            )
        )
        
        UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
        // Warm up georelay directory and refresh if stale (once/day)
        GeoRelayDirectory.shared.prefetchIfNeeded()
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(chatViewModel)
                .onAppear {
                    NotificationDelegate.shared.chatViewModel = chatViewModel
                    // Inject live Noise service into VerificationService to avoid creating new BLE instances
                    VerificationService.shared.configure(with: chatViewModel.meshService.getNoiseService())
                    // Prewarm Nostr identity and QR to make first VERIFY sheet fast
                    let nickname = chatViewModel.nickname
                    DispatchQueue.global(qos: .utility).async {
                        let npub = try? idBridge.getCurrentNostrIdentity()?.npub
                        _ = VerificationService.shared.buildMyQRString(nickname: nickname, npub: npub)
                    }

                    appDelegate.chatViewModel = chatViewModel

                    // Initialize network activation policy; will start Tor/Nostr only when allowed
                    NetworkActivationService.shared.start()
                    
                    // Start presence service (will wait for Tor readiness)
                    GeohashPresenceService.shared.start()

                    // Check for shared content
                    checkForSharedContent()
                }
                .onOpenURL { url in
                    handleURL(url)
                }
                #if os(iOS)
                .onChange(of: scenePhase) { newPhase in
                    switch newPhase {
                    case .background:
                        // Keep BLE mesh running in background; BLEService adapts scanning automatically
                        // Always send Tor to dormant on background for a clean restart later.
                        TorManager.shared.setAppForeground(false)
                        TorManager.shared.goDormantOnBackground()
                        // Stop geohash sampling while backgrounded
                        Task { @MainActor in
                            chatViewModel.endGeohashSampling()
                        }
                        // Proactively disconnect Nostr to avoid spurious socket errors while Tor is down
                        NostrRelayManager.shared.disconnect()
                        didEnterBackground = true
                    case .active:
                        // Restart services when becoming active
                        chatViewModel.meshService.startServices()
                        TorManager.shared.setAppForeground(true)
                        // On initial cold launch, Tor was just started in onAppear.
                        // Skip the deterministic restart the first time we become active.
                        if didHandleInitialActive && didEnterBackground {
                            if TorManager.shared.isAutoStartAllowed() && !TorManager.shared.isReady {
                                TorManager.shared.ensureRunningOnForeground()
                            }
                        } else {
                            didHandleInitialActive = true
                        }
                        didEnterBackground = false
                        if TorManager.shared.isAutoStartAllowed() {
                            Task.detached {
                                let _ = await TorManager.shared.awaitReady(timeout: 60)
                                await MainActor.run {
                                    // Rebuild proxied sessions to bind to the live Tor after readiness
                                    TorURLSession.shared.rebuild()
                                    // Reconnect Nostr via fresh sessions; will gate until Tor 100%
                                    NostrRelayManager.shared.resetAllConnections()
                                }
                            }
                        }
                        checkForSharedContent()
                    case .inactive:
                        break
                    @unknown default:
                        break
                    }
                }
                .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
                    // Check for shared content when app becomes active
                    checkForSharedContent()
                }
                #elseif os(macOS)
                .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
                    // App became active
                }
                #endif
        }
        #if os(macOS)
        .windowStyle(.hiddenTitleBar)
        .windowResizability(.contentSize)
        #endif
    }
    
    private func handleURL(_ url: URL) {
        if url.scheme == "bitchat" && url.host == "share" {
            // Handle shared content
            checkForSharedContent()
        }
    }
    
    private func checkForSharedContent() {
        // Check app group for shared content from extension
        guard let userDefaults = UserDefaults(suiteName: BitchatApp.groupID) else {
            return
        }
        
        guard let sharedContent = userDefaults.string(forKey: "sharedContent"),
              let sharedDate = userDefaults.object(forKey: "sharedContentDate") as? Date else {
            return
        }
        
        // Only process if shared within configured window
        if Date().timeIntervalSince(sharedDate) < TransportConfig.uiShareAcceptWindowSeconds {
            let contentType = userDefaults.string(forKey: "sharedContentType") ?? "text"
            
            // Clear the shared content
            userDefaults.removeObject(forKey: "sharedContent")
            userDefaults.removeObject(forKey: "sharedContentType")
            userDefaults.removeObject(forKey: "sharedContentDate")
            // No need to force synchronize here
            
            // Send the shared content immediately on the main queue
            DispatchQueue.main.async {
                if contentType == "url" {
                    // Try to parse as JSON first
                    if let data = sharedContent.data(using: .utf8),
                       let urlData = try? JSONSerialization.jsonObject(with: data) as? [String: String],
                       let url = urlData["url"] {
                        // Send plain URL
                        self.chatViewModel.sendMessage(url)
                    } else {
                        // Fallback to simple URL
                        self.chatViewModel.sendMessage(sharedContent)
                    }
                } else {
                    self.chatViewModel.sendMessage(sharedContent)
                }
            }
        }
    }
}

#if os(iOS)
final class AppDelegate: NSObject, UIApplicationDelegate {
    weak var chatViewModel: ChatViewModel?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        return true
    }
    
    func applicationWillTerminate(_ application: UIApplication) {
        chatViewModel?.applicationWillTerminate()
    }
}
#endif

#if os(macOS)
import AppKit

final class MacAppDelegate: NSObject, NSApplicationDelegate {
    weak var chatViewModel: ChatViewModel?
    
    func applicationWillTerminate(_ notification: Notification) {
        chatViewModel?.applicationWillTerminate()
    }
    
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }
}
#endif

final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    static let shared = NotificationDelegate()
    weak var chatViewModel: ChatViewModel?
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let identifier = response.notification.request.identifier
        let userInfo = response.notification.request.content.userInfo
        
        // Check if this is a private message notification
        if identifier.hasPrefix("private-") {
            // Get peer ID from userInfo
            if let peerID = userInfo["peerID"] as? String {
                DispatchQueue.main.async {
                    self.chatViewModel?.startPrivateChat(with: PeerID(str: peerID))
                }
            }
        }
        // Handle deeplink (e.g., geohash activity)
        if let deep = userInfo["deeplink"] as? String, let url = URL(string: deep) {
            #if os(iOS)
            DispatchQueue.main.async { UIApplication.shared.open(url) }
            #else
            DispatchQueue.main.async { NSWorkspace.shared.open(url) }
            #endif
        }
        
        completionHandler()
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let identifier = notification.request.identifier
        let userInfo = notification.request.content.userInfo
        
        // Check if this is a private message notification
        if identifier.hasPrefix("private-") {
            // Get peer ID from userInfo
            if let peerID = userInfo["peerID"] as? String {
                // Don't show notification if the private chat is already open
                // Access main-actor-isolated property via Task
                Task { @MainActor in
                    if self.chatViewModel?.selectedPrivateChatPeer == PeerID(str: peerID) {
                        completionHandler([])
                    } else {
                        completionHandler([.banner, .sound])
                    }
                }
                return
            }
        }
        // Suppress geohash activity notification if we're already in that geohash channel
        if identifier.hasPrefix("geo-activity-"),
           let deep = userInfo["deeplink"] as? String,
           let gh = deep.components(separatedBy: "/").last {
            if case .location(let ch) = LocationChannelManager.shared.selectedChannel, ch.geohash == gh {
                completionHandler([])
                return
            }
        }
        
        // Show notification in all other cases
        completionHandler([.banner, .sound])
    }
}



================================================
FILE: bitchat/Features/media/ImageUtils.swift
================================================
import Foundation
import ImageIO
import UniformTypeIdentifiers
#if os(iOS)
import UIKit
#else
import AppKit
#endif

enum ImageUtilsError: Error {
    case invalidImage
    case encodingFailed
}

enum ImageUtils {
    private static let compressionQuality: CGFloat = 0.82
    private static let targetImageBytes: Int = 45_000

    static func processImage(at url: URL, maxDimension: CGFloat = 448) throws -> URL {
        // Security H1: Check file size BEFORE reading into memory
        let attrs = try FileManager.default.attributesOfItem(atPath: url.path)
        guard let fileSize = attrs[.size] as? Int else {
            throw ImageUtilsError.invalidImage
        }
        // Allow up to 10MB source images (will be scaled down)
        guard fileSize <= 10 * 1024 * 1024 else {
            throw ImageUtilsError.invalidImage
        }

        let data = try Data(contentsOf: url)
        #if os(iOS)
        guard let image = UIImage(data: data) else { throw ImageUtilsError.invalidImage }
        return try processImage(image, maxDimension: maxDimension)
        #else
        guard let image = NSImage(data: data) else { throw ImageUtilsError.invalidImage }
        return try processImage(image, maxDimension: maxDimension)
        #endif
    }

    #if os(iOS)
    static func processImage(_ image: UIImage, maxDimension: CGFloat = 448) throws -> URL {
        return try autoreleasepool {
            // Scale the image first
            let scaled = scaledImage(image, maxDimension: maxDimension)

            // Get CGImage from UIImage - this is the key to stripping metadata
            guard let cgImage = scaled.cgImage else {
                throw ImageUtilsError.encodingFailed
            }

            // Use CGImageDestination to encode without metadata (same as macOS)
            var quality = compressionQuality
            guard var jpegData = encodeJPEG(from: cgImage, quality: quality) else {
                throw ImageUtilsError.encodingFailed
            }

            // Compress to target size
            while jpegData.count > targetImageBytes && quality > 0.3 {
                quality -= 0.1
                autoreleasepool {
                    if let next = encodeJPEG(from: cgImage, quality: quality) {
                        jpegData = next
                    }
                }
            }

            let outputURL = try makeOutputURL()
            try jpegData.write(to: outputURL, options: .atomic)
            return outputURL
        }
    }

    private static func scaledImage(_ image: UIImage, maxDimension: CGFloat) -> UIImage {
        let size = image.size
        let maxSide = max(size.width, size.height)
        guard maxSide > maxDimension else { return image }
        let scale = maxDimension / maxSide
        let newSize = CGSize(width: size.width * scale, height: size.height * scale)

        // Draw into a new context to get a clean CGImage without metadata
        UIGraphicsBeginImageContextWithOptions(newSize, true, 1.0)
        image.draw(in: CGRect(origin: .zero, size: newSize))
        let rendered = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return rendered ?? image
    }

    // Shared EXIF-stripping JPEG encoder for both iOS and macOS
    private static func encodeJPEG(from cgImage: CGImage, quality: CGFloat) -> Data? {
        guard let data = CFDataCreateMutable(nil, 0) else {
            return nil
        }
        guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
            return nil
        }
        // Security: Strip ALL metadata (EXIF, GPS, TIFF, IPTC, XMP)
        // By only specifying compression quality and no metadata keys,
        // we ensure a clean JPEG with no privacy-leaking information
        let options: [CFString: Any] = [
            kCGImageDestinationLossyCompressionQuality: quality
        ]
        CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
        guard CGImageDestinationFinalize(destination) else {
            return nil
        }
        return data as Data
    }
    #else
    static func processImage(_ image: NSImage, maxDimension: CGFloat = 448) throws -> URL {
        return try autoreleasepool {
            let scaled = scaledImage(image, maxDimension: maxDimension)
            guard let inputCG = scaled.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
                throw ImageUtilsError.encodingFailed
            }
            let width = inputCG.width
            let height = inputCG.height
            let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
            guard let context = CGContext(
                data: nil,
                width: width,
                height: height,
                bitsPerComponent: 8,
                bytesPerRow: 0,
                space: colorSpace,
                bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
            ) else {
                throw ImageUtilsError.encodingFailed
            }
            context.draw(inputCG, in: CGRect(x: 0, y: 0, width: width, height: height))
            guard let cgImage = context.makeImage() else {
                throw ImageUtilsError.encodingFailed
            }
            var quality = compressionQuality
            guard var jpegData = encodeJPEG(from: cgImage, quality: quality) else {
                throw ImageUtilsError.encodingFailed
            }
            while jpegData.count > targetImageBytes && quality > 0.3 {
                quality -= 0.1
                autoreleasepool {
                    if let next = encodeJPEG(from: cgImage, quality: quality) {
                        jpegData = next
                    }
                }
            }
            let outputURL = try makeOutputURL()
            try jpegData.write(to: outputURL, options: .atomic)
            return outputURL
        }
    }

    private static func scaledImage(_ image: NSImage, maxDimension: CGFloat) -> NSImage {
        let size = image.size
        let maxSide = max(size.width, size.height)
        guard maxSide > maxDimension else { return image }
        let scale = maxDimension / maxSide
        let newSize = NSSize(width: size.width * scale, height: size.height * scale)
        let scaledImage = NSImage(size: newSize)
        scaledImage.lockFocus()
        image.draw(in: NSRect(origin: .zero, size: newSize),
                   from: NSRect(origin: .zero, size: size),
                   operation: .copy,
                   fraction: 1.0)
        scaledImage.unlockFocus()
        return scaledImage
    }

    // Shared EXIF-stripping JPEG encoder for both iOS and macOS
    private static func encodeJPEG(from cgImage: CGImage, quality: CGFloat) -> Data? {
        guard let data = CFDataCreateMutable(nil, 0) else {
            return nil
        }
        guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
            return nil
        }
        // Security: Strip ALL metadata (EXIF, GPS, TIFF, IPTC, XMP)
        // By only specifying compression quality and no metadata keys,
        // we ensure a clean JPEG with no privacy-leaking information
        let options: [CFString: Any] = [
            kCGImageDestinationLossyCompressionQuality: quality
        ]
        CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
        guard CGImageDestinationFinalize(destination) else {
            return nil
        }
        return data as Data
    }
    #endif

    private static func makeOutputURL() throws -> URL {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMdd_HHmmss"
        let fileName = "img_\(formatter.string(from: Date())).jpg"

        let directory = try applicationFilesDirectory().appendingPathComponent("images/outgoing", isDirectory: true)
        try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
        return directory.appendingPathComponent(fileName)
    }

    private static func applicationFilesDirectory() throws -> URL {
        let base = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        return base.appendingPathComponent("files", isDirectory: true)
    }
}


================================================
FILE: bitchat/Features/voice/VoiceNotePlaybackController.swift
================================================
import Foundation
import AVFoundation
import BitLogger

/// Controls playback for a single voice note and coordinates exclusive playback across the app.
final class VoiceNotePlaybackController: NSObject, ObservableObject, AVAudioPlayerDelegate {
    @Published private(set) var isPlaying: Bool = false
    @Published private(set) var currentTime: TimeInterval = 0
    @Published private(set) var duration: TimeInterval = 0
    @Published private(set) var progress: Double = 0

    private var player: AVAudioPlayer?
    private var timer: Timer?
    private var url: URL

    init(url: URL) {
        self.url = url
        super.init()
        // Don't load anything eagerly - wait until user interaction or view is fully displayed
    }

    func loadDuration() {
        guard duration == 0 else { return }

        DispatchQueue.global(qos: .utility).async { [weak self] in
            guard let self = self else { return }
            do {
                let player = try AVAudioPlayer(contentsOf: self.url)
                let loadedDuration = player.duration
                DispatchQueue.main.async { [weak self] in
                    guard let self = self, self.duration == 0 else { return }
                    self.duration = loadedDuration
                }
            } catch {
                SecureLogger.error("Failed to load audio duration: \(error)", category: .session)
            }
        }
    }

    deinit {
        timer?.invalidate()
    }

    func replaceURL(_ url: URL) {
        guard url != self.url else { return }
        stop()
        self.url = url
        player = nil
        duration = 0
        // Duration will be loaded on demand when needed
    }

    func togglePlayback() {
        isPlaying ? pause() : play()
    }

    func play() {
        guard ensurePlayerReady() else { return }
        VoiceNotePlaybackCoordinator.shared.activate(self)
        player?.play()
        startTimer()
        updateProgress()
        isPlaying = true
    }

    func pause() {
        player?.pause()
        stopTimer()
        updateProgress()
        isPlaying = false
    }

    func stop() {
        player?.stop()
        player?.currentTime = 0
        stopTimer()
        updateProgress()
        isPlaying = false
        VoiceNotePlaybackCoordinator.shared.deactivate(self)
    }

    func seek(to fraction: Double) {
        guard ensurePlayerReady() else { return }
        let clamped = max(0, min(1, fraction))
        if let player = player {
            player.currentTime = clamped * player.duration
            if isPlaying {
                player.play()
            }
            updateProgress()
        }
    }

    // MARK: - AVAudioPlayerDelegate

    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        // Delegate callback may be on background thread - ensure main thread for UI updates
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.stopTimer()
            self.updateProgress()
            self.isPlaying = false
            VoiceNotePlaybackCoordinator.shared.deactivate(self)
        }
    }

    // MARK: - Private Helpers

    private func preparePlayer(for url: URL) {
        // Prepare player synchronously (only called when playback is requested)
        do {
            let player = try AVAudioPlayer(contentsOf: url)
            player.delegate = self
            player.prepareToPlay()
            self.player = player
            duration = player.duration
            currentTime = player.currentTime
            progress = duration > 0 ? currentTime / duration : 0
        } catch {
            SecureLogger.error("Voice note playback failed for \(url.lastPathComponent): \(error)", category: .session)
            player = nil
            duration = 0
            currentTime = 0
            progress = 0
        }
    }

    private func ensurePlayerReady() -> Bool {
        if player == nil {
            preparePlayer(for: url)
        }
        #if os(iOS)
        let session = AVAudioSession.sharedInstance()
        do {
            try session.setCategory(.playback, mode: .spokenAudio, options: [.mixWithOthers])
            try session.setActive(true, options: [])
        } catch {
            SecureLogger.error("Failed to activate audio session: \(error)", category: .session)
        }
        #endif
        return player != nil
    }

    private func startTimer() {
        if timer != nil { return }
        timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { [weak self] _ in
            self?.updateProgress()
        }
        if let timer = timer {
            RunLoop.main.add(timer, forMode: .common)
        }
    }

    private func stopTimer() {
        timer?.invalidate()
        timer = nil
    }

    private func updateProgress() {
        guard let player = player else {
            currentTime = 0
            duration = 0
            progress = 0
            return
        }
        currentTime = player.currentTime
        duration = player.duration
        progress = duration > 0 ? currentTime / duration : 0
    }
}

/// Ensures only one voice note plays at a time.
final class VoiceNotePlaybackCoordinator {
    static let shared = VoiceNotePlaybackCoordinator()

    private weak var activeController: VoiceNotePlaybackController?

    private init() {}

    func activate(_ controller: VoiceNotePlaybackController) {
        if activeController === controller {
            return
        }
        activeController?.pause()
        activeController = controller
    }

    func deactivate(_ controller: VoiceNotePlaybackController) {
        if activeController === controller {
            activeController = nil
        }
    }
}


================================================
FILE: bitchat/Features/voice/VoiceRecorder.swift
================================================
import Foundation
import AVFoundation

/// Manages audio capture for mesh voice notes with predictable encoding settings.
/// Recording runs on an internal serial queue to avoid AVAudioSession contention.
final class VoiceRecorder: NSObject, AVAudioRecorderDelegate {
    enum RecorderError: Error {
        case microphoneAccessDenied
        case recorderInitializationFailed
        case recordingInProgress
    }

    static let shared = VoiceRecorder()

    private let queue = DispatchQueue(label: "com.bitchat.voice-recorder")
    private let paddingInterval: TimeInterval = 0.5
    private let maxRecordingDuration: TimeInterval = 120

    private var recorder: AVAudioRecorder?
    private var currentURL: URL?
    private var stopWorkItem: DispatchWorkItem?

    private override init() {
        super.init()
    }

    // MARK: - Permissions

    @discardableResult
    func requestPermission() async -> Bool {
        #if os(iOS)
        return await withCheckedContinuation { continuation in
            AVAudioSession.sharedInstance().requestRecordPermission { granted in
                continuation.resume(returning: granted)
            }
        }
        #elseif os(macOS)
        return await withCheckedContinuation { continuation in
            AVCaptureDevice.requestAccess(for: .audio) { granted in
                continuation.resume(returning: granted)
            }
        }
        #else
        return true
        #endif
    }

    // MARK: - Recording Lifecycle

    func startRecording() throws -> URL {
        try queue.sync {
            if recorder?.isRecording == true {
                throw RecorderError.recordingInProgress
            }

            #if os(iOS)
            let session = AVAudioSession.sharedInstance()
            guard session.recordPermission == .granted else {
                throw RecorderError.microphoneAccessDenied
            }
            #if targetEnvironment(simulator)
            // allowBluetoothHFP is not available on iOS Simulator
            try session.setCategory(
                .playAndRecord,
                mode: .default,
                options: [.defaultToSpeaker, .allowBluetoothA2DP]
            )
            #else
            try session.setCategory(
                .playAndRecord,
                mode: .default,
                options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetoothHFP]
            )
            #endif
            try session.setActive(true, options: .notifyOthersOnDeactivation)
            #endif
            #if os(macOS)
            guard AVCaptureDevice.authorizationStatus(for: .audio) == .authorized else {
                throw RecorderError.microphoneAccessDenied
            }
            #endif

            let outputURL = try makeOutputURL()
            let settings: [String: Any] = [
                AVFormatIDKey: kAudioFormatMPEG4AAC,
                AVSampleRateKey: 16_000,
                AVNumberOfChannelsKey: 1,
                AVEncoderBitRateKey: 16_000
            ]

            let audioRecorder = try AVAudioRecorder(url: outputURL, settings: settings)
            audioRecorder.delegate = self
            audioRecorder.isMeteringEnabled = true
            audioRecorder.prepareToRecord()
            audioRecorder.record(forDuration: maxRecordingDuration)

            recorder = audioRecorder
            currentURL = outputURL
            stopWorkItem?.cancel()
            stopWorkItem = nil
            return outputURL
        }
    }

    func stopRecording(completion: @escaping (URL?) -> Void) {
        queue.async { [weak self] in
            guard let self = self, let recorder = self.recorder, recorder.isRecording else {
                completion(self?.currentURL)
                return
            }

            let item = DispatchWorkItem { [weak self] in
                guard let self = self else { return }
                recorder.stop()
                self.cleanupSession()
                let url = self.currentURL
                self.recorder = nil
                self.currentURL = url
                completion(url)
            }
            self.stopWorkItem = item
            self.queue.asyncAfter(deadline: .now() + self.paddingInterval, execute: item)
        }
    }

    func cancelRecording() {
        queue.async { [weak self] in
            guard let self = self else { return }
            self.stopWorkItem?.cancel()
            self.stopWorkItem = nil
            if let recorder = self.recorder, recorder.isRecording {
                recorder.stop()
            }
            self.cleanupSession()
            if let url = self.currentURL {
                try? FileManager.default.removeItem(at: url)
            }
            self.recorder = nil
            self.currentURL = nil
        }
    }

    // MARK: - Metering

    func currentAveragePower() -> Float {
        queue.sync {
            recorder?.updateMeters()
            return recorder?.averagePower(forChannel: 0) ?? -160
        }
    }

    // MARK: - Helpers

    private func makeOutputURL() throws -> URL {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMdd_HHmmss"
        let fileName = "voice_\(formatter.string(from: Date())).m4a"

        let baseDirectory = try applicationFilesDirectory().appendingPathComponent("voicenotes/outgoing", isDirectory: true)
        try FileManager.default.createDirectory(at: baseDirectory, withIntermediateDirectories: true, attributes: nil)
        return baseDirectory.appendingPathComponent(fileName)
    }

    private func applicationFilesDirectory() throws -> URL {
        #if os(iOS)
        return try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent("files", isDirectory: true)
        #else
        let base = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        return base.appendingPathComponent("files", isDirectory: true)
        #endif
    }

    private func cleanupSession() {
        #if os(iOS)
        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
        #endif
    }
}


================================================
FILE: bitchat/Features/voice/Waveform.swift
================================================
import AVFoundation
import Foundation
import BitLogger

/// Generates and caches downsampled waveforms for audio files so UI rendering is cheap.
final class WaveformCache {
    static let shared = WaveformCache()

    private let queue = DispatchQueue(label: "com.bitchat.waveform-cache", attributes: .concurrent)
    private var cache: [URL: (waveform: [Float], lastAccess: Date)] = [:]
    private let maxCacheSize = 20  // Limit cache to prevent unbounded memory growth

    private init() {}

    func cachedWaveform(for url: URL) -> [Float]? {
        queue.sync {
            guard let entry = cache[url] else { return nil }
            return entry.waveform
        }
    }

    func waveform(for url: URL, bins: Int = 120, completion: @escaping ([Float]) -> Void) {
        queue.async { [weak self] in
            guard let self = self else { return }

            // Check cache (read-only, no update needed on cache hit for performance)
            if let entry = self.cache[url] {
                DispatchQueue.main.async { completion(entry.waveform) }
                return
            }

            guard let computed = self.computeWaveform(url: url, bins: bins) else {
                DispatchQueue.main.async { completion([]) }
                return
            }

            self.queue.async(flags: .barrier) { [weak self] in
                guard let self = self else { return }

                // Evict oldest entry if cache is full
                if self.cache.count >= self.maxCacheSize {
                    if let oldest = self.cache.min(by: { $0.value.lastAccess < $1.value.lastAccess }) {
                        self.cache.removeValue(forKey: oldest.key)
                    }
                }

                self.cache[url] = (computed, Date())
            }
            DispatchQueue.main.async { completion(computed) }
        }
    }

    func purge(url: URL) {
        queue.async(flags: .barrier) { [weak self] in
            self?.cache.removeValue(forKey: url)
        }
    }

    func purgeAll() {
        queue.async(flags: .barrier) { [weak self] in
            self?.cache.removeAll()
        }
    }

    private func computeWaveform(url: URL, bins: Int) -> [Float]? {
        guard bins > 0 else { return nil }
        // Use autoreleasepool to manage memory from audio buffer allocations
        return autoreleasepool {
            do {
                let audioFile = try AVAudioFile(forReading: url)
                let length = Int(audioFile.length)
                guard length > 0 else { return nil }

                guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: AVAudioFrameCount(length)) else {
                    return nil
                }
                try audioFile.read(into: buffer, frameCount: AVAudioFrameCount(length))
                guard let channelData = buffer.floatChannelData else { return nil }

                let channelCount = Int(audioFile.processingFormat.channelCount)
                let frameLength = Int(buffer.frameLength)
                let samplesPerBin = max(1, frameLength / bins)

                var magnitudes: [Float] = Array(repeating: 0, count: bins)
                for bin in 0..<bins {
                    let start = bin * samplesPerBin
                    let end = min(frameLength, start + samplesPerBin)
                    if start >= end { break }

                    var sum: Float = 0
                    var sampleCount = 0
                    for frame in start..<end {
                        var sampleValue: Float = 0
                        for channel in 0..<channelCount {
                            sampleValue += fabsf(channelData[channel][frame])
                        }
                        sum += sampleValue / Float(channelCount)
                        sampleCount += 1
                    }
                    magnitudes[bin] = sampleCount > 0 ? sum / Float(sampleCount) : 0
                }

                if let maxMagnitude = magnitudes.max(), maxMagnitude > 0 {
                    magnitudes = magnitudes.map { min($0 / maxMagnitude, 1.0) }
                }
                return magnitudes
            } catch {
                SecureLogger.error("Waveform extraction failed for \(url.lastPathComponent): \(error)", category: .session)
                return nil
            }
        }
    }
}


================================================
FILE: bitchat/Identity/IdentityModels.swift
================================================
//
// IdentityModels.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//

///
/// # IdentityModels
///
/// Defines BitChat's innovative three-layer identity model that balances
/// privacy, security, and usability in a decentralized mesh network.
///
/// ## Overview
/// BitChat's identity system separates concerns across three distinct layers:
/// 1. **Ephemeral Identity**: Short-lived, rotatable peer IDs for privacy
/// 2. **Cryptographic Identity**: Long-term Noise static keys for security
/// 3. **Social Identity**: User-assigned names and trust relationships
///
/// This separation allows users to maintain stable cryptographic identities
/// while frequently rotating their network identifiers for privacy.
///
/// ## Three-Layer Architecture
///
/// ### Layer 1: Ephemeral Identity
/// - Random 8-byte peer IDs that rotate periodically
/// - Provides network-level privacy and prevents tracking
/// - Changes don't affect cryptographic relationships
/// - Includes handshake state tracking
///
/// ### Layer 2: Cryptographic Identity
/// - Based on Noise Protocol static key pairs
/// - Fingerprint derived from SHA256 of public key
/// - Enables end-to-end encryption and authentication
/// - Persists across peer ID rotations
///
/// ### Layer 3: Social Identity
/// - User-assigned names (petnames) for contacts
/// - Trust levels from unknown to verified
/// - Favorite/blocked status
/// - Personal notes and metadata
///
/// ## Privacy Design
/// The model is designed with privacy-first principles:
/// - No mandatory persistent storage
/// - Optional identity caching with user consent
/// - Ephemeral IDs prevent long-term tracking
/// - Social mappings stored locally only
///
/// ## Trust Model
/// Four levels of trust:
/// 1. **Unknown**: New or unverified peers
/// 2. **Casual**: Basic interaction history
/// 3. **Trusted**: User has explicitly trusted
/// 4. **Verified**: Cryptographic verification completed
///
/// ## Identity Resolution
/// When a peer rotates their ephemeral ID:
/// 1. Cryptographic handshake reveals their fingerprint
/// 2. System looks up social identity by fingerprint
/// 3. UI seamlessly maintains user relationships
/// 4. Historical messages remain properly attributed
///
/// ## Conflict Resolution
/// Handles edge cases like:
/// - Multiple peers claiming same nickname
/// - Nickname changes and conflicts
/// - Identity rotation during active chats
/// - Network partitions and rejoins
///
/// ## Usage Example
/// ```swift
/// // When peer connects with new ID
/// let ephemeral = EphemeralIdentity(peerID: "abc123", ...)
/// // After handshake
/// let crypto = CryptographicIdentity(fingerprint: "sha256...", ...)
/// // User assigns name
/// let social = SocialIdentity(localPetname: "Alice", ...)
/// ```
///

import Foundation

// MARK: - Three-Layer Identity Model

/// Represents the ephemeral layer of identity - short-lived peer IDs that provide network privacy.
/// These IDs rotate periodically to prevent tracking while maintaining cryptographic relationships.
struct EphemeralIdentity {
    let peerID: PeerID          // 8 random bytes
    let sessionStart: Date
    var handshakeState: HandshakeState
}

enum HandshakeState {
    case none
    case initiated
    case inProgress
    case completed(fingerprint: String)
    case failed(reason: String)
}

/// Represents the cryptographic layer of identity - the stable Noise Protocol static key pair.
/// This identity persists across ephemeral ID rotations and enables secure communication.
/// The fingerprint serves as the permanent identifier for a peer's cryptographic identity.
struct CryptographicIdentity: Codable {
    let fingerprint: String     // SHA256 of public key
    let publicKey: Data         // Noise static public key
    // Optional Ed25519 signing public key (used to authenticate public messages)
    var signingPublicKey: Data? = nil
    let firstSeen: Date
    let lastHandshake: Date?
}

/// Represents the social layer of identity - user-assigned names and trust relationships.
/// This layer provides human-friendly identification and relationship management.
/// All data in this layer is local-only and never transmitted over the network.
struct SocialIdentity: Codable {
    let fingerprint: String
    var localPetname: String?   // User's name for this peer
    var claimedNickname: String // What peer calls themselves
    var trustLevel: TrustLevel
    var isFavorite: Bool
    var isBlocked: Bool
    var notes: String?
}

enum TrustLevel: String, Codable {
    case unknown = "unknown"
    case casual = "casual"
    case trusted = "trusted"
    case verified = "verified"
}

// MARK: - Identity Cache

/// Persistent storage for identity mappings and relationships.
/// Provides efficient lookup between fingerprints, nicknames, and social identities.
/// Storage is optional and controlled by user privacy settings.
struct IdentityCache: Codable {
    // Fingerprint -> Social mapping
    var socialIdentities: [String: SocialIdentity] = [:]
    
    // Nickname -> [Fingerprints] reverse index
    // Multiple fingerprints can claim same nickname
    var nicknameIndex: [String: Set<String>] = [:]
    
    // Verified fingerprints (cryptographic proof)
    var verifiedFingerprints: Set<String> = []
    
    // Last interaction timestamps (privacy: optional)
    var lastInteractions: [String: Date] = [:] 
    
    // Blocked Nostr pubkeys (lowercased hex) for geohash chats
    var blockedNostrPubkeys: Set<String> = []
    
    // Schema version for future migrations
    var version: Int = 1
}

//

// MARK: - Migration Support
//


================================================
FILE: bitchat/Identity/SecureIdentityStateManager.swift
================================================
//
// SecureIdentityStateManager.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//

///
/// # SecureIdentityStateManager
///
/// Manages the persistent storage and retrieval of identity mappings with
/// encryption at rest. This singleton service maintains the relationship between
/// ephemeral peer IDs, cryptographic fingerprints, and social identities.
///
/// ## Overview
/// The SecureIdentityStateManager provides a secure, privacy-preserving way to
/// maintain identity relationships across app launches. It implements:
/// - Encrypted storage of identity mappings
/// - In-memory caching for performance
/// - Thread-safe access patterns
/// - Automatic debounced persistence
///
/// ## Architecture
/// The manager operates at three levels:
/// 1. **In-Memory State**: Fast access to active identities
/// 2. **Encrypted Cache**: Persistent storage in Keychain
/// 3. **Privacy Controls**: User-configurable persistence settings
///
/// ## Security Features
///
/// ### Encryption at Rest
/// - Identity cache encrypted with AES-GCM
/// - Unique 256-bit encryption key per device
/// - Key stored separately in Keychain
/// - No plaintext identity data on disk
///
/// ### Privacy by Design
/// - Persistence is optional (user-controlled)
/// - Minimal data retention
/// - No cloud sync or backup
/// - Automatic cleanup of stale entries
///
/// ### Thread Safety
/// - Concurrent read access via GCD barriers
/// - Write operations serialized
/// - Atomic state updates
/// - No data races or corruption
///
/// ## Data Model
/// Manages three types of identity data:
/// 1. **Ephemeral Sessions**: Current peer connections
/// 2. **Cryptographic Identities**: Public keys and fingerprints
/// 3. **Social Identities**: User-assigned names and trust
///
/// ## Persistence Strategy
/// - Changes batched and debounced (2-second window)
/// - Automatic save on app termination
/// - Crash-resistant with atomic writes
/// - Migration support for schema changes
///
/// ## Usage Patterns
/// ```swift
/// // Register a new peer identity
/// manager.registerPeerIdentity(peerID, publicKey, fingerprint)
/// 
/// // Update social identity
/// manager.updateSocialIdentity(fingerprint, nickname, trustLevel)
/// 
/// // Query identity
/// let identity = manager.resolvePeerIdentity(peerID)
/// ```
///
/// ## Performance Optimizations
/// - In-memory cache eliminates Keychain roundtrips
/// - Debounced saves reduce I/O operations
/// - Efficient data structures for lookups
/// - Background queue for expensive operations
///
/// ## Privacy Considerations
/// - Users can disable all persistence
/// - Identity cache can be wiped instantly
/// - No analytics or telemetry
/// - Ephemeral mode for high-risk users
///
/// ## Future Enhancements
/// - Selective identity export
/// - Cross-device identity sync (optional)
/// - Identity attestation support
/// - Advanced conflict resolution
///

import BitLogger
import Foundation
import CryptoKit

protocol SecureIdentityStateManagerProtocol {
    // MARK: Secure Loading/Saving
    func forceSave()
    
    // MARK: Social Identity Management
    func getSocialIdentity(for fingerprint: String) -> SocialIdentity?
    
    // MARK: Cryptographic Identities
    func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?)
    func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity]
    func updateSocialIdentity(_ identity: SocialIdentity)
    
    // MARK: Favorites Management
    func getFavorites() -> Set<String>
    func setFavorite(_ fingerprint: String, isFavorite: Bool)
    func isFavorite(fingerprint: String) -> Bool
    
    // MARK: Blocked Users Management
    func isBlocked(fingerprint: String) -> Bool
    func setBlocked(_ fingerprint: String, isBlocked: Bool)
    
    // MARK: Geohash (Nostr) Blocking
    func isNostrBlocked(pubkeyHexLowercased: String) -> Bool
    func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool)
    func getBlockedNostrPubkeys() -> Set<String>
    
    // MARK: Ephemeral Session Management
    func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState)
    func updateHandshakeState(peerID: PeerID, state: HandshakeState)
    
    // MARK: Cleanup
    func clearAllIdentityData()
    func removeEphemeralSession(peerID: PeerID)
    
    // MARK: Verification
    func setVerified(fingerprint: String, verified: Bool)
    func isVerified(fingerprint: String) -> Bool
    func getVerifiedFingerprints() -> Set<String>
}

/// Singleton manager for secure identity state persistence and retrieval.
/// Provides thread-safe access to identity mappings with encryption at rest.
/// All identity data is stored encrypted in the device Keychain for security.
final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
    private let keychain: KeychainManagerProtocol
    private let cacheKey = "bitchat.identityCache.v2"
    private let encryptionKeyName = "identityCacheEncryptionKey"
    
    // In-memory state
    private var ephemeralSessions: [PeerID: EphemeralIdentity] = [:]
    private var cryptographicIdentities: [String: CryptographicIdentity] = [:]
    private var cache: IdentityCache = IdentityCache()
    
    // Thread safety
    private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent)
    
    // Debouncing for keychain saves
    private var saveTimer: Timer?
    private let saveDebounceInterval: TimeInterval = 2.0  // Save at most once every 2 seconds
    private var pendingSave = false
    
    // Encryption key
    private let encryptionKey: SymmetricKey
    
    init(_ keychain: KeychainManagerProtocol) {
        self.keychain = keychain
        
        // Generate or retrieve encryption key from keychain
        let loadedKey: SymmetricKey
        
        // Try to load from keychain
        if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) {
            loadedKey = SymmetricKey(data: keyData)
            SecureLogger.logKeyOperation(.load, keyType: "identity cache encryption key", success: true)
        }
        // Generate new key if needed
        else {
            loadedKey = SymmetricKey(size: .bits256)
            let keyData = loadedKey.withUnsafeBytes { Data($0) }
            // Save to keychain
            let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName)
            SecureLogger.logKeyOperation(.generate, keyType: "identity cache encryption key", success: saved)
        }
        
        self.encryptionKey = loadedKey
        
        // Load identity cache on init
        loadIdentityCache()
    }
    
    deinit {
        forceSave()
    }
    
    // MARK: - Secure Loading/Saving
    
    private func loadIdentityCache() {
        guard let encryptedData = keychain.getIdentityKey(forKey: cacheKey) else {
            // No existing cache, start fresh
            return
        }
        
        do {
            let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
            let decryptedData = try AES.GCM.open(sealedBox, using: encryptionKey)
            cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData)
        } catch {
            // Log error but continue with empty cache
            SecureLogger.error(error, context: "Failed to load identity cache", category: .security)
        }
    }
    
    private func saveIdentityCache() {
        // Mark that we need to save
        pendingSave = true
        
        // Cancel any existing timer
        saveTimer?.invalidate()
        
        // Schedule a new save after the debounce interval
        saveTimer = Timer.scheduledTimer(withTimeInterval: saveDebounceInterval, repeats: false) { [weak self] _ in
            self?.performSave()
        }
    }
    
    private func performSave() {
        guard pendingSave else { return }
        pendingSave = false
        
        do {
            let data = try JSONEncoder().encode(cache)
            let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
            let saved = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey)
            if saved {
                SecureLogger.debug("Identity cache saved to keychain", category: .security)
            }
        } catch {
            SecureLogger.error(error, context: "Failed to save identity cache", category: .security)
        }
    }
    
    // Force immediate save (for app termination)
    func forceSave() {
        saveTimer?.invalidate()
        performSave()
    }
    
    // MARK: - Social Identity Management
    
    func getSocialIdentity(for fingerprint: String) -> SocialIdentity? {
        queue.sync {
            return cache.socialIdentities[fingerprint]
        }
    }

    // MARK: - Cryptographic Identities

    /// Insert or update a cryptographic identity and optionally persist its signing key and claimed nickname.
    /// - Parameters:
    ///   - fingerprint: SHA-256 hex of the Noise static public key
    ///   - noisePublicKey: Noise static public key data
    ///   - signingPublicKey: Optional Ed25519 signing public key for authenticating public messages
    ///   - claimedNickname: Optional latest claimed nickname to persist into social identity
    func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String? = nil) {
        queue.async(flags: .barrier) {
            let now = Date()
            if var existing = self.cryptographicIdentities[fingerprint] {
                // Update keys if changed
                if existing.publicKey != noisePublicKey {
                    existing = CryptographicIdentity(
                        fingerprint: fingerprint,
                        publicKey: noisePublicKey,
                        signingPublicKey: signingPublicKey ?? existing.signingPublicKey,
                        firstSeen: existing.firstSeen,
                        lastHandshake: now
                    )
                    self.cryptographicIdentities[fingerprint] = existing
                } else {
                    // Update signing key and lastHandshake
                    existing.signingPublicKey = signingPublicKey ?? existing.signingPublicKey
                    let updated = CryptographicIdentity(
                        fingerprint: existing.fingerprint,
                        publicKey: existing.publicKey,
                        signingPublicKey: existing.signingPublicKey,
                        firstSeen: existing.firstSeen,
                        lastHandshake: now
                    )
                    self.cryptographicIdentities[fingerprint] = updated
                }
                // Persist updated state (already assigned in branches above)
            } else {
                // New entry
                let entry = CryptographicIdentity(
                    fingerprint: fingerprint,
                    publicKey: noisePublicKey,
                    signingPublicKey: signingPublicKey,
                    firstSeen: now,
                    lastHandshake: now
                )
                self.cryptographicIdentities[fingerprint] = entry
            }

            // Optionally persist claimed nickname into social identity
            if let claimed = claimedNickname {
                var identity = self.cache.socialIdentities[fingerprint] ?? SocialIdentity(
                    fingerprint: fingerprint,
                    localPetname: nil,
                    claimedNickname: claimed,
                    trustLevel: .unknown,
                    isFavorite: false,
                    isBlocked: false,
                    notes: nil
                )
                // Update claimed nickname if changed
                if identity.claimedNickname != claimed {
                    identity.claimedNickname = claimed
                    self.cache.socialIdentities[fingerprint] = identity
                } else if self.cache.socialIdentities[fingerprint] == nil {
                    self.cache.socialIdentities[fingerprint] = identity
                }
            }

            self.saveIdentityCache()
        }
    }

    /// Find cryptographic identities whose fingerprint prefix matches a peerID (16-hex) short ID
    func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity] {
        queue.sync {
            // Defensive: ensure hex and correct length
            guard peerID.isShort else { return [] }
            return cryptographicIdentities.values.filter { $0.fingerprint.hasPrefix(peerID.id) }
        }
    }
    
    func updateSocialIdentity(_ identity: SocialIdentity) {
        queue.async(flags: .barrier) {
            let previousClaimedNickname = self.cache.socialIdentities[identity.fingerprint]?.claimedNickname
            self.cache.socialIdentities[identity.fingerprint] = identity
            
            // Update nickname index
            if let previousClaimedNickname,
               previousClaimedNickname != identity.claimedNickname {
                self.cache.nicknameIndex[previousClaimedNickname]?.remove(identity.fingerprint)
                if self.cache.nicknameIndex[previousClaimedNickname]?.isEmpty == true {
                    self.cache.nicknameIndex.removeValue(forKey: previousClaimedNickname)
                }
            }
            
            // Add new nickname to index
            if self.cache.nicknameIndex[identity.claimedNickname] == nil {
                self.cache.nicknameIndex[identity.claimedNickname] = Set<String>()
            }
            self.cache.nicknameIndex[identity.claimedNickname]?.insert(identity.fingerprint)
            
            // Save to keychain
            self.saveIdentityCache()
        }
    }
    
    // MARK: - Favorites Management
    
    func getFavorites() -> Set<String> {
        queue.sync {
            let favorites = cache.socialIdentities.values
                .filter { $0.isFavorite }
                .map { $0.fingerprint }
            return Set(favorites)
        }
    }
    
    func setFavorite(_ fingerprint: String, isFavorite: Bool) {
        queue.async(flags: .barrier) {
            if var identity = self.cache.socialIdentities[fingerprint] {
                identity.isFavorite = isFavorite
                self.cache.socialIdentities[fingerprint] = identity
            } else {
                // Create new social identity for this fingerprint
                let newIdentity = SocialIdentity(
                    fingerprint: fingerprint,
                    localPetname: nil,
                    claimedNickname: "Unknown",
                    trustLevel: .unknown,
                    isFavorite: isFavorite,
                    isBlocked: false,
                    notes: nil
                )
                self.cache.socialIdentities[fingerprint] = newIdentity
            }
            self.saveIdentityCache()
        }
    }
    
    func isFavorite(fingerprint: String) -> Bool {
        queue.sync {
            return cache.socialIdentities[fingerprint]?.isFavorite ?? false
        }
    }
    
    // MARK: - Blocked Users Management
    
    func isBlocked(fingerprint: String) -> Bool {
        queue.sync {
            return cache.socialIdentities[fingerprint]?.isBlocked ?? false
        }
    }
    
    func setBlocked(_ fingerprint: String, isBlocked: Bool) {
        SecureLogger.info("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: .security)
        
        queue.async(flags: .barrier) {
            if var identity = self.cache.socialIdentities[fingerprint] {
                identity.isBlocked = isBlocked
                if isBlocked {
                    identity.isFavorite = false  // Can't be both favorite and blocked
                }
                self.cache.socialIdentities[fingerprint] = identity
            } else {
                // Create new social identity for this fingerprint
                let newIdentity = SocialIdentity(
                    fingerprint: fingerprint,
                    localPetname: nil,
                    claimedNickname: "Unknown",
                    trustLevel: .unknown,
                    isFavorite: false,
                    isBlocked: isBlocked,
                    notes: nil
                )
                self.cache.socialIdentities[fingerprint] = newIdentity
            }
            self.saveIdentityCache()
        }
    }

    // MARK: - Geohash (Nostr) Blocking
    
    func isNostrBlocked(pubkeyHexLowercased: String) -> Bool {
        queue.sync {
            return cache.blockedNostrPubkeys.contains(pubkeyHexLowercased.lowercased())
        }
    }
    
    func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool) {
        let key = pubkeyHexLowercased.lowercased()
        queue.async(flags: .barrier) {
            if isBlocked {
                self.cache.blockedNostrPubkeys.insert(key)
            } else {
                self.cache.blockedNostrPubkeys.remove(key)
            }
            self.saveIdentityCache()
        }
    }
    
    func getBlockedNostrPubkeys() -> Set<String> {
        queue.sync { cache.blockedNostrPubkeys }
    }
    
    // MARK: - Ephemeral Session Management
    
    func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState = .none) {
        queue.async(flags: .barrier) {
            self.ephemeralSessions[peerID] = EphemeralIdentity(
                peerID: peerID,
                sessionStart: Date(),
                handshakeState: handshakeState
            )
        }
    }
    
    func updateHandshakeState(peerID: PeerID, state: HandshakeState) {
        queue.async(flags: .barrier) {
            self.ephemeralSessions[peerID]?.handshakeState = state
            
            // If handshake completed, update last interaction
            if case .completed(let fingerprint) = state {
                self.cache.lastInteractions[fingerprint] = Date()
                self.saveIdentityCache()
            }
        }
    }
    
    // MARK: - Cleanup
    
    func clearAllIdentityData() {
        SecureLogger.warning("Clearing all identity data", category: .security)
        
        queue.async(flags: .barrier) {
            self.cache = IdentityCache()
            self.ephemeralSessions.removeAll()
            self.cryptographicIdentities.removeAll()
            
            // Delete from keychain
            let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey)
            SecureLogger.logKeyOperation(.delete, keyType: "identity cache", success: deleted)
        }
    }
    
    func removeEphemeralSession(peerID: PeerID) {
        queue.async(flags: .barrier) {
            self.ephemeralSessions.removeValue(forKey: peerID)
        }
    }
    
    // MARK: - Verification
    
    func setVerified(fingerprint: String, verified: Bool) {
        SecureLogger.info("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: .security)
        
        queue.async(flags: .barrier) {
            if verified {
                self.cache.verifiedFingerprints.insert(fingerprint)
            } else {
                self.cache.verifiedFingerprints.remove(fingerprint)
            }
            
            // Update trust level if social identity exists
            if var identity = self.cache.socialIdentities[fingerprint] {
                identity.trustLevel = verified ? .verified : .casual
                self.cache.socialIdentities[fingerprint] = identity
            }
            
            self.saveIdentityCache()
        }
    }
    
    func isVerified(fingerprint: String) -> Bool {
        queue.sync {
            return cache.verifiedFingerprints.contains(fingerprint)
        }
    }
    
    func getVerifiedFingerprints() -> Set<String> {
        queue.sync {
            return cache.verifiedFingerprints
        }
    }

    var debugNicknameIndex: [String: Set<String>] {
        queue.sync { cache.nicknameIndex }
    }

    func debugEphemeralSession(for peerID: PeerID) -> EphemeralIdentity? {
        queue.sync { ephemeralSessions[peerID] }
    }

    func debugLastInteraction(for fingerprint: String) -> Date? {
        queue.sync { cache.lastInteractions[fingerprint] }
    }
}


================================================
FILE: bitchat/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	<key>CFBundleDisplayName</key>
	<string>bitchat</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(MARKETING_VERSION)</string>
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>bitchat</string>
			</array>
		</dict>
	</array>
	<key>CFBundleVersion</key>
	<string>$(CURRENT_PROJECT_VERSION)</string>
	<key>LSMinimumSystemVersion</key>
	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
	<key>NSBluetoothAlwaysUsageDescription</key>
	<string>bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.</string>
	<key>NSBluetoothPeripheralUsageDescription</key>
	<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
	<key>NSCameraUsageDescription</key>
	<string>bitchat uses the camera to scan QR codes to verify peers.</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>bitchat lets you pick images from your photo library to share with nearby peers.</string>
	<key>NSMicrophoneUsageDescription</key>
	<string>bitchat uses the microphone to record voice notes that relay across the mesh.</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string>
	<key>UIBackgroundModes</key>
	<array>
		<string>bluetooth-central</string>
		<string>bluetooth-peripheral</string>
	</array>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIRequiresFullScreen</key>
	<true/>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
	</array>
</dict>
</plist>


================================================
FILE: bitchat/LaunchScreen.storyboard
================================================
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
    <device id="retina6_12" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="EHf-IW-A2E">
            <objects>
                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
                        <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="bitchat" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
                                <rect key="frame" x="0.0" y="403.66666666666669" width="393" height="45"/>
                                <fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="38"/>
                                <color key="textColor" red="0.0" green="1.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <nil key="highlightedColor"/>
                            </label>
                        </subviews>
                        <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
                        <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                        <constraints>
                            <constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="5cJ-9S-tgC"/>
                            <constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="Q5M-cg-NOt"/>
                            <constraint firstItem="GJd-Yh-RWb" firstAttribute="trailing" secondItem="Bcu-3y-fUS" secondAttribute="trailing" id="XAW-aL-0N7"/>
                        </constraints>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="53" y="375"/>
        </scene>
    </scenes>
</document>


================================================
FILE: bitchat/Localizable.xcstrings
================================================
{
  "sourceLanguage" : "en",
  "strings" : {
    "%@" : {
      "comment" : "Non-localizable symbol used in code",
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@"
          }
        }
      },
      "shouldTranslate" : false
    },
    "%@ active" : {
      "comment" : "A label at the bottom of the people list sheet showing the number of active users.",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ نشطين"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ সক্রিয়"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ aktiv"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ active"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ activos"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ aktibo"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ actifs"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ פעילים"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ सक्रिय"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ aktif"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ attivi"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ 人がアクティブ"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "활성 사용자 %@명"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ aktif"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ सक्रिय"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ actief"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ aktywni"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ ativos"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ ativos"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ активных"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ aktiva"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ செயலில்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ กำลังใช้งาน"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ aktif"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ активних"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ فعال"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "%@ đang hoạt động"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "活跃用户 %@ 名"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "活躍使用者 %@ 位"
          }
        }
      }
    },
    "app_info.app_name" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bitchat"
          }
        }
      }
    },
    "app_info.close" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "إغلاق"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "বন্ধ করুন"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "schließen"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "close"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "cerrar"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "isara"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "fermer"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "סגור"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "बंद करें"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "tutup"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "chiudi"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "閉じる"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "닫기"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "tutup"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "बन्द"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "sluiten"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "zamknij"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "fechar"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "fechar"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "закрыть"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "stäng"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "மூடு"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ปิด"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kapat"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "закрити"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "بند کریں"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "đóng"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "关闭"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "關閉"
          }
        }
      }
    },
    "app_info.done" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "تم"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "সম্পন্ন"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "FERTIG"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "DONE"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "LISTO"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "TAPOS"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "TERMINÉ"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "בוצע"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "समाप्त"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "SELESAI"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "FATTO"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "完了"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "확인"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "SELESAI"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "सम्पन्न"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "KLAAR"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "GOTOWE"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "CONCLUÍDO"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "CONCLUÍDO"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ГОТОВО"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "KLART"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "முடிந்தது"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "เสร็จสิ้น"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "BİTTİ"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ГОТОВО"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "مکمل"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "XONG"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "完成"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "完成"
          }
        }
      }
    },
    "app_info.features.encryption.description" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "الرسائل الخاصة مشفرة ببروتوكول noise"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "গোপন বার্তাগুলো নোইজ প্রোটোকলের মাধ্যমে এনক্রিপ্ট করা হয়"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "private nachrichten werden mit dem noise-protokoll verschlüsselt"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "private messages encrypted with noise protocol"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mensajes privados cifrados con el protocolo Noise"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ang mga pribadong mensahe ay ini-encrypt gamit ang Noise protocol"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "messages privés chiffrés avec le protocole noise"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "הודעות פרטיות מוצפנות בפרוטוקול noise"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "नॉइज़ प्रोटोकॉल से निजी संदेश एन्क्रिप्ट किए जाते हैं"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "pesan pribadi dienkripsi dengan protokol noise"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "messaggi privati cifrati con il protocollo noise"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "プライベートメッセージはnoiseプロトコルで暗号化されます"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "비공개 메시지가 노이즈 프로토콜로 암호화됩니다"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "pesan pribadi dienkripsi dengan protokol noise"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "व्यक्तिगत सन्देशहरू noise प्रोटोकलले सङ्केत गर्छ"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "privéberichten worden versleuteld met het Noise-protocol"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "prywatne wiadomości są szyfrowane protokołem Noise"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mensagens privadas encriptadas com o protocolo Noise"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mensagens privadas criptografadas com o protocolo noise"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "личные сообщения шифруются протоколом noise"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "privata meddelanden krypteras med Noise-protokollet"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "தனிப்பட்ட செய்திகள் Noise நெறிமுறையால் குறியாக்கம் செய்யப்படுகின்றன"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ข้อความส่วนตัวถูกเข้ารหัสด้วยโปรโตคอล Noise"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "özel mesajlar noise protokolü ile şifrelenir"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "приватні повідомлення шифруються протоколом noise"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "نجی پیغامات Noise پروٹوکول سے خفیہ کیے جاتے ہیں"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "tin nhắn riêng tư được mã hóa bằng giao thức Noise"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "私密消息使用 noise 协议加密"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "私密訊息使用 noise 協議加密"
          }
        }
      }
    },
    "app_info.features.encryption.title" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "تشفير طرف لطرف"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "এন্ড-টু-এন্ড এনক্রিপশন"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "end-to-end-verschlüsselung"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "end-to-end encryption"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "cifrado de extremo a extremo"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "end-to-end na pag-encrypt"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "chiffrement de bout en bout"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "הצפנה מקצה לקצה"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "एंड-टू-एंड एन्क्रिप्शन"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "enkripsi ujung ke ujung"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "crittografia end-to-end"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "エンドツーエンド暗号"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "종단간 암호화"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "enkripsi ujung ke ujung"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "एन्ड-टु-एन्ड सङ्केत"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "end-to-end-versleuteling"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "szyfrowanie end-to-end"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "encriptação ponta a ponta"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "criptografia ponto a ponto"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "сквозное шифрование"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ända-till-ände-kryptering"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "முற்றிலும் குறியாக்கம்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "การเข้ารหัสปลายทางถึงปลายทาง"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "uçtan uca şifreleme"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "скрізьове шифрування"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "اینڈ ٹو اینڈ انکرپشن"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mã hóa đầu cuối"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "端到端加密"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "端到端加密"
          }
        }
      }
    },
    "app_info.features.extended_range.description" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "يُعاد تمرير الرسائل بين الأقران لتصل لمسافات أبعد"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "বার্তাগুলো সহ-পিয়ারদের মাধ্যমে রিলে হয়ে দূরেও পৌঁছে যায়"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "nachrichten werden zwischen peers weitergeleitet und reichen weiter"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "messages relay through peers, going the distance"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "los mensajes se retransmiten entre pares y llegan lejos"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ipinapasa ang mga mensahe sa mga peer para makarating sa malalayo"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "messages relayés entre pairs pour aller plus loin"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "הודעות משודרות בין עמיתים ומגיעות רחוק יותר"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "संदेश साथियों के माध्यम से रिले होकर दूर तक पहुँचते हैं"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "pesan diteruskan antar peer sehingga jangkauannya lebih jauh"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "i messaggi vengono inoltrati tra peer per arrivare più lontano"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "メッセージはピア間でリレーされより遠くに届きます"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "메시지가 피어를 통해 중계되어 더 멀리 도달합니다"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "pesan diteruskan antar peer sehingga jangkauannya lebih jauh"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "सन्देशहरू सहकर्मीमार्फत रिले भएर टाढासम्म पुग्छन्"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "berichten worden via peers doorgestuurd om verder te reiken"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "wiadomości są przekazywane przez peerów, aby dotrzeć dalej"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mensagens retransmitidas entre pares para chegar mais longe"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mensagens retransmitidas entre pares para alcançar mais longe"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "сообщения ретранслируются между пирами и уходят дальше"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "meddelanden vidarebefordras via peers för att nå längre"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "செய்திகள் துணை இணைப்புகளின் மூலம் பரிமாறி தூரம் சென்றடைகின்றன"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ข้อความส่งต่อผ่านเพียร์เพื่อไปได้ไกลขึ้น"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mesajlar eşler üzerinden aktarılarak uzağa ulaşır"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "повідомлення ретранслюються між пірами й долітають далі"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "پیغامات ہم منصبوں کے ذریعے آگے بڑھا کر دور تک پہنچتے ہیں"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "tin nhắn được chuyển tiếp qua các nút ngang hàng để đi xa hơn"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "消息通过同伴中继,传得更远"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "訊息透過同伴中繼,傳得更遠"
          }
        }
      }
    },
    "app_info.features.extended_range.title" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "نطاق ممتد"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "বর্ধিত পরিসর"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "erweiterte reichweite"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "extended range"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "alcance ampliado"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mas malawak na saklaw"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "portée étendue"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "טווח מורחב"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "विस्तारित दायरा"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "jangkauan diperluas"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "portata estesa"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "拡張レンジ"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "확장된 범위"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "jangkauan diperluas"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "विस्तारित पहुँच"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "groter bereik"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "większy zasięg"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "alcance alargado"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "alcance estendido"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "расширенный радиус"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "utökat räckvidd"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "விரிந்த வரம்பு"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ระยะสื่อสารที่กว้างขึ้น"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "genişletilmiş menzil"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "розширена дальність"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "وسیع دائرہ"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "phạm vi mở rộng"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "扩展范围"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "擴展範圍"
          }
        }
      }
    },
    "app_info.features.favorites.description" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "تلقَّ تنبيهات عندما ينضم أحباؤك"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "আপনার প্রিয় মানুষ যোগ দিলে বিজ্ঞপ্তি পান"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "erhalte hinweise, wenn deine lieblingsmenschen online kommen"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "get notified when your favorite people join"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "recibe avisos cuando tus personas favoritas se conecten"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "tumanggap ng abiso kapag sumali ang paborito mong mga tao"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "reçois une alerte quand tes personnes favorites arrivent"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "קבל התראות כשהאנשים המועדפים שלך מצטרפים"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "जब आपके पसंदीदा लोग जुड़ें तो सूचना पाएं"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "dapatkan notifikasi saat orang favoritmu bergabung"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ricevi avvisi quando entrano le tue persone preferite"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "お気に入りの人が参加したら通知を受け取れます"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "즐겨찾는 사용자가 참여하면 알림을 받습니다"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "dapatkan notifikasi saat orang favoritmu bergabung"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "तिम्रा मनपर्ने मानिस जोडिएपछि सूचनाहरू पाऊ"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ontvang meldingen wanneer je favoriete mensen meedoen"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "otrzymuj powiadomienia, gdy dołączają twoje ulubione osoby"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "recebe notificações quando as tuas pessoas favoritas entram"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "receba avisos quando suas pessoas favoritas entrarem"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "получай уведомления, когда подключаются любимые люди"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "få aviseringar när dina favoriter ansluter"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "உங்களுக்குப் பிரியமானவர்கள் சேர்ந்தவுடன் அறிவிப்பு பெறுங்கள்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "รับการแจ้งเตือนเมื่อคนโปรดของคุณเข้าร่วม"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favori kişileriniz katıldığında bildirim alın"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "отримуй сповіщення, коли підключаються улюблені люди"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "جب آپ کے پسندیدہ لوگ شامل ہوں تو اطلاع پائیں"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "nhận thông báo khi những người yêu thích của bạn tham gia"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "你喜欢的人加入时立刻提醒"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "你喜歡的人加入時立刻提醒"
          }
        }
      }
    },
    "app_info.features.favorites.title" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "المفضلة"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "প্রিয়সমূহ"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favoriten"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favorites"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favoritos"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mga paborito"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favoris"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "מועדפים"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "पसंदीदा"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favorit"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "preferiti"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "お気に入り"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "즐겨찾기"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favorit"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "मनपर्ने"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favorieten"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ulubione"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favoritos"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favoritos"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "избранное"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favoriter"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "சிறப்புப் பட்டியல்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "รายการโปรด"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "favoriler"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "вибране"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "پسندیدہ"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "yêu thích"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "收藏"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "收藏"
          }
        }
      }
    },
    "app_info.features.geohash.description" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "قنوات geohash للدردشة مع أشخاص قريبين عبر مرحلات لامركزية مجهولة"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ডিসেন্ট্রালাইজড বেনামি রিলে দিয়ে কাছাকাছি অঞ্চলের মানুষের সাথে কথা বলার জন্য জিওহ্যাশ চ্যানেল"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohash-kanäle zum chatten mit menschen in der nähe über dezentrale anonyme relays"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohash channels to chat with people in nearby regions over decentralized anonymous relays"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canales geohash para chatear con personas en regiones cercanas a través de relays descentralizados anónimos"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mga geohash channel para makipag-chat sa mga tao sa karatig na lugar sa pamamagitan ng mga desentralisado at anonimong relay"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canaux geohash pour discuter avec des personnes proches via des relais décentralisés anonymes"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ערוצי geohash לשיחה עם אנשים קרובים דרך ממסרים אנונימיים מבוזרים"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "विकेंद्रीकृत गुमनाम रिले के माध्यम से आसपास के क्षेत्रों के लोगों से चैट करने के लिए जियोहैश चैनल"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kanal geohash untuk ngobrol dengan orang di wilayah sekitar lewat relay anonim terdesentralisasi"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canali geohash per chattare con persone vicine tramite relay anonimi decentralizzati"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohashチャンネルで近くの人と匿名分散リレー越しにチャット"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "분산형 익명 릴레이를 통해 geohash 채널에서 주변 지역의 사람들과 대화할 수 있습니다"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kanal geohash untuk ngobrol dengan orang di wilayah sekitar lewat relay anonim terdesentralisasi"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohash च्यानलहरूले नजिकका व्यक्तिसँग विकेन्द्रित गोप्य रिलेबाट कुराकानी गर्न मद्दत गर्छ"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohash-kanalen om te chatten met mensen in de buurt via gedecentraliseerde, anonieme relais"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kanały geohash do rozmów z osobami w pobliżu przez zdecentralizowane, anonimowe przekaźniki"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canais geohash para conversar com pessoas em regiões próximas através de relés descentralizados anónimos"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canais geohash para conversar com pessoas em regiões próximas por relays descentralizados anônimos"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "каналы geohash для чата с людьми поблизости через децентрализованные анонимные реле"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohash-kanaler för att chatta med folk i närheten via decentraliserade, anonyma reläer"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "மையமற்ற மறை நெட்வொர்க் ரிலேக்கள் மூலம் அருகிலுள்ள பகுதிகளில் உள்ளவர்களுடன் பேச geohash சேனல்கள்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ช่อง geohash สำหรับพูดคุยกับคนในพื้นที่ใกล้เคียงผ่านรีเลย์แบบกระจายศูนย์และไม่ระบุตัว"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "merkeziyetsiz anonim röleler üzerinden yakın bölgelerdeki insanlarla sohbet etmek için geohash kanalları"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "канали geohash для спілкування з людьми поблизу через децентралізовані анонімні ретранслятори"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "غیر مرکزی گمنام ریلوں کے ذریعے قریب کے لوگوں سے بات کرنے کیلئے geohash چینلز"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "các kênh geohash để trò chuyện với mọi người ở vùng lân cận thông qua các relay ẩn danh phi tập trung"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohash 频道让你通过去中心化匿名中继与附近地区的人聊天"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "geohash 頻道讓你透過去中心化匿名中繼與附近地區的人聊天"
          }
        }
      }
    },
    "app_info.features.geohash.title" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "قنوات محلية"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "স্থানীয় চ্যানেল"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "lokale kanäle"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "local channels"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canales locales"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mga lokal na channel"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canaux locaux"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ערוצים מקומיים"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "स्थानीय चैनल"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kanal lokal"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canali locali"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ローカルチャンネル"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "지역 채널"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kanal lokal"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "स्थानीय च्यानल"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "lokale kanalen"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kanały lokalne"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canais locais"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "canais locais"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "локальные каналы"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "lokala kanaler"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "உள்ளூர் சேனல்கள்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ช่องท้องถิ่น"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "yerel kanallar"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "локальні канали"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "مقامی چینلز"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "kênh địa phương"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "本地频道"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "本地頻道"
          }
        }
      }
    },
    "app_info.features.mentions.description" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "استخدم @nickname لتنبيه أشخاص محددين"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "নির্দিষ্ট কাউকে জানানোর জন্য @nickname ব্যবহার করুন"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "nutze @nickname, um bestimmte personen zu benachrichtigen"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "use @nickname to notify specific people"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "usa @nickname para avisar a personas concretas"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "gumamit ng @nickname para abisuhan ang partikular na tao"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "utilise @nickname pour avertir des personnes précises"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "השתמש ב-@nickname כדי להתריע לאנשים ספציפיים"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "विशेष लोगों को सूचित करने के लिए @nickname उपयोग करें"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "pakai @nickname untuk memberi tahu orang tertentu"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "usa @nickname per avvisare persone specifiche"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "@nicknameで特定の人に通知"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "@닉네임을 사용하여 특정 사람에게 알림을 보낼 수 있습니다"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "guna @nickname untuk memberi tahu orang tertentu"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "विशेष व्यक्तिलाई सूचित गर्न @nickname प्रयोग गर"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "gebruik @nickname om een specifiek persoon te pingen"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "użyj @nickname, aby powiadomić konkretną osobę"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "usa @nickname para avisar pessoas específicas"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "use @nickname para notificar pessoas específicas"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "используй @nickname, чтобы уведомить конкретных людей"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "använd @nickname för att meddela en specifik person"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "குறிப்பிட்டவரை அறிவிக்க @nickname பயன்படுத்துங்கள்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ใช้ @nickname เพื่อแจ้งเตือนบุคคลเฉพาะ"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "belirli kişileri bildirmek için @nickname kullanın"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "використовуй @nickname, щоб сповістити конкретних людей"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "کسی مخصوص شخص کو خبردار کرنے کیلئے @nickname استعمال کریں"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "dùng @nickname để thông báo cho người cụ thể"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "使用 @nickname 提醒特定的人"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "使用 @nickname 提醒特定的人"
          }
        }
      }
    },
    "app_info.features.mentions.title" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "إشارات"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "মেনশন"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "erwähnungen"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mentions"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "menciones"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mga banggit"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mentions"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "אזכורים"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "मेंशन"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mention"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "menzioni"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "メンション"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "멘션"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "mention"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "उल्लेख"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "vermeldingen"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "wzmianki"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "menções"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "menções"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "упоминания"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "omnämnanden"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "மேற்கோள்கள்"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "การกล่าวถึง"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bahsetmeler"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "згадки"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ذکر"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "nhắc tới"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "提及"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "提及"
          }
        }
      }
    },
    "app_info.features.offline.description" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "يعمل بدون إنترنت باستخدام bluetooth منخفض الطاقة"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ব্লুটুথ লো এনার্জি ব্যবহার করে ইন্টারনেট ছাড়াই কাজ করে"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "funktioniert ohne internet per bluetooth low energy"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "works without internet using Bluetooth low energy"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "funciona sin internet utilizando Bluetooth de bajo consumo"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "gumagana kahit walang internet gamit ang Bluetooth Low Energy"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "fonctionne sans internet avec le bluetooth basse énergie"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "עובד בלי אינטרנט באמצעות bluetooth בתצריכת אנרגיה נמוכה"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ब्लूटूथ लो एनर्जी से इंटरनेट बिना भी काम करता है"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bekerja tanpa internet memakai bluetooth low energy"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "funziona senza internet usando bluetooth a basso consumo"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bluetooth low energyでインターネットなしでも動作"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "저전력 bluetooth를 사용하여 인터넷 없이 작동합니다"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "berfungsi tanpa internet menggunakan bluetooth low energy"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "bluetooth low energy प्रयोग गरेर इन्टरनेट बिना काम गर्छ"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "werkt zonder internet met Bluetooth Low Energy"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "działa bez internetu dzięki Bluetooth Low Energy"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "funciona sem internet usando Bluetooth de baixo consumo"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "funciona sem internet usando bluetooth de baixa energia"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "работает без интернета через bluetooth low energy"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "fungerar utan internet med Bluetooth Low Energy"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bluetooth குறைந்த மின்சாரத்தைப் பயன்படுத்தி இணையமின்றி இயங்குகிறது"
          }
        },
        "th" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ทำงานได้แม้ไม่มีอินเทอร์เน็ตด้วย Bluetooth พลังงานต่ำ"
          }
        },
        "tr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bluetooth Low Energy kullanarak internet olmadan çalışır"
          }
        },
        "uk" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "працює без інтернету через bluetooth low energy"
          }
        },
        "ur" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bluetooth Low Energy کے ساتھ بغیر انٹرنیٹ کے کام کرتا ہے"
          }
        },
        "vi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "hoạt động không cần internet bằng Bluetooth năng lượng thấp"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "利用低功耗 bluetooth 离线工作"
          }
        },
        "zh-Hant" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "利用低功耗 bluetooth 離線工作"
          }
        }
      }
    },
    "app_info.features.offline.title" : {
      "extractionState" : "manual",
      "localizations" : {
        "ar" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "تواصل بدون اتصال"
          }
        },
        "bn" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "অফলাইন যোগাযোগ"
          }
        },
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "offline-kommunikation"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "offline communication"
          }
        },
        "es" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "comunicación sin conexión"
          }
        },
        "fil" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "offline na komunikasyon"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "communication hors ligne"
          }
        },
        "he" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "תקשורת לא מקוונת"
          }
        },
        "hi" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ऑफलाइन संचार"
          }
        },
        "id" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "komunikasi offline"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "comunicazione offline"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "オフライン通信"
          }
        },
        "ko" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "오프라인 통신"
          }
        },
        "ms" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "komunikasi offline"
          }
        },
        "ne" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "अफलाइन सञ्चार"
          }
        },
        "nl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "offline communicatie"
          }
        },
        "pl" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "komunikacja offline"
          }
        },
        "pt" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "comunicação offline"
          }
        },
        "pt-BR" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "comunicação offline"
          }
        },
        "ru" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "офлайн-связь"
          }
        },
        "sv" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "offlinekommunikation"
          }
        },
        "ta" : {
          "stringUnit" : {
            "state" : "translated"
Download .txt
gitextract_fcb18bnp/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── fetch_georelays.yml
│       └── swift-tests.yml
├── .gitignore
├── BRING_THE_NOISE.md
├── Configs/
│   ├── Debug.xcconfig
│   ├── Local.xcconfig.example
│   └── Release.xcconfig
├── Justfile
├── LICENSE
├── PRIVACY_POLICY.md
├── Package.resolved
├── Package.swift
├── README.md
├── WHITEPAPER.md
├── bitchat/
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── AppIcon.appiconset/
│   │   │   └── Contents.json
│   │   ├── AppIconDebug.appiconset/
│   │   │   └── Contents.json
│   │   └── Contents.json
│   ├── BitchatApp.swift
│   ├── Features/
│   │   ├── media/
│   │   │   └── ImageUtils.swift
│   │   └── voice/
│   │       ├── VoiceNotePlaybackController.swift
│   │       ├── VoiceRecorder.swift
│   │       └── Waveform.swift
│   ├── Identity/
│   │   ├── IdentityModels.swift
│   │   └── SecureIdentityStateManager.swift
│   ├── Info.plist
│   ├── LaunchScreen.storyboard
│   ├── Localizable.xcstrings
│   ├── Models/
│   │   ├── BitchatMessage.swift
│   │   ├── BitchatPacket.swift
│   │   ├── BitchatPeer.swift
│   │   ├── CommandInfo.swift
│   │   ├── MessagePadding.swift
│   │   ├── NoisePayload.swift
│   │   ├── PeerID.swift
│   │   ├── ReadReceipt.swift
│   │   └── RequestSyncPacket.swift
│   ├── Noise/
│   │   ├── NoiseProtocol.swift
│   │   ├── NoiseRateLimiter.swift
│   │   ├── NoiseSecurityConstants.swift
│   │   ├── NoiseSecurityError.swift
│   │   ├── NoiseSecurityValidator.swift
│   │   ├── NoiseSession.swift
│   │   ├── NoiseSessionError.swift
│   │   ├── NoiseSessionManager.swift
│   │   ├── NoiseSessionState.swift
│   │   └── SecureNoiseSession.swift
│   ├── Nostr/
│   │   ├── Bech32.swift
│   │   ├── GeoRelayDirectory.swift
│   │   ├── NostrEmbeddedBitChat.swift
│   │   ├── NostrIdentity.swift
│   │   ├── NostrIdentityBridge.swift
│   │   ├── NostrProtocol.swift
│   │   ├── NostrRelayManager.swift
│   │   └── XChaCha20Poly1305Compat.swift
│   ├── Protocols/
│   │   ├── BinaryEncodingUtils.swift
│   │   ├── BinaryProtocol.swift
│   │   ├── BitchatFilePacket.swift
│   │   ├── BitchatProtocol.swift
│   │   ├── Geohash.swift
│   │   ├── LocationChannel.swift
│   │   └── Packets.swift
│   ├── Services/
│   │   ├── AutocompleteService.swift
│   │   ├── BLE/
│   │   │   ├── BLEService.swift
│   │   │   └── MimeType.swift
│   │   ├── CommandProcessor.swift
│   │   ├── FavoritesPersistenceService.swift
│   │   ├── GeohashParticipantTracker.swift
│   │   ├── GeohashPresenceService.swift
│   │   ├── KeychainManager.swift
│   │   ├── LocationNotesManager.swift
│   │   ├── LocationStateManager.swift
│   │   ├── MeshTopologyTracker.swift
│   │   ├── MessageDeduplicationService.swift
│   │   ├── MessageFormattingEngine.swift
│   │   ├── MessageRouter.swift
│   │   ├── NetworkActivationService.swift
│   │   ├── NoiseEncryptionService.swift
│   │   ├── NostrTransport.swift
│   │   ├── NotificationService.swift
│   │   ├── NotificationStreamAssembler.swift
│   │   ├── PrivateChatManager.swift
│   │   ├── RelayController.swift
│   │   ├── TransferProgressManager.swift
│   │   ├── Transport.swift
│   │   ├── TransportConfig.swift
│   │   ├── UnifiedPeerService.swift
│   │   └── VerificationService.swift
│   ├── Sync/
│   │   ├── GCSFilter.swift
│   │   ├── GossipSyncManager.swift
│   │   ├── PacketIdUtil.swift
│   │   ├── RequestSyncManager.swift
│   │   └── SyncTypeFlags.swift
│   ├── Utils/
│   │   ├── Color+Peer.swift
│   │   ├── CompressionUtil.swift
│   │   ├── Data+SHA256.swift
│   │   ├── FileTransferLimits.swift
│   │   ├── Font+Bitchat.swift
│   │   ├── InputValidator.swift
│   │   ├── MessageDeduplicator.swift
│   │   ├── PeerDisplayNameResolver.swift
│   │   ├── String+DJB2.swift
│   │   └── String+Nickname.swift
│   ├── ViewModels/
│   │   ├── ChatViewModel.swift
│   │   ├── Extensions/
│   │   │   ├── ChatViewModel+Nostr.swift
│   │   │   ├── ChatViewModel+PrivateChat.swift
│   │   │   ├── ChatViewModel+Tor.swift
│   │   │   └── README.md
│   │   ├── GeoChannelCoordinator.swift
│   │   ├── MessageRateLimiter.swift
│   │   ├── MinimalDistancePalette.swift
│   │   ├── PublicMessagePipeline.swift
│   │   └── PublicTimelineStore.swift
│   ├── Views/
│   │   ├── AppInfoView.swift
│   │   ├── Components/
│   │   │   ├── CommandSuggestionsView.swift
│   │   │   ├── DeliveryStatusView.swift
│   │   │   ├── PaymentChipView.swift
│   │   │   └── TextMessageView.swift
│   │   ├── ContentView.swift
│   │   ├── FingerprintView.swift
│   │   ├── GeohashPeopleList.swift
│   │   ├── LocationChannelsSheet.swift
│   │   ├── LocationNotesView.swift
│   │   ├── Media/
│   │   │   ├── BlockRevealImageView.swift
│   │   │   ├── VoiceNoteView.swift
│   │   │   └── WaveformView.swift
│   │   ├── MeshPeerList.swift
│   │   ├── MessageTextHelpers.swift
│   │   └── VerificationViews.swift
│   ├── _PreviewHelpers/
│   │   ├── BitchatMessage+Preview.swift
│   │   └── PreviewKeychainManager.swift
│   ├── bitchat-macOS.entitlements
│   └── bitchat.entitlements
├── bitchat.xcodeproj/
│   ├── project.pbxproj
│   └── xcshareddata/
│       └── xcschemes/
│           ├── bitchat (iOS).xcscheme
│           └── bitchat (macOS).xcscheme
├── bitchatShareExtension/
│   ├── Info.plist
│   ├── Localization/
│   │   └── Localizable.xcstrings
│   ├── ShareViewController.swift
│   └── bitchatShareExtension.entitlements
├── bitchatTests/
│   ├── BLEServiceCoreTests.swift
│   ├── BLEServiceTests.swift
│   ├── BitchatPeerTests.swift
│   ├── ChatViewModelDeliveryStatusTests.swift
│   ├── ChatViewModelExtensionsTests.swift
│   ├── ChatViewModelRefactoringTests.swift
│   ├── ChatViewModelTests.swift
│   ├── ChatViewModelTorTests.swift
│   ├── CommandProcessorTests.swift
│   ├── EndToEnd/
│   │   ├── PrivateChatE2ETests.swift
│   │   └── PublicChatE2ETests.swift
│   ├── Features/
│   │   └── ImageUtilsTests.swift
│   ├── FontBitchatTests.swift
│   ├── Fragmentation/
│   │   └── FragmentationTests.swift
│   ├── GCSFilterTests.swift
│   ├── GeohashBookmarksStoreTests.swift
│   ├── GeohashParticipantTrackerTests.swift
│   ├── GeohashPresenceTests.swift
│   ├── GossipSyncManagerTests.swift
│   ├── Info.plist
│   ├── InputValidatorTests.swift
│   ├── Integration/
│   │   ├── IntegrationTests.swift
│   │   └── TestNetworkHelper.swift
│   ├── KeychainErrorHandlingTests.swift
│   ├── Localization/
│   │   └── PrimaryLocalizationKeys.json
│   ├── LocationChannelsTests.swift
│   ├── LocationNotesManagerTests.swift
│   ├── MessageDeduplicationServiceTests.swift
│   ├── MessageFormattingEngineTests.swift
│   ├── MimeTypeTests.swift
│   ├── Mocks/
│   │   ├── MockBLEBus.swift
│   │   ├── MockBLEService.swift
│   │   ├── MockIdentityManager.swift
│   │   ├── MockKeychain.swift
│   │   └── MockTransport.swift
│   ├── Noise/
│   │   ├── NoiseCoverageTests.swift
│   │   ├── NoiseProtocolTests.swift
│   │   ├── NoiseRateLimiterTests.swift
│   │   └── NoiseTestVectors.json
│   ├── Nostr/
│   │   └── GeoRelayDirectoryTests.swift
│   ├── NostrProtocolTests.swift
│   ├── NotificationBlockingTests.swift
│   ├── NotificationStreamAssemblerTests.swift
│   ├── PreviewKeychainManagerTests.swift
│   ├── Protocol/
│   │   ├── BinaryProtocolPaddingTests.swift
│   │   └── BinaryProtocolTests.swift
│   ├── ProtocolContractTests.swift
│   ├── Protocols/
│   │   ├── BinaryEncodingUtilsTests.swift
│   │   ├── BitchatFilePacketTests.swift
│   │   ├── LocationChannelTests.swift
│   │   └── PacketsTests.swift
│   ├── PublicMessagePipelineTests.swift
│   ├── PublicTimelineStoreTests.swift
│   ├── README.md
│   ├── ReadReceiptTests.swift
│   ├── Services/
│   │   ├── AutocompleteServiceTests.swift
│   │   ├── FavoritesPersistenceServiceTests.swift
│   │   ├── GeohashPresenceServiceTests.swift
│   │   ├── LocationStateManagerTests.swift
│   │   ├── MeshTopologyTrackerTests.swift
│   │   ├── MessageRouterTests.swift
│   │   ├── NetworkActivationServiceTests.swift
│   │   ├── NoiseEncryptionServiceTests.swift
│   │   ├── NostrRelayManagerTests.swift
│   │   ├── NostrTransportTests.swift
│   │   ├── NotificationServiceTests.swift
│   │   ├── PrivateChatManagerTests.swift
│   │   ├── RelayControllerTests.swift
│   │   ├── SecureIdentityStateManagerTests.swift
│   │   ├── TransferProgressManagerTests.swift
│   │   ├── UnifiedPeerServiceTests.swift
│   │   └── VerificationServiceTests.swift
│   ├── SubscriptionRateLimitTests.swift
│   ├── Sync/
│   │   └── RequestSyncManagerTests.swift
│   ├── TestUtilities/
│   │   ├── TestConstants.swift
│   │   └── TestHelpers.swift
│   ├── Utils/
│   │   ├── HexStringTests.swift
│   │   └── PeerIDTests.swift
│   ├── ViewSmokeTests.swift
│   └── XChaCha20Poly1305CompatTests.swift
├── docs/
│   ├── GeohashPresenceSpec.md
│   ├── REQUEST_SYNC_MANAGER.md
│   ├── SOURCE_ROUTING.md
│   ├── TOR-INTEGRATION.md
│   └── privacy-assessment.md
├── localPackages/
│   ├── Arti/
│   │   ├── .gitignore
│   │   ├── Cargo.toml
│   │   ├── Frameworks/
│   │   │   ├── arti.xcframework/
│   │   │   │   ├── Info.plist
│   │   │   │   ├── ios-arm64/
│   │   │   │   │   ├── Headers/
│   │   │   │   │   │   └── arti.h
│   │   │   │   │   └── libarti_bitchat.a
│   │   │   │   ├── ios-arm64-simulator/
│   │   │   │   │   ├── Headers/
│   │   │   │   │   │   └── arti.h
│   │   │   │   │   └── libarti_bitchat.a
│   │   │   │   └── macos-arm64/
│   │   │   │       ├── Headers/
│   │   │   │       │   └── arti.h
│   │   │   │       └── libarti_bitchat.a
│   │   │   └── include/
│   │   │       └── arti.h
│   │   ├── Package.swift
│   │   ├── Sources/
│   │   │   ├── C/
│   │   │   │   ├── arti_shim.c
│   │   │   │   └── include/
│   │   │   │       ├── arti.h
│   │   │   │       └── module.modulemap
│   │   │   ├── TorManager.swift
│   │   │   ├── TorNotifications.swift
│   │   │   └── TorURLSession.swift
│   │   ├── arti-bitchat/
│   │   │   ├── Cargo.toml
│   │   │   ├── cbindgen.toml
│   │   │   └── src/
│   │   │       ├── lib.rs
│   │   │       └── socks.rs
│   │   └── build-ios.sh
│   └── BitLogger/
│       ├── Package.swift
│       ├── Sources/
│       │   ├── OSLog+Categories.swift
│       │   ├── SecureLogger.swift
│       │   └── String+Sanitization.swift
│       └── Tests/
│           └── StringSanitizationTests.swift
└── relays/
    └── online_relays_gps.csv
Download .txt
SYMBOL INDEX (22 symbols across 2 files)

FILE: localPackages/Arti/arti-bitchat/src/lib.rs
  type ArtiState (line 22) | struct ArtiState {
  function init_state (line 37) | fn init_state() -> Result<(), &'static str> {
  function arti_start (line 62) | pub extern "C" fn arti_start(data_dir: *const c_char, socks_port: u16) -...
  function arti_stop (line 126) | pub extern "C" fn arti_stop() -> c_int {
  function arti_is_running (line 165) | pub extern "C" fn arti_is_running() -> c_int {
  function arti_bootstrap_progress (line 175) | pub extern "C" fn arti_bootstrap_progress() -> c_int {
  function arti_bootstrap_summary (line 189) | pub extern "C" fn arti_bootstrap_summary(buf: *mut c_char, len: c_int) -...
  function arti_go_dormant (line 217) | pub extern "C" fn arti_go_dormant() -> c_int {
  function arti_wake (line 232) | pub extern "C" fn arti_wake() -> c_int {
  function update_summary (line 240) | fn update_summary(s: &str) {
  function run_arti (line 248) | async fn run_arti(

FILE: localPackages/Arti/arti-bitchat/src/socks.rs
  constant SOCKS5_VERSION (line 15) | const SOCKS5_VERSION: u8 = 0x05;
  constant SOCKS5_AUTH_NONE (line 16) | const SOCKS5_AUTH_NONE: u8 = 0x00;
  constant SOCKS5_CMD_CONNECT (line 17) | const SOCKS5_CMD_CONNECT: u8 = 0x01;
  constant SOCKS5_ATYP_IPV4 (line 18) | const SOCKS5_ATYP_IPV4: u8 = 0x01;
  constant SOCKS5_ATYP_DOMAIN (line 19) | const SOCKS5_ATYP_DOMAIN: u8 = 0x03;
  constant SOCKS5_ATYP_IPV6 (line 20) | const SOCKS5_ATYP_IPV6: u8 = 0x04;
  constant SOCKS5_REP_SUCCESS (line 21) | const SOCKS5_REP_SUCCESS: u8 = 0x00;
  constant SOCKS5_REP_FAILURE (line 22) | const SOCKS5_REP_FAILURE: u8 = 0x01;
  constant SOCKS5_REP_CONN_REFUSED (line 23) | const SOCKS5_REP_CONN_REFUSED: u8 = 0x05;
  function handle_socks_connection (line 26) | pub async fn handle_socks_connection(
  function send_reply (line 198) | async fn send_reply(stream: &mut TcpStream, rep: u8) -> io::Result<()> {
Condensed preview — 254 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,462K chars).
[
  {
    "path": ".gitattributes",
    "chars": 452,
    "preview": "# Prevent Github Languages stats skewing:\n\n# Binaries and assets\n**/*.xcframework/** linguist-vendored\n**/*.xcassets/** "
  },
  {
    "path": ".github/workflows/fetch_georelays.yml",
    "chars": 1153,
    "preview": "name: Fetch GeoRelays Data\n\non:\n  schedule:\n    - cron: '0 6 * * 0'\n  workflow_dispatch:\n\npermissions:\n  contents: write"
  },
  {
    "path": ".github/workflows/swift-tests.yml",
    "chars": 432,
    "preview": "name: Build & Test\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  test:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 1121,
    "preview": "# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n"
  },
  {
    "path": "BRING_THE_NOISE.md",
    "chars": 6027,
    "preview": "# Bringing the Noise: Secure Communication in BitChat\n\n## Overview\n\nBitChat implements the Noise Protocol Framework for "
  },
  {
    "path": "Configs/Debug.xcconfig",
    "chars": 93,
    "preview": "#include \"Release.xcconfig\"\n\n// Optional include of local configs\n#include? \"Local.xcconfig\"\n"
  },
  {
    "path": "Configs/Local.xcconfig.example",
    "chars": 218,
    "preview": "// Your Apple Developer Team ID - https://stackoverflow.com/a/18727947\nDEVELOPMENT_TEAM = ABC123\n\n// Unique bundle id to"
  },
  {
    "path": "Configs/Release.xcconfig",
    "chars": 242,
    "preview": "MARKETING_VERSION = 1.5.1\nCURRENT_PROJECT_VERSION = 1\n\nIPHONEOS_DEPLOYMENT_TARGET = 16.0\nMACOSX_DEPLOYMENT_TARGET = 13.0"
  },
  {
    "path": "Justfile",
    "chars": 5701,
    "preview": "# BitChat macOS Build Justfile\n# Handles temporary modifications needed to build and run on macOS\n\n# Default recipe - sh"
  },
  {
    "path": "LICENSE",
    "chars": 1210,
    "preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
  },
  {
    "path": "PRIVACY_POLICY.md",
    "chars": 4605,
    "preview": "# bitchat Privacy Policy\n\n*Last updated: January 2025*\n\n## Our Commitment\n\nbitchat is designed with privacy as its found"
  },
  {
    "path": "Package.resolved",
    "chars": 314,
    "preview": "{\n  \"pins\" : [\n    {\n      \"identity\" : \"swift-secp256k1\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"htt"
  },
  {
    "path": "Package.swift",
    "chars": 1597,
    "preview": "// swift-tools-version: 5.9\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"bitchat\",\n    defaultLocalizat"
  },
  {
    "path": "README.md",
    "chars": 5478,
    "preview": "<img width=\"256\" height=\"256\" alt=\"icon_128x128@2x\" src=\"https://github.com/user-attachments/assets/90133f83-b4f6-41c6-a"
  },
  {
    "path": "WHITEPAPER.md",
    "chars": 19628,
    "preview": "# BitChat Protocol Whitepaper\n\n**Version 1.1**\n\n**Date: July 25, 2025**\n\n---\n\n## Abstract\n\nBitChat is a decentralized, p"
  },
  {
    "path": "bitchat/Assets.xcassets/AccentColor.colorset/Contents.json",
    "chars": 328,
    "preview": "{\n  \"colors\" : [\n    {\n      \"color\" : {\n        \"color-space\" : \"srgb\",\n        \"components\" : {\n          \"alpha\" : \"1"
  },
  {
    "path": "bitchat/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1437,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"icon_1024x1024.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n "
  },
  {
    "path": "bitchat/Assets.xcassets/AppIconDebug.appiconset/Contents.json",
    "chars": 644,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"image-1024.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n     "
  },
  {
    "path": "bitchat/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "bitchat/BitchatApp.swift",
    "chars": 11955,
    "preview": "//\n// BitchatApp.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For m"
  },
  {
    "path": "bitchat/Features/media/ImageUtils.swift",
    "chars": 8418,
    "preview": "import Foundation\nimport ImageIO\nimport UniformTypeIdentifiers\n#if os(iOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\nenum"
  },
  {
    "path": "bitchat/Features/voice/VoiceNotePlaybackController.swift",
    "chars": 5788,
    "preview": "import Foundation\nimport AVFoundation\nimport BitLogger\n\n/// Controls playback for a single voice note and coordinates ex"
  },
  {
    "path": "bitchat/Features/voice/VoiceRecorder.swift",
    "chars": 6258,
    "preview": "import Foundation\nimport AVFoundation\n\n/// Manages audio capture for mesh voice notes with predictable encoding settings"
  },
  {
    "path": "bitchat/Features/voice/Waveform.swift",
    "chars": 4388,
    "preview": "import AVFoundation\nimport Foundation\nimport BitLogger\n\n/// Generates and caches downsampled waveforms for audio files s"
  },
  {
    "path": "bitchat/Identity/IdentityModels.swift",
    "chars": 5707,
    "preview": "//\n// IdentityModels.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// F"
  },
  {
    "path": "bitchat/Identity/SecureIdentityStateManager.swift",
    "chars": 20470,
    "preview": "//\n// SecureIdentityStateManager.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public "
  },
  {
    "path": "bitchat/Info.plist",
    "chars": 2266,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "bitchat/LaunchScreen.storyboard",
    "chars": 3216,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
  },
  {
    "path": "bitchat/Localizable.xcstrings",
    "chars": 922355,
    "preview": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"%@\" : {\n      \"comment\" : \"Non-localizable symbol used in code\",\n     "
  },
  {
    "path": "bitchat/Models/BitchatMessage.swift",
    "chars": 12329,
    "preview": "//\n// BitchatMessage.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// F"
  },
  {
    "path": "bitchat/Models/BitchatPacket.swift",
    "chars": 3429,
    "preview": "//\n// BitchatPacket.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// Fo"
  },
  {
    "path": "bitchat/Models/BitchatPeer.swift",
    "chars": 2891,
    "preview": "import Foundation\nimport CoreBluetooth\n\n/// Represents a peer in the BitChat network with all associated metadata\nstruct"
  },
  {
    "path": "bitchat/Models/CommandInfo.swift",
    "chars": 1826,
    "preview": "//\n// CommandsInfo.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For"
  },
  {
    "path": "bitchat/Models/MessagePadding.swift",
    "chars": 2212,
    "preview": "//\n// MessagePadding.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// F"
  },
  {
    "path": "bitchat/Models/NoisePayload.swift",
    "chars": 1140,
    "preview": "//\n// NoisePayload.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For"
  },
  {
    "path": "bitchat/Models/PeerID.swift",
    "chars": 6450,
    "preview": "//\n// PeerID.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For more "
  },
  {
    "path": "bitchat/Models/ReadReceipt.swift",
    "chars": 3298,
    "preview": "//\n// ReadReceipt.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For "
  },
  {
    "path": "bitchat/Models/RequestSyncPacket.swift",
    "chars": 3562,
    "preview": "import Foundation\n\n// REQUEST_SYNC payload TLV (type, length16, value)\n//  - 0x01: P (uint8) — Golomb-Rice parameter\n// "
  },
  {
    "path": "bitchat/Noise/NoiseProtocol.swift",
    "chars": 40106,
    "preview": "//\n// NoiseProtocol.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// Fo"
  },
  {
    "path": "bitchat/Noise/NoiseRateLimiter.swift",
    "chars": 3958,
    "preview": "//\n// NoiseRateLimiter.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n//"
  },
  {
    "path": "bitchat/Noise/NoiseSecurityConstants.swift",
    "chars": 1254,
    "preview": "//\n// NoiseSecurityConstants.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public doma"
  },
  {
    "path": "bitchat/Noise/NoiseSecurityError.swift",
    "chars": 382,
    "preview": "//\n// NoiseSecurityError.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n"
  },
  {
    "path": "bitchat/Noise/NoiseSecurityValidator.swift",
    "chars": 598,
    "preview": "//\n// NoiseSecurityValidator.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public doma"
  },
  {
    "path": "bitchat/Noise/NoiseSession.swift",
    "chars": 8235,
    "preview": "//\n// NoiseSession.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For"
  },
  {
    "path": "bitchat/Noise/NoiseSessionError.swift",
    "chars": 320,
    "preview": "//\n// NoiseSessionError.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n/"
  },
  {
    "path": "bitchat/Noise/NoiseSessionManager.swift",
    "chars": 8328,
    "preview": "//\n// NoiseSessionManager.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain."
  },
  {
    "path": "bitchat/Noise/NoiseSessionState.swift",
    "chars": 279,
    "preview": "//\n// NoiseSessionState.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n/"
  },
  {
    "path": "bitchat/Noise/SecureNoiseSession.swift",
    "chars": 2592,
    "preview": "//\n// SecureNoiseSession.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n"
  },
  {
    "path": "bitchat/Nostr/Bech32.swift",
    "chars": 4395,
    "preview": "import Foundation\n\n/// Bech32 encoding for Nostr (minimal implementation)\nenum Bech32 {\n    private static let charset ="
  },
  {
    "path": "bitchat/Nostr/GeoRelayDirectory.swift",
    "chars": 16903,
    "preview": "import BitLogger\nimport Foundation\nimport Tor\n#if os(iOS)\nimport UIKit\n#elseif os(macOS)\nimport AppKit\n#endif\n\n/// Direc"
  },
  {
    "path": "bitchat/Nostr/NostrEmbeddedBitChat.swift",
    "chars": 4959,
    "preview": "import Foundation\n\n// MARK: - BitChat-over-Nostr Adapter\n\nstruct NostrEmbeddedBitChat {\n    /// Build a `bitchat1:` base"
  },
  {
    "path": "bitchat/Nostr/NostrIdentity.swift",
    "chars": 2086,
    "preview": "import Foundation\nimport P256K\n\n/// Manages Nostr identity (secp256k1 keypair) for NIP-17 private messaging\nstruct Nostr"
  },
  {
    "path": "bitchat/Nostr/NostrIdentityBridge.swift",
    "chars": 6603,
    "preview": "import Foundation\nimport CryptoKit\n\n/// Bridge between Noise and Nostr identities\nfinal class NostrIdentityBridge {\n    "
  },
  {
    "path": "bitchat/Nostr/NostrProtocol.swift",
    "chars": 21046,
    "preview": "import BitLogger\nimport Foundation\nimport CryptoKit\nimport P256K\nimport Security\n\n// Note: This file depends on Data ext"
  },
  {
    "path": "bitchat/Nostr/NostrRelayManager.swift",
    "chars": 43418,
    "preview": "import BitLogger\nimport Foundation\nimport Network\nimport Combine\nimport Tor\n\nprotocol NostrRelayConnectionProtocol: AnyO"
  },
  {
    "path": "bitchat/Nostr/XChaCha20Poly1305Compat.swift",
    "chars": 5461,
    "preview": "import Foundation\nimport CryptoKit\n\n/// Minimal XChaCha20-Poly1305 compatibility wrapper using CryptoKit's ChaChaPoly.\n/"
  },
  {
    "path": "bitchat/Protocols/BinaryEncodingUtils.swift",
    "chars": 7485,
    "preview": "//\n// BinaryEncodingUtils.swift\n// bitchat\n//\n// Binary encoding utilities for efficient protocol messages\n//\n\nimport Fo"
  },
  {
    "path": "bitchat/Protocols/BinaryProtocol.swift",
    "chars": 16714,
    "preview": "//\n// BinaryProtocol.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// F"
  },
  {
    "path": "bitchat/Protocols/BitchatFilePacket.swift",
    "chars": 5795,
    "preview": "//\n// BitchatFilePacket.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n/"
  },
  {
    "path": "bitchat/Protocols/BitchatProtocol.swift",
    "chars": 7506,
    "preview": "//\n// BitchatProtocol.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchat/Protocols/Geohash.swift",
    "chars": 7017,
    "preview": "import Foundation\n\n/// Lightweight Geohash encoder used for Location Channels.\n/// Encodes latitude/longitude to base32 "
  },
  {
    "path": "bitchat/Protocols/LocationChannel.swift",
    "chars": 4278,
    "preview": "import Foundation\n\n/// Levels of location channels mapped to geohash precisions.\nenum GeohashChannelLevel: CaseIterable,"
  },
  {
    "path": "bitchat/Protocols/Packets.swift",
    "chars": 5770,
    "preview": "import Foundation\n\n// MARK: - Protocol TLV Packets\n\nstruct AnnouncementPacket {\n    let nickname: String\n    let noisePu"
  },
  {
    "path": "bitchat/Services/AutocompleteService.swift",
    "chars": 3763,
    "preview": "//\n// AutocompleteService.swift\n// bitchat\n//\n// Handles autocomplete suggestions for mentions and commands\n// This is f"
  },
  {
    "path": "bitchat/Services/BLE/BLEService.swift",
    "chars": 210286,
    "preview": "import BitLogger\nimport Foundation\nimport CoreBluetooth\nimport Combine\nimport CryptoKit\n#if os(iOS)\nimport UIKit\n#endif\n"
  },
  {
    "path": "bitchat/Services/BLE/MimeType.swift",
    "chars": 5847,
    "preview": "//\n// MimeType.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For mor"
  },
  {
    "path": "bitchat/Services/CommandProcessor.swift",
    "chars": 15488,
    "preview": "//\n// CommandProcessor.swift\n// bitchat\n//\n// Handles command parsing and execution for BitChat\n// This is free and unen"
  },
  {
    "path": "bitchat/Services/FavoritesPersistenceService.swift",
    "chars": 12243,
    "preview": "import BitLogger\nimport Foundation\nimport Combine\n\n/// Manages persistent favorite relationships between peers\n@MainActo"
  },
  {
    "path": "bitchat/Services/GeohashParticipantTracker.swift",
    "chars": 5303,
    "preview": "//\n// GeohashParticipantTracker.swift\n// bitchat\n//\n// Tracks participants in geohash-based location channels.\n// This i"
  },
  {
    "path": "bitchat/Services/GeohashPresenceService.swift",
    "chars": 9897,
    "preview": "//\n// GeohashPresenceService.swift\n// bitchat\n//\n// Manages the broadcasting of ephemeral presence heartbeats (Kind 2000"
  },
  {
    "path": "bitchat/Services/KeychainManager.swift",
    "chars": 22715,
    "preview": "//\n// KeychainManager.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchat/Services/LocationNotesManager.swift",
    "chars": 10100,
    "preview": "import BitLogger\nimport Foundation\n\n/// Dependencies for location notes, allowing tests to stub relay/identity behavior."
  },
  {
    "path": "bitchat/Services/LocationStateManager.swift",
    "chars": 21995,
    "preview": "import BitLogger\nimport Foundation\nimport Combine\n\n#if os(iOS) || os(macOS)\nimport CoreLocation\n\nprotocol LocationStateM"
  },
  {
    "path": "bitchat/Services/MeshTopologyTracker.swift",
    "chars": 5073,
    "preview": "import Foundation\n\n/// Tracks observed mesh topology and computes hop-by-hop routes.\nfinal class MeshTopologyTracker {\n "
  },
  {
    "path": "bitchat/Services/MessageDeduplicationService.swift",
    "chars": 9428,
    "preview": "//\n// MessageDeduplicationService.swift\n// bitchat\n//\n// Handles message deduplication using LRU caches.\n// This is free"
  },
  {
    "path": "bitchat/Services/MessageFormattingEngine.swift",
    "chars": 18291,
    "preview": "//\n// MessageFormattingEngine.swift\n// bitchat\n//\n// Handles message text formatting, including mentions, hashtags, URLs"
  },
  {
    "path": "bitchat/Services/MessageRouter.swift",
    "chars": 6442,
    "preview": "import BitLogger\nimport Foundation\n\n/// Routes messages using available transports (Mesh, Nostr, etc.)\n@MainActor\nfinal "
  },
  {
    "path": "bitchat/Services/NetworkActivationService.swift",
    "chars": 6578,
    "preview": "import Foundation\nimport BitLogger\nimport Combine\nimport Tor\n\n@MainActor\nprotocol NetworkActivationTorControlling: AnyOb"
  },
  {
    "path": "bitchat/Services/NoiseEncryptionService.swift",
    "chars": 31121,
    "preview": "//\n// NoiseEncryptionService.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public doma"
  },
  {
    "path": "bitchat/Services/NostrTransport.swift",
    "chars": 14843,
    "preview": "import BitLogger\nimport Foundation\nimport Combine\n\n// Minimal Nostr transport conforming to Transport for offline sendin"
  },
  {
    "path": "bitchat/Services/NotificationService.swift",
    "chars": 6212,
    "preview": "//\n// NotificationService.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain."
  },
  {
    "path": "bitchat/Services/NotificationStreamAssembler.swift",
    "chars": 6081,
    "preview": "//\n// NotificationStreamAssembler.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public"
  },
  {
    "path": "bitchat/Services/PrivateChatManager.swift",
    "chars": 11283,
    "preview": "//\n// PrivateChatManager.swift\n// bitchat\n//\n// Manages private chat sessions and messages\n// This is free and unencumbe"
  },
  {
    "path": "bitchat/Services/RelayController.swift",
    "chars": 3086,
    "preview": "import Foundation\n\n// RelayDecision encapsulates a single relay scheduling choice.\nstruct RelayDecision {\n    let should"
  },
  {
    "path": "bitchat/Services/TransferProgressManager.swift",
    "chars": 2422,
    "preview": "import Foundation\nimport Combine\n\n/// Centralized progress bus for Bluetooth file transfers.\n/// Emits Combine events co"
  },
  {
    "path": "bitchat/Services/Transport.swift",
    "chars": 3452,
    "preview": "import Foundation\nimport Combine\n\n/// Abstract transport interface used by ChatViewModel and services.\n/// BLEService im"
  },
  {
    "path": "bitchat/Services/TransportConfig.swift",
    "chars": 11852,
    "preview": "import Foundation\n\n/// Centralized knobs for transport- and UI-related limits.\n/// Keep values aligned with existing beh"
  },
  {
    "path": "bitchat/Services/UnifiedPeerService.swift",
    "chars": 13440,
    "preview": "//\n//  UnifiedPeerService.swift\n//  bitchat\n//\n//  Unified peer state management combining mesh connectivity and favorit"
  },
  {
    "path": "bitchat/Services/VerificationService.swift",
    "chars": 9155,
    "preview": "import Foundation\n\n/// QR verification scaffolding: schema, signing, and basic challenge/response helpers.\nfinal class V"
  },
  {
    "path": "bitchat/Sync/GCSFilter.swift",
    "chars": 8031,
    "preview": "import Foundation\nimport CryptoKit\n\n// Golomb-Coded Set (GCS) filter utilities for sync.\n// Hashing:\n//  - Packet ID is "
  },
  {
    "path": "bitchat/Sync/GossipSyncManager.swift",
    "chars": 18875,
    "preview": "import Foundation\nimport BitLogger\n\n// Gossip-based sync manager using on-demand GCS filters\nfinal class GossipSyncManag"
  },
  {
    "path": "bitchat/Sync/PacketIdUtil.swift",
    "chars": 633,
    "preview": "import Foundation\nimport CryptoKit\n\n// Deterministic packet ID used for gossip sync membership\n// ID = first 16 bytes of"
  },
  {
    "path": "bitchat/Sync/RequestSyncManager.swift",
    "chars": 3121,
    "preview": "//\n// RequestSyncManager.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n"
  },
  {
    "path": "bitchat/Sync/SyncTypeFlags.swift",
    "chars": 3378,
    "preview": "import Foundation\n\n/// Bitfield describing which message types are covered by a REQUEST_SYNC round.\n/// Matches the Andr"
  },
  {
    "path": "bitchat/Utils/Color+Peer.swift",
    "chars": 1323,
    "preview": "//\n// Color+Peer.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For m"
  },
  {
    "path": "bitchat/Utils/CompressionUtil.swift",
    "chars": 3012,
    "preview": "//\n// CompressionUtil.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchat/Utils/Data+SHA256.swift",
    "chars": 523,
    "preview": "//\n//  Data+SHA256.swift\n//  bitchat\n//\n//  Created by Islam on 26/09/2025.\n//\n\nimport struct Foundation.Data\nimport str"
  },
  {
    "path": "bitchat/Utils/FileTransferLimits.swift",
    "chars": 1253,
    "preview": "import Foundation\n\n/// Centralized thresholds for Bluetooth file transfers to keep payload sizes sane on constrained rad"
  },
  {
    "path": "bitchat/Utils/Font+Bitchat.swift",
    "chars": 1191,
    "preview": "import SwiftUI\n\n/// Provides Dynamic Type aware font helpers that map existing fixed sizes onto\n/// preferred text style"
  },
  {
    "path": "bitchat/Utils/InputValidator.swift",
    "chars": 2739,
    "preview": "import Foundation\nimport BitLogger\n\n/// Comprehensive input validation for BitChat protocol\n/// Prevents injection attac"
  },
  {
    "path": "bitchat/Utils/MessageDeduplicator.swift",
    "chars": 4262,
    "preview": "import Foundation\n\n// MARK: - Message Deduplicator (shared)\n\n/// Thread-safe deduplicator with LRU eviction and time-bas"
  },
  {
    "path": "bitchat/Utils/PeerDisplayNameResolver.swift",
    "chars": 1226,
    "preview": "import Foundation\n\n/// Resolves a stable display name for peers, adding a short suffix when collisions exist.\nstruct Pee"
  },
  {
    "path": "bitchat/Utils/String+DJB2.swift",
    "chars": 364,
    "preview": "//\n// String+DJB2.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For "
  },
  {
    "path": "bitchat/Utils/String+Nickname.swift",
    "chars": 811,
    "preview": "//\n// String+Nickname.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchat/ViewModels/ChatViewModel.swift",
    "chars": 170500,
    "preview": "//\n// ChatViewModel.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// Fo"
  },
  {
    "path": "bitchat/ViewModels/Extensions/ChatViewModel+Nostr.swift",
    "chars": 38783,
    "preview": "//\n// ChatViewModel+Nostr.swift\n// bitchat\n//\n// Geohash and Nostr logic for ChatViewModel\n//\n\nimport Foundation\nimport "
  },
  {
    "path": "bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift",
    "chars": 47094,
    "preview": "//\n// ChatViewModel+PrivateChat.swift\n// bitchat\n//\n// Private chat and media transfer logic for ChatViewModel\n//\n\nimpor"
  },
  {
    "path": "bitchat/ViewModels/Extensions/ChatViewModel+Tor.swift",
    "chars": 2176,
    "preview": "//\n// ChatViewModel+Tor.swift\n// bitchat\n//\n// Tor lifecycle handling for ChatViewModel\n//\n\nimport Foundation\nimport Com"
  },
  {
    "path": "bitchat/ViewModels/Extensions/README.md",
    "chars": 537,
    "preview": "# ChatViewModel Extensions\n\nThis directory contains extensions to `ChatViewModel` to modularize its functionality.\n\n- `C"
  },
  {
    "path": "bitchat/ViewModels/GeoChannelCoordinator.swift",
    "chars": 3724,
    "preview": "//\n// GeoChannelCoordinator.swift\n// bitchat\n//\n// Centralizes Combine wiring for location channel selection and samplin"
  },
  {
    "path": "bitchat/ViewModels/MessageRateLimiter.swift",
    "chars": 2285,
    "preview": "//\n// MessageRateLimiter.swift\n// bitchat\n//\n// Handles per-sender and per-content token buckets for public message inta"
  },
  {
    "path": "bitchat/ViewModels/MinimalDistancePalette.swift",
    "chars": 7529,
    "preview": "//\n// MinimalDistancePalette.swift\n// bitchat\n//\n// Lightweight palette generator that keeps peer colors evenly spaced.\n"
  },
  {
    "path": "bitchat/ViewModels/PublicMessagePipeline.swift",
    "chars": 6525,
    "preview": "//\n// PublicMessagePipeline.swift\n// bitchat\n//\n// Handles batching and deduplication of public chat messages before sur"
  },
  {
    "path": "bitchat/ViewModels/PublicTimelineStore.swift",
    "chars": 4289,
    "preview": "//\n// PublicTimelineStore.swift\n// bitchat\n//\n// Maintains mesh and geohash public timelines with simple caps and helper"
  },
  {
    "path": "bitchat/Views/AppInfoView.swift",
    "chars": 9135,
    "preview": "import SwiftUI\n\nstruct AppInfoView: View {\n    @Environment(\\.dismiss) var dismiss\n    @Environment(\\.colorScheme) var c"
  },
  {
    "path": "bitchat/Views/Components/CommandSuggestionsView.swift",
    "chars": 2871,
    "preview": "//\n//  CommandSuggestionsView.swift\n//  bitchat\n//\n//  Created by Islam on 29/10/2025.\n//\n\nimport SwiftUI\n\nstruct Comman"
  },
  {
    "path": "bitchat/Views/Components/DeliveryStatusView.swift",
    "chars": 4726,
    "preview": "//\n// DeliveryStatusView.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n"
  },
  {
    "path": "bitchat/Views/Components/PaymentChipView.swift",
    "chars": 3627,
    "preview": "//\n// PaymentChipView.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchat/Views/Components/TextMessageView.swift",
    "chars": 4123,
    "preview": "//\n// TextMessageView.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchat/Views/ContentView.swift",
    "chars": 92627,
    "preview": "//\n// ContentView.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For "
  },
  {
    "path": "bitchat/Views/FingerprintView.swift",
    "chars": 12337,
    "preview": "//\n// FingerprintView.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchat/Views/GeohashPeopleList.swift",
    "chars": 6560,
    "preview": "import SwiftUI\n\nstruct GeohashPeopleList: View {\n    @ObservedObject var viewModel: ChatViewModel\n    let textColor: Col"
  },
  {
    "path": "bitchat/Views/LocationChannelsSheet.swift",
    "chars": 27331,
    "preview": "import SwiftUI\nimport CoreLocation\n#if os(iOS)\nimport UIKit\n#else\nimport AppKit\n#endif\nstruct LocationChannelsSheet: Vie"
  },
  {
    "path": "bitchat/Views/LocationNotesView.swift",
    "chars": 12100,
    "preview": "import SwiftUI\n\nstruct LocationNotesView: View {\n    @EnvironmentObject var viewModel: ChatViewModel\n    @StateObject pr"
  },
  {
    "path": "bitchat/Views/Media/BlockRevealImageView.swift",
    "chars": 6624,
    "preview": "import SwiftUI\n\n#if os(iOS)\nimport UIKit\nprivate typealias PlatformImage = UIImage\n#else\nimport AppKit\nprivate typealias"
  },
  {
    "path": "bitchat/Views/Media/VoiceNoteView.swift",
    "chars": 4261,
    "preview": "import SwiftUI\nimport AVFoundation\n\nstruct VoiceNoteView: View {\n    private let url: URL\n    private let isSending: Boo"
  },
  {
    "path": "bitchat/Views/Media/WaveformView.swift",
    "chars": 2671,
    "preview": "import SwiftUI\n\nstruct WaveformView: View {\n    let samples: [Float]\n    let playbackProgress: Double\n    let sendProgre"
  },
  {
    "path": "bitchat/Views/MeshPeerList.swift",
    "chars": 8480,
    "preview": "import SwiftUI\n\nstruct MeshPeerList: View {\n    @ObservedObject var viewModel: ChatViewModel\n    let textColor: Color\n  "
  },
  {
    "path": "bitchat/Views/MessageTextHelpers.swift",
    "chars": 2549,
    "preview": "//\n// MessageTextHelpers.swift\n// Shared text parsing helpers for message rendering.\n//\n\nimport Foundation\n\nextension St"
  },
  {
    "path": "bitchat/Views/VerificationViews.swift",
    "chars": 15605,
    "preview": "import SwiftUI\nimport CoreImage\nimport CoreImage.CIFilterBuiltins\n#if os(iOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n\n/"
  },
  {
    "path": "bitchat/_PreviewHelpers/BitchatMessage+Preview.swift",
    "chars": 664,
    "preview": "//\n// BitchatMessage+Preview.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public doma"
  },
  {
    "path": "bitchat/_PreviewHelpers/PreviewKeychainManager.swift",
    "chars": 1985,
    "preview": "//\n// PreviewKeychainManager.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public doma"
  },
  {
    "path": "bitchat/bitchat-macOS.entitlements",
    "chars": 849,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "bitchat/bitchat.entitlements",
    "chars": 403,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "bitchat.xcodeproj/project.pbxproj",
    "chars": 38100,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 90;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "bitchat.xcodeproj/xcshareddata/xcschemes/bitchat (iOS).xcscheme",
    "chars": 5012,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1640\"\n   version = \"1.3\">\n   <BuildAction\n      "
  },
  {
    "path": "bitchat.xcodeproj/xcshareddata/xcschemes/bitchat (macOS).xcscheme",
    "chars": 3915,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1640\"\n   version = \"1.3\">\n   <BuildAction\n      "
  },
  {
    "path": "bitchatShareExtension/Info.plist",
    "chars": 1378,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "bitchatShareExtension/Localization/Localizable.xcstrings",
    "chars": 21643,
    "preview": "{\n  \"sourceLanguage\": \"en\",\n  \"strings\": {\n    \"share.fallback.shared_link_title\": {\n      \"extractionState\": \"manual\",\n"
  },
  {
    "path": "bitchatShareExtension/ShareViewController.swift",
    "chars": 7918,
    "preview": "//\n// ShareViewController.swift\n// bitchatShareExtension\n//\n// This is free and unencumbered software released into the "
  },
  {
    "path": "bitchatShareExtension/bitchatShareExtension.entitlements",
    "chars": 346,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "bitchatTests/BLEServiceCoreTests.swift",
    "chars": 5322,
    "preview": "//\n// BLEServiceCoreTests.swift\n// bitchatTests\n//\n// Focused BLEService tests for packet handling behavior.\n//\n\nimport "
  },
  {
    "path": "bitchatTests/BLEServiceTests.swift",
    "chars": 10590,
    "preview": "//\n// BLEServiceTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public domain"
  },
  {
    "path": "bitchatTests/BitchatPeerTests.swift",
    "chars": 3597,
    "preview": "import Foundation\nimport Testing\n@testable import bitchat\n\n@Suite(\"BitchatPeer Tests\")\nstruct BitchatPeerTests {\n    typ"
  },
  {
    "path": "bitchatTests/ChatViewModelDeliveryStatusTests.swift",
    "chars": 7540,
    "preview": "//\n// ChatViewModelDeliveryStatusTests.swift\n// bitchatTests\n//\n// Tests for ChatViewModel delivery status state machine"
  },
  {
    "path": "bitchatTests/ChatViewModelExtensionsTests.swift",
    "chars": 45215,
    "preview": "//\n// ChatViewModelExtensionsTests.swift\n// bitchatTests\n//\n// Tests for ChatViewModel extensions (PrivateChat, Nostr, T"
  },
  {
    "path": "bitchatTests/ChatViewModelRefactoringTests.swift",
    "chars": 5261,
    "preview": "//\n// ChatViewModelRefactoringTests.swift\n// bitchatTests\n//\n// Pinning tests to characterize ChatViewModel behavior bef"
  },
  {
    "path": "bitchatTests/ChatViewModelTests.swift",
    "chars": 15433,
    "preview": "//\n// ChatViewModelTests.swift\n// bitchatTests\n//\n// Tests for ChatViewModel using MockTransport for isolation.\n// This "
  },
  {
    "path": "bitchatTests/ChatViewModelTorTests.swift",
    "chars": 5551,
    "preview": "//\n// ChatViewModelTorTests.swift\n// bitchatTests\n//\n// Tests for ChatViewModel+Tor.swift Tor lifecycle notification han"
  },
  {
    "path": "bitchatTests/CommandProcessorTests.swift",
    "chars": 16363,
    "preview": "import Foundation\nimport Testing\n@testable import bitchat\n\n@Suite(.serialized)\nstruct CommandProcessorTests {\n\n    @Main"
  },
  {
    "path": "bitchatTests/EndToEnd/PrivateChatE2ETests.swift",
    "chars": 10058,
    "preview": "//\n// PrivateChatE2ETests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public do"
  },
  {
    "path": "bitchatTests/EndToEnd/PublicChatE2ETests.swift",
    "chars": 16538,
    "preview": "//\n// PublicChatE2ETests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public dom"
  },
  {
    "path": "bitchatTests/Features/ImageUtilsTests.swift",
    "chars": 2053,
    "preview": "import Testing\nimport Foundation\n#if os(iOS)\nimport UIKit\n#else\nimport AppKit\n#endif\n@testable import bitchat\n\nprivate f"
  },
  {
    "path": "bitchatTests/FontBitchatTests.swift",
    "chars": 722,
    "preview": "import SwiftUI\nimport XCTest\n@testable import bitchat\n\nfinal class FontBitchatTests: XCTestCase {\n//    func testMonospa"
  },
  {
    "path": "bitchatTests/Fragmentation/FragmentationTests.swift",
    "chars": 15647,
    "preview": "//\n// FragmentationTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public dom"
  },
  {
    "path": "bitchatTests/GCSFilterTests.swift",
    "chars": 739,
    "preview": "import Testing\nimport struct Foundation.Data\n@testable import bitchat\n\nstruct GCSFilterTests {\n    @Test func buildFilte"
  },
  {
    "path": "bitchatTests/GeohashBookmarksStoreTests.swift",
    "chars": 1209,
    "preview": "import Testing\nimport Foundation\n@testable import bitchat\n\nstruct GeohashBookmarksStoreTests {\n    private let storeKey "
  },
  {
    "path": "bitchatTests/GeohashParticipantTrackerTests.swift",
    "chars": 10072,
    "preview": "//\n// GeohashParticipantTrackerTests.swift\n// bitchatTests\n//\n// Tests for GeohashParticipantTracker.\n// This is free an"
  },
  {
    "path": "bitchatTests/GeohashPresenceTests.swift",
    "chars": 19908,
    "preview": "//\n// GeohashPresenceTests.swift\n// bitchatTests\n//\n// Tests for the Geohash Presence (Kind 20001) feature.\n// This is f"
  },
  {
    "path": "bitchatTests/GossipSyncManagerTests.swift",
    "chars": 10183,
    "preview": "import Foundation\nimport Testing\n@testable import bitchat\n\nstruct GossipSyncManagerTests {\n\n    private let myPeerID = P"
  },
  {
    "path": "bitchatTests/Info.plist",
    "chars": 614,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "bitchatTests/InputValidatorTests.swift",
    "chars": 7670,
    "preview": "//\n// InputValidatorTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public do"
  },
  {
    "path": "bitchatTests/Integration/IntegrationTests.swift",
    "chars": 20996,
    "preview": "//\n// IntegrationTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public domai"
  },
  {
    "path": "bitchatTests/Integration/TestNetworkHelper.swift",
    "chars": 4397,
    "preview": "//\n// TestNetworkHelper.swift\n// bitchatTests\n//\n// Extracted shared, mutable integration state for nodes and noise sess"
  },
  {
    "path": "bitchatTests/KeychainErrorHandlingTests.swift",
    "chars": 7373,
    "preview": "//\n// KeychainErrorHandlingTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the pu"
  },
  {
    "path": "bitchatTests/Localization/PrimaryLocalizationKeys.json",
    "chars": 22376,
    "preview": "{\n  \"app\": [\n    \"app_info.app_name\",\n    \"app_info.close\",\n    \"app_info.done\",\n    \"app_info.features.encryption.title"
  },
  {
    "path": "bitchatTests/LocationChannelsTests.swift",
    "chars": 2383,
    "preview": "import Testing\nimport Foundation\n@testable import bitchat\n\nstruct LocationChannelsTests {\n    @Test func geohashEncoderP"
  },
  {
    "path": "bitchatTests/LocationNotesManagerTests.swift",
    "chars": 6416,
    "preview": "import Testing\nimport Foundation\n@testable import bitchat\n\n@MainActor\nstruct LocationNotesManagerTests {\n    @Test\n    f"
  },
  {
    "path": "bitchatTests/MessageDeduplicationServiceTests.swift",
    "chars": 20545,
    "preview": "//\n// MessageDeduplicationServiceTests.swift\n// bitchatTests\n//\n// Tests for MessageDeduplicationService, LRUDeduplicati"
  },
  {
    "path": "bitchatTests/MessageFormattingEngineTests.swift",
    "chars": 13866,
    "preview": "//\n// MessageFormattingEngineTests.swift\n// bitchatTests\n//\n// Tests for MessageFormattingEngine regex patterns and util"
  },
  {
    "path": "bitchatTests/MimeTypeTests.swift",
    "chars": 3376,
    "preview": "//\n// MimeTypeTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public domain.\n"
  },
  {
    "path": "bitchatTests/Mocks/MockBLEBus.swift",
    "chars": 1622,
    "preview": "//\n// MockBLEBus.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public domain.\n// "
  },
  {
    "path": "bitchatTests/Mocks/MockBLEService.swift",
    "chars": 11519,
    "preview": "//\n// MockBLEService.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public domain."
  },
  {
    "path": "bitchatTests/Mocks/MockIdentityManager.swift",
    "chars": 3499,
    "preview": "//\n// MockIdentityManager.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain."
  },
  {
    "path": "bitchatTests/Mocks/MockKeychain.swift",
    "chars": 5548,
    "preview": "//\n// MockKeychain.swift\n// bitchat\n//\n// This is free and unencumbered software released into the public domain.\n// For"
  },
  {
    "path": "bitchatTests/Mocks/MockTransport.swift",
    "chars": 8526,
    "preview": "//\n// MockTransport.swift\n// bitchatTests\n//\n// Mock Transport implementation for unit testing ChatViewModel.\n// This is"
  },
  {
    "path": "bitchatTests/Noise/NoiseCoverageTests.swift",
    "chars": 32473,
    "preview": "import CryptoKit\nimport Foundation\nimport Testing\n\n@testable import bitchat\n\n@Suite(\"Noise Coverage Tests\")\nstruct Noise"
  },
  {
    "path": "bitchatTests/Noise/NoiseProtocolTests.swift",
    "chars": 40061,
    "preview": "//\n// NoiseProtocolTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public dom"
  },
  {
    "path": "bitchatTests/Noise/NoiseRateLimiterTests.swift",
    "chars": 2240,
    "preview": "import XCTest\n@testable import bitchat\n\nfinal class NoiseRateLimiterTests: XCTestCase {\n    func test_allowHandshake_blo"
  },
  {
    "path": "bitchatTests/Noise/NoiseTestVectors.json",
    "chars": 4432,
    "preview": "[\n    {\n        \"protocol_name\": \"Noise_XX_25519_ChaChaPoly_SHA256\",\n        \"init_prologue\": \"4a6f686e2047616c74\",\n    "
  },
  {
    "path": "bitchatTests/Nostr/GeoRelayDirectoryTests.swift",
    "chars": 14128,
    "preview": "import Foundation\nimport Tor\nimport XCTest\n@testable import bitchat\n\n@MainActor\nfinal class GeoRelayDirectoryTests: XCTe"
  },
  {
    "path": "bitchatTests/NostrProtocolTests.swift",
    "chars": 9890,
    "preview": "//\n// NostrProtocolTests.swift\n// bitchatTests\n//\n// Tests for NIP-17 gift-wrapped private messages\n//\n\nimport Testing\ni"
  },
  {
    "path": "bitchatTests/NotificationBlockingTests.swift",
    "chars": 3767,
    "preview": "//\n// NotificationBlockingTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the pub"
  },
  {
    "path": "bitchatTests/NotificationStreamAssemblerTests.swift",
    "chars": 6345,
    "preview": "import Testing\nimport Foundation\n@testable import bitchat\n\nstruct NotificationStreamAssemblerTests {\n    private func ma"
  },
  {
    "path": "bitchatTests/PreviewKeychainManagerTests.swift",
    "chars": 2300,
    "preview": "import Foundation\nimport Testing\n@testable import bitchat\n\n@Suite(\"PreviewKeychainManager Tests\")\nstruct PreviewKeychain"
  },
  {
    "path": "bitchatTests/Protocol/BinaryProtocolPaddingTests.swift",
    "chars": 1331,
    "preview": "//\n// BinaryProtocolPaddingTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the pu"
  },
  {
    "path": "bitchatTests/Protocol/BinaryProtocolTests.swift",
    "chars": 33654,
    "preview": "//\n// BinaryProtocolTests.swift\n// bitchatTests\n//\n// This is free and unencumbered software released into the public do"
  },
  {
    "path": "bitchatTests/ProtocolContractTests.swift",
    "chars": 5433,
    "preview": "import Testing\nimport Foundation\nimport Combine\nimport CoreBluetooth\n@testable import bitchat\n\nprivate final class Defau"
  },
  {
    "path": "bitchatTests/Protocols/BinaryEncodingUtilsTests.swift",
    "chars": 3161,
    "preview": "import Foundation\nimport XCTest\n@testable import bitchat\n\nfinal class BinaryEncodingUtilsTests: XCTestCase {\n    func te"
  },
  {
    "path": "bitchatTests/Protocols/BitchatFilePacketTests.swift",
    "chars": 2595,
    "preview": "import XCTest\n@testable import bitchat\n\nfinal class BitchatFilePacketTests: XCTestCase {\n\n    func testRoundTripPreserve"
  },
  {
    "path": "bitchatTests/Protocols/LocationChannelTests.swift",
    "chars": 2258,
    "preview": "import Foundation\nimport Testing\n\n@testable import bitchat\n\nstruct LocationChannelTests {\n    @Test\n    func geohashChan"
  },
  {
    "path": "bitchatTests/Protocols/PacketsTests.swift",
    "chars": 4479,
    "preview": "import Foundation\nimport Testing\n\n@testable import bitchat\n\nstruct PacketsTests {\n    @Test\n    func announcementPacketR"
  },
  {
    "path": "bitchatTests/PublicMessagePipelineTests.swift",
    "chars": 4787,
    "preview": "//\n// PublicMessagePipelineTests.swift\n// bitchatTests\n//\n// Tests for PublicMessagePipeline ordering and deduplication."
  },
  {
    "path": "bitchatTests/PublicTimelineStoreTests.swift",
    "chars": 2868,
    "preview": "import Foundation\nimport Testing\n@testable import bitchat\n\n@Suite(\"PublicTimelineStore Tests\")\nstruct PublicTimelineStor"
  },
  {
    "path": "bitchatTests/README.md",
    "chars": 2798,
    "preview": "# Test Harness Guide\n\nThis test suite uses an in-memory networking harness to make end-to-end and integration tests dete"
  },
  {
    "path": "bitchatTests/ReadReceiptTests.swift",
    "chars": 2369,
    "preview": "import Foundation\nimport Testing\n@testable import bitchat\n\n@Suite(\"ReadReceipt Tests\")\nstruct ReadReceiptTests {\n\n    @T"
  },
  {
    "path": "bitchatTests/Services/AutocompleteServiceTests.swift",
    "chars": 1825,
    "preview": "import Foundation\nimport Testing\n@testable import bitchat\n\n@Suite(\"AutocompleteService Tests\")\nstruct AutocompleteServic"
  },
  {
    "path": "bitchatTests/Services/FavoritesPersistenceServiceTests.swift",
    "chars": 4571,
    "preview": "import XCTest\n@testable import bitchat\n\n@MainActor\nfinal class FavoritesPersistenceServiceTests: XCTestCase {\n    privat"
  },
  {
    "path": "bitchatTests/Services/GeohashPresenceServiceTests.swift",
    "chars": 8566,
    "preview": "import Combine\nimport XCTest\n@testable import bitchat\n\n@MainActor\nfinal class GeohashPresenceServiceTests: XCTestCase {\n"
  },
  {
    "path": "bitchatTests/Services/LocationStateManagerTests.swift",
    "chars": 12215,
    "preview": "import CoreLocation\nimport MapKit\nimport XCTest\n@testable import bitchat\n\n@MainActor\nfinal class LocationStateManagerTes"
  }
]

// ... and 54 more files (download for full content)

About this extraction

This page contains the full source code of the permissionlesstech/bitchat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 254 files (3.1 MB), approximately 827.1k tokens, and a symbol index with 22 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!