mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Add save/copy menu to the image attachment view.
// FREEBIE
This commit is contained in:
parent
df509f2d54
commit
6a3b462541
6 changed files with 177 additions and 82 deletions
|
@ -2,6 +2,7 @@
|
|||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "PureLayout.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// A convenience method for doing responsive layout. Scales between two
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "PureLayout.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
||||
// TODO: We'll eventually want to promote these into an OWSMath.h header.
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
//
|
||||
// FullImageViewController.h
|
||||
// Signal
|
||||
//
|
||||
// Created by Dylan Bourgeois on 11/11/14.
|
||||
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSInteraction.h"
|
||||
#import "OWSMessageData.h"
|
||||
|
||||
@interface FullImageViewController : UIViewController
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
|
||||
fromRect:(CGRect)rect
|
||||
forInteraction:(TSInteraction *)interaction
|
||||
messageItem:(id<OWSMessageData>)messageItem
|
||||
isAnimated:(BOOL)animated;
|
||||
|
||||
- (void)presentFromViewController:(UIViewController *)viewController;
|
||||
|
|
|
@ -1,44 +1,60 @@
|
|||
//
|
||||
// FullImageViewController.m
|
||||
// Signal
|
||||
//
|
||||
// Created by Dylan Bourgeois on 11/11/14.
|
||||
// Animated GIF support added by Mike Okner (@mikeokner) on 11/27/15.
|
||||
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLAnimatedImage.h"
|
||||
#import "FullImageViewController.h"
|
||||
#import "UIUtil.h"
|
||||
|
||||
#define kImageViewCornerRadius 5.0f
|
||||
#import "UIView+OWS.h"
|
||||
#import "TSPhotoAdapter.h"
|
||||
#import "TSMessageAdapter.h"
|
||||
#import "TSAnimatedAdapter.h"
|
||||
|
||||
#define kMinZoomScale 1.0f
|
||||
#define kMaxZoomScale 8.0f
|
||||
#define kTargetDoubleTapZoom 3.0f
|
||||
|
||||
#define kBackgroundAlpha 0.6f
|
||||
|
||||
// In order to use UIMenuController, the view from which it is
|
||||
// presented must have certain custom behaviors.
|
||||
@interface AttachmentMenuView : UIView
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation AttachmentMenuView
|
||||
|
||||
- (BOOL)canBecomeFirstResponder {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// We only use custom actions in UIMenuController.
|
||||
- (BOOL)canPerformAction:(SEL)action
|
||||
withSender:(id)sender
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface FullImageViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIView *backgroundView;
|
||||
@property (nonatomic) UIView *backgroundView;
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
@property (nonatomic) UIImageView *imageView;
|
||||
@property (nonatomic) UIButton *shareButton;
|
||||
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic) CGRect originRect;
|
||||
@property (nonatomic) BOOL isPresenting;
|
||||
@property (nonatomic) BOOL isAnimated;
|
||||
@property (nonatomic) NSData *fileData;
|
||||
|
||||
@property (nonatomic, strong) UIImageView *imageView;
|
||||
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *singleTap;
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *doubleTap;
|
||||
|
||||
@property (nonatomic, strong) UIButton *shareButton;
|
||||
|
||||
@property CGRect originRect;
|
||||
@property BOOL isPresenting;
|
||||
@property BOOL isAnimated;
|
||||
@property NSData *fileData;
|
||||
|
||||
@property TSAttachmentStream *attachment;
|
||||
@property TSInteraction *interaction;
|
||||
@property (nonatomic) TSAttachmentStream *attachment;
|
||||
@property (nonatomic) TSInteraction *interaction;
|
||||
@property (nonatomic) id<OWSMessageData> messageItem;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -48,6 +64,7 @@
|
|||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
|
||||
fromRect:(CGRect)rect
|
||||
forInteraction:(TSInteraction *)interaction
|
||||
messageItem:(id<OWSMessageData>)messageItem
|
||||
isAnimated:(BOOL)animated {
|
||||
self = [super initWithNibName:nil bundle:nil];
|
||||
|
||||
|
@ -55,6 +72,7 @@
|
|||
self.attachment = attachment;
|
||||
self.originRect = rect;
|
||||
self.interaction = interaction;
|
||||
self.messageItem = messageItem;
|
||||
self.isAnimated = animated;
|
||||
self.fileData = [NSData dataWithContentsOfURL:[attachment mediaURL]];
|
||||
}
|
||||
|
@ -66,6 +84,12 @@
|
|||
return self.attachment.image;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [AttachmentMenuView new];
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
|
||||
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
|
@ -77,16 +101,24 @@
|
|||
[self populateImageView:self.image];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
if ([UIMenuController sharedMenuController].isMenuVisible) {
|
||||
[[UIMenuController sharedMenuController] setMenuVisible:NO
|
||||
animated:NO];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Initializers
|
||||
|
||||
- (void)initializeBackground {
|
||||
self.imageView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
|
||||
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.backgroundView = [[UIView alloc] initWithFrame:CGRectInset(self.view.bounds, -512, -512)];
|
||||
|
||||
self.backgroundView = [UIView new];
|
||||
self.backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
|
||||
|
||||
[self.view addSubview:self.backgroundView];
|
||||
[self.backgroundView autoPinEdgesToSuperviewEdges];
|
||||
}
|
||||
|
||||
- (void)initializeScrollView {
|
||||
|
@ -111,7 +143,6 @@
|
|||
} else {
|
||||
// Present the static image using standard UIImageView
|
||||
self.imageView = [[UIImageView alloc] initWithFrame:self.originRect];
|
||||
self.imageView.layer.cornerRadius = kImageViewCornerRadius;
|
||||
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
self.imageView.userInteractionEnabled = YES;
|
||||
self.imageView.clipsToBounds = YES;
|
||||
|
@ -128,56 +159,106 @@
|
|||
}
|
||||
|
||||
- (void)initializeGestureRecognizers {
|
||||
self.doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageDoubleTapped:)];
|
||||
self.doubleTap.numberOfTapsRequired = 2;
|
||||
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(imageDismissGesture:)];
|
||||
singleTap.delegate = self;
|
||||
[self.view addGestureRecognizer:singleTap];
|
||||
|
||||
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(imageDismissGesture:)];
|
||||
doubleTap.numberOfTapsRequired = 2;
|
||||
doubleTap.delegate = self;
|
||||
[self.view addGestureRecognizer:doubleTap];
|
||||
|
||||
// UISwipeGestureRecognizer supposedly supports multiple directions,
|
||||
// but in practice it works better if you use a separate GR for each
|
||||
// direction.
|
||||
for (NSNumber *direction in @[
|
||||
@(UISwipeGestureRecognizerDirectionRight),
|
||||
@(UISwipeGestureRecognizerDirectionLeft),
|
||||
@(UISwipeGestureRecognizerDirectionUp),
|
||||
@(UISwipeGestureRecognizerDirectionDown),
|
||||
]) {
|
||||
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(imageDismissGesture:)];
|
||||
swipe.direction = (UISwipeGestureRecognizerDirection) direction.integerValue;
|
||||
swipe.delegate = self;
|
||||
[self.view addGestureRecognizer:swipe];
|
||||
}
|
||||
|
||||
self.singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageSingleTapped:)];
|
||||
[self.singleTap requireGestureRecognizerToFail:self.doubleTap];
|
||||
|
||||
self.singleTap.delegate = self;
|
||||
self.doubleTap.delegate = self;
|
||||
|
||||
[self.view addGestureRecognizer:self.singleTap];
|
||||
[self.view addGestureRecognizer:self.doubleTap];
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(longPressGesture:)];
|
||||
longPress.delegate = self;
|
||||
[self.view addGestureRecognizer:longPress];
|
||||
}
|
||||
|
||||
#pragma mark - Gesture Recognizers
|
||||
|
||||
- (void)imageDoubleTapped:(UITapGestureRecognizer *)doubleTap {
|
||||
CGPoint tap = [doubleTap locationInView:doubleTap.view];
|
||||
CGPoint convertCoord = [self.scrollView convertPoint:tap fromView:doubleTap.view];
|
||||
CGRect targetZoomRect;
|
||||
UIEdgeInsets targetInsets;
|
||||
|
||||
CGSize zoom;
|
||||
|
||||
if (self.scrollView.zoomScale == 1.0f) {
|
||||
zoom = CGSizeMake(self.view.bounds.size.width / kTargetDoubleTapZoom,
|
||||
self.view.bounds.size.height / kTargetDoubleTapZoom);
|
||||
targetZoomRect = CGRectMake(
|
||||
convertCoord.x - (zoom.width / 2.0f), convertCoord.y - (zoom.height / 2.0f), zoom.width, zoom.height);
|
||||
targetInsets = [self contentInsetForScrollView:kTargetDoubleTapZoom];
|
||||
} else {
|
||||
zoom = CGSizeMake(self.view.bounds.size.width * self.scrollView.zoomScale,
|
||||
self.view.bounds.size.height * self.scrollView.zoomScale);
|
||||
targetZoomRect = CGRectMake(
|
||||
convertCoord.x - (zoom.width / 2.0f), convertCoord.y - (zoom.height / 2.0f), zoom.width, zoom.height);
|
||||
targetInsets = [self contentInsetForScrollView:1.0f];
|
||||
- (void)imageDismissGesture:(UIGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
self.view.userInteractionEnabled = NO;
|
||||
|
||||
[CATransaction begin];
|
||||
[CATransaction setCompletionBlock:^{
|
||||
self.scrollView.contentInset = targetInsets;
|
||||
self.view.userInteractionEnabled = YES;
|
||||
}];
|
||||
[self.scrollView zoomToRect:targetZoomRect animated:YES];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (void)imageSingleTapped:(UITapGestureRecognizer *)singleTap {
|
||||
[self dismiss];
|
||||
- (void)longPressGesture:(UIGestureRecognizer *)sender {
|
||||
// We "eagerly" respond when the long press begins, not when it ends.
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
|
||||
[self.view becomeFirstResponder];
|
||||
|
||||
if ([UIMenuController sharedMenuController].isMenuVisible) {
|
||||
[[UIMenuController sharedMenuController] setMenuVisible:NO
|
||||
animated:NO];
|
||||
}
|
||||
|
||||
NSArray *menuItems = @[
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"ATTACHMENT_VIEW_COPY_ACTION", @"Short name for edit menu item to copy contents of media message.")
|
||||
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:)],
|
||||
];
|
||||
[UIMenuController sharedMenuController].menuItems = menuItems;
|
||||
CGPoint location = [sender locationInView:self.view];
|
||||
CGRect targetRect = CGRectMake(location.x,
|
||||
location.y,
|
||||
1, 1);
|
||||
[[UIMenuController sharedMenuController] setTargetRect:targetRect
|
||||
inView:self.view];
|
||||
[[UIMenuController sharedMenuController] setMenuVisible:YES
|
||||
animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)performEditingActionWithSelector:(SEL)selector {
|
||||
OWSAssert(self.messageItem.messageType == TSIncomingMessageAdapter ||
|
||||
self.messageItem.messageType == TSOutgoingMessageAdapter);
|
||||
OWSAssert([self.messageItem isMediaMessage]);
|
||||
OWSAssert([self.messageItem isKindOfClass:[TSMessageAdapter class]]);
|
||||
OWSAssert([self.messageItem conformsToProtocol:@protocol(OWSMessageEditing)]);
|
||||
OWSAssert([[self.messageItem media] isKindOfClass:[TSPhotoAdapter class]] ||
|
||||
[[self.messageItem media] isKindOfClass:[TSAnimatedAdapter class]]);
|
||||
|
||||
id<OWSMessageEditing> messageEditing = (id<OWSMessageEditing>) self.messageItem.media;
|
||||
OWSAssert([messageEditing canPerformEditingAction:selector]);
|
||||
[messageEditing performEditingAction:selector];
|
||||
}
|
||||
|
||||
- (void)copyAttachment:(id)sender {
|
||||
[self performEditingActionWithSelector:NSSelectorFromString(@"copy:")];
|
||||
}
|
||||
|
||||
- (void)saveAttachment:(id)sender {
|
||||
[self performEditingActionWithSelector:NSSelectorFromString(@"save:")];
|
||||
}
|
||||
|
||||
- (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.
|
||||
}
|
||||
|
||||
#pragma mark - Presentation
|
||||
|
|
|
@ -252,8 +252,6 @@ typedef enum : NSUInteger {
|
|||
[JSQMessagesCollectionViewCell registerMenuAction:@selector(delete:)];
|
||||
SEL saveSelector = NSSelectorFromString(@"save:");
|
||||
[JSQMessagesCollectionViewCell registerMenuAction:saveSelector];
|
||||
[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] ];
|
||||
|
||||
[self initializeCollectionViewLayout];
|
||||
[self registerCustomMessageNibs];
|
||||
|
@ -391,6 +389,13 @@ typedef enum : NSUInteger {
|
|||
atScrollPosition:UICollectionViewScrollPositionBottom
|
||||
animated:NO];
|
||||
}
|
||||
|
||||
// Other views might change these custom menu items, so we
|
||||
// need to set them every time we enter this view.
|
||||
SEL saveSelector = NSSelectorFromString(@"save:");
|
||||
[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]];
|
||||
}
|
||||
|
||||
- (void)startReadTimer {
|
||||
|
@ -1174,7 +1179,8 @@ typedef enum : NSUInteger {
|
|||
FullImageViewController *vc = [[FullImageViewController alloc]
|
||||
initWithAttachment:attStream
|
||||
fromRect:convertedRect
|
||||
forInteraction:[self interactionAtIndexPath:indexPath]
|
||||
forInteraction:interaction
|
||||
messageItem:messageItem
|
||||
isAnimated:NO];
|
||||
|
||||
[vc presentFromViewController:self.navigationController];
|
||||
|
@ -1200,7 +1206,8 @@ typedef enum : NSUInteger {
|
|||
FullImageViewController *vc =
|
||||
[[FullImageViewController alloc] initWithAttachment:attStream
|
||||
fromRect:convertedRect
|
||||
forInteraction:[self interactionAtIndexPath:indexPath]
|
||||
forInteraction:interaction
|
||||
messageItem:messageItem
|
||||
isAnimated:YES];
|
||||
[vc presentFromViewController:self.navigationController];
|
||||
}
|
||||
|
|
|
@ -67,6 +67,15 @@
|
|||
/* No comment provided by engineer. */
|
||||
"ATTACHMENT_QUEUED" = "New attachment queued for retrieval.";
|
||||
|
||||
/* Short name for edit menu item to copy contents of media message. */
|
||||
"ATTACHMENT_VIEW_COPY_ACTION" = "Copy";
|
||||
|
||||
/* Short name for edit menu item to save contents of media message. */
|
||||
"ATTACHMENT_VIEW_SAVE_ACTION" = "Save";
|
||||
|
||||
/* Short name for edit menu item to share contents of media message. */
|
||||
"ATTACHMENT_VIEW_SHARE_ACTION" = "Share";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to work properly. You can grant this permission in the Settings app >> Privacy >> Microphone >> Signal";
|
||||
|
||||
|
|
Loading…
Reference in a new issue