Add notification sync support

This commit is contained in:
Leon Böttger authored and Leon Böttger committed 2026-03-08 13:49:07 +01:00
1 parent a352e9ed1c
commit 48eb5be4a5
8 files changed
+1014 -9

No files matched your search

@@ -31,8 +31,10 @@ class InboundNearbyConnection: NearbyConnection {
private var isAuthenticated = false
private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil
var isPlainTextTransfer = false
private var isMirroredNotificationTransfer = false
var wasUserRejected = false
var shouldSuppressAppDelegates = false
var delegate: InboundNearbyConnectionDelegate?
enum State {
@@ -187,6 +189,20 @@ class InboundNearbyConnection: NearbyConnection {
override func processBytesPayload(payload: Data, id: Int64) throws -> Bool {
if id == textPayloadID {
#if os(macOS)
if isMirroredNotificationTransfer {
if let payloadString = String(data: payload, encoding: .utf8) {
log("[InboundNearbyConnection \(self.id)] Received mirrored notification payload (\(payload.count) bytes)")
MirroredNotificationPresenter.shared.present(rawPayload: payloadString, senderDeviceName: remoteDeviceInfo?.name)
} else {
log("[InboundNearbyConnection \(self.id)] Failed to decode mirrored notification payload as UTF-8")
}
try sendDisconnectionAndDisconnect()
return true
}
#endif
if let urlStr = String(data: payload, encoding: .utf8) {
if isPlainTextTransfer {
@@ -507,14 +523,43 @@ class InboundNearbyConnection: NearbyConnection {
}
else if let textMetadata = frame.v1.introduction.textMetadata.first {
let isURL = textMetadata.type == .url
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
textPayloadID = textMetadata.payloadID
#if os(macOS)
if textMetadata.type == .text && textMetadata.textTitle == MirroredNotificationProtocol.introductionTitle {
isMirroredNotificationTransfer = true
shouldSuppressAppDelegates = true
if !isAuthenticated {
guard let device = self.remoteDeviceInfo else {
self.rejectTransfer(with: .reject)
return
}
let metadata = TransferMetadata(
files: [],
id: id,
pinCode: pinCode,
transferType: .notificationSync,
allowsToBeAddedAsTrustedDevice: false
)
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(transfer: metadata, device: device, connection: self)
}
return
}
submitUserConsent(accepted: true, trustDevice: false)
return
}
#endif
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
if !isURL {
isPlainTextTransfer = true
}
checkIfCanProceed(metadata: metadata)
}
else if let wifiMetadata = frame.v1.introduction.wifiCredentialsMetadata.first {
@@ -0,0 +1,183 @@
//
// MirroredNotificationSupport.swift
// QuickDrop
//
import Foundation
import CryptoKit
import UserNotifications
import LUI
enum MirroredNotificationProtocol {
static let introductionTitle = "__quickdrop_mirror_notification_v1__"
}
final class MirroredNotificationPresenter {
static let shared = MirroredNotificationPresenter()
private let center = UNUserNotificationCenter.current()
private let queue = DispatchQueue(label: "MirroredNotificationPresenter")
private var pendingNotifications: [(MirroredNotificationEnvelope, String?)] = []
private var authorizationRequestInFlight = false
private init() {}
func present(rawPayload: String, senderDeviceName: String?) {
guard let envelope = decodeEnvelope(from: rawPayload) else {
log("[MirroredNotificationPresenter] Failed to decode mirrored notification payload")
return
}
queue.async {
self.postNotificationIfAuthorized(envelope: envelope, senderDeviceName: senderDeviceName)
}
}
private func decodeEnvelope(from rawPayload: String) -> MirroredNotificationEnvelope? {
guard let data = rawPayload.data(using: .utf8) else {
return nil
}
do {
return try JSONDecoder().decode(MirroredNotificationEnvelope.self, from: data)
} catch {
log("[MirroredNotificationPresenter] Failed to decode envelope JSON: \(error.localizedDescription)")
return nil
}
}
private func postNotificationIfAuthorized(envelope: MirroredNotificationEnvelope, senderDeviceName: String?) {
center.getNotificationSettings { settings in
self.queue.async {
self.handleAuthorizationStatus(
settings.authorizationStatus,
envelope: envelope,
senderDeviceName: senderDeviceName
)
}
}
}
private func handleAuthorizationStatus(
_ status: UNAuthorizationStatus,
envelope: MirroredNotificationEnvelope,
senderDeviceName: String?
) {
switch status {
case .authorized, .provisional, .ephemeral:
post(envelope: envelope, senderDeviceName: senderDeviceName)
case .notDetermined:
pendingNotifications.append((envelope, senderDeviceName))
requestAuthorizationIfNeeded()
case .denied:
log("[MirroredNotificationPresenter] Notifications are denied in system settings; cannot mirror.")
@unknown default:
log("[MirroredNotificationPresenter] Unknown notification authorization status; skipping mirrored notification.")
}
}
private func requestAuthorizationIfNeeded() {
if authorizationRequestInFlight {
return
}
authorizationRequestInFlight = true
DispatchQueue.main.async {
self.center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
self.queue.async {
self.authorizationRequestInFlight = false
if let error = error {
log("[MirroredNotificationPresenter] Authorization request failed: \(error.localizedDescription)")
}
guard granted else {
log("[MirroredNotificationPresenter] Authorization request not granted; dropping queued mirrored notifications.")
self.pendingNotifications.removeAll()
return
}
let queued = self.pendingNotifications
self.pendingNotifications.removeAll()
for (queuedEnvelope, queuedSender) in queued {
self.post(envelope: queuedEnvelope, senderDeviceName: queuedSender)
}
}
}
}
}
private func post(envelope: MirroredNotificationEnvelope, senderDeviceName: String?) {
let senderName = senderDeviceName?.trimmingCharacters(in: .whitespacesAndNewlines)
let hasSenderName = senderName?.isEmpty == false
let displaySender = hasSenderName ? senderName! : "NotificationSyncFallbackSenderAndroid".localized()
let identifierSender = hasSenderName ? senderName! : "android"
let cleanAppName = envelope.appName.trimmingCharacters(in: .whitespacesAndNewlines)
let cleanTitle = envelope.title.trimmingCharacters(in: .whitespacesAndNewlines)
let cleanBody = envelope.body.trimmingCharacters(in: .whitespacesAndNewlines)
let content = UNMutableNotificationContent()
content.title = cleanTitle.isEmpty ? (cleanAppName.isEmpty ? "NotificationSyncFallbackTitle".localized() : cleanAppName) : cleanTitle
content.body = cleanBody
content.subtitle = cleanAppName.isEmpty ? displaySender : "\(cleanAppName) · \(displaySender)"
content.sound = .default
content.threadIdentifier = "quickdrop-mirror-\(sanitizeIdentifierComponent(identifierSender))"
let identifier = makeIdentifier(sender: identifierSender, notificationKey: envelope.notificationKey)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
center.add(request) { error in
if let error = error {
log("[MirroredNotificationPresenter] Could not post notification: \(error.localizedDescription)")
} else {
log("[MirroredNotificationPresenter] Posted mirrored notification (\(identifier))")
}
}
}
private func makeIdentifier(sender: String, notificationKey: String) -> String {
let raw = "\(sender)|\(notificationKey)"
let digest = SHA256.hash(data: Data(raw.utf8))
let prefix = digest.prefix(12).map { String(format: "%02x", $0) }.joined()
return "quickdrop-mirror-\(prefix)"
}
private func sanitizeIdentifierComponent(_ value: String) -> String {
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
let scalarView = value.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
let sanitized = String(scalarView)
return sanitized.isEmpty ? "android" : sanitized
}
}
private struct MirroredNotificationEnvelope: Codable {
let version: Int
let notificationKey: String
let packageName: String
let appName: String
let title: String
let body: String
let postedAtEpochMs: Int64
enum CodingKeys: String, CodingKey {
case version
case notificationKey
case packageName
case appName
case title
case body
case postedAtEpochMs
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 1
notificationKey = try container.decode(String.self, forKey: .notificationKey)
packageName = try container.decode(String.self, forKey: .packageName)
appName = try container.decode(String.self, forKey: .appName)
title = try container.decode(String.self, forKey: .title)
body = try container.decode(String.self, forKey: .body)
postedAtEpochMs = try container.decode(Int64.self, forKey: .postedAtEpochMs)
}
}
@@ -45,6 +45,10 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
private var securityScopeUrl: URL?
private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName"
private var defaultPort: NWEndpoint.Port = 50362
private let txtKeyEndpointInfo = "n"
private let txtKeyNotificationSyncCapabilities = "qd_caps"
private let txtKeyNotificationSyncKeyFingerprint = "qd_kid"
private let notificationSyncCapabilityToken = "nsync1"
// MARK: Shared Instance
@@ -307,14 +311,29 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
mdnsServices = serviceTypes.map { serviceType in
let service = NetService(domain: "", type: serviceType, name: name, port: port)
service.delegate = self
service.setTXTRecord(NetService.data(fromTXTRecord: [
"n": deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
]))
var txtRecord: [String: Data] = [
txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
]
#if os(macOS)
txtRecord[txtKeyNotificationSyncCapabilities] = Data(notificationSyncCapabilityToken.utf8)
if let keyFingerprint = notificationSyncKeyFingerprintHex() {
txtRecord[txtKeyNotificationSyncKeyFingerprint] = Data(keyFingerprint.utf8)
}
#endif
service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord))
service.publish()
return service
}
}
private func notificationSyncKeyFingerprintHex() -> String? {
guard let keyID = IdentityManager.shared.getPublicKey()?.toGenericPublicKey().id() else {
return nil
}
return keyID.hex.lowercased()
}
private func stopMDNS() {
// Gracefully stop each published service
@@ -341,7 +360,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?) {
incomingConnections.removeValue(forKey: connection.id)
if connection.shouldSuppressAppDelegates {
return
}
if !connection.wasUserRejected {
inboundAppDelegates.forEach { delegate in
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo, savedFiles: savedFiles, wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
@@ -513,7 +536,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
var foundService = FoundServiceInfo(service: service)
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
guard let endpointInfoEncoded = txtRecord.dictionary["n"] else { return }
guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return }
guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
guard var endpointInfo = EndpointInfo(data: endpointInfoData) else { return }
var deviceInfo: RemoteDeviceInfo?
@@ -269,6 +269,15 @@ class OutboundNearbyConnection: NearbyConnection {
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
if frame.v1.type == .keepAlive {
log("[OutboundNearbyConnection \(self.id)] Received keep-alive while waiting for connection response")
if frame.v1.hasKeepAlive, !frame.v1.keepAlive.ack {
sendKeepAlive(ack: true)
}
return
}
guard frame.v1.type == .connectionResponse else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
+7
View File
@@ -139,6 +139,7 @@ public struct TransferMetadata {
case text
case url
case wifiPassword
case notificationSync
}
private func getSummary() -> String {
@@ -196,6 +197,12 @@ public struct TransferMetadata {
case .wifiPassword:
return "DeviceSendingWifiPassword".localized(with: deviceName)
case .notificationSync:
if let textDescription, !textDescription.isEmpty {
return textDescription
}
return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName)
}
}
+14
View File
@@ -68,6 +68,7 @@
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
@@ -136,6 +137,7 @@
EB3B9F6B2EF7151900D6704E /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
EB3B9F6C2EF7151900D6704E /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
@@ -188,11 +190,13 @@
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; };
EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; };
@@ -209,6 +213,7 @@
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */; };
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; };
EB5C37FF2E377CE70052EFD7 /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
@@ -271,6 +276,7 @@
EB950B4A2E520436002D68FA /* securegcm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */; };
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; };
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.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 */; };
@@ -486,6 +492,7 @@
69DA9A1829E0BF5200A442DA /* QuickDrop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDrop.entitlements; sourceTree = "<group>"; };
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyConnectionManager.swift; sourceTree = "<group>"; };
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboundNearbyConnection.swift; sourceTree = "<group>"; };
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = "<group>"; };
69DA9A2829E18CB500A442DA /* ukey.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ukey.pb.swift; sourceTree = "<group>"; };
69DA9A2929E18CB500A442DA /* wire_format.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = wire_format.pb.swift; sourceTree = "<group>"; };
69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = device_to_device_messages.pb.swift; sourceTree = "<group>"; };
@@ -767,6 +774,7 @@
children = (
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */,
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
698DFAE529E2F91A0064F247 /* NearbyConnection.swift */,
);
@@ -1289,6 +1297,7 @@
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */,
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
@@ -1325,6 +1334,7 @@
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
@@ -1379,6 +1389,7 @@
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */,
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */,
@@ -1443,6 +1454,7 @@
EB3B9F6B2EF7151900D6704E /* DataModel.swift in Sources */,
EB3B9F6C2EF7151900D6704E /* NearbyConnection.swift in Sources */,
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */,
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */,
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */,
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */,
@@ -1471,6 +1483,7 @@
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */,
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */,
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
EB1325D62E701D8E00FD749C /* Settings.swift in Sources */,
@@ -1510,6 +1523,7 @@
EB950B4A2E520436002D68FA /* securegcm.pb.swift in Sources */,
EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */,
EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */,
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
+699
View File
@@ -21890,6 +21890,705 @@
}
}
},
"NotificationSyncConsentPromptFromDevice" : {
"extractionState" : "manual",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "هل تريد تفعيل مزامنة الإشعارات من %1$@؟"
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voleu activar la sincronització de notificacions des de %1$@?"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Chcete povolit synchronizaci oznámení z %1$@?"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vil du aktivere synkronisering af notifikationer fra %1$@?"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Möchtest du die Benachrichtigungssynchronisierung von %1$@ aktivieren?"
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Θέλετε να ενεργοποιήσετε τον συγχρονισμό ειδοποιήσεων από το %1$@;"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Do you want to enable notification sync from %1$@?"
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "Do you want to enable notification sync from %1$@?"
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Do you want to enable notification sync from %1$@?"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Do you want to enable notification sync from %1$@?"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "¿Quieres activar la sincronización de notificaciones desde %1$@?"
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "¿Quieres activar la sincronización de notificaciones desde %1$@?"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Haluatko ottaa käyttöön ilmoitusten synkronoinnin laitteesta %1$@?"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Autoriser la synchronisation des notifications depuis %1$@ ?"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voulez-vous activer la synchronisation des notifications à partir de %1$@ ?"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "האם להפעיל סנכרון התראות מ-%1$@?"
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "क्या आप %1$@ से नोटिफिकेशन सिंक चालू करना चाहते हैं?"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Želite li omogućiti sinkronizaciju obavijesti s %1$@?"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Szeretnéd engedélyezni az értesítések szinkronizálását innen: %1$@?"
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "Apakah Anda ingin mengaktifkan sinkronisasi notifikasi dari %1$@?"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vuoi attivare la sincronizzazione delle notifiche da %1$@?"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ からの通知同期を有効にしますか?"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@에서 알림 동기화를 활성화할까요?"
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "Adakah anda ingin mengaktifkan penyegerakan notifikasi dari %1$@?"
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vil du aktivere varselsynkronisering fra %1$@?"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wil je meldingssynchronisatie van %1$@ inschakelen?"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Chcesz włączyć synchronizację powiadomień z %1$@?"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Deseja ativar a sincronização de notificações de %1$@?"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pretende ativar a sincronização de notificações de %1$@?"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dorești să activezi sincronizarea notificărilor de la %1$@?"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Включить синхронизацию уведомлений с устройства %1$@?"
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "Vill du aktivera synkronisering av aviseringar från %1$@?"
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "ต้องการเปิดใช้งานการซิงค์การแจ้งเตือนจาก %1$@ หรือไม่?"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "%1$@ cihazından bildirim senkronizasyonunu etkinleştirmek istiyor musunuz?"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Увімкнути синхронізацію сповіщень з %1$@?"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bạn có muốn bật đồng bộ thông báo từ %1$@ không?"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "要启用来自 %1$@ 的通知同步吗?"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "你想啟用從 %1$@ 同步通知嗎?"
}
}
}
},
"NotificationSyncFallbackSenderAndroid" : {
"extractionState" : "manual",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "אנדרואיד"
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "एंड्रॉइड"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "Android"
}
}
}
},
"NotificationSyncFallbackTitle" : {
"extractionState" : "manual",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "إشعار"
}
},
"ca" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notificació"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Oznámení"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notifikation"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Benachrichtigung"
}
},
"el" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ειδοποίηση"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notification"
}
},
"en-AU" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notification"
}
},
"en-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notification"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notification"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notificación"
}
},
"es-419" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notificación"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ilmoitus"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notification"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notification"
}
},
"he" : {
"stringUnit" : {
"state" : "translated",
"value" : "התראה"
}
},
"hi" : {
"stringUnit" : {
"state" : "translated",
"value" : "सूचना"
}
},
"hr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Obavijest"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Értesítés"
}
},
"id" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notifikasi"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notifica"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "通知"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "알림"
}
},
"ms" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notifikasi"
}
},
"nb" : {
"stringUnit" : {
"state" : "translated",
"value" : "Varsel"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Melding"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Powiadomienie"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notificação"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notificação"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
"value" : "Notificare"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Уведомление"
}
},
"sv" : {
"stringUnit" : {
"state" : "translated",
"value" : "Avisering"
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "การแจ้งเตือน"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bildirim"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
"value" : "Сповіщення"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Thông báo"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "通知"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "通知"
}
}
}
},
"OpenFinderAfterReceiving" : {
"localizations" : {
"ar" : {
+25
View File
@@ -81,6 +81,31 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
#if os(macOS)
DispatchQueue.main.async {
if transfer.type == .notificationSync {
self.hideQuickDropToast()
self.toastActions = nil
self.progress = nil
self.consentState = nil
let senderName = device.name?.trimmingCharacters(in: .whitespacesAndNewlines)
let resolvedSenderName = (senderName?.isEmpty == false) ? senderName! : "UnknownDevice".localized()
self.activeDeviceName = resolvedSenderName
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "NotificationSyncConsentPromptFromDevice".localized(with: resolvedSenderName)
alert.informativeText = pinCodeMessage
alert.addButton(withTitle: "Accept".localized())
alert.addButton(withTitle: "Decline".localized())
let result = alert.runModal()
if result == .alertFirstButtonReturn {
primaryButtonAction(true)
} else {
secondaryButtonAction()
}
return
}
self.toastActions = nil
self.progress = nil
self.activeDeviceName = device.name ?? "UnknownDevice".localized()