mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Add clipboard sync notification
This commit is contained in:
1 parent
5fdc5f8a1b
commit
2f6b0ff5ba
7 files changed
+178
-6
No files matched your search
@@ -30,11 +30,13 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
private var textPayloadID: Int64 = 0
|
||||
private var bytesToBeTransferred: Int64 = 0
|
||||
private var isAuthenticated = false
|
||||
private var receiverAuthenticationAccepted = false
|
||||
private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil
|
||||
private var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata?
|
||||
private var pairingMetadata: Sharing_Nearby_PairingMetadata?
|
||||
private var isPairingSetupRequest = false
|
||||
var isPlainTextTransfer = false
|
||||
var isClipboardSyncTransfer = false
|
||||
var isControlTransfer = false
|
||||
|
||||
var wasUserRejected = false
|
||||
@@ -287,6 +289,14 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.clearContents()
|
||||
pasteboard.setString(urlStr, forType: .string)
|
||||
#if !EXTENSION
|
||||
if isClipboardSyncTransfer {
|
||||
ClipboardSyncNotificationPresenter.shared.present(
|
||||
clipboardText: urlStr,
|
||||
senderDeviceName: remoteDeviceInfo?.name
|
||||
)
|
||||
}
|
||||
#endif
|
||||
#elseif os(iOS)
|
||||
// iOS clipboard
|
||||
UIPasteboard.general.string = urlStr
|
||||
@@ -570,6 +580,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
|
||||
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
|
||||
receiverAuthenticationAccepted = frame.v1.pairedKeyResult.status == .success
|
||||
currentState = .receivedPairedKeyResult
|
||||
}
|
||||
|
||||
@@ -727,8 +738,17 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
if let textMetadata = frame.v1.introduction.textMetadata.first {
|
||||
let isURL = textMetadata.type == .url
|
||||
textPayloadID = textMetadata.payloadID
|
||||
isClipboardSyncTransfer = !isURL && isAuthenticated && receiverAuthenticationAccepted
|
||||
|
||||
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 {
|
||||
isPlainTextTransfer = true
|
||||
|
||||
@@ -379,7 +379,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
|
||||
if !connection.wasUserRejected {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public enum ReceiverAuthenticationPolicy: Equatable {
|
||||
public protocol InboundAppDelegate: AnyObject {
|
||||
func obtainUserConsent(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 showPlusScreen()
|
||||
func notificationSyncSetupConfirmed()
|
||||
@@ -217,14 +217,16 @@ public struct TransferMetadata {
|
||||
public let textDescription: String?
|
||||
public let type: TransferType
|
||||
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.id = id
|
||||
self.pinCode = pinCode
|
||||
self.textDescription = textDescription
|
||||
self.type = transferType
|
||||
self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice
|
||||
self.isClipboardSync = isClipboardSync
|
||||
}
|
||||
|
||||
public enum TransferType {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
69DA9A1429E0BF5200A442DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; };
|
||||
A1C0DE112F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
|
||||
A1C0DE122F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
|
||||
A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
|
||||
A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
|
||||
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||
@@ -511,6 +513,7 @@
|
||||
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>"; };
|
||||
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncCoordinator.swift; sourceTree = "<group>"; };
|
||||
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.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>"; };
|
||||
A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncQrSheetManager.swift; sourceTree = "<group>"; };
|
||||
@@ -934,6 +937,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */,
|
||||
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */,
|
||||
EB1325F72E70287C00FD749C /* SendToExtension.swift */,
|
||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
||||
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
||||
@@ -1366,6 +1370,7 @@
|
||||
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||
A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
|
||||
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
|
||||
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
|
||||
@@ -1425,6 +1430,7 @@
|
||||
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
|
||||
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
|
||||
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||
A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
|
||||
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
|
||||
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */,
|
||||
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
|
||||
|
||||
@@ -169,8 +169,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
|
||||
|
||||
public func userNotificationCenter(_: UNUserNotificationCenter,
|
||||
willPresent _: UNNotification,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
if notification.request.identifier == ClipboardSyncNotificationPresenter.notificationIdentifier {
|
||||
completionHandler([.banner])
|
||||
return
|
||||
}
|
||||
completionHandler([.sound, .banner])
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// 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 let center = UNUserNotificationCenter.current()
|
||||
private let queue = DispatchQueue(label: "ClipboardSyncNotificationPresenter")
|
||||
private var pendingNotifications: [(String, String?)] = []
|
||||
private var authorizationRequestInFlight = false
|
||||
|
||||
private init() {}
|
||||
|
||||
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.notificationIdentifier
|
||||
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||
center.removeDeliveredNotifications(withIdentifiers: [identifier])
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -185,6 +185,11 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
|
||||
|
||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) {
|
||||
#if os(macOS)
|
||||
if transfer.isClipboardSync {
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
let mainMessage = transfer.getDescription(deviceName: device.name ?? "AndroidDevice".localized())
|
||||
|
||||
@@ -244,7 +249,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -388,6 +393,9 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
if wasPendingConsent {
|
||||
return
|
||||
}
|
||||
if wasClipboardSync && error == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
if let name = device?.name {
|
||||
|
||||
Reference in new issue
Block a user