From 477a7939ed04131332e3512d6ee5907d2457c68d Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Thu, 21 May 2015 10:32:37 -0400 Subject: [PATCH] use selected text as quote when sharing from Safari --- Blog.xcodeproj/project.pbxproj | 18 +++++- samhuri.net/ExtensionItemProcessor.h | 14 +++++ samhuri.net/ExtensionItemProcessor.m | 54 +++++++++++++++++ samhuri.net/GrabSelectedText.js | 11 ++++ samhuri.net/Info.plist | 2 + samhuri.net/ShareViewController.m | 90 ++++++++++++++-------------- samhuri.net/SharedContent.h | 16 +++++ samhuri.net/SharedContent.m | 22 +++++++ 8 files changed, 180 insertions(+), 47 deletions(-) create mode 100644 samhuri.net/ExtensionItemProcessor.h create mode 100644 samhuri.net/ExtensionItemProcessor.m create mode 100644 samhuri.net/GrabSelectedText.js create mode 100644 samhuri.net/SharedContent.h create mode 100644 samhuri.net/SharedContent.m diff --git a/Blog.xcodeproj/project.pbxproj b/Blog.xcodeproj/project.pbxproj index f195602..d7afeb9 100644 --- a/Blog.xcodeproj/project.pbxproj +++ b/Blog.xcodeproj/project.pbxproj @@ -57,6 +57,9 @@ 7BE3A0351AE461E700E45CCB /* PreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BE3A0341AE461E700E45CCB /* PreviewViewController.m */; }; 7BF029331A27117200E42EDE /* ModelStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BF029321A27117200E42EDE /* ModelStore.m */; }; 7BF029381A280CB200E42EDE /* BlogController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BF029371A280CB200E42EDE /* BlogController.m */; }; + 7BF960261B0D802900A19A2B /* GrabSelectedText.js in Resources */ = {isa = PBXBuildFile; fileRef = 7BF960251B0D802900A19A2B /* GrabSelectedText.js */; }; + 7BF960271B0E1B1000A19A2B /* SharedContent.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFCC904E1195A3DA84D276 /* SharedContent.m */; }; + 7BF960281B0E1B1500A19A2B /* ExtensionItemProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFC0549FB7723D8893A91C /* ExtensionItemProcessor.m */; }; 848D1BC47C9F03BB91A24EB4 /* libPods-samhuri.net.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D351844AAA829A65456E283E /* libPods-samhuri.net.a */; }; B8B8958B2AA40812EFE04FEF /* libPods-BlogTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C36BC848680D4F831E4DE23 /* libPods-BlogTests.a */; }; /* End PBXBuildFile section */ @@ -94,8 +97,11 @@ /* Begin PBXFileReference section */ 1613DC56A86AFA7E50460A37 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BCFC0549FB7723D8893A91C /* ExtensionItemProcessor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExtensionItemProcessor.m; sourceTree = ""; }; 1BCFC23988387A5CAE551C90 /* UIColor+Hex.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+Hex.m"; sourceTree = ""; }; + 1BCFC27DA8A32D0484C273A0 /* ExtensionItemProcessor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExtensionItemProcessor.h; sourceTree = ""; }; 1BCFC32EC1E5AE196D194D21 /* CommonUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommonUI.m; sourceTree = ""; }; + 1BCFC384864D31C679683404 /* SharedContent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SharedContent.h; sourceTree = ""; }; 1BCFC3B62AA92DB07923F7C1 /* PostCollection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostCollection.m; sourceTree = ""; }; 1BCFC40D1BFCFBF04BC79A35 /* MuseoSans-900.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "MuseoSans-900.otf"; sourceTree = ""; }; 1BCFC462F5540C67B9D97299 /* MuseoSans-700.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "MuseoSans-700.otf"; sourceTree = ""; }; @@ -106,6 +112,7 @@ 1BCFCB67571197A762B88624 /* MuseoSans-100-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "MuseoSans-100-Italic.otf"; sourceTree = ""; }; 1BCFCB89F8C8583AE061757E /* MuseoSans-500.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "MuseoSans-500.otf"; sourceTree = ""; }; 1BCFCC3154DB1D3B3C025211 /* MuseoSans-100.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "MuseoSans-100.otf"; sourceTree = ""; }; + 1BCFCC904E1195A3DA84D276 /* SharedContent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SharedContent.m; sourceTree = ""; }; 1BCFCCF30594E0E2DCC32116 /* PostCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostCell.h; sourceTree = ""; }; 1BCFCD0E9504E1E8AB8C0275 /* MuseoSans-300-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file.otf; path = "MuseoSans-300-Italic.otf"; sourceTree = ""; }; 1BCFCDD73D4AE8F16A9C9E3D /* ChangeTitleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChangeTitleViewController.h; sourceTree = ""; }; @@ -162,6 +169,7 @@ 7BF029321A27117200E42EDE /* ModelStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModelStore.m; sourceTree = ""; }; 7BF029361A280CB200E42EDE /* BlogController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlogController.h; sourceTree = ""; }; 7BF029371A280CB200E42EDE /* BlogController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlogController.m; sourceTree = ""; }; + 7BF960251B0D802900A19A2B /* GrabSelectedText.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = GrabSelectedText.js; sourceTree = ""; }; 9C36BC848680D4F831E4DE23 /* libPods-BlogTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BlogTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A12E4260DF3BBC175C358446 /* Pods-samhuri.net.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-samhuri.net.debug.xcconfig"; path = "Pods/Target Support Files/Pods-samhuri.net/Pods-samhuri.net.debug.xcconfig"; sourceTree = ""; }; A2EB178BEF4356711B2710AE /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; @@ -236,10 +244,15 @@ 7B08B3E81AF9CAD300435579 /* samhuri.net */ = { isa = PBXGroup; children = ( + 7B08B3EE1AF9CAD300435579 /* MainInterface.storyboard */, 7B08B3EB1AF9CAD300435579 /* ShareViewController.h */, 7B08B3EC1AF9CAD300435579 /* ShareViewController.m */, - 7B08B3EE1AF9CAD300435579 /* MainInterface.storyboard */, + 7BF960251B0D802900A19A2B /* GrabSelectedText.js */, 7B08B3E91AF9CAD300435579 /* Supporting Files */, + 1BCFCC904E1195A3DA84D276 /* SharedContent.m */, + 1BCFC384864D31C679683404 /* SharedContent.h */, + 1BCFC0549FB7723D8893A91C /* ExtensionItemProcessor.m */, + 1BCFC27DA8A32D0484C273A0 /* ExtensionItemProcessor.h */, ); path = samhuri.net; sourceTree = ""; @@ -500,6 +513,7 @@ files = ( 7B08B3EF1AF9CAD300435579 /* MainInterface.storyboard in Resources */, 7B010BAF1AFB3EA900351D18 /* auth.json in Resources */, + 7BF960261B0D802900A19A2B /* GrabSelectedText.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -639,9 +653,11 @@ 7B08B3F71AF9CBBD00435579 /* NSDate+marshmallows.m in Sources */, 7B08B3FB1AF9CBC500435579 /* BlogService.m in Sources */, 7B08B3FD1AF9CBC500435579 /* ModelStore.m in Sources */, + 7BF960281B0E1B1500A19A2B /* ExtensionItemProcessor.m in Sources */, 7B08B4021AF9D3EA00435579 /* SamhuriNet.m in Sources */, 7B08B3ED1AF9CAD300435579 /* ShareViewController.m in Sources */, 7B08B3F91AF9CBC500435579 /* BlogStatus.m in Sources */, + 7BF960271B0E1B1000A19A2B /* SharedContent.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/samhuri.net/ExtensionItemProcessor.h b/samhuri.net/ExtensionItemProcessor.h new file mode 100644 index 0000000..c1100cd --- /dev/null +++ b/samhuri.net/ExtensionItemProcessor.h @@ -0,0 +1,14 @@ +// +// Created by Sami Samhuri on 15-05-20. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// +@import Foundation; + +@class PMKPromise; + +@interface ExtensionItemProcessor : NSObject + +- (PMKPromise *)sharedContentForPListItem:(NSExtensionItem *)item; +- (PMKPromise *)sharedContentForURLItem:(NSExtensionItem *)item text:(NSString *)text; + +@end diff --git a/samhuri.net/ExtensionItemProcessor.m b/samhuri.net/ExtensionItemProcessor.m new file mode 100644 index 0000000..f4ee90d --- /dev/null +++ b/samhuri.net/ExtensionItemProcessor.m @@ -0,0 +1,54 @@ +// +// Created by Sami Samhuri on 15-05-20. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// +#import +#import +#import "ExtensionItemProcessor.h" +#import "SharedContent.h" +#import "NSString+marshmallows.h" + +@implementation ExtensionItemProcessor + +- (PMKPromise *)sharedContentForPListItem:(NSExtensionItem *)item { + return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) { + NSItemProvider *provider = [self providerForIdentifier:(NSString *)kUTTypePropertyList fromExtensionItem:item]; + if (!provider) { + reject([NSError errorWithDomain:@"ShareViewControllerDomain" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Cannot find PList provider"}]); + return; + } + [provider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList options:nil completionHandler:^(NSDictionary *stuff, NSError *error) { + NSDictionary *results = stuff[NSExtensionJavaScriptPreprocessingResultsKey]; + NSURL *url = [NSURL URLWithString:results[@"url"]]; + NSString *quotedText = [results[@"selectedText"] mm_stringByTrimmingWhitespace]; + NSString *quote = quotedText.length ? @"> " : @""; + NSString *text = [NSString stringWithFormat:@"%@\n\n%@%@", results[@"title"], quote, quotedText]; + fulfill([SharedContent contentWithURL:url text:text]); + }]; + }]; +} + +- (PMKPromise *)sharedContentForURLItem:(NSExtensionItem *)item text:(NSString *)text { + return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) { + NSItemProvider *provider = [self providerForIdentifier:(NSString *)kUTTypeURL fromExtensionItem:item]; + if (!provider) { + reject([NSError errorWithDomain:@"ShareViewControllerDomain" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Cannot find URL provider"}]); + return; + } + [provider loadItemForTypeIdentifier:(NSString *)kUTTypeURL options:nil completionHandler:^(NSURL *url, NSError *error) { + // TODO: fetch title? + fulfill([SharedContent contentWithURL:url text:text]); + }]; + }]; +} + +- (NSItemProvider *)providerForIdentifier:(NSString *)identifier fromExtensionItem:(NSExtensionItem *)item { + for (NSItemProvider *provider in item.attachments) { + if ([provider hasItemConformingToTypeIdentifier:identifier]) { + return provider; + } + } + return nil; +} + +@end diff --git a/samhuri.net/GrabSelectedText.js b/samhuri.net/GrabSelectedText.js new file mode 100644 index 0000000..76fc1c6 --- /dev/null +++ b/samhuri.net/GrabSelectedText.js @@ -0,0 +1,11 @@ +var SelectedTextPreprocessor = function() {}; +SelectedTextPreprocessor.prototype = { + run: function(args) { + args.completionFunction({ + "url": document.URL, + "title": document.title, + "selectedText": window.getSelection().toString() + }); + } +}; +window.ExtensionPreprocessingJS = new SelectedTextPreprocessor(); diff --git a/samhuri.net/Info.plist b/samhuri.net/Info.plist index 0f5e831..1f712f3 100644 --- a/samhuri.net/Info.plist +++ b/samhuri.net/Info.plist @@ -26,6 +26,8 @@ NSExtensionAttributes + NSExtensionJavaScriptPreprocessingFile + GrabSelectedText NSExtensionActivationRule NSExtensionActivationSupportsText diff --git a/samhuri.net/ShareViewController.m b/samhuri.net/ShareViewController.m index 499cfe8..0276cef 100644 --- a/samhuri.net/ShareViewController.m +++ b/samhuri.net/ShareViewController.m @@ -6,24 +6,48 @@ // Copyright (c) 2015 Guru Logic Inc. All rights reserved. // #import +#import #import "ShareViewController.h" #import "SamhuriNet.h" #import "BlogController.h" #import "Post.h" +#import "SharedContent.h" +#import "ExtensionItemProcessor.h" @interface ShareViewController () - -@property (nonatomic, readonly, strong) NSItemProvider *URLProvider; -@property (nonatomic, assign) BOOL checkedForURLProvider; - +@property(nonatomic, strong) SharedContent *content; @end @implementation ShareViewController -@synthesize URLProvider = _URLProvider; +- (void)viewDidLoad { + [super viewDidLoad]; + [self findSharedContent].then(^(SharedContent *content) { + self.textView.text = content.text; + self.content = content; + }).catch(^(NSError *error) { + [self displayError:error]; + [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; + }); +} + +- (PMKPromise *)findSharedContent { + for (NSExtensionItem *item in self.extensionContext.inputItems) { + for (NSItemProvider *provider in item.attachments) { + if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePropertyList]) { + return [[ExtensionItemProcessor new] sharedContentForPListItem:item]; + } + if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) { + return [[ExtensionItemProcessor new] sharedContentForURLItem:item text:self.contentText]; + } + } + } + NSDictionary *info = @{NSLocalizedDescriptionKey: @"Cannot find PList or URL extension item to share."}; + return [PMKPromise promiseWithValue:[NSError errorWithDomain:@"SharedViewControllerDomain" code:1 userInfo:info]]; +} - (BOOL)isContentValid { - return self.URLProvider != nil; + return self.textView.text.length > 0; } - (UIView *)loadPreviewView { @@ -38,48 +62,14 @@ NSRange titleEndRange = [self.contentText rangeOfString:@"\n\n"]; NSString *title = titleEndRange.location == NSNotFound ? self.contentText : [self.contentText substringToIndex:titleEndRange.location]; NSString *body = titleEndRange.location == NSNotFound ? @"" : [self.contentText substringFromIndex:titleEndRange.location + titleEndRange.length]; - NSLog(@"title = %@", title); - NSLog(@"body = %@", body); - NSItemProvider *urlProvider = [self firstURLProvider]; BOOL reallyPost = YES; - [urlProvider loadItemForTypeIdentifier:@"public.url" options:nil completionHandler:^(NSURL *url, NSError *error) { - // TODO: image - NSLog(@"url = %@", url); - if (reallyPost) { - Post *post = [Post newDraftWithTitle:title body:body url:url]; - [blogController requestCreateDraft:post publishImmediatelyToEnvironment:@"production" waitForCompilation:NO].catch(^(NSError *error) { - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { - [self dismissViewControllerAnimated:YES completion:nil]; - }]]; - [self presentViewController:alert animated:YES completion:nil]; - }); - } - [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; - }]; -} - -- (NSItemProvider *)firstURLProvider { - NSExtensionItem *item = self.extensionContext.inputItems.firstObject; - NSLog(@"item = %@", item); - for (NSItemProvider *provider in item.attachments) { - NSLog(@"provider = %@", provider); - if ([provider hasItemConformingToTypeIdentifier:@"public.url"]) { - return provider; - } + if (reallyPost) { + Post *post = [Post newDraftWithTitle:title body:body url:self.content.url]; + [blogController requestCreateDraft:post publishImmediatelyToEnvironment:@"production" waitForCompilation:NO].catch(^(NSError *error) { + [self displayError:error]; + }); } - return nil; -} - -- (NSItemProvider *)URLProvider { - if (!self.checkedForURLProvider) { - _URLProvider = [self firstURLProvider]; - if (!_URLProvider) { - NSLog(@"ERROR: No URL provider found"); - } - self.checkedForURLProvider = YES; - } - return _URLProvider; + [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil]; } - (NSArray *)configurationItems { @@ -87,4 +77,12 @@ return @[]; } +- (void)displayError:(NSError *)error { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + [self dismissViewControllerAnimated:YES completion:nil]; + }]]; + [self presentViewController:alert animated:YES completion:nil]; +} + @end diff --git a/samhuri.net/SharedContent.h b/samhuri.net/SharedContent.h new file mode 100644 index 0000000..ada0c80 --- /dev/null +++ b/samhuri.net/SharedContent.h @@ -0,0 +1,16 @@ +// +// Created by Sami Samhuri on 15-05-20. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// +@import Foundation; + +@interface SharedContent : NSObject + +@property(nonatomic, readonly, copy) NSURL *url; +@property(nonatomic, readonly, copy) NSString *text; + ++ (instancetype)contentWithURL:(NSURL *)url text:(NSString *)text; +- (instancetype)initWithURL:(NSURL *)url contentText:(NSString *)text; + + +@end diff --git a/samhuri.net/SharedContent.m b/samhuri.net/SharedContent.m new file mode 100644 index 0000000..87c1e08 --- /dev/null +++ b/samhuri.net/SharedContent.m @@ -0,0 +1,22 @@ +// +// Created by Sami Samhuri on 15-05-20. +// Copyright (c) 2015 Guru Logic Inc. All rights reserved. +// +#import "SharedContent.h" + +@implementation SharedContent + ++ (instancetype)contentWithURL:(NSURL *)url text:(NSString *)text { + return [[self alloc] initWithURL:url contentText:text]; +} + +- (instancetype)initWithURL:(NSURL *)url contentText:(NSString *)text { + self = [super init]; + if (self) { + _url = [url copy]; + _text = [text copy]; + } + return self; +} + +@end