26 Commits
Author SHA1 Message Date
Leon Böttger dd9462d9f5 Bump build number 2026-05-23 14:46:52 +02:00
Leon Böttger 238769656a Avoid link local IPv6 addresses 2026-05-20 19:38:23 +02:00
Leon Böttger fd29254e38 Extract NearbyBonjourServiceResolver 2026-05-20 19:21:54 +02:00
Leon Böttger 6b0e14de3e More robust outgoing connection 2026-05-19 21:48:26 +02:00
Leon Böttger db70d40e43 Fix clipboard notification occasionally not appearing 2026-05-19 21:29:18 +02:00
Leon Böttger 4394b78242 Update version 2026-05-18 21:18:56 +02:00
Leon Böttger 488f1a14b2 Only poll clipboard if clipboard sync was enabled 2026-05-16 14:51:07 +02:00
Leon Böttger 72bfd88161 Add clipboard sync pairing 2026-05-16 14:46:22 +02:00
Leon Böttger 2f6b0ff5ba Add clipboard sync notification 2026-05-14 15:49:50 +02:00
Leon Böttger 5fdc5f8a1b Add Clipboard Listener 2026-05-14 14:44:17 +02:00
Leon Böttger ee091f60cd Update Screenshots 2026-05-14 10:54:45 +02:00
Leon Böttger 6c00c0f321 Package update 2026-05-14 10:54:32 +02:00
Leon Böttger 65a0ab3226 Improve iOS UI 2026-05-02 19:31:49 +02:00
Leon Böttger f5ca60a0ad Bump version 2026-05-01 10:43:40 +02:00
Leon Böttger ce16bca415 Fix zipping large file freezes app 2026-04-27 21:03:04 +02:00
Leon Böttger e6d1c7b1a3 Add device name lookup for unnamed devices 2026-04-27 20:29:35 +02:00
Leon Böttger 84d2150893 Always try to send logs on contact 2026-04-17 21:02:16 +02:00
Leon Böttger 007784c483 More robust connection 2026-04-17 19:34:11 +02:00
Leon Böttger 78cf2309f9 Package updates 2026-04-12 15:14:06 +02:00
Leon Böttger 976fc280db NearbyConnection: Fix missing disconnect handler 2026-04-10 19:34:18 +02:00
Leon Böttger 02ab04c69e Fix toast view liquid glass not updating background 2026-04-05 11:24:08 +02:00
Leon Böttger fcbe99dd85 Formatting 2026-04-05 11:05:55 +02:00
Leon Böttger feaf557ca2 Fix outgoing transfer flakiness 2026-04-03 19:19:22 +02:00
Leon Böttger 9a488a2ae2 Refactoring 2026-04-01 19:00:24 +02:00
Leon Böttger 80b81ae228 Fix ToastView progress bar animation 2026-03-31 21:30:17 +02:00
Leon Böttger 0112e512c0 Add notification sync documentation 2026-03-31 21:19:09 +02:00
50 changed files with 9844 additions and 6545 deletions

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

@@ -0,0 +1,220 @@
{
"forceMultiLine" : true,
"screenshots" : [
{
"baseText" : "Quick Transfer to Android & more.",
"color" : {
"alpha" : 1,
"blue" : 0.19851559400558472,
"green" : 0.19851559400558472,
"red" : 0.19851559400558472
},
"fileName" : "1.png",
"fontSize" : 107,
"key" : "1",
"localizations" : {
"ar-SA" : "نقل سريع إلى أندرويد والمزيد.",
"ca" : "Transferència ràpida a Android i més.",
"cs" : "Rychlý přenos na Android i jinam.",
"da" : "Hurtig filoverførsel til Android m.m.",
"de-DE" : "Schneller Transfer zu Android & mehr.",
"el" : "Γρήγορη μεταφορά σε Android και άλλα.",
"en-AU" : "Quick Transfer to Android & more.",
"en-CA" : "Quick Transfer to Android & more.",
"en-GB" : "Quick Transfer to Android & more.",
"es-ES" : "Transferencia rápida a Android y más.",
"es-MX" : "Transferencia rápida a Android y más.",
"fi" : "Nopea tiedostonsiirto Androidille ym.",
"fr-CA" : "Transfert rapide vers Android+.",
"fr-FR" : "Transfert rapide vers Android+.",
"he" : "העברת קבצים מהירה לאנדרואיד ועוד.",
"hi" : "एंड्रॉइड आदि पर तेज़ फ़ाइल ट्रांसफर।",
"hr" : "Brz prijenos na Android i više.",
"hu" : "Gyors fájlátvitel Androidra és másra.",
"id" : "Transfer cepat ke Android & lainnya.",
"it" : "Trasferisci file su Android e altro.",
"ja" : "Android などへのファイルを素早く転送。",
"ko" : "Android 등으로 빠른 파일 전송.",
"ms" : "Pindahan pantas ke Android+.",
"nl-NL" : "Snel bestanden naar Android en meer.",
"no" : "Rask overføring til Android og mer.",
"pl" : "Szybki transfer na Androida i więcej.",
"pt-BR" : "Transfer rápido para Android e mais.",
"pt-PT" : "Transfer rápido para Android e mais.",
"ro" : "Transfer rapid la Android și altele.",
"ru" : "Передача на Android и не только.",
"sk" : "Rýchly prenos na Android aj inde.",
"sv" : "Snabb överföring till Android m.m.",
"th" : "ถ่ายโอนไฟล์เร็วไปยัง Android และอื่นๆ",
"tr" : "Android'e hızlı aktarım ve fazlası.",
"uk" : "Передача на Android і не тільки.",
"vi" : "Chuyển nhanh sang Android và hơn nữa.",
"zh-Hans" : "快速文件传输到安卓及更多设备。",
"zh-Hant" : "快速檔案傳輸到 Android 及更多裝置。"
},
"yOffset" : 253
},
{
"baseText" : "Browse & manage received files.",
"color" : {
"alpha" : 1,
"blue" : 0.19851559400558472,
"green" : 0.19851559400558472,
"red" : 0.19851559400558472
},
"fileName" : "2.png",
"fontSize" : 107,
"key" : "2",
"localizations" : {
"ar-SA" : "تصفح وأدر الملفات المستلمة.",
"ca" : "Navega i gestiona els fitxers rebuts.",
"cs" : "Přijaté soubory.",
"da" : "Se og administrer modtagne filer.",
"de-DE" : "Empfangene Dateien ansehen & ordnen.",
"el" : "Δες και διαχειρίσου ληφθέντα αρχεία.",
"en-AU" : "Browse & manage received files.",
"en-CA" : "Browse & manage received files.",
"en-GB" : "Browse & manage received files.",
"es-ES" : "Archivos recibidos: ver y gestionar.",
"es-MX" : "Archivos recibidos: ver y gestionar.",
"fi" : "Selaa ja hallitse saatuja tiedostoja.",
"fr-CA" : "Parcourez et gérez vos fichiers.",
"fr-FR" : "Parcourez et gérez vos fichiers.",
"he" : "עיין ונהל קבצים שהתקבלו.",
"hi" : "प्राप्त फ़ाइलें देखें व संभालें।",
"hr" : "Primljene datoteke.",
"hu" : "Fogadott fájlok.",
"id" : "Jelajahi & kelola file yang diterima.",
"it" : "Sfoglia e gestisci i file ricevuti.",
"ja" : "受信ファイルを閲覧&管理",
"ko" : "받은 파일을 찾아보고 관리하세요.",
"ms" : "Semak & urus fail diterima.",
"nl-NL" : "Beheer ontvangen bestanden.",
"no" : "Se og administrer mottatte filer.",
"pl" : "Odebrane pliki: przegląd i obsługa.",
"pt-BR" : "Veja e gerencie arquivos recebidos.",
"pt-PT" : "Gere ficheiros recebidos.",
"ro" : "Fișiere primite: vezi și gestionează.",
"ru" : "Полученные файлы: обзор и управление.",
"sk" : "Prehliadaj a spravuj prijaté súbory.",
"sv" : "Bläddra och hantera mottagna filer.",
"th" : "เรียกดูและจัดการไฟล์ที่ได้รับ",
"tr" : "Alınan dosyalara göz atın ve yönetin.",
"uk" : "Отримані файли.",
"vi" : "Duyệt và quản lý các tệp đã nhận.",
"zh-Hans" : "浏览和管理收到的文件。",
"zh-Hant" : "瀏覽並管理已接收的檔案。"
},
"yOffset" : 253
},
{
"baseText" : "Share files right from your apps.",
"color" : {
"alpha" : 1,
"blue" : 0.19851559400558472,
"green" : 0.19851559400558472,
"red" : 0.19851559400558472
},
"fileName" : "3.png",
"fontSize" : 107,
"key" : "3",
"localizations" : {
"ar-SA" : "شارك الملفات مباشرة من تطبيقاتك.",
"ca" : "Comparteix fitxers des de les apps.",
"cs" : "Sdílejte soubory přímo z aplikací.",
"da" : "Del filer direkte fra dine apps.",
"de-DE" : "Teile Dateien direkt aus deinen Apps.",
"el" : "Μοιράσου αρχεία από τις εφαρμογές.",
"en-AU" : "Share files right from your apps.",
"en-CA" : "Share files right from your apps.",
"en-GB" : "Share files right from your apps.",
"es-ES" : "Comparte archivos desde tus apps.",
"es-MX" : "Comparte archivos desde tus apps.",
"fi" : "Jaa tiedostoja sovelluksistasi.",
"fr-CA" : "Partagez des fichiers via vos apps.",
"fr-FR" : "Partagez des fichiers via vos apps.",
"he" : "שתף קבצים ישירות מהאפליקציות שלך.",
"hi" : "अपने ऐप्स से सीधे फ़ाइलें साझा करें।",
"hr" : "Dijeli datoteke iz svojih aplikacija.",
"hu" : "Ossz meg fájlokat az appjaidból.",
"id" : "Bagikan file dari aplikasimu.",
"it" : "Condividi file dalle tue app.",
"ja" : "アプリから直接ファイルを共有できます。",
"ko" : "앱에서 바로 파일을 공유하세요.",
"ms" : "Kongsi fail terus dari aplikasi anda.",
"nl-NL" : "Deel bestanden direct vanuit je apps.",
"no" : "Del filer direkte fra appene dine.",
"pl" : "Udostępniaj pliki z aplikacji.",
"pt-BR" : "Compartilhe arquivos direto dos apps.",
"pt-PT" : "Partilha ficheiros das tuas apps.",
"ro" : "Partajează fișiere din aplicații.",
"ru" : "Делитесь файлами из приложений.",
"sk" : "Zdieľaj súbory zo svojich aplikácií.",
"sv" : "Dela filer direkt från dina appar.",
"th" : "แชร์ไฟล์ได้โดยตรงจากแอปของคุณ",
"tr" : "Uygulamalardan paylaş.",
"uk" : "Діліться файлами прямо з додатків.",
"vi" : "Chia sẻ tệp ngay từ ứng dụng của bạn.",
"zh-Hans" : "直接从你的应用中分享文件。",
"zh-Hant" : "直接從你的應用程式分享檔案。"
},
"yOffset" : 253
},
{
"baseText" : "Ultra-fast transfer over Wi-Fi.",
"color" : {
"alpha" : 1,
"blue" : 0.19851559400558472,
"green" : 0.19851559400558472,
"red" : 0.19851559400558472
},
"fileName" : "4.png",
"fontSize" : 107,
"key" : "4",
"localizations" : {
"ar-SA" : "نقل فائق السرعة عبر الواي فاي.",
"ca" : "Transferència Wi-Fi ultraràpida.",
"cs" : "Ultra-rychlý přenos přes Wi-Fi.",
"da" : "Ultrahurtig overførsel via Wi-Fi.",
"de-DE" : "Ultraschneller Transfer über WLAN.",
"el" : "Ταχύτατη μεταφορά Wi-Fi.",
"en-AU" : "Ultra-fast transfer over Wi-Fi.",
"en-CA" : "Ultra-fast transfer over Wi-Fi.",
"en-GB" : "Ultra-fast transfer over Wi-Fi.",
"es-ES" : "Transfiere rápido por Wi-Fi.",
"es-MX" : "Transfiere rápido por Wi-Fi.",
"fi" : "Huippunopea siirto Wi-Fillä.",
"fr-CA" : "Transfert Wi-Fi ultra-rapide.",
"fr-FR" : "Transfert Wi-Fi ultra-rapide.",
"he" : "העברה מהירה במיוחד דרך Wi-Fi.",
"hi" : "Wi-Fi पर अल्ट्रा-फास्ट ट्रांसफर।",
"hr" : "Ultra-brz prijenos putem Wi-Fi-ja.",
"hu" : "Ultragyors átvitel Wi-Fin.",
"id" : "Transfer ultra-cepat melalui Wi-Fi.",
"it" : "Transfer rapido via Wi-Fi.",
"ja" : "Wi-Fi経由で超高速転送。",
"ko" : "Wi-Fi를 통한 초고속 전송.",
"ms" : "Pindahan ultra-pantas melalui Wi-Fi.",
"nl-NL" : "Ultrasnelle overdracht via Wi-Fi.",
"no" : "Ultrarask overføring over Wi-Fi.",
"pl" : "Błyskawiczny transfer przez Wi-Fi.",
"pt-BR" : "Transfira muito rápido via Wi-Fi.",
"pt-PT" : "Transfere muito rápido por Wi-Fi.",
"ro" : "Transfer ultra-rapid prin Wi-Fi.",
"ru" : "Быстрая передача Wi-Fi.",
"sk" : "Ultra-rýchly prenos cez Wi-Fi.",
"sv" : "Ultrasnabb överföring via Wi-Fi.",
"th" : "ถ่ายโอนด้วยความเร็วสูงผ่าน Wi-Fi",
"tr" : "Wi-Fi ile ultra hızlı transfer.",
"uk" : "Надшвидка передача через Wi-Fi.",
"vi" : "Chuyển siêu nhanh qua Wi-Fi.",
"zh-Hans" : "超快的 Wi-Fi 传输。",
"zh-Hant" : "超快速 Wi-Fi 傳輸。"
},
"yOffset" : 253
}
],
"sourceLocale" : "en-US",
"syncSettings" : true,
"version" : 2
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.
@@ -30,11 +30,13 @@ class InboundNearbyConnection: NearbyConnection {
private var textPayloadID: Int64 = 0 private var textPayloadID: Int64 = 0
private var bytesToBeTransferred: Int64 = 0 private var bytesToBeTransferred: Int64 = 0
private var isAuthenticated = false private var isAuthenticated = false
private var receiverAuthenticationAccepted = false
private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil
private var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata? private var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata?
private var pairingMetadata: Sharing_Nearby_PairingMetadata? private var pairingMetadata: Sharing_Nearby_PairingMetadata?
private var isPairingSetupRequest = false private var isPairingSetupRequest = false
var isPlainTextTransfer = false var isPlainTextTransfer = false
var isClipboardSyncTransfer = false
var isControlTransfer = false var isControlTransfer = false
var wasUserRejected = false var wasUserRejected = false
@@ -279,11 +281,22 @@ class InboundNearbyConnection: NearbyConnection {
if let urlStr = String(data: payload, encoding: .utf8) { if let urlStr = String(data: payload, encoding: .utf8) {
if isPlainTextTransfer { if isPlainTextTransfer {
#if os(macOS) && !EXTENSION
ClipboardSyncCoordinator.shared.suppressNextClipboardSync(for: urlStr)
#endif
#if os(macOS) #if os(macOS)
// macOS clipboard // macOS clipboard
let pasteboard = NSPasteboard.general let pasteboard = NSPasteboard.general
pasteboard.clearContents() pasteboard.clearContents()
pasteboard.setString(urlStr, forType: .string) pasteboard.setString(urlStr, forType: .string)
#if !EXTENSION
if isClipboardSyncTransfer {
ClipboardSyncNotificationPresenter.shared.present(
clipboardText: urlStr,
senderDeviceName: remoteDeviceInfo?.name
)
}
#endif
#elseif os(iOS) #elseif os(iOS)
// iOS clipboard // iOS clipboard
UIPasteboard.general.string = urlStr UIPasteboard.general.string = urlStr
@@ -567,6 +580,7 @@ class InboundNearbyConnection: NearbyConnection {
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws { private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") } guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
receiverAuthenticationAccepted = frame.v1.pairedKeyResult.status == .success
currentState = .receivedPairedKeyResult currentState = .receivedPairedKeyResult
} }
@@ -664,6 +678,8 @@ class InboundNearbyConnection: NearbyConnection {
return return
} }
// Existing trust only proves this is the same device key we trusted before.
// It does not by itself grant a new feature, such as clipboard sync.
let trustedData = TrustStore.shared.findTrustedKey(for: peerCertificate.secretID) let trustedData = TrustStore.shared.findTrustedKey(for: peerCertificate.secretID)
if let trustedData { if let trustedData {
guard let currentData = try? peerCertificate.serializedData(), guard let currentData = try? peerCertificate.serializedData(),
@@ -671,13 +687,10 @@ class InboundNearbyConnection: NearbyConnection {
self.rejectTransfer(with: .reject) self.rejectTransfer(with: .reject)
return return
} }
DispatchQueue.main.async {
self.delegate?.notificationSyncSetupConfirmed(connection: self)
}
submitUserConsent(accepted: true, trustDevice: false)
return
} }
// The QR token proves the user accepted this specific pairing use case.
// Require it even for already-trusted devices before adding a new grant.
let matches = TrustStore.shared.confirmPendingPairingTrust( let matches = TrustStore.shared.confirmPendingPairingTrust(
secretIdHex: peerCertificate.secretID.hex, secretIdHex: peerCertificate.secretID.hex,
useCase: pairingUseCase, useCase: pairingUseCase,
@@ -689,9 +702,19 @@ class InboundNearbyConnection: NearbyConnection {
return return
} }
TrustStore.shared.addTrusted(certificate: peerCertificate, device: device) // Preserve the existing trusted certificate when present; otherwise store
// the newly paired device and attach the confirmed use-case grant.
if trustedData != nil {
TrustStore.shared.grantUseCase(pairingUseCase, for: peerCertificate.secretID.hex)
} else {
TrustStore.shared.addTrusted(
certificate: peerCertificate,
device: device,
grantedUseCases: [pairingUseCase]
)
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.notificationSyncSetupConfirmed(connection: self) self.delegate?.pairingSetupConfirmed(connection: self)
} }
submitUserConsent(accepted: true, trustDevice: false) submitUserConsent(accepted: true, trustDevice: false)
return return
@@ -724,8 +747,32 @@ class InboundNearbyConnection: NearbyConnection {
if let textMetadata = frame.v1.introduction.textMetadata.first { if let textMetadata = frame.v1.introduction.textMetadata.first {
let isURL = textMetadata.type == .url let isURL = textMetadata.type == .url
textPayloadID = textMetadata.payloadID textPayloadID = textMetadata.payloadID
let isRemoteCopy = frame.v1.introduction.hasUseCase
&& frame.v1.introduction.useCase == .remoteCopy
if isRemoteCopy && !isURL {
let peerFingerprint = peerCertificate?.secretID.hex
let isClipboardSyncAllowed = isAuthenticated
&& receiverAuthenticationAccepted
&& TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: peerFingerprint)
guard isClipboardSyncAllowed else {
log("[InboundNearbyConnection \(self.id)] Rejecting clipboard sync transfer because the sender is not trusted for clipboard sync.")
self.rejectTransfer(with: .reject, markAsUserRejected: false)
return
}
isClipboardSyncTransfer = true
} else {
isClipboardSyncTransfer = false
}
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil) let metadata = TransferMetadata(
files: [],
id: id,
pinCode: pinCode,
textDescription: textMetadata.textTitle,
transferType: isURL ? .url : .text,
allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil,
isClipboardSync: isClipboardSyncTransfer
)
if !isURL { if !isURL {
isPlainTextTransfer = true isPlainTextTransfer = true
@@ -774,7 +821,6 @@ class InboundNearbyConnection: NearbyConnection {
let pairingUseCase = PairingUseCase(protoValue: pairingMetadata.useCase) { let pairingUseCase = PairingUseCase(protoValue: pairingMetadata.useCase) {
let secretIdHex = peerCertificate.secretID.hex let secretIdHex = peerCertificate.secretID.hex
if accepted { if accepted {
if TrustStore.shared.findTrustedKey(for: peerCertificate.secretID) == nil {
if let token = pairingToken { if let token = pairingToken {
TrustStore.shared.registerPendingPairingTrust( TrustStore.shared.registerPendingPairingTrust(
secretIdHex: secretIdHex, secretIdHex: secretIdHex,
@@ -785,7 +831,6 @@ class InboundNearbyConnection: NearbyConnection {
log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.") log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.")
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex) TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
} }
}
} else { } else {
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex) TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
} }
@@ -873,7 +918,9 @@ class InboundNearbyConnection: NearbyConnection {
switch useCase { switch useCase {
case .notificationSync: case .notificationSync:
return .notificationSync return .notificationSync
case .clipboardSync, .none: case .clipboardSync:
return .clipboardSync
case .none:
return nil return nil
} }
} }
@@ -0,0 +1,237 @@
//
// NearbyBonjourServiceResolver.swift
// QuickDrop
//
// Created by Leon Böttger on 19.05.26.
//
import Darwin
import Foundation
import Network
struct NearbyResolvedBonjourEndpoint {
let endpoint: NWEndpoint
/// Numeric host string; IPv6 link-local hosts may include a `%scopeID` suffix for NWEndpoint routing.
let host: String
let port: UInt16
let interface: NWInterface?
let description: String
let isIPv4: Bool
let isLinkLocal: Bool
}
final class NearbyBonjourServiceResolver: NSObject, NetServiceDelegate {
private static let defaultWatchdogGracePeriod: TimeInterval = 0.5
private let service: NetService
private let resolveTimeout: TimeInterval
private let watchdogGracePeriod: TimeInterval
private let interface: NWInterface?
private var timeoutWorkItem: DispatchWorkItem?
private var completion: (([NearbyResolvedBonjourEndpoint]) -> Void)?
private var finished = false
init?(
result: NWBrowser.Result,
resolveTimeout: TimeInterval,
watchdogGracePeriod: TimeInterval = defaultWatchdogGracePeriod,
completion: @escaping ([NearbyResolvedBonjourEndpoint]) -> Void
) {
guard let components = Self.serviceEndpointComponents(from: result) else {
return nil
}
self.service = NetService(
domain: Self.normalizedServiceDomain(components.domain),
type: Self.normalizedServiceType(components.type),
name: components.name
)
self.resolveTimeout = resolveTimeout
self.watchdogGracePeriod = watchdogGracePeriod
self.interface = components.interface
self.completion = completion
super.init()
}
private static func serviceEndpointComponents(
from result: NWBrowser.Result
) -> (name: String, type: String, domain: String, interface: NWInterface?)? {
guard case let NWEndpoint.service(name: name, type: type, domain: domain, interface: interface) = result.endpoint else {
return nil
}
return (name: name, type: type, domain: domain, interface: interface)
}
func start() {
service.delegate = self
service.resolve(withTimeout: resolveTimeout)
let timeoutWorkItem = DispatchWorkItem { [weak self] in
self?.finish(endpoints: [])
}
self.timeoutWorkItem = timeoutWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + resolveTimeout + watchdogGracePeriod, execute: timeoutWorkItem)
}
func cancel() {
finish(endpoints: [], notify: false)
}
func netServiceDidResolveAddress(_ sender: NetService) {
finish(endpoints: Self.resolvedEndpoints(from: sender, interface: interface))
}
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
finish(endpoints: [])
}
private func finish(endpoints: [NearbyResolvedBonjourEndpoint], notify: Bool = true) {
guard !finished else { return }
finished = true
timeoutWorkItem?.cancel()
service.stop()
service.delegate = nil
let completion = completion
self.completion = nil
if notify {
DispatchQueue.main.async {
completion?(endpoints)
}
}
}
private static func resolvedEndpoints(
from service: NetService,
interface: NWInterface?
) -> [NearbyResolvedBonjourEndpoint] {
let endpoints = (service.addresses ?? [])
.compactMap { resolvedEndpoint(from: $0, interface: interface) }
var seenDescriptions = Set<String>()
let uniqueEndpoints = endpoints.filter { endpoint in
seenDescriptions.insert(endpoint.description).inserted
}
return uniqueEndpoints.sorted { lhs, rhs in
if lhs.isIPv4 != rhs.isIPv4 {
return lhs.isIPv4
}
if lhs.isLinkLocal != rhs.isLinkLocal {
return !lhs.isLinkLocal
}
return lhs.description < rhs.description
}
}
private static func resolvedEndpoint(
from addressData: Data,
interface: NWInterface?
) -> NearbyResolvedBonjourEndpoint? {
addressData.withUnsafeBytes { rawBuffer in
guard let baseAddress = rawBuffer.baseAddress else { return nil }
let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self)
let addressFamily = Int32(sockaddrPointer.pointee.sa_family)
guard addressFamily == AF_INET || addressFamily == AF_INET6 else {
return nil
}
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
var portBuffer = [CChar](repeating: 0, count: Int(NI_MAXSERV))
let result = getnameinfo(
sockaddrPointer,
socklen_t(addressData.count),
&hostBuffer,
socklen_t(hostBuffer.count),
&portBuffer,
socklen_t(portBuffer.count),
NI_NUMERICHOST | NI_NUMERICSERV
)
guard result == 0 else { return nil }
var host = String(cString: hostBuffer)
if addressFamily == AF_INET6 {
host = scopedIPv6Host(host, addressData: addressData)
}
guard let portRawValue = UInt16(String(cString: portBuffer)),
let port = NWEndpoint.Port(rawValue: portRawValue) else {
return nil
}
let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: port)
let description: String
if addressFamily == AF_INET6 {
description = "[\(host)]:\(portRawValue)"
} else {
description = "\(host):\(portRawValue)"
}
let isIPv6LinkLocal = addressFamily == AF_INET6 && host.lowercased().hasPrefix("fe80:")
return NearbyResolvedBonjourEndpoint(
endpoint: endpoint,
host: host,
port: portRawValue,
interface: isIPv6LinkLocal ? interface : nil,
description: description,
isIPv4: addressFamily == AF_INET,
isLinkLocal: isIPv6LinkLocal
)
}
}
private static func scopedIPv6Host(
_ host: String,
addressData: Data
) -> String {
guard !host.contains("%"),
host.lowercased().hasPrefix("fe80:") else {
return host
}
guard let scopeID = ipv6ScopeID(from: addressData), scopeID != 0 else {
return host
}
return "\(host)%\(scopeID)"
}
private static func ipv6ScopeID(from addressData: Data) -> UInt32? {
addressData.withUnsafeBytes { rawBuffer in
guard let baseAddress = rawBuffer.baseAddress else { return nil }
let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self)
guard Int32(sockaddrPointer.pointee.sa_family) == AF_INET6 else { return nil }
return baseAddress.assumingMemoryBound(to: sockaddr_in6.self).pointee.sin6_scope_id
}
}
private static func normalizedServiceDomain(_ domain: String) -> String {
let trimmedDomain = domain.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedDomain.isEmpty else { return "local." }
return trimmedDomain.hasSuffix(".") ? trimmedDomain : "\(trimmedDomain)."
}
private static func normalizedServiceType(_ type: String) -> String {
let trimmedType = type.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmedType.hasSuffix(".") ? trimmedType : "\(trimmedType)."
}
}
+23 -5
View File
@@ -46,7 +46,7 @@ class NearbyConnection {
private var dataTransferInactivityTimer: DispatchSourceTimer? private var dataTransferInactivityTimer: DispatchSourceTimer?
private let ackInterval: TimeInterval = 5 private let ackInterval: TimeInterval = 5
private let ackTimeoutInterval: TimeInterval = 30 private let ackTimeoutInterval: TimeInterval = 60
private var lastKeepAliveFrameSent = Date() private var lastKeepAliveFrameSent = Date()
// UKEY2-related state // UKEY2-related state
@@ -424,14 +424,29 @@ class NearbyConnection {
let chunk = payloadTransfer.payloadChunk let chunk = payloadTransfer.payloadChunk
guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") } guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") }
guard payloadTransfer.hasPayloadChunk, chunk.hasOffset, chunk.hasFlags else {
if payloadTransfer.controlMessage.event == .payloadCanceled { if payloadTransfer.hasPayloadChunk {
guard chunk.hasOffset, chunk.hasFlags else {
log("[NearbyConnection \(self.id)] Payload transfer chunk is missing offset or flags. Frame is \(payloadTransfer.debugDescription)")
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
}
} else if payloadTransfer.hasControlMessage || payloadTransfer.packetType == .control {
switch payloadTransfer.controlMessage.event {
case .payloadCanceled:
log("[NearbyConnection \(self.id)] Cancel control frame received.") log("[NearbyConnection \(self.id)] Cancel control frame received.")
throw NearbyError.canceled(reason: .userCanceled) throw NearbyError.canceled(reason: .userCanceled)
case .payloadReceivedAck:
// Legacy peers can send ACKs as control events instead of packetType=.payloadAck.
return
case .payloadError:
throw NearbyError.protocolError("Peer reported payload error")
case .unknownEventType:
return
} }
} else if payloadTransfer.packetType == .payloadAck {
log("[NearbyConnection \(self.id)] Payload transfer chunk is missing offset or flags. Frame is \(payloadTransfer.debugDescription)") return
} else {
log("[NearbyConnection \(self.id)] Payload transfer frame missing supported payload content. Frame is \(payloadTransfer.debugDescription)")
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags") throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
} }
@@ -484,6 +499,9 @@ class NearbyConnection {
// Older peers can omit the keepAlive payload; default to ACK reply. // Older peers can omit the keepAlive payload; default to ACK reply.
sendKeepAlive(ack: true) sendKeepAlive(ack: true)
} }
} else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .disconnection = offlineFrame.v1.type {
log("[NearbyConnection \(self.id)] Received disconnection frame.")
disconnect()
} else { } else {
if offlineFrame.hasV1, offlineFrame.v1.hasType, offlineFrame.v1.type == .bandwidthUpgradeRetry { if offlineFrame.hasV1, offlineFrame.v1.hasType, offlineFrame.v1.type == .bandwidthUpgradeRetry {
@@ -33,9 +33,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private var outboundAppDelegates: [OutboundAppDelegate] = [] private var outboundAppDelegates: [OutboundAppDelegate] = []
private var inboundAppDelegates: [InboundAppDelegate] = [] private var inboundAppDelegates: [InboundAppDelegate] = []
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:] private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
private var pendingOutgoingTransferPreparations: [String: UUID] = [:]
private var outgoingServiceResolvers: [String: NearbyBonjourServiceResolver] = [:]
private let pendingNotificationSyncTrustQueue = DispatchQueue(label: "NearbyConnectionManager.pendingNotificationSyncTrust")
private var pendingNotificationSyncTrust: [String: PendingReceiverTrust] = [:]
private var startedDeviceDiscovery = false private var startedDeviceDiscovery = false
private var startedAdvertising = false { didSet { informAboutStatus() }} private var startedAdvertising = false { didSet { informAboutStatus() }}
private var browsers: [NWBrowser] = [] private var browsers: [NWBrowser] = []
private let deviceNameResolver = NearbyDeviceNameResolver()
private let serviceTypes = ["_FC9F5ED42C8A._tcp."] private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
private var qrCodePrivateKey: ECPrivateKey? private var qrCodePrivateKey: ECPrivateKey?
private var qrCodeAdvertisingToken: Data? private var qrCodeAdvertisingToken: Data?
@@ -46,9 +51,10 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName" private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName"
private var defaultPort: NWEndpoint.Port = 50362 private var defaultPort: NWEndpoint.Port = 50362
private let txtKeyEndpointInfo = "n" private let txtKeyEndpointInfo = "n"
private let txtKeyNotificationSyncCapabilities = "qd_caps" private let txtQuickDropCapabilities = "qd_caps"
private let txtKeyNotificationSyncKeyFingerprint = "qd_kid" private let txtQuickDropDeviceKeyFingerprint = "qd_kid"
private let notificationSyncCapabilityToken = "nsync1" private let notificationSyncCapabilityToken = "nsync1"
private let clipboardReceiveCapabilityToken = "cliprecv1"
private let unnamedAndroidFallbackDeviceName = "AndroidDevice".localized() private let unnamedAndroidFallbackDeviceName = "AndroidDevice".localized()
@@ -315,11 +321,15 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
var txtRecord: [String: Data] = [ var txtRecord: [String: Data] = [
txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!, txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
] ]
#if os(macOS)
txtRecord[txtKeyNotificationSyncCapabilities] = Data(notificationSyncCapabilityToken.utf8)
if let keyFingerprint = notificationSyncKeyFingerprintHex() { if let keyFingerprint = notificationSyncKeyFingerprintHex() {
txtRecord[txtKeyNotificationSyncKeyFingerprint] = Data(keyFingerprint.utf8) txtRecord[txtQuickDropDeviceKeyFingerprint] = Data(keyFingerprint.utf8)
} }
#if os(macOS)
let capabilityTokens = [
notificationSyncCapabilityToken,
clipboardReceiveCapabilityToken,
]
txtRecord[txtQuickDropCapabilities] = Data(capabilityTokens.joined(separator: ",").utf8)
#endif #endif
service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord)) service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord))
service.publish() service.publish()
@@ -370,14 +380,21 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
if !connection.wasUserRejected { if !connection.wasUserRejected {
inboundAppDelegates.forEach { delegate in inboundAppDelegates.forEach { delegate in
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo, savedFiles: savedFiles, wasPlainTextTransfer: connection.isPlainTextTransfer, error: error) delegate.connectionWasTerminated(
connectionID: connection.id,
from: connection.remoteDeviceInfo,
savedFiles: savedFiles,
wasPlainTextTransfer: connection.isPlainTextTransfer,
wasClipboardSync: connection.isClipboardSyncTransfer,
error: error
)
} }
} }
} }
func notificationSyncSetupConfirmed(connection _: InboundNearbyConnection) { func pairingSetupConfirmed(connection _: InboundNearbyConnection) {
inboundAppDelegates.forEach { delegate in inboundAppDelegates.forEach { delegate in
delegate.notificationSyncSetupConfirmed() delegate.pairingSetupConfirmed()
} }
} }
@@ -429,6 +446,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
self.addFoundDevice(service: res) self.addFoundDevice(service: res)
case let .removed(res): case let .removed(res):
self.removeFoundDevice(service: res) self.removeFoundDevice(service: res)
case let .changed(_, new, _):
self.addFoundDevice(service: new)
default: default:
log("[NearbyConnectionManager] Ignoring change \(change)") log("[NearbyConnectionManager] Ignoring change \(change)")
} }
@@ -473,7 +492,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
browsers.removeAll() browsers.removeAll()
startedDeviceDiscovery = false startedDeviceDiscovery = false
deviceNameResolver.cancelAll()
for delegate in outboundAppDelegates { for delegate in outboundAppDelegates {
@@ -486,6 +505,16 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
var isDeviceDiscoveryActive: Bool {
startedDeviceDiscovery
}
func discoveredDevices() -> [RemoteDeviceInfo] {
foundServices.values.compactMap(\.device)
}
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) { public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
outboundAppDelegates.append(delegate) outboundAppDelegates.append(delegate)
for service in foundServices.values { for service in foundServices.values {
@@ -510,10 +539,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
public func cancelTransfer(id: String) { public func cancelTransfer(id: String) {
if let transfer = incomingConnections[id] { if let transfer = incomingConnections[id] {
transfer.cancel() transfer.cancel()
} }
pendingOutgoingTransferPreparations.removeValue(forKey: id)
outgoingServiceResolvers.removeValue(forKey: id)?.cancel()
if let info = outgoingTransfers[id] { if let info = outgoingTransfers[id] {
info.connection.cancel() info.connection.cancel()
} }
@@ -552,14 +582,57 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return } guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return } guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return }
guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return } guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
var endpointInfo = EndpointInfo(data: endpointInfoData) let parsedEndpointInfo = EndpointInfo(data: endpointInfoData)
let keyFingerprint = normalizedKeyFingerprint(
txtRecord.dictionary[txtQuickDropDeviceKeyFingerprint]
)
let capabilityTokens = Set(
txtRecord.dictionary[txtQuickDropCapabilities]?
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty } ?? []
)
let supportsNotificationSync = capabilityTokens.contains(notificationSyncCapabilityToken)
let supportsClipboardReceive = capabilityTokens.contains(clipboardReceiveCapabilityToken)
var endpointInfo = parsedEndpointInfo
?? EndpointInfo(name: unnamedAndroidFallbackDeviceName, deviceType: .unknown) ?? EndpointInfo(name: unnamedAndroidFallbackDeviceName, deviceType: .unknown)
let isQuickDropPeer = keyFingerprint != nil
let cleanedName = endpointInfo.name?.trimmingCharacters(in: .whitespacesAndNewlines) let needsDeviceNameLookup = deviceNameResolver.needsLookup(for: parsedEndpointInfo)
if cleanedName?.isEmpty != false { if needsDeviceNameLookup {
endpointInfo.name = unnamedAndroidFallbackDeviceName endpointInfo.name = unnamedAndroidFallbackDeviceName
} }
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID) let deviceInfo = addFoundDevice(
foundService: &foundService,
endpointInfo: endpointInfo,
endpointID: endpointID,
isQuickDropPeer: isQuickDropPeer,
keyFingerprint: keyFingerprint,
supportsNotificationSync: supportsNotificationSync,
supportsClipboardReceive: supportsClipboardReceive
)
if needsDeviceNameLookup {
deviceNameResolver.resolveName(for: service, endpointID: endpointID) { [weak self] deviceName in
guard let self else { return }
guard self.foundServices[endpointID]?.device?.name == self.unnamedAndroidFallbackDeviceName else { return }
var resolvedEndpointInfo = endpointInfo
resolvedEndpointInfo.name = deviceName
var resolvedFoundService = FoundServiceInfo(service: service)
_ = self.addFoundDevice(
foundService: &resolvedFoundService,
endpointInfo: resolvedEndpointInfo,
endpointID: endpointID,
isQuickDropPeer: isQuickDropPeer,
keyFingerprint: keyFingerprint,
supportsNotificationSync: supportsNotificationSync,
supportsClipboardReceive: supportsClipboardReceive
)
}
}
else {
deviceNameResolver.cancelLookup(for: endpointID)
}
if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey { if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey {
@@ -576,7 +649,15 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken) let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken)
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return } guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
endpointInfo.name = name endpointInfo.name = name
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID) let deviceInfo = addFoundDevice(
foundService: &foundService,
endpointInfo: endpointInfo,
endpointID: endpointID,
isQuickDropPeer: isQuickDropPeer,
keyFingerprint: keyFingerprint,
supportsNotificationSync: supportsNotificationSync,
supportsClipboardReceive: supportsClipboardReceive
)
for delegate in outboundAppDelegates { for delegate in outboundAppDelegates {
delegate.startTransferWithQrCode(device: deviceInfo) delegate.startTransferWithQrCode(device: deviceInfo)
} }
@@ -588,9 +669,24 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
private func addFoundDevice(foundService: inout FoundServiceInfo, endpointInfo: EndpointInfo, endpointID: String) -> RemoteDeviceInfo { private func addFoundDevice(
foundService: inout FoundServiceInfo,
endpointInfo: EndpointInfo,
endpointID: String,
isQuickDropPeer: Bool = false,
keyFingerprint: String? = nil,
supportsNotificationSync: Bool = false,
supportsClipboardReceive: Bool = false
) -> RemoteDeviceInfo {
let hadPreviousDevice = foundServices[endpointID]?.device != nil let hadPreviousDevice = foundServices[endpointID]?.device != nil
let deviceInfo = RemoteDeviceInfo(info: endpointInfo, id: endpointID) let deviceInfo = RemoteDeviceInfo(
info: endpointInfo,
id: endpointID,
isQuickDropPeer: isQuickDropPeer,
keyFingerprint: keyFingerprint,
supportsNotificationSync: supportsNotificationSync,
supportsClipboardReceive: supportsClipboardReceive
)
foundService.device = deviceInfo foundService.device = deviceInfo
foundServices[endpointID] = foundService foundServices[endpointID] = foundService
@@ -608,9 +704,28 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
public func resolveDiscoveredDeviceIdForKeyFingerprint(_ keyFingerprint: String) -> String? {
guard let normalizedFingerprint = normalizedKeyFingerprint(keyFingerprint) else {
return nil
}
return foundServices.values
.first(where: { $0.device?.keyFingerprint == normalizedFingerprint })?
.device?
.id
}
private func removeFoundDevice(service: NWBrowser.Result) { private func removeFoundDevice(service: NWBrowser.Result) {
guard let endpointID = endpointID(for: service) else { return } guard let endpointID = endpointID(for: service) else { return }
guard let _ = foundServices.removeValue(forKey: endpointID) else { return } deviceNameResolver.cancelLookup(for: endpointID)
guard let currentService = foundServices[endpointID] else { return }
// Ignore delayed removals for an older Bonjour result after a changed result
// has already replaced the service for this endpoint ID.
guard serviceIdentity(currentService.service) == serviceIdentity(service) else {
return
}
foundServices.removeValue(forKey: endpointID)
for delegate in outboundAppDelegates { for delegate in outboundAppDelegates {
delegate.removeDevice(id: endpointID) delegate.removeDevice(id: endpointID)
} }
@@ -657,32 +772,132 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
public func startOutgoingTransfer(deviceID: String, delegate: OutboundAppDelegate, urls: [URL], textToSend: String?) { @discardableResult
public func startOutgoingTransfer(
deviceID: String,
delegate: OutboundAppDelegate,
urls: [URL],
textToSend: String?,
preparationFinished: (() -> Void)? = nil,
isClipboardSyncTransfer: Bool = false,
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
) -> Bool {
log("Starting outgoing transfer to \(deviceID)") log("Starting outgoing transfer to \(deviceID)")
guard let info = foundServices[deviceID] else { return } guard foundServices[deviceID] != nil else {
let error = NearbyError.protocolError("TargetDeviceNoLongerNearby".localized())
delegate.connectionFailed(error: error)
return false
}
let preparationID = UUID()
pendingOutgoingTransferPreparations[deviceID] = preparationID
DispatchQueue.global(qos: .userInitiated).async { [weak self, weak delegate] in
guard let self else { return }
do { do {
let localUrls = try saveFilesToTemp(urls: urls) let localUrls = try self.saveFilesToTemp(urls: urls)
DispatchQueue.main.async {
guard let info = self.foundServices[deviceID],
self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
return
}
guard let delegate else {
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
return
}
preparationFinished?()
self.resolveOutgoingEndpoint(for: info.service, deviceID: deviceID) { endpoint, requiredInterface in
guard let info = self.foundServices[deviceID],
self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
return
}
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
let tcp = NWProtocolTCP.Options() let tcp = NWProtocolTCP.Options()
tcp.noDelay = true tcp.noDelay = true
let nwconn = NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp)) let parameters = NWParameters(tls: .none, tcp: tcp)
parameters.requiredInterface = requiredInterface
let nwconn = NWConnection(to: endpoint, using: parameters)
let conn = OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: localUrls, textToSend: textToSend) let conn = OutboundNearbyConnection(
connection: nwconn,
id: deviceID,
urlsToSend: localUrls,
textToSend: textToSend,
isClipboardSyncTransfer: isClipboardSyncTransfer,
receiverAuthenticationPolicy: receiverAuthenticationPolicy
)
conn.remoteDeviceInfo = info.device
conn.delegate = self conn.delegate = self
conn.qrCodePrivateKey = qrCodePrivateKey conn.qrCodePrivateKey = self.qrCodePrivateKey
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate) let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
outgoingTransfers[deviceID] = transfer self.outgoingTransfers[deviceID] = transfer
conn.start() conn.start()
}
}
} catch { } catch {
log("[NearbyConnectionManager] Error storing URLs: \(error)") log("[NearbyConnectionManager] Error storing URLs: \(error)")
outboundAppDelegates.forEach { delegate in DispatchQueue.main.async {
delegate.connectionFailed(error: error) guard self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
return
}
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
delegate?.connectionFailed(error: error)
} }
} }
} }
return true
}
private func resolveOutgoingEndpoint(
for service: NWBrowser.Result,
deviceID: String,
completion: @escaping (NWEndpoint, NWInterface?) -> Void
) {
guard let resolver = NearbyBonjourServiceResolver(result: service, resolveTimeout: 2.5, completion: { [weak self] endpoints in
guard let self else { return }
self.outgoingServiceResolvers.removeValue(forKey: deviceID)
if let endpoint = endpoints.first {
completion(endpoint.endpoint, endpoint.interface)
} else {
log("[NearbyConnectionManager] Could not resolve outgoing service for \(deviceID); falling back to Bonjour endpoint \(service.endpoint)")
completion(service.endpoint, nil)
}
}) else {
log("[NearbyConnectionManager] Cannot create resolver for outgoing service; falling back to Bonjour endpoint")
completion(service.endpoint, nil)
return
}
outgoingServiceResolvers.removeValue(forKey: deviceID)?.cancel()
outgoingServiceResolvers[deviceID] = resolver
resolver.start()
}
private func normalizedKeyFingerprint(_ keyFingerprint: String?) -> String? {
let normalized = keyFingerprint?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return normalized?.isEmpty == false ? normalized : nil
}
private func serviceIdentity(_ service: NWBrowser.Result) -> String {
String(describing: service.endpoint)
}
func connectionWasEstablished(connection: OutboundNearbyConnection) { func connectionWasEstablished(connection: OutboundNearbyConnection) {
guard let transfer = outgoingTransfers[connection.id] else { return } guard let transfer = outgoingTransfers[connection.id] else { return }
@@ -726,6 +941,78 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
} }
func receiverAuthenticationPending(
connection _: OutboundNearbyConnection,
certificateData: Data,
device: RemoteDeviceInfo,
fingerprint: String
) {
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else { return }
log("[NearbyConnectionManager] Receiver auth pending: fingerprint=\(normalizedFingerprint) device=\(device.name ?? "unknown") (\(device.id ?? "no-id"))")
pendingNotificationSyncTrustQueue.sync {
pendingNotificationSyncTrust[normalizedFingerprint] = PendingReceiverTrust(
certificateData: certificateData,
device: device
)
}
}
public func commitNotificationSyncReceiverTrust(_ fingerprint: String) -> Bool {
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else {
return false
}
let pending = pendingNotificationSyncTrustQueue.sync {
pendingNotificationSyncTrust.removeValue(forKey: normalizedFingerprint)
}
if pending == nil {
let alreadyTrusted = TrustStore.shared.trustedCertificates.keys.contains(normalizedFingerprint)
if alreadyTrusted {
log("[NearbyConnectionManager] Commit trust skipped: already trusted for \(normalizedFingerprint)")
return true
}
log("[NearbyConnectionManager] Commit trust failed: no pending entry for \(normalizedFingerprint)")
return false
}
guard let pending,
let certificate = try? Sharing_Nearby_PublicCertificate(serializedBytes: pending.certificateData) else {
log("[NearbyConnectionManager] Commit trust failed: could not parse certificate for \(normalizedFingerprint)")
return false
}
let derivedFingerprint = certificate.secretID.hex.lowercased()
guard derivedFingerprint == normalizedFingerprint else {
log("[NearbyConnectionManager] Commit trust failed: fingerprint mismatch pending=\(normalizedFingerprint) derived=\(derivedFingerprint)")
return false
}
log("[NearbyConnectionManager] Commit trust succeeded for fingerprint=\(normalizedFingerprint)")
TrustStore.shared.addTrusted(certificate: certificate, device: pending.device)
return true
}
public func clearPendingNotificationSyncReceiverTrust(_ fingerprint: String? = nil) {
if fingerprint == nil {
pendingNotificationSyncTrustQueue.sync {
pendingNotificationSyncTrust.removeAll()
}
log("[NearbyConnectionManager] Cleared all pending receiver trust entries")
return
}
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else { return }
_ = pendingNotificationSyncTrustQueue.sync {
pendingNotificationSyncTrust.removeValue(forKey: normalizedFingerprint)
}
log("[NearbyConnectionManager] Cleared pending receiver trust for \(normalizedFingerprint)")
}
public func getActiveConnectionsCount() -> Int { public func getActiveConnectionsCount() -> Int {
return incomingConnections.count + outgoingTransfers.count return incomingConnections.count + outgoingTransfers.count
} }
@@ -862,4 +1149,10 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
let connection: OutboundNearbyConnection let connection: OutboundNearbyConnection
let delegate: OutboundAppDelegate let delegate: OutboundAppDelegate
} }
private struct PendingReceiverTrust {
let certificateData: Data
let device: RemoteDeviceInfo
}
} }
@@ -0,0 +1,134 @@
//
// NearbyDeviceNameResolver.swift
// QuickDrop
//
// Created by Leon Böttger on 25.04.2026.
//
import Darwin
import Foundation
import Network
final class NearbyDeviceNameResolver {
private static let hostnameLookupQueue = DispatchQueue(
label: "com.leonboettger.quickdrop.deviceNameHostnameLookup",
qos: .utility
)
private var hostnameResolvers: [UUID: NearbyBonjourServiceResolver] = [:]
private var lookupTokens: [String: UUID] = [:]
func needsLookup(for endpointInfo: EndpointInfo?) -> Bool {
let cleanedName = endpointInfo?.name?.trimmingCharacters(in: .whitespacesAndNewlines)
return cleanedName?.isEmpty != false
}
func resolveName(for service: NWBrowser.Result, endpointID: String, completion: @escaping (String) -> Void) {
let lookupToken = UUID()
lookupTokens[endpointID] = lookupToken
resolveAdvertisedHostname(for: service) { [weak self] hostname in
guard let self else { return }
guard self.lookupTokens[endpointID] == lookupToken else { return }
self.lookupTokens.removeValue(forKey: endpointID)
guard let hostname else { return }
completion(hostname)
}
}
func cancelLookup(for endpointID: String) {
lookupTokens.removeValue(forKey: endpointID)
}
func cancelAll() {
lookupTokens.removeAll()
hostnameResolvers.values.forEach { $0.cancel() }
hostnameResolvers.removeAll()
}
private func resolveAdvertisedHostname(for service: NWBrowser.Result, completion: @escaping (String?) -> Void) {
let resolverID = UUID()
guard let resolver = NearbyBonjourServiceResolver(
result: service,
resolveTimeout: 0.5,
watchdogGracePeriod: 1.0,
completion: { [weak self] endpoints in
self?.hostnameResolvers.removeValue(forKey: resolverID)
guard let ipv4Address = endpoints.first(where: \.isIPv4)?.host else {
completion(nil)
return
}
Self.hostnameLookupQueue.async {
let hostname = NearbyHostLookup.hostname(forIPv4: ipv4Address)
DispatchQueue.main.async {
completion(hostname)
}
}
}
) else {
completion(nil)
return
}
hostnameResolvers[resolverID] = resolver
resolver.start()
}
}
private enum NearbyHostLookup {
static func hostname(forIPv4 address: String) -> String? {
reverseDNSName(forIPv4: address).flatMap(prettifyDisplayName)
}
private static func reverseDNSName(forIPv4 address: String) -> String? {
var socketAddress = sockaddr_in()
socketAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
socketAddress.sin_family = sa_family_t(AF_INET)
guard inet_pton(AF_INET, address, &socketAddress.sin_addr) == 1 else {
return nil
}
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = withUnsafePointer(to: &socketAddress) { pointer in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
getnameinfo(
sockaddrPointer,
socklen_t(MemoryLayout<sockaddr_in>.size),
&hostBuffer,
socklen_t(hostBuffer.count),
nil,
0,
NI_NAMEREQD
)
}
}
guard result == 0 else { return nil }
return String(cString: hostBuffer)
}
private static func prettifyDisplayName(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}
let withoutZone = value.split(separator: "%").first.map(String.init) ?? value
let withoutDomain = withoutZone.split(separator: ".").first.map(String.init) ?? withoutZone
let prettified = withoutDomain
.replacingOccurrences(of: "-", with: " ")
.replacingOccurrences(of: "_", with: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !prettified.isEmpty else { return nil }
return prettified.capitalized
}
}
@@ -22,11 +22,14 @@ class OutboundNearbyConnection: NearbyConnection {
private var currentState: State = .initial private var currentState: State = .initial
private let urlsToSend: [URL] private let urlsToSend: [URL]
private let textToSend: String? private let textToSend: String?
private let isClipboardSyncTransfer: Bool
private let receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
private var ukeyClientFinishMsgData: Data? private var ukeyClientFinishMsgData: Data?
private var queue: [OutgoingFileTransfer] = [] private var queue: [OutgoingFileTransfer] = []
private var currentTransfer: OutgoingFileTransfer? private var currentTransfer: OutgoingFileTransfer?
private var totalBytesToSend: Int64 = 0 private var totalBytesToSend: Int64 = 0
private var textPayloadID: Int64 = 0 private var textPayloadID: Int64 = 0
private var receiverAuthenticationVerified = false
public var qrCodePrivateKey: ECPrivateKey? public var qrCodePrivateKey: ECPrivateKey?
public var delegate: OutboundNearbyConnectionDelegate? public var delegate: OutboundNearbyConnectionDelegate?
@@ -35,10 +38,19 @@ class OutboundNearbyConnection: NearbyConnection {
case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles
} }
init(connection: NWConnection, id: String, urlsToSend: [URL], textToSend: String?) { init(
connection: NWConnection,
id: String,
urlsToSend: [URL],
textToSend: String?,
isClipboardSyncTransfer: Bool = false,
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
) {
self.urlsToSend = urlsToSend self.urlsToSend = urlsToSend
self.textToSend = textToSend self.textToSend = textToSend
self.isClipboardSyncTransfer = isClipboardSyncTransfer
self.receiverAuthenticationPolicy = receiverAuthenticationPolicy
super.init(connection: connection, id: id) super.init(connection: connection, id: id)
@@ -368,7 +380,12 @@ class OutboundNearbyConnection: NearbyConnection {
pairedResult.v1 = Sharing_Nearby_V1Frame() pairedResult.v1 = Sharing_Nearby_V1Frame()
pairedResult.v1.type = .pairedKeyResult pairedResult.v1.type = .pairedKeyResult
pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame() pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame()
if receiverAuthenticationPolicy.requiresReceiverAuthentication {
try verifyReceiverAuthentication(frame: frame)
pairedResult.v1.pairedKeyResult.status = .success
} else {
pairedResult.v1.pairedKeyResult.status = .unable pairedResult.v1.pairedKeyResult.status = .unable
}
try sendTransferSetupFrame(pairedResult) try sendTransferSetupFrame(pairedResult)
currentState = .sentPairedKeyResult currentState = .sentPairedKeyResult
@@ -377,10 +394,16 @@ class OutboundNearbyConnection: NearbyConnection {
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws { private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") } guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
if receiverAuthenticationPolicy.requiresReceiverAuthentication, !receiverAuthenticationVerified {
throw NearbyError.protocolError("Receiver authentication incomplete")
}
var introduction = Sharing_Nearby_Frame() var introduction = Sharing_Nearby_Frame()
introduction.version = .v1 introduction.version = .v1
introduction.v1.type = .introduction introduction.v1.type = .introduction
if isClipboardSyncTransfer {
introduction.v1.introduction.useCase = .remoteCopy
}
if let textToSend = textToSend { if let textToSend = textToSend {
var meta = Sharing_Nearby_TextMetadata() var meta = Sharing_Nearby_TextMetadata()
@@ -473,6 +496,84 @@ class OutboundNearbyConnection: NearbyConnection {
} }
private func verifyReceiverAuthentication(frame: Sharing_Nearby_Frame) throws {
guard let expectedFingerprint = receiverAuthenticationPolicy.expectedReceiverKeyFingerprint, !expectedFingerprint.isEmpty else {
throw NearbyError.protocolError("Missing expected receiver fingerprint")
}
guard let certificate = frame.v1.certificateInfo.publicCertificate.first else {
throw NearbyError.protocolError("Missing receiver certificate")
}
let certificateData = try certificate.serializedData()
let secretID = certificate.secretID
guard !secretID.isEmpty else {
throw NearbyError.protocolError("Missing receiver key ID")
}
let peerGenericKey = try Securemessage_GenericPublicKey(serializedBytes: certificate.publicKey)
guard let derivedKeyID = peerGenericKey.id() else {
throw NearbyError.protocolError("Receiver key ID derivation failed")
}
guard secretID == derivedKeyID else {
throw NearbyError.protocolError("Receiver key ID mismatch")
}
let actualFingerprint = derivedKeyID.hex.lowercased()
guard actualFingerprint == expectedFingerprint else {
throw NearbyError.protocolError("Receiver fingerprint mismatch")
}
let pinnedCertificateData = TrustStore.shared.findTrustedKey(for: secretID)
if pinnedCertificateData == nil, !receiverAuthenticationPolicy.allowsReceiverTrustBootstrap {
throw NearbyError.protocolError("Receiver certificate is not trusted")
}
if let pinnedCertificateData, pinnedCertificateData != certificateData {
throw NearbyError.protocolError("Receiver certificate changed")
}
let pairedKeyEncryption = frame.v1.pairedKeyEncryption
guard pairedKeyEncryption.secretIDHash == secretID else {
throw NearbyError.protocolError("Receiver sent unexpected key ID")
}
guard let authKeyData = authKey?.data() else {
throw NearbyError.protocolError("Missing auth key for receiver verification")
}
let domain = Domain.instance(curve: .EC256r1)
let ecKey = peerGenericKey.ecP256PublicKey
let point = Point(
BInt(magnitude: [UInt8](ecKey.x.ensureLength(length: 32))),
BInt(magnitude: [UInt8](ecKey.y.ensureLength(length: 32)))
)
guard let peerPublicKey = try? ECPublicKey(domain: domain, w: point) else {
throw NearbyError.protocolError("Invalid receiver public key")
}
let signature = try ECSignature(asn1: .build(pairedKeyEncryption.signedData), domain: domain)
guard peerPublicKey.verify(signature: signature, msg: authKeyData) else {
throw NearbyError.protocolError("Receiver signature verification failed")
}
if pinnedCertificateData == nil, receiverAuthenticationPolicy.allowsReceiverTrustBootstrap {
guard let device = remoteDeviceInfo else {
throw NearbyError.protocolError("Missing remote device info for receiver trust")
}
delegate?.receiverAuthenticationPending(
connection: self,
certificateData: certificateData,
device: device,
fingerprint: actualFingerprint
)
}
receiverAuthenticationVerified = true
}
private func hasURL() -> Bool { private func hasURL() -> Bool {
urlsToSend.count == 1 && !urlsToSend[0].isFileURL urlsToSend.count == 1 && !urlsToSend[0].isFileURL
} }
@@ -593,3 +694,32 @@ class OutboundNearbyConnection: NearbyConnection {
var currentOffset: Int64 var currentOffset: Int64
} }
} }
private extension ReceiverAuthenticationPolicy {
var expectedReceiverKeyFingerprint: String? {
switch self {
case .none:
return nil
case .trustedReceiver(let fingerprint), .bootstrapReceiverTrust(let fingerprint):
return Self.normalizedFingerprint(fingerprint)
}
}
var allowsReceiverTrustBootstrap: Bool {
if case .bootstrapReceiverTrust = self {
return true
}
return false
}
var requiresReceiverAuthentication: Bool {
if case .none = self {
return false
}
return true
}
static func normalizedFingerprint(_ fingerprint: String) -> String {
fingerprint.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
}
+104 -8
View File
@@ -19,7 +19,7 @@ protocol InboundNearbyConnectionDelegate {
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double) func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?) func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?)
func showPlusScreen() func showPlusScreen()
func notificationSyncSetupConfirmed(connection: InboundNearbyConnection) func pairingSetupConfirmed(connection: InboundNearbyConnection)
} }
@@ -29,20 +29,33 @@ protocol OutboundNearbyConnectionDelegate {
func transferAccepted(connection: OutboundNearbyConnection) func transferAccepted(connection: OutboundNearbyConnection)
func failedWithError(connection: OutboundNearbyConnection, error: Error) func failedWithError(connection: OutboundNearbyConnection, error: Error)
func transferFinished(connection: OutboundNearbyConnection) func transferFinished(connection: OutboundNearbyConnection)
func receiverAuthenticationPending(
connection: OutboundNearbyConnection,
certificateData: Data,
device: RemoteDeviceInfo,
fingerprint: String
)
}
public enum ReceiverAuthenticationPolicy: Equatable {
case none
case trustedReceiver(fingerprint: String)
case bootstrapReceiverTrust(fingerprint: String)
} }
public protocol InboundAppDelegate: AnyObject { public protocol InboundAppDelegate: AnyObject {
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo) func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, error: Error?) func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: Error?)
func transferProgress(connectionID: String, progress: Double) func transferProgress(connectionID: String, progress: Double)
func showPlusScreen() func showPlusScreen()
func notificationSyncSetupConfirmed() func pairingSetupConfirmed()
} }
extension InboundAppDelegate { extension InboundAppDelegate {
func notificationSyncSetupConfirmed() {} func pairingSetupConfirmed() {}
} }
@@ -62,20 +75,87 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable {
public let name: String? public let name: String?
public let type: DeviceType public let type: DeviceType
public let qrCodeData: Data? public let qrCodeData: Data?
public let isQuickDropPeer: Bool
public var id: String? public var id: String?
public let keyFingerprint: String?
public let supportsNotificationSync: Bool
public let supportsClipboardReceive: Bool
init(name: String?, type: DeviceType?, id: String? = nil) { init(
name: String?,
type: DeviceType?,
id: String? = nil,
isQuickDropPeer: Bool = false,
keyFingerprint: String? = nil,
supportsNotificationSync: Bool = false,
supportsClipboardReceive: Bool = false
) {
self.name = name self.name = name
self.type = type ?? .phone self.type = type ?? .phone
self.id = id self.id = id
self.qrCodeData = nil self.qrCodeData = nil
self.isQuickDropPeer = isQuickDropPeer
self.keyFingerprint = Self.normalizedKeyFingerprint(keyFingerprint)
self.supportsNotificationSync = supportsNotificationSync
self.supportsClipboardReceive = supportsClipboardReceive
} }
init(info: EndpointInfo, id: String? = nil) { init(
info: EndpointInfo,
id: String? = nil,
isQuickDropPeer: Bool = false,
keyFingerprint: String? = nil,
supportsNotificationSync: Bool = false,
supportsClipboardReceive: Bool = false
) {
self.name = info.name self.name = info.name
self.type = info.deviceType self.type = info.deviceType
self.qrCodeData = info.qrCodeData self.qrCodeData = info.qrCodeData
self.id = id self.id = id
self.isQuickDropPeer = isQuickDropPeer
self.keyFingerprint = Self.normalizedKeyFingerprint(keyFingerprint)
self.supportsNotificationSync = supportsNotificationSync
self.supportsClipboardReceive = supportsClipboardReceive
}
private enum CodingKeys: String, CodingKey {
case name
case type
case qrCodeData
case isQuickDropPeer
case isVerifiedQuickDropPeer
case id
case keyFingerprint
case supportsNotificationSync
case supportsClipboardReceive
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name)
type = try container.decode(DeviceType.self, forKey: .type)
qrCodeData = try container.decodeIfPresent(Data.self, forKey: .qrCodeData)
isQuickDropPeer = try container.decodeIfPresent(Bool.self, forKey: .isQuickDropPeer)
?? container.decodeIfPresent(Bool.self, forKey: .isVerifiedQuickDropPeer)
?? false
id = try container.decodeIfPresent(String.self, forKey: .id)
keyFingerprint = Self.normalizedKeyFingerprint(
try container.decodeIfPresent(String.self, forKey: .keyFingerprint)
)
supportsNotificationSync = try container.decodeIfPresent(Bool.self, forKey: .supportsNotificationSync) ?? false
supportsClipboardReceive = try container.decodeIfPresent(Bool.self, forKey: .supportsClipboardReceive) ?? false
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(name, forKey: .name)
try container.encode(type, forKey: .type)
try container.encodeIfPresent(qrCodeData, forKey: .qrCodeData)
try container.encode(isQuickDropPeer, forKey: .isQuickDropPeer)
try container.encodeIfPresent(id, forKey: .id)
try container.encodeIfPresent(keyFingerprint, forKey: .keyFingerprint)
try container.encode(supportsNotificationSync, forKey: .supportsNotificationSync)
try container.encode(supportsClipboardReceive, forKey: .supportsClipboardReceive)
} }
@@ -120,6 +200,13 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable {
} }
return "iphone" return "iphone"
} }
private static func normalizedKeyFingerprint(_ keyFingerprint: String?) -> String? {
let normalized = keyFingerprint?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return normalized?.isEmpty == false ? normalized : nil
}
} }
@@ -130,14 +217,16 @@ public struct TransferMetadata {
public let textDescription: String? public let textDescription: String?
public let type: TransferType public let type: TransferType
public let allowsToBeAddedAsTrustedDevice: Bool public let allowsToBeAddedAsTrustedDevice: Bool
public let isClipboardSync: Bool
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file, allowsToBeAddedAsTrustedDevice: Bool) { init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file, allowsToBeAddedAsTrustedDevice: Bool, isClipboardSync: Bool = false) {
self.files = files self.files = files
self.id = id self.id = id
self.pinCode = pinCode self.pinCode = pinCode
self.textDescription = textDescription self.textDescription = textDescription
self.type = transferType self.type = transferType
self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice
self.isClipboardSync = isClipboardSync
} }
public enum TransferType { public enum TransferType {
@@ -146,6 +235,7 @@ public struct TransferMetadata {
case url case url
case wifiPassword case wifiPassword
case notificationSync case notificationSync
case clipboardSync
} }
private func getSummary() -> String { private func getSummary() -> String {
@@ -209,6 +299,12 @@ public struct TransferMetadata {
return textDescription return textDescription
} }
return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName) return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName)
case .clipboardSync:
if let textDescription, !textDescription.isEmpty {
return textDescription
}
return "ClipboardSyncConsentPromptFromDevice".localized(with: deviceName)
} }
} }
@@ -217,7 +313,7 @@ public struct TransferMetadata {
} }
} }
enum PairingUseCase: String { enum PairingUseCase: String, Codable, Hashable {
case notificationSync = "notification_sync" case notificationSync = "notification_sync"
case clipboardSync = "clipboard_sync" case clipboardSync = "clipboard_sync"
@@ -1,8 +1,8 @@
// //
// NotificationSyncPairingToken.swift // PairingToken.swift
// QuickDrop // QuickDrop
// //
// Created by Codex on 2026-03-15. // Created by Leon Böttger on 2026-03-15.
// //
import Foundation import Foundation
@@ -10,7 +10,7 @@ import QRCode
import SwiftUI import SwiftUI
import LUI import LUI
enum NotificationSyncPairingToken { enum PairingToken {
static func generate() -> String { static func generate() -> String {
Data.randomData(length: 16).urlSafeBase64EncodedString() Data.randomData(length: 16).urlSafeBase64EncodedString()
} }
@@ -22,14 +22,14 @@ enum NotificationSyncPairingToken {
return keyId.hex.lowercased() return keyId.hex.lowercased()
} }
static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase = .notificationSync) -> String { static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase) -> String {
"quickdrop://pair?token=\(token)&usecase=\(useCase.rawValue)&receiver=\(receiverFingerprint)" "quickdrop://pair?token=\(token)&usecase=\(useCase.rawValue)&receiver=\(receiverFingerprint)"
} }
static func makeQrImage( static func makeQrImage(
token: String, token: String,
receiverFingerprint: String, receiverFingerprint: String,
useCase: PairingUseCase = .notificationSync, useCase: PairingUseCase,
foregroundColor: CGColor = CGColor(gray: 0, alpha: 1), foregroundColor: CGColor = CGColor(gray: 0, alpha: 1),
backgroundColor: CGColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1) backgroundColor: CGColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
) -> Image? { ) -> Image? {
@@ -47,7 +47,7 @@ enum NotificationSyncPairingToken {
.image(dimension: 1000) .image(dimension: 1000)
return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up) return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up)
} catch { } catch {
log("[NotificationSyncPairingToken] QR code generation failed: \(error)") log("[PairingToken] QR code generation failed: \(error)")
return nil return nil
} }
} }
+73 -3
View File
@@ -11,6 +11,7 @@ import CryptoKit
class TrustStore: ObservableObject { class TrustStore: ObservableObject {
static let shared = TrustStore() static let shared = TrustStore()
static let trustedCertificatesDidChange = Notification.Name("TrustStore.trustedCertificatesDidChange")
@Published private(set) var trustedCertificates: [String: TrustedCertificate] = [:] @Published private(set) var trustedCertificates: [String: TrustedCertificate] = [:]
@@ -58,12 +59,18 @@ class TrustStore: ObservableObject {
do { do {
try persistTrustedCertificates() try persistTrustedCertificates()
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey) AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
notifyTrustedCertificatesDidChange()
} catch { } catch {
print("[TrustStore] Failed to save trusted certificates to keychain: \(error.localizedDescription)") print("[TrustStore] Failed to save trusted certificates to keychain: \(error.localizedDescription)")
} }
} }
private func notifyTrustedCertificatesDidChange() {
NotificationCenter.default.post(name: Self.trustedCertificatesDidChange, object: self)
}
// MARK: - Public API // MARK: - Public API
func findTrustedKey(for secretIdHash: Data) -> Data? { func findTrustedKey(for secretIdHash: Data) -> Data? {
@@ -71,19 +78,45 @@ class TrustStore: ObservableObject {
} }
func addTrusted(certificate: Sharing_Nearby_PublicCertificate, device: RemoteDeviceInfo) { func addTrusted(
certificate: Sharing_Nearby_PublicCertificate,
device: RemoteDeviceInfo,
grantedUseCases: Set<PairingUseCase> = []
) {
guard let publicKeyData = try? certificate.serializedData() else { return } guard let publicKeyData = try? certificate.serializedData() else { return }
let fingerprint = certificate.secretID.hex
let existingGrants = trustedCertificates[fingerprint]?.grantedUseCases ?? []
trustedCertificates[certificate.secretID.hex] = TrustedCertificate( trustedCertificates[fingerprint] = TrustedCertificate(
device: device, device: device,
creationDate: Date(), creationDate: Date(),
certificateData: publicKeyData certificateData: publicKeyData,
grantedUseCases: existingGrants.union(grantedUseCases)
) )
saveTrustedCertificates() saveTrustedCertificates()
} }
func grantUseCase(_ useCase: PairingUseCase, for secretIdHex: String) {
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard let existing = trustedCertificates[normalized] else { return }
trustedCertificates[normalized] = existing.withGrantedUseCase(useCase)
saveTrustedCertificates()
}
func hasUseCaseGrant(_ useCase: PairingUseCase, for secretIdHex: String?) -> Bool {
guard let normalized = secretIdHex?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!normalized.isEmpty else {
return false
}
return trustedCertificates[normalized]?.grantedUseCases.contains(useCase) == true
}
func removeTrusted(secretIdHex: String) { func removeTrusted(secretIdHex: String) {
trustedCertificates.removeValue(forKey: secretIdHex) trustedCertificates.removeValue(forKey: secretIdHex)
saveTrustedCertificates() saveTrustedCertificates()
@@ -142,6 +175,43 @@ class TrustStore: ObservableObject {
let device: RemoteDeviceInfo let device: RemoteDeviceInfo
let creationDate: Date let creationDate: Date
let certificateData: Data let certificateData: Data
let grantedUseCases: Set<PairingUseCase>
init(
device: RemoteDeviceInfo,
creationDate: Date,
certificateData: Data,
grantedUseCases: Set<PairingUseCase> = []
) {
self.device = device
self.creationDate = creationDate
self.certificateData = certificateData
self.grantedUseCases = grantedUseCases
}
private enum CodingKeys: String, CodingKey {
case device
case creationDate
case certificateData
case grantedUseCases
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
device = try container.decode(RemoteDeviceInfo.self, forKey: .device)
creationDate = try container.decode(Date.self, forKey: .creationDate)
certificateData = try container.decode(Data.self, forKey: .certificateData)
grantedUseCases = try container.decodeIfPresent(Set<PairingUseCase>.self, forKey: .grantedUseCases) ?? []
}
func withGrantedUseCase(_ useCase: PairingUseCase) -> TrustedCertificate {
TrustedCertificate(
device: device,
creationDate: creationDate,
certificateData: certificateData,
grantedUseCases: grantedUseCases.union([useCase])
)
}
} }
struct PendingPairingTrust: Equatable { struct PendingPairingTrust: Equatable {
+59 -20
View File
@@ -20,13 +20,23 @@
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; }; A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; }; A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; }; A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
A1F5B3DB2F3A1A0100A1B2C6 /* NotificationSyncPairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */; }; A1C0DE112F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
A1F5B3DC2F3A1A0100A1B2C7 /* NotificationSyncPairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */; }; A1C0DE122F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
A1F5B3DD2F3A1A0100A1B2C8 /* NotificationSyncQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */; }; A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
A1F5B3DE2F3A1A0100A1B2C9 /* NotificationSyncQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */; }; A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; }; EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; }; EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; }; EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
EB0A22E32FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
EB0A22E42FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
EB0A22E52FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; }; EB1325D32E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; }; EB1325D42E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; }; EB1325D52E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
@@ -80,6 +90,7 @@
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; }; EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; }; EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; }; EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; }; EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
@@ -148,6 +159,7 @@
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; }; EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; }; EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; }; EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
EB3B9F722EF7151900D6704E /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; }; EB3B9F722EF7151900D6704E /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; };
@@ -199,11 +211,13 @@
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; }; EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; }; EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; };
EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; }; EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; };
EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; }; EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; };
@@ -213,6 +227,7 @@
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; }; EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; }; EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; }; EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; }; EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
@@ -285,6 +300,7 @@
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; }; EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; }; EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; }; EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; }; EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; }; EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
@@ -503,9 +519,12 @@
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; }; 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; }; 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = "<group>"; }; A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = "<group>"; };
A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncPairingToken.swift; sourceTree = "<group>"; }; A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncCoordinator.swift; sourceTree = "<group>"; };
A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncQrSheetManager.swift; sourceTree = "<group>"; }; A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.swift; sourceTree = "<group>"; };
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingToken.swift; sourceTree = "<group>"; };
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingQrSheetManager.swift; sourceTree = "<group>"; };
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; }; EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyBonjourServiceResolver.swift; sourceTree = "<group>"; };
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; }; EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; }; EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; }; EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; };
@@ -518,6 +537,7 @@
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; }; EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; }; EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; }; EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceNameResolver.swift; sourceTree = "<group>"; };
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropGitHub.app; sourceTree = BUILT_PRODUCTS_DIR; }; EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropGitHub.app; sourceTree = BUILT_PRODUCTS_DIR; };
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDrop copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDrop copy-Info.plist"; sourceTree = "<absolute>"; }; EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDrop copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDrop copy-Info.plist"; sourceTree = "<absolute>"; };
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickDropShareExtensionGitHub.appex; sourceTree = BUILT_PRODUCTS_DIR; }; EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickDropShareExtensionGitHub.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -750,12 +770,12 @@
path = NearbyShare; path = NearbyShare;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A1F5B3DA2F3A1A0100A1B2C5 /* NotificationSync */ = { A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */, A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */,
); );
path = NotificationSync; path = Pairing;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EB1325EA2E7025D900FD749C /* SelfTest */ = { EB1325EA2E7025D900FD749C /* SelfTest */ = {
@@ -779,7 +799,7 @@
EB886E512E4E19390052B7D9 /* HKDF.swift */, EB886E512E4E19390052B7D9 /* HKDF.swift */,
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */, EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */, EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */, A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -788,6 +808,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */, 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */,
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */,
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */, 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */, A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */,
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */, 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
@@ -911,7 +933,7 @@
EB7654772E525ACC006B4AA3 /* MainMenu.xib */, EB7654772E525ACC006B4AA3 /* MainMenu.xib */,
EB6E51A92F2F95EE006D9812 /* UserManual */, EB6E51A92F2F95EE006D9812 /* UserManual */,
EB6E51AA2F2F9606006D9812 /* Toast */, EB6E51AA2F2F9606006D9812 /* Toast */,
A1F5B3DA2F3A1A0100A1B2C5 /* NotificationSync */, A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */,
EB6E51A82F2F95E0006D9812 /* Settings */, EB6E51A82F2F95E0006D9812 /* Settings */,
EB6E51A72F2F95D0006D9812 /* Introduction */, EB6E51A72F2F95D0006D9812 /* Introduction */,
EBC05C812E33B58300EECC18 /* IssueViews */, EBC05C812E33B58300EECC18 /* IssueViews */,
@@ -922,6 +944,8 @@
EBC05C6B2E33B57100EECC18 /* Util */ = { EBC05C6B2E33B57100EECC18 /* Util */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */,
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */,
EB1325F72E70287C00FD749C /* SendToExtension.swift */, EB1325F72E70287C00FD749C /* SendToExtension.swift */,
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */, EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
EB886E662E4F2F7C0052B7D9 /* License.swift */, EB886E662E4F2F7C0052B7D9 /* License.swift */,
@@ -1296,6 +1320,7 @@
EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */, EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */,
EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */, EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */,
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */, EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
EB0A22E42FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */, EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
EB950B742E520957002D68FA /* Attachments.swift in Sources */, EB950B742E520957002D68FA /* Attachments.swift in Sources */,
@@ -1315,6 +1340,7 @@
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */, EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */, EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */, EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */, EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */, EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
@@ -1346,14 +1372,16 @@
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */, EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */, EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
A1F5B3DD2F3A1A0100A1B2C8 /* NotificationSyncQrSheetManager.swift in Sources */, A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */,
A1F5B3DB2F3A1A0100A1B2C6 /* NotificationSyncPairingToken.swift in Sources */, A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */,
EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */, EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */, EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */, EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */, EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */, EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */, EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */, EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */, EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */,
@@ -1371,12 +1399,14 @@
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */, EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */, EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */, EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */,
EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */, EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */,
EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */, EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */, EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */, EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */,
EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */, EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
EB7A95F42EF6E1BE0021111B /* IntroductionView.swift in Sources */, EB7A95F42EF6E1BE0021111B /* IntroductionView.swift in Sources */,
A1C0DE112F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1403,14 +1433,16 @@
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */, EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */, EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */, EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
A1F5B3DE2F3A1A0100A1B2C9 /* NotificationSyncQrSheetManager.swift in Sources */, A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */,
A1F5B3DC2F3A1A0100A1B2C7 /* NotificationSyncPairingToken.swift in Sources */, A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */,
EB3B9F142EF7150400D6704E /* NearbyConnection.swift in Sources */, EB3B9F142EF7150400D6704E /* NearbyConnection.swift in Sources */,
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */, EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */,
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */, EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */, EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */, EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */,
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */, EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */, EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */,
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */, EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */,
@@ -1428,12 +1460,14 @@
EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */, EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */,
EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */, EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */,
EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */, EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */,
EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */, EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */,
EB3B9F2B2EF7150400D6704E /* ActiveNetworkInterface.swift in Sources */, EB3B9F2B2EF7150400D6704E /* ActiveNetworkInterface.swift in Sources */,
EB3B9F2C2EF7150400D6704E /* ApIsolation6Tester.swift in Sources */, EB3B9F2C2EF7150400D6704E /* ApIsolation6Tester.swift in Sources */,
EB3B9F2D2EF7150400D6704E /* IssueView.swift in Sources */, EB3B9F2D2EF7150400D6704E /* IssueView.swift in Sources */,
EB3B9F2E2EF7150400D6704E /* NetworkFilterIssueView.swift in Sources */, EB3B9F2E2EF7150400D6704E /* NetworkFilterIssueView.swift in Sources */,
EB3B9F2F2EF7150400D6704E /* IntroductionView.swift in Sources */, EB3B9F2F2EF7150400D6704E /* IntroductionView.swift in Sources */,
A1C0DE122F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1457,6 +1491,7 @@
EB3B9F592EF7151900D6704E /* securemessage.pb.swift in Sources */, EB3B9F592EF7151900D6704E /* securemessage.pb.swift in Sources */,
EB3B9F5A2EF7151900D6704E /* securegcm.pb.swift in Sources */, EB3B9F5A2EF7151900D6704E /* securegcm.pb.swift in Sources */,
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */, EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */,
EB0A22E52FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */, EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */,
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */, EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */,
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */, EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */,
@@ -1476,6 +1511,7 @@
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */, EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */,
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */, EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */,
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */,
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */, EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */,
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */, EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */,
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */, EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */,
@@ -1496,6 +1532,7 @@
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB886E672E4F2F800052B7D9 /* License.swift in Sources */, EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */, EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */, EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */,
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */, EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */,
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */, EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */,
@@ -1503,6 +1540,7 @@
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */, EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */,
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */, EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */,
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */, EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */, EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */, EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
@@ -1547,10 +1585,12 @@
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */, EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */, EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
EB0A22E32FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
EB950B582E5205B4002D68FA /* Zip.swift in Sources */, EB950B582E5205B4002D68FA /* Zip.swift in Sources */,
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */, EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */,
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */, EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */,
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */, EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */,
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */,
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */, EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */, EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */, EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
@@ -1706,7 +1746,6 @@
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = EXTENSION; SWIFT_ACTIVE_COMPILATION_CONDITIONS = EXTENSION;
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = "";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@@ -1773,8 +1812,8 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
SHARED_BUILD = 138; SHARED_BUILD = 149;
SHARED_VERSION = 3.0; SHARED_VERSION = 4.0;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1836,8 +1875,8 @@
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = macosx; SDKROOT = macosx;
SHARED_BUILD = 138; SHARED_BUILD = 149;
SHARED_VERSION = 3.0; SHARED_VERSION = 4.0;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/leif-ibsen/BigInt", "location" : "https://github.com/leif-ibsen/BigInt",
"state" : { "state" : {
"revision" : "8c6f93aa37504b7b1ba3954335b5548a19fbbd82", "revision" : "c2f49bee454760e433bafff17bfdc5b96f75d7c8",
"version" : "1.22.0" "version" : "1.23.0"
} }
}, },
{ {
@@ -52,7 +52,7 @@
"location" : "https://github.com/leonboe1/LUI.git", "location" : "https://github.com/leonboe1/LUI.git",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "3582f59e758c39a849ab691f3fe26f7d0f618a94" "revision" : "3af5f2e26d102887980957b2d88a38ea8604b8d8"
} }
}, },
{ {
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git", "location" : "https://github.com/apple/swift-protobuf.git",
"state" : { "state" : {
"revision" : "a008af1a102ff3dd6cc3764bb69bf63226d0f5f6", "revision" : "f6506eaa86ed2e01cb0ae14a75035b7fdbf0918f",
"version" : "1.36.1" "version" : "1.38.0"
} }
}, },
{ {
@@ -124,7 +124,7 @@
"location" : "https://github.com/siteline/swiftui-introspect", "location" : "https://github.com/siteline/swiftui-introspect",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "26986a57e31c813f98262ddb62d4b07d40008535" "revision" : "c3db5696c625f8571f5f26fb292568db517374cc"
} }
}, },
{ {
@@ -32,8 +32,8 @@
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
+11 -4
View File
@@ -35,6 +35,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// MARK: NSApplicationDelegate functions // MARK: NSApplicationDelegate functions
func applicationDidFinishLaunching(_: Notification) { func applicationDidFinishLaunching(_: Notification) {
UNUserNotificationCenter.current().delegate = self
// app did not lauch before // app did not lauch before
if !Settings.sharedInstance.appLaunchedBefore { if !Settings.sharedInstance.appLaunchedBefore {
runAfter(seconds: 0.1) { log("[AppDelegate] Opening Welcome Screen") } runAfter(seconds: 0.1) { log("[AppDelegate] Opening Welcome Screen") }
@@ -66,9 +68,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
startReceiving() startReceiving()
} }
UNUserNotificationCenter.current().delegate = self
// MARK: Menu Bar // MARK: Menu Bar
let menu = NSMenu() let menu = NSMenu()
@@ -131,6 +130,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
func startReceiving() { func startReceiving() {
guard receiveModel == nil else { guard receiveModel == nil else {
log("[AppDelegate] startReceiving called while receiver is already active. Ignoring duplicate call.") log("[AppDelegate] startReceiving called while receiver is already active. Ignoring duplicate call.")
ClipboardSyncCoordinator.shared.start()
return return
} }
@@ -146,6 +146,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
} }
} }
}) })
ClipboardSyncCoordinator.shared.start()
} }
@@ -166,14 +168,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
public func userNotificationCenter(_: UNUserNotificationCenter, public func userNotificationCenter(_: UNUserNotificationCenter,
willPresent _: UNNotification, willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
if ClipboardSyncNotificationPresenter.isClipboardSyncNotification(notification) {
completionHandler([.banner])
return
}
completionHandler([.sound, .banner]) completionHandler([.sound, .banner])
} }
func applicationWillTerminate(_: Notification) { func applicationWillTerminate(_: Notification) {
UNUserNotificationCenter.current().removeAllDeliveredNotifications() UNUserNotificationCenter.current().removeAllDeliveredNotifications()
ClipboardSyncCoordinator.shared.stop()
NearbyConnectionManager.shared.stopDeviceDiscovery() NearbyConnectionManager.shared.stopDeviceDiscovery()
NearbyConnectionManager.shared.becomeInvisible() NearbyConnectionManager.shared.becomeInvisible()
} }
+20 -3
View File
@@ -11,11 +11,21 @@ import SwiftUI
let configuration = LUIConfiguration( let configuration = LUIConfiguration(
appName: "QuickDrop", appName: "QuickDrop",
plusVersionInfo: GetPlusViewInformation(lifetimeID: "plusversion", monthlyID: "quickdrop.monthly", yearlyID: "quickdrop.yearly", plusViewHeader: isMac() ? "plusview_title" : nil, description: isMac() ? "plusview_description" : "plusview_description_phone", onBoughtPlusVersionAlertMessage: "plusview_success_description", plusVersionFeatures: [], settingsLabelColor: .orange, settingsFooter: "plusview_footer_phone"), plusVersionInfo: GetPlusViewInformation(
lifetimeID: "plusversion",
monthlyID: "quickdrop.monthly",
yearlyID: "quickdrop.yearly",
plusViewHeader: isMac() ? "plusview_title" : nil,
description: isMac() ? "plusview_description" : "plusview_description_phone",
onBoughtPlusVersionAlertMessage: "plusview_success_description",
plusVersionFeatures: [],
settingsLabelColor: .orange,
settingsFooter: "plusview_footer_phone"
),
hasRoundedFont: false, hasRoundedFont: false,
switchedToIAPafterBundleVersion: AppVersionBundle(appVersion: "1.2", buildVersion: 0, legacyUserDefaultsKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue), switchedToInAppPurchaseWithBundleVersion: AppVersionBundle(appVersion: "1.2", buildVersion: 0, legacyUserDefaultsKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue),
copyrightInformation: licenseText, copyrightInformation: licenseText,
@@ -37,5 +47,12 @@ let configuration = LUIConfiguration(
supportsPortraitSplitView: false, supportsPortraitSplitView: false,
supportsDarkModeShadow: true supportsDarkModeShadow: false,
privacyPolicyURL: .privacyPolicyURL
) )
extension String {
static let privacyPolicyURL = "https://leonboettger.com/quickdrop-privacy"
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,317 @@
//
// ClipboardSyncCoordinator.swift
// QuickDrop
//
#if os(macOS) && !EXTENSION
import AppKit
import Foundation
import LUI
final class ClipboardSyncCoordinator {
static let shared = ClipboardSyncCoordinator()
private let pollInterval: TimeInterval = 1
private let transferTimeout: TimeInterval = 75
private var isStarted = false
private var pollTimer: Timer?
private var trustStoreObserver: NSObjectProtocol?
private var lastObservedChangeCount: Int
private var suppressedClipboardText: String?
private var startedOwnDiscovery = false
private var activeTransfersByFingerprint: [String: ClipboardSyncTransferDelegate] = [:]
private var queuedTextByFingerprint: [String: String] = [:]
private init() {
lastObservedChangeCount = NSPasteboard.general.changeCount
}
func start() {
guard !isStarted else {
refreshPollingState()
return
}
isStarted = true
trustStoreObserver = NotificationCenter.default.addObserver(
forName: TrustStore.trustedCertificatesDidChange,
object: TrustStore.shared,
queue: .main
) { [weak self] _ in
self?.refreshPollingState()
}
refreshPollingState()
}
func stop() {
guard isStarted || pollTimer != nil else { return }
log("[ClipboardSyncCoordinator] Stopping clipboard watcher")
isStarted = false
if let trustStoreObserver {
NotificationCenter.default.removeObserver(trustStoreObserver)
self.trustStoreObserver = nil
}
stopPolling(clearTransfers: true)
}
private func refreshPollingState() {
guard isStarted else { return }
guard hasTrustedClipboardSyncDevice else {
if pollTimer != nil {
log("[ClipboardSyncCoordinator] Pausing clipboard watcher because no trusted clipboard sync device is saved")
}
stopPolling(clearTransfers: true)
return
}
startPollingIfNeeded()
}
private var hasTrustedClipboardSyncDevice: Bool {
TrustStore.shared.trustedCertificates.values.contains {
$0.grantedUseCases.contains(.clipboardSync)
}
}
private func startPollingIfNeeded() {
guard pollTimer == nil else { return }
let manager = NearbyConnectionManager.shared
if !manager.isDeviceDiscoveryActive {
startedOwnDiscovery = true
manager.startDeviceDiscovery()
}
lastObservedChangeCount = NSPasteboard.general.changeCount
log("[ClipboardSyncCoordinator] Starting clipboard watcher at changeCount=\(lastObservedChangeCount)")
pollTimer = Timer.scheduledTimer(withTimeInterval: pollInterval, repeats: true) { [weak self] _ in
self?.pollClipboard()
}
}
private func stopPolling(clearTransfers: Bool) {
pollTimer?.invalidate()
pollTimer = nil
suppressedClipboardText = nil
if clearTransfers {
activeTransfersByFingerprint.values.forEach { $0.cancelTimeout() }
activeTransfersByFingerprint.removeAll()
queuedTextByFingerprint.removeAll()
}
guard startedOwnDiscovery else { return }
startedOwnDiscovery = false
NearbyConnectionManager.shared.stopDeviceDiscovery()
}
func suppressNextClipboardSync(for text: String) {
if Thread.isMainThread {
setSuppressedClipboardText(text)
} else {
DispatchQueue.main.sync {
setSuppressedClipboardText(text)
}
}
}
private func setSuppressedClipboardText(_ text: String) {
log("[ClipboardSyncCoordinator] Suppressing next clipboard sync for incoming text with \(text.utf8.count) bytes")
suppressedClipboardText = text
}
private func pollClipboard() {
let pasteboard = NSPasteboard.general
let currentChangeCount = pasteboard.changeCount
guard currentChangeCount != lastObservedChangeCount else { return }
log("[ClipboardSyncCoordinator] Clipboard change detected: \(lastObservedChangeCount) -> \(currentChangeCount)")
lastObservedChangeCount = currentChangeCount
guard let clipboardText = pasteboard.string(forType: .string) else {
log("[ClipboardSyncCoordinator] Clipboard change ignored because no plain text content is available")
return
}
guard !clipboardText.isEmpty else {
log("[ClipboardSyncCoordinator] Clipboard change ignored because text content is empty")
return
}
if suppressedClipboardText == clipboardText {
log("[ClipboardSyncCoordinator] Ignoring clipboard change because it matches an incoming clipboard transfer")
suppressedClipboardText = nil
return
}
suppressedClipboardText = nil
sendClipboardText(clipboardText)
}
private func sendClipboardText(_ clipboardText: String) {
let targets = nearbyTrustedClipboardTargets()
guard !targets.isEmpty else {
log("[ClipboardSyncCoordinator] Clipboard change will not be sent because no trusted clipboard-capable devices are nearby")
return
}
log("[ClipboardSyncCoordinator] Sending clipboard contents to \(targets.count) trusted clipboard-capable device(s)")
for target in targets {
startTransfer(to: target, clipboardText: clipboardText)
}
}
private func startTransfer(to target: ClipboardSyncTarget, clipboardText: String) {
guard activeTransfersByFingerprint[target.fingerprint] == nil else {
queuedTextByFingerprint[target.fingerprint] = clipboardText
log("[ClipboardSyncCoordinator] Queued latest clipboard text for \(target.logName) because a transfer is already active")
return
}
let delegate = ClipboardSyncTransferDelegate(target: target) { [weak self] finishedTarget, finishedDelegate in
DispatchQueue.main.async {
self?.handleTransferCompleted(for: finishedTarget, delegate: finishedDelegate)
}
}
activeTransfersByFingerprint[target.fingerprint] = delegate
delegate.startTimeout(after: transferTimeout)
NearbyConnectionManager.shared.startOutgoingTransfer(
deviceID: target.deviceID,
delegate: delegate,
urls: [],
textToSend: clipboardText,
isClipboardSyncTransfer: true,
receiverAuthenticationPolicy: .trustedReceiver(fingerprint: target.fingerprint)
)
}
private func handleTransferCompleted(for target: ClipboardSyncTarget, delegate: ClipboardSyncTransferDelegate) {
guard activeTransfersByFingerprint[target.fingerprint] === delegate else {
log("[ClipboardSyncCoordinator] Ignoring stale clipboard sync completion for \(target.logName)")
return
}
activeTransfersByFingerprint.removeValue(forKey: target.fingerprint)
guard let queuedText = queuedTextByFingerprint.removeValue(forKey: target.fingerprint) else {
return
}
guard let refreshedTarget = nearbyTrustedClipboardTargets()
.first(where: { $0.fingerprint == target.fingerprint }) else {
log("[ClipboardSyncCoordinator] Dropping queued clipboard text for \(target.logName) because the device is no longer an eligible target")
return
}
log("[ClipboardSyncCoordinator] Sending queued latest clipboard text to \(refreshedTarget.logName)")
startTransfer(to: refreshedTarget, clipboardText: queuedText)
}
private func nearbyTrustedClipboardTargets() -> [ClipboardSyncTarget] {
let trustedFingerprints = Set(
TrustStore.shared.trustedCertificates.keys.map {
$0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
)
var seenFingerprints = Set<String>()
return NearbyConnectionManager.shared.discoveredDevices()
.compactMap { device -> ClipboardSyncTarget? in
guard let deviceID = device.id,
!deviceID.isEmpty,
let fingerprint = device.keyFingerprint,
trustedFingerprints.contains(fingerprint),
TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: fingerprint),
device.supportsClipboardReceive,
device.type != .computer,
seenFingerprints.insert(fingerprint).inserted else {
return nil
}
return ClipboardSyncTarget(
device: device,
deviceID: deviceID,
fingerprint: fingerprint
)
}
.sorted { lhs, rhs in
lhs.logName < rhs.logName
}
}
}
private struct ClipboardSyncTarget {
let device: RemoteDeviceInfo
let deviceID: String
let fingerprint: String
var logName: String {
device.name ?? deviceID
}
}
private final class ClipboardSyncTransferDelegate: OutboundAppDelegate {
private let target: ClipboardSyncTarget
private let onComplete: (ClipboardSyncTarget, ClipboardSyncTransferDelegate) -> Void
private var isCompleted = false
private var timeoutWorkItem: DispatchWorkItem?
init(target: ClipboardSyncTarget, onComplete: @escaping (ClipboardSyncTarget, ClipboardSyncTransferDelegate) -> Void) {
self.target = target
self.onComplete = onComplete
}
func startTimeout(after timeout: TimeInterval) {
let timeoutWorkItem = DispatchWorkItem { [weak self] in
guard let self, !self.isCompleted else { return }
log("[ClipboardSyncCoordinator] Clipboard sync to \(self.target.logName) timed out")
NearbyConnectionManager.shared.cancelTransfer(id: self.target.deviceID)
self.complete()
}
self.timeoutWorkItem = timeoutWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: timeoutWorkItem)
}
func cancelTimeout() {
timeoutWorkItem?.cancel()
timeoutWorkItem = nil
}
func addDevice(device _: RemoteDeviceInfo) {}
func removeDevice(id _: String) {}
func startTransferWithQrCode(device _: RemoteDeviceInfo) {}
func connectionWasEstablished(pinCode _: String) {}
func transferAccepted() {}
func transferProgress(progress _: Double) {}
func connectionFailed(error: Error) {
log("[ClipboardSyncCoordinator] Clipboard sync to \(target.logName) failed: \(error.localizedDescription)")
complete()
}
func transferFinished() {
log("[ClipboardSyncCoordinator] Clipboard sync to \(target.logName) finished")
complete()
}
private func complete() {
guard !isCompleted else { return }
isCompleted = true
cancelTimeout()
onComplete(target, self)
}
}
#endif
@@ -0,0 +1,140 @@
//
// ClipboardSyncNotificationPresenter.swift
// QuickDrop
//
#if os(macOS) && !EXTENSION
import Foundation
import UserNotifications
import LUI
final class ClipboardSyncNotificationPresenter {
static let shared = ClipboardSyncNotificationPresenter()
static let notificationIdentifier = "quickdrop-clipboard-sync"
private static let notificationIdentifierPrefix = "\(notificationIdentifier)-"
private let center = UNUserNotificationCenter.current()
private let queue = DispatchQueue(label: "ClipboardSyncNotificationPresenter")
private var pendingNotifications: [(String, String?)] = []
private var authorizationRequestInFlight = false
private var postedNotificationIdentifiers: Set<String> = []
private init() {}
static func isClipboardSyncNotification(_ notification: UNNotification) -> Bool {
notification.request.identifier == notificationIdentifier
|| notification.request.identifier.hasPrefix(notificationIdentifierPrefix)
|| notification.request.content.threadIdentifier == notificationIdentifier
}
func present(clipboardText: String, senderDeviceName: String?) {
queue.async {
self.postNotificationIfAuthorized(
clipboardText: clipboardText,
senderDeviceName: senderDeviceName
)
}
}
private func postNotificationIfAuthorized(clipboardText: String, senderDeviceName: String?) {
center.getNotificationSettings { settings in
self.queue.async {
self.handleAuthorizationStatus(
settings.authorizationStatus,
clipboardText: clipboardText,
senderDeviceName: senderDeviceName
)
}
}
}
private func handleAuthorizationStatus(
_ status: UNAuthorizationStatus,
clipboardText: String,
senderDeviceName: String?
) {
switch status {
case .authorized, .provisional, .ephemeral:
post(clipboardText: clipboardText, senderDeviceName: senderDeviceName)
case .notDetermined:
pendingNotifications.append((clipboardText, senderDeviceName))
requestAuthorizationIfNeeded()
case .denied:
log("[ClipboardSyncNotificationPresenter] Notifications are denied in system settings; cannot show clipboard sync notification.")
@unknown default:
log("[ClipboardSyncNotificationPresenter] Unknown notification authorization status; skipping clipboard sync notification.")
}
}
private func requestAuthorizationIfNeeded() {
if authorizationRequestInFlight {
return
}
authorizationRequestInFlight = true
DispatchQueue.main.async {
self.center.requestAuthorization(options: [.alert, .badge]) { granted, error in
self.queue.async {
self.authorizationRequestInFlight = false
if let error = error {
log("[ClipboardSyncNotificationPresenter] Authorization request failed: \(error.localizedDescription)")
}
guard granted else {
log("[ClipboardSyncNotificationPresenter] Authorization request not granted; dropping queued clipboard sync notifications.")
self.pendingNotifications.removeAll()
return
}
let queued = self.pendingNotifications
self.pendingNotifications.removeAll()
for (queuedText, queuedSender) in queued {
self.post(clipboardText: queuedText, senderDeviceName: queuedSender)
}
}
}
}
}
private func post(clipboardText: String, senderDeviceName: String?) {
let senderName = senderDeviceName?.trimmingCharacters(in: .whitespacesAndNewlines)
let content = UNMutableNotificationContent()
content.title = "CopiedToClipboard".localized()
content.body = preview(for: clipboardText)
content.subtitle = senderName?.isEmpty == false ? senderName! : "QuickDrop"
content.threadIdentifier = Self.notificationIdentifier
content.interruptionLevel = .active
let identifier = Self.makeNotificationIdentifier()
var identifiersToRemove = postedNotificationIdentifiers
identifiersToRemove.insert(Self.notificationIdentifier)
postedNotificationIdentifiers = [identifier]
center.removePendingNotificationRequests(withIdentifiers: Array(identifiersToRemove))
center.removeDeliveredNotifications(withIdentifiers: Array(identifiersToRemove))
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
center.add(request) { error in
if let error = error {
log("[ClipboardSyncNotificationPresenter] Could not post notification: \(error.localizedDescription)")
} else {
log("[ClipboardSyncNotificationPresenter] Posted clipboard sync notification (\(identifier)).")
}
}
}
private func preview(for clipboardText: String) -> String {
clipboardText
.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func makeNotificationIdentifier() -> String {
"\(notificationIdentifierPrefix)\(UUID().uuidString)"
}
}
#endif
@@ -1,33 +1,39 @@
// //
// NotificationSyncQrSheetManager.swift // PairingQrSheetManager.swift
// QuickDrop // QuickDrop
// //
// Created by Codex on 2026-03-15. // Created by Leon Böttger on 2026-03-15.
// //
import AppKit import AppKit
import SwiftUI import SwiftUI
final class NotificationSyncQrSheetManager: NSObject, NSWindowDelegate { final class PairingQrSheetManager: NSObject, NSWindowDelegate {
static let shared = NotificationSyncQrSheetManager() static let shared = PairingQrSheetManager()
private var alert: NSAlert? private var alert: NSAlert?
private weak var alertWindow: NSWindow? private weak var alertWindow: NSWindow?
func open(token: String, receiverFingerprint: String, deviceName: String) { func open(
token: String,
receiverFingerprint: String,
deviceName: String,
useCase: PairingUseCase
) {
guard alert == nil else { return } guard alert == nil else { return }
guard let qrImage = NotificationSyncPairingToken.makeQrImage( guard let qrImage = PairingToken.makeQrImage(
token: token, token: token,
receiverFingerprint: receiverFingerprint receiverFingerprint: receiverFingerprint,
useCase: useCase
) else { return } ) else { return }
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "NotificationSyncQrPairingTitle".localized() alert.messageText = "PairingQrSheetTitle".localized()
alert.informativeText = "NotificationSyncQrPairingDescription".localized(with: deviceName) alert.informativeText = "PairingQrSheetDescription".localized(with: deviceName)
alert.addButton(withTitle: "Cancel".localized()) alert.addButton(withTitle: "Cancel".localized())
alert.alertStyle = .informational alert.alertStyle = .informational
let accessoryView = NSHostingView(rootView: NotificationSyncQrAccessoryView(qrImage: qrImage)) let accessoryView = NSHostingView(rootView: PairingQrAccessoryView(qrImage: qrImage))
accessoryView.frame = NSRect(x: 0, y: 0, width: 180, height: 180) accessoryView.frame = NSRect(x: 0, y: 0, width: 180, height: 180)
alert.accessoryView = accessoryView alert.accessoryView = accessoryView
alert.layout() alert.layout()
@@ -71,7 +77,7 @@ final class NotificationSyncQrSheetManager: NSObject, NSWindowDelegate {
} }
} }
private struct NotificationSyncQrAccessoryView: View { private struct PairingQrAccessoryView: View {
let qrImage: Image let qrImage: Image
var body: some View { var body: some View {
+44 -4
View File
@@ -13,10 +13,12 @@ let toastViewSize = CGSize(width: 340, height: 65)
let toastCornerRadius: CGFloat = 22 let toastCornerRadius: CGFloat = 22
let toastTrailingPadding: CGFloat = 20 let toastTrailingPadding: CGFloat = 20
let toastSlideDistance: CGFloat = 420 let toastSlideDistance: CGFloat = 420
let toastSlideAnimationDuration: TimeInterval = 0.5
struct QuickDropToastView: View { struct QuickDropToastView: View {
@ObservedObject var settings = Settings.sharedInstance @ObservedObject var settings = Settings.sharedInstance
@ObservedObject var receiveModel: ReceiveModel @ObservedObject var receiveModel: ReceiveModel
let animateProgressVisibility: Bool
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -28,9 +30,11 @@ struct QuickDropToastView: View {
init( init(
receiveModel: ReceiveModel, receiveModel: ReceiveModel,
animateProgressVisibility: Bool = true,
onCancel: @escaping () -> Void = {} onCancel: @escaping () -> Void = {}
) { ) {
self.receiveModel = receiveModel self.receiveModel = receiveModel
self.animateProgressVisibility = animateProgressVisibility
self.onCancel = onCancel self.onCancel = onCancel
} }
@@ -134,7 +138,12 @@ struct QuickDropToastView: View {
CapsuleProgress(value: progress) CapsuleProgress(value: progress)
} }
} }
.animation(.easeInOut, value: receiveModel.progress == nil) .animation(
animateProgressVisibility
? .easeInOut
: .easeInOut.delay(toastSlideAnimationDuration),
value: receiveModel.progress == nil
)
Button(action: onCancel) { Button(action: onCancel) {
Image(systemName: "xmark") Image(systemName: "xmark")
@@ -286,6 +295,7 @@ struct QuickDropToastHostView: View {
let onCancel: () -> Void let onCancel: () -> Void
@State private var didRenderOnce = false @State private var didRenderOnce = false
@State private var hasFinishedSlideIn = false
private let slideAnimation = Animation.smooth private let slideAnimation = Animation.smooth
private let fadeAnimation = Animation.easeInOut private let fadeAnimation = Animation.easeInOut
@@ -298,7 +308,11 @@ struct QuickDropToastHostView: View {
let hiddenBlur: CGFloat = isSlide ? 0 : 8 let hiddenBlur: CGFloat = isSlide ? 0 : 8
let animation = isSlide ? slideAnimation : fadeAnimation let animation = isSlide ? slideAnimation : fadeAnimation
QuickDropToastView(receiveModel: receiveModel, onCancel: onCancel) QuickDropToastView(
receiveModel: receiveModel,
animateProgressVisibility: hasFinishedSlideIn,
onCancel: onCancel
)
.frame(width: toastViewSize.width, height: toastViewSize.height) .frame(width: toastViewSize.width, height: toastViewSize.height)
.offset(x: isVisible ? 0 : hiddenOffset) .offset(x: isVisible ? 0 : hiddenOffset)
.opacity(isVisible ? 1 : hiddenOpacity) .opacity(isVisible ? 1 : hiddenOpacity)
@@ -314,7 +328,18 @@ struct QuickDropToastHostView: View {
didRenderOnce = true didRenderOnce = true
} }
} }
.task(id: isVisible) {
hasFinishedSlideIn = !isVisible || !isSlide
guard isVisible, isSlide else { return }
try? await Task.sleep(nanoseconds: UInt64(toastSlideAnimationDuration * 1_000_000_000))
guard !Task.isCancelled else { return }
hasFinishedSlideIn = true
}
.onDisappear { .onDisappear {
hasFinishedSlideIn = false
didRenderOnce = false didRenderOnce = false
} }
} }
@@ -414,8 +439,7 @@ struct AdaptiveToastBackgroundLayer: View {
var body: some View { var body: some View {
Group { Group {
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
Color.clear GlassEffectBackgroundView(cornerRadius: cornerRadius)
.glassEffect(.regular, in: .rect(cornerRadius: cornerRadius, style: .continuous))
} else { } else {
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow, cornerRadius: cornerRadius) VisualEffectView(material: .hudWindow, blendingMode: .behindWindow, cornerRadius: cornerRadius)
} }
@@ -424,6 +448,22 @@ struct AdaptiveToastBackgroundLayer: View {
} }
@available(macOS 26.0, *)
struct GlassEffectBackgroundView: NSViewRepresentable {
var cornerRadius: CGFloat = 0
func makeNSView(context: Context) -> NSGlassEffectView {
let v = NSGlassEffectView()
v.cornerRadius = cornerRadius
return v
}
func updateNSView(_ v: NSGlassEffectView, context: Context) {
v.cornerRadius = cornerRadius
}
}
// NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy // NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy
struct VisualEffectView: NSViewRepresentable { struct VisualEffectView: NSViewRepresentable {
var material: NSVisualEffectView.Material = .hudWindow var material: NSVisualEffectView.Material = .hudWindow
@@ -13,6 +13,7 @@ struct TutorialView: View {
let tab: Tab let tab: Tab
let openPlus: () -> Void let openPlus: () -> Void
let openAppAdvertisementView: () -> Void
@State private var licenseWindow: NSWindow? @State private var licenseWindow: NSWindow?
@ObservedObject var iapManager = IAPManager.sharedInstance @ObservedObject var iapManager = IAPManager.sharedInstance
@@ -87,6 +88,15 @@ struct TutorialView: View {
else if tab == .send { else if tab == .send {
EnableExtensionView() EnableExtensionView()
} }
else if tab == .notificationSync {
Button {
openAppAdvertisementView()
} label: {
Text("DownloadMobileApp".localized())
}
.buttonStyle(.borderedProminent)
.padding()
}
} }
} }
} }
+14 -3
View File
@@ -44,7 +44,7 @@ struct WelcomeScreen: View {
Divider() Divider()
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") { ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
SupportMail.sendSupportMail() LogExportPresenter.showUploadLogsAlert(openSupportMailWhenNotUploading: true)
} }
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") { ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
@@ -79,7 +79,11 @@ struct WelcomeScreen: View {
SettingsView() SettingsView()
} }
else { else {
TutorialView(tab: selection, openPlus: openPlusScreen) TutorialView(
tab: selection,
openPlus: openPlusScreen,
openAppAdvertisementView: openAppAdvertisementView
)
.onAppear { .onAppear {
if selection == .troubleshooting { if selection == .troubleshooting {
checkForNetworkIssues() checkForNetworkIssues()
@@ -96,7 +100,7 @@ struct WelcomeScreen: View {
} }
func openPrivacyPolicy() { func openPrivacyPolicy() {
if let url = URL(string: "https://leonboettger.com/quickdrop-privacy") { if let url = URL(string: .privacyPolicyURL) {
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
} }
} }
@@ -177,6 +181,7 @@ struct ExternalLinkLabel: View {
enum Tab: CaseIterable { enum Tab: CaseIterable {
case receive case receive
case send case send
case notificationSync
case troubleshooting case troubleshooting
case settings case settings
@@ -186,6 +191,8 @@ enum Tab: CaseIterable {
return "ReceiveFiles".localized() return "ReceiveFiles".localized()
case .send: case .send:
return "SendFiles".localized() return "SendFiles".localized()
case .notificationSync:
return "NotificationSyncSidebar".localized()
case .troubleshooting: case .troubleshooting:
return "TroubleshootingAndFaq".localized() return "TroubleshootingAndFaq".localized()
case .settings: case .settings:
@@ -206,6 +213,8 @@ enum Tab: CaseIterable {
"UserManualDescription" "UserManualDescription"
case .send: case .send:
"SendFilesDescription" "SendFilesDescription"
case .notificationSync:
"NotificationSyncManualDescription"
default: default:
"" ""
} }
@@ -217,6 +226,8 @@ enum Tab: CaseIterable {
return "tray.and.arrow.down" return "tray.and.arrow.down"
case .send: case .send:
return "tray.and.arrow.up" return "tray.and.arrow.up"
case .notificationSync:
return "bell"
case .troubleshooting: case .troubleshooting:
return "exclamationmark.triangle" return "exclamationmark.triangle"
case .settings: case .settings:
+21 -9
View File
@@ -84,7 +84,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
#if os(macOS) #if os(macOS)
DispatchQueue.main.async { DispatchQueue.main.async {
if transfer.type == .notificationSync { if transfer.type == .notificationSync || transfer.type == .clipboardSync {
self.hideQuickDropToast() self.hideQuickDropToast()
self.toastActions = nil self.toastActions = nil
self.progress = nil self.progress = nil
@@ -93,7 +93,10 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
let resolvedSenderName = (senderName?.isEmpty == false) ? senderName! : "AndroidDevice".localized() let resolvedSenderName = (senderName?.isEmpty == false) ? senderName! : "AndroidDevice".localized()
self.activeDeviceName = resolvedSenderName self.activeDeviceName = resolvedSenderName
let compareMessage = "NotificationSyncToastQrSubtitle".localized() let pairingUseCase: PairingUseCase = transfer.type == .clipboardSync ? .clipboardSync : .notificationSync
let compareMessage = transfer.type == .clipboardSync
? "ClipboardSyncToastQrSubtitle".localized()
: "NotificationSyncToastQrSubtitle".localized()
self.consentState = ConsentToastState( self.consentState = ConsentToastState(
transferID: transferID, transferID: transferID,
pinCodeMessage: "", pinCodeMessage: "",
@@ -105,17 +108,18 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
withAnimation { withAnimation {
self.consentState = nil self.consentState = nil
} }
guard let receiverFingerprint = NotificationSyncPairingToken.receiverFingerprintHex() else { guard let receiverFingerprint = PairingToken.receiverFingerprintHex() else {
log("[ReceiveModel] Missing local receiver fingerprint; cannot start notification sync pairing.") log("[ReceiveModel] Missing local receiver fingerprint; cannot start notification sync pairing.")
secondaryButtonAction() secondaryButtonAction()
self.hideQuickDropToast(style: .fade) self.hideQuickDropToast(style: .fade)
return return
} }
let pairingToken = NotificationSyncPairingToken.generate() let pairingToken = PairingToken.generate()
NotificationSyncQrSheetManager.shared.open( PairingQrSheetManager.shared.open(
token: pairingToken, token: pairingToken,
receiverFingerprint: receiverFingerprint, receiverFingerprint: receiverFingerprint,
deviceName: resolvedSenderName deviceName: resolvedSenderName,
useCase: pairingUseCase
) )
NearbyConnectionManager.shared.submitUserConsent( NearbyConnectionManager.shared.submitUserConsent(
transferID: transferID, transferID: transferID,
@@ -185,6 +189,11 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) { func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) {
#if os(macOS)
if transfer.isClipboardSync {
return
}
#endif
let mainMessage = transfer.getDescription(deviceName: device.name ?? "AndroidDevice".localized()) let mainMessage = transfer.getDescription(deviceName: device.name ?? "AndroidDevice".localized())
@@ -232,19 +241,19 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
} }
func notificationSyncSetupConfirmed() { func pairingSetupConfirmed() {
#if os(macOS) #if os(macOS)
DispatchQueue.main.async { DispatchQueue.main.async {
if self.consentState?.notificationSyncStage != nil { if self.consentState?.notificationSyncStage != nil {
self.hideQuickDropToast(style: .fade) self.hideQuickDropToast(style: .fade)
} }
NotificationSyncQrSheetManager.shared.close() PairingQrSheetManager.shared.close()
} }
#endif #endif
} }
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, error: (any Error)?) { func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: (any Error)?) {
processes.removeValue(forKey: connectionID) processes.removeValue(forKey: connectionID)
@@ -388,6 +397,9 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
if wasPendingConsent { if wasPendingConsent {
return return
} }
if wasClipboardSync && error == nil {
return
}
if let error = error { if let error = error {
if let name = device?.name { if let name = device?.name {
+7 -1
View File
@@ -129,7 +129,13 @@ class SendModel: ObservableObject, OutboundAppDelegate {
selectedDevice = device selectedDevice = device
runAfter(seconds: 0.3) { runAfter(seconds: 0.3) {
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: self.urls, textToSend: self.textToSend) NearbyConnectionManager.shared.startOutgoingTransfer(
deviceID: device.id!,
delegate: self,
urls: self.urls,
textToSend: self.textToSend,
receiverAuthenticationPolicy: .none
)
self.progressState = "Connecting".localized() self.progressState = "Connecting".localized()
} }
} }
+7 -1
View File
@@ -84,7 +84,13 @@ class SendModel: ObservableObject, OutboundAppDelegate {
progressValue = 0 progressValue = 0
progressState = "Connecting".localized() progressState = "Connecting".localized()
selectedDevice = device selectedDevice = device
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: urls, textToSend: textToSend) NearbyConnectionManager.shared.startOutgoingTransfer(
deviceID: device.id!,
delegate: self,
urls: urls,
textToSend: textToSend,
receiverAuthenticationPolicy: .none
)
} }
} }
} }
+16 -5
View File
@@ -65,7 +65,7 @@ struct DeviceListView: View {
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
BottomBarView(header: "QuickDrop", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) { BottomBarView(header: "QuickDrop", ignoresKeyboard: true, navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
VStack { VStack {
let attachments = nearbyConnectionManager.attachments let attachments = nearbyConnectionManager.attachments
@@ -288,14 +288,25 @@ struct DeviceButtonLabel: View {
.fontWeight(.bold) .fontWeight(.bold)
} }
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 5) {
Text(name) Text(name)
.fontWeight(.medium) .foregroundColor(.mainColor)
if device.isQuickDropPeer {
Image(.menuBarIcon)
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 14, height: 14)
.foregroundColor(Color.primary) .foregroundColor(Color.primary)
.opacity(0.5)
}
}
Text(isSelected ? progress ?? "..." : "Available".localized()) Text(isSelected ? progress ?? "..." : "Available".localized())
.font(.system(size: 12)) .foregroundColor(.mainColor.opacity(0.6))
.foregroundColor(Color.primary.opacity(0.6)) .monospacedDigit()
} }
Spacer() Spacer()
+18 -7
View File
@@ -95,6 +95,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
progressDeviceIconWrap!.wantsLayer = true progressDeviceIconWrap!.wantsLayer = true
progressDeviceIconWrap!.layer!.masksToBounds = false progressDeviceIconWrap!.layer!.masksToBounds = false
progressDeviceSecondaryIcon?.image = NSImage(named: "MenuBarIcon")
progressDeviceSecondaryIcon?.image?.isTemplate = true
progressDeviceSecondaryIcon?.contentTintColor = .white
} }
@@ -112,9 +115,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
timeoutDispatchWorkItem?.cancel() timeoutDispatchWorkItem?.cancel()
if chosenDevice == nil {
NearbyConnectionManager.shared.stopDeviceDiscovery() NearbyConnectionManager.shared.stopDeviceDiscovery()
}
NearbyConnectionManager.shared.removeOutboundAppDelegate(self) NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
} }
@@ -252,14 +253,18 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
progressView?.animator().isHidden = false progressView?.animator().isHidden = false
progressDeviceName?.stringValue = getDeviceName(device: device) progressDeviceName?.stringValue = getDeviceName(device: device)
progressDeviceIcon?.image = imageForDeviceType(type: device.type) progressDeviceIcon?.image = imageForDeviceType(type: device.type)
progressDeviceSecondaryIcon?.isHidden = !device.isQuickDropPeer
progressProgressBar?.startAnimation(nil) progressProgressBar?.startAnimation(nil)
progressState?.stringValue = "Preparing".localized() progressState?.stringValue = "Preparing".localized()
chosenDevice = device chosenDevice = device
NearbyConnectionManager.shared.stopDeviceDiscovery()
runAfter(seconds: 0.3) { runAfter(seconds: 0.3) {
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: self.urls, textToSend: self.textToSend) NearbyConnectionManager.shared.startOutgoingTransfer(
deviceID: device.id!,
delegate: self,
urls: self.urls,
textToSend: self.textToSend,
preparationFinished: {
self.progressState?.stringValue = "Connecting".localized() self.progressState?.stringValue = "Connecting".localized()
let timeoutAlert = DispatchWorkItem { let timeoutAlert = DispatchWorkItem {
@@ -289,6 +294,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
self.timeoutDispatchWorkItem = timeoutAlert self.timeoutDispatchWorkItem = timeoutAlert
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: timeoutAlert) DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: timeoutAlert)
},
receiverAuthenticationPolicy: .none
)
} }
} }
@@ -386,8 +394,11 @@ extension ShareViewController: NSCollectionViewDataSource {
let device = foundDevices[indexPath[1]] let device = foundDevices[indexPath[1]]
collectionViewItem.textField?.stringValue = getDeviceName(device: device) collectionViewItem.configure(
collectionViewItem.imageView?.image = imageForDeviceType(type: device.type) deviceName: getDeviceName(device: device),
deviceImage: imageForDeviceType(type: device.type),
showsQuickDropPeerMarker: device.isQuickDropPeer && device.type == .phone
)
collectionViewItem.clickHandler = { collectionViewItem.clickHandler = {
self.selectDevice(device: device) self.selectDevice(device: device)
} }
@@ -177,17 +177,17 @@ Gw
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="rb3-78-PXe"/> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="rb3-78-PXe"/>
</imageView> </imageView>
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BEO-kU-a2Q"> <imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BEO-kU-a2Q">
<rect key="frame" x="24" y="-8" width="32" height="32"/> <rect key="frame" x="16.800000000000001" y="16.800000000000001" width="14.4" height="14.4"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="32" id="05I-Ef-84Z"/> <constraint firstAttribute="height" constant="14.4" id="05I-Ef-84Z"/>
<constraint firstAttribute="width" constant="32" id="V0g-Fz-HWH"/> <constraint firstAttribute="width" constant="14.4" id="V0g-Fz-HWH"/>
</constraints> </constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSCaution" id="k1c-Xl-fTQ"/> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="k1c-Xl-fTQ"/>
</imageView> </imageView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="trailing" secondItem="BEO-kU-a2Q" secondAttribute="trailing" constant="-8" id="59U-Zi-ESa"/> <constraint firstItem="BEO-kU-a2Q" firstAttribute="centerX" secondItem="84U-lB-SRB" secondAttribute="centerX" id="59U-Zi-ESa"/>
<constraint firstAttribute="bottom" secondItem="BEO-kU-a2Q" secondAttribute="bottom" constant="-8" id="8ty-pq-miR"/> <constraint firstItem="BEO-kU-a2Q" firstAttribute="centerY" secondItem="84U-lB-SRB" secondAttribute="centerY" id="8ty-pq-miR"/>
<constraint firstItem="84U-lB-SRB" firstAttribute="top" secondItem="jBg-Ih-Gac" secondAttribute="top" id="MB9-UI-2mb"/> <constraint firstItem="84U-lB-SRB" firstAttribute="top" secondItem="jBg-Ih-Gac" secondAttribute="top" id="MB9-UI-2mb"/>
<constraint firstItem="84U-lB-SRB" firstAttribute="leading" secondItem="jBg-Ih-Gac" secondAttribute="leading" id="b23-hJ-aWT"/> <constraint firstItem="84U-lB-SRB" firstAttribute="leading" secondItem="jBg-Ih-Gac" secondAttribute="leading" id="b23-hJ-aWT"/>
<constraint firstAttribute="trailing" secondItem="84U-lB-SRB" secondAttribute="trailing" id="eKl-3V-K49"/> <constraint firstAttribute="trailing" secondItem="84U-lB-SRB" secondAttribute="trailing" id="eKl-3V-K49"/>
+11
View File
@@ -9,6 +9,7 @@ import Cocoa
class DeviceListCell: NSCollectionViewItem { class DeviceListCell: NSCollectionViewItem {
public var clickHandler: (() -> Void)? public var clickHandler: (() -> Void)?
@IBOutlet private var peerMarkerView: NSImageView?
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@@ -17,10 +18,20 @@ class DeviceListCell: NSCollectionViewItem {
btn.setButtonType(.momentaryPushIn) btn.setButtonType(.momentaryPushIn)
btn.action = #selector(onClick) btn.action = #selector(onClick)
btn.target = self btn.target = self
peerMarkerView?.image = NSImage(named: "MenuBarIcon")
peerMarkerView?.image?.isTemplate = true
peerMarkerView?.contentTintColor = .white
} }
@IBAction func onClick(_: Any?) { @IBAction func onClick(_: Any?) {
guard let handler = clickHandler else { return } guard let handler = clickHandler else { return }
handler() handler()
} }
func configure(deviceName: String, deviceImage: NSImage, showsQuickDropPeerMarker: Bool) {
textField?.stringValue = deviceName
imageView?.image = deviceImage
peerMarkerView?.isHidden = !showsQuickDropPeerMarker
}
} }
+11
View File
@@ -36,10 +36,20 @@
</constraints> </constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="QGU-rR-rJQ"/> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="QGU-rR-rJQ"/>
</imageView> </imageView>
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="89K-DQ-f8g">
<rect key="frame" x="95.799999999999997" y="58.799999999999997" width="14.4" height="14.4"/>
<constraints>
<constraint firstAttribute="width" constant="14.4" id="Lfh-Ha-Njl"/>
<constraint firstAttribute="height" constant="14.4" id="Wm1-YW-85X"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="0fp-y5-fj9"/>
</imageView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerX" secondItem="O1J-am-CoK" secondAttribute="centerX" id="Bja-VK-UQf"/>
<constraint firstItem="O1J-am-CoK" firstAttribute="centerX" secondItem="Hz6-mo-xeY" secondAttribute="centerX" id="R76-2L-pXb"/> <constraint firstItem="O1J-am-CoK" firstAttribute="centerX" secondItem="Hz6-mo-xeY" secondAttribute="centerX" id="R76-2L-pXb"/>
<constraint firstAttribute="bottom" secondItem="QPM-sE-maP" secondAttribute="bottom" id="Ryn-nY-yZp"/> <constraint firstAttribute="bottom" secondItem="QPM-sE-maP" secondAttribute="bottom" id="Ryn-nY-yZp"/>
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerY" secondItem="O1J-am-CoK" secondAttribute="centerY" id="Yeh-mY-trP"/>
<constraint firstItem="O1J-am-CoK" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="ZFA-S9-fpT"/> <constraint firstItem="O1J-am-CoK" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="ZFA-S9-fpT"/>
<constraint firstAttribute="trailing" secondItem="QPM-sE-maP" secondAttribute="trailing" id="eOl-Va-HXq"/> <constraint firstAttribute="trailing" secondItem="QPM-sE-maP" secondAttribute="trailing" id="eOl-Va-HXq"/>
<constraint firstItem="QPM-sE-maP" firstAttribute="top" secondItem="O1J-am-CoK" secondAttribute="bottom" constant="10" id="leg-nD-mHB"/> <constraint firstItem="QPM-sE-maP" firstAttribute="top" secondItem="O1J-am-CoK" secondAttribute="bottom" constant="10" id="leg-nD-mHB"/>
@@ -53,6 +63,7 @@
<collectionViewItem id="1Lb-iJ-Nar" customClass="DeviceListCell" customModule="QuickDropShareExtension" customModuleProvider="target"> <collectionViewItem id="1Lb-iJ-Nar" customClass="DeviceListCell" customModule="QuickDropShareExtension" customModuleProvider="target">
<connections> <connections>
<outlet property="imageView" destination="O1J-am-CoK" id="zQn-mV-bRy"/> <outlet property="imageView" destination="O1J-am-CoK" id="zQn-mV-bRy"/>
<outlet property="peerMarkerView" destination="89K-DQ-f8g" id="iWY-ru-o1h"/>
<outlet property="textField" destination="QPM-sE-maP" id="s0j-Qq-7iA"/> <outlet property="textField" destination="QPM-sE-maP" id="s0j-Qq-7iA"/>
<outlet property="view" destination="Hz6-mo-xeY" id="gZn-20-o77"/> <outlet property="view" destination="Hz6-mo-xeY" id="gZn-20-o77"/>
</connections> </connections>