From af36c207fb2e327b38ad1925900fe51ffb112230 Mon Sep 17 00:00:00 2001 From: pstasiak Date: Tue, 24 Mar 2015 22:16:34 -0500 Subject: [PATCH] Issue #693 - Copying images, location to pasteboard. --- JSQMessages.xcodeproj/project.pbxproj | 22 +++++++++++-------- .../ModelTests/JSQLocationMediaItemTests.m | 11 ++++++++++ .../ModelTests/JSQPhotoMediaItemTests.m | 10 +++++++++ JSQMessagesViewController.podspec | 3 +-- .../Controllers/JSQMessagesViewController.m | 21 ++++++++++++++++-- .../Model/JSQLocationMediaItem.m | 15 +++++++++++++ .../Model/JSQMessageData.h | 2 -- .../Model/JSQMessageMediaData.h | 20 +++++++++++++++++ .../Model/JSQPhotoMediaItem.m | 11 ++++++++++ 9 files changed, 100 insertions(+), 15 deletions(-) diff --git a/JSQMessages.xcodeproj/project.pbxproj b/JSQMessages.xcodeproj/project.pbxproj index e537894af..d6485e0a1 100644 --- a/JSQMessages.xcodeproj/project.pbxproj +++ b/JSQMessages.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1F0EFE0F1AC23D7E003FF3DB /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F0EFE0E1AC23D7E003FF3DB /* MobileCoreServices.framework */; }; 88078A9D19D8FEB5005B4595 /* JSQMessagesMediaPlaceholderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 88078A9C19D8FEB5005B4595 /* JSQMessagesMediaPlaceholderView.m */; }; 88324C3419F6301C00BC732D /* JSQMessagesMediaViewBubbleImageMaskerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88324C3319F6301C00BC732D /* JSQMessagesMediaViewBubbleImageMaskerTests.m */; }; 883C11781A09FB100092A16D /* JSQMessagesCellTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 883C11771A09FB100092A16D /* JSQMessagesCellTextView.m */; }; @@ -120,9 +121,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0B4D05069814EB50FB0F4229 /* Pods-JSQMessagesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JSQMessagesTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-JSQMessagesTests/Pods-JSQMessagesTests.release.xcconfig"; sourceTree = ""; }; - 1D4D3B82D90888BCEAF890D3 /* Pods-JSQMessagesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JSQMessagesTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-JSQMessagesTests/Pods-JSQMessagesTests.debug.xcconfig"; sourceTree = ""; }; - 3BA6237809BE0D008CFE3697 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + 1F0EFE0E1AC23D7E003FF3DB /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 242A2BCAF25E39CC1DAF654E /* Pods-JSQMessagesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JSQMessagesTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-JSQMessagesTests/Pods-JSQMessagesTests.release.xcconfig"; sourceTree = ""; }; + 4D257782A244F7688A2F6AA8 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; 88078A9B19D8FEB5005B4595 /* JSQMessagesMediaPlaceholderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSQMessagesMediaPlaceholderView.h; sourceTree = ""; }; 88078A9C19D8FEB5005B4595 /* JSQMessagesMediaPlaceholderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQMessagesMediaPlaceholderView.m; sourceTree = ""; }; 88324C3319F6301C00BC732D /* JSQMessagesMediaViewBubbleImageMaskerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSQMessagesMediaViewBubbleImageMaskerTests.m; sourceTree = ""; }; @@ -271,6 +272,7 @@ AD6E75315517DE46FE495B65 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; C3F882AA48978C11F64DC2DF /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; E23238C8D8DF79244DEE1787 /* libPods-JSQMessagesTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JSQMessagesTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F68D899A0AE7E8180C89579D /* Pods-JSQMessagesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JSQMessagesTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-JSQMessagesTests/Pods-JSQMessagesTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -278,6 +280,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1F0EFE0F1AC23D7E003FF3DB /* MobileCoreServices.framework in Frameworks */, 88445B4419E1B5110014F889 /* MapKit.framework in Frameworks */, 88445B4219E1B50B0014F889 /* CoreLocation.framework in Frameworks */, 88445B3719E0AE5C0014F889 /* QuartzCore.framework in Frameworks */, @@ -310,6 +313,7 @@ 636A8663AEEE5C37B65C515D /* Frameworks */ = { isa = PBXGroup; children = ( + 1F0EFE0E1AC23D7E003FF3DB /* MobileCoreServices.framework */, 88445B3419E0AE4A0014F889 /* CoreGraphics.framework */, 88445B4119E1B50B0014F889 /* CoreLocation.framework */, 88445B3219E0AE450014F889 /* Foundation.framework */, @@ -326,10 +330,10 @@ 842892590A65F355D8619D29 /* Pods */ = { isa = PBXGroup; children = ( - 3BA6237809BE0D008CFE3697 /* Pods.debug.xcconfig */, AD6E75315517DE46FE495B65 /* Pods.release.xcconfig */, - 1D4D3B82D90888BCEAF890D3 /* Pods-JSQMessagesTests.debug.xcconfig */, - 0B4D05069814EB50FB0F4229 /* Pods-JSQMessagesTests.release.xcconfig */, + 4D257782A244F7688A2F6AA8 /* Pods.debug.xcconfig */, + F68D899A0AE7E8180C89579D /* Pods-JSQMessagesTests.debug.xcconfig */, + 242A2BCAF25E39CC1DAF654E /* Pods-JSQMessagesTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -1030,7 +1034,7 @@ }; 88A25F2619D8DEC500924534 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3BA6237809BE0D008CFE3697 /* Pods.debug.xcconfig */; + baseConfigurationReference = 4D257782A244F7688A2F6AA8 /* Pods.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; @@ -1068,7 +1072,7 @@ }; 88A25F2919D8DEC500924534 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1D4D3B82D90888BCEAF890D3 /* Pods-JSQMessagesTests.debug.xcconfig */; + baseConfigurationReference = F68D899A0AE7E8180C89579D /* Pods-JSQMessagesTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; @@ -1087,7 +1091,7 @@ }; 88A25F2A19D8DEC500924534 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0B4D05069814EB50FB0F4229 /* Pods-JSQMessagesTests.release.xcconfig */; + baseConfigurationReference = 242A2BCAF25E39CC1DAF654E /* Pods-JSQMessagesTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; diff --git a/JSQMessagesTests/ModelTests/JSQLocationMediaItemTests.m b/JSQMessagesTests/ModelTests/JSQLocationMediaItemTests.m index 4829fb06a..f5cff7430 100644 --- a/JSQMessagesTests/ModelTests/JSQLocationMediaItemTests.m +++ b/JSQMessagesTests/ModelTests/JSQLocationMediaItemTests.m @@ -12,6 +12,7 @@ #import "JSQLocationMediaItem.h" +#import @interface JSQLocationMediaItemTests : XCTestCase @@ -61,4 +62,14 @@ - (void)testMediaDataProtocol XCTAssertNotNil([item mediaView], @"Media view should NOT be nil once item has media data"); } +- (void)testCopyableItemInMediaProtocol { + JSQLocationMediaItem *item = [[JSQLocationMediaItem alloc] initWithLocation:self.location]; + XCTAssertNotNil(item); + + XCTAssertEqualObjects((NSString *)kUTTypeURL, [item mediaDataType]); + + NSURL *locationURL = [[NSURL alloc] initWithString:@"http://maps.google.com/maps?z=12&t=m&q=loc:37.795313+-122.393757"]; + XCTAssertEqualObjects(locationURL, [item mediaData]); +} + @end diff --git a/JSQMessagesTests/ModelTests/JSQPhotoMediaItemTests.m b/JSQMessagesTests/ModelTests/JSQPhotoMediaItemTests.m index 3e568b7b5..f2d5a23c3 100644 --- a/JSQMessagesTests/ModelTests/JSQPhotoMediaItemTests.m +++ b/JSQMessagesTests/ModelTests/JSQPhotoMediaItemTests.m @@ -12,6 +12,7 @@ #import "JSQPhotoMediaItem.h" +#import @interface JSQPhotoMediaItemTests : XCTestCase @@ -73,4 +74,13 @@ - (void)testMediaDataProtocol XCTAssertNotNil([item mediaView], @"Media view should NOT be nil once item has media data"); } +- (void)testCopyableItemInMediaProtocol { + JSQPhotoMediaItem *item = [[JSQPhotoMediaItem alloc] initWithImage:[UIImage imageNamed:@"demo_avatar_jobs"]]; + XCTAssertNotNil(item); + XCTAssertEqual([item mediaDataType], (NSString *)kUTTypeJPEG); + + UIImage *itemImage = [[UIImage alloc] initWithData:[item mediaData]]; + XCTAssertNotNil(itemImage); +} + @end diff --git a/JSQMessagesViewController.podspec b/JSQMessagesViewController.podspec index d03ca6d92..3210ef64c 100644 --- a/JSQMessagesViewController.podspec +++ b/JSQMessagesViewController.podspec @@ -18,8 +18,7 @@ Pod::Spec.new do |s| s.source_files = 'JSQMessagesViewController/**/*.{h,m}' s.resources = ['JSQMessagesViewController/Assets/JSQMessagesAssets.bundle', 'JSQMessagesViewController/**/*.{xib}'] - - s.frameworks = 'QuartzCore', 'CoreGraphics', 'CoreLocation', 'MapKit', 'UIKit', 'Foundation' + s.frameworks = 'QuartzCore', 'CoreGraphics', 'CoreLocation', 'MapKit', 'UIKit', 'Foundation', 'MobileCoreServices' s.requires_arc = true s.dependency 'JSQSystemSoundPlayer', '~> 2.0.1' diff --git a/JSQMessagesViewController/Controllers/JSQMessagesViewController.m b/JSQMessagesViewController/Controllers/JSQMessagesViewController.m index 99527c6ad..05db5fac8 100644 --- a/JSQMessagesViewController/Controllers/JSQMessagesViewController.m +++ b/JSQMessagesViewController/Controllers/JSQMessagesViewController.m @@ -41,6 +41,8 @@ #import "UIDevice+JSQMessages.h" #import "NSBundle+JSQMessages.h" +#import + static void * kJSQMessagesKeyValueObservingContext = &kJSQMessagesKeyValueObservingContext; @@ -572,6 +574,11 @@ - (BOOL)collectionView:(JSQMessagesCollectionView *)collectionView shouldShowMen // disable menu for media messages id messageItem = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath]; if ([messageItem isMediaMessage]) { + + if ([[messageItem media] respondsToSelector:@selector(mediaDataType)]) { + return YES; + } + return NO; } @@ -599,8 +606,18 @@ - (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL) - (void)collectionView:(JSQMessagesCollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { if (action == @selector(copy:)) { - id messageData = [collectionView.dataSource collectionView:collectionView messageDataForItemAtIndexPath:indexPath]; - [[UIPasteboard generalPasteboard] setString:[messageData text]]; + + id messageData = [self collectionView:collectionView messageDataForItemAtIndexPath:indexPath]; + + if ([messageData isMediaMessage]) { + id mediaData = [messageData media]; + if ([messageData respondsToSelector:@selector(mediaDataType)]) { + [[UIPasteboard generalPasteboard] setValue:[mediaData mediaData] + forPasteboardType:[mediaData mediaDataType]]; + } + } else { + [[UIPasteboard generalPasteboard] setString:[messageData text]]; + } } else if (action == @selector(delete:)) { [collectionView.dataSource collectionView:collectionView didDeleteMessageAtIndexPath:indexPath]; diff --git a/JSQMessagesViewController/Model/JSQLocationMediaItem.m b/JSQMessagesViewController/Model/JSQLocationMediaItem.m index d1635e30c..12fc9a3b6 100644 --- a/JSQMessagesViewController/Model/JSQLocationMediaItem.m +++ b/JSQMessagesViewController/Model/JSQLocationMediaItem.m @@ -21,6 +21,8 @@ #import "JSQMessagesMediaPlaceholderView.h" #import "JSQMessagesMediaViewBubbleImageMasker.h" +#import + @interface JSQLocationMediaItem () @@ -158,6 +160,19 @@ - (NSUInteger)mediaHash return self.hash; } +- (NSString *)mediaDataType +{ + return (NSString *)kUTTypeURL; +} + +- (id)mediaData +{ + NSString *locationAsGoogleMapsString = [NSString stringWithFormat:@"http://maps.google.com/maps?z=12&t=m&q=loc:%f+%f", self.coordinate.latitude, self.coordinate.longitude ]; + NSURL *locationURL = [[NSURL alloc] initWithString:locationAsGoogleMapsString]; + return locationURL; +} + + #pragma mark - NSObject - (BOOL)isEqual:(id)object diff --git a/JSQMessagesViewController/Model/JSQMessageData.h b/JSQMessagesViewController/Model/JSQMessageData.h index 41cd912ef..7be25e59a 100644 --- a/JSQMessagesViewController/Model/JSQMessageData.h +++ b/JSQMessagesViewController/Model/JSQMessageData.h @@ -81,8 +81,6 @@ */ - (NSUInteger)messageHash; -@optional - /** * @return The body text of the message. * diff --git a/JSQMessagesViewController/Model/JSQMessageMediaData.h b/JSQMessagesViewController/Model/JSQMessageMediaData.h index 4c8ab63af..3e584ac9a 100644 --- a/JSQMessagesViewController/Model/JSQMessageMediaData.h +++ b/JSQMessagesViewController/Model/JSQMessageMediaData.h @@ -78,4 +78,24 @@ */ - (NSUInteger)mediaHash; +@optional + +/** + * @return String which identifies type of the data returned by `mediaData` method. + * + * @discussion If implemented, you must not return `nil` from this, as well as `copyableData`, method. + * This type is frequently, but not necessarily, a UTI (Uniform Type Identifier). It identifies a + * representation of the data on the pasteboard. Apps can define their own types for custom data, + * however, in this case, only those apps that know of the type could understand the data written to the pasteboard. + */ +- (NSString *)mediaDataType; + +/** + * @return Data object of class corresponding to type returned by `mediaDataType`. + * + * @discussion You should return an object that is of a class type appropriate to the representation type, + * which typically is a UTI. + */ +- (id)mediaData; + @end diff --git a/JSQMessagesViewController/Model/JSQPhotoMediaItem.m b/JSQMessagesViewController/Model/JSQPhotoMediaItem.m index 1752fd1eb..e7cbf4bb0 100644 --- a/JSQMessagesViewController/Model/JSQPhotoMediaItem.m +++ b/JSQMessagesViewController/Model/JSQPhotoMediaItem.m @@ -21,6 +21,7 @@ #import "JSQMessagesMediaPlaceholderView.h" #import "JSQMessagesMediaViewBubbleImageMasker.h" +#import @interface JSQPhotoMediaItem () @@ -89,6 +90,16 @@ - (NSUInteger)mediaHash return self.hash; } +- (NSString *)mediaDataType +{ + return (NSString *)kUTTypeJPEG; +} + +- (id)mediaData +{ + return UIImageJPEGRepresentation(self.image, 1); +} + #pragma mark - NSObject - (NSUInteger)hash