diff --git a/Classes/Systems/AppRouter/AppController+SetupRoutes.swift b/Classes/Systems/AppRouter/AppController+SetupRoutes.swift index af715f5be..79d565d51 100644 --- a/Classes/Systems/AppRouter/AppController+SetupRoutes.swift +++ b/Classes/Systems/AppRouter/AppController+SetupRoutes.swift @@ -14,6 +14,7 @@ extension AppController { register(route: BookmarkShortcutRoute.self) register(route: SwitchAccountShortcutRoute.self) register(route: SearchShortcutRoute.self) + register(route: IssueNotificationRoute.self) } } diff --git a/Classes/Systems/AppRouter/AppController+UNUserNotificationCenterDelegate.swift b/Classes/Systems/AppRouter/AppController+UNUserNotificationCenterDelegate.swift new file mode 100644 index 000000000..7b37becb1 --- /dev/null +++ b/Classes/Systems/AppRouter/AppController+UNUserNotificationCenterDelegate.swift @@ -0,0 +1,43 @@ +// +// AppController+UNUserNotificationCenterDelegate.swift +// Freetime +// +// Created by Ryan Nystrom on 10/9/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import UserNotifications + +extension AppController: UNUserNotificationCenterDelegate { + + func attachNotificationDelegate() { + UNUserNotificationCenter.current().delegate = self + } + + // MARK: UNUserNotificationCenterDelegate + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + completionHandler([.alert, .sound]) + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + switch response.actionIdentifier { + case UNNotificationDismissActionIdentifier: break + case UNNotificationDefaultActionIdentifier: + if let (path, params) = response.notification.request.content.routableUserInfo { + handle(path: path, params: params) + } + default: print(response.actionIdentifier) + } + completionHandler() + } + +} diff --git a/Classes/Systems/AppRouter/AppController.swift b/Classes/Systems/AppRouter/AppController.swift index ca0c45723..c4c333389 100644 --- a/Classes/Systems/AppRouter/AppController.swift +++ b/Classes/Systems/AppRouter/AppController.swift @@ -10,7 +10,7 @@ import UIKit import GitHubSession import GitHubAPI -final class AppController: LoginSplashViewControllerDelegate, GitHubSessionListener { +final class AppController: NSObject, LoginSplashViewControllerDelegate, GitHubSessionListener { private var splitViewController: AppSplitViewController! private let sessionManager = GitHubSessionManager() @@ -20,7 +20,9 @@ final class AppController: LoginSplashViewControllerDelegate, GitHubSessionListe private var watchAppSync: WatchAppUserSessionSync? private var routes = [String: (Routable & RoutePerformable).Type]() - init() { + override init() { + super.init() + attachNotificationDelegate() sessionManager.addListener(listener: self) } @@ -67,11 +69,13 @@ final class AppController: LoginSplashViewControllerDelegate, GitHubSessionListe @discardableResult func handle(path: String, params: [String: String]) -> Bool { guard let routeType = routes[path], - let route = routeType.from(params: params) + let route = routeType.from(params: params), + let client = appClient else { return false } return route.perform( sessionManager: sessionManager, - splitViewController: splitViewController + splitViewController: splitViewController, + client: client ) } diff --git a/Classes/Systems/AppRouter/BookmarkShortcutRoute.swift b/Classes/Systems/AppRouter/BookmarkShortcutRoute.swift index 1033f084e..80630b83d 100644 --- a/Classes/Systems/AppRouter/BookmarkShortcutRoute.swift +++ b/Classes/Systems/AppRouter/BookmarkShortcutRoute.swift @@ -21,7 +21,8 @@ struct BookmarkShortcutRoute: Routable { extension BookmarkShortcutRoute: RoutePerformable { func perform( sessionManager: GitHubSessionManager, - splitViewController: AppSplitViewController + splitViewController: AppSplitViewController, + client: GithubClient ) -> Bool { return splitViewController.masterTabBarController?.selectTab(of: BookmarkViewController.self) != nil } diff --git a/Classes/Systems/AppRouter/IssueNotificationRoute.swift b/Classes/Systems/AppRouter/IssueNotificationRoute.swift new file mode 100644 index 000000000..558ff6c9a --- /dev/null +++ b/Classes/Systems/AppRouter/IssueNotificationRoute.swift @@ -0,0 +1,46 @@ +// +// IssueNotificationRoute.swift +// Freetime +// +// Created by Ryan Nystrom on 10/9/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation +import GitHubSession + +struct IssueNotificationRoute: Routable { + let owner: String + let repo: String + let number: Int + static func from(params: [String : String]) -> IssueNotificationRoute? { + guard let owner = params["owner"], + let repo = params["repo"], + let number = (params["number"] as NSString?)?.integerValue + else { return nil } + return IssueNotificationRoute(owner: owner, repo: repo, number: number) + } + static var path: String { + return "com.githawk.issue-notifications" + } + var encoded: [String : String] { + return [ + "owner": owner, + "repo": repo, + "number": "\(number)" + ] + } +} + +extension IssueNotificationRoute: RoutePerformable { + func perform( + sessionManager: GitHubSessionManager, + splitViewController: AppSplitViewController, + client: GithubClient + ) -> Bool { + let model = IssueDetailsModel(owner: owner, repo: repo, number: number) + let controller = IssuesViewController(client: client, model: model, scrollToBottom: true) + splitViewController.showDetailViewController(controller, sender: nil) + return true + } +} diff --git a/Classes/Systems/AppRouter/RoutePerformable.swift b/Classes/Systems/AppRouter/RoutePerformable.swift index 30902cf7c..25298b630 100644 --- a/Classes/Systems/AppRouter/RoutePerformable.swift +++ b/Classes/Systems/AppRouter/RoutePerformable.swift @@ -13,6 +13,7 @@ protocol RoutePerformable { @discardableResult func perform( sessionManager: GitHubSessionManager, - splitViewController: AppSplitViewController + splitViewController: AppSplitViewController, + client: GithubClient ) -> Bool } diff --git a/Classes/Systems/AppRouter/SearchShortcutRoute.swift b/Classes/Systems/AppRouter/SearchShortcutRoute.swift index d3501a328..1d04d4c81 100644 --- a/Classes/Systems/AppRouter/SearchShortcutRoute.swift +++ b/Classes/Systems/AppRouter/SearchShortcutRoute.swift @@ -21,7 +21,8 @@ struct SearchShortcutRoute: Routable { extension SearchShortcutRoute: RoutePerformable { func perform( sessionManager: GitHubSessionManager, - splitViewController: AppSplitViewController + splitViewController: AppSplitViewController, + client: GithubClient ) -> Bool { guard let controller = splitViewController.masterTabBarController?.selectTab(of: SearchViewController.self) else { return false } diff --git a/Classes/Systems/AppRouter/SwitchAccountShortcutRoute.swift b/Classes/Systems/AppRouter/SwitchAccountShortcutRoute.swift index 35394c84c..b472206dd 100644 --- a/Classes/Systems/AppRouter/SwitchAccountShortcutRoute.swift +++ b/Classes/Systems/AppRouter/SwitchAccountShortcutRoute.swift @@ -26,7 +26,8 @@ struct SwitchAccountShortcutRoute: Routable { extension SwitchAccountShortcutRoute: RoutePerformable { func perform( sessionManager: GitHubSessionManager, - splitViewController: AppSplitViewController + splitViewController: AppSplitViewController, + client: GithubClient ) -> Bool { let userSessions = sessionManager.userSessions guard let needle = userSessions.first(where: { username == $0.username }) diff --git a/Classes/Systems/AppRouter/UNMutableNotificationContent+Routable.swift b/Classes/Systems/AppRouter/UNMutableNotificationContent+Routable.swift new file mode 100644 index 000000000..f1e8355c1 --- /dev/null +++ b/Classes/Systems/AppRouter/UNMutableNotificationContent+Routable.swift @@ -0,0 +1,20 @@ +// +// UNMutableNotificationContent+Routable.swift +// Freetime +// +// Created by Ryan Nystrom on 10/9/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import UserNotifications + +let UNNotificationContentRoutePathKey = "path" + +extension UNMutableNotificationContent { + + func set(route: T) { + userInfo[UNNotificationContentRoutePathKey] = T.path + route.encoded.forEach { userInfo[$0] = $1 } + } + +} diff --git a/Classes/Systems/AppRouter/UNNotificationContent+Routable.swift b/Classes/Systems/AppRouter/UNNotificationContent+Routable.swift new file mode 100644 index 000000000..fab29c662 --- /dev/null +++ b/Classes/Systems/AppRouter/UNNotificationContent+Routable.swift @@ -0,0 +1,26 @@ +// +// UNNotificationContent+Routable.swift +// Freetime +// +// Created by Ryan Nystrom on 10/9/18. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import UserNotifications + +extension UNNotificationContent { + + var routableUserInfo: (path: String, params: [String: String])? { + guard let path = userInfo[UNNotificationContentRoutePathKey] as? String else { return nil } + var params = [String: String]() + userInfo.forEach { + guard let key = $0 as? String, + let value = $1 as? String, + value != UNNotificationContentRoutePathKey + else { return } + params[key] = value + } + return (path, params) + } + +} diff --git a/Classes/Systems/BadgeNotifications.swift b/Classes/Systems/BadgeNotifications.swift index b6cb00e74..3dbd1b5e9 100644 --- a/Classes/Systems/BadgeNotifications.swift +++ b/Classes/Systems/BadgeNotifications.swift @@ -134,6 +134,16 @@ final class BadgeNotifications { content.body = $0.subject.title content.subtitle = "\(type.localizedString) \(identifier.string)" + // currently only handling issues + if let identifier = $0.subject.identifier, + case .number(let n) = identifier { + content.set(route: IssueNotificationRoute( + owner: $0.repository.owner.login, + repo: $0.repository.name, + number: n + )) + } + let request = UNNotificationRequest( identifier: $0.id, content: content, diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index 4210f975a..abf07102e 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -301,6 +301,10 @@ 29A08FBD1F12EF7C00C5368E /* IssueReferencedCommitCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A08FBA1F12EF7C00C5368E /* IssueReferencedCommitCell.swift */; }; 29A08FBE1F12EF7C00C5368E /* IssueReferencedCommitModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A08FBB1F12EF7C00C5368E /* IssueReferencedCommitModel.swift */; }; 29A08FBF1F12EF7C00C5368E /* IssueReferencedCommitSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A08FBC1F12EF7C00C5368E /* IssueReferencedCommitSectionController.swift */; }; + 29A1053F216D9062004734A0 /* UNNotificationContent+Routable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A1053E216D9062004734A0 /* UNNotificationContent+Routable.swift */; }; + 29A10541216D912F004734A0 /* IssueNotificationRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A10540216D912F004734A0 /* IssueNotificationRoute.swift */; }; + 29A10543216D9381004734A0 /* UNMutableNotificationContent+Routable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A10542216D9381004734A0 /* UNMutableNotificationContent+Routable.swift */; }; + 29A10545216D9515004734A0 /* AppController+UNUserNotificationCenterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A10544216D9515004734A0 /* AppController+UNUserNotificationCenterDelegate.swift */; }; 29A195021EC66B8B00C3E289 /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A195011EC66B8B00C3E289 /* UIColor+Hex.swift */; }; 29A195071EC7601000C3E289 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 29A195061EC7601000C3E289 /* Localizable.stringsdict */; }; 29A195081EC7602500C3E289 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 29A195061EC7601000C3E289 /* Localizable.stringsdict */; }; @@ -837,6 +841,10 @@ 29A08FBA1F12EF7C00C5368E /* IssueReferencedCommitCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueReferencedCommitCell.swift; sourceTree = ""; }; 29A08FBB1F12EF7C00C5368E /* IssueReferencedCommitModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueReferencedCommitModel.swift; sourceTree = ""; }; 29A08FBC1F12EF7C00C5368E /* IssueReferencedCommitSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IssueReferencedCommitSectionController.swift; sourceTree = ""; }; + 29A1053E216D9062004734A0 /* UNNotificationContent+Routable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotificationContent+Routable.swift"; sourceTree = ""; }; + 29A10540216D912F004734A0 /* IssueNotificationRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueNotificationRoute.swift; sourceTree = ""; }; + 29A10542216D9381004734A0 /* UNMutableNotificationContent+Routable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNMutableNotificationContent+Routable.swift"; sourceTree = ""; }; + 29A10544216D9515004734A0 /* AppController+UNUserNotificationCenterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppController+UNUserNotificationCenterDelegate.swift"; sourceTree = ""; }; 29A195011EC66B8B00C3E289 /* UIColor+Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Hex.swift"; sourceTree = ""; }; 29A195061EC7601000C3E289 /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; 29A195091EC78B4800C3E289 /* NotificationType+Icon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NotificationType+Icon.swift"; sourceTree = ""; }; @@ -1096,13 +1104,17 @@ isa = PBXGroup; children = ( 290CA7632169799600DE04F8 /* AppController.swift */, + 290CA779216AFC1300DE04F8 /* AppController+SetupRoutes.swift */, + 29A10544216D9515004734A0 /* AppController+UNUserNotificationCenterDelegate.swift */, 290CA76521697A7900DE04F8 /* AppSplitViewController.swift */, 290CA773216AE94D00DE04F8 /* BookmarkShortcutRoute.swift */, + 29A10540216D912F004734A0 /* IssueNotificationRoute.swift */, 290CA76D216AE8FA00DE04F8 /* Routable.swift */, - 290CA779216AFC1300DE04F8 /* AppController+SetupRoutes.swift */, 290CA777216AFAE600DE04F8 /* RoutePerformable.swift */, 290CA769216AC82700DE04F8 /* SearchShortcutRoute.swift */, 290CA771216AE93E00DE04F8 /* SwitchAccountShortcutRoute.swift */, + 29A10542216D9381004734A0 /* UNMutableNotificationContent+Routable.swift */, + 29A1053E216D9062004734A0 /* UNNotificationContent+Routable.swift */, ); path = AppRouter; sourceTree = ""; @@ -2816,6 +2828,7 @@ 2967DC56211751CB00FD3683 /* UIContentSizeCategory+Preferred.swift in Sources */, 7BBFEE5B1F8A8A0400C68E47 /* SearchBarSectionController.swift in Sources */, 292FCAFA1EDFCC510026635E /* IssueCommentDetailsViewModel.swift in Sources */, + 29A10541216D912F004734A0 /* IssueNotificationRoute.swift in Sources */, 2949674E1EF9719300B1CF1A /* IssueCommentHrCell.swift in Sources */, 2949674C1EF9716400B1CF1A /* IssueCommentHrModel.swift in Sources */, 294967531EFC1EDB00B1CF1A /* IssueCommentHtmlCell.swift in Sources */, @@ -2970,6 +2983,7 @@ 29DA1E801F5DF2960050C64B /* LoadMoreSectionController.swift in Sources */, 29416BFB1F113D0A00D03E1A /* LoginSplashViewController.swift in Sources */, 29DA1E8C1F5F8CC40050C64B /* MarkdownAttribute.swift in Sources */, + 29A10545216D9515004734A0 /* AppController+UNUserNotificationCenterDelegate.swift in Sources */, 29351E9C2079106300FF8C17 /* String+GitHubEmoji.swift in Sources */, 98647DF31F758CCF00A4DE7A /* NewIssueTableViewController.swift in Sources */, 290EF5761F06BA06006A2160 /* NoNewNotificationsCell.swift in Sources */, @@ -3020,6 +3034,7 @@ 2924C18820D5B2F200FCFCFF /* PeopleSectionController.swift in Sources */, 292ACE181F5C945B00C9A02C /* RepositoryIssueSummaryModel.swift in Sources */, DCA5ED1B1FAEF78B0072F074 /* BookmarkSectionController.swift in Sources */, + 29A1053F216D9062004734A0 /* UNNotificationContent+Routable.swift in Sources */, 29693EE520FAA05F00336200 /* IssueAutocomplete.swift in Sources */, 986B87341F2CAE9800AAB55C /* RepositoryIssueSummaryType.swift in Sources */, 2905AFAD1F7357C50015AE32 /* RepositoryIssuesViewController.swift in Sources */, @@ -3111,6 +3126,7 @@ 2977788A20B306F200F2AFC2 /* LabelLayoutManager.swift in Sources */, 29DB264A1FCA10A800C3D0C9 /* GithubHighlighting.swift in Sources */, 2950AB1D2083B1E400C6F19A /* EmptyLoadingView.swift in Sources */, + 29A10543216D9381004734A0 /* UNMutableNotificationContent+Routable.swift in Sources */, 29B0EF871F93DF6C00870291 /* RepositoryCodeBlobViewController.swift in Sources */, 98F9F4001F9CCFFE005A0266 /* ImageUploadTableViewController.swift in Sources */, 29C167671ECA005500439D62 /* Constants.swift in Sources */,