diff --git a/Fastmate.xcodeproj/project.pbxproj b/Fastmate.xcodeproj/project.pbxproj index 0017981..8f4571d 100644 --- a/Fastmate.xcodeproj/project.pbxproj +++ b/Fastmate.xcodeproj/project.pbxproj @@ -19,10 +19,10 @@ 5D6E3B3B212824AF00ED16C9 /* Fastmate.js in Resources */ = {isa = PBXBuildFile; fileRef = 5D6E3B3A212824AF00ED16C9 /* Fastmate.js */; }; 5D8DDE2A244399BD00747135 /* KVOBlockObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D8DDE29244399BD00747135 /* KVOBlockObserver.m */; }; 5DA21A162128617900C765BF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5DA21A142128617900C765BF /* Main.storyboard */; }; - 5DA21A19212865F000C765BF /* UnreadCountObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA21A18212865F000C765BF /* UnreadCountObserver.m */; }; 5DC2AF2D225933BE004C94E5 /* VersionChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DC2AF2C225933BE004C94E5 /* VersionChecker.m */; }; 5DC5E5952457F96D00C30171 /* NotificationCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DC5E5942457F96D00C30171 /* NotificationCenter.m */; }; 5DCFE8792258CEC5006B1A21 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DCFE8782258CEC5006B1A21 /* SettingsViewController.m */; }; + 5DF817F52790B9B700D765E4 /* UnreadCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF817F42790B9B700D765E4 /* UnreadCount.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -46,14 +46,13 @@ 5D8DDE29244399BD00747135 /* KVOBlockObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KVOBlockObserver.m; sourceTree = ""; }; 5D8DDE2B2443AD4800747135 /* UserDefaultsKeys.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserDefaultsKeys.h; sourceTree = ""; }; 5DA21A152128617900C765BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 5DA21A17212865F000C765BF /* UnreadCountObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UnreadCountObserver.h; sourceTree = ""; }; - 5DA21A18212865F000C765BF /* UnreadCountObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnreadCountObserver.m; sourceTree = ""; }; 5DC2AF2B225933BE004C94E5 /* VersionChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VersionChecker.h; sourceTree = ""; }; 5DC2AF2C225933BE004C94E5 /* VersionChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VersionChecker.m; sourceTree = ""; }; 5DC5E5932457F96D00C30171 /* NotificationCenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationCenter.h; sourceTree = ""; }; 5DC5E5942457F96D00C30171 /* NotificationCenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationCenter.m; sourceTree = ""; }; 5DCFE8772258CEC5006B1A21 /* SettingsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SettingsViewController.h; sourceTree = ""; }; 5DCFE8782258CEC5006B1A21 /* SettingsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsViewController.m; sourceTree = ""; }; + 5DF817F42790B9B700D765E4 /* UnreadCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadCount.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -99,12 +98,11 @@ 5D1DDBD127897C4D00244F04 /* AppDelegate.swift */, 5D1DDBD3278B718000244F04 /* UserDefaults.swift */, 5D1DDBD5278C483400244F04 /* MainWindowController.swift */, + 5DF817F42790B9B700D765E4 /* UnreadCount.swift */, 5D6E3B352122FF9E00ED16C9 /* WebViewController.h */, 5D6E3B362122FF9E00ED16C9 /* WebViewController.m */, 5D0F8E93240B910A00287BD1 /* PrintManager.h */, 5D0F8E94240B910A00287BD1 /* PrintManager.m */, - 5DA21A17212865F000C765BF /* UnreadCountObserver.h */, - 5DA21A18212865F000C765BF /* UnreadCountObserver.m */, 5DCFE8772258CEC5006B1A21 /* SettingsViewController.h */, 5DCFE8782258CEC5006B1A21 /* SettingsViewController.m */, 5DC2AF2B225933BE004C94E5 /* VersionChecker.h */, @@ -202,12 +200,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5DF817F52790B9B700D765E4 /* UnreadCount.swift in Sources */, 5D1DDBD227897C4D00244F04 /* AppDelegate.swift in Sources */, 5D8DDE2A244399BD00747135 /* KVOBlockObserver.m in Sources */, 5D1DDBD6278C483400244F04 /* MainWindowController.swift in Sources */, 5DC5E5952457F96D00C30171 /* NotificationCenter.m in Sources */, 5D1DDBD4278B718000244F04 /* UserDefaults.swift in Sources */, - 5DA21A19212865F000C765BF /* UnreadCountObserver.m in Sources */, 5D39BEE32122383900693D7E /* main.m in Sources */, 5DCFE8792258CEC5006B1A21 /* SettingsViewController.m in Sources */, 5D6E3B372122FF9E00ED16C9 /* WebViewController.m in Sources */, diff --git a/Fastmate/AppDelegate.swift b/Fastmate/AppDelegate.swift index bce563a..82add3e 100644 --- a/Fastmate/AppDelegate.swift +++ b/Fastmate/AppDelegate.swift @@ -18,15 +18,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var isAutomaticUpdateCheck = false private var subscriptions = Set() - lazy var unreadCountObserver = UnreadCountObserver() + @Published var mainWebViewController: WebViewController? - @objc var mainWebViewController: WebViewController? { - didSet { - unreadCountObserver.webViewController = mainWebViewController + func applicationDidFinishLaunching(_ notification: Notification) { + UserDefaults.standard.registerFastmateDefaults() + + FastmateNotificationCenter.sharedInstance().delegate = self + FastmateNotificationCenter.sharedInstance().registerForNotifications() + + DispatchQueue.global().async { + self.createUserScriptsFolderIfNeeded() } - } - func applicationDidFinishLaunching(_ notification: Notification) { NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didWakeNotification, object: nil) .sink { _ in self.mainWebViewController?.reload() } .store(in: &subscriptions) @@ -35,14 +38,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { .assign(to: \.statusItemVisible, on: self) .store(in: &subscriptions) - DispatchQueue.global().async { - self.createUserScriptsFolderIfNeeded() - } + let unreadCountPublisher = $mainWebViewController + .compactMap(\.?.unreadCountPublisher) + .switchToLatest() + .share() - FastmateNotificationCenter.sharedInstance().delegate = self - FastmateNotificationCenter.sharedInstance().registerForNotifications() + dockBadgeLabelPublisher(with: unreadCountPublisher.eraseToAnyPublisher()) + .assign(to: \.badgeLabel, on: NSApplication.shared.dockTile) + .store(in: &subscriptions) - UserDefaults.standard.registerFastmateDefaults() + statusItemImagePublisher(with: unreadCountPublisher.eraseToAnyPublisher()) + .sink { self.statusItem?.button?.image = $0 } + .store(in: &subscriptions) } func applicationDidBecomeActive(_ notification: Notification) { @@ -56,7 +63,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) self.statusItem?.button?.target = self self.statusItem?.button?.action = #selector(statusItemSelected(sender:)) - self.unreadCountObserver.statusItem = self.statusItem } else if let item = statusItem { NSStatusBar.system.removeStatusItem(item) } @@ -102,6 +108,29 @@ class AppDelegate: NSObject, NSApplicationDelegate { mainWebViewController?.handleMailtoURL(url) } } + + func dockBadgeLabelPublisher(with unreadCount: AnyPublisher) -> AnyPublisher { + unreadCount + .combineLatest( + UserDefaults.standard.publisher(for: \.shouldShowUnreadMailInDock), + UserDefaults.standard.publisher(for: \.shouldShowUnreadMailCountInDock) + ).map { count, shouldShowBadge, shouldShowCountInBadge in + guard count > 0, shouldShowBadge else { return nil } + return shouldShowCountInBadge ? String(count) : " " + }.eraseToAnyPublisher() + } + + func statusItemImagePublisher(with unreadCount: AnyPublisher) -> AnyPublisher { + unreadCount + .map { $0 > 0 } + .combineLatest( + UserDefaults.standard.publisher(for: \.shouldShowUnreadMailInStatusBar), + UserDefaults.standard.publisher(for: \.shouldShowUnreadMailIndicator) + ) + .map { $0 && $1 && $2 ? "status-bar-unread" : "status-bar" } + .map(NSImage.init(imageLiteralResourceName:)) + .eraseToAnyPublisher() + } } extension AppDelegate: FastmateNotificationCenterDelegate { diff --git a/Fastmate/Fastmate-Bridging-Header.h b/Fastmate/Fastmate-Bridging-Header.h index a65765b..b48d4e8 100644 --- a/Fastmate/Fastmate-Bridging-Header.h +++ b/Fastmate/Fastmate-Bridging-Header.h @@ -1,6 +1,5 @@ #import "WebViewController.h" #import "KVOBlockObserver.h" #import "NotificationCenter.h" -#import "UnreadCountObserver.h" #import "VersionChecker.h" #import "PrintManager.h" diff --git a/Fastmate/MainWindowController.swift b/Fastmate/MainWindowController.swift index 2233f13..42e64e9 100644 --- a/Fastmate/MainWindowController.swift +++ b/Fastmate/MainWindowController.swift @@ -21,7 +21,7 @@ class MainWindowController: NSWindowController, NSWindowDelegate { return } - webViewController.webView.publisher(for: \.title) + webViewController.publisher(for: \.webView?.title) .replaceNil(with: "Fastmate") .assign(to: \.title, on: window) .store(in: &subscriptions) diff --git a/Fastmate/UnreadCount.swift b/Fastmate/UnreadCount.swift new file mode 100644 index 0000000..3d0690d --- /dev/null +++ b/Fastmate/UnreadCount.swift @@ -0,0 +1,55 @@ +import Combine + +private extension Dictionary where Key == String, Value == NSNumber { + // Extracts total unread count from a dictionary with folder name -> count + var unreadCount: Int { + mapValues(\.intValue) + .reduce(0, { $0 + $1.value }) + } +} + +private extension String { + // Extracts unread count from a Fastmail web view title + var unreadCount: Int { + let regex = try! NSRegularExpression(pattern: "^(\\d+) •", options: .anchorsMatchLines) + let result = regex.firstMatch(in: self, options: [], range: NSRange(location: 0, length: self.count)) + if let result = result, result.numberOfRanges > 1 { + let range = Range(result.range(at: 1), in: self)! + let countString = self[range] + return Int(String(countString)) ?? 0 + } + return 0 + } +} + +extension WebViewController { + var unreadCountPublisher: AnyPublisher { + let titleCount = publisher(for: \.webView?.title) + .map(\.?.unreadCount) + .replaceNil(with: 0) + + let allFoldersCount = publisher(for: \.mailboxes) + .map(\.unreadCount) + + let watchedFolders = UserDefaults.standard.publisher(for: \.watchedFolders) + .map { $0.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }} + + let specificFoldersCount = publisher(for: \.mailboxes) + .combineLatest(watchedFolders) + .map { mailboxes, watchedFolders in + mailboxes.filter { title, _ in watchedFolders.contains(title) } + } + .map(\.unreadCount) + + return UserDefaults.standard.publisher(for: \.watchedFolderType) + .map { type -> AnyPublisher in + switch type { + case .selected: return titleCount.eraseToAnyPublisher() + case .all: return allFoldersCount.eraseToAnyPublisher() + case .specific: return specificFoldersCount.eraseToAnyPublisher() + } + } + .switchToLatest() + .eraseToAnyPublisher() + } +} diff --git a/Fastmate/UnreadCountObserver.h b/Fastmate/UnreadCountObserver.h deleted file mode 100644 index 9b57078..0000000 --- a/Fastmate/UnreadCountObserver.h +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import "WebViewController.h" - -@interface UnreadCountObserver : NSObject - -@property (nonatomic, strong) WebViewController *webViewController; -@property (nonatomic, weak) NSStatusItem *statusItem; - -@end diff --git a/Fastmate/UnreadCountObserver.m b/Fastmate/UnreadCountObserver.m deleted file mode 100644 index 951d492..0000000 --- a/Fastmate/UnreadCountObserver.m +++ /dev/null @@ -1,151 +0,0 @@ -#import "UnreadCountObserver.h" -#import "KVOBlockObserver.h" -#import "UserDefaultsKeys.h" - -@interface UnreadCountObserver() - -@property (nonatomic, assign) NSUInteger unreadCount; -@property (nonatomic, strong) NSArray *observers; - -@property (nonatomic, copy) NSString *webViewTitle; -@property (nonatomic, copy) NSDictionary *mailBoxes; - -@end - -@implementation UnreadCountObserver - -- (instancetype)init { - if (self = [super init]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self registerObservers]; - }); - } - return self; -} - -- (void)registerObservers { - __weak typeof(self) weakSelf = self; - void (^updateBlock)(id) = ^(id _) { - [weakSelf updateUnreadCount]; - }; - - NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; - self.observers = @[ - [[KVOBlockObserver alloc] initWithObject:defaults keyPath:ShouldShowUnreadMailIndicatorKey block:updateBlock], - [[KVOBlockObserver alloc] initWithObject:defaults keyPath:ShouldShowUnreadMailInDockKey block:updateBlock], - [[KVOBlockObserver alloc] initWithObject:defaults keyPath:ShouldShowUnreadMailCountInDockKey block:updateBlock], - [[KVOBlockObserver alloc] initWithObject:defaults keyPath:ShouldShowStatusBarIconKey block:updateBlock], - [[KVOBlockObserver alloc] initWithObject:defaults keyPath:ShouldShowUnreadMailInStatusBarKey block:updateBlock], - [[KVOBlockObserver alloc] initWithObject:defaults keyPath:WatchedFolderTypeKey block:updateBlock], - [[KVOBlockObserver alloc] initWithObject:defaults keyPath:WatchedFoldersKey block:updateBlock], - [[KVOBlockObserver alloc] initWithObject:self keyPath:@"webViewController.mailboxes" block:^(NSDictionary *mailboxes) { - weakSelf.mailBoxes = mailboxes; - }], - [[KVOBlockObserver alloc] initWithObject:self keyPath:@"webViewController.webView.title" block:^(NSString *title) { - weakSelf.webViewTitle = title; - }], - ]; - updateBlock(nil); -} - -- (WatchedFolderType)watchedFolderType { - return [NSUserDefaults.standardUserDefaults integerForKey:WatchedFolderTypeKey]; -} - -- (NSArray *)watchedFolders { - NSString *watchedFoldersString = [NSUserDefaults.standardUserDefaults stringForKey:WatchedFoldersKey]; - NSArray *watchedFolders = [watchedFoldersString componentsSeparatedByString:@","]; - NSMutableArray *normalizedFolders = [NSMutableArray new]; - for (NSString *folder in watchedFolders) { - [normalizedFolders addObject:[folder stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; - } - return normalizedFolders; -} - -- (void)setWebViewTitle:(NSString *)title { - if (![_webViewTitle isEqual:title]) { - _webViewTitle = title; - [self updateUnreadCount]; - } -} - -- (void)setMailBoxes:(NSDictionary *)mailBoxes { - if (![_mailBoxes isEqual:mailBoxes]) { - _mailBoxes = mailBoxes; - [self updateUnreadCount]; - } -} - -- (NSUInteger)titleUnreadCount { - if (self.webViewTitle == nil) { - return 0; - } - - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^(\\d+) •" options:NSRegularExpressionAnchorsMatchLines error:nil]; - NSTextCheckingResult *result = [regex firstMatchInString:self.webViewTitle options:0 range:NSMakeRange(0, self.webViewTitle.length)]; - if (result && result.numberOfRanges > 1) { - NSString *unreadCountString = [self.webViewTitle substringWithRange:[result rangeAtIndex:1]]; - return unreadCountString.integerValue; - } - return 0; -} - -- (void)updateUnreadCount { - NSUInteger totalCount = 0; - switch ([self watchedFolderType]) { - case WatchedFolderTypeDefault: - totalCount = self.titleUnreadCount; - break; - case WatchedFolderTypeSpecific: { - for (NSString *folder in [self watchedFolders]) { - totalCount += self.mailBoxes[folder].integerValue; - } - break; - } - case WatchedFolderTypeAll: - for (NSNumber *count in self.mailBoxes.allValues) { - totalCount += count.integerValue; - } - break; - } - - self.unreadCount = totalCount; - [self updateDockIndicator]; - [self updateStatusBarIndicator]; -} - -- (void)updateDockIndicator { - NSString *badgeLabel = nil; - if ([self shouldShowDockIndicator]) { - badgeLabel = [self shouldShowCountInDock] ? [NSString stringWithFormat:@"%ld", self.unreadCount] : @" "; - } - NSApplication.sharedApplication.dockTile.badgeLabel = badgeLabel; -} - -- (void)updateStatusBarIndicator { - self.statusItem.button.image = [NSImage imageNamed:[self shouldShowStatusBarIndicator] ? @"status-bar-unread" : @"status-bar"]; -} - -- (void)setStatusItem:(NSStatusItem *)statusItem { - _statusItem = statusItem; - [self updateStatusBarIndicator]; -} - -- (BOOL)shouldShowStatusBarIndicator { - return self.statusItem - && self.unreadCount > 0 - && [NSUserDefaults.standardUserDefaults boolForKey:ShouldShowUnreadMailInStatusBarKey] - && [NSUserDefaults.standardUserDefaults boolForKey:ShouldShowUnreadMailIndicatorKey]; -} - -- (BOOL)shouldShowDockIndicator { - return self.unreadCount > 0 - && [NSUserDefaults.standardUserDefaults boolForKey:ShouldShowUnreadMailInDockKey] - && [NSUserDefaults.standardUserDefaults boolForKey:ShouldShowUnreadMailIndicatorKey]; -} - -- (BOOL)shouldShowCountInDock { - return [NSUserDefaults.standardUserDefaults boolForKey:ShouldShowUnreadMailCountInDockKey]; -} - -@end diff --git a/Fastmate/UserDefaults.swift b/Fastmate/UserDefaults.swift index 4ea6828..66017b3 100644 --- a/Fastmate/UserDefaults.swift +++ b/Fastmate/UserDefaults.swift @@ -13,7 +13,7 @@ extension UserDefaults { #keyPath(shouldShowUnreadMailInStatusBar): true, #keyPath(shouldUseFastmailBeta): false, #keyPath(shouldUseTransparentTitleBar): true, - #keyPath(watchedFolderType): WatchedFolderType.default.rawValue, + #keyPath(watchedFolderType): WatchedFolderType.selected.rawValue, #keyPath(watchedFolders): "", ]) } @@ -52,7 +52,7 @@ extension UserDefaults { } @objc enum WatchedFolderType: UInt { - case `default` + case selected case all case specific } diff --git a/Fastmate/WebViewController.h b/Fastmate/WebViewController.h index 67965ec..af75c9a 100644 --- a/Fastmate/WebViewController.h +++ b/Fastmate/WebViewController.h @@ -1,10 +1,12 @@ #import +NS_ASSUME_NONNULL_BEGIN + @class WKWebView; @interface WebViewController : NSViewController -@property (nonatomic, readonly) WKWebView *webView; +@property (nonatomic, readonly, nullable) WKWebView *webView; - (void)composeNewEmail; - (void)focusSearchField; @@ -17,6 +19,8 @@ - (void)reload; @property (nonatomic, strong) NSDictionary *mailboxes; // Name -> unreadCount -@property (nonatomic, strong) NSURL *currentlyViewedAttachment; +@property (nonatomic, strong, nullable) NSURL *currentlyViewedAttachment; @end + +NS_ASSUME_NONNULL_END diff --git a/Fastmate/WebViewController.m b/Fastmate/WebViewController.m index 452dc36..fe505bb 100644 --- a/Fastmate/WebViewController.m +++ b/Fastmate/WebViewController.m @@ -27,6 +27,13 @@ @interface WebViewController ()