From 75478da2140c446a5ea1b6b2a8802a2dc1002684 Mon Sep 17 00:00:00 2001 From: Jens Goldhammer Date: Wed, 6 Jan 2021 20:44:33 +0100 Subject: [PATCH 1/5] feat(create-meeting): custom url for new meetings - allow the user to choose a custom url in the preferences to create a new meeting. The url is opened in the default browser currently --- MeetingBar/AppDelegate.swift | 17 +++++++++ MeetingBar/Constants.swift | 1 + MeetingBar/DefaultsKeys.swift | 4 ++ MeetingBar/PreferencesView.swift | 49 +++++++++++++++++-------- MeetingBar/StatusBarItemControler.swift | 2 - 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/MeetingBar/AppDelegate.swift b/MeetingBar/AppDelegate.swift index 09940daf..bd61e0f8 100644 --- a/MeetingBar/AppDelegate.swift +++ b/MeetingBar/AppDelegate.swift @@ -337,6 +337,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele openMeetingURL(nil, CreateMeetingLinks.outlook_office365) case .outlook_live: openMeetingURL(nil, CreateMeetingLinks.outlook_live) + case .url: + var url: String = Defaults[.createMeetingServiceUrl] + + if !url.isEmpty, let checkedUrl = NSURL(string: url) { + if !url.starts(with: "http://") && !url.starts(with: "https://") { + url = "https://" + url + } + + openMeetingURL(nil, URL(string: url)!) + } else { + let createUrlAlert = NSAlert() + createUrlAlert.messageText = "Cannot create new meeting" + createUrlAlert.informativeText = "The custom url \(url) is missing or not valid. Please enter a custom url in the app preferences." + createUrlAlert.alertStyle = NSAlert.Style.informational + createUrlAlert.addButton(withTitle: "OK") + createUrlAlert.runModal() + } } } diff --git a/MeetingBar/Constants.swift b/MeetingBar/Constants.swift index 0e50b2c9..ca62180b 100644 --- a/MeetingBar/Constants.swift +++ b/MeetingBar/Constants.swift @@ -72,6 +72,7 @@ enum CreateMeetingServices: String, Codable, CaseIterable { case gcalendar = "Google Calendar" case outlook_live = "Outlook Live" case outlook_office365 = "Outlook Office365" + case url = "Custom url" } enum Links { diff --git a/MeetingBar/DefaultsKeys.swift b/MeetingBar/DefaultsKeys.swift index cc9be7a1..8010450c 100644 --- a/MeetingBar/DefaultsKeys.swift +++ b/MeetingBar/DefaultsKeys.swift @@ -78,6 +78,10 @@ extension Defaults.Keys { // Integrations static let createMeetingService = Key("createMeetingService", default: .zoom) + + // custom url to create meetings + static let createMeetingServiceUrl = Key("createMeetingServiceUrl", default: "") + static let useChromeForMeetLinks = Key("useChromeForMeetLinks", default: .defaultBrowser) static let useChromeForHangoutsLinks = Key("useChromeForHangoutsLinks", default: .defaultBrowser) static let useAppForZoomLinks = Key("useAppForZoomLinks", default: false) diff --git a/MeetingBar/PreferencesView.swift b/MeetingBar/PreferencesView.swift index af2ff2a4..577ad419 100644 --- a/MeetingBar/PreferencesView.swift +++ b/MeetingBar/PreferencesView.swift @@ -429,7 +429,6 @@ struct Configuration: View { }.foregroundColor(.gray).font(.system(size: 12)).padding(.horizontal, 10) Divider() HStack { - Text("Create meetings in").frame(width: 150, alignment: .leading) CreateMeetingServicePicker() }.padding(.horizontal, 10) Spacer() @@ -437,6 +436,40 @@ struct Configuration: View { } } +struct CreateMeetingServicePicker: View { + @Default(.createMeetingService) var createMeetingService + @Default(.createMeetingServiceUrl) var createMeetingServiceUrl + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text("Create meetings via").frame(width: 150, alignment: .leading) + Picker(selection: $createMeetingService, label: Text("")) { + Text(CreateMeetingServices.meet.rawValue).tag(CreateMeetingServices.meet) + Text(CreateMeetingServices.zoom.rawValue).tag(CreateMeetingServices.zoom) + Text(CreateMeetingServices.teams.rawValue).tag(CreateMeetingServices.teams) + Text(CreateMeetingServices.hangouts.rawValue).tag(CreateMeetingServices.hangouts) + Text(CreateMeetingServices.gcalendar.rawValue).tag(CreateMeetingServices.gcalendar) + Text(CreateMeetingServices.outlook_live.rawValue).tag(CreateMeetingServices.outlook_live) + Text(CreateMeetingServices.outlook_office365.rawValue).tag(CreateMeetingServices.outlook_office365) + Text(CreateMeetingServices.url.rawValue).tag(CreateMeetingServices.url) + }.labelsHidden() + } + + + if createMeetingService == CreateMeetingServices.url { + HStack { + Text("Custom url").frame(width: 150, alignment: .leading) + TextField("Please enter a valid url", text: $createMeetingServiceUrl).textFieldStyle(RoundedBorderTextFieldStyle()) + } + HStack { + Text("Tip: Google Meet supports choosing account via parameter, e.g. https://meet.google.com/new?authuser=1").foregroundColor(.gray).font(.system(size: 12)) + } + } + } + } +} + struct Bookmark: View { @Default(.bookmarkMeetingName) var bookmarkMeetingName @Default(.bookmarkMeetingURL) var bookmarkMeetingURL @@ -621,21 +654,7 @@ struct Bookmark: View { -struct CreateMeetingServicePicker: View { - @Default(.createMeetingService) var createMeetingService - var body: some View { - Picker(selection: $createMeetingService, label: Text("")) { - Text(CreateMeetingServices.meet.rawValue).tag(CreateMeetingServices.meet) - Text(CreateMeetingServices.zoom.rawValue).tag(CreateMeetingServices.zoom) - Text(CreateMeetingServices.teams.rawValue).tag(CreateMeetingServices.teams) - Text(CreateMeetingServices.hangouts.rawValue).tag(CreateMeetingServices.hangouts) - Text(CreateMeetingServices.gcalendar.rawValue).tag(CreateMeetingServices.gcalendar) - Text(CreateMeetingServices.outlook_live.rawValue).tag(CreateMeetingServices.outlook_live) - Text(CreateMeetingServices.outlook_office365.rawValue).tag(CreateMeetingServices.outlook_office365) - }.labelsHidden() - } -} struct JoinEventNotificationPicker: View { @Default(.joinEventNotification) var joinEventNotification diff --git a/MeetingBar/StatusBarItemControler.swift b/MeetingBar/StatusBarItemControler.swift index 9a91341d..740f5918 100644 --- a/MeetingBar/StatusBarItemControler.swift +++ b/MeetingBar/StatusBarItemControler.swift @@ -277,7 +277,6 @@ class StatusBarItemControler: NSObject, NSMenuDelegate { withTitle: dateTitle, action: nil, keyEquivalent: "" - ) titleItem.attributedTitle = NSAttributedString(string: dateTitle, attributes: [NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 13)]) titleItem.isEnabled = false @@ -291,7 +290,6 @@ class StatusBarItemControler: NSObject, NSMenuDelegate { withTitle: "Nothing for \(title.lowercased())", action: nil, keyEquivalent: "" - ) item.isEnabled = false } From 4ebf0a0f0bab2a879272ba16b97eca8da23dd594 Mon Sep 17 00:00:00 2001 From: Jens Goldhammer Date: Fri, 8 Jan 2021 22:26:10 +0100 Subject: [PATCH 2/5] feat(notifications): display notifications or alerts - depending on the notification settings of the user, we will display an information via notifications or NSAlerts. - when notifications are enabled, we will send macos notifications - when notifications are disabled, we will send a NSAlert. Testable with an empty custom url to create a new meeting or join the next event when there is no event to join --- MeetingBar/AppDelegate.swift | 27 ++++++++------ MeetingBar/Notifications.swift | 64 +++++++++++++++++++++++++++++--- MeetingBar/PreferencesView.swift | 2 +- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/MeetingBar/AppDelegate.swift b/MeetingBar/AppDelegate.swift index bd61e0f8..dc7c2441 100644 --- a/MeetingBar/AppDelegate.swift +++ b/MeetingBar/AppDelegate.swift @@ -213,7 +213,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele } bookmarkObserver = Defaults.observe(keys: .bookmarkMeetingURL, .bookmarkMeetingURL2, .bookmarkMeetingURL3, .bookmarkMeetingURL4, .bookmarkMeetingURL5, .bookmarkMeetingName, .bookmarkMeetingName2, .bookmarkMeetingName3, .bookmarkMeetingName4, .bookmarkMeetingName5, .bookmarkMeetingService, .bookmarkMeetingService2, .bookmarkMeetingService3, .bookmarkMeetingService4, .bookmarkMeetingService5) { - self.statusBarItem.updateMenu() + self.statusBarItem.updateMenu() } eventTitleFormatObserver = Defaults.observe(.eventTitleFormat) { change in @@ -297,6 +297,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele completion(NSBackgroundActivityScheduler.Result.finished) } } + + /** + * implementation is necessary to show notifications even when the app has focus! + */ + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.alert, .badge, .sound]) } + internal func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { switch response.actionIdentifier { case "JOIN_ACTION", UNNotificationDefaultActionIdentifier: @@ -339,20 +346,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele openMeetingURL(nil, CreateMeetingLinks.outlook_live) case .url: var url: String = Defaults[.createMeetingServiceUrl] + let checkedUrl = NSURL(string: url) - if !url.isEmpty, let checkedUrl = NSURL(string: url) { - if !url.starts(with: "http://") && !url.starts(with: "https://") { - url = "https://" + url - } - + if !url.isEmpty && checkedUrl != nil { openMeetingURL(nil, URL(string: url)!) } else { - let createUrlAlert = NSAlert() - createUrlAlert.messageText = "Cannot create new meeting" - createUrlAlert.informativeText = "The custom url \(url) is missing or not valid. Please enter a custom url in the app preferences." - createUrlAlert.alertStyle = NSAlert.Style.informational - createUrlAlert.addButton(withTitle: "OK") - createUrlAlert.runModal() + if !url.isEmpty { + url += " " + } + + sendNotification("Cannot create new meeeting", "Custom url \(url)is missing or invalid. Please enter a value in the app preferences.") } } } diff --git a/MeetingBar/Notifications.swift b/MeetingBar/Notifications.swift index e5791ecc..f8aa2369 100644 --- a/MeetingBar/Notifications.swift +++ b/MeetingBar/Notifications.swift @@ -7,7 +7,7 @@ // import EventKit import UserNotifications - +import AppKit import Defaults func requestNotificationAuthorization() { @@ -37,8 +37,12 @@ func registerNotificationCategories() { notificationCenter.setNotificationCategories([eventCategory]) } -func sendNotification(_ title: String, _ text: String) { - requestNotificationAuthorization() // By the apple best practices +/** + * displays a notification for the given title and text immediately -> trigger is nil + */ +func displayNotification(_ title: String, _ text: String) { + requestNotificationAuthorization() + // By the apple best practices NSLog("Send notification: \(title) - \(text)") let center = UNUserNotificationCenter.current() @@ -47,11 +51,61 @@ func sendNotification(_ title: String, _ text: String) { content.title = title content.body = text - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) center.add(request) } +/** + * check whether the notifications for meetingbar are enabled and alert or banner style is enabled. + * in this case the method will return true, otherwise false. + * + */ +func notificationsEnabled() -> Bool { + let center = UNUserNotificationCenter.current() + let group = DispatchGroup() + group.enter() + + var correctAlertStyle = false + var notificationsEnabled = false + + center.getNotificationSettings { notificationSettings in + correctAlertStyle = notificationSettings.alertStyle == UNAlertStyle.alert || notificationSettings.alertStyle == UNAlertStyle.banner + notificationsEnabled = notificationSettings.authorizationStatus == UNAuthorizationStatus.authorized + group.leave() + } + + group.wait() + return correctAlertStyle && notificationsEnabled +} + +/** + * sends a notification to the user. + */ +func sendNotification(_ title: String, _ text: String) { + requestNotificationAuthorization() // By the apple best practices + + if notificationsEnabled() { + displayNotification(title, text) + } else { + displayAlert(title: title, text: text) + } +} + +/** + * adds an alert for the user- we will only use NSAlert if the user has switched off notifications + */ +func displayAlert(title: String, text: String) { + NSLog("Display alert: \(title) - \(text)") + + let userAlert = NSAlert() + userAlert.messageText = title + userAlert.informativeText = text + userAlert.alertStyle = NSAlert.Style.informational + userAlert.addButton(withTitle: "OK") + + userAlert.runModal() +} + func scheduleEventNotification(_ event: EKEvent) { requestNotificationAuthorization() // By the apple best practices diff --git a/MeetingBar/PreferencesView.swift b/MeetingBar/PreferencesView.swift index 577ad419..c048a222 100644 --- a/MeetingBar/PreferencesView.swift +++ b/MeetingBar/PreferencesView.swift @@ -460,7 +460,7 @@ struct CreateMeetingServicePicker: View { if createMeetingService == CreateMeetingServices.url { HStack { Text("Custom url").frame(width: 150, alignment: .leading) - TextField("Please enter a valid url", text: $createMeetingServiceUrl).textFieldStyle(RoundedBorderTextFieldStyle()) + TextField("Please enter a valid url (with the url scheme, e.g. https://)", text: $createMeetingServiceUrl).textFieldStyle(RoundedBorderTextFieldStyle()) } HStack { Text("Tip: Google Meet supports choosing account via parameter, e.g. https://meet.google.com/new?authuser=1").foregroundColor(.gray).font(.system(size: 12)) From a93e270af0a56a475603d38722a307c4c01c035f Mon Sep 17 00:00:00 2001 From: Jens Goldhammer Date: Sat, 9 Jan 2021 22:17:05 +0100 Subject: [PATCH 3/5] add DevTeam override config file --- MeetingBar.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MeetingBar.xcodeproj/project.pbxproj b/MeetingBar.xcodeproj/project.pbxproj index 6d476c86..93419efa 100644 --- a/MeetingBar.xcodeproj/project.pbxproj +++ b/MeetingBar.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ 40DC7053250E5E1000217DD9 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; 40E60312250E81EA005986C7 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; E249D533259BBC3800429BF1 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + E2AE715525AA54A600D3C7CF /* DevTeamOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DevTeamOverride.xcconfig; sourceTree = ""; }; E4DD0F0A2529D0D20029E395 /* Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Release.xcconfig"; sourceTree = ""; }; E4DD0F0B2529D0D20029E395 /* Project.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Project.xcconfig; sourceTree = ""; }; E4DD0F0C2529D0D20029E395 /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; @@ -175,6 +176,7 @@ E4DD0F092529D0D20029E395 /* XCConfig */ = { isa = PBXGroup; children = ( + E2AE715525AA54A600D3C7CF /* DevTeamOverride.xcconfig */, E4DD0F0B2529D0D20029E395 /* Project.xcconfig */, E4DD0F0A2529D0D20029E395 /* Project-Release.xcconfig */, E4DD0F0C2529D0D20029E395 /* Project-Debug.xcconfig */, From 6b7b278178d59d122e3786db79d9ac5960b7f383 Mon Sep 17 00:00:00 2001 From: Jens Goldhammer Date: Sat, 9 Jan 2021 22:54:46 +0100 Subject: [PATCH 4/5] fix code build --- MeetingBar/Notifications.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MeetingBar/Notifications.swift b/MeetingBar/Notifications.swift index 41ac7fab..a769d457 100644 --- a/MeetingBar/Notifications.swift +++ b/MeetingBar/Notifications.swift @@ -52,7 +52,7 @@ func registerNotificationCategories() { func sendNotification(title: String, text: String, subtitle: String = "") { requestNotificationAuthorization() // By the apple best practices - NSLog("Send notification: \(title) - \(text)") + NSLog("Send notification: \(title) - \(text) - \(subtitle)") let center = UNUserNotificationCenter.current() let content = UNMutableNotificationContent() @@ -62,8 +62,8 @@ func sendNotification(title: String, text: String, subtitle: String = "") { content.subtitle = subtitle } - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + center.add(request) { error in if let error = error { NSLog("%@", "request \(request) could not be added because of error \(error)") @@ -102,7 +102,7 @@ func sendNotification(_ title: String, _ text: String) { requestNotificationAuthorization() // By the apple best practices if notificationsEnabled() { - displayNotification(title, text) + sendNotification(title, text) } else { displayAlert(title: title, text: text) } From ec1072133bb90b2c7a9c02e478ba46189cc49596 Mon Sep 17 00:00:00 2001 From: Jens Goldhammer Date: Sat, 9 Jan 2021 22:55:45 +0100 Subject: [PATCH 5/5] change development team and code signing settings to allow developer overrides --- MeetingBar.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/MeetingBar.xcodeproj/project.pbxproj b/MeetingBar.xcodeproj/project.pbxproj index 93419efa..d22975b2 100644 --- a/MeetingBar.xcodeproj/project.pbxproj +++ b/MeetingBar.xcodeproj/project.pbxproj @@ -478,12 +478,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = MeetingBar/MeetingBar.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 108; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = KGH289N6T8; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = MeetingBar/Info.plist; @@ -505,12 +505,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = MeetingBar/MeetingBar.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 108; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = KGH289N6T8; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = MeetingBar/Info.plist; @@ -532,10 +532,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoLauncher/AutoLauncher.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = KGH289N6T8; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = AutoLauncher/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -555,10 +555,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoLauncher/AutoLauncher.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = KGH289N6T8; + DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = AutoLauncher/Info.plist; LD_RUNPATH_SEARCH_PATHS = (