From b6120205760f3cf048c5958e3258ba5e64619002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=20C=C5=93ur?= Date: Thu, 24 Nov 2022 03:47:56 +0800 Subject: [PATCH] Support UserNotifications framework (#3040) --- Transmission.xcodeproj/project.pbxproj | 4 + macosx/CMakeLists.txt | 1 + macosx/Controller.mm | 296 ++++++++++++++++++------- 3 files changed, 216 insertions(+), 85 deletions(-) diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index d504f7d6c..d2b6794c2 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -399,6 +399,7 @@ C809AEE7291ECFD000BFDBE1 /* NSDataAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = C809AEE6291ECFD000BFDBE1 /* NSDataAdditions.mm */; }; C841A28129197724009F18E8 /* NSKeyedUnarchiverAdditions.mm in Sources */ = {isa = PBXBuildFile; fileRef = C841A28029197724009F18E8 /* NSKeyedUnarchiverAdditions.mm */; }; C86BCD9928228A9600F45599 /* SparkleProxy.mm in Sources */ = {isa = PBXBuildFile; fileRef = C86BCD9828228A9600F45599 /* SparkleProxy.mm */; }; + C87369652809984200573C90 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C87369642809984200573C90 /* UserNotifications.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C88771AD2803EE7B005C7523 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C88771A92803EE42005C7523 /* libz.tbd */; }; C88771AE2803EE7C005C7523 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C88771A92803EE42005C7523 /* libz.tbd */; }; C88771AF2803EE7D005C7523 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C88771A92803EE42005C7523 /* libz.tbd */; }; @@ -1183,6 +1184,7 @@ C841A27F29197724009F18E8 /* NSKeyedUnarchiverAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSKeyedUnarchiverAdditions.h; sourceTree = ""; }; C841A28029197724009F18E8 /* NSKeyedUnarchiverAdditions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NSKeyedUnarchiverAdditions.mm; sourceTree = ""; }; C86BCD9828228A9600F45599 /* SparkleProxy.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SparkleProxy.mm; sourceTree = ""; }; + C87369642809984200573C90 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; C88771A92803EE42005C7523 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; C88771AB2803EE53005C7523 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; C887BEC02807FCE900867D3C /* create.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = create.cc; sourceTree = ""; }; @@ -1239,6 +1241,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + C87369652809984200573C90 /* UserNotifications.framework in Frameworks */, 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, 4D3EA0AA08AE13C600EA10C2 /* IOKit.framework in Frameworks */, 4D1838DD09DEC0E80047D688 /* libtransmission.a in Frameworks */, @@ -1873,6 +1876,7 @@ A2F35BBA15C5A0A100EBF632 /* Frameworks */ = { isa = PBXGroup; children = ( + C87369642809984200573C90 /* UserNotifications.framework */, 55869925257074EC00F77A43 /* libcurl.tbd */, C88771AB2803EE53005C7523 /* libiconv.tbd */, C88771A92803EE42005C7523 /* libz.tbd */, diff --git a/macosx/CMakeLists.txt b/macosx/CMakeLists.txt index 2c1fe0159..14c9e2723 100644 --- a/macosx/CMakeLists.txt +++ b/macosx/CMakeLists.txt @@ -444,6 +444,7 @@ target_link_libraries(${TR_NAME}-mac "-framework IOKit" "-framework Quartz" "-framework Security" + "-weak_framework UserNotifications" ) if(NOT CMAKE_GENERATOR STREQUAL Xcode) diff --git a/macosx/Controller.mm b/macosx/Controller.mm index da071cd74..3cc812c12 100644 --- a/macosx/Controller.mm +++ b/macosx/Controller.mm @@ -5,6 +5,7 @@ @import IOKit; @import IOKit.pwr_mgt; @import Carbon; +@import UserNotifications; @import Sparkle; @@ -232,7 +233,7 @@ static void removeKeRangerRansomware() NSLog(@"OSX.KeRanger.A ransomware removal completed, proceeding to normal operation"); } -@interface Controller () +@interface Controller () @property(nonatomic) IBOutlet NSWindow* fWindow; @property(nonatomic) IBOutlet NSStackView* fStackView; @@ -782,8 +783,6 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool self.fBadger = [[Badger alloc] initWithLib:self.fLib]; - NSUserNotificationCenter.defaultUserNotificationCenter.delegate = self; - //observe notifications NSNotificationCenter* nc = NSNotificationCenter.defaultCenter; @@ -842,6 +841,35 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool } } +- (void)applicationWillFinishLaunching:(NSNotification*)notification +{ + // user notifications + if (@available(macOS 10.14, *)) + { + UNUserNotificationCenter.currentNotificationCenter.delegate = self; + UNNotificationAction* actionShow = [UNNotificationAction actionWithIdentifier:@"actionShow" + title:NSLocalizedString(@"Show", "notification button") + options:UNNotificationActionOptionForeground]; + UNNotificationCategory* categoryShow = [UNNotificationCategory categoryWithIdentifier:@"categoryShow" actions:@[ actionShow ] + intentIdentifiers:@[] + options:UNNotificationCategoryOptionNone]; + [UNUserNotificationCenter.currentNotificationCenter setNotificationCategories:[NSSet setWithObject:categoryShow]]; + [UNUserNotificationCenter.currentNotificationCenter + requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) + completionHandler:^(BOOL granted, NSError* _Nullable error) { + if (error.code > 0) + { + NSLog(@"UserNotifications not configured: %@", error.localizedDescription); + } + }]; + } + else + { + // Fallback on earlier versions + NSUserNotificationCenter.defaultUserNotificationCenter.delegate = self; + } +} + - (void)applicationDidFinishLaunching:(NSNotification*)notification { NSApp.servicesProvider = self; @@ -2338,11 +2366,37 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool self.fTotalTorrentsField.stringValue = totalTorrentsString; } +- (void)userNotificationCenter:(UNUserNotificationCenter*)center + willPresentNotification:(UNNotification*)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler API_AVAILABLE(macos(10.14)) +{ + completionHandler(-1); +} + - (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center shouldPresentNotification:(NSUserNotification*)notification { return YES; } +- (void)userNotificationCenter:(UNUserNotificationCenter*)center + didReceiveNotificationResponse:(UNNotificationResponse*)response + withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(macos(10.14)) +{ + if (!response.notification.request.content.userInfo.count) + { + return; + } + + if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) + { + [self didActivateNotificationByDefaultActionWithUserInfo:response.notification.request.content.userInfo]; + } + else if ([response.actionIdentifier isEqualToString:@"actionShow"]) + { + [self didActivateNotificationByActionShowWithUserInfo:response.notification.request.content.userInfo]; + } +} + - (void)userNotificationCenter:(NSUserNotificationCenter*)center didActivateNotification:(NSUserNotification*)notification { if (!notification.userInfo) @@ -2352,29 +2406,67 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) //reveal { - Torrent* torrent = [self torrentForHash:notification.userInfo[@"Hash"]]; - NSString* location = torrent.dataLocation; - if (!location) - { - location = notification.userInfo[@"Location"]; - } - if (location) - { - [NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:@[ [NSURL fileURLWithPath:location] ]]; - } + [self didActivateNotificationByActionShowWithUserInfo:notification.userInfo]; } else if (notification.activationType == NSUserNotificationActivationTypeContentsClicked) { - Torrent* torrent = [self torrentForHash:notification.userInfo[@"Hash"]]; - if (!torrent) + [self didActivateNotificationByDefaultActionWithUserInfo:notification.userInfo]; + } +} + +- (void)didActivateNotificationByActionShowWithUserInfo:(NSDictionary*)userInfo +{ + Torrent* torrent = [self torrentForHash:userInfo[@"Hash"]]; + NSString* location = torrent.dataLocation; + if (!location) + { + location = userInfo[@"Location"]; + } + if (location) + { + [NSWorkspace.sharedWorkspace activateFileViewerSelectingURLs:@[ [NSURL fileURLWithPath:location] ]]; + } +} + +- (void)didActivateNotificationByDefaultActionWithUserInfo:(NSDictionary*)userInfo +{ + Torrent* torrent = [self torrentForHash:userInfo[@"Hash"]]; + if (!torrent) + { + return; + } + //select in the table - first see if it's already shown + NSInteger row = [self.fTableView rowForItem:torrent]; + if (row == -1) + { + //if it's not shown, see if it's in a collapsed row + if ([self.fDefaults boolForKey:@"SortByGroup"]) { - return; + __block TorrentGroup* parent = nil; + [self.fDisplayedTorrents enumerateObjectsWithOptions:NSEnumerationConcurrent + usingBlock:^(TorrentGroup* group, NSUInteger idx, BOOL* stop) { + if ([group.torrents containsObject:torrent]) + { + parent = group; + *stop = YES; + } + }]; + if (parent) + { + [[self.fTableView animator] expandItem:parent]; + row = [self.fTableView rowForItem:torrent]; + } } - //select in the table - first see if it's already shown - NSInteger row = [self.fTableView rowForItem:torrent]; + if (row == -1) { - //if it's not shown, see if it's in a collapsed row + //not found - must be filtering + NSAssert([self.fDefaults boolForKey:@"FilterBar"], @"expected the filter to be enabled"); + [self.fFilterBar reset:YES]; + + row = [self.fTableView rowForItem:torrent]; + + //if it's not shown, it has to be in a collapsed row...again if ([self.fDefaults boolForKey:@"SortByGroup"]) { __block TorrentGroup* parent = nil; @@ -2392,41 +2484,13 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool row = [self.fTableView rowForItem:torrent]; } } - - if (row == -1) - { - //not found - must be filtering - NSAssert([self.fDefaults boolForKey:@"FilterBar"], @"expected the filter to be enabled"); - [self.fFilterBar reset:YES]; - - row = [self.fTableView rowForItem:torrent]; - - //if it's not shown, it has to be in a collapsed row...again - if ([self.fDefaults boolForKey:@"SortByGroup"]) - { - __block TorrentGroup* parent = nil; - [self.fDisplayedTorrents enumerateObjectsWithOptions:NSEnumerationConcurrent - usingBlock:^(TorrentGroup* group, NSUInteger idx, BOOL* stop) { - if ([group.torrents containsObject:torrent]) - { - parent = group; - *stop = YES; - } - }]; - if (parent) - { - [[self.fTableView animator] expandItem:parent]; - row = [self.fTableView rowForItem:torrent]; - } - } - } } - - NSAssert1(row != -1, @"expected a row to be found for torrent %@", torrent); - - [self showMainWindow:nil]; - [self.fTableView selectAndScrollToRow:row]; } + + NSAssert1(row != -1, @"expected a row to be found for torrent %@", torrent); + + [self showMainWindow:nil]; + [self.fTableView selectAndScrollToRow:row]; } - (Torrent*)torrentForHash:(NSString*)hash @@ -2461,24 +2525,39 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool } } + NSString* title = NSLocalizedString(@"Download Complete", "notification title"); + NSString* body = torrent.name; NSString* location = torrent.dataLocation; - - NSString* notificationTitle = NSLocalizedString(@"Download Complete", "notification title"); - NSUserNotification* notification = [[NSUserNotification alloc] init]; - notification.title = notificationTitle; - notification.informativeText = torrent.name; - - notification.hasActionButton = YES; - notification.actionButtonTitle = NSLocalizedString(@"Show", "notification button"); - NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithObject:torrent.hashString forKey:@"Hash"]; if (location) { userInfo[@"Location"] = location; } - notification.userInfo = userInfo; - [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification]; + if (@available(macOS 10.14, *)) + { + NSString* identifier = [@"Download Complete " stringByAppendingString:torrent.hashString]; + UNMutableNotificationContent* content = [UNMutableNotificationContent new]; + content.title = title; + content.body = body; + content.categoryIdentifier = @"categoryShow"; + content.userInfo = userInfo; + + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil]; + [UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil]; + } + else + { + // Fallback on earlier versions + NSUserNotification* notification = [[NSUserNotification alloc] init]; + notification.title = title; + notification.informativeText = body; + notification.hasActionButton = YES; + notification.actionButtonTitle = NSLocalizedString(@"Show", "notification button"); + notification.userInfo = userInfo; + + [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification]; + } if (!self.fWindow.mainWindow) { @@ -2513,24 +2592,39 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool } } + NSString* title = NSLocalizedString(@"Seeding Complete", "notification title"); + NSString* body = torrent.name; NSString* location = torrent.dataLocation; - - NSString* notificationTitle = NSLocalizedString(@"Seeding Complete", "notification title"); - NSUserNotification* userNotification = [[NSUserNotification alloc] init]; - userNotification.title = notificationTitle; - userNotification.informativeText = torrent.name; - - userNotification.hasActionButton = YES; - userNotification.actionButtonTitle = NSLocalizedString(@"Show", "notification button"); - NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithObject:torrent.hashString forKey:@"Hash"]; if (location) { userInfo[@"Location"] = location; } - userNotification.userInfo = userInfo; - [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:userNotification]; + if (@available(macOS 10.14, *)) + { + NSString* identifier = [@"Seeding Complete " stringByAppendingString:torrent.hashString]; + UNMutableNotificationContent* content = [UNMutableNotificationContent new]; + content.title = title; + content.body = body; + content.categoryIdentifier = @"categoryShow"; + content.userInfo = userInfo; + + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil]; + [UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil]; + } + else + { + // Fallback on earlier versions + NSUserNotification* userNotification = [[NSUserNotification alloc] init]; + userNotification.title = title; + userNotification.informativeText = body; + userNotification.hasActionButton = YES; + userNotification.actionButtonTitle = NSLocalizedString(@"Show", "notification button"); + userNotification.userInfo = userInfo; + + [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:userNotification]; + } //removing from the list calls fullUpdateUI if (torrent.removeWhenFinishSeeding) @@ -3364,13 +3458,30 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool if (![dict[@"ByUser"] boolValue]) { - NSUserNotification* notification = [[NSUserNotification alloc] init]; - notification.title = isLimited ? NSLocalizedString(@"Speed Limit Auto Enabled", "notification title") : - NSLocalizedString(@"Speed Limit Auto Disabled", "notification title"); - notification.informativeText = NSLocalizedString(@"Bandwidth settings changed", "notification description"); - notification.hasActionButton = NO; + NSString* title = isLimited ? NSLocalizedString(@"Speed Limit Auto Enabled", "notification title") : + NSLocalizedString(@"Speed Limit Auto Disabled", "notification title"); + NSString* body = NSLocalizedString(@"Bandwidth settings changed", "notification description"); - [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification]; + if (@available(macOS 10.14, *)) + { + NSString* identifier = @"Bandwidth settings changed"; + UNMutableNotificationContent* content = [UNMutableNotificationContent new]; + content.title = title; + content.body = body; + + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil]; + [UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil]; + } + else + { + // Fallback on earlier versions + NSUserNotification* notification = [[NSUserNotification alloc] init]; + notification.title = title; + notification.informativeText = body; + notification.hasActionButton = NO; + + [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification]; + } } } @@ -3468,12 +3579,27 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool [self openFiles:@[ fullFile ] addType:ADD_AUTO forcePath:nil]; NSString* notificationTitle = NSLocalizedString(@"Torrent File Auto Added", "notification title"); - NSUserNotification* notification = [[NSUserNotification alloc] init]; - notification.title = notificationTitle; - notification.informativeText = file; - notification.hasActionButton = NO; - [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification]; + if (@available(macOS 10.14, *)) + { + NSString* identifier = [@"Torrent File Auto Added " stringByAppendingString:file]; + UNMutableNotificationContent* content = [UNMutableNotificationContent new]; + content.title = notificationTitle; + content.body = file; + + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:nil]; + [UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:request withCompletionHandler:nil]; + } + else + { + // Fallback on earlier versions + NSUserNotification* notification = [[NSUserNotification alloc] init]; + notification.title = notificationTitle; + notification.informativeText = file; + notification.hasActionButton = NO; + + [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification]; + } } }