diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ce0a59d07..182b5b841 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 340757C21E5602D6001F15DD /* AttachmentSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = 340757C11E5602D6001F15DD /* AttachmentSharing.m */; }; 341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */; }; 34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34535D811E256BE9008A4747 /* UIView+OWS.m */; }; 348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */; }; @@ -599,6 +600,8 @@ /* Begin PBXFileReference section */ 1B5E7D6C9007F5E5761D79DD /* libPods-SignalTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SignalTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 340757C01E5602D6001F15DD /* AttachmentSharing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentSharing.h; sourceTree = ""; }; + 340757C11E5602D6001F15DD /* AttachmentSharing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentSharing.m; sourceTree = ""; }; 341BB7471DB727EE001E2975 /* JSQMediaItem+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMediaItem+OWS.h"; sourceTree = ""; }; 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMediaItem+OWS.m"; sourceTree = ""; }; 34535D801E256BE9008A4747 /* UIView+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+OWS.h"; sourceTree = ""; }; @@ -2013,18 +2016,20 @@ 76EB04FE18170B33006006FC /* View Controllers */ = { isa = PBXGroup; children = ( - FC3196311A08141D0094C78E /* Settings */, - FC3196321A08142D0094C78E /* Signals */, - FCFD25791A1543D500F4C644 /* Signup */, - B6BADBE51B88D1AC0086A80D /* LockInteractionController.h */, - B6BADBE61B88D1AC0086A80D /* LockInteractionController.m */, + 340757C01E5602D6001F15DD /* AttachmentSharing.h */, + 340757C11E5602D6001F15DD /* AttachmentSharing.m */, + 451764261DE939F300EDB8B9 /* ContactsPicker.swift */, + E94066141DFC5B7B00B15392 /* ContactsPicker.xib */, 76EB050B18170B33006006FC /* InCallViewController.h */, 76EB050C18170B33006006FC /* InCallViewController.m */, + 45514DE11DDFA183003EFF90 /* InviteFlow.swift */, + B6BADBE51B88D1AC0086A80D /* LockInteractionController.h */, + B6BADBE61B88D1AC0086A80D /* LockInteractionController.m */, 458E382F1D6682450094BD24 /* OWSQRCodeScanningViewController.h */, 458E38301D6682450094BD24 /* OWSQRCodeScanningViewController.m */, - 451764261DE939F300EDB8B9 /* ContactsPicker.swift */, - 45514DE11DDFA183003EFF90 /* InviteFlow.swift */, - E94066141DFC5B7B00B15392 /* ContactsPicker.xib */, + FC3196311A08141D0094C78E /* Settings */, + FC3196321A08142D0094C78E /* Signals */, + FCFD25791A1543D500F4C644 /* Signup */, ); name = "View Controllers"; path = "view controllers"; @@ -3060,6 +3065,7 @@ 45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */, 76EB061218170B33006006FC /* LoggingUtil.m in Sources */, 76EB060E18170B33006006FC /* DecayingSampleEstimator.m in Sources */, + 340757C21E5602D6001F15DD /* AttachmentSharing.m in Sources */, 76EB05BA18170B33006006FC /* CommitPacket.m in Sources */, 76EB060218170B33006006FC /* InitiatorSessionDescriptor.m in Sources */, 76EB05FC18170B33006006FC /* CallConnectUtil_Server.m in Sources */, diff --git a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m index 248f9d8d3..3704fd017 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m @@ -1,11 +1,8 @@ -// TSMessageAdapter.m // -// Signal -// -// Created by Frederic Jacobs on 24/11/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // +#import "AttachmentSharing.h" #import "OWSCall.h" #import "TSAttachmentPointer.h" #import "TSAttachmentStream.h" @@ -222,14 +219,15 @@ - (BOOL)canPerformEditingAction:(SEL)action { - // Deletes are always handled by TSMessageAdapter if (action == @selector(delete:)) { return YES; } // Delegate other actions for media items - if (self.isMediaMessage) { + if ([self attachmentStream] && action == NSSelectorFromString(@"share:")) { + return YES; + } else if (self.isMediaMessage) { return [self.mediaItem canPerformEditingAction:action]; } else if (self.messageType == TSInfoMessageAdapter || self.messageType == TSErrorMessageAdapter) { return NO; @@ -249,8 +247,16 @@ DDLogDebug(@"Deleting interaction with uniqueId: %@", self.interaction.uniqueId); [self.interaction remove]; return; + } else if (action == NSSelectorFromString(@"share:")) { + TSAttachmentStream *stream = [self attachmentStream]; + OWSAssert(stream); + if (stream) { + [AttachmentSharing showShareUIForAttachment:stream]; + } + return; } + // Delegate other actions for media items if (self.isMediaMessage) { [self.mediaItem performEditingAction:action]; @@ -271,6 +277,29 @@ [self.mediaItem class]); } +- (TSAttachmentStream *)attachmentStream +{ + if (![self.interaction isKindOfClass:[TSMessage class]]) { + return nil; + } + + TSMessage *message = (TSMessage *)self.interaction; + + if (![message hasAttachments]) { + return nil; + } + OWSAssert(message.attachmentIds.count <= 1); + NSString *attachmentID = message.attachmentIds[0]; + TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentID]; + + if (![attachment isKindOfClass:[TSAttachmentStream class]]) { + return nil; + } + + TSAttachmentStream *stream = (TSAttachmentStream *)attachment; + return stream; +} + - (BOOL)isMediaMessage { return _mediaItem ? YES : NO; } diff --git a/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m index 15d269dc8..d093ee810 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m @@ -1,5 +1,6 @@ -// Created by Frederic Jacobs on 17/12/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "TSPhotoAdapter.h" #import "TSAttachmentStream.h" diff --git a/Signal/src/view controllers/AttachmentSharing.h b/Signal/src/view controllers/AttachmentSharing.h new file mode 100644 index 000000000..a55e01ee7 --- /dev/null +++ b/Signal/src/view controllers/AttachmentSharing.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +@class TSAttachmentStream; + +@interface AttachmentSharing : NSObject + ++ (void)showShareUIForAttachment:(TSAttachmentStream *)stream; + +@end diff --git a/Signal/src/view controllers/AttachmentSharing.m b/Signal/src/view controllers/AttachmentSharing.m new file mode 100644 index 000000000..40d569d6d --- /dev/null +++ b/Signal/src/view controllers/AttachmentSharing.m @@ -0,0 +1,76 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "AttachmentSharing.h" +#import "TSAttachmentStream.h" + +@implementation AttachmentSharing + ++ (void)showShareUIForAttachment:(TSAttachmentStream *)stream { + OWSAssert(stream); + + NSString *filePath = stream.filePath; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *error; + NSData *data = [NSData dataWithContentsOfFile:filePath options:0 error:&error]; + if (!data || error) { + DDLogError(@"%@ %s could not read data from attachment: %@.", + self.tag, + __PRETTY_FUNCTION__, + error); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [AttachmentSharing showShareUIForData:data]; + }); + }); +} + ++ (void)showShareUIForData:(NSData *)data { + AssertIsOnMainThread(); + OWSAssert(data); + + UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[data, ] + applicationActivities:@[ + ]]; + + [activityViewController setCompletionWithItemsHandler:^(UIActivityType __nullable activityType, + BOOL completed, + NSArray * __nullable returnedItems, + NSError * __nullable activityError) { + + if (activityError) { + DDLogInfo(@"%@ Failed to share with activityError: %@", self.tag, activityError); + } else if (completed) { + DDLogInfo(@"%@ Did share with activityType: %@", self.tag, activityType); + } + }]; + + // Find the frontmost presented UIViewController from which to present the + // share view. + UIWindow *window = [UIApplication sharedApplication].keyWindow; + UIViewController *fromViewController = window.rootViewController; + while (fromViewController.presentedViewController) { + fromViewController = fromViewController.presentedViewController; + } + OWSAssert(fromViewController); + [fromViewController presentViewController:activityViewController + animated:YES + completion:nil]; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end diff --git a/Signal/src/view controllers/FullImageViewController.m b/Signal/src/view controllers/FullImageViewController.m index 89b849053..202db5a2e 100644 --- a/Signal/src/view controllers/FullImageViewController.m +++ b/Signal/src/view controllers/FullImageViewController.m @@ -216,9 +216,8 @@ action:@selector(copyAttachment:)], [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"ATTACHMENT_VIEW_SAVE_ACTION", @"Short name for edit menu item to save contents of media message.") action:@selector(saveAttachment:)], - // TODO: We should implement sharing. - // [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"ATTACHMENT_VIEW_SHARE_ACTION", @"Short name for edit menu item to share contents of media message.") - // action:@selector(shareAttachment:)], + [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"ATTACHMENT_VIEW_SHARE_ACTION", @"Short name for edit menu item to share contents of media message.") + action:@selector(shareAttachment:)], ]; [UIMenuController sharedMenuController].menuItems = menuItems; CGPoint location = [sender locationInView:self.view]; @@ -241,9 +240,8 @@ OWSAssert([[self.messageItem media] isKindOfClass:[TSPhotoAdapter class]] || [[self.messageItem media] isKindOfClass:[TSAnimatedAdapter class]]); - id messageEditing = (id) self.messageItem.media; - OWSAssert([messageEditing canPerformEditingAction:selector]); - [messageEditing performEditingAction:selector]; + OWSAssert([self.messageItem canPerformEditingAction:selector]); + [self.messageItem performEditingAction:selector]; } - (void)copyAttachment:(id)sender { @@ -255,10 +253,7 @@ } - (void)shareAttachment:(id)sender { - // TODO: We should implement sharing with UIActivityViewController. - // - // It seems that loading of the contents of the attachment is done - // with TSAttachment and TSAttachmentStream. + [self performEditingActionWithSelector:NSSelectorFromString(@"share:")]; } #pragma mark - Presentation diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m index a18fade99..e6cd6ac0a 100644 --- a/Signal/src/view controllers/MessagesViewController.m +++ b/Signal/src/view controllers/MessagesViewController.m @@ -252,6 +252,8 @@ typedef enum : NSUInteger { [JSQMessagesCollectionViewCell registerMenuAction:@selector(delete:)]; SEL saveSelector = NSSelectorFromString(@"save:"); [JSQMessagesCollectionViewCell registerMenuAction:saveSelector]; + SEL shareSelector = NSSelectorFromString(@"share:"); + [JSQMessagesCollectionViewCell registerMenuAction:shareSelector]; [self initializeCollectionViewLayout]; [self registerCustomMessageNibs]; @@ -393,9 +395,14 @@ typedef enum : NSUInteger { // Other views might change these custom menu items, so we // need to set them every time we enter this view. SEL saveSelector = NSSelectorFromString(@"save:"); + SEL shareSelector = NSSelectorFromString(@"share:"); [UIMenuController sharedMenuController].menuItems = @[ [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SAVE_ACTION", @"Short name for edit menu item to save contents of media message.") - action:saveSelector]]; + action:saveSelector], + [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SHARE_ACTION", + @"Short name for edit menu item to share contents of media message.") + action:shareSelector], + ]; } - (void)startReadTimer { diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index eeefad8c0..493bc62de 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -169,6 +169,9 @@ /* Short name for edit menu item to save contents of media message. */ "EDIT_ITEM_SAVE_ACTION" = "Save"; +/* Short name for edit menu item to share contents of media message. */ +"EDIT_ITEM_SHARE_ACTION" = "Share"; + /* body of email sent to contacts when inviting to install Signal. Embeds {{link to install Signal}} and {{link to WhisperSystems home page}} */ "EMAIL_INVITE_BODY" = "Hey,\n\nLately I've been using Signal to keep the conversations on my iPhone private. I'd like you to install it too, so we can be confident that only you and I can read our messages or hear our calls.\n\nSignal is available for iPhones and Android. Get it here: %@\n\nSignal works like your existing messaging app. We can send pictures and video, make calls, and start group chats. The best part is, no one else can see any of it, not even the people who make Signal!\n\nYou can read more about Open Whisper Systems, the people who make Signal, here: %@";