Merge branch 'charlesmchen/attachmentViews'

pull/1/head
Matthew Chen 8 years ago
commit b1f5540938

@ -18,7 +18,6 @@
#import "OWSWebRTCDataProtos.pb.h"
#import "PhoneManager.h"
#import "PropertyListPreferences.h"
#import "PureLayout.h"
#import "PushManager.h"
#import "RPAccountManager.h"
#import "TSSocketManager.h"
@ -27,6 +26,7 @@
#import "UIUtil.h"
#import "UIView+OWS.h"
#import <JSQSystemSoundPlayer.h>
#import <PureLayout/PureLayout.h>
#import <SignalServiceKit/Contact.h>
#import <SignalServiceKit/ContactsUpdater.h>
#import <SignalServiceKit/Cryptography.h>

@ -2,6 +2,7 @@
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <PureLayout/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
@interface FullImageViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
// In order to use UIMenuController, the view from which it is
// presented must have certain custom behaviors.
@interface AttachmentMenuView : UIView
@end
#pragma mark -
@property (nonatomic, strong) UIView *backgroundView;
@implementation AttachmentMenuView
@property (nonatomic, strong) UIScrollView *scrollView;
- (BOOL)canBecomeFirstResponder {
return YES;
}
// We only use custom actions in UIMenuController.
- (BOOL)canPerformAction:(SEL)action
withSender:(id)sender
{
return NO;
}
@property (nonatomic, strong) UIImageView *imageView;
@end
#pragma mark -
@property (nonatomic, strong) UITapGestureRecognizer *singleTap;
@property (nonatomic, strong) UITapGestureRecognizer *doubleTap;
@interface FullImageViewController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
@property (nonatomic, strong) UIButton *shareButton;
@property (nonatomic) UIView *backgroundView;
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) UIImageView *imageView;
@property (nonatomic) UIButton *shareButton;
@property CGRect originRect;
@property BOOL isPresenting;
@property BOOL isAnimated;
@property NSData *fileData;
@property (nonatomic) CGRect originRect;
@property (nonatomic) BOOL isPresenting;
@property (nonatomic) BOOL isAnimated;
@property (nonatomic) 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.backgroundColor = [UIColor colorWithWhite:0 alpha:kBackgroundAlpha];
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;
self.singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageSingleTapped:)];
[self.singleTap requireGestureRecognizerToFail:self.doubleTap];
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];
}
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(longPressGesture:)];
longPress.delegate = self;
[self.view addGestureRecognizer:longPress];
}
self.singleTap.delegate = self;
self.doubleTap.delegate = self;
#pragma mark - Gesture Recognizers
[self.view addGestureRecognizer:self.singleTap];
[self.view addGestureRecognizer:self.doubleTap];
- (void)imageDismissGesture:(UIGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
[self dismiss];
}
}
#pragma mark - Gesture Recognizers
- (void)longPressGesture:(UIGestureRecognizer *)sender {
// We "eagerly" respond when the long press begins, not when it ends.
if (sender.state == UIGestureRecognizerStateBegan) {
- (void)imageDoubleTapped:(UITapGestureRecognizer *)doubleTap {
CGPoint tap = [doubleTap locationInView:doubleTap.view];
CGPoint convertCoord = [self.scrollView convertPoint:tap fromView:doubleTap.view];
CGRect targetZoomRect;
UIEdgeInsets targetInsets;
[self.view becomeFirstResponder];
CGSize zoom;
if ([UIMenuController sharedMenuController].isMenuVisible) {
[[UIMenuController sharedMenuController] setMenuVisible:NO
animated:NO];
}
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];
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];
}
}
self.view.userInteractionEnabled = NO;
- (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]]);
[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.scrollView.contentInset = targetInsets;
self.view.userInteractionEnabled = YES;
}];
[self.scrollView zoomToRect:targetZoomRect animated:YES];
[CATransaction commit];
id<OWSMessageEditing> messageEditing = (id<OWSMessageEditing>) self.messageItem.media;
OWSAssert([messageEditing canPerformEditingAction:selector]);
[messageEditing performEditingAction:selector];
}
- (void)imageSingleTapped:(UITapGestureRecognizer *)singleTap {
[self dismiss];
- (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
@ -193,14 +274,19 @@
presentViewController:self
animated:NO
completion:^{
[UIView animateWithDuration:0.4f
UIWindow *window = [UIApplication sharedApplication].keyWindow;
self.imageView.frame = [self.view convertRect:self.originRect
fromView:window];
[UIView animateWithDuration:0.25f
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut
animations:^() {
self.view.alpha = 1.0f;
self.imageView.frame = [self resizedFrameForImageView:self.image.size];
self.imageView.center =
CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height / 2.0f);
CGPointMake(self.view.bounds.size.width / 2.0f,
self.view.bounds.size.height / 2.0f);
}
completion:^(BOOL completed) {
self.scrollView.frame = self.view.bounds;
@ -215,9 +301,9 @@
- (void)dismiss {
self.view.userInteractionEnabled = NO;
[UIView animateWithDuration:0.4f
[UIView animateWithDuration:0.25f
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveLinear
animations:^() {
self.backgroundView.backgroundColor = [UIColor clearColor];
self.scrollView.alpha = 0;
@ -234,7 +320,6 @@
[self updateLayouts];
}
- (void)updateLayouts {
if (_isPresenting) {
return;
@ -246,7 +331,6 @@
self.scrollView.contentInset = [self contentInsetForScrollView:self.scrollView.zoomScale];
}
#pragma mark - Resizing
- (CGRect)resizedFrameForImageView:(CGSize)imageSize {

@ -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 {
@ -1160,9 +1165,12 @@ typedef enum : NSUInteger {
if(tappedImage == nil) {
DDLogWarn(@"tapped TSPhotoAdapter with nil image");
} else {
CGRect convertedRect =
[self.collectionView convertRect:[collectionView cellForItemAtIndexPath:indexPath].frame
toView:nil];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *) [collectionView cellForItemAtIndexPath:indexPath];
OWSAssert([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]);
CGRect convertedRect = [cell.mediaView convertRect:cell.mediaView.bounds
toView:window];
__block TSAttachment *attachment = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment =
@ -1174,7 +1182,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];
@ -1187,9 +1196,12 @@ typedef enum : NSUInteger {
if(tappedImage == nil) {
DDLogWarn(@"tapped TSAnimatedAdapter with nil image");
} else {
CGRect convertedRect =
[self.collectionView convertRect:[collectionView cellForItemAtIndexPath:indexPath].frame
toView:nil];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *) [collectionView cellForItemAtIndexPath:indexPath];
OWSAssert([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]);
CGRect convertedRect = [cell.mediaView convertRect:cell.mediaView.bounds
toView:window];
__block TSAttachment *attachment = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment =
@ -1200,7 +1212,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];
}
@ -1225,16 +1238,23 @@ typedef enum : NSUInteger {
_videoPlayer = [[MPMoviePlayerController alloc] initWithContentURL:attStream.mediaURL];
[_videoPlayer prepareToPlay];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(moviePlayBackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerWillExitFullscreen:)
name:MPMoviePlayerWillExitFullscreenNotification
object:_videoPlayer];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(moviePlayerDidExitFullscreen:)
name:MPMoviePlayerDidExitFullscreenNotification
object:_videoPlayer];
_videoPlayer.controlStyle = MPMovieControlStyleDefault;
_videoPlayer.shouldAutoplay = YES;
[self.view addSubview:_videoPlayer.view];
[_videoPlayer setFullscreen:YES animated:YES];
// We can't animate from the cell media frame;
// MPMoviePlayerController will animate a crop of its
// contents rather than scaling them.
_videoPlayer.view.frame = self.view.bounds;
[_videoPlayer setFullscreen:YES animated:NO];
}
} else if ([messageMedia isAudio]) {
if (messageMedia.isAudioPlaying) {
@ -1365,9 +1385,28 @@ typedef enum : NSUInteger {
}
}
// There's more than one way to exit the fullscreen video playback.
// There's a done button, a "toggle fullscreen" button and I think
// there's some gestures too. These fire slightly different notifications.
// We want to hide & clean up the video player immediately in all of
// these cases.
- (void)moviePlayerWillExitFullscreen:(id)sender {
DDLogDebug(@"%@ %s", self.tag, __PRETTY_FUNCTION__);
[self clearVideoPlayer];
}
// See comment on moviePlayerWillExitFullscreen:
- (void)moviePlayerDidExitFullscreen:(id)sender {
DDLogDebug(@"%@ %s", self.tag, __PRETTY_FUNCTION__);
[self clearVideoPlayer];
}
- (void)moviePlayBackDidFinish:(id)sender {
DDLogDebug(@"playback finished");
- (void)clearVideoPlayer {
[_videoPlayer stop];
[_videoPlayer.view removeFromSuperview];
_videoPlayer = nil;
}
- (void)collectionView:(JSQMessagesCollectionView *)collectionView

@ -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…
Cancel
Save