├── .gitignore ├── LICENSE ├── Podfile ├── README.md ├── Screenshots ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png └── screenshot5.png ├── Scripts └── jb_copyresources.sh ├── libGitHubIssues Demo ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.h ├── ViewController.m ├── libGitHubIssues Demo.entitlements └── main.m ├── libGitHubIssues-(Jailbreak) ├── Package │ ├── DEBIAN │ │ └── control │ ├── Library │ │ └── Application Support │ │ │ └── libGitHubIssues │ │ │ ├── libGitHubIssues_Mark.png │ │ │ ├── libGitHubIssues_Mark@2x.png │ │ │ ├── libGitHubIssues_Mark@3x.png │ │ │ ├── libGitHubIssues_Profile.png │ │ │ ├── libGitHubIssues_Profile@2x.png │ │ │ └── libGitHubIssues_Profile@3x.png │ └── usr │ │ ├── include │ │ └── libGitHubIssues.h │ │ └── lib │ │ └── libGitHubIssues.dylib └── PackageVersion.plist ├── libGitHubIssues.podspec ├── libGitHubIssues.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── Matt.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── Matt.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── libGitHubIssues Demo.xcscheme │ ├── libGitHubIssues-(Jailbreak).xcscheme │ ├── libGitHubIssues.xcscheme │ └── xcschememanagement.plist ├── libGitHubIssues.xcworkspace ├── contents.xcworkspacedata └── xcuserdata │ └── Matt.xcuserdatad │ ├── UserInterfaceState.xcuserstate │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist └── libGitHubIssues ├── GICommentComposeController.h ├── GICommentComposeController.m ├── GIIssueComposeController.h ├── GIIssueComposeController.m ├── GIIssueDetailTableController.h ├── GIIssueDetailTableController.m ├── GIIssuesCommentTableCell.h ├── GIIssuesCommentTableCell.m ├── GIIssuesLabelView.h ├── GIIssuesLabelView.m ├── GIIssuesTableViewCell.h ├── GIIssuesTableViewCell.m ├── GIIssuesViewController.h ├── GIIssuesViewController.m ├── GILoginController.h ├── GILoginController.m ├── GIResources.h ├── GIResources.m ├── GIRootViewController.h ├── GIRootViewController.m ├── GIUserViewController.h ├── GIUserViewController.m ├── OCTClient+Fingerprint.h ├── OCTClient+Fingerprint.m ├── OCTClient_OCTClient_Issues.h ├── OCTClient_OCTClient_Issues.m ├── OCTIssue+New.h ├── OCTIssue+New.m ├── OCTIssueComment+New.h ├── OCTIssueComment+New.m ├── Supporting Files ├── libGitHubIssues_Mark.png ├── libGitHubIssues_Mark@2x.png ├── libGitHubIssues_Mark@3x.png ├── libGitHubIssues_Profile.png ├── libGitHubIssues_Profile@2x.png └── libGitHubIssues_Profile@3x.png └── libGitHubIssues.h /.gitignore: -------------------------------------------------------------------------------- 1 | Pods 2 | LatestBuild 3 | Podfile.lock 4 | Packages/ 5 | .dylib -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Matt Clarke 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '8.0' 2 | 3 | target 'libGitHubIssues' do 4 | pod 'OctoKit', '~> 0.5' 5 | pod "RFMarkdownTextView", "~> 1.4" 6 | pod 'UITextView+Placeholder', '~> 1.2' 7 | pod 'SAMKeychain' 8 | end 9 | 10 | target 'libGitHubIssues-(Jailbreak)' do 11 | pod 'OctoKit', '~> 0.5' 12 | pod "RFMarkdownTextView", "~> 1.4" 13 | pod 'UITextView+Placeholder', '~> 1.2' 14 | pod 'SAMKeychain' 15 | end 16 | 17 | target 'libGitHubIssues Demo' do 18 | pod 'libGitHubIssues' 19 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #libGitHubIssues 2 | Integrate GitHub's Issues system into your app to use as a bugtracker. A single public view controller is provided to present modally, and supports a native OAuth application flow for users to login. Existing issues can be viewed without the user needing to log in. 3 | 4 | ##Screenshots 5 | 6 | ![Issues Overview](/Screenshots/screenshot1.png?raw=true "Issues Overview") 7 | ![Issue Detail](/Screenshots/screenshot2.png?raw=true "Issue Detail") 8 | ![Login UI](/Screenshots/screenshot3.png?raw=true "Login UI") 9 | ![Comment Composer](/Screenshots/screenshot4.png?raw=true "Comment Composer") 10 | ![Issue Composer](/Screenshots/screenshot5.png?raw=true "Issue COmposer") 11 | 12 | ##Installation 13 | 14 | You can install *libGitHubIssues* into your application in two ways: via [CocoaPods](https://guides.cocoapods.org/using/getting-started.html), or as a dependancy in a jailbroken package. 15 | 16 | ###REQUIRED: GitHub Application 17 | 18 | To utilise this project, you will first need to create an OAuth2 application for GitHub [here](https://github.com/settings/developers); fill in the homepage URL with your website, as it won't be needed for functionality. 19 | 20 | Make a note of the client ID and secret; you will need these when using this project. 21 | 22 | ###CocoaPods 23 | 24 | Add 25 | 26 | pod "libGitHubIssues", "~> 0.0.1" 27 | 28 | to your Podfile. 29 | 30 | ###Jailbroken Package 31 | 32 | Please note that *libGitHubIssues* is not yet available via a default Cydia repository, due to cirumstances beyond my control. This should be resolved soon. In the meantime, please download a copy in a .deb format from the Releases tab. 33 | 34 | When using *libGitHubIssues* in a jailbroken package: 35 | 36 | 1. On your device, download and install *libGitHubIssues* from Cydia (found on the BigBoss repository). 37 | 2. Copy libGitHubIssues.dylib from /usr/lib to your development machine's theos/lib directory. 38 | 3. Copy libGitHubIssues.h from /usr/include/libGitHubIssues/ to your development machine's theos/include directory. 39 | 4. Add -lGitHubIssues to your project makefile's LDFLAGS field 40 | 5. Add a dependancy upon com.matchstic.libgithubissues to your project's control file. 41 | 42 | ##Usage 43 | 44 | #import 45 | 46 | ... 47 | 48 | GIRootViewController *rootModal = [[GIRootViewController alloc] init]; 49 | 50 | [GIRootViewController registerClientID:@"" andSecret:@""]; 51 | [GIRootViewController registerCurrentRepositoryName:@"" andOwner:@""]; 52 | 53 | [self presentViewController:rootModal animated:YES completion:nil]; 54 | 55 | ##Contributing 56 | 57 | To work on this project, clone or fork it: 58 | 59 | $ git clone https://github.com/Matchstic/libGitHubIssues.git 60 | 61 | update CocoaPods: 62 | 63 | $ pod update 64 | 65 | and open *libGitHubIssues.xcworkspace*. 66 | 67 | To build the *libGitHubIssues-(Jailbreak)* target you will need iOSOpenDev. 68 | 69 | === 70 | 71 | Released under the BSD 2-Clause license. 72 | -------------------------------------------------------------------------------- /Screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/Screenshots/screenshot1.png -------------------------------------------------------------------------------- /Screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/Screenshots/screenshot2.png -------------------------------------------------------------------------------- /Screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/Screenshots/screenshot3.png -------------------------------------------------------------------------------- /Screenshots/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/Screenshots/screenshot4.png -------------------------------------------------------------------------------- /Screenshots/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/Screenshots/screenshot5.png -------------------------------------------------------------------------------- /Scripts/jb_copyresources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp -R "libGitHubIssues/Supporting Files/." "libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/" -------------------------------------------------------------------------------- /libGitHubIssues Demo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // libGitHubIssues Demo 4 | // 5 | // Created by Matt Clarke on 19/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // libGitHubIssues Demo 4 | // 5 | // Created by Matt Clarke on 19/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /libGitHubIssues Demo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // libGitHubIssues Demo 4 | // 5 | // Created by Matt Clarke on 19/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // libGitHubIssues Demo 4 | // 5 | // Created by Matt Clarke on 19/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "GIRootViewController.h" 11 | 12 | @interface ViewController () 13 | 14 | @end 15 | 16 | @implementation ViewController 17 | 18 | - (void)viewDidAppear:(BOOL)animated { 19 | [super viewDidAppear:animated]; 20 | // Do any additional setup after loading the view, typically from a nib. 21 | 22 | GIRootViewController *rootModal = [[GIRootViewController alloc] init]; 23 | 24 | /* 25 | * For your own implementation, you would specify the client ID and secret as explained in the README. 26 | * However, I have omitted them here as it is *strongly* advised not to make these public. 27 | * 28 | * Not providing them here just prevents users from being able to login. 29 | */ 30 | [GIRootViewController registerClientID:@"" andSecret:@""]; 31 | [GIRootViewController registerCurrentRepositoryName:@"Xen" andOwner:@"Matchstic"]; 32 | 33 | [self presentViewController:rootModal animated:YES completion:nil]; 34 | } 35 | 36 | - (void)didReceiveMemoryWarning { 37 | [super didReceiveMemoryWarning]; 38 | // Dispose of any resources that can be recreated. 39 | } 40 | 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/libGitHubIssues Demo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | $(AppIdentifierPrefix)com.matchstic.libGitHubIssues-Demo 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /libGitHubIssues Demo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // libGitHubIssues Demo 4 | // 5 | // Created by Matt Clarke on 19/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: com.matchstic.libgithubissues 2 | Name: libGitHubIssues 3 | Version: 0.0.1 4 | Description: Integrate GitHub Issues as a bugtracker 5 | Section: System 6 | Depends: firmware (>= 8.0) 7 | Priority: optional 8 | Architecture: iphoneos-arm 9 | Author: Matt Clarke 10 | dev: Matt Clarke 11 | Maintainer: Matt Clarke 12 | -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Mark.png -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Mark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Mark@2x.png -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Mark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Mark@3x.png -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Profile.png -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Profile@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Profile@2x.png -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Profile@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues-(Jailbreak)/Package/Library/Application Support/libGitHubIssues/libGitHubIssues_Profile@3x.png -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/usr/include/libGitHubIssues.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIRootViewController.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 16/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | /* 10 | Usage: 11 | 12 | #import 13 | 14 | ... 15 | 16 | GIRootViewController *rootModal = [[GIRootViewController alloc] init]; 17 | 18 | [GIRootViewController registerClientID:@"" andSecret:@""]; 19 | [GIRootViewController registerCurrentRepositoryName:@"" andOwner:@""]; 20 | 21 | [self presentViewController:rootModal animated:YES completion:nil]; 22 | */ 23 | 24 | #import 25 | 26 | @interface GIRootViewController : UINavigationController 27 | 28 | /** 29 | Configure libGitHubIssues with an identifier with the client ID and secret for your application on GitHub.\n\n 30 | 31 | The client ID and secret can be found at: https://github.com/settings/developers 32 | 33 | @param clientId Cient ID from your GitHub application. 34 | @param clientSecret Client secret from your GitHub application. 35 | */ 36 | +(void)registerClientID:(NSString*)clientId andSecret:(NSString*)clientSecret; 37 | 38 | /** 39 | Configure which repository libGitHubIssues should access Issues from.\n\n 40 | 41 | Parameters are in the form: https://github.com// 42 | 43 | @param name Name of repository 44 | @param owner Owner of repository 45 | */ 46 | +(void)registerCurrentRepositoryName:(NSString*)name andOwner:(NSString*)owner; 47 | 48 | @end 49 | 50 | -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/Package/usr/lib/libGitHubIssues.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues-(Jailbreak)/Package/usr/lib/libGitHubIssues.dylib -------------------------------------------------------------------------------- /libGitHubIssues-(Jailbreak)/PackageVersion.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BugFix 6 | 1 7 | Major 8 | 0 9 | Minor 10 | 0 11 | PackageRevision 12 | 13 | Stage 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /libGitHubIssues.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint libGitHubIssues.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "libGitHubIssues" 19 | s.version = "0.0.1" 20 | s.summary = "A kit to make use of GitHub Issues as a bugtracker system." 21 | 22 | # This description is used to generate tags and improve search results. 23 | # * Think: What does it do? Why did you write it? What is the focus? 24 | # * Try to keep it short, snappy and to the point. 25 | # * Write the description between the DESC delimiters below. 26 | # * Finally, don't worry about the indent, CocoaPods strips it! 27 | s.description = <<-DESC 28 | libGitHubIssues allows you to integrate GitHub's Issues system into your app to use as a bugtracker. It provides a single public view controller 29 | to present modally, and supports a native OAuth application flow for users to login. Existing issues can be viewed without the user needing to 30 | log in. 31 | DESC 32 | 33 | s.homepage = "https://github.com/Matchstic/libGitHubIssues" 34 | 35 | 36 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 37 | # 38 | # Licensing your code is important. See http://choosealicense.com for more info. 39 | # CocoaPods will detect a license file if there is a named LICENSE* 40 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 41 | # 42 | 43 | s.license = { :type => "BSD", :file => "LICENSE" } 44 | 45 | 46 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 47 | # 48 | # Specify the authors of the library, with email addresses. Email addresses 49 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 50 | # accepts just a name if you'd rather not provide an email address. 51 | # 52 | # Specify a social_media_url where others can refer to, for example a twitter 53 | # profile URL. 54 | # 55 | 56 | s.author = { "Matt Clarke" => "matt@incendo.ws" } 57 | s.social_media_url = "http://twitter.com/_Matchstic" 58 | 59 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 60 | # 61 | # If this Pod runs only on iOS or OS X, then specify the platform and 62 | # the deployment target. You can optionally include the target after the platform. 63 | # 64 | 65 | s.platform = :ios, "8.0" 66 | 67 | 68 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 69 | # 70 | # Specify the location from where the source should be retrieved. 71 | # Supports git, hg, bzr, svn and HTTP. 72 | # 73 | 74 | s.source = { :git => "https://github.com/Matchstic/libGitHubIssues.git", :tag => "#{s.version}" } 75 | 76 | 77 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 78 | # 79 | # CocoaPods is smart about how it includes source code. For source files 80 | # giving a folder will include any swift, h, m, mm, c & cpp files. 81 | # For header files it will include any header in the folder. 82 | # Not including the public_header_files will make all headers public. 83 | # 84 | 85 | s.source_files = "libGitHubIssues/*.{h,m}" 86 | 87 | 88 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 89 | # 90 | # A list of resources included with the Pod. These are copied into the 91 | # target bundle with a build phase script. Anything else will be cleaned. 92 | # You can preserve files from being cleaned, please don't preserve 93 | # non-essential files like tests, examples and documentation. 94 | # 95 | 96 | s.resource_bundles = { 97 | 'libGitHubIssues' => [ 98 | 'libGitHubIssues/Supporting Files/*.png' 99 | ] 100 | } 101 | 102 | 103 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 104 | # 105 | # Link your library with frameworks, or libraries. Libraries do not include 106 | # the lib prefix of their name. 107 | # 108 | 109 | s.framework = "UIKit" 110 | 111 | 112 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 113 | # 114 | # If your library depends on compiler flags you can set them in the xcconfig hash 115 | # where they will only apply to your library. If you depend on other Podspecs 116 | # you can include multiple dependencies to ensure it works. 117 | 118 | s.requires_arc = true 119 | 120 | s.dependency "OctoKit", "~> 0.5" 121 | s.dependency "RFMarkdownTextView", "~> 1.4" 122 | s.dependency "UITextView+Placeholder", "~> 1.2" 123 | s.dependency "SAMKeychain" 124 | 125 | end 126 | -------------------------------------------------------------------------------- /libGitHubIssues.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /libGitHubIssues.xcodeproj/project.xcworkspace/xcuserdata/Matt.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues.xcodeproj/project.xcworkspace/xcuserdata/Matt.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /libGitHubIssues.xcodeproj/xcuserdata/Matt.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /libGitHubIssues.xcodeproj/xcuserdata/Matt.xcuserdatad/xcschemes/libGitHubIssues Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /libGitHubIssues.xcodeproj/xcuserdata/Matt.xcuserdatad/xcschemes/libGitHubIssues-(Jailbreak).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /libGitHubIssues.xcodeproj/xcuserdata/Matt.xcuserdatad/xcschemes/libGitHubIssues.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /libGitHubIssues.xcodeproj/xcuserdata/Matt.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Github Issues.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | libGitHubIssues Demo.xcscheme 13 | 14 | orderHint 15 | 13 16 | 17 | libGitHubIssues-(Jailbreak).xcscheme 18 | 19 | orderHint 20 | 15 21 | 22 | libGitHubIssues.xcscheme 23 | 24 | orderHint 25 | 14 26 | 27 | 28 | SuppressBuildableAutocreation 29 | 30 | C913A62E1E079FB30030FDF8 31 | 32 | primary 33 | 34 | 35 | C913A64A1E079FF40030FDF8 36 | 37 | primary 38 | 39 | 40 | C913A68D1E07A10C0030FDF8 41 | 42 | primary 43 | 44 | 45 | C913A6D21E07A2610030FDF8 46 | 47 | primary 48 | 49 | 50 | C92986F91E049F28002BDFEB 51 | 52 | primary 53 | 54 | 55 | C9F46D0B1E07B3DF00813F13 56 | 57 | primary 58 | 59 | 60 | C9F46D271E07B40C00813F13 61 | 62 | primary 63 | 64 | 65 | C9F46D6E1E07B51000813F13 66 | 67 | primary 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /libGitHubIssues.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /libGitHubIssues.xcworkspace/xcuserdata/Matt.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues.xcworkspace/xcuserdata/Matt.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /libGitHubIssues.xcworkspace/xcuserdata/Matt.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /libGitHubIssues/GICommentComposeController.h: -------------------------------------------------------------------------------- 1 | // 2 | // GICommentComposeController.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol GICommentComposeDelegate 12 | 13 | -(void)didSendComment; 14 | 15 | @end 16 | 17 | @interface GICommentComposeController : UIViewController 18 | 19 | @property (nonatomic, strong) UITextView *textView; 20 | @property (nonatomic, strong) id delegate; 21 | @property (nonatomic, strong) id issue; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /libGitHubIssues/GICommentComposeController.m: -------------------------------------------------------------------------------- 1 | // 2 | // GICommentComposeController.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GICommentComposeController.h" 10 | #import 11 | #import 12 | #import 13 | #import "GIResources.h" 14 | #import "OCTClient_OCTClient_Issues.h" 15 | 16 | @interface GICommentComposeController () 17 | 18 | @end 19 | 20 | @implementation GICommentComposeController 21 | 22 | - (void)viewDidLoad { 23 | [super viewDidLoad]; 24 | // Do any additional setup after loading the view. 25 | 26 | [self.navigationItem setTitle:@"Compose"]; 27 | [self setTitle:@"Compose"]; 28 | 29 | UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithTitle:@"Send" style:UIBarButtonItemStylePlain target:self action:@selector(didTapCommentButton:)]; 30 | item.enabled = NO; 31 | self.navigationItem.rightBarButtonItem = item; 32 | } 33 | 34 | -(void)viewDidAppear:(BOOL)animated { 35 | [super viewDidAppear:animated]; 36 | 37 | [self.textView becomeFirstResponder]; 38 | } 39 | 40 | - (void)didReceiveMemoryWarning { 41 | [super didReceiveMemoryWarning]; 42 | // Dispose of any resources that can be recreated. 43 | } 44 | 45 | -(void)loadView { 46 | self.view = [[UIView alloc] initWithFrame:CGRectZero]; 47 | self.view.backgroundColor = [UIColor whiteColor]; 48 | 49 | self.textView = [[UITextView alloc] initWithFrame:CGRectZero]; 50 | self.textView.textColor = [UIColor darkTextColor]; 51 | self.textView.textAlignment = NSTextAlignmentNatural; 52 | self.textView.restorationIdentifier = @"com.matchstic.commentcompose"; 53 | self.textView.font = [UIFont systemFontOfSize:14]; 54 | self.textView.placeholder = @"Comment..."; 55 | self.textView.delegate = self; 56 | 57 | [self.view addSubview:self.textView]; 58 | 59 | // Create a new RFToolbarButton 60 | RFToolbarButton *hashButton = [RFToolbarButton buttonWithTitle:@"#"]; 61 | 62 | // Add a button target to the exampleButton 63 | [hashButton addEventHandler:^{ 64 | // Do anything in this block here 65 | [_textView insertText:@"#"]; 66 | } forControlEvents:UIControlEventTouchUpInside]; 67 | 68 | RFToolbarButton *starButton = [RFToolbarButton buttonWithTitle:@"*"]; 69 | 70 | // Add a button target to the exampleButton 71 | [starButton addEventHandler:^{ 72 | // Do anything in this block here 73 | [_textView insertText:@"*"]; 74 | } forControlEvents:UIControlEventTouchUpInside]; 75 | 76 | RFToolbarButton *linkButton = [RFToolbarButton buttonWithTitle:@"Link"]; 77 | 78 | // Add a button target to the exampleButton 79 | [linkButton addEventHandler:^{ 80 | // Do anything in this block here 81 | [_textView insertText:@"[Link Title](http://...)"]; 82 | } forControlEvents:UIControlEventTouchUpInside]; 83 | 84 | RFToolbarButton *imageButton = [RFToolbarButton buttonWithTitle:@"Image"]; 85 | 86 | // Add a button target to the exampleButton 87 | [imageButton addEventHandler:^{ 88 | // Do anything in this block here 89 | [_textView insertText:@"![Alt text](http://...)"]; 90 | } forControlEvents:UIControlEventTouchUpInside]; 91 | 92 | 93 | RFKeyboardToolbar *toolbar = [RFKeyboardToolbar toolbarWithButtons:@[ hashButton, starButton, linkButton, imageButton ]]; 94 | 95 | self.textView.inputAccessoryView = toolbar; 96 | } 97 | 98 | -(void)viewDidLayoutSubviews { 99 | [super viewDidLayoutSubviews]; 100 | 101 | self.textView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 102 | } 103 | 104 | -(void)didTapCommentButton:(id)sender { 105 | // Send off to GitHub, and tell delegate to reload+pop. 106 | 107 | // TODO: Add animation to show in-progress of this task. 108 | 109 | OCTClient *client = (OCTClient*)[GIResources _getCurrentClient]; 110 | 111 | RACSignal *request = (RACSignal*)[GIResources _getCurrentRepository]; 112 | [[request collect] subscribeNext:^(NSArray *repositories) { 113 | OCTRepository *repo = [repositories firstObject]; 114 | 115 | RACSignal *sigTwo = [client createIssueCommentWithBody:self.textView.text forIssue:self.issue inRepository:repo]; 116 | [sigTwo subscribeNext:^(id thing){ 117 | 118 | } error:^(NSError *error) { 119 | [self _presentError:error]; 120 | 121 | [self.navigationItem setTitle:@"Error"]; 122 | [self setTitle:@"Error"]; 123 | } completed:^{ 124 | dispatch_async(dispatch_get_main_queue(), ^{ 125 | [self.delegate didSendComment]; 126 | }); 127 | }]; 128 | 129 | } error:^(NSError *error) { 130 | // Invoked when an error occurs. You won't receive any results if this 131 | // happens. 132 | [self _presentError:error]; 133 | 134 | [self.navigationItem setTitle:@"Error"]; 135 | [self setTitle:@"Error"]; 136 | }]; 137 | } 138 | 139 | -(void)_presentError:(NSError*)error { 140 | dispatch_async(dispatch_get_main_queue(), ^{ 141 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"%@", error.localizedFailureReason] preferredStyle:UIAlertControllerStyleAlert]; 142 | 143 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault 144 | handler:^(UIAlertAction * action) {}]; 145 | 146 | [alert addAction:defaultAction]; 147 | [self presentViewController:alert animated:YES completion:nil]; 148 | }); 149 | } 150 | 151 | - (void)textViewDidChange:(UITextView *)textView { 152 | // If both the password and username fields have content, enable login button. 153 | if (self.textView.text && 154 | ![self.textView.text isEqualToString:@""]) { 155 | // Enable right button! 156 | self.navigationItem.rightBarButtonItem.enabled = YES; 157 | } else { 158 | self.navigationItem.rightBarButtonItem.enabled = NO; 159 | } 160 | } 161 | 162 | @end 163 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssueComposeController.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssueComposeController.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol GIIssueComposeDelegate 12 | 13 | -(void)didSendIssue; 14 | 15 | @end 16 | 17 | @interface GIIssueComposeController : UIViewController 18 | 19 | @property (nonatomic, strong) id delegate; 20 | @property (nonatomic, strong) UITextField *titleField; 21 | @property (nonatomic, strong) UIView *separator; 22 | @property (nonatomic, strong) UITextView *textView; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssueComposeController.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssueComposeController.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIIssueComposeController.h" 10 | #import 11 | #import 12 | #import 13 | #import "GIResources.h" 14 | #import "OCTClient_OCTClient_Issues.h" 15 | 16 | @interface GIIssueComposeController () 17 | 18 | @end 19 | 20 | @implementation GIIssueComposeController 21 | 22 | - (void)viewDidLoad { 23 | [super viewDidLoad]; 24 | // Do any additional setup after loading the view. 25 | 26 | [self.navigationItem setTitle:@"Compose"]; 27 | [self setTitle:@"Compose"]; 28 | 29 | self.navigationController.navigationBar.translucent = NO; 30 | 31 | UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithTitle:@"Send" style:UIBarButtonItemStylePlain target:self action:@selector(didTapSendButton:)]; 32 | item.enabled = NO; 33 | self.navigationItem.rightBarButtonItem = item; 34 | } 35 | 36 | - (void)didReceiveMemoryWarning { 37 | [super didReceiveMemoryWarning]; 38 | // Dispose of any resources that can be recreated. 39 | } 40 | 41 | -(void)loadView { 42 | self.view = [[UIView alloc] initWithFrame:CGRectZero]; 43 | self.view.backgroundColor = [UIColor whiteColor]; 44 | 45 | UIView *leftBlock = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)]; 46 | leftBlock.backgroundColor = [UIColor clearColor]; 47 | 48 | self.titleField = [[UITextField alloc] initWithFrame:CGRectZero]; 49 | self.titleField.textAlignment = NSTextAlignmentNatural; 50 | self.titleField.textColor = [UIColor grayColor]; 51 | self.titleField.placeholder = @"Title..."; 52 | self.titleField.autocapitalizationType = UITextAutocapitalizationTypeSentences; 53 | self.titleField.autocorrectionType = UITextAutocorrectionTypeYes; 54 | self.titleField.returnKeyType = UIReturnKeyDone; 55 | self.titleField.delegate = self; 56 | [self.titleField addTarget:self action:@selector(textFieldContentDidChange:) forControlEvents:UIControlEventEditingChanged]; 57 | self.titleField.leftView = leftBlock; 58 | self.titleField.leftViewMode = UITextFieldViewModeAlways; 59 | 60 | [self.view addSubview:self.titleField]; 61 | 62 | self.separator = [[UIView alloc] initWithFrame:CGRectZero]; 63 | self.separator.backgroundColor = [UIColor lightGrayColor]; 64 | 65 | [self.view addSubview:self.separator]; 66 | 67 | self.textView = [[UITextView alloc] initWithFrame:CGRectZero]; 68 | self.textView.textColor = [UIColor darkTextColor]; 69 | self.textView.textAlignment = NSTextAlignmentNatural; 70 | self.textView.restorationIdentifier = @"com.matchstic.issuecompose"; 71 | self.textView.font = [UIFont systemFontOfSize:14]; 72 | self.textView.placeholder = @"Details..."; 73 | 74 | [self.view addSubview:self.textView]; 75 | 76 | // Create a new RFToolbarButton 77 | RFToolbarButton *hashButton = [RFToolbarButton buttonWithTitle:@"#"]; 78 | 79 | // Add a button target to the exampleButton 80 | [hashButton addEventHandler:^{ 81 | // Do anything in this block here 82 | [_textView insertText:@"#"]; 83 | } forControlEvents:UIControlEventTouchUpInside]; 84 | 85 | RFToolbarButton *starButton = [RFToolbarButton buttonWithTitle:@"*"]; 86 | 87 | // Add a button target to the exampleButton 88 | [starButton addEventHandler:^{ 89 | // Do anything in this block here 90 | [_textView insertText:@"*"]; 91 | } forControlEvents:UIControlEventTouchUpInside]; 92 | 93 | RFToolbarButton *linkButton = [RFToolbarButton buttonWithTitle:@"Link"]; 94 | 95 | // Add a button target to the exampleButton 96 | [linkButton addEventHandler:^{ 97 | // Do anything in this block here 98 | [_textView insertText:@"[Link Title](http://...)"]; 99 | } forControlEvents:UIControlEventTouchUpInside]; 100 | 101 | RFToolbarButton *imageButton = [RFToolbarButton buttonWithTitle:@"Image"]; 102 | 103 | // Add a button target to the exampleButton 104 | [imageButton addEventHandler:^{ 105 | // Do anything in this block here 106 | [_textView insertText:@"![Alt text](http://...)"]; 107 | } forControlEvents:UIControlEventTouchUpInside]; 108 | 109 | RFKeyboardToolbar *toolbar = [RFKeyboardToolbar toolbarWithButtons:@[ hashButton, starButton, linkButton, imageButton ]]; 110 | 111 | self.textView.inputAccessoryView = toolbar; 112 | } 113 | 114 | -(void)viewDidLayoutSubviews { 115 | [super viewDidLayoutSubviews]; 116 | 117 | self.titleField.frame = CGRectMake(0, 0, self.view.frame.size.width, 40); 118 | self.separator.frame = CGRectMake(10, 39, self.view.frame.size.width-20, 1); 119 | self.textView.frame = CGRectMake(5, 40, self.view.frame.size.width-10, self.view.frame.size.height-40); 120 | } 121 | 122 | -(BOOL)textFieldShouldReturn:(UITextField *)textField { 123 | [self.titleField resignFirstResponder]; 124 | 125 | return NO; 126 | } 127 | 128 | -(void)textFieldDidBeginEditing:(UITextField *)textField { 129 | if (![self.textView hasText]) { 130 | self.textView.text = @""; 131 | self.textView.attributedText = nil; 132 | } 133 | } 134 | 135 | -(void)textFieldContentDidChange:(UITextField*)sender { 136 | // If both the password and username fields have content, enable login button. 137 | if (self.titleField.text && 138 | ![self.titleField.text isEqualToString:@""]) { 139 | // Enable right button! 140 | self.navigationItem.rightBarButtonItem.enabled = YES; 141 | } else { 142 | self.navigationItem.rightBarButtonItem.enabled = NO; 143 | } 144 | } 145 | 146 | -(void)didTapSendButton:(id)sender { 147 | [self.textView resignFirstResponder]; 148 | [self.titleField resignFirstResponder]; 149 | 150 | // TODO: Add animation to show in-progress of this task. 151 | 152 | // Send to GitHub and pop back. 153 | OCTClient *client = (OCTClient*)[GIResources _getCurrentClient]; 154 | 155 | RACSignal *request = (RACSignal*)[GIResources _getCurrentRepository]; 156 | [[request collect] subscribeNext:^(NSArray *repositories) { 157 | OCTRepository *repo = [repositories firstObject]; 158 | 159 | RACSignal *sigTwo = [client createIssueWithTitle:self.titleField.text body:self.textView.text assignee:nil milestone:nil labels:nil inRepository:repo]; 160 | [sigTwo subscribeNext:^(id thing){ 161 | 162 | } error:^(NSError *error) { 163 | [self _presentError:error]; 164 | 165 | [self.navigationItem setTitle:@"Error"]; 166 | [self setTitle:@"Error"]; 167 | } completed:^{ 168 | dispatch_async(dispatch_get_main_queue(), ^{ 169 | [self.delegate didSendIssue]; 170 | }); 171 | }]; 172 | 173 | } error:^(NSError *error) { 174 | // Invoked when an error occurs. You won't receive any results if this 175 | // happens. 176 | [self _presentError:error]; 177 | 178 | [self.navigationItem setTitle:@"Error"]; 179 | [self setTitle:@"Error"]; 180 | }]; 181 | } 182 | 183 | -(void)_presentError:(NSError*)error { 184 | dispatch_async(dispatch_get_main_queue(), ^{ 185 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"%@", error.localizedFailureReason] preferredStyle:UIAlertControllerStyleAlert]; 186 | 187 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault 188 | handler:^(UIAlertAction * action) {}]; 189 | 190 | [alert addAction:defaultAction]; 191 | [self presentViewController:alert animated:YES completion:nil]; 192 | }); 193 | } 194 | 195 | @end 196 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssueDetailTableController.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssueDetailTableController.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "GILoginController.h" 11 | #import "GICommentComposeController.h" 12 | 13 | @interface GIIssueDetailTableController : UITableViewController { 14 | NSArray *_dataSource; 15 | } 16 | 17 | @property (nonatomic, strong) id issue; 18 | 19 | -(instancetype)initWithIssue:(id)issue; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssueDetailTableController.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssueDetailTableController.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIIssueDetailTableController.h" 10 | #import "GIIssuesTableViewCell.h" 11 | #import "GIIssuesCommentTableCell.h" 12 | #import "GIResources.h" 13 | #import "OCTClient_OCTClient_Issues.h" 14 | #import "OCTIssueComment+New.h" 15 | #import "OCTIssue+New.h" 16 | 17 | #define REUSE_TOP @"com.matchstic.issues" 18 | #define REUSE_COMMENTS @"com.matchstic.comments" 19 | #define REUSE_CLOSED @"com.matchstic.closed" 20 | 21 | @interface GIIssueDetailTableController () 22 | 23 | @end 24 | 25 | static GIIssuesTableViewCell *heightCheckerCell2; 26 | static GIIssuesCommentTableCell *heightCheckerCell3; 27 | 28 | @implementation GIIssueDetailTableController 29 | 30 | - (void)viewDidLoad { 31 | [super viewDidLoad]; 32 | 33 | // Add the right item for profile. 34 | UIBarButtonItem *composeItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCompose target:self action:@selector(didTapComposeButton:)]; 35 | self.navigationItem.rightBarButtonItem = composeItem; 36 | 37 | [self.tableView registerClass:[GIIssuesTableViewCell class] forCellReuseIdentifier:REUSE_TOP]; 38 | [self.tableView registerClass:[GIIssuesCommentTableCell class] forCellReuseIdentifier:REUSE_COMMENTS]; 39 | self.tableView.allowsSelection = NO; 40 | 41 | [self loadComments]; 42 | } 43 | 44 | -(void)loadComments { 45 | [self.navigationItem setTitle:@"Loading..."]; 46 | [self setTitle:@"Loading..."]; 47 | 48 | // Now, we download the comments! 49 | RACSignal *sig = [(OCTClient*)[GIResources _getCurrentClient] fetchIssueCommentsForIssue:self.issue since:nil]; 50 | 51 | [[sig collect] subscribeNext:^(NSArray *issues) { 52 | 53 | NSMutableArray *newData = [NSMutableArray array]; 54 | 55 | // Array of OCTResponse. 56 | for (OCTResponse *response in issues) { 57 | OCTIssueCommentNew *parsed = response.parsedResult; 58 | 59 | // If needed, add the "issue closed" tab in. 60 | 61 | [newData addObject:parsed]; 62 | } 63 | 64 | _dataSource = newData; 65 | 66 | dispatch_async(dispatch_get_main_queue(), ^{ 67 | NSString *title = [NSString stringWithFormat:@"#%@", [(OCTIssueNew*)self.issue number]]; 68 | 69 | [self.navigationItem setTitle:title]; 70 | [self setTitle:title]; 71 | 72 | [self.tableView reloadData]; 73 | NSLog(@"Should be reloaded now..."); 74 | }); 75 | 76 | } error:^(NSError *error) { 77 | // Invoked when an error occurs. You won't receive any results if this 78 | // happens. 79 | [self _presentError:error]; 80 | 81 | [self.navigationItem setTitle:@"Error"]; 82 | [self setTitle:@"Error"]; 83 | }]; 84 | } 85 | 86 | -(void)_presentError:(NSError*)error { 87 | dispatch_async(dispatch_get_main_queue(), ^{ 88 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"%@", error.localizedFailureReason] preferredStyle:UIAlertControllerStyleAlert]; 89 | 90 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault 91 | handler:^(UIAlertAction * action) {}]; 92 | 93 | [alert addAction:defaultAction]; 94 | [self presentViewController:alert animated:YES completion:nil]; 95 | }); 96 | } 97 | 98 | - (void)didReceiveMemoryWarning { 99 | [super didReceiveMemoryWarning]; 100 | // Dispose of any resources that can be recreated. 101 | } 102 | 103 | -(instancetype)initWithIssue:(id)issue { 104 | self = [super initWithStyle:UITableViewStyleGrouped]; 105 | 106 | if (self) { 107 | self.issue = issue; 108 | } 109 | 110 | return self; 111 | } 112 | 113 | #pragma mark - Table view data source 114 | 115 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 116 | return 2; 117 | } 118 | 119 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 120 | switch (section) { 121 | case 0: 122 | return 1; 123 | break; 124 | case 1: 125 | return _dataSource.count; 126 | 127 | default: 128 | break; 129 | } 130 | return 0; 131 | } 132 | 133 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 134 | if (indexPath.section == 0) { 135 | GIIssuesTableViewCell *cell = (GIIssuesTableViewCell*)[tableView dequeueReusableCellWithIdentifier:REUSE_TOP forIndexPath:indexPath]; 136 | if (!cell) { 137 | cell = [[GIIssuesTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:REUSE_TOP]; 138 | } 139 | 140 | // Configure the cell... 141 | [cell setupWithIssue:self.issue withExtras:YES]; 142 | cell.accessoryType = UITableViewCellAccessoryNone; 143 | 144 | return cell; 145 | } else { 146 | GIIssuesCommentTableCell *cell = (GIIssuesCommentTableCell*)[tableView dequeueReusableCellWithIdentifier:REUSE_COMMENTS forIndexPath:indexPath]; 147 | if (!cell) { 148 | cell = [[GIIssuesCommentTableCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:REUSE_COMMENTS]; 149 | } 150 | 151 | // Configure the cell... 152 | OCTIssueCommentNew *comment = _dataSource[indexPath.row]; 153 | [cell setupWithComment:comment]; 154 | 155 | return cell; 156 | 157 | } 158 | } 159 | 160 | -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 161 | if (indexPath.section == 0) { 162 | if (!heightCheckerCell2) { 163 | heightCheckerCell2 = [[GIIssuesTableViewCell alloc] initWithFrame:CGRectZero]; 164 | } 165 | 166 | // Set width correctly to the cell. 167 | [heightCheckerCell2 setupWithIssue:self.issue withExtras:YES]; 168 | heightCheckerCell2.accessoryType = UITableViewCellAccessoryNone; 169 | heightCheckerCell2.frame = CGRectMake(0, 0, self.tableView.frame.size.width, 0); 170 | [heightCheckerCell2 layoutSubviews]; 171 | 172 | return heightCheckerCell2._viewHeight; 173 | } else { 174 | if (!heightCheckerCell3) { 175 | heightCheckerCell3 = [[GIIssuesCommentTableCell alloc] initWithFrame:CGRectZero]; 176 | } 177 | 178 | OCTIssueCommentNew *comment = _dataSource[indexPath.row]; 179 | 180 | // Set width correctly to the cell. 181 | [heightCheckerCell3 setupWithComment:comment]; 182 | heightCheckerCell3.accessoryType = UITableViewCellAccessoryNone; 183 | heightCheckerCell3.frame = CGRectMake(0, 0, self.tableView.frame.size.width, 0); 184 | [heightCheckerCell3 layoutSubviews]; 185 | 186 | return heightCheckerCell3._viewHeight; 187 | } 188 | } 189 | 190 | -(void)didTapComposeButton:(id)sender { 191 | OCTClient *client = [GIResources _getCurrentClient]; 192 | 193 | if (!client.token || [client.token isEqualToString:@""]) { 194 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Sign In Required" message:@"You need to be signed in to create a new comment.\n\nSign in now?" preferredStyle:UIAlertControllerStyleAlert]; 195 | 196 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"Sign In" style:UIAlertActionStyleDefault 197 | handler:^(UIAlertAction * action) { 198 | 199 | // Go to login UI. 200 | GILoginController *login = [[GILoginController alloc] init]; 201 | login.delegate = self; 202 | 203 | [self.navigationController pushViewController:login animated:YES]; 204 | 205 | }]; 206 | 207 | [alert addAction:defaultAction]; 208 | 209 | UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel 210 | handler:^(UIAlertAction * action) { 211 | 212 | }]; 213 | [alert addAction:cancelAction]; 214 | [self presentViewController:alert animated:YES completion:nil]; 215 | } else { 216 | GICommentComposeController *compose = [[GICommentComposeController alloc] init]; 217 | compose.delegate = self; 218 | compose.issue = self.issue; 219 | [self.navigationController pushViewController:compose animated:YES]; 220 | } 221 | } 222 | 223 | -(void)didFinishAuthenticationWithClient:(id)client { 224 | [self.navigationController popToViewController:self animated:YES]; 225 | 226 | GICommentComposeController *compose = [[GICommentComposeController alloc] init]; 227 | compose.delegate = self; 228 | compose.issue = self.issue; 229 | [self.navigationController pushViewController:compose animated:YES]; 230 | } 231 | 232 | -(void)didSendComment { 233 | // Reload UI and pop back here. 234 | [self.navigationController popToViewController:self animated:YES]; 235 | 236 | [self loadComments]; 237 | } 238 | 239 | @end 240 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesCommentTableCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesCommentTableCell.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GIIssuesCommentTableCell : UITableViewCell { 12 | id _comment; 13 | } 14 | 15 | @property (nonatomic, readwrite) CGFloat _viewHeight; 16 | 17 | @property (nonatomic, strong) UILabel *bodyLabel; 18 | @property (nonatomic, strong) UIImageView *avatarView; 19 | @property (nonatomic, strong) UILabel *userLabel; 20 | @property (nonatomic, strong) UILabel *commentDateLabel; 21 | 22 | -(void)setupWithComment:(id)comment; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesCommentTableCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesCommentTableCell.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIIssuesCommentTableCell.h" 10 | #import "OCTIssueComment+New.h" 11 | #import "GIResources.h" 12 | #import 13 | 14 | @implementation GIIssuesCommentTableCell 15 | 16 | - (void)awakeFromNib { 17 | [super awakeFromNib]; 18 | // Initialization code 19 | } 20 | 21 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated { 22 | [super setSelected:selected animated:animated]; 23 | 24 | // Configure the view for the selected state 25 | } 26 | 27 | -(void)_configureViewsIfNeeded { 28 | if (!self.bodyLabel) { 29 | self.bodyLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 30 | self.bodyLabel.text = @""; 31 | self.bodyLabel.textColor = [UIColor grayColor]; 32 | self.bodyLabel.font = [UIFont systemFontOfSize:16]; 33 | self.bodyLabel.textAlignment = NSTextAlignmentLeft; 34 | self.bodyLabel.numberOfLines = 0; 35 | 36 | [self.contentView addSubview:self.bodyLabel]; 37 | } 38 | 39 | if (!self.avatarView) { 40 | self.avatarView = [[UIImageView alloc] initWithImage:nil]; 41 | self.avatarView.backgroundColor = [UIColor lightGrayColor]; 42 | 43 | [self.contentView addSubview:self.avatarView]; 44 | } 45 | 46 | if (!self.userLabel) { 47 | self.userLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 48 | self.userLabel.text = @"USER"; 49 | self.userLabel.textColor = [UIColor darkTextColor]; 50 | self.userLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]; 51 | self.userLabel.textAlignment = NSTextAlignmentLeft; 52 | 53 | [self.contentView addSubview:self.userLabel]; 54 | } 55 | 56 | if (!self.commentDateLabel) { 57 | self.commentDateLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 58 | self.commentDateLabel.text = @"0mins ago"; 59 | self.commentDateLabel.textColor = [UIColor colorWithWhite:0.75 alpha:1.0]; 60 | self.commentDateLabel.font = [UIFont systemFontOfSize:12]; 61 | self.commentDateLabel.textAlignment = NSTextAlignmentLeft; 62 | 63 | [self.contentView addSubview:self.commentDateLabel]; 64 | } 65 | } 66 | 67 | -(void)setupWithComment:(OCTIssueCommentNew*)comment { 68 | _comment = comment; 69 | self.accessoryType = UITableViewCellAccessoryNone; 70 | 71 | [self _configureViewsIfNeeded]; 72 | 73 | // Probably shouldn;t be doing this here... 74 | NSString *actual = comment.body; 75 | NSRange range = [actual rangeOfString:@"\n> "]; 76 | 77 | if (range.location != NSNotFound) { 78 | // We have email data to strip out from here-on-in! 79 | actual = [actual substringToIndex:range.location-1]; 80 | 81 | // Walk backwards to last \n and remove. Also, if another \n before that too without text, delete. 82 | NSUInteger len = [actual length]; 83 | unichar buffer[len+1]; 84 | 85 | // Iterate over string, and break on first non-newline. 86 | [actual getCharacters:buffer range:NSMakeRange(0, len)]; 87 | 88 | int i = (int)len; 89 | for(; i > 0; i--) { 90 | char character = buffer[i]; 91 | if (character == '\n') { 92 | break; 93 | } 94 | } 95 | 96 | actual = [actual substringToIndex:i-1]; 97 | } 98 | 99 | self.bodyLabel.text = actual; 100 | 101 | // Avatar and user stuff. 102 | NSString *avatarURL = [comment.user objectForKey:@"avatar_url"]; 103 | NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:avatarURL]]; 104 | 105 | GIIssuesCommentTableCell * __weak weakself = self; 106 | 107 | [self.avatarView setImageWithURLRequest:request placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) { 108 | [weakself setUserImage:image]; 109 | } failure:nil]; 110 | 111 | self.userLabel.text = [comment.user objectForKey:@"login"]; 112 | 113 | // Format date label. 114 | self.commentDateLabel.text = [GIResources formatDate:comment.createdAt]; 115 | } 116 | 117 | -(void)setUserImage:(UIImage*)img { 118 | self.avatarView.image = img; 119 | self.avatarView.frame = CGRectMake(self.avatarView.frame.origin.x, self.avatarView.frame.origin.y, 18, 18); 120 | self.avatarView.layer.cornerRadius = 9; 121 | self.avatarView.layer.masksToBounds = YES; 122 | } 123 | 124 | -(void)layoutSubviews { 125 | [super layoutSubviews]; 126 | 127 | CGFloat y = 10; 128 | CGFloat xOrigin = 10; 129 | 130 | CGRect rect = [GIResources boundedRectForFont:self.bodyLabel.font andText:self.bodyLabel.text width:self.contentView.frame.size.width - xOrigin*2]; 131 | 132 | self.bodyLabel.frame = CGRectMake(xOrigin, y, rect.size.width, rect.size.height); 133 | 134 | y += self.bodyLabel.frame.size.height + 10; 135 | 136 | self.avatarView.frame = CGRectMake(xOrigin, y, 18, 18); 137 | self.avatarView.layer.cornerRadius = 9; 138 | 139 | [self.userLabel sizeToFit]; 140 | self.userLabel.frame = CGRectMake(xOrigin + self.avatarView.frame.size.width + 5, y, self.userLabel.frame.size.width, self.userLabel.frame.size.height); 141 | 142 | y += self.userLabel.frame.size.height + 10; 143 | 144 | // Updated label. 145 | [self.commentDateLabel sizeToFit]; 146 | self.commentDateLabel.frame = CGRectMake(xOrigin, y, self.commentDateLabel.frame.size.width, self.commentDateLabel.frame.size.height); 147 | 148 | y += self.commentDateLabel.frame.size.height + 10; 149 | 150 | self._viewHeight = y; 151 | } 152 | 153 | @end 154 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesLabelView.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesLabelView.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GIIssuesLabelView : UIView 12 | 13 | @property (nonatomic, strong) UILabel *label; 14 | 15 | -(void)setupWithDictionary:(NSDictionary*)dict; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesLabelView.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesLabelView.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIIssuesLabelView.h" 10 | 11 | @implementation GIIssuesLabelView 12 | 13 | // Assumes input like "00FF00" (RRGGBB). 14 | + (UIColor *)colorFromHexString:(NSString *)hexString { 15 | unsigned rgbValue = 0; 16 | NSScanner *scanner = [NSScanner scannerWithString:hexString]; 17 | [scanner setScanLocation:0]; 18 | [scanner scanHexInt:&rgbValue]; 19 | return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0]; 20 | } 21 | 22 | -(void)setupWithDictionary:(NSDictionary*)dict { 23 | self.label = [[UILabel alloc] initWithFrame:CGRectZero]; 24 | self.label.text = [dict objectForKey:@"name"]; 25 | self.label.textAlignment = NSTextAlignmentCenter; 26 | self.label.font = [UIFont systemFontOfSize:14]; 27 | self.label.textColor = [UIColor colorWithWhite:0.0 alpha:0.4]; 28 | 29 | [self addSubview:self.label]; 30 | 31 | // Configure own params. 32 | self.layer.cornerRadius = 1.5; 33 | self.layer.masksToBounds = YES; 34 | 35 | // And background color! 36 | self.backgroundColor = [GIIssuesLabelView colorFromHexString:[dict objectForKey:@"color"]]; 37 | } 38 | 39 | -(void)layoutSubviews { 40 | [super layoutSubviews]; 41 | 42 | self.label.frame = self.bounds; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesTableViewCell.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GIIssuesTableViewCell : UITableViewCell { 12 | BOOL _usingExtras; 13 | } 14 | 15 | @property (nonatomic, strong) id issue; 16 | @property (nonatomic, readwrite) CGFloat _viewHeight; 17 | 18 | // UI. 19 | @property (nonatomic, strong) UILabel *titleLabel; 20 | @property (nonatomic, strong) UILabel *bodyLabel; 21 | @property (nonatomic, strong) UILabel *commentsLabel; 22 | @property (nonatomic, strong) UILabel *userLabel; 23 | @property (nonatomic, strong) UIImageView *userImageView; 24 | @property (nonatomic, strong) UIView *labelRegionView; 25 | @property (nonatomic, strong) UILabel *updatedAtLabel; 26 | @property (nonatomic, strong) UIView *closeOpenIndicator; 27 | 28 | -(void)setupWithIssue:(id)issue withExtras:(BOOL)extras; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesTableViewCell.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIIssuesTableViewCell.h" 10 | #import "OCTIssue+New.h" 11 | #import "GIIssuesLabelView.h" 12 | #import "GIResources.h" 13 | #import 14 | 15 | @implementation GIIssuesTableViewCell 16 | 17 | - (void)awakeFromNib { 18 | [super awakeFromNib]; 19 | // Initialization code 20 | } 21 | 22 | - (void)setSelected:(BOOL)selected animated:(BOOL)animated { 23 | [super setSelected:selected animated:animated]; 24 | 25 | // Configure the view for the selected state 26 | } 27 | 28 | -(void)_configureViewsIfNeeded { 29 | if (!self.titleLabel) { 30 | self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 31 | self.titleLabel.text = @"TITLE"; 32 | self.titleLabel.textColor = [UIColor darkTextColor]; 33 | self.titleLabel.font = [UIFont systemFontOfSize:16]; 34 | self.titleLabel.textAlignment = NSTextAlignmentLeft; 35 | self.titleLabel.numberOfLines = 0; 36 | 37 | [self.contentView addSubview:self.titleLabel]; 38 | } 39 | 40 | if (!self.bodyLabel) { 41 | self.bodyLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 42 | self.bodyLabel.text = @""; 43 | self.bodyLabel.textColor = [UIColor grayColor]; 44 | self.bodyLabel.font = [UIFont systemFontOfSize:16]; 45 | self.bodyLabel.textAlignment = NSTextAlignmentLeft; 46 | self.bodyLabel.numberOfLines = 0; 47 | 48 | [self.contentView addSubview:self.bodyLabel]; 49 | } 50 | 51 | if (!self.commentsLabel) { 52 | self.commentsLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 53 | self.commentsLabel.text = @"0 comments"; 54 | self.commentsLabel.textColor = [UIColor grayColor]; 55 | self.commentsLabel.font = [UIFont systemFontOfSize:14]; 56 | self.commentsLabel.textAlignment = NSTextAlignmentLeft; 57 | 58 | [self.contentView addSubview:self.commentsLabel]; 59 | } 60 | 61 | if (!self.userImageView) { 62 | self.userImageView = [[UIImageView alloc] initWithImage:nil]; 63 | self.userImageView.backgroundColor = [UIColor lightGrayColor]; 64 | 65 | [self.contentView addSubview:self.userImageView]; 66 | } 67 | 68 | if (!self.userLabel) { 69 | self.userLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 70 | self.userLabel.text = @"USER"; 71 | self.userLabel.textColor = [UIColor darkTextColor]; 72 | self.userLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]; 73 | self.userLabel.textAlignment = NSTextAlignmentLeft; 74 | 75 | [self.contentView addSubview:self.userLabel]; 76 | } 77 | 78 | if (!self.labelRegionView) { 79 | self.labelRegionView = [[UIView alloc] initWithFrame:CGRectZero]; 80 | self.labelRegionView.backgroundColor = [UIColor clearColor]; 81 | self.labelRegionView.userInteractionEnabled = NO; 82 | 83 | [self.contentView addSubview:self.labelRegionView]; 84 | } 85 | 86 | if (!self.updatedAtLabel) { 87 | self.updatedAtLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 88 | self.updatedAtLabel.text = @"0mins ago"; 89 | self.updatedAtLabel.textColor = [UIColor colorWithWhite:0.75 alpha:1.0]; 90 | self.updatedAtLabel.font = [UIFont systemFontOfSize:12]; 91 | self.updatedAtLabel.textAlignment = NSTextAlignmentLeft; 92 | 93 | [self.contentView addSubview:self.updatedAtLabel]; 94 | } 95 | 96 | if (!self.closeOpenIndicator) { 97 | self.closeOpenIndicator = [[UIView alloc] initWithFrame:CGRectZero]; 98 | 99 | [self.contentView addSubview:self.closeOpenIndicator]; 100 | } 101 | 102 | self.textLabel.numberOfLines = 0; 103 | } 104 | 105 | -(void)prepareForReuse { 106 | [super prepareForReuse]; 107 | 108 | [self.userImageView cancelImageRequestOperation]; 109 | } 110 | 111 | -(void)setupWithIssue:(OCTIssueNew*)issue withExtras:(BOOL)extras { 112 | self.issue = issue; 113 | self.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 114 | _usingExtras = extras; 115 | 116 | [self _configureViewsIfNeeded]; 117 | 118 | NSString *title = [NSString stringWithFormat:@"#%@ %@", issue.number, issue.title]; 119 | 120 | NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys: 121 | [UIFont systemFontOfSize:16], NSFontAttributeName, 122 | [UIColor darkTextColor], NSForegroundColorAttributeName, 123 | nil]; 124 | 125 | NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:title attributes:attributesDictionary]; 126 | [attr addAttribute:NSFontAttributeName 127 | value:[UIFont systemFontOfSize:14.0] 128 | range:NSMakeRange(0, 1 + issue.number.length)]; 129 | [attr addAttribute:NSForegroundColorAttributeName 130 | value:[UIColor colorWithWhite:0.5 alpha:1.0] 131 | range:NSMakeRange(0, 1 + issue.number.length)]; 132 | 133 | NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; 134 | paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; 135 | 136 | [attr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [attr length])]; 137 | 138 | self.titleLabel.attributedText = attr; 139 | 140 | if (_usingExtras) { 141 | self.bodyLabel.text = issue.body; 142 | } 143 | 144 | int count = [issue.comments intValue]; 145 | self.commentsLabel.text = [NSString stringWithFormat:count != 1 ? @"%d comments" : @"%d comment", count]; 146 | 147 | self.userLabel.text = [issue.user objectForKey:@"login"]; 148 | 149 | // Labels. 150 | for (UIView *view in self.labelRegionView.subviews) { 151 | [view removeFromSuperview]; 152 | } 153 | 154 | for (NSDictionary *labelDict in issue.labels) { 155 | // Add each label to the region view. 156 | GIIssuesLabelView *view = [[GIIssuesLabelView alloc] initWithFrame:CGRectZero]; 157 | [view setupWithDictionary:labelDict]; 158 | 159 | [self.labelRegionView addSubview:view]; 160 | } 161 | 162 | self.closeOpenIndicator.backgroundColor = issue.state == OCTIssueStateOpen ? 163 | [UIColor colorWithRed:0.35 green:0.80 blue:0.22 alpha:1.0] : 164 | [UIColor colorWithRed:0.80 green:0.22 blue:0.22 alpha:1.0]; 165 | 166 | // Format the updated label. 167 | self.updatedAtLabel.text = [GIResources formatDate:issue.createdAt]; 168 | 169 | NSString *avatarURL = [issue.user objectForKey:@"avatar_url"]; 170 | NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:avatarURL]]; 171 | 172 | GIIssuesTableViewCell * __weak weakself = self; 173 | 174 | [self.userImageView setImageWithURLRequest:request placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) { 175 | [weakself setUserImage:image]; 176 | } failure:nil]; 177 | } 178 | 179 | -(void)setUserImage:(UIImage*)img { 180 | self.userImageView.image = img; 181 | self.userImageView.frame = CGRectMake(self.userImageView.frame.origin.x, self.userImageView.frame.origin.y, 18, 18); 182 | self.userImageView.layer.cornerRadius = 9; 183 | self.userImageView.layer.masksToBounds = YES; 184 | } 185 | 186 | -(void)layoutSubviews { 187 | [super layoutSubviews]; 188 | 189 | self.closeOpenIndicator.frame = CGRectMake(1, 5, 1.5, self.contentView.frame.size.height-10); 190 | 191 | CGFloat y = 10; 192 | CGFloat xOrigin = 10; 193 | 194 | // Alright, let's lay this damn thing out! 195 | CGRect rect = [GIResources boundedRectForFont:self.titleLabel.font andText:self.titleLabel.attributedText width:self.contentView.frame.size.width - xOrigin]; 196 | self.titleLabel.frame = CGRectMake(xOrigin, y, rect.size.width, rect.size.height); 197 | 198 | y += self.titleLabel.frame.size.height + 10; 199 | 200 | if (_usingExtras) { 201 | // When using extras, the accessory indicator will be "none". 202 | rect = [GIResources boundedRectForFont:self.bodyLabel.font andText:self.bodyLabel.text width:self.contentView.frame.size.width - xOrigin*2]; 203 | 204 | self.bodyLabel.frame = CGRectMake(xOrigin, y, rect.size.width, rect.size.height); 205 | 206 | y += self.bodyLabel.frame.size.height + 10; 207 | } 208 | 209 | // We will assume a label will be 20px high, and its inner text + 20 margin for width. 210 | 211 | int i = 0; 212 | CGFloat widthLeftInRow = self.contentView.frame.size.width - xOrigin; 213 | CGFloat xOnRow = 0; 214 | for (GIIssuesLabelView *view in self.labelRegionView.subviews) { 215 | rect = [GIResources boundedRectForFont:view.label.font andText:view.label.text width:self.contentView.frame.size.width - xOrigin]; 216 | 217 | if (rect.size.width + 25 < widthLeftInRow) { 218 | view.frame = CGRectMake(xOnRow, i*5 + i*20, rect.size.width + 20, 20); 219 | xOnRow += view.frame.size.width + 5; 220 | widthLeftInRow -= xOnRow; 221 | } else { 222 | i++; 223 | widthLeftInRow = self.contentView.frame.size.width - xOrigin; 224 | xOnRow = 0; 225 | 226 | view.frame = CGRectMake(0, i*5 + i*20, rect.size.width + 20, 20); 227 | xOnRow += view.frame.size.width + 5; 228 | widthLeftInRow -= xOnRow; 229 | } 230 | } 231 | 232 | UIView *bottomView = [self.labelRegionView.subviews lastObject]; 233 | self.labelRegionView.frame = CGRectMake(xOrigin, y, self.contentView.frame.size.width - xOrigin, bottomView.frame.origin.y + bottomView.frame.size.height); 234 | 235 | y += self.labelRegionView.frame.size.height + 10; 236 | 237 | self.userImageView.frame = CGRectMake(xOrigin, y, 18, 18); 238 | self.userImageView.layer.cornerRadius = 9; 239 | 240 | [self.userLabel sizeToFit]; 241 | self.userLabel.frame = CGRectMake(xOrigin + self.userImageView.frame.size.width + 5, y, self.userLabel.frame.size.width, self.userLabel.frame.size.height); 242 | 243 | [self.commentsLabel sizeToFit]; 244 | self.commentsLabel.frame = CGRectMake(self.userLabel.frame.origin.x + 5 + self.userLabel.frame.size.width, y, self.commentsLabel.frame.size.width, self.commentsLabel.frame.size.height); 245 | 246 | y += self.userLabel.frame.size.height + 10; 247 | 248 | // Updated label. 249 | [self.updatedAtLabel sizeToFit]; 250 | self.updatedAtLabel.frame = CGRectMake(xOrigin, y, self.updatedAtLabel.frame.size.width, self.updatedAtLabel.frame.size.height); 251 | 252 | y += self.updatedAtLabel.frame.size.height + 10; 253 | 254 | self._viewHeight = y; 255 | } 256 | 257 | @end 258 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesViewController.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 17/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "GILoginController.h" 11 | #import "GIIssueComposeController.h" 12 | 13 | @interface GIIssuesViewController : UITableViewController { 14 | NSArray *_dataSource; 15 | UISegmentedControl *_segmented; 16 | BOOL _showNewIssueAfterAuthentication; 17 | } 18 | 19 | @property (nonatomic, strong) UIActivityIndicatorView *spinner; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /libGitHubIssues/GIIssuesViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIIssuesViewController.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 17/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIIssuesViewController.h" 10 | #import "GIResources.h" 11 | #import "OCTClient_OCTClient_Issues.h" 12 | #import "OCTIssue+New.h" 13 | #import "GIIssuesTableViewCell.h" 14 | #import "GIIssueDetailTableController.h" 15 | #import "GIUserViewController.h" 16 | 17 | #define REUSE @"com.matchstic.issues" 18 | #define REUSE_TOP @"com.matchstic.create" 19 | 20 | static GIIssuesTableViewCell *heightCheckerCell; 21 | 22 | @interface GIIssuesViewController () 23 | 24 | @end 25 | 26 | @implementation GIIssuesViewController 27 | 28 | -(instancetype)init { 29 | self = [super initWithStyle:UITableViewStyleGrouped]; 30 | 31 | if (self) { 32 | _dataSource = [NSArray array]; 33 | } 34 | 35 | return self; 36 | } 37 | 38 | - (void)viewDidLoad { 39 | [super viewDidLoad]; 40 | 41 | [self.tableView registerClass:[GIIssuesTableViewCell class] forCellReuseIdentifier:REUSE]; 42 | [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:REUSE_TOP]; 43 | 44 | [self reloadWithMode:0]; 45 | 46 | // Add segmented view as title. 47 | _segmented = [[UISegmentedControl alloc] initWithItems:@[@"Open", @"Closed"]]; 48 | [_segmented addTarget:self action:@selector(_segmentedChanged:) forControlEvents:UIControlEventValueChanged]; 49 | _segmented.selectedSegmentIndex = 0; 50 | 51 | self.navigationItem.titleView = _segmented; 52 | 53 | // Add the right item for profile. 54 | UIImage *icon = [GIResources imageWithName:@"libGitHubIssues_Profile"]; 55 | UIBarButtonItem *profileItem = [[UIBarButtonItem alloc] initWithImage:icon style:UIBarButtonItemStylePlain target:self action:@selector(didTapProfileButton:)]; 56 | self.navigationItem.rightBarButtonItem = profileItem; 57 | } 58 | 59 | -(void)_segmentedChanged:(UISegmentedControl*)sender { 60 | [self reloadWithMode:(int)sender.selectedSegmentIndex]; 61 | } 62 | 63 | -(void)reloadWithMode:(int)mode { 64 | self.navigationItem.titleView = nil; 65 | 66 | [self.navigationItem setTitle:@"Loading..."]; 67 | [self setTitle:@"Loading..."]; 68 | 69 | // Pull down issues. We ideally should split them up so it's not a huge download. 70 | RACSignal *request = (RACSignal*)[GIResources _getCurrentRepository]; 71 | [[request collect] subscribeNext:^(NSArray *repositories) { 72 | OCTRepository *repo = [repositories firstObject]; 73 | RACSignal *sigTwo = [(OCTClient*)[GIResources _getCurrentClient] fetchIssuesForRepository:repo state:mode == 0 ? OCTClientIssueStateOpen : OCTClientIssueStateClosed notMatchingEtag:nil since:nil]; 74 | 75 | [[sigTwo collect] subscribeNext:^(NSArray *issues) { 76 | 77 | NSMutableArray *newData = [NSMutableArray array]; 78 | 79 | // Array of OCTResponse. 80 | for (OCTResponse *response in issues) { 81 | OCTIssueNew *parsed = response.parsedResult; 82 | 83 | [newData addObject:parsed]; 84 | } 85 | 86 | _dataSource = newData; 87 | dispatch_async(dispatch_get_main_queue(), ^{ 88 | [self.navigationItem setTitle:@"Back"]; 89 | [self setTitle:@"Back"]; 90 | 91 | self.navigationItem.titleView = _segmented; 92 | 93 | [self.tableView reloadData]; 94 | }); 95 | 96 | } error:^(NSError *error) { 97 | // Invoked when an error occurs. You won't receive any results if this 98 | // happens. 99 | [self _presentError:error]; 100 | 101 | [self.navigationItem setTitle:@"Error"]; 102 | [self setTitle:@"Error"]; 103 | }]; 104 | } error:^(NSError *error) { 105 | // Invoked when an error occurs. You won't receive any results if this 106 | // happens. 107 | [self _presentError:error]; 108 | 109 | [self.navigationItem setTitle:@"Error"]; 110 | [self setTitle:@"Error"]; 111 | }]; 112 | } 113 | 114 | - (void)didReceiveMemoryWarning { 115 | [super didReceiveMemoryWarning]; 116 | // Dispose of any resources that can be recreated. 117 | } 118 | 119 | -(void)_presentError:(NSError*)error { 120 | dispatch_async(dispatch_get_main_queue(), ^{ 121 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"%@", error.localizedFailureReason] preferredStyle:UIAlertControllerStyleAlert]; 122 | 123 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault 124 | handler:^(UIAlertAction * action) {}]; 125 | 126 | [alert addAction:defaultAction]; 127 | [self presentViewController:alert animated:YES completion:nil]; 128 | }); 129 | } 130 | 131 | #pragma mark - Table view data source 132 | 133 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 134 | return 2; 135 | } 136 | 137 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 138 | return section == 0 ? 1 : _dataSource.count; 139 | } 140 | 141 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 142 | if (indexPath.section == 1) { 143 | GIIssuesTableViewCell *cell = (GIIssuesTableViewCell*)[tableView dequeueReusableCellWithIdentifier:REUSE forIndexPath:indexPath]; 144 | if (!cell) { 145 | cell = [[GIIssuesTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:REUSE]; 146 | } 147 | 148 | // Configure the cell... 149 | OCTIssueNew *issue = [_dataSource objectAtIndex:indexPath.row]; 150 | [cell setupWithIssue:issue withExtras:NO]; 151 | 152 | return cell; 153 | } else { 154 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:REUSE_TOP forIndexPath:indexPath]; 155 | if (!cell) { 156 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:REUSE_TOP]; 157 | } 158 | 159 | cell.textLabel.text = @"Create New Issue..."; 160 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 161 | 162 | return cell; 163 | } 164 | } 165 | 166 | -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 167 | if (indexPath.section == 1) { 168 | OCTIssueNew *issue = [_dataSource objectAtIndex:indexPath.row]; 169 | 170 | if (!heightCheckerCell) { 171 | heightCheckerCell = [[GIIssuesTableViewCell alloc] initWithFrame:CGRectZero]; 172 | } 173 | 174 | // Set width correctly to the cell. 175 | [heightCheckerCell setupWithIssue:issue withExtras:NO]; 176 | heightCheckerCell.frame = CGRectMake(0, 0, self.tableView.frame.size.width-20, 0); 177 | [heightCheckerCell layoutSubviews]; 178 | 179 | return heightCheckerCell._viewHeight; 180 | } else { 181 | return 40; 182 | } 183 | } 184 | 185 | -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 186 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 187 | 188 | if (indexPath.section == 1) { 189 | OCTIssueNew *issue = [_dataSource objectAtIndex:indexPath.row]; 190 | 191 | // Move to this issue's detail view. 192 | GIIssueDetailTableController *detail = [[GIIssueDetailTableController alloc] initWithIssue:issue]; 193 | [self.navigationController pushViewController:detail animated:YES]; 194 | } else { 195 | // Display create new issue UI! 196 | OCTClient *client = [GIResources _getCurrentClient]; 197 | 198 | if (!client.token || [client.token isEqualToString:@""]) { 199 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Sign In Required" message:@"You need to be signed in to create a new issue.\n\nSign in now?" preferredStyle:UIAlertControllerStyleAlert]; 200 | 201 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"Sign In" style:UIAlertActionStyleDefault 202 | handler:^(UIAlertAction * action) { 203 | 204 | // Go to login UI. 205 | _showNewIssueAfterAuthentication = YES; 206 | [self openLoginController]; 207 | 208 | }]; 209 | 210 | [alert addAction:defaultAction]; 211 | 212 | UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel 213 | handler:^(UIAlertAction * action) { 214 | 215 | }]; 216 | [alert addAction:cancelAction]; 217 | [self presentViewController:alert animated:YES completion:nil]; 218 | } else { 219 | // TODO: Push into compose UI 220 | GIIssueComposeController *compose = [[GIIssueComposeController alloc] init]; 221 | compose.delegate = self; 222 | [self.navigationController pushViewController:compose animated:YES]; 223 | } 224 | } 225 | } 226 | 227 | -(void)openLoginController { 228 | GILoginController *login = [[GILoginController alloc] init]; 229 | login.delegate = self; 230 | 231 | [self.navigationController pushViewController:login animated:YES]; 232 | } 233 | 234 | -(void)didFinishAuthenticationWithClient:(id)client { 235 | [self.navigationController popToViewController:self animated:YES]; 236 | 237 | // Now push into compose UI. 238 | if (_showNewIssueAfterAuthentication) { 239 | 240 | } else { 241 | GIUserViewController *user = [[GIUserViewController alloc] init]; 242 | [self.navigationController pushViewController:user animated:YES]; 243 | } 244 | } 245 | 246 | -(void)didSendIssue { 247 | // Reload UI and pop back here. 248 | [self.navigationController popToViewController:self animated:YES]; 249 | 250 | [self reloadWithMode:(int)_segmented.selectedSegmentIndex]; 251 | } 252 | 253 | -(void)didTapProfileButton:(id)sender { 254 | OCTClient *client = [GIResources _getCurrentClient]; 255 | 256 | if (!client.token || [client.token isEqualToString:@""]) { 257 | _showNewIssueAfterAuthentication = NO; 258 | [self openLoginController]; 259 | } else { 260 | GIUserViewController *user = [[GIUserViewController alloc] init]; 261 | [self.navigationController pushViewController:user animated:YES]; 262 | } 263 | } 264 | 265 | @end 266 | -------------------------------------------------------------------------------- /libGitHubIssues/GILoginController.h: -------------------------------------------------------------------------------- 1 | // 2 | // GILoginController.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 16/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @protocol GILoginControllerDelegate 12 | 13 | -(void)didFinishAuthenticationWithClient:(id)client; 14 | 15 | @end 16 | 17 | @interface GILoginController : UIViewController 18 | 19 | @property (nonatomic, strong) id delegate; 20 | 21 | @property (nonatomic, strong) UIView *backer; 22 | 23 | @property (nonatomic, strong) UIImageView *headerImageView; 24 | @property (nonatomic, strong) UILabel *explainLabel; 25 | 26 | @property (nonatomic, strong) UITextField *username; 27 | @property (nonatomic, strong) UITextField *password; 28 | @property (nonatomic, strong) UITextField *twoFactorAuthPassword; 29 | @property (nonatomic, strong) UIButton *loginButton; 30 | @property (nonatomic, strong) UIButton *twoFAButton; 31 | @property (nonatomic, strong) UIButton *cancelButton; 32 | @property (nonatomic, strong) UIActivityIndicatorView *spinny; 33 | 34 | @property (nonatomic, strong) UIView *separatorView; 35 | @property (nonatomic, strong) UIButton *createAccount; 36 | 37 | -(void)_show2FAWithAnimation:(BOOL)anim; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /libGitHubIssues/GILoginController.m: -------------------------------------------------------------------------------- 1 | // 2 | // GILoginController.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 16/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GILoginController.h" 10 | #import "GIResources.h" 11 | #import "OCTClient+Fingerprint.h" 12 | #import 13 | 14 | typedef enum : NSUInteger { 15 | kGIStateBegin, 16 | kGIStateWaiting2FA, 17 | } GILoginState; 18 | 19 | @interface GILoginController () 20 | 21 | @property (nonatomic, readwrite) GILoginState state; 22 | 23 | @end 24 | 25 | @implementation GILoginController 26 | 27 | -(instancetype)init { 28 | self = [super init]; 29 | 30 | if (self) { 31 | [self.navigationItem setTitle:@"Sign In"]; 32 | [self setTitle:@"Sign In"]; 33 | self.state = kGIStateBegin; 34 | } 35 | 36 | return self; 37 | } 38 | 39 | - (void)viewDidLoad { 40 | [super viewDidLoad]; 41 | // Do any additional setup after loading the view. 42 | } 43 | 44 | - (void)didReceiveMemoryWarning { 45 | [super didReceiveMemoryWarning]; 46 | // Dispose of any resources that can be recreated. 47 | } 48 | 49 | -(void)loadView { 50 | self.view = [[UIView alloc] initWithFrame:CGRectZero]; 51 | self.view.backgroundColor = [UIColor whiteColor]; 52 | 53 | self.backer = [[UIView alloc] initWithFrame:CGRectZero]; 54 | self.backer.backgroundColor = [UIColor whiteColor]; 55 | [self.view addSubview:self.backer]; 56 | 57 | UIImage *ret = [GIResources imageWithName:@"libGitHubIssues_Mark"]; 58 | 59 | self.headerImageView = [[UIImageView alloc] initWithImage:ret]; 60 | self.headerImageView.frame = CGRectMake(0, 0, 50, 50); 61 | 62 | [self.backer addSubview:self.headerImageView]; 63 | 64 | self.explainLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 65 | self.explainLabel.text = @"GitHub is used to keep track of bugs and features"; 66 | self.explainLabel.numberOfLines = 0; 67 | self.explainLabel.textColor = [UIColor darkTextColor]; 68 | self.explainLabel.textAlignment = NSTextAlignmentCenter; 69 | self.explainLabel.font = [UIFont systemFontOfSize:[UIFont systemFontSize]]; 70 | 71 | [self.backer addSubview:self.explainLabel]; 72 | 73 | UIView *leftBlock = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)]; 74 | leftBlock.backgroundColor = [UIColor clearColor]; 75 | 76 | UIView *leftBlock2 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)]; 77 | leftBlock2.backgroundColor = [UIColor clearColor]; 78 | 79 | UIView *leftBlock3 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)]; 80 | leftBlock3.backgroundColor = [UIColor clearColor]; 81 | 82 | self.username = [[UITextField alloc] initWithFrame:CGRectZero]; 83 | self.username.textAlignment = NSTextAlignmentNatural; 84 | self.username.textColor = [UIColor grayColor]; 85 | self.username.placeholder = @"Username..."; 86 | self.username.autocapitalizationType = UITextAutocapitalizationTypeNone; 87 | self.username.autocorrectionType = UITextAutocorrectionTypeNo; 88 | self.username.returnKeyType = UIReturnKeyNext; 89 | self.username.delegate = self; 90 | [self.username addTarget:self action:@selector(textFieldContentDidChange:) forControlEvents:UIControlEventEditingChanged]; 91 | self.username.layer.cornerRadius = 2.5; 92 | self.username.layer.borderColor = [UIColor lightGrayColor].CGColor; 93 | self.username.layer.borderWidth = 1; 94 | self.username.leftView = leftBlock; 95 | self.username.leftViewMode = UITextFieldViewModeAlways; 96 | 97 | [self.backer addSubview:self.username]; 98 | 99 | self.password = [[UITextField alloc] initWithFrame:CGRectZero]; 100 | self.password.textAlignment = NSTextAlignmentNatural; 101 | self.password.textColor = [UIColor grayColor]; 102 | self.password.placeholder = @"Password..."; 103 | self.password.autocorrectionType = UITextAutocorrectionTypeNo; 104 | self.password.autocapitalizationType = UITextAutocapitalizationTypeNone; 105 | self.password.secureTextEntry = YES; 106 | self.password.returnKeyType = UIReturnKeyDone; 107 | self.password.delegate = self; 108 | [self.password addTarget:self action:@selector(textFieldContentDidChange:) forControlEvents:UIControlEventEditingChanged]; 109 | self.password.layer.cornerRadius = 2.5; 110 | self.password.layer.borderColor = [UIColor lightGrayColor].CGColor; 111 | self.password.layer.borderWidth = 1; 112 | self.password.leftView = leftBlock2; 113 | self.password.leftViewMode = UITextFieldViewModeAlways; 114 | 115 | [self.backer addSubview:self.password]; 116 | 117 | self.twoFactorAuthPassword = [[UITextField alloc] initWithFrame:CGRectZero]; 118 | self.twoFactorAuthPassword.textAlignment = NSTextAlignmentNatural; 119 | self.twoFactorAuthPassword.textColor = [UIColor grayColor]; 120 | self.twoFactorAuthPassword.placeholder = @"Enter code..."; 121 | self.twoFactorAuthPassword.autocapitalizationType = UITextAutocapitalizationTypeNone; 122 | self.twoFactorAuthPassword.autocorrectionType = UITextAutocorrectionTypeNo; 123 | self.twoFactorAuthPassword.secureTextEntry = YES; 124 | self.twoFactorAuthPassword.returnKeyType = UIReturnKeyDone; 125 | self.twoFactorAuthPassword.delegate = self; 126 | [self.twoFactorAuthPassword addTarget:self action:@selector(textFieldContentDidChange2FA:) forControlEvents:UIControlEventEditingChanged]; 127 | self.twoFactorAuthPassword.layer.cornerRadius = 2.5; 128 | self.twoFactorAuthPassword.layer.borderColor = [UIColor lightGrayColor].CGColor; 129 | self.twoFactorAuthPassword.layer.borderWidth = 1; 130 | self.twoFactorAuthPassword.leftView = leftBlock3; 131 | self.twoFactorAuthPassword.leftViewMode = UITextFieldViewModeAlways; 132 | self.twoFactorAuthPassword.hidden = YES; 133 | 134 | [self.backer addSubview:self.twoFactorAuthPassword]; 135 | 136 | self.loginButton = [UIButton buttonWithType:UIButtonTypeSystem]; 137 | [self.loginButton addTarget:self action:@selector(didClickLoginButton:) forControlEvents:UIControlEventTouchUpInside]; 138 | [self.loginButton setTitle:@"Sign In" forState:UIControlStateNormal]; 139 | [self.loginButton setTitle:@"Sign In" forState:UIControlStateDisabled]; 140 | self.loginButton.enabled = NO; 141 | 142 | // Styling. 143 | [self.loginButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; 144 | [self.loginButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted]; 145 | [self.loginButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled]; 146 | self.loginButton.layer.borderWidth = 1; 147 | self.loginButton.backgroundColor = [UIColor colorWithRed:0.83 green:0.83 blue:0.84 alpha:1.0]; 148 | self.loginButton.layer.borderColor = [UIColor lightGrayColor].CGColor; 149 | self.loginButton.layer.cornerRadius = 2.5; 150 | 151 | [self.backer addSubview:self.loginButton]; 152 | 153 | // Submit code for 2FA 154 | self.twoFAButton = [UIButton buttonWithType:UIButtonTypeSystem]; 155 | [self.twoFAButton addTarget:self action:@selector(didClickLoginWith2FA:) forControlEvents:UIControlEventTouchUpInside]; 156 | [self.twoFAButton setTitle:@"Submit Code" forState:UIControlStateNormal]; 157 | [self.twoFAButton setTitle:@"Submit Code" forState:UIControlStateDisabled]; 158 | self.twoFAButton.enabled = NO; 159 | self.twoFAButton.hidden = YES; 160 | 161 | // Styling. 162 | [self.twoFAButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal]; 163 | [self.twoFAButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateHighlighted]; 164 | [self.twoFAButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled]; 165 | self.twoFAButton.layer.borderWidth = 1; 166 | self.twoFAButton.backgroundColor = [UIColor colorWithRed:0.83 green:0.83 blue:0.84 alpha:1.0]; 167 | self.twoFAButton.layer.borderColor = [UIColor lightGrayColor].CGColor; 168 | self.twoFAButton.layer.cornerRadius = 2.5; 169 | 170 | [self.backer addSubview:self.twoFAButton]; 171 | 172 | // Cancel button for going back from 2FA UI 173 | self.cancelButton = [UIButton buttonWithType:UIButtonTypeSystem]; 174 | [self.cancelButton addTarget:self action:@selector(didClickCancel:) forControlEvents:UIControlEventTouchUpInside]; 175 | [self.cancelButton setTitle:@"Cancel" forState:UIControlStateNormal]; 176 | self.cancelButton.enabled = YES; 177 | self.cancelButton.hidden = YES; 178 | 179 | // Styling. 180 | [self.cancelButton setTitleColor:[UIColor colorWithRed:0.86 green:0.38 blue:0.38 alpha:1.0] forState:UIControlStateNormal]; 181 | [self.cancelButton setTitleColor:[UIColor colorWithRed:0.87 green:0.61 blue:0.61 alpha:1.0] forState:UIControlStateHighlighted]; 182 | self.cancelButton.layer.borderWidth = 1; 183 | self.cancelButton.backgroundColor = [UIColor colorWithRed:0.93 green:0.80 blue:0.80 alpha:1.0]; 184 | self.cancelButton.layer.borderColor = [UIColor colorWithRed:0.87 green:0.61 blue:0.61 alpha:1.0].CGColor; 185 | self.cancelButton.layer.cornerRadius = 2.5; 186 | 187 | [self.backer addSubview:self.cancelButton]; 188 | 189 | self.separatorView = [[UIView alloc] initWithFrame:CGRectZero]; 190 | self.separatorView.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0]; 191 | 192 | [self.backer addSubview:self.separatorView]; 193 | 194 | self.createAccount = [UIButton buttonWithType:UIButtonTypeSystem]; 195 | [self.createAccount addTarget:self action:@selector(didClickCreateNewAccount:) forControlEvents:UIControlEventTouchUpInside]; 196 | [self.createAccount setTitle:@"Sign Up for GitHub" forState:UIControlStateNormal]; 197 | 198 | [self.createAccount setTitleColor:[UIColor colorWithRed:103.0f/255.0f green:153.0f/255.0f blue:76.0f/255.0f alpha:1.0] forState:UIControlStateNormal]; 199 | [self.createAccount setTitleColor:[UIColor colorWithRed:180.0f/255.0f green:219.0f/255.0f blue:158.0f/255.0f alpha:1.0] forState:UIControlStateHighlighted]; 200 | self.createAccount.layer.borderWidth = 1; 201 | self.createAccount.backgroundColor = [UIColor colorWithRed:218.0f/255.0f green:241.0f/255.0f blue:205.0f/255.0f alpha:1.0]; 202 | self.createAccount.layer.borderColor = [UIColor colorWithRed:180.0f/255.0f green:219.0f/255.0f blue:158.0f/255.0f alpha:1.0].CGColor; 203 | self.createAccount.layer.cornerRadius = 2.5; 204 | 205 | [self.backer addSubview:self.createAccount]; 206 | 207 | self.spinny = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)]; 208 | self.spinny.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; 209 | self.spinny.hidden = YES; 210 | 211 | [self.backer addSubview:self.spinny]; 212 | } 213 | 214 | -(void)viewDidLayoutSubviews { 215 | // Setup our UI. 216 | [super viewDidLayoutSubviews]; 217 | 218 | // Set initial width for backer. 219 | // TODO: Adjust for iPad. 220 | CGFloat maxWidth = self.view.frame.size.width*0.75; 221 | 222 | CGFloat y = 0; 223 | 224 | self.headerImageView.frame = CGRectMake(maxWidth/2 - 25, y, 50, 50); 225 | 226 | y += self.headerImageView.frame.size.height + 10; 227 | 228 | CGRect rect = [GIResources boundedRectForFont:self.explainLabel.font andText:self.explainLabel.text width:maxWidth]; 229 | self.explainLabel.frame = CGRectMake(maxWidth/2 - rect.size.width/2, y, rect.size.width, rect.size.height); 230 | 231 | y += self.explainLabel.frame.size.height + 20; 232 | 233 | if (self.state == kGIStateBegin) { 234 | self.username.frame = CGRectMake(0, y, maxWidth, 40); 235 | 236 | y += self.username.frame.size.height + 5; 237 | 238 | self.password.frame = CGRectMake(self.username.frame.origin.x, y, self.username.frame.size.width, self.username.frame.size.height); 239 | 240 | y += self.password.frame.size.height + 5; 241 | 242 | self.loginButton.frame = CGRectMake(self.username.frame.origin.x, y, self.username.frame.size.width, 40); 243 | 244 | y += self.loginButton.frame.size.height + 20; 245 | 246 | self.separatorView.frame = CGRectMake(self.username.frame.origin.x, y, self.username.frame.size.width, 1); 247 | 248 | y += 21; 249 | 250 | self.createAccount.frame = CGRectMake(self.username.frame.origin.x, y, self.username.frame.size.width, self.loginButton.frame.size.height); 251 | 252 | y += self.createAccount.frame.size.height; 253 | } else { 254 | self.twoFactorAuthPassword.frame = CGRectMake(0, y, maxWidth, 40); 255 | 256 | y += self.twoFactorAuthPassword.frame.size.height + 5; 257 | 258 | self.twoFAButton.frame = CGRectMake(self.twoFactorAuthPassword.frame.origin.x, y, self.twoFactorAuthPassword.frame.size.width, 40); 259 | 260 | y += self.twoFAButton.frame.size.height + 20; 261 | 262 | self.separatorView.frame = CGRectMake(self.username.frame.origin.x, y, self.username.frame.size.width, 1); 263 | 264 | y += 21; 265 | 266 | self.cancelButton.frame = CGRectMake(self.twoFactorAuthPassword.frame.origin.x, y, self.twoFactorAuthPassword.frame.size.width, 40); 267 | 268 | y += self.cancelButton.frame.size.height; 269 | } 270 | 271 | self.backer.frame = CGRectMake(self.view.frame.size.width/2 - maxWidth/2, self.view.frame.size.height/2 - y/2, maxWidth, y); 272 | } 273 | 274 | #pragma mark UIButton Callbacks 275 | 276 | -(void)didClickLoginButton:(id)sender { 277 | NSLog(@"Clicked login!"); 278 | 279 | [self.username resignFirstResponder]; 280 | [self.password resignFirstResponder]; 281 | 282 | NSString *pass = self.password.text; 283 | 284 | OCTUser *user = [OCTUser userWithRawLogin:self.username.text server:OCTServer.dotComServer]; 285 | 286 | self.spinny.hidden = NO; 287 | self.spinny.alpha = 0.0; 288 | self.spinny.center = self.loginButton.center; 289 | [self.spinny startAnimating]; 290 | 291 | [UIView animateWithDuration:0.15 animations:^{ 292 | self.spinny.alpha = 1.0; 293 | self.loginButton.alpha = 0.25; 294 | }]; 295 | 296 | NSString *fingerprint = [GIResources _generateFingerprint]; 297 | 298 | [[[OCTClient 299 | _gi_signInAsUser:user password:pass oneTimePassword:nil scopes:OCTClientAuthorizationScopesUser | OCTClientAuthorizationScopesRepository note:@"GitHub Issues App" noteURL:nil fingerprint:fingerprint] 300 | deliverOn:RACScheduler.mainThreadScheduler] 301 | subscribeNext:^(OCTClient *client) { 302 | 303 | // We have success! 304 | [self _successfulAuth:client]; 305 | 306 | [UIView animateWithDuration:0.15 animations:^{ 307 | self.spinny.alpha = 0.0; 308 | self.loginButton.alpha = 1.0; 309 | } completion:^(BOOL finished) { 310 | [self.spinny stopAnimating]; 311 | self.spinny.hidden = YES; 312 | }]; 313 | 314 | } error:^(NSError *error) { 315 | if ([error.domain isEqual:OCTClientErrorDomain] && error.code == OCTClientErrorTwoFactorAuthenticationOneTimePasswordRequired) { 316 | // Show OTP field and have the user try again. 317 | [self _show2FAWithAnimation:YES]; 318 | } else { 319 | // The error isn't a 2FA prompt, so present it to the user. 320 | [self _presentError:error]; 321 | } 322 | 323 | [UIView animateWithDuration:0.15 animations:^{ 324 | self.spinny.alpha = 0.0; 325 | self.loginButton.alpha = 1.0; 326 | } completion:^(BOOL finished) { 327 | [self.spinny stopAnimating]; 328 | self.spinny.hidden = YES; 329 | }]; 330 | }]; 331 | } 332 | 333 | -(void)_successfulAuth:(OCTClient*)client { 334 | [GIResources _setSuccessfulClient:client]; 335 | 336 | // Now, we move back to whatever called us with this client. 337 | [self.delegate didFinishAuthenticationWithClient:client]; 338 | } 339 | 340 | -(void)_presentError:(NSError*)error { 341 | if ([error.domain isEqual:OCTClientErrorDomain] && error.code == OCTClientErrorAuthenticationFailed) { 342 | 343 | if (self.state == kGIStateBegin) { 344 | self.explainLabel.text = @"Authentication failed!\nIncorrect username or password"; 345 | } else { 346 | self.explainLabel.text = @"Authentication failed!\nInvalid 2-Factor code provided"; 347 | } 348 | 349 | CGRect rect = [GIResources boundedRectForFont:self.explainLabel.font andText:self.explainLabel.text width:self.backer.frame.size.width]; 350 | self.explainLabel.frame = CGRectMake(self.backer.frame.size.width/2 - rect.size.width/2, self.explainLabel.frame.origin.y, rect.size.width, rect.size.height); 351 | self.explainLabel.textColor = [UIColor colorWithRed:0.63 green:0.02 blue:0.02 alpha:1.0]; 352 | 353 | return; 354 | 355 | } else if ([error.domain isEqual:OCTClientErrorDomain] && error.code == OCTClientErrorRequestForbidden) { 356 | 357 | self.explainLabel.text = @"Too many attempts to sign in, please try again later."; 358 | CGRect rect = [GIResources boundedRectForFont:self.explainLabel.font andText:self.explainLabel.text width:self.backer.frame.size.width]; 359 | self.explainLabel.frame = CGRectMake(self.backer.frame.size.width/2 - rect.size.width/2, self.explainLabel.frame.origin.y, rect.size.width, rect.size.height); 360 | self.explainLabel.textColor = [UIColor colorWithRed:0.63 green:0.02 blue:0.02 alpha:1.0]; 361 | 362 | return; 363 | 364 | } 365 | 366 | dispatch_async(dispatch_get_main_queue(), ^{ 367 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"%@", error.localizedFailureReason] preferredStyle:UIAlertControllerStyleAlert]; 368 | 369 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault 370 | handler:^(UIAlertAction * action) {}]; 371 | 372 | [alert addAction:defaultAction]; 373 | [self presentViewController:alert animated:YES completion:nil]; 374 | }); 375 | } 376 | 377 | -(void)_show2FAWithAnimation:(BOOL)anim { 378 | self.state = kGIStateWaiting2FA; 379 | 380 | [self.twoFactorAuthPassword setText:nil]; 381 | 382 | // Setup frames. 383 | self.twoFactorAuthPassword.alpha = 0.0; 384 | self.twoFactorAuthPassword.hidden = NO; 385 | self.twoFAButton.alpha = 0.0; 386 | self.twoFAButton.hidden = NO; 387 | self.cancelButton.alpha = 0.0; 388 | self.cancelButton.hidden = NO; 389 | 390 | // Initialise these two views frames to what they will replace. 391 | self.twoFAButton.frame = self.loginButton.frame; 392 | self.twoFactorAuthPassword.frame = self.password.frame; 393 | self.cancelButton.frame = self.createAccount.frame; 394 | 395 | self.explainLabel.text = @"This account uses 2-Factor Authentication, and needs a code"; 396 | CGRect rect = [GIResources boundedRectForFont:self.explainLabel.font andText:self.explainLabel.text width:self.backer.frame.size.width]; 397 | self.explainLabel.frame = CGRectMake(self.backer.frame.size.width/2 - rect.size.width/2, self.explainLabel.frame.origin.y, rect.size.width, rect.size.height); 398 | self.explainLabel.textColor = [UIColor darkTextColor]; 399 | 400 | [UIView animateWithDuration:anim ? 0.3 : 0.0 animations:^{ 401 | self.loginButton.alpha = 0.0; 402 | self.twoFAButton.alpha = 1.0; 403 | 404 | self.password.alpha = 0.0; 405 | self.twoFactorAuthPassword.alpha = 1.0; 406 | 407 | self.createAccount.alpha = 0.0; 408 | self.cancelButton.alpha = 1.0; 409 | 410 | self.username.alpha = 0.0; 411 | 412 | [self viewDidLayoutSubviews]; 413 | } completion:^(BOOL finished) { 414 | self.loginButton.hidden = YES; 415 | self.password.hidden = YES; 416 | self.username.hidden = YES; 417 | self.createAccount.hidden = YES; 418 | }]; 419 | } 420 | 421 | -(void)didClickLoginWith2FA:(id)sender { 422 | [self.twoFactorAuthPassword resignFirstResponder]; 423 | 424 | NSString *pass = self.password.text; 425 | NSString *twoFA = self.twoFactorAuthPassword.text; 426 | 427 | OCTUser *user = [OCTUser userWithRawLogin:self.username.text server:OCTServer.dotComServer]; 428 | 429 | self.spinny.hidden = NO; 430 | self.spinny.alpha = 0.0; 431 | self.spinny.center = self.twoFAButton.center; 432 | [self.spinny startAnimating]; 433 | 434 | [UIView animateWithDuration:0.15 animations:^{ 435 | self.spinny.alpha = 1.0; 436 | self.twoFAButton.alpha = 0.0; 437 | }]; 438 | 439 | NSString *fingerprint = [GIResources _generateFingerprint]; 440 | 441 | [[[OCTClient 442 | _gi_signInAsUser:user password:pass oneTimePassword:twoFA scopes:OCTClientAuthorizationScopesUser | OCTClientAuthorizationScopesRepository note:@"GitHub Issues App" noteURL:nil fingerprint:fingerprint] 443 | deliverOn:RACScheduler.mainThreadScheduler] 444 | subscribeNext:^(OCTClient *client) { 445 | 446 | // We have success! 447 | [self _successfulAuth:client]; 448 | 449 | [UIView animateWithDuration:0.15 animations:^{ 450 | self.spinny.alpha = 0.0; 451 | self.twoFAButton.alpha = 1.0; 452 | } completion:^(BOOL finished) { 453 | [self.spinny stopAnimating]; 454 | self.spinny.hidden = YES; 455 | }]; 456 | 457 | } error:^(NSError *error) { 458 | [self _presentError:error]; 459 | 460 | [UIView animateWithDuration:0.15 animations:^{ 461 | self.spinny.alpha = 0.0; 462 | self.twoFAButton.alpha = 1.0; 463 | } completion:^(BOOL finished) { 464 | [self.spinny stopAnimating]; 465 | self.spinny.hidden = YES; 466 | }]; 467 | }]; 468 | 469 | } 470 | 471 | -(void)didClickCancel:(id)sender { 472 | [self.twoFactorAuthPassword resignFirstResponder]; 473 | 474 | self.state = kGIStateBegin; 475 | 476 | // Revert to "begin" state. 477 | [self.username setText:nil]; 478 | [self.password setText:nil]; 479 | 480 | self.loginButton.hidden = NO; 481 | self.password.hidden = NO; 482 | self.username.hidden = NO; 483 | self.createAccount.hidden = NO; 484 | 485 | self.loginButton.frame = self.twoFAButton.frame; 486 | self.password.frame = self.twoFactorAuthPassword.frame; 487 | self.createAccount.frame = self.cancelButton.frame; 488 | 489 | self.explainLabel.text = @"GitHub is used to keep track of bugs and features"; 490 | CGRect rect = [GIResources boundedRectForFont:self.explainLabel.font andText:self.explainLabel.text width:self.backer.frame.size.width]; 491 | self.explainLabel.frame = CGRectMake(self.backer.frame.size.width/2 - rect.size.width/2, self.explainLabel.frame.origin.y, rect.size.width, rect.size.height); 492 | 493 | [UIView animateWithDuration:0.3 animations:^{ 494 | self.loginButton.alpha = 1.0; 495 | self.twoFAButton.alpha = 0.0; 496 | 497 | self.password.alpha = 1.0; 498 | self.twoFactorAuthPassword.alpha = 0.0; 499 | 500 | self.createAccount.alpha = 1.0; 501 | self.cancelButton.alpha = 0.0; 502 | 503 | self.username.alpha = 1.0; 504 | 505 | [self viewDidLayoutSubviews]; 506 | } completion:^(BOOL finished) { 507 | self.twoFAButton.hidden = YES; 508 | self.twoFactorAuthPassword.hidden = YES; 509 | self.cancelButton.hidden = YES; 510 | }]; 511 | 512 | } 513 | 514 | 515 | -(void)didClickCreateNewAccount:(id)sender { 516 | // Open Safari for the user to create an account 517 | NSURL *url = [NSURL URLWithString:@"https://github.com/join"]; 518 | [[UIApplication sharedApplication] openURL:url options:[NSDictionary dictionary] completionHandler:nil]; 519 | } 520 | 521 | #pragma mark UITextField Delegate 522 | 523 | -(BOOL)textFieldShouldReturn:(UITextField *)textField { 524 | if ([textField isEqual:self.username]) { 525 | [self.password becomeFirstResponder]; 526 | return NO; 527 | } else if ([textField isEqual:self.password]) { 528 | [self.password resignFirstResponder]; 529 | return NO; 530 | } else if ([textField isEqual:self.twoFactorAuthPassword]) { 531 | [self.twoFactorAuthPassword resignFirstResponder]; 532 | return NO; 533 | } 534 | 535 | return YES; 536 | } 537 | 538 | -(void)textFieldContentDidChange:(UITextField*)sender { 539 | // If both the password and username fields have content, enable login button. 540 | if (self.username.text && 541 | ![self.username.text isEqualToString:@""] && 542 | self.password.text && 543 | ![self.password.text isEqualToString:@""]) { 544 | self.loginButton.enabled = YES; 545 | } else if (self.loginButton.enabled) { 546 | self.loginButton.enabled = NO; 547 | } 548 | } 549 | 550 | -(void)textFieldContentDidChange2FA:(UITextField*)sender { 551 | // If both the password and username fields have content, enable login button. 552 | if (self.twoFactorAuthPassword.text && 553 | ![self.twoFactorAuthPassword.text isEqualToString:@""]) { 554 | self.twoFAButton.enabled = YES; 555 | } else if (self.twoFAButton.enabled) { 556 | self.twoFAButton.enabled = NO; 557 | } 558 | } 559 | 560 | @end 561 | -------------------------------------------------------------------------------- /libGitHubIssues/GIResources.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIResources.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 16/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface GIResources : NSObject 13 | 14 | +(CGRect)boundedRectForFont:(UIFont*)font andText:(id)text width:(CGFloat)width; 15 | +(UIImage *)decodeBase64ToImage:(NSString *)strEncodeData; 16 | +(NSString*)formatDate:(NSDate*)date; 17 | +(UIImage*)imageWithName:(NSString*)name; 18 | 19 | +(void)_setSuccessfulClient:(id)client; 20 | +(void)_registerClientID:(NSString*)clientid andSecret:(NSString*)secret; 21 | +(void)_setCurrentRepositoryName:(NSString*)name andOwner:(NSString*)owner; 22 | +(id)_getCurrentClient; 23 | +(id)_getCurrentRepository; 24 | +(NSString*)_generateFingerprint; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /libGitHubIssues/GIResources.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIResources.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 16/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIResources.h" 10 | #import 11 | #import 12 | 13 | static OCTClient *sharedClient; 14 | static OCTClient *sharedUnauthenticatedClient; 15 | static RACSignal *sharedRepository; 16 | static NSString *_clientID; 17 | static NSString *_clientSecret; 18 | 19 | NSString *letters = @"abcdefghijklmnopqrstuvwxyz0123456789"; 20 | 21 | @implementation GIResources 22 | 23 | +(CGRect)boundedRectForFont:(UIFont*)font andText:(id)text width:(CGFloat)width { 24 | if (!text || !font) { 25 | return CGRectZero; 26 | } 27 | 28 | if (![text isKindOfClass:[NSAttributedString class]]) { 29 | NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName:font}]; 30 | CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX} 31 | options:NSStringDrawingUsesLineFragmentOrigin 32 | context:nil]; 33 | return rect; 34 | } else { 35 | return [(NSAttributedString*)text boundingRectWithSize:(CGSize){width, CGFLOAT_MAX} 36 | options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) 37 | context:nil]; 38 | } 39 | } 40 | 41 | +(UIImage *)decodeBase64ToImage:(NSString *)strEncodeData { 42 | NSData *data = [[NSData alloc]initWithBase64EncodedString:strEncodeData options:NSDataBase64DecodingIgnoreUnknownCharacters]; 43 | return [UIImage imageWithData:data]; 44 | } 45 | 46 | +(NSString*)formatDate:(NSDate*)date { 47 | // Format will output time: 48 | // - within 24 hours "9:35" 49 | // - within 1 week "2d ago" 50 | // - within 1 month "1w go" 51 | // - within 1 year "5m ago" 52 | // - else, "5y ago" 53 | 54 | NSCalendarUnit units = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitDay | NSCalendarUnitWeekOfYear | 55 | NSCalendarUnitMonth | NSCalendarUnitYear; 56 | 57 | // if `date` is before "now" (i.e. in the past) then the components will be positive 58 | NSDateComponents *components = [[NSCalendar currentCalendar] components:units 59 | fromDate:date 60 | toDate:[NSDate date] 61 | options:0]; 62 | 63 | if (components.year > 0) { 64 | return [NSString stringWithFormat:@"%ldy ago", (long)components.year]; 65 | } else if (components.month > 0) { 66 | return [NSString stringWithFormat:@"%ldm ago", (long)components.month]; 67 | } else if (components.weekOfYear > 0) { 68 | return [NSString stringWithFormat:@"%ldw ago", (long)components.weekOfYear]; 69 | } else if (components.day > 0) { 70 | return [NSString stringWithFormat:@"%ldd ago", (long)components.day]; 71 | } else if (components.hour > 0) { 72 | return [NSString stringWithFormat:@"%ldhr ago", (long)components.hour]; 73 | } else { 74 | return [NSString stringWithFormat:@"%ldmins ago", (long)components.minute]; 75 | } 76 | } 77 | 78 | +(UIImage*)imageWithName:(NSString*)name { 79 | #if BUILD_FOR_CYDIA==0 80 | NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"libGitHubIssues" withExtension:@"bundle"]; 81 | NSString *path = [[NSBundle bundleWithURL:bundleURL] pathForResource:name ofType:@"png"]; 82 | #else 83 | NSString *suffix = @""; 84 | switch ((int)[UIScreen mainScreen].scale) { 85 | case 2: 86 | suffix = @"@2x"; 87 | break; 88 | case 3: 89 | suffix = @"@3x"; 90 | break; 91 | 92 | default: 93 | break; 94 | } 95 | 96 | NSString *path = [NSString stringWithFormat:@"/Library/Application Support/libGitHubIssues/%@%@.png", name, suffix]; 97 | #endif 98 | 99 | return [UIImage imageWithContentsOfFile:path]; 100 | } 101 | 102 | +(void)_setSuccessfulClient:(OCTClient*)client { 103 | // Force an unauthenticated login if OAuth2 token failed. 104 | if (!client.token || [client.token isEqualToString:@""]) { 105 | NSLog(@"No token returned from GitHub..."); 106 | client = nil; 107 | } 108 | 109 | sharedClient = client; 110 | 111 | NSString *identifier = [NSString stringWithFormat:@"com.matchstic.libGitHubIssues.%@", _clientID]; 112 | 113 | if (client) { 114 | [SAMKeychain setPassword:client.token forService:identifier account:client.user.rawLogin]; 115 | } else { 116 | NSDictionary *account = [[SAMKeychain accountsForService:identifier] firstObject]; 117 | NSString *name = [account objectForKey:kSAMKeychainAccountKey]; 118 | 119 | [SAMKeychain deletePasswordForService:identifier account:name]; 120 | } 121 | } 122 | 123 | +(OCTClient*)_getCurrentClient { 124 | NSString *identifier = [NSString stringWithFormat:@"com.matchstic.libGitHubIssues.%@", _clientID]; 125 | 126 | NSDictionary *account = [[SAMKeychain accountsForService:identifier] firstObject]; 127 | NSString *username = [account objectForKey:kSAMKeychainAccountKey]; 128 | 129 | NSString *token = [SAMKeychain passwordForService:identifier account:username]; 130 | 131 | if (username && ![username isEqualToString:@""] && token && ![token isEqualToString:@""]) { 132 | OCTUser *user = [OCTUser userWithRawLogin:username server:OCTServer.dotComServer]; 133 | sharedClient = [OCTClient authenticatedClientWithUser:user token:token]; 134 | 135 | return sharedClient; 136 | } 137 | 138 | if (sharedUnauthenticatedClient) { 139 | return sharedUnauthenticatedClient; 140 | } 141 | 142 | sharedUnauthenticatedClient = [[OCTClient alloc] initWithServer:OCTServer.dotComServer]; 143 | return sharedUnauthenticatedClient; 144 | } 145 | 146 | // Must be called first! 147 | +(void)_registerClientID:(NSString*)clientid andSecret:(NSString*)secret { 148 | [OCTClient setClientID:clientid clientSecret:secret]; 149 | 150 | _clientID = clientid; 151 | _clientSecret = secret; 152 | 153 | //NSString *identifier = [NSString stringWithFormat:@"com.matchstic.libGitHubIssues.%@", clientid]; 154 | 155 | //sharedKeychain = [[GIKeychainWrapper alloc] initWithKeychainID:[identifier UTF8String]]; 156 | } 157 | 158 | +(void)_setCurrentRepositoryName:(NSString*)name andOwner:(NSString*)owner { 159 | sharedRepository = [(OCTClient*)[GIResources _getCurrentClient] fetchRepositoryWithName:name owner:owner]; 160 | } 161 | 162 | +(id)_getCurrentRepository { 163 | return sharedRepository; 164 | } 165 | 166 | +(NSString*)_generateFingerprint { 167 | NSInteger len = 20; 168 | NSMutableString *randomString = [NSMutableString stringWithCapacity: len]; 169 | 170 | for (int i=0; i 10 | 11 | @interface GIRootViewController : UINavigationController 12 | 13 | /** 14 | Configure libGitHubIssues with an identifier with the client ID and secret for your application on GitHub.\n\n 15 | 16 | The client ID and secret can be found at: https://github.com/settings/developers 17 | 18 | @param clientId Cient ID from your GitHub application. 19 | @param clientSecret Client secret from your GitHub application. 20 | */ 21 | +(void)registerClientID:(NSString*)clientId andSecret:(NSString*)clientSecret; 22 | 23 | /** 24 | Configure which repository libGitHubIssues should access Issues from.\n\n 25 | 26 | Parameters are in the form: https://github.com// 27 | 28 | @param name Name of repository 29 | @param owner Owner of repository 30 | */ 31 | +(void)registerCurrentRepositoryName:(NSString*)name andOwner:(NSString*)owner; 32 | 33 | @end 34 | 35 | -------------------------------------------------------------------------------- /libGitHubIssues/GIRootViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIRootViewController.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 16/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIRootViewController.h" 10 | #import "GILoginController.h" 11 | #import "GIIssuesViewController.h" 12 | #import "GIResources.h" 13 | #import 14 | 15 | @interface GIRootViewController () 16 | 17 | @end 18 | 19 | @implementation GIRootViewController 20 | 21 | +(void)registerClientID:(NSString*)clientId andSecret:(NSString*)clientSecret { 22 | [GIResources _registerClientID:clientId andSecret:clientSecret]; 23 | } 24 | 25 | +(void)registerCurrentRepositoryName:(NSString*)name andOwner:(NSString*)owner { 26 | [GIResources _setCurrentRepositoryName:name andOwner:owner]; 27 | } 28 | 29 | - (void)viewDidLoad { 30 | [super viewDidLoad]; 31 | // Do any additional setup after loading the view, typically from a nib. 32 | 33 | [self.navigationBar setBarStyle:UIBarStyleDefault]; 34 | 35 | GIIssuesViewController *table = [[GIIssuesViewController alloc] init]; 36 | [self setViewControllers:@[table] animated:NO]; 37 | 38 | UIBarButtonItem *cancel = [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStylePlain target:self action:@selector(didClickCancel:)]; 39 | table.navigationItem.leftBarButtonItem = cancel; 40 | } 41 | 42 | -(void)didClickCancel:(id)sender { 43 | [self dismissViewControllerAnimated:YES completion:nil]; 44 | } 45 | 46 | - (void)didReceiveMemoryWarning { 47 | [super didReceiveMemoryWarning]; 48 | // Dispose of any resources that can be recreated. 49 | } 50 | 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /libGitHubIssues/GIUserViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // GIUserViewController.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GIUserViewController : UIViewController 12 | 13 | @property (nonatomic, strong) UIImageView *backgroundAvatarView; 14 | @property (nonatomic, strong) UIVisualEffectView *effectView; 15 | @property (nonatomic, strong) UIVisualEffectView *vibrancyView; 16 | 17 | @property (nonatomic, strong) UIView *centraliserView; 18 | @property (nonatomic, strong) UIImageView *foregroundAvatarView; 19 | @property (nonatomic, strong) UILabel *nameLabel; 20 | @property (nonatomic, strong) UILabel *emailLabel; 21 | @property (nonatomic, strong) UIView *separatorView; 22 | 23 | @property (nonatomic, strong) UIButton *logoutButton; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /libGitHubIssues/GIUserViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // GIUserViewController.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 18/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "GIUserViewController.h" 10 | #import "GIResources.h" 11 | #import 12 | #import 13 | 14 | @interface GIUserViewController () 15 | 16 | @end 17 | 18 | @implementation GIUserViewController 19 | 20 | - (void)viewDidLoad { 21 | [super viewDidLoad]; 22 | // Do any additional setup after loading the view. 23 | 24 | [self.navigationItem setTitle:@"Loading..."]; 25 | [self setTitle:@"Loading..."]; 26 | 27 | OCTClient *client = (OCTClient*)[GIResources _getCurrentClient]; 28 | 29 | RACSignal *sig = [client fetchUserInfo]; 30 | [[sig collect] subscribeNext:^(NSArray *issues) { 31 | 32 | OCTUser *user = [issues firstObject]; 33 | 34 | GIUserViewController * __weak weakself = self; 35 | 36 | NSURLRequest *request = [NSURLRequest requestWithURL:user.avatarURL]; 37 | [self.backgroundAvatarView setImageWithURLRequest:request placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) { 38 | weakself.backgroundAvatarView.image = image; 39 | } failure:nil]; 40 | 41 | [self.foregroundAvatarView setImageWithURLRequest:request placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) { 42 | weakself.foregroundAvatarView.image = image; 43 | } failure:nil]; 44 | 45 | dispatch_async(dispatch_get_main_queue(), ^{ 46 | [self.navigationItem setTitle:@"Profile"]; 47 | [self setTitle:@"Profile"]; 48 | 49 | self.nameLabel.text = user.name; 50 | self.emailLabel.text = user.email; 51 | }); 52 | 53 | } error:^(NSError *error) { 54 | // Invoked when an error occurs. You won't receive any results if this 55 | // happens. 56 | [self _presentError:error]; 57 | 58 | [self.navigationItem setTitle:@"Error"]; 59 | [self setTitle:@"Error"]; 60 | }]; 61 | 62 | } 63 | 64 | -(void)_presentError:(NSError*)error { 65 | dispatch_async(dispatch_get_main_queue(), ^{ 66 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"%@", error.localizedFailureReason] preferredStyle:UIAlertControllerStyleAlert]; 67 | 68 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault 69 | handler:^(UIAlertAction * action) {}]; 70 | 71 | [alert addAction:defaultAction]; 72 | [self presentViewController:alert animated:YES completion:nil]; 73 | }); 74 | } 75 | 76 | - (void)didReceiveMemoryWarning { 77 | [super didReceiveMemoryWarning]; 78 | // Dispose of any resources that can be recreated. 79 | } 80 | 81 | -(void)loadView { 82 | self.view = [[UIView alloc] initWithFrame:CGRectZero]; 83 | self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; 84 | 85 | self.backgroundAvatarView = [[UIImageView alloc] initWithFrame:CGRectZero]; 86 | self.backgroundAvatarView.backgroundColor = [UIColor grayColor]; 87 | self.backgroundAvatarView.contentMode = UIViewContentModeScaleAspectFill; 88 | self.backgroundAvatarView.layer.masksToBounds = YES; 89 | 90 | [self.view addSubview:self.backgroundAvatarView]; 91 | 92 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 93 | 94 | self.effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; 95 | self.effectView.frame = CGRectZero; 96 | 97 | [self.view addSubview:self.effectView]; 98 | 99 | self.centraliserView = [[UIView alloc] initWithFrame:CGRectZero]; 100 | self.centraliserView.backgroundColor = [UIColor clearColor]; 101 | 102 | [self.effectView.contentView addSubview:self.centraliserView]; 103 | 104 | self.foregroundAvatarView = [[UIImageView alloc] initWithFrame:CGRectZero]; 105 | self.foregroundAvatarView.backgroundColor = [UIColor lightGrayColor]; 106 | self.foregroundAvatarView.layer.borderColor = [UIColor whiteColor].CGColor; 107 | self.foregroundAvatarView.layer.borderWidth = 1; 108 | self.foregroundAvatarView.layer.masksToBounds = YES; 109 | self.foregroundAvatarView.contentMode = UIViewContentModeScaleAspectFill; 110 | 111 | [self.centraliserView addSubview:self.foregroundAvatarView]; 112 | 113 | self.vibrancyView = [[UIVisualEffectView alloc] initWithEffect:[UIVibrancyEffect effectForBlurEffect:effect]]; 114 | self.vibrancyView.frame = CGRectZero; 115 | 116 | [self.centraliserView addSubview:self.vibrancyView]; 117 | 118 | self.nameLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 119 | self.nameLabel.text = @"Loading"; 120 | self.nameLabel.textAlignment = NSTextAlignmentCenter; 121 | self.nameLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; 122 | 123 | [self.vibrancyView.contentView addSubview:self.nameLabel]; 124 | 125 | self.emailLabel = [[UILabel alloc] initWithFrame:CGRectZero]; 126 | self.emailLabel.text = @"Loading"; 127 | self.emailLabel.textAlignment = NSTextAlignmentCenter; 128 | self.emailLabel.font = [UIFont systemFontOfSize:14]; 129 | 130 | [self.vibrancyView.contentView addSubview:self.emailLabel]; 131 | 132 | self.separatorView = [[UIView alloc] initWithFrame:CGRectZero]; 133 | self.separatorView.backgroundColor = [UIColor colorWithRed:200/255.0 green:199/255.0 blue:204/255.0 alpha:1.0]; 134 | 135 | [self.view addSubview:self.separatorView]; 136 | 137 | // Handle logout button. 138 | self.logoutButton = [UIButton buttonWithType:UIButtonTypeSystem]; 139 | [self.logoutButton setTitle:@"Sign Out" forState:UIControlStateNormal]; 140 | [self.logoutButton setTitleColor:[UIColor colorWithRed:0.86 green:0.38 blue:0.38 alpha:1.0] forState:UIControlStateNormal]; 141 | [self.logoutButton setTitleColor:[UIColor colorWithRed:0.87 green:0.61 blue:0.61 alpha:1.0] forState:UIControlStateHighlighted]; 142 | self.logoutButton.titleLabel.font = [UIFont systemFontOfSize:18]; 143 | self.logoutButton.backgroundColor = [UIColor colorWithRed:0.93 green:0.80 blue:0.80 alpha:1.0]; 144 | self.logoutButton.layer.borderColor = [UIColor colorWithRed:0.87 green:0.61 blue:0.61 alpha:1.0].CGColor; 145 | self.logoutButton.layer.borderWidth = 1; 146 | 147 | [self.logoutButton addTarget:self action:@selector(didTapLogoutButton:) forControlEvents:UIControlEventTouchUpInside]; 148 | 149 | [self.view addSubview:self.logoutButton]; 150 | } 151 | 152 | -(void)viewDidLayoutSubviews { 153 | [super viewDidLayoutSubviews]; 154 | 155 | CGFloat initialY = 0; 156 | initialY += [[UIApplication sharedApplication] statusBarFrame].size.height; 157 | initialY += self.navigationController.navigationBar.frame.size.height; 158 | 159 | if (!self.navigationController.navigationBar.translucent) { 160 | initialY = 0; 161 | } 162 | 163 | // First, background image. Square if possible, but never taller than 40% of the height. 164 | CGFloat width = self.view.frame.size.width; 165 | CGFloat height = width; 166 | 167 | if (height > self.view.frame.size.height * 0.4) { 168 | height = self.view.frame.size.height * 0.4; 169 | } 170 | 171 | self.backgroundAvatarView.frame = CGRectMake(0, initialY, width, height); 172 | 173 | // Effect view. 174 | self.effectView.frame = self.backgroundAvatarView.frame; 175 | 176 | // Next, set up the centraliser view. 177 | CGFloat y = 0; 178 | 179 | self.foregroundAvatarView.frame = CGRectMake(self.view.frame.size.width/2 - 50, y, 100, 100); 180 | self.foregroundAvatarView.layer.cornerRadius = 100/2; 181 | 182 | y += self.foregroundAvatarView.frame.size.height + 10; 183 | 184 | self.vibrancyView.frame = CGRectMake(0, y, self.view.frame.size.width, 45); 185 | 186 | self.nameLabel.frame = CGRectMake(0, 0, self.view.frame.size.width, 20); 187 | 188 | y += self.nameLabel.frame.size.height + 5; 189 | 190 | self.emailLabel.frame = CGRectMake(0, self.nameLabel.frame.size.height + 5, self.view.frame.size.width, 20); 191 | 192 | y += self.emailLabel.frame.size.height; 193 | 194 | // However, if the height space available is less than the centraliser's height, we got a problem. 195 | 196 | if (y >= height) { 197 | height = y + 20; 198 | 199 | self.backgroundAvatarView.frame = CGRectMake(0, initialY, width, height); 200 | self.effectView.frame = self.backgroundAvatarView.frame; 201 | 202 | self.centraliserView.frame = CGRectMake(0, 10, self.view.frame.size.width, y); 203 | } else { 204 | self.centraliserView.frame = CGRectMake(0, self.effectView.frame.size.height/2 - y/2, self.view.frame.size.width, y); 205 | } 206 | 207 | self.separatorView.frame = CGRectMake(0, self.backgroundAvatarView.frame.size.height + self.backgroundAvatarView.frame.origin.y, self.view.frame.size.width, 1); 208 | 209 | self.logoutButton.frame = CGRectMake(-1, self.backgroundAvatarView.frame.size.height + self.backgroundAvatarView.frame.origin.y + 40, self.view.frame.size.width+2, 44); 210 | } 211 | 212 | -(void)didTapLogoutButton:(id)sender { 213 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Sign Out" message:@"Are you sure you want to sign out?" preferredStyle:UIAlertControllerStyleAlert]; 214 | 215 | UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"Yes" style:UIAlertActionStyleDefault 216 | handler:^(UIAlertAction * action) { 217 | 218 | [GIResources _setSuccessfulClient:nil]; 219 | [self.navigationController popViewControllerAnimated:YES]; 220 | 221 | }]; 222 | 223 | UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel 224 | handler:^(UIAlertAction * action) {}]; 225 | 226 | [alert addAction:defaultAction]; 227 | [alert addAction:cancelAction]; 228 | [self presentViewController:alert animated:YES completion:nil]; 229 | } 230 | 231 | @end 232 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTClient+Fingerprint.h: -------------------------------------------------------------------------------- 1 | // 2 | // OCTClient+Fingerprint.h 3 | // libGitHubIssues 4 | // 5 | // Created by Matt Clarke on 19/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface OCTClient (GIFingerprint) 13 | 14 | + (RACSignal *)_gi_signInAsUser:(OCTUser *)user password:(NSString *)password oneTimePassword:(NSString *)oneTimePassword scopes:(OCTClientAuthorizationScopes)scopes note:(NSString *)note noteURL:(NSURL *)noteURL fingerprint:(NSString *)fingerprint; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTClient+Fingerprint.m: -------------------------------------------------------------------------------- 1 | // 2 | // OCTClient+Fingerprint.m 3 | // libGitHubIssues 4 | // 5 | // Created by Matt Clarke on 19/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "OCTClient+Fingerprint.h" 10 | #import 11 | #import 12 | 13 | static NSString * const _gi_OCTClientOneTimePasswordHeaderField = @"X-GitHub-OTP"; 14 | 15 | @interface OCTClient () 16 | @property (nonatomic, strong, readwrite) OCTUser *user; 17 | @property (nonatomic, copy, readwrite) NSString *token; 18 | 19 | // Returns any user agent previously given to +setUserAgent:. 20 | + (NSString *)userAgent; 21 | 22 | // Returns any OAuth client ID previously given to +setClientID:clientSecret:. 23 | + (NSString *)clientID; 24 | 25 | // Returns any OAuth client secret previously given to 26 | // +setClientID:clientSecret:. 27 | + (NSString *)clientSecret; 28 | 29 | // A subject to send callback URLs to after they're received by the app. 30 | + (RACSubject *)callbackURLs; 31 | 32 | // Creates a request. 33 | // 34 | // method - The HTTP method to use in the request (e.g., "GET" or "POST"). 35 | // path - The path to request, relative to the base API endpoint. This path 36 | // should _not_ begin with a forward slash. 37 | // etag - An ETag to compare the server data against, previously retrieved 38 | // from an instance of OCTResponse. 39 | // 40 | // Returns a request which can be modified further before being enqueued. 41 | - (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters notMatchingEtag:(NSString *)etag; 42 | 43 | + (NSArray *)scopesArrayFromScopes:(OCTClientAuthorizationScopes)scopes; 44 | 45 | + (NSError *)tokenUnsupportedError; 46 | + (NSError *)unsupportedVersionError; 47 | + (OCTServer *)HTTPSEnterpriseServerWithServer:(OCTServer *)server; 48 | 49 | @end 50 | 51 | @implementation OCTClient (GIFingerprint) 52 | 53 | // Added since OctoKit on CocoaPods is outdated. :( 54 | + (RACSignal *)_gi_signInAsUser:(OCTUser *)user password:(NSString *)password oneTimePassword:(NSString *)oneTimePassword scopes:(OCTClientAuthorizationScopes)scopes note:(NSString *)note noteURL:(NSURL *)noteURL fingerprint:(NSString *)fingerprint { 55 | NSParameterAssert(user != nil); 56 | NSParameterAssert(password != nil); 57 | 58 | NSString *clientID = [OCTClient clientID]; 59 | NSString *clientSecret = [OCTClient clientSecret]; 60 | NSAssert(clientID != nil && clientSecret != nil, @"+setClientID:clientSecret: must be invoked before calling %@", NSStringFromSelector(_cmd)); 61 | 62 | RACSignal * (^authorizationSignalWithUser)(OCTUser *user) = ^(OCTUser *user) { 63 | return [RACSignal defer:^{ 64 | OCTClient *client = [self unauthenticatedClientWithUser:user]; 65 | [client setAuthorizationHeaderWithUsername:user.rawLogin password:password]; 66 | 67 | NSString *path = [NSString stringWithFormat:@"authorizations/clients/%@", clientID]; 68 | NSMutableDictionary *params = [@{ 69 | @"scopes": [self scopesArrayFromScopes:scopes], 70 | @"client_secret": clientSecret, 71 | } mutableCopy]; 72 | 73 | if (note != nil) params[@"note"] = note; 74 | if (noteURL != nil) params[@"note_url"] = noteURL.absoluteString; 75 | if (fingerprint != nil) params[@"fingerprint"] = fingerprint; 76 | 77 | NSMutableURLRequest *request = [client requestWithMethod:@"PUT" path:path parameters:params]; 78 | request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; 79 | if (oneTimePassword != nil) [request setValue:oneTimePassword forHTTPHeaderField:_gi_OCTClientOneTimePasswordHeaderField]; 80 | 81 | NSString *previewContentType = @"application/vnd.github.v3+json"; 82 | [request setValue:previewContentType forHTTPHeaderField:@"Accept"]; 83 | 84 | RACSignal *tokenSignal = [client enqueueRequest:request resultClass:OCTAuthorization.class]; 85 | return [RACSignal combineLatest:@[ 86 | [RACSignal return:client], 87 | tokenSignal 88 | ]]; 89 | }]; 90 | }; 91 | 92 | return [[[[[authorizationSignalWithUser(user) 93 | flattenMap:^(RACTuple *clientAndResponse) { 94 | RACTupleUnpack(OCTClient *client, OCTResponse *response) = clientAndResponse; 95 | OCTAuthorization *authorization = response.parsedResult; 96 | 97 | // To increase security, tokens are no longer returned when the authorization 98 | // already exists. If that happens, we need to delete the existing 99 | // authorization for this app and create a new one, so we end up with a token 100 | // of our own. 101 | // 102 | // The `fingerprint` field provided will be used to ensure uniqueness and 103 | // avoid deleting unrelated tokens. 104 | if (authorization.token.length == 0/* && response.statusCode == 200*/) { 105 | NSString *path = [NSString stringWithFormat:@"authorizations/%@", authorization.objectID]; 106 | 107 | NSMutableURLRequest *request = [client requestWithMethod:@"DELETE" path:path parameters:nil]; 108 | request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; 109 | if (oneTimePassword != nil) [request setValue:oneTimePassword forHTTPHeaderField:_gi_OCTClientOneTimePasswordHeaderField]; 110 | 111 | return [[client 112 | enqueueRequest:request resultClass:nil] 113 | then:^{ 114 | // Try logging in again. 115 | return authorizationSignalWithUser(user); 116 | }]; 117 | } else { 118 | return [RACSignal return:clientAndResponse]; 119 | } 120 | }] 121 | catch:^(NSError *error) { 122 | if (error.code == OCTClientErrorUnsupportedServerScheme) { 123 | OCTServer *secureServer = [self HTTPSEnterpriseServerWithServer:user.server]; 124 | OCTUser *secureUser = [OCTUser userWithRawLogin:user.rawLogin server:secureServer]; 125 | return authorizationSignalWithUser(secureUser); 126 | } 127 | 128 | NSNumber *statusCode = error.userInfo[OCTClientErrorHTTPStatusCodeKey]; 129 | if (statusCode.integerValue == 404) { 130 | if (error.userInfo[OCTClientErrorOAuthScopesStringKey] != nil) { 131 | error = self.class.tokenUnsupportedError; 132 | } else { 133 | error = self.class.unsupportedVersionError; 134 | } 135 | } 136 | 137 | return [RACSignal error:error]; 138 | }] 139 | reduceEach:^(OCTClient *client, OCTResponse *response) { 140 | OCTAuthorization *authorization = response.parsedResult; 141 | 142 | client.token = authorization.token; 143 | return client; 144 | }] 145 | replayLazily] 146 | setNameWithFormat:@"+signInAsUser: %@ password:oneTimePassword:scopes:", user]; 147 | } 148 | 149 | @end 150 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTClient_OCTClient_Issues.h: -------------------------------------------------------------------------------- 1 | // 2 | // OCTClient+Issues.h 3 | // OctoKit 4 | // 5 | // Created by leichunfeng on 15/3/7. 6 | // Copyright (c) 2015年 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_ENUM(NSInteger, OCTClientIssueState) { 12 | OCTClientIssueStateOpen, 13 | OCTClientIssueStateClosed, 14 | OCTClientIssueStateAll, 15 | }; 16 | 17 | @interface OCTClient (Issues) 18 | 19 | /// Creates an issue. 20 | /// 21 | /// title - The title of the issue. This must not be nil. 22 | /// body - The contents of the issue. This can be nil. 23 | /// assignee - Login for the user that this issue should be assigned to. NOTE: 24 | /// Only users with push access can set the assignee for new issues. 25 | // The assignee is silently dropped otherwise. This can be nil. 26 | /// milestone - Milestone to associate this issue with. NOTE: Only users with 27 | /// push access can set the milestone for new issues. The milestone 28 | /// is silently dropped otherwise. This can be nil. 29 | /// labels - Labels to associate with this issue. NOTE: Only users with push 30 | /// access can set labels for new issues. Labels are silently dropped 31 | /// otherwise. This can be nil. 32 | /// repository - The repository in which to create the issue. This must not be nil. 33 | /// 34 | /// Returns a signal which will send the created `OCTIssue` then complete, or error. 35 | - (RACSignal *)createIssueWithTitle:(NSString *)title body:(NSString *)body assignee:(NSString *)assignee milestone:(NSNumber *)milestone labels:(NSArray *)labels inRepository:(OCTRepository *)repository; 36 | 37 | - (RACSignal *)createIssueCommentWithBody:(NSString *)body forIssue:(id)issue inRepository:(OCTRepository *)repository; 38 | 39 | /// Fetch the issues with the given state from the repository. 40 | /// 41 | /// repository - The repository whose issues should be fetched. Cannot be nil. 42 | /// state - The state of issues to return. 43 | /// etag - An Etag from a previous request, used to avoid downloading 44 | // unnecessary data. May be nil. 45 | /// since - Only issues updated or created after this date will be fetched. 46 | /// May be nil. 47 | /// 48 | /// Returns a signal which will send each `OCTResponse`-wrapped `OCTIssue`s and 49 | /// complete or error. 50 | - (RACSignal *)fetchIssuesForRepository:(OCTRepository *)repository state:(OCTClientIssueState)state notMatchingEtag:(NSString *)etag since:(NSDate *)since; 51 | 52 | - (RACSignal *)fetchIssueCommentsForIssue:(id)issue since:(NSDate*)since; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTClient_OCTClient_Issues.m: -------------------------------------------------------------------------------- 1 | // 2 | // OCTClient+Issues.m 3 | // OctoKit 4 | // 5 | // Created by leichunfeng on 15/3/7. 6 | // Copyright (c) 2015年 GitHub. All rights reserved. 7 | // 8 | 9 | #import "OCTClient_OCTClient_Issues.h" 10 | #import "OCTIssue+New.h" 11 | #import "OCTIssueComment+New.h" 12 | #import "NSDateFormatter+OCTFormattingAdditions.h" 13 | 14 | @implementation OCTClient (Issues) 15 | 16 | - (RACSignal *)createIssueCommentWithBody:(NSString *)body forIssue:(OCTIssueNew*)issue inRepository:(OCTRepository *)repository { 17 | NSParameterAssert(body != nil); 18 | NSParameterAssert(repository != nil); 19 | 20 | NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; 21 | parameters[@"body"] = body; 22 | 23 | NSString *path = [NSString stringWithFormat:@"repos/%@/%@/issues/%@/comments", repository.ownerLogin, repository.name, issue.number]; 24 | NSURLRequest *request = [self requestWithMethod:@"POST" path:path parameters:parameters notMatchingEtag:nil]; 25 | 26 | return [[self enqueueRequest:request resultClass:OCTIssueNew.class] oct_parsedResults]; 27 | } 28 | 29 | - (RACSignal *)createIssueWithTitle:(NSString *)title body:(NSString *)body assignee:(NSString *)assignee milestone:(NSNumber *)milestone labels:(NSArray *)labels inRepository:(OCTRepository *)repository { 30 | NSParameterAssert(title != nil); 31 | NSParameterAssert(repository != nil); 32 | 33 | NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; 34 | parameters[@"title"] = title; 35 | 36 | if (milestone != nil) parameters[@"milestone"] = milestone; 37 | if (body != nil) parameters[@"body"] = body; 38 | if (assignee != nil) parameters[@"assignee"] = assignee; 39 | if (labels != nil) parameters[@"labels"] = labels; 40 | 41 | NSString *path = [NSString stringWithFormat:@"repos/%@/%@/issues", repository.ownerLogin, repository.name]; 42 | NSURLRequest *request = [self requestWithMethod:@"POST" path:path parameters:parameters notMatchingEtag:nil]; 43 | 44 | return [[self enqueueRequest:request resultClass:OCTIssueNew.class] oct_parsedResults]; 45 | } 46 | 47 | - (RACSignal *)fetchIssuesForRepository:(OCTRepository *)repository state:(OCTClientIssueState)state notMatchingEtag:(NSString *)etag since:(NSDate *)since { 48 | NSParameterAssert(repository != nil); 49 | 50 | NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; 51 | 52 | NSDictionary *stateToStateString = @{ 53 | @(OCTClientIssueStateOpen): @"open", 54 | @(OCTClientIssueStateClosed): @"closed", 55 | @(OCTClientIssueStateAll): @"all", 56 | }; 57 | NSString *stateString = stateToStateString[@(state)]; 58 | NSAssert(stateString != nil, @"Unknown state: %@", @(state)); 59 | 60 | parameters[@"state"] = stateString; 61 | if (since != nil) parameters[@"since"] = [NSDateFormatter oct_stringFromDate:since]; 62 | 63 | NSString *path = [NSString stringWithFormat:@"repos/%@/%@/issues", repository.ownerLogin, repository.name]; 64 | NSURLRequest *request = [self requestWithMethod:@"GET" path:path parameters:parameters notMatchingEtag:etag]; 65 | return [self enqueueRequest:request resultClass:OCTIssueNew.class]; 66 | } 67 | 68 | - (RACSignal *)fetchIssueCommentsForIssue:(OCTIssueNew *)issue since:(NSDate*)since { 69 | NSParameterAssert(issue != nil); 70 | 71 | NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; 72 | 73 | if (since != nil) parameters[@"since"] = [NSDateFormatter oct_stringFromDate:since]; 74 | 75 | NSString *path = [[issue.commentsURL absoluteString] copy]; 76 | path = [path stringByReplacingOccurrencesOfString:@"https://api.github.com/" withString:@""]; 77 | 78 | NSURLRequest *request = [self requestWithMethod:@"GET" path:path parameters:parameters notMatchingEtag:nil]; 79 | return [self enqueueRequest:request resultClass:OCTIssueCommentNew.class]; 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTIssue+New.h: -------------------------------------------------------------------------------- 1 | // 2 | // OCTIssue+New.h 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 17/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | typedef NS_ENUM(NSInteger, OCTIssueState) { 13 | OCTIssueStateOpen, 14 | OCTIssueStateClosed, 15 | }; 16 | 17 | @class OCTPullRequest; 18 | 19 | // An issue on a repository. 20 | @interface OCTIssueNew : OCTObject 21 | 22 | // The URL for this issue. 23 | @property (nonatomic, copy, readonly) NSURL *URL; 24 | 25 | // The comments URL for this issue. 26 | @property (nonatomic, copy, readonly) NSURL *commentsURL; 27 | 28 | // The webpage URL for this issue. 29 | @property (nonatomic, copy, readonly) NSURL *HTMLURL; 30 | 31 | // The title of this issue. 32 | @property (nonatomic, copy, readonly) NSString *title; 33 | 34 | // The body text of this issue. 35 | @property (nonatomic, copy, readonly) NSString *body; 36 | 37 | // The labels for this issue. 38 | // Is an array of dictionaries. 39 | @property (nonatomic, copy, readonly) NSArray *labels; 40 | 41 | // The user who submitted this issue. 42 | // Note is in dictionary form. 43 | @property (nonatomic, copy, readonly) NSDictionary *user; 44 | 45 | // The comments count. 46 | @property (nonatomic, copy, readonly) NSString *comments; 47 | 48 | // The time the issue was created. 49 | @property (nonatomic, copy, readonly) NSDate *createdAt; 50 | 51 | // The time the issue was updated. 52 | @property (nonatomic, copy, readonly) NSDate *updatedAt; 53 | 54 | // The time the issue was closed at, or nil. 55 | @property (nonatomic, copy, readonly) NSDate *closedAt; 56 | 57 | @property (nonatomic, copy, readonly) NSDictionary *closedBy; 58 | 59 | // The pull request that is attached to (i.e., the same as) this issue, or nil 60 | // if this issue does not have code attached. 61 | @property (nonatomic, copy, readonly) OCTPullRequest *pullRequest; 62 | 63 | // The state of the issue. 64 | @property (nonatomic, assign, readonly) OCTIssueState state; 65 | 66 | // The issue number. 67 | @property (nonatomic, copy, readonly) NSString *number; 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTIssue+New.m: -------------------------------------------------------------------------------- 1 | // 2 | // OCTIssue+New.m 3 | // Github Issues 4 | // 5 | // Created by Matt Clarke on 17/12/2016. 6 | // Copyright © 2016 Matt Clarke. All rights reserved. 7 | // 8 | 9 | #import "OCTIssue+New.h" 10 | #import 11 | #import 12 | #import "NSDateFormatter+OCTFormattingAdditions.h" 13 | #import "NSValueTransformer+OCTPredefinedTransformerAdditions.h" 14 | 15 | @interface OCTIssueNew () 16 | 17 | // The webpage URL for any attached pull request. 18 | @property (nonatomic, copy, readonly) NSURL *pullRequestHTMLURL; 19 | 20 | @end 21 | 22 | @implementation OCTIssueNew 23 | 24 | #pragma mark Properties 25 | 26 | - (OCTPullRequest *)pullRequest { 27 | if (self.pullRequestHTMLURL == nil) return nil; 28 | 29 | // We don't have a "real" pull request model within the issue data, but we 30 | // have enough information to construct one. 31 | return [OCTPullRequest modelWithDictionary:@{ 32 | @keypath(OCTPullRequest.new, objectID): self.objectID, 33 | @keypath(OCTPullRequest.new, HTMLURL): self.pullRequestHTMLURL, 34 | @keypath(OCTPullRequest.new, title): self.title, 35 | } error:NULL]; 36 | } 37 | 38 | #pragma mark MTLJSONSerializing 39 | 40 | + (NSDictionary *)JSONKeyPathsByPropertyKey { 41 | return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary: 42 | @{ 43 | @"URL": @"url", 44 | @"HTMLURL": @"html_url", 45 | @"pullRequestHTMLURL": @"pull_request.html_url", 46 | @"commentsURL": @"comments_url", 47 | @"user": @"user", 48 | @"labels": @"labels", 49 | @"comments": @"comments", 50 | @"createdAt": @"created_at", 51 | @"updatedAt": @"updated_at", 52 | @"closedAt": @"closed_at", 53 | @"body": @"body", 54 | @"closedBy": @"closed_by", 55 | }]; 56 | } 57 | 58 | + (NSValueTransformer *)URLJSONTransformer { 59 | return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; 60 | } 61 | 62 | + (NSValueTransformer *)HTMLURLJSONTransformer { 63 | return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; 64 | } 65 | 66 | + (NSValueTransformer *)commentsURLJSONTransformer { 67 | return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; 68 | } 69 | 70 | + (NSValueTransformer *)pullRequestHTMLURLJSONTransformer { 71 | return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; 72 | } 73 | 74 | + (NSValueTransformer *)createdAtJSONTransformer { 75 | return [NSValueTransformer valueTransformerForName:OCTDateValueTransformerName]; 76 | } 77 | 78 | + (NSValueTransformer *)updatedAtJSONTransformer { 79 | return [NSValueTransformer valueTransformerForName:OCTDateValueTransformerName]; 80 | } 81 | 82 | + (NSValueTransformer *)closedAtJSONTransformer { 83 | return [NSValueTransformer valueTransformerForName:OCTDateValueTransformerName]; 84 | } 85 | 86 | + (NSValueTransformer *)numberJSONTransformer { 87 | return [MTLValueTransformer 88 | reversibleTransformerWithForwardBlock:^(NSNumber *num) { 89 | return num.stringValue; 90 | } reverseBlock:^ id (NSString *str) { 91 | if (str == nil) return nil; 92 | 93 | return [NSDecimalNumber decimalNumberWithString:str]; 94 | }]; 95 | } 96 | 97 | + (NSValueTransformer *)commentsJSONTransformer { 98 | return [MTLValueTransformer 99 | reversibleTransformerWithForwardBlock:^(NSNumber *num) { 100 | return num.stringValue; 101 | } reverseBlock:^ id (NSString *str) { 102 | if (str == nil) return nil; 103 | 104 | return [NSDecimalNumber decimalNumberWithString:str]; 105 | }]; 106 | } 107 | 108 | + (NSValueTransformer *)stateJSONTransformer { 109 | NSDictionary *statesByName = @{ 110 | @"open": @(OCTIssueStateOpen), 111 | @"closed": @(OCTIssueStateClosed), 112 | }; 113 | 114 | return [MTLValueTransformer 115 | reversibleTransformerWithForwardBlock:^(NSString *stateName) { 116 | return statesByName[stateName]; 117 | } 118 | reverseBlock:^(NSNumber *state) { 119 | return [statesByName allKeysForObject:state].lastObject; 120 | }]; 121 | } 122 | 123 | @end 124 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTIssueComment+New.h: -------------------------------------------------------------------------------- 1 | // 2 | // OCTIssueComment.h 3 | // OctoKit 4 | // 5 | // Created by Justin Spahr-Summers on 2012-10-02. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | // A single comment on an issue. 13 | @interface OCTIssueCommentNew : OCTObject 14 | 15 | // The webpage URL for this comment. 16 | @property (nonatomic, copy, readonly) NSURL *HTMLURL; 17 | 18 | // The webpage URL for this comment. 19 | @property (nonatomic, copy, readonly) NSDictionary *user; 20 | 21 | // The webpage URL for this comment. 22 | @property (nonatomic, copy, readonly) NSDate *createdAt; 23 | 24 | // The webpage URL for this comment. 25 | @property (nonatomic, copy, readonly) NSDate *updatedAt; 26 | 27 | // The webpage URL for this comment. 28 | @property (nonatomic, copy, readonly) NSString *body; 29 | 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /libGitHubIssues/OCTIssueComment+New.m: -------------------------------------------------------------------------------- 1 | // 2 | // OCTIssueComment.m 3 | // OctoKit 4 | // 5 | // Created by Justin Spahr-Summers on 2012-10-02. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "OCTIssueComment+New.h" 10 | #import "NSValueTransformer+OCTPredefinedTransformerAdditions.h" 11 | 12 | @implementation OCTIssueCommentNew 13 | 14 | #pragma mark MTLJSONSerializing 15 | 16 | + (NSDictionary *)JSONKeyPathsByPropertyKey { 17 | return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{ 18 | @"HTMLURL": @"html_url", 19 | @"user": @"user", 20 | @"createdAt": @"created_at", 21 | @"updatedAt": @"updated_at", 22 | @"body": @"body", 23 | }]; 24 | } 25 | 26 | + (NSValueTransformer *)HTMLURLJSONTransformer { 27 | return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; 28 | } 29 | 30 | + (NSValueTransformer *)createdAtJSONTransformer { 31 | return [NSValueTransformer valueTransformerForName:OCTDateValueTransformerName]; 32 | } 33 | 34 | + (NSValueTransformer *)updatedAtJSONTransformer { 35 | return [NSValueTransformer valueTransformerForName:OCTDateValueTransformerName]; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /libGitHubIssues/Supporting Files/libGitHubIssues_Mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues/Supporting Files/libGitHubIssues_Mark.png -------------------------------------------------------------------------------- /libGitHubIssues/Supporting Files/libGitHubIssues_Mark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues/Supporting Files/libGitHubIssues_Mark@2x.png -------------------------------------------------------------------------------- /libGitHubIssues/Supporting Files/libGitHubIssues_Mark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues/Supporting Files/libGitHubIssues_Mark@3x.png -------------------------------------------------------------------------------- /libGitHubIssues/Supporting Files/libGitHubIssues_Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues/Supporting Files/libGitHubIssues_Profile.png -------------------------------------------------------------------------------- /libGitHubIssues/Supporting Files/libGitHubIssues_Profile@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues/Supporting Files/libGitHubIssues_Profile@2x.png -------------------------------------------------------------------------------- /libGitHubIssues/Supporting Files/libGitHubIssues_Profile@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matchstic/libGitHubIssues/HEAD/libGitHubIssues/Supporting Files/libGitHubIssues_Profile@3x.png -------------------------------------------------------------------------------- /libGitHubIssues/libGitHubIssues.h: -------------------------------------------------------------------------------- 1 | #import "GIRootViewController.h" --------------------------------------------------------------------------------