diff options
-rw-r--r-- | Juick.xcodeproj/project.pbxproj | 8 | ||||
-rw-r--r-- | Juick/APIClient.h | 3 | ||||
-rw-r--r-- | Juick/APIClient.m | 331 | ||||
-rw-r--r-- | Juick/AppDelegate.m | 11 | ||||
-rw-r--r-- | Juick/Helpers/NSData+Hex.h | 19 | ||||
-rw-r--r-- | Juick/Helpers/NSData+Hex.m | 22 | ||||
-rw-r--r-- | Juick/Main.storyboard | 6 | ||||
-rw-r--r-- | Juick/Supporting Files/Juick-Prefix.pch | 2 | ||||
-rw-r--r-- | Juick/ViewControllers/ChatViewController.m | 2 | ||||
-rw-r--r-- | Juick/ViewControllers/FeedViewController.m | 2 | ||||
-rw-r--r-- | Juick/ViewControllers/LoginViewController.m | 16 | ||||
-rw-r--r-- | Juick/ViewControllers/MessagesViewController.m | 6 | ||||
-rw-r--r-- | Juick/ViewControllers/NewPostViewController.h | 3 | ||||
-rw-r--r-- | Juick/ViewControllers/NewPostViewController.m | 29 | ||||
-rw-r--r-- | Juick/ViewControllers/ThreadViewController.m | 6 | ||||
-rw-r--r-- | Podfile | 2 | ||||
-rw-r--r-- | Podfile.lock | 24 |
17 files changed, 284 insertions, 208 deletions
diff --git a/Juick.xcodeproj/project.pbxproj b/Juick.xcodeproj/project.pbxproj index a7b563a..5a8a4fb 100644 --- a/Juick.xcodeproj/project.pbxproj +++ b/Juick.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 776C41BD1FD3EF180063B82E /* MessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 776C41BB1FD3EF180063B82E /* MessageCell.m */; }; 776C41BE1FD3EF180063B82E /* MessageCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 776C41BC1FD3EF180063B82E /* MessageCell.xib */; }; 776C41C11FD3FF6E0063B82E /* FeedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 776C41C01FD3FF6E0063B82E /* FeedViewController.m */; }; + 778560602343D24E00BB37A2 /* NSData+Hex.m in Sources */ = {isa = PBXBuildFile; fileRef = 7785605F2343D24E00BB37A2 /* NSData+Hex.m */; }; 77975A1D182B6E9A00410C2B /* NewPostViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 77975A1C182B6E9A00410C2B /* NewPostViewController.m */; }; 77975A1F182BDCE900410C2B /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77975A1E182BDCE900410C2B /* SystemConfiguration.framework */; }; 77975A21182BDCF300410C2B /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77975A20182BDCF300410C2B /* MobileCoreServices.framework */; }; @@ -141,6 +142,8 @@ 776C41BC1FD3EF180063B82E /* MessageCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MessageCell.xib; sourceTree = "<group>"; }; 776C41BF1FD3FF6E0063B82E /* FeedViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FeedViewController.h; sourceTree = "<group>"; }; 776C41C01FD3FF6E0063B82E /* FeedViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FeedViewController.m; sourceTree = "<group>"; }; + 7785605E2343D24E00BB37A2 /* NSData+Hex.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSData+Hex.h"; sourceTree = "<group>"; }; + 7785605F2343D24E00BB37A2 /* NSData+Hex.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Hex.m"; sourceTree = "<group>"; }; 77975A1B182B6E9A00410C2B /* NewPostViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewPostViewController.h; sourceTree = "<group>"; }; 77975A1C182B6E9A00410C2B /* NewPostViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewPostViewController.m; sourceTree = "<group>"; }; 77975A1E182BDCE900410C2B /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; @@ -339,6 +342,8 @@ 77C67EE41828342000427098 /* NSURL+PathParameters.m */, 77B09992189D0B9900A84F59 /* UIImage+Utils.h */, 77B09993189D0B9900A84F59 /* UIImage+Utils.m */, + 7785605E2343D24E00BB37A2 /* NSData+Hex.h */, + 7785605F2343D24E00BB37A2 /* NSData+Hex.m */, ); path = Helpers; sourceTree = "<group>"; @@ -537,12 +542,10 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Juick/Pods-Juick-resources.sh", "${PODS_ROOT}/DateTools/DateTools/DateTools/DateTools.bundle", - "${PODS_ROOT}/SAMKeychain/Support/SAMKeychain.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/DateTools.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SAMKeychain.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -578,6 +581,7 @@ 77C67EE51828342000427098 /* NSURL+PathParameters.m in Sources */, 77E35A82189A5B5A00B2D216 /* LoginViewController.m in Sources */, 773E6397204BCB64008B8F8D /* ConversationCell.m in Sources */, + 778560602343D24E00BB37A2 /* NSData+Hex.m in Sources */, 776C41BD1FD3EF180063B82E /* MessageCell.m in Sources */, 77317BB8181BBE8500D60005 /* main.m in Sources */, 77B8B39C207A5629005CB20C /* MessageInputView.m in Sources */, diff --git a/Juick/APIClient.h b/Juick/APIClient.h index f2b2a7c..d8317a4 100644 --- a/Juick/APIClient.h +++ b/Juick/APIClient.h @@ -9,9 +9,10 @@ #import "Message.h" #import "DeviceRegistration.h" -@interface APIClient : NSObject +@interface APIClient : NSObject<NSURLSessionDelegate, NSURLSessionTaskDelegate> @property (nonatomic, strong) NSDateFormatter *dateFormatter; +@property (nonatomic, strong) NSURLCredential *credential; +(APIClient *) sharedClient; diff --git a/Juick/APIClient.m b/Juick/APIClient.m index 2712f1d..3332556 100644 --- a/Juick/APIClient.m +++ b/Juick/APIClient.m @@ -9,10 +9,12 @@ #import "Message.h" #import "Chat.h" -@interface APIClient() -@property(nonatomic, readwrite) AFHTTPSessionManager *manager; +@interface APIClient () @property(nonatomic, strong) NSOperationQueue *backgroundQueue; +@property(nonatomic, strong) NSURL *baseURL; @property(nonatomic, strong) NSURLSession *urlSession; +@property(nonatomic, strong) NSURLProtectionSpace *apiProtectionSpace; +-(void) get:(NSString *)path params:(NSDictionary *)params callback:(void(^)(NSDictionary *, NSError *))callback; @end @implementation APIClient @@ -30,197 +32,244 @@ if (self = [super init]) { NSString *baseURLString = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"base_url"]; NSLog(@"Initializing with %@ base URL", baseURLString); - _manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:baseURLString]]; - _manager.requestSerializer = [AFJSONRequestSerializer new]; - [_manager.requestSerializer setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; + self.baseURL = [NSURL URLWithString:baseURLString]; + self.apiProtectionSpace = [[NSURLProtectionSpace alloc] initWithHost:self.baseURL.host port:0 protocol:NSURLProtectionSpaceHTTPS realm:@"Juick" authenticationMethod:NSURLAuthenticationMethodHTTPBasic]; self.backgroundQueue = [NSOperationQueue new]; + self.urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:self.backgroundQueue]; + self.dateFormatter = [[NSDateFormatter alloc] init]; self.dateFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss"; [self.dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]; - _urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; } return self; } --(AFHTTPSessionManager *) manager { - NSString *username = [SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.username"]; - if (username) { - [_manager.requestSerializer - setAuthorizationHeaderFieldWithUsername:username - password:[SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.password"]]; - } - return _manager; -} - -(void) pullNextFromPath:(NSString *)path params:(NSDictionary *) params callback:(void(^)(NSArray<Message *> *, NSError *))callback { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self.backgroundQueue addOperationWithBlock:^{ - [self.manager GET:path parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - NSInteger statusCode = [((NSHTTPURLResponse *) task.response) statusCode]; - if (statusCode == 200) { - NSMutableArray *messages = [NSMutableArray new]; - [((NSArray *)responseObject) enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [messages addObject:[Message fromJSON:obj]]; - }]; - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(messages, nil); - }]; - } else { - callback(nil, [NSError errorWithDomain:@"JuickErrorDomain" code:statusCode userInfo:nil]); - } - - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - NSLog(@"REST Error: %@", error); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - NSInteger statusCode = ((NSHTTPURLResponse *)task.response).statusCode; - callback(nil, [NSError errorWithDomain:@"JuickErrorDomain" code:statusCode userInfo:nil]); - }]; + [self get:path params:params callback:^(NSDictionary *response, NSError *error) { + NSMutableArray *messages = [NSMutableArray new]; + if (!error) { + [((NSArray *)response) enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [messages addObject:[Message fromJSON:obj]]; }]; - }]; - }); + } + callback(messages, error); + }]; } -(void) postMessage:(NSString *)text result:(void (^)(Message *, NSError *))callback { - [self.backgroundQueue addOperationWithBlock:^{ - [self.manager POST:@"post" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) { - [formData appendPartWithFormData:[text dataUsingEncoding:NSUTF8StringEncoding] name:@"body"]; - } progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - NSLog(@"Success!"); - Message *result = [Message fromJSON:responseObject]; - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(result, nil); - }]; - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - NSLog(@"Error: %@", [error localizedDescription]); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(nil, error); - }]; - }]; + [self post:@"post" params:@{ + @"body": text + } callback:^(NSDictionary *response, NSError *err) { + Message *result; + if (!err) { + result = [Message fromJSON:response[@"newMessage"]]; + } + callback(result, err); }]; } -(void) postReplyToThread:(NSNumber *)mid inReplyTo:(NSNumber *)rid text:(NSString *)text result:(void(^)(Message *, NSError *))callback { - [self.backgroundQueue addOperationWithBlock:^{ - [self.manager POST:@"comment" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) { - [formData appendPartWithFormData:[[mid stringValue] dataUsingEncoding:NSUTF8StringEncoding] name:@"mid"]; - [formData appendPartWithFormData:[[NSString stringWithFormat:@"%d", [rid intValue]] dataUsingEncoding:NSUTF8StringEncoding] name:@"rid"]; - [formData appendPartWithFormData:[text dataUsingEncoding:NSUTF8StringEncoding] name:@"body"]; - } progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { + [self post:@"comment" params:@{ + @"mid": mid, + @"rid": [NSString stringWithFormat:@"%ld", [rid integerValue]], + @"body": text + } callback:^(NSDictionary *response, NSError *err) { + Message *reply; + if (!err) { NSLog(@"Success!"); - Message *reply = [Message fromJSON:responseObject]; - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(reply, nil); - }]; - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - NSLog(@"Error: %@", [error localizedDescription]); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(nil, error); - }]; - }]; + reply = [Message fromJSON:response]; + } + callback(reply, err); }]; } + -(void) postPMToUser:(NSString *)uname text:(NSString *)text result:(void (^)(NSError *))callback { [self.backgroundQueue addOperationWithBlock:^{ - [self.manager POST:@"pm" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) { - [formData appendPartWithFormData:[text dataUsingEncoding:NSUTF8StringEncoding] name:@"body"]; - [formData appendPartWithFormData:[uname dataUsingEncoding:NSUTF8StringEncoding] name:@"uname"]; - } progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(nil); - }]; - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - NSLog(@"Error: %@", [error localizedDescription]); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(error); - }]; + [self post:@"pm" params:@{ + @"uname": uname, + @"body": text + } callback:^(NSDictionary *response, NSError *err) { + callback(err); }]; }]; } -(void) fetchChats:(void (^)(NSArray *, NSError *))callback { - [self.backgroundQueue addOperationWithBlock:^{ - [self.manager GET:@"groups_pms" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - NSMutableArray *groups = [NSMutableArray new]; - NSArray *pms = [(NSDictionary *)responseObject objectForKey:@"pms"]; - [pms enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [groups addObject:[Chat fromJSON:obj]]; - }]; - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(groups, nil); - }]; - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - NSLog(@"Error: %@", [error localizedDescription]); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(nil, error); - }]; + [self get:@"groups_pms" params:nil callback:^(NSDictionary *response, NSError *err) { + NSMutableArray *groups = [NSMutableArray new]; + NSArray *pms = [response objectForKey:@"pms"]; + [pms enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [groups addObject:[Chat fromJSON:obj]]; }]; + callback(groups, err); }]; } + -(void) fetchChatWithUser:(NSString *)uname callback:(void (^)(NSArray *, NSError *))callback { NSDictionary *params = @{@"uname": uname}; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self.backgroundQueue addOperationWithBlock:^{ - [self.manager GET:@"pm" parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - NSMutableArray *messages = [NSMutableArray new]; - NSArray *messagesList = (NSArray *)responseObject; - [messagesList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [messages addObject:[Message fromJSON:obj]]; - }]; - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(messages, nil); - }]; - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - NSLog(@"Error: %@", [error localizedDescription]); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(nil, error); - }]; + [self get:@"pm" params:params callback:^(NSDictionary *response, NSError *err) { + NSMutableArray *messages = [NSMutableArray new]; + if (!err) { + NSArray *messagesList = (NSArray *)response; + [messagesList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [messages addObject:[Message fromJSON:obj]]; }]; - }]; - }); + } + callback(messages, err); + }]; } + -(void) authenticate:(void (^)(User *user, NSError *error))callback { - [self.manager GET:@"me" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - NSString *username = [SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.username"]; - NSString *password = [SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.password"]; - [self.manager.requestSerializer setAuthorizationHeaderFieldWithUsername:username password:password]; - callback([User fromJSON:responseObject], nil); - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - [self.manager.requestSerializer clearAuthorizationHeader]; - callback(nil, error); + [self get:@"me" params:nil callback:^(NSDictionary *response, NSError *error) { + User *user; + if (!error) { + user = [User fromJSON:response]; + } + callback(user, error); }]; } -(void) getUserByName:(NSString *) name callback:(void(^)(User *))callback { NSDictionary *params = @{@"uname": name}; - [self.manager GET:@"users" parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - callback([User fromJSON:[(NSArray *)responseObject firstObject]]); - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - callback(nil); + [self get:@"users" params:params callback:^(NSDictionary *response, NSError *err) { + if (!err) { + callback([User fromJSON:[(NSArray *)response firstObject]]); + } else { + callback(nil); + } }]; } -(void) refreshDeviceRegistration:(DeviceRegistration *)registrationData callback:(void (^)(BOOL))callback { - [self.manager PUT:@"/notifications" parameters:@[[registrationData toJSON]] success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { - callback(YES); - } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { - NSLog(@"fail %@", [error localizedDescription]); - callback(NO); - } ]; + NSURL *notificationsUrl = [NSURL URLWithString:@"notifications" relativeToURL:self.baseURL]; + NSError *error; + NSDictionary *token = [registrationData toJSON]; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@[token] options:kNilOptions error:&error]; + [self fetchDataWithURL:notificationsUrl data:jsonData boundary:nil callback:^(NSData *response, NSError *err) { + callback(!err); + }]; } + +- (NSURLCredential *)credential { + NSDictionary *credentials = [[NSURLCredentialStorage sharedCredentialStorage] credentialsForProtectionSpace:self.apiProtectionSpace]; + return [credentials.objectEnumerator nextObject]; +} + +- (void)setCredential:(NSURLCredential *)credential { + [[NSURLCredentialStorage sharedCredentialStorage] setCredential:credential forProtectionSpace:self.apiProtectionSpace]; +} + + -(BOOL) isAuthenticated { - NSString *password = [SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.password"]; + NSString *password = self.credential.password; return password != nil; } -- (void)fetchImageWithURL:(NSURL *)url callback:(void (^)(NSData *))callback { - NSURLSessionDataTask *dataTask = [self.urlSession dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - callback(data); - }]; +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { + if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) { + completionHandler(NSURLSessionAuthChallengeUseCredential, self.credential); + } else { + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + } +} + +-(void) fetchDataWithURL:(NSURL *) url data:(NSData *)postData boundary:(NSString *)boundary callback:(void(^)(NSData *, NSError *))callback { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + if (boundary) { + request.HTTPMethod = @"POST"; + request.HTTPBody = postData; + NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; + [request setValue:contentType forHTTPHeaderField: @"Content-Type"]; + } else if (postData) { + request.HTTPMethod = @"PUT"; + request.HTTPBody = postData; + [request setValue:@"application/json" forHTTPHeaderField: @"Content-Type"]; + } else { + request.HTTPMethod = @"GET"; + } + + NSURLSessionDataTask *task = [self.urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; + if (!response || statusCode != 200) { + NSError *err = [NSError errorWithDomain:@"JuickErrorDomain" + code:statusCode + userInfo:nil]; + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + callback(nil, err); + }]; + } else { + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + callback(data, error); + }]; + } }]; [self.backgroundQueue addOperationWithBlock:^{ - [dataTask resume]; + [task resume]; + }]; +} + +-(NSURL *) url:(NSURL *)url withParameters:(NSDictionary *)params { + NSURLComponents *parametersUrl = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; + NSMutableArray *items = [NSMutableArray array]; + for (NSString * key in params) { + NSString *value = [NSString stringWithFormat:@"%@", params[key]]; + [items addObject:[NSURLQueryItem queryItemWithName:key value:value]]; + } + [parametersUrl setQueryItems:items]; + return [parametersUrl URL]; +} + +-(NSData *) multipartData:(NSDictionary *)params withBoundary:(NSString *)boundary { + NSMutableData *body = [NSMutableData data]; + for (NSString *key in params) { + [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=%@\r\n\r\n", key] dataUsingEncoding:NSUTF8StringEncoding]]; + [body appendData:[[NSString stringWithFormat:@"%@\r\n", params[key]] dataUsingEncoding:NSUTF8StringEncoding]]; + } + [body appendData:[[NSString stringWithFormat:@"--%@--", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; + return body; +} + +-(void) get:(NSString *) path params:(NSDictionary *)params callback:(void(^)(NSDictionary *, NSError *))callback { + NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseURL]; + NSURL *requestUrl = params ? [self url:url withParameters:params] : url; + [self fetchDataWithURL:requestUrl data:nil boundary:nil callback:^(NSData *data, NSError *err) { + if (err) { + callback(nil, err); + } else { + NSError *jsonError; + NSDictionary *jsonData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + if (jsonError) { + callback(nil, jsonError); + } else { + callback(jsonData, nil); + } + } + }]; +} + +-(void) post:(NSString *) path params:(NSDictionary *)params callback:(void(^)(NSDictionary *, NSError *))callback { + NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseURL]; + NSString *boundary = [NSString stringWithFormat:@"Boundary-%@", [[NSUUID UUID] UUIDString]]; + [self fetchDataWithURL:url data:[self multipartData:params withBoundary:boundary] boundary:boundary callback:^(NSData *data, NSError *err) { + NSError *jsonError; + NSDictionary *jsonData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + if (jsonError) { + callback(nil, jsonError); + } else { + callback(jsonData, nil); + } + }]; +} + +-(void) fetchImageWithURL:(NSURL *) url callback:(void(^)(NSData *))callback { + [self fetchDataWithURL:url data:nil boundary:nil callback:^(NSData *data, NSError *err) { + if (err) { + callback(nil); + } else { + callback(data); + } }]; } diff --git a/Juick/AppDelegate.m b/Juick/AppDelegate.m index 9501316..61c8da5 100644 --- a/Juick/AppDelegate.m +++ b/Juick/AppDelegate.m @@ -16,6 +16,7 @@ #import "DeviceRegistration.h" #import "NewPostViewController.h" #import "FeedViewController.h" +#import "NSData+Hex.h" NSString * const UserUpdatedNotificationName = @"UserUpdated"; @@ -40,13 +41,6 @@ NSString * const UserUpdatedNotificationName = @"UserUpdated"; NSDictionary *userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (userInfo) { [self parseNotificationPayload:userInfo]; - } else { - if ([[NSUserDefaults standardUserDefaults] objectForKey:@"FirstRun"] == nil) { - [SAMKeychain deletePasswordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.username"]; - [SAMKeychain deletePasswordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.password"]; - [SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock]; - [[NSUserDefaults standardUserDefaults] setObject:@"1" forKey:@"FirstRun"]; - } } return YES; } @@ -64,8 +58,7 @@ NSString * const UserUpdatedNotificationName = @"UserUpdated"; } -(void) application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - NSString *token = [[deviceToken description] stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:@"<>"]]; - token = [token stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSString *token = [deviceToken hexString]; [[NSOperationQueue new] addOperationWithBlock:^{ DeviceRegistration *registration = [DeviceRegistration new]; registration.type = @"apns"; diff --git a/Juick/Helpers/NSData+Hex.h b/Juick/Helpers/NSData+Hex.h new file mode 100644 index 0000000..bd8e519 --- /dev/null +++ b/Juick/Helpers/NSData+Hex.h @@ -0,0 +1,19 @@ +// +// NSData+Hex.h +// Juick +// +// Created by Vitaly Takmazov on 01/10/2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (Hex) + +- (NSString *)hexString; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Juick/Helpers/NSData+Hex.m b/Juick/Helpers/NSData+Hex.m new file mode 100644 index 0000000..243a380 --- /dev/null +++ b/Juick/Helpers/NSData+Hex.m @@ -0,0 +1,22 @@ +// +// NSData+Hex.m +// Juick +// +// Created by Vitaly Takmazov on 01/10/2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +#import "NSData+Hex.h" + +@implementation NSData (Hex) +- (NSString *)hexString { + NSMutableString *string = [NSMutableString stringWithCapacity:self.length * 3]; + [self enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop){ + for (NSUInteger offset = 0; offset < byteRange.length; ++offset) { + uint8_t byte = ((const uint8_t *)bytes)[offset]; + [string appendFormat:@"%02x", byte]; + } + }]; + return string; +} +@end diff --git a/Juick/Main.storyboard b/Juick/Main.storyboard index 1ab86fe..81266dd 100644 --- a/Juick/Main.storyboard +++ b/Juick/Main.storyboard @@ -143,9 +143,6 @@ <userDefinedRuntimeAttributes> <userDefinedRuntimeAttribute type="string" keyPath="placeholder" value="Username..."/> </userDefinedRuntimeAttributes> - <connections> - <action selector="usernameChanged:" destination="4g9-hM-bzq" eventType="editingChanged" id="qst-Yi-KZ6"/> - </connections> </textField> <textField opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="xGs-fu-6K0"> <rect key="frame" x="0.0" y="274" width="339" height="34"/> @@ -154,9 +151,6 @@ <userDefinedRuntimeAttributes> <userDefinedRuntimeAttribute type="string" keyPath="placeholder" value="Password..."/> </userDefinedRuntimeAttributes> - <connections> - <action selector="passwordChanged:" destination="4g9-hM-bzq" eventType="editingChanged" id="Td0-2L-Dgo"/> - </connections> </textField> </subviews> </stackView> diff --git a/Juick/Supporting Files/Juick-Prefix.pch b/Juick/Supporting Files/Juick-Prefix.pch index c9a0d25..3a66330 100644 --- a/Juick/Supporting Files/Juick-Prefix.pch +++ b/Juick/Supporting Files/Juick-Prefix.pch @@ -17,9 +17,7 @@ #import <MobileCoreServices/MobileCoreServices.h> #import <UserNotifications/UserNotifications.h> - #import <SAMKeychain/SAMKeychain.h> #import "UIImage+Utils.h" - #import <AFNetworking/AFNetworking.h> #import <DateTools/DateTools.h> #import <PHFComposeBarView/PHFComposeBarView.h> #import <UIView+Shimmer.h> diff --git a/Juick/ViewControllers/ChatViewController.m b/Juick/ViewControllers/ChatViewController.m index 0de3d2a..989b531 100644 --- a/Juick/ViewControllers/ChatViewController.m +++ b/Juick/ViewControllers/ChatViewController.m @@ -26,7 +26,7 @@ self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; self.tableView.allowsSelection = NO; - self.me = [SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.username"]; + self.me = [APIClient sharedClient].credential.user; [self reloadChat]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; self.refreshControl = [UIRefreshControl new]; diff --git a/Juick/ViewControllers/FeedViewController.m b/Juick/ViewControllers/FeedViewController.m index 523ea52..31bf0ca 100644 --- a/Juick/ViewControllers/FeedViewController.m +++ b/Juick/ViewControllers/FeedViewController.m @@ -70,7 +70,7 @@ } } -- (void)userDidSignedIn:(User *) user { +- (void)userDidSignedIn:(NSNotification *) notification { [self refreshPath]; [self refreshData]; } diff --git a/Juick/ViewControllers/LoginViewController.m b/Juick/ViewControllers/LoginViewController.m index 898e59c..f9b17a3 100644 --- a/Juick/ViewControllers/LoginViewController.m +++ b/Juick/ViewControllers/LoginViewController.m @@ -29,8 +29,8 @@ NSString * const UserSignedInNotificationName = @"UserSignedIn"; target:self action:@selector(cancelSignIn)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(doneSignIn)]; - self.usernameField.text = [SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.username"]; - self.passwordField.text = [SAMKeychain passwordForService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.password"];; + self.usernameField.text = [APIClient sharedClient].credential.user; + self.passwordField.text = [APIClient sharedClient].credential.password; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil]; } @@ -57,6 +57,7 @@ NSString * const UserSignedInNotificationName = @"UserSignedIn"; } - (void) doneSignIn { + [[APIClient sharedClient] setCredential:[NSURLCredential credentialWithUser:self.usernameField.text password:self.passwordField.text persistence:NSURLCredentialPersistenceSynchronizable]]; [[APIClient sharedClient] authenticate:^(User *user, NSError *error) { if (user) { [[NSNotificationCenter defaultCenter] postNotificationName:UserSignedInNotificationName object:user]; @@ -73,15 +74,4 @@ NSString * const UserSignedInNotificationName = @"UserSignedIn"; } }]; } -- (IBAction)passwordChanged:(id)sender { - if ([self.passwordField.text isKindOfClass:[NSString class]]) { - [SAMKeychain setPassword:self.passwordField.text forService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.password"]; - } -} - -- (IBAction)usernameChanged:(id)sender { - if ([self.usernameField.text isKindOfClass:[NSString class]]) { - [SAMKeychain setPassword:self.usernameField.text forService:[[NSBundle mainBundle] bundleIdentifier] account:@"com.juick.username"]; - } -} @end diff --git a/Juick/ViewControllers/MessagesViewController.m b/Juick/ViewControllers/MessagesViewController.m index afa0f9a..ad28484 100644 --- a/Juick/ViewControllers/MessagesViewController.m +++ b/Juick/ViewControllers/MessagesViewController.m @@ -106,6 +106,7 @@ NSString* const messageCellIdentifier = @"messageCell"; self.refreshControl = [UIRefreshControl new]; [self.refreshControl addTarget:self action:@selector(refreshData) forControlEvents:UIControlEventValueChanged]; [self refreshData]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(newMessage:) name:NewMessageNotificationName object:nil]; } - (void) composePressed { @@ -174,4 +175,9 @@ NSString* const messageCellIdentifier = @"messageCell"; [self.navigationController pushViewController:threadVC animated:YES]; } +-(void) newMessage:(NSNotification *)obj { + Message *msg = (Message *)[obj object]; + [self viewThreadForMessage:msg mid:msg.mid scrollTo:0]; +} + @end diff --git a/Juick/ViewControllers/NewPostViewController.h b/Juick/ViewControllers/NewPostViewController.h index 7a1dfac..05e3c73 100644 --- a/Juick/ViewControllers/NewPostViewController.h +++ b/Juick/ViewControllers/NewPostViewController.h @@ -9,6 +9,9 @@ #import <UIKit/UIKit.h> #import "Message.h" +extern NSString * const NewMessageNotificationName; +extern NSString * const ReplyPostedNotificationName; + @interface NewPostViewController : UIViewController @property (weak, nonatomic) IBOutlet UITextView *textView; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint; diff --git a/Juick/ViewControllers/NewPostViewController.m b/Juick/ViewControllers/NewPostViewController.m index 65172f1..afa14a9 100644 --- a/Juick/ViewControllers/NewPostViewController.m +++ b/Juick/ViewControllers/NewPostViewController.m @@ -10,6 +10,10 @@ #import "MessagesViewController.h" #import "QuoteView.h" #import "APIClient.h" +#import "AppDelegate.h" + +NSString * const NewMessageNotificationName = @"NewMessage"; +NSString * const ReplyPostedNotificationName = @"ReplyPosted"; @interface NewPostViewController () @@ -42,17 +46,28 @@ - (IBAction)sendAction:(id)sender { self.navigationController.navigationItem.rightBarButtonItem.enabled = NO; if (_replyTo == nil) { + self.navigationController.navigationItem.rightBarButtonItem.enabled = YES; [[APIClient sharedClient] postMessage:self.textView.text result:^(Message *msg, NSError *err) { - self.navigationController.navigationItem.rightBarButtonItem.enabled = YES; - [self.navigationController popViewControllerAnimated:YES]; - [(MessagesViewController *)self.navigationController.visibleViewController setShouldScrollToUnreadOnRefresh:YES]; + if (!err) { + [self dismissViewControllerAnimated:YES completion:^{ + [[NSNotificationCenter defaultCenter] postNotificationName:NewMessageNotificationName object:msg]; + }]; + } else { + self.navigationController.navigationItem.rightBarButtonItem.enabled = YES; + // TODO: display error + } }]; } else { + self.navigationController.navigationItem.rightBarButtonItem.enabled = NO; [[APIClient sharedClient] postReplyToThread:_replyTo.mid inReplyTo:_replyTo.rid text:self.textView.text result:^(Message *msg, NSError *err) { - self.navigationController.navigationItem.rightBarButtonItem.enabled = YES; - [self.navigationController popViewControllerAnimated:YES]; - [(MessagesViewController *)self.navigationController.visibleViewController setShouldScrollToUnreadOnRefresh:YES]; - [(MessagesViewController *)self.navigationController.visibleViewController refreshData]; + if (!err) { + [self dismissViewControllerAnimated:YES completion:^{ + [[NSNotificationCenter defaultCenter] postNotificationName:ReplyPostedNotificationName object:msg]; + }]; + } else { + self.navigationController.navigationItem.rightBarButtonItem.enabled = YES; + // TODO: display error + } }]; } } diff --git a/Juick/ViewControllers/ThreadViewController.m b/Juick/ViewControllers/ThreadViewController.m index 3ea64f7..af56bc3 100644 --- a/Juick/ViewControllers/ThreadViewController.m +++ b/Juick/ViewControllers/ThreadViewController.m @@ -19,6 +19,7 @@ [self setPath:[APIClient threadUrl]]; [super viewDidLoad]; self.messagesDelegate = self; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(replyPosted:) name:ReplyPostedNotificationName object:nil]; } -(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { @@ -41,4 +42,9 @@ [self refreshData]; } +- (void)replyPosted:(NSNotification *) notification { + [self setShouldScrollToUnreadOnRefresh:YES]; + [self refreshData]; +} + @end @@ -2,8 +2,6 @@ source 'https://cdn.jsdelivr.net/cocoa/' platform :ios, "11.1" target "Juick" do - pod 'AFNetworking' - pod 'SAMKeychain' pod 'DateTools' pod 'PHFComposeBarView' pod 'UIView+Shimmer' diff --git a/Podfile.lock b/Podfile.lock index 107c8bc..08a1a2b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,50 +1,28 @@ PODS: - - AFNetworking (3.2.1): - - AFNetworking/NSURLSession (= 3.2.1) - - AFNetworking/Reachability (= 3.2.1) - - AFNetworking/Security (= 3.2.1) - - AFNetworking/Serialization (= 3.2.1) - - AFNetworking/UIKit (= 3.2.1) - - AFNetworking/NSURLSession (3.2.1): - - AFNetworking/Reachability - - AFNetworking/Security - - AFNetworking/Serialization - - AFNetworking/Reachability (3.2.1) - - AFNetworking/Security (3.2.1) - - AFNetworking/Serialization (3.2.1) - - AFNetworking/UIKit (3.2.1): - - AFNetworking/NSURLSession - DateTools (2.0.0) - PHFComposeBarView (2.1.0): - PHFDelegateChain (~> 1.0) - PHFDelegateChain (1.0.1) - - SAMKeychain (1.5.3) - "UIView+Shimmer (1.0.0)" DEPENDENCIES: - - AFNetworking - DateTools - PHFComposeBarView - - SAMKeychain - "UIView+Shimmer" SPEC REPOS: https://cdn.jsdelivr.net/cocoa/: - - AFNetworking - DateTools - PHFComposeBarView - PHFDelegateChain - - SAMKeychain - "UIView+Shimmer" SPEC CHECKSUMS: - AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057 DateTools: 933ac9c490f21f92127cf690ccd8c397e0126caf PHFComposeBarView: 6382ab846e2f4d8634273c4a78d074bc5deed07f PHFDelegateChain: 491f9cd8a3fb8761f390ff05f74a0e168d48d285 - SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c "UIView+Shimmer": ed634f95e8f4bda666b28b47bd85a4336a4117d8 -PODFILE CHECKSUM: df93a469bf7674ef7f4d2c559037f99f6c878d5f +PODFILE CHECKSUM: 07617045e3d7a5f7cdf98542a754f10d85b5ff19 COCOAPODS: 1.7.1 |