diff --git a/Clocker/Events and Reminders/CalendarHandler.swift b/Clocker/Events and Reminders/CalendarHandler.swift index 846bb3e..40e11b1 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() { @@ -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) @@ -332,14 +332,82 @@ 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? + + // 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 +421,5 @@ struct EventInfo { let isEndDate: Bool let isAllDay: Bool let isSingleDay: Bool + let meetingURL: URL? } 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) } } 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 e09c59d..3a42410 100644 --- a/Clocker/Panel/ParentPanelController.swift +++ b/Clocker/Panel/ParentPanelController.swift @@ -698,6 +698,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) @@ -812,9 +822,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) @@ -824,11 +834,15 @@ 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) 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")