diff --git a/Blog/AppDelegate.m b/Blog/AppDelegate.m index 26d8c04..f039032 100644 --- a/Blog/AppDelegate.m +++ b/Blog/AppDelegate.m @@ -103,9 +103,14 @@ #pragma mark - UISplitViewDelegate methods -- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController { - UINavigationController *navigationController = [secondaryViewController isKindOfClass:[UINavigationController class]] ? (UINavigationController *)secondaryViewController : nil; - if ([navigationController.topViewController isKindOfClass:[EditorViewController class]] && ([(EditorViewController *)navigationController.topViewController post] == nil)) { +- (BOOL) splitViewController:(UISplitViewController *)splitViewController +collapseSecondaryViewController:(UIViewController *)secondaryViewController + ontoPrimaryViewController:(UIViewController *)primaryViewController { + UINavigationController *navigationController = [secondaryViewController isKindOfClass:[UINavigationController class]] + ? (UINavigationController *)secondaryViewController : nil; + if ([navigationController + .topViewController isKindOfClass:[EditorViewController class]] && ([(EditorViewController *)navigationController + .topViewController post] == nil)) { // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. return YES; } diff --git a/Blog/BlogController.h b/Blog/BlogController.h index e2b047f..31b4265 100644 --- a/Blog/BlogController.h +++ b/Blog/BlogController.h @@ -21,6 +21,7 @@ extern NSString *BlogPostDeletedNotification; @class PMKPromise; @class ModelStore; @class BlogService; +@class Post; @interface BlogController : NSObject @@ -36,8 +37,8 @@ extern NSString *BlogPostDeletedNotification; - (PMKPromise *)requestAllPostsWithCaching:(BOOL)useCache; - (PMKPromise *)requestPostWithPath:(NSString *)path; -- (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link; -- (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link; +- (PMKPromise *)requestCreateDraft:(Post *)draft; +- (PMKPromise *)requestUpdatePost:(Post *)post; - (PMKPromise *)requestPublishDraftWithPath:(NSString *)path; - (PMKPromise *)requestUnpublishPostWithPath:(NSString *)path; - (PMKPromise *)requestDeletePostWithPath:(NSString *)path; diff --git a/Blog/BlogController.m b/Blog/BlogController.m index 5a84e42..9b94644 100644 --- a/Blog/BlogController.m +++ b/Blog/BlogController.m @@ -100,24 +100,20 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification"; } } -- (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link { - Post *post = [[Post alloc] initWithDictionary:@{@"objectID": draftID ?: [NSNull null], - @"title": title ?: [NSNull null], - @"body": body ?: [NSNull null], - @"link": link ?: [NSNull null], - @"draft": @YES, - } error:nil]; - return [_service requestCreateDraftWithID:draftID title:title body:body link:link].then(^(Post *post) { +- (PMKPromise *)requestCreateDraft:(Post *)draft { + return [_service requestCreateDraftWithID:draft.objectID title:draft.title body:draft.body + link:draft.url.absoluteString].then(^(Post *post) { [_store addDraft:post]; return post; }); } -- (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link { - return [_service requestUpdatePostWithPath:path title:title body:body link:link].then(^(Post *post) { - [_store savePost:post]; - return post; - }); +- (PMKPromise *)requestUpdatePost:(Post *)post { + return [_service requestUpdatePostWithPath:post.path title:post.title body:post.body link:post.url.absoluteString] + .then(^(Post *post) { + [_store savePost:post]; + return post; + }); } - (PMKPromise *)requestPublishDraftWithPath:(NSString *)path { diff --git a/Blog/BlogService.m b/Blog/BlogService.m index dd611ad..bb0a481 100644 --- a/Blog/BlogService.m +++ b/Blog/BlogService.m @@ -92,7 +92,7 @@ NSString *const BlogServiceErrorDomain = @"BlogServiceErrorDomain"; - (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link { NSDictionary *fields = @{ @"id" : draftID, - @"title" : title, + @"title" : title ?: [NSNull null], @"body" : body, @"link" : link ?: [NSNull null], }; @@ -109,7 +109,7 @@ NSString *const BlogServiceErrorDomain = @"BlogServiceErrorDomain"; - (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link { NSDictionary *fields = @{ - @"title" : title, + @"title" : title ?: [NSNull null], @"body" : body, @"link" : link ?: [NSNull null], }; diff --git a/Blog/EditorViewController.h b/Blog/EditorViewController.h index 6f0919e..ae5e938 100644 --- a/Blog/EditorViewController.h +++ b/Blog/EditorViewController.h @@ -15,5 +15,6 @@ @property (strong, nonatomic) BlogController *blogController; @property (strong, nonatomic) Post *post; +@property (copy, nonatomic) void (^postUpdatedBlock)(Post *post); @end diff --git a/Blog/EditorViewController.m b/Blog/EditorViewController.m index 7e1c301..6c5911a 100644 --- a/Blog/EditorViewController.m +++ b/Blog/EditorViewController.m @@ -36,7 +36,7 @@ CGPoint scrollOffset = CGPointZero; if (self.post) { // FIXME: date, status (draft, published) - title = self.post.title ?: @"Untitled"; + title = self.post.title.length ? self.post.title : @"Untitled"; text = self.post.body; // TODO: restore scroll offset for this post ... user defaults? } @@ -67,20 +67,44 @@ } - (PMKPromise *)savePostBody { - if (!self.post || !self.textView) { return [PMKPromise promiseWithValue:nil]; } - - Post *newPost = [self.post copyWithBody:self.textView.text]; - if (![self.post isEqual:newPost]) - { - self.post = newPost; - return [self.blogController requestUpdatePostWithPath:self.post.path title:self.post.title body:self.post.body link:self.post.url.absoluteString] - .then(^(Post *post) { - NSLog(@"saved post at path %@", self.post.path); - }).catch(^(NSError *error) { - NSLog(@"Error saving post at path %@: %@ %@", self.post.path, error.localizedDescription, error.userInfo); - }); + NSString *body = self.textView.text; + if (!self.post || !body.length) { + return [PMKPromise promiseWithValue:nil]; } - return [PMKPromise promiseWithValue:self.post]; + + Post *newPost = [self.post copyWithBody:body]; + if ([newPost isEqual:self.post]) { + return [PMKPromise promiseWithValue:self.post]; + } + + self.post = newPost; + NSString *path = self.post.path; + PMKPromise *savePromise; + NSString *verb; + if (self.post.new) { + verb = @"create"; + savePromise = [self.blogController requestCreateDraft:self.post]; + } + else { + verb = @"update"; + savePromise = [self.blogController requestUpdatePost:self.post]; + } + return savePromise.then(^(Post *post) { + NSLog(@"%@ post at path %@", verb, path); + + // TODO: something better than this + // update our post because "new" may have changed, which is essential to correct operation + self.post = post; + [self configureView]; + if (self.postUpdatedBlock) { + self.postUpdatedBlock(self.post); + } + + return post; + }).catch(^(NSError *error) { + NSLog(@"Falied to %@ post at path %@: %@ %@", verb, path, error.localizedDescription, error.userInfo); + return error; + }); } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { diff --git a/Blog/Post.h b/Blog/Post.h index b0d84dd..48c7b3e 100644 --- a/Blog/Post.h +++ b/Blog/Post.h @@ -22,8 +22,10 @@ @property (nonatomic, readonly, strong) NSURL *url; @property (nonatomic, readonly, getter=isDraft) BOOL draft; @property (nonatomic, readonly, getter=isLink) BOOL link; +@property (nonatomic, readonly, getter=isNew) BOOL new; @property (nonatomic, readonly) NSString *formattedDate; ++ (instancetype)newDraftWithTitle:(NSString *)title body:(NSString *)body url:(NSURL *)url; - (instancetype)copyWithBody:(NSString *)body; - (instancetype)copyWithTitle:(NSString *)title; - (instancetype)copyWithURL:(NSURL *)url; diff --git a/Blog/Post.m b/Blog/Post.m index 7075c80..7c61b0a 100644 --- a/Blog/Post.m +++ b/Blog/Post.m @@ -41,56 +41,56 @@ }]; } ++ (instancetype)newDraftWithTitle:(NSString *)title body:(NSString *)body url:(NSURL *)url { + NSDictionary *fields = @{ + @"new" : @(YES), + @"draft" : @(YES), + @"title" : title ?: @"", + @"body" : body ?: @"", + @"url" : url ?: [NSNull null], + }; + return [[self alloc] initWithDictionary:fields error:nil]; +} + - (id)copyWithZone:(NSZone *)zone { return self; } - (instancetype)copyWithBody:(NSString *)body { - return [[Post alloc] initWithDictionary:@{@"objectID": self.objectID ?: [NSNull null], - @"slug": self.slug ?: [NSNull null], - @"author": self.author ?: [NSNull null], - @"title": self.title ?: [NSNull null], - @"date": self.date ?: [NSNull null], - @"body": body ?: [NSNull null], - @"path": self.path ?: [NSNull null], - @"url": self.url ?: [NSNull null], - @"draft": @(self.draft), - } error:nil]; + return [self copyWithTitle:self.title body:body url:self.url]; } - (instancetype)copyWithTitle:(NSString *)title { - return [[Post alloc] initWithDictionary:@{@"objectID": self.objectID ?: [NSNull null], - @"slug": self.slug ?: [NSNull null], - @"author": self.author ?: [NSNull null], - @"title": title ?: [NSNull null], - @"date": self.date ?: [NSNull null], - @"body": self.body ?: [NSNull null], - @"path": self.path ?: [NSNull null], - @"url": self.url ?: [NSNull null], - @"draft": @(self.draft), - } error:nil]; + return [self copyWithTitle:title body:self.body url:self.url]; } - (instancetype)copyWithURL:(NSURL *)url { - return [[Post alloc] initWithDictionary:@{@"objectID": self.objectID ?: [NSNull null], - @"slug": self.slug ?: [NSNull null], - @"author": self.author ?: [NSNull null], - @"title": self.title ?: [NSNull null], - @"date": self.date ?: [NSNull null], - @"body": self.body ?: [NSNull null], - @"path": self.path ?: [NSNull null], - @"url": url ?: [NSNull null], - @"draft": @(self.draft), - } error:nil]; + return [self copyWithTitle:self.title body:self.body url:url]; +} + +- (instancetype)copyWithTitle:(NSString *)title body:(NSString *)body url:(NSURL *)url { + return [[Post alloc] initWithDictionary:@{ + @"objectID" : self.objectID ?: [NSNull null], + @"slug" : self.slug ?: [NSNull null], + @"author" : self.author ?: [NSNull null], + @"title" : title ?: [NSNull null], + @"date" : self.date ?: [NSNull null], + @"body" : body ?: [NSNull null], + @"path" : self.path ?: [NSNull null], + @"url" : url ?: [NSNull null], + @"draft" : @(self.draft), + @"new" : @(self.new), + } error:nil]; } - (BOOL)isEqualToPost:(Post *)other { return [self.objectID isEqualToString:other.objectID] && [self.path isEqualToString:other.path] - && [self.title isEqualToString:other.title] - && [self.body isEqualToString:other.body] + && ((!self.title && !other.title) || [self.title isEqual:other.title]) + && ((!self.body && !other.body) || [self.body isEqual:other.body]) && self.draft == other.draft && ((!self.url && !other.url) || [self.url isEqual:other.url]); + // include "new" here too? } - (BOOL)isEqual:(id)object { @@ -154,7 +154,7 @@ } else { NSAssert(self.slug && self.date, @"slug and date are required"); - NSString *paddedMonth = [self paddedMonthForDate:self.date]; + NSString *paddedMonth = [self paddedMonthForDate:self.time]; _path = [NSString stringWithFormat:@"/posts/%ld/%@/%@", (long)self.time.mm_year, paddedMonth, self.slug]; } } diff --git a/Blog/PostsViewController.m b/Blog/PostsViewController.m index 0fe5e0a..9a249b0 100644 --- a/Blog/PostsViewController.m +++ b/Blog/PostsViewController.m @@ -18,7 +18,6 @@ @interface PostsViewController () @property (strong, nonatomic) NSMutableArray *posts; -@property (strong, nonatomic) EditorViewController *editorViewController; @property (strong, nonatomic) IBOutlet UIBarButtonItem *publishButton; @property (strong, nonatomic) IBOutlet UIBarButtonItem *addButton; @property (weak, nonatomic) UILabel *titleLabel; @@ -72,8 +71,8 @@ }]; } - self.blogStatusTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(updateBlogStatus) userInfo:nil repeats:YES]; - (void)setupBlogStatusTimer { + self.blogStatusTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(updateBlogStatus) userInfo:nil repeats:YES]; } - (void)teardownBlogStatusTimer { @@ -84,7 +83,6 @@ - (void)viewDidLoad { [super viewDidLoad]; UINavigationController *detailNavController = self.splitViewController.viewControllers.lastObject; - self.editorViewController = (EditorViewController *)detailNavController.topViewController; } - (void)updateStatusLabel:(NSString *)blogStatus { @@ -98,8 +96,7 @@ } - (void)updateBlogStatus { - [self updateStatusLabel:[NSString stringWithFormat:@"%@ as of %@", self.blogStatusText, - [self.blogStatusDate mm_relativeToNow]]]; + [self updateStatusLabel:[NSString stringWithFormat:@"%@ as of %@", self.blogStatusText, [self.blogStatusDate mm_relativeToNow]]]; } - (void)viewWillAppear:(BOOL)animated { @@ -161,10 +158,15 @@ } - (IBAction)insertNewObject:(id)sender { - Post *post = [[Post alloc] initWithDictionary:@{@"draft": @(YES)} error:nil]; + NSString *title = [UIPasteboard generalPasteboard].string; + NSURL *url = [UIPasteboard generalPasteboard].URL; + // TODO: image, anything else interesting + Post *post = [Post newDraftWithTitle:title body:nil url:url]; [self.posts insertObject:post atIndex:0]; NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + [self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop]; + [self performSegueWithIdentifier:@"showDetail" sender:sender]; } - (IBAction)publish:(id)sender { @@ -182,6 +184,16 @@ [controller setPost:post]; controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; controller.navigationItem.leftItemsSupplementBackButton = YES; + controller.postUpdatedBlock = ^(Post *post) { + NSUInteger row = [self.posts indexOfObjectPassingTest:^BOOL(Post *p, NSUInteger idx, BOOL *stop) { + return [p.objectID isEqualToString:post.objectID]; + }]; + if (row != NSNotFound) { + [self.posts replaceObjectAtIndex:row withObject:post]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; + [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } + }; } } @@ -200,7 +212,7 @@ Post *post = self.posts[indexPath.row]; // FIXME: unique title - NSString *title = post.title ?: @"Untitled"; + NSString *title = post.title.length ? post.title : @"Untitled"; NSString *date = post.draft ? @"Draft" : post.formattedDate; [cell configureWithTitle:title date:date]; return cell;