├── .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 | 
7 | 
8 | 
9 | 
10 | 
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:@""];
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:@""];
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"
--------------------------------------------------------------------------------