From 7de6ab54c4130304942a80a336bd2699c0b5c331 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Fri, 2 Jul 2021 12:41:21 -0500 Subject: [PATCH 1/4] Support meeting invites. --- .../CalendarHandler.swift | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/Clocker/Events and Reminders/CalendarHandler.swift b/Clocker/Events and Reminders/CalendarHandler.swift index 846bb3e..2a01866 100644 --- a/Clocker/Events and Reminders/CalendarHandler.swift +++ b/Clocker/Events and Reminders/CalendarHandler.swift @@ -332,14 +332,83 @@ extension EventCenter { let isEndDate = autoupdatingCalendar.isDate(date, inSameDayAs: event.endDate) && (event.startDate.compare(date) == .orderedAscending) let isAllDay = event.isAllDay || (event.startDate.compare(date) == .orderedAscending && event.endDate.compare(nextDate) == .orderedSame) let isSingleDay = event.isAllDay && (event.startDate.compare(date) == .orderedSame && event.endDate.compare(nextDate) == .orderedSame) - + let meetingURL = retrieveMeetingURL(event) let eventInfo = EventInfo(event: event, isStartDate: isStartDate, isEndDate: isEndDate, isAllDay: isAllDay, - isSingleDay: isSingleDay) + isSingleDay: isSingleDay, + meetingURL: meetingURL) return eventInfo } + + static var dataDetector: NSDataDetector? = nil + + // Borrowing logic from Ityscal + @discardableResult + private func findAppropriateURLs(_ description: String) -> URL? { + guard let results = EventCenter.dataDetector?.matches(in: description, options: .reportCompletion, range: NSMakeRange(0, description.count)) else { + return nil + } + for result in results { + if result.resultType == .link, var actualLink = result.url?.absoluteString { + // Check for Zoom links + if actualLink.contains("zoom.us/j/") || actualLink.contains("zoom.us/s/") || actualLink.contains("zoom.us/w/") { + // Create a Zoom App link + let workspace = NSWorkspace.shared + if (workspace.urlForApplication(toOpen: URL(string: "zoommtg://")!) != nil) { + actualLink = actualLink.replacingOccurrences(of: "https://", with: "zoommtg://") + actualLink = actualLink.replacingOccurrences(of: "?", with: "&") + actualLink = actualLink.replacingOccurrences(of: "/j/", with: "/join?confno=") + actualLink = actualLink.replacingOccurrences(of: "/s/", with: "/join?confno=") + actualLink = actualLink.replacingOccurrences(of: "/w/", with: "/join?confno=") + if let appLink = URL(string: actualLink) { + return appLink + } + + } + } else if actualLink.contains("zoommtg://") + || actualLink.contains("meet.google.com/") + || actualLink.contains("hangouts.google.com/") + || actualLink.contains("webex.com/") + || actualLink.contains("gotomeeting.com/join") + || actualLink.contains("ringcentral.com/j") + || actualLink.contains("bigbluebutton.org/gl") + || actualLink.contains("://bigbluebutton.") + || actualLink.contains("://bbb.") + || actualLink.contains("indigo.collocall.de") + || actualLink.contains("public.senfcall.de") + || actualLink.contains("youcanbook.me/zoom/") + || actualLink.contains("workplace.com/groupcall") { + if let zoomLink = result.url { + return zoomLink + } + } + } + } + return nil + } + + private func retrieveMeetingURL(_ event: EKEvent) -> URL? { + if EventCenter.dataDetector == nil { + // TODO: Handle Try-Catch gracefully + EventCenter.dataDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + } + + if let location = event.location { + return findAppropriateURLs(location) + } + + if let url = event.url { + return findAppropriateURLs(url.absoluteString) + } + + if let notes = event.notes { + return findAppropriateURLs(notes) + } + + return nil + } } struct CalendarInfo { @@ -353,4 +422,5 @@ struct EventInfo { let isEndDate: Bool let isAllDay: Bool let isSingleDay: Bool + let meetingURL: URL? } From fa71c58ff4301bdbabc75f051fc4b9e72fb1f76a Mon Sep 17 00:00:00 2001 From: Abhishek Date: Fri, 2 Jul 2021 13:13:50 -0500 Subject: [PATCH 2/4] Return EventInfo instead of EKEvent. --- Clocker/Events and Reminders/CalendarHandler.swift | 6 +++--- Clocker/Panel/ParentPanelController.swift | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Clocker/Events and Reminders/CalendarHandler.swift b/Clocker/Events and Reminders/CalendarHandler.swift index 2a01866..267ff2b 100644 --- a/Clocker/Events and Reminders/CalendarHandler.swift +++ b/Clocker/Events and Reminders/CalendarHandler.swift @@ -150,7 +150,7 @@ extension EventCenter { return menubarText } - func nextOccuring(_: [EventInfo]) -> EKEvent? { + func nextOccuring(_: [EventInfo]) -> EventInfo? { if calendarAccessDenied() || calendarAccessNotDetermined() { return nil } @@ -162,14 +162,14 @@ extension EventCenter { }.first if let firstEvent = filteredEvent { - return firstEvent.event + return firstEvent } let filteredAllDayEvent = relevantEvents.filter { $0.isAllDay }.first - return filteredAllDayEvent?.event + return filteredAllDayEvent } func initializeStoreIfNeccesary() { diff --git a/Clocker/Panel/ParentPanelController.swift b/Clocker/Panel/ParentPanelController.swift index d6ad916..afd1fe6 100644 --- a/Clocker/Panel/ParentPanelController.swift +++ b/Clocker/Panel/ParentPanelController.swift @@ -810,9 +810,9 @@ class ParentPanelController: NSWindowController { return } - self.calendarColorView.layer?.backgroundColor = upcomingEvent.calendar.color.cgColor - self.nextEventLabel.stringValue = upcomingEvent.title - self.nextEventLabel.toolTip = upcomingEvent.title + self.calendarColorView.layer?.backgroundColor = upcomingEvent.event.calendar.color.cgColor + self.nextEventLabel.stringValue = upcomingEvent.event.title + self.nextEventLabel.toolTip = upcomingEvent.event.title if upcomingEvent.isAllDay == true { let title = events.count == 1 ? "All-Day" : "All Day - Total \(events.count) events today" self.setCalendarButtonTitle(buttonTitle: title) @@ -822,7 +822,7 @@ class ParentPanelController: NSWindowController { return } - let timeSince = Date().timeAgo(since: upcomingEvent.startDate) + let timeSince = Date().timeAgo(since: upcomingEvent.event.startDate) let withoutAn = timeSince.replacingOccurrences(of: "an", with: CLEmptyString) let withoutAgo = withoutAn.replacingOccurrences(of: "ago", with: CLEmptyString) From fa7ca265d8bb425280b710321a315632449c7cab Mon Sep 17 00:00:00 2001 From: Abhishek Date: Fri, 2 Jul 2021 13:40:38 -0500 Subject: [PATCH 3/4] Meeting invites! --- .../CalendarHandler.swift | 43 +++++++++---------- Clocker/Overall App/Themer.swift | 13 +++++- Clocker/Panel/ParentPanelController.swift | 14 ++++++ 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/Clocker/Events and Reminders/CalendarHandler.swift b/Clocker/Events and Reminders/CalendarHandler.swift index 267ff2b..40e11b1 100644 --- a/Clocker/Events and Reminders/CalendarHandler.swift +++ b/Clocker/Events and Reminders/CalendarHandler.swift @@ -201,7 +201,7 @@ extension EventCenter { for event in events { if selectedCalendars.contains(event.event.calendar.calendarIdentifier) { if filteredEvents[date] == nil { - filteredEvents[date] = [] + filteredEvents[date] = Array() } filteredEvents[date]?.append(event) @@ -341,9 +341,9 @@ extension EventCenter { meetingURL: meetingURL) return eventInfo } - - static var dataDetector: NSDataDetector? = nil - + + static var dataDetector: NSDataDetector? + // Borrowing logic from Ityscal @discardableResult private func findAppropriateURLs(_ description: String) -> URL? { @@ -356,7 +356,7 @@ extension EventCenter { if actualLink.contains("zoom.us/j/") || actualLink.contains("zoom.us/s/") || actualLink.contains("zoom.us/w/") { // Create a Zoom App link let workspace = NSWorkspace.shared - if (workspace.urlForApplication(toOpen: URL(string: "zoommtg://")!) != nil) { + if workspace.urlForApplication(toOpen: URL(string: "zoommtg://")!) != nil { actualLink = actualLink.replacingOccurrences(of: "https://", with: "zoommtg://") actualLink = actualLink.replacingOccurrences(of: "?", with: "&") actualLink = actualLink.replacingOccurrences(of: "/j/", with: "/join?confno=") @@ -365,21 +365,20 @@ extension EventCenter { if let appLink = URL(string: actualLink) { return appLink } - } } else if actualLink.contains("zoommtg://") - || actualLink.contains("meet.google.com/") - || actualLink.contains("hangouts.google.com/") - || actualLink.contains("webex.com/") - || actualLink.contains("gotomeeting.com/join") - || actualLink.contains("ringcentral.com/j") - || actualLink.contains("bigbluebutton.org/gl") - || actualLink.contains("://bigbluebutton.") - || actualLink.contains("://bbb.") - || actualLink.contains("indigo.collocall.de") - || actualLink.contains("public.senfcall.de") - || actualLink.contains("youcanbook.me/zoom/") - || actualLink.contains("workplace.com/groupcall") { + || actualLink.contains("meet.google.com/") + || actualLink.contains("hangouts.google.com/") + || actualLink.contains("webex.com/") + || actualLink.contains("gotomeeting.com/join") + || actualLink.contains("ringcentral.com/j") + || actualLink.contains("bigbluebutton.org/gl") + || actualLink.contains("://bigbluebutton.") + || actualLink.contains("://bbb.") + || actualLink.contains("indigo.collocall.de") + || actualLink.contains("public.senfcall.de") + || actualLink.contains("youcanbook.me/zoom/") + || actualLink.contains("workplace.com/groupcall") { if let zoomLink = result.url { return zoomLink } @@ -388,21 +387,21 @@ extension EventCenter { } return nil } - + private func retrieveMeetingURL(_ event: EKEvent) -> URL? { if EventCenter.dataDetector == nil { // TODO: Handle Try-Catch gracefully EventCenter.dataDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) } - + if let location = event.location { return findAppropriateURLs(location) } - + if let url = event.url { return findAppropriateURLs(url.absoluteString) } - + if let notes = event.notes { return findAppropriateURLs(notes) } diff --git a/Clocker/Overall App/Themer.swift b/Clocker/Overall App/Themer.swift index c0ed7ce..e31a40c 100644 --- a/Clocker/Overall App/Themer.swift +++ b/Clocker/Overall App/Themer.swift @@ -187,8 +187,8 @@ extension Themer { return themeIndex == .light - ? NSImage(named: NSImage.Name("Settings"))! - : NSImage(named: NSImage.Name("Settings-White"))! + ? NSImage(named: NSImage.Name("Settings"))! + : NSImage(named: NSImage.Name("Settings-White"))! } func pinImage() -> NSImage { @@ -442,6 +442,15 @@ extension Themer { NSColor(deviceRed: 42.0 / 255.0, green: 55.0 / 255.0, blue: 62.0 / 255.0, alpha: 1.0) } + func videoCallImage() -> NSImage? { + if #available(macOS 11.0, *) { + let symbolConfig = NSImage.SymbolConfiguration(pointSize: 20, weight: .regular) + return symbolImage(for: "video.circle.fill")?.withSymbolConfiguration(symbolConfig) + } else { + return nil + } + } + func symbolImage(for name: String) -> NSImage? { assert(name.isEmpty == false) diff --git a/Clocker/Panel/ParentPanelController.swift b/Clocker/Panel/ParentPanelController.swift index afd1fe6..6bd30aa 100644 --- a/Clocker/Panel/ParentPanelController.swift +++ b/Clocker/Panel/ParentPanelController.swift @@ -696,6 +696,16 @@ class ParentPanelController: NSWindowController { func removeUpcomingEventView() { OperationQueue.main.addOperation { + let eventCenter = EventCenter.sharedCenter() + let now = Date() + if let events = eventCenter.eventsForDate[NSCalendar.autoupdatingCurrent.startOfDay(for: now)], events.isEmpty == false { + guard let upcomingEvent = eventCenter.nextOccuring(events), let meetingLink = upcomingEvent.meetingURL else { + return + } + NSWorkspace.shared.open(meetingLink) + return + } + if self.stackView.arrangedSubviews.contains(self.upcomingEventView!), self.upcomingEventView?.isHidden == false { self.upcomingEventView?.isHidden = true UserDefaults.standard.set("NO", forKey: CLShowUpcomingEventView) @@ -827,6 +837,10 @@ class ParentPanelController: NSWindowController { let withoutAgo = withoutAn.replacingOccurrences(of: "ago", with: CLEmptyString) self.setCalendarButtonTitle(buttonTitle: "in \(withoutAgo.lowercased())") + + if upcomingEvent.meetingURL != nil { + self.whiteRemoveButton.image = Themer.shared().videoCallImage() + } if #available(OSX 10.14, *) { PerfLogger.endMarker("Fetch Calendar Events") From 1e418124fbc23b1df659a9a19a7850bf3be4d279 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Fri, 2 Jul 2021 13:48:33 -0500 Subject: [PATCH 4/4] Dispatch queue for refetching events. --- Clocker/Events and Reminders/EventCenter.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Clocker/Events and Reminders/EventCenter.swift b/Clocker/Events and Reminders/EventCenter.swift index f07fb6b..5bc40c7 100644 --- a/Clocker/Events and Reminders/EventCenter.swift +++ b/Clocker/Events and Reminders/EventCenter.swift @@ -16,6 +16,8 @@ class EventCenter: NSObject { var eventsForDate: [Date: [EventInfo]] = [:] var filteredEvents: [Date: [EventInfo]] = [:] + + private let fetchQueue = DispatchQueue(label: "com.abhishek.fetch") @discardableResult class func sharedCenter() -> EventCenter { return shared @@ -41,11 +43,16 @@ class EventCenter: NSObject { private func refetchAll() { Logger.info("\nRefetching events from the store") + eventsForDate = [:] filteredEvents = [:] + autoreleasepool { + fetchQueue.async { + // We get events for a 120 day period. + // If the user uses a calendar often, this will be called frequently + self.fetchEvents(-40, 80) + } + } - // We get events for a 120 day period. - // If the user uses a calendar often, this will be called frequently - fetchEvents(-40, 80) } }