Rework attachment approval UI.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent 1fee5d97e6
commit d04f9111db

@ -2561,11 +2561,7 @@
"DEBUG=1",
"$(inherited)",
);
"GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = (
"DEBUG=1",
"$(inherited)",
"SSK_BUILDING_FOR_TESTS=1",
);
"GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1";
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "cancel-cross-white@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "cancel-cross-white@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "cancel-cross-white@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -57,6 +57,7 @@ enum TSImageQuality {
class SignalAttachment: NSObject {
static let TAG = "[SignalAttachment]"
let TAG = "[SignalAttachment]"
// MARK: Properties
@ -95,7 +96,8 @@ class SignalAttachment: NSObject {
// To avoid redundant work of repeatedly compressing/uncompressing
// images, we cache the UIImage associated with this attachment if
// possible.
public var image: UIImage?
private var cachedImage: UIImage?
private var cachedVideoPreview: UIImage?
private(set) public var isVoiceMessage = false
@ -152,6 +154,42 @@ class SignalAttachment: NSObject {
return SignalAttachmentError.missingData.errorDescription
}
public func image() -> UIImage? {
if let cachedImage = cachedImage {
return cachedImage
}
guard let image = UIImage(data:dataSource.data()) else {
return nil
}
cachedImage = image
return image
}
public func videoPreview() -> UIImage? {
if let cachedVideoPreview = cachedVideoPreview {
return cachedVideoPreview
}
guard let mediaUrl = dataUrl else {
return nil
}
do {
let asset = AVURLAsset(url:mediaUrl)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
let cgImage = try generator.copyCGImage(at: CMTimeMake(0, 1), actualTime: nil)
let image = UIImage(cgImage: cgImage)
cachedVideoPreview = image
return image
} catch let error {
Logger.verbose("\(TAG) Could not generate video thumbnail: \(error.localizedDescription)")
return nil
}
}
// Returns the MIME type for this attachment or nil if no MIME type
// can be identified.
var mimeType: String {
@ -454,7 +492,7 @@ class SignalAttachment: NSObject {
attachment.error = .couldNotParseImage
return attachment
}
attachment.image = image
attachment.cachedImage = image
if isInputImageValidOutputImage(image: image, dataSource: dataSource, dataUTI: dataUTI) {
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType)")
@ -513,7 +551,7 @@ class SignalAttachment: NSObject {
let dataSource = DataSourceValue.emptyDataSource()
dataSource.sourceFilename = filename
let attachment = SignalAttachment(dataSource : dataSource, dataUTI: dataUTI)
attachment.image = image
attachment.cachedImage = image
Logger.verbose("\(TAG) Writing \(attachment.mimeType) as image/jpeg")
return compressImageAsJPEG(image : image, attachment : attachment, filename:filename)
@ -545,7 +583,7 @@ class SignalAttachment: NSObject {
if UInt(jpgImageData.count) <= kMaxFileSizeImage {
let recompressedAttachment = SignalAttachment(dataSource : dataSource, dataUTI: kUTTypeJPEG as String)
recompressedAttachment.image = dstImage
recompressedAttachment.cachedImage = dstImage
return recompressedAttachment
}

@ -42,9 +42,9 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
#pragma mark - Attachment Approval
@property (nonatomic) UIView *attachmentApprovalView;
@property (nonatomic, nullable) MediaMessageView *attachmentView;
@property (nonatomic, nullable) SignalAttachment *attachmentToApprove;
@property (nonatomic) BOOL isLargeAttachment;
@end
@ -132,10 +132,6 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
self.voiceMemoButton.imageView.tintColor = [UIColor ows_materialBlueColor];
[self.rightButtonWrapper addSubview:self.voiceMemoButton];
_attachmentApprovalView = [UIView containerView];
[self addSubview:self.attachmentApprovalView];
[self.attachmentApprovalView autoPinToSuperviewEdges];
// We want to be permissive about the voice message gesture, so we hang
// the long press GR on the button's wrapper, not the button itself.
UILongPressGestureRecognizer *longPressGestureRecognizer =
@ -215,12 +211,51 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
{
[NSLayoutConstraint deactivateConstraints:self.contentContraints];
const int textViewVInset = 5;
const int contentHInset = 6;
const int contentHSpacing = 6;
// We want to grow the text input area to fit its content within reason.
const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
+ self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom
+ self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom);
const CGFloat kMaxTextViewHeight = 100.f;
const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top
+ self.inputTextView.contentInset.bottom);
const CGFloat textViewHeight = ceil(MAX(kMinTextViewHeight, MIN(kMaxTextViewHeight, textViewDesiredHeight)));
const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;
if (self.attachmentToApprove) {
self.contentView.hidden = YES;
self.attachmentApprovalView.hidden = NO;
OWSAssert(self.attachmentView);
self.inputTextView.hidden = YES;
self.attachmentButton.hidden = YES;
self.voiceMemoButton.hidden = YES;
UIButton *rightButton = self.sendButton;
rightButton.enabled = YES;
rightButton.hidden = NO;
[rightButton setContentHuggingHigh];
[rightButton setCompressionResistanceHigh];
[self.attachmentView setContentHuggingLow];
OWSAssert(rightButton.superview == self.rightButtonWrapper);
self.contentContraints = @[
[self.attachmentApprovalView autoSetDimension:ALDimensionHeight toSize:300.f],
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:contentHInset],
[self.attachmentView autoSetDimension:ALDimensionHeight toSize:(self.isLargeAttachment ? 300.f : 150.f)],
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.attachmentView],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
[rightButton autoPinTrailingToSuperviewWithMargin:contentHInset],
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],
];
[self setNeedsLayout];
@ -235,26 +270,11 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
return;
}
self.contentView.hidden = NO;
self.attachmentApprovalView.hidden = YES;
self.inputTextView.hidden = NO;
self.attachmentButton.hidden = NO;
self.voiceMemoButton.hidden = NO;
[self.attachmentView removeFromSuperview];
self.attachmentView = nil;
for (UIView *subview in self.attachmentApprovalView.subviews) {
[subview removeFromSuperview];
}
const int textViewVInset = 5;
const int contentHInset = 6;
const int contentHSpacing = 6;
// We want to grow the text input area to fit its content within reason.
const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
+ self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom
+ self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom);
const CGFloat kMaxTextViewHeight = 100.f;
const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top
+ self.inputTextView.contentInset.bottom);
const CGFloat textViewHeight = ceil(MAX(kMinTextViewHeight, MIN(kMaxTextViewHeight, textViewDesiredHeight)));
const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;
UIButton *leftButton = self.attachmentButton;
UIButton *rightButton = (self.shouldShowVoiceMemoButton ? self.voiceMemoButton : self.sendButton);
@ -321,7 +341,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
- (void)ensureShouldShowVoiceMemoButton
{
self.shouldShowVoiceMemoButton = self.inputTextView.trimmedText.length < 1;
self.shouldShowVoiceMemoButton = (self.attachmentToApprove != nil && self.inputTextView.trimmedText.length < 1);
}
- (void)handleLongPress:(UIGestureRecognizer *)sender
@ -619,7 +639,11 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
{
OWSAssert(self.inputToolbarDelegate);
[self.inputToolbarDelegate sendButtonPressed];
if (self.attachmentToApprove) {
[self attachmentApprovalSendPressed];
} else {
[self.inputToolbarDelegate sendButtonPressed];
}
}
- (void)attachmentButtonPressed
@ -696,61 +720,60 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
OWSAssert(attachment);
self.attachmentToApprove = attachment;
self.isLargeAttachment = (attachment.isImage || attachment.isAnimatedImage);
MediaMessageView *attachmentView = [[MediaMessageView alloc] initWithAttachment:attachment];
self.attachmentView = attachmentView;
[self.attachmentApprovalView addSubview:attachmentView];
[attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10];
[attachmentView autoPinWidthToSuperviewWithMargin:20];
UIView *buttonRow = [UIView containerView];
[self.attachmentApprovalView addSubview:buttonRow];
[buttonRow autoPinWidthToSuperviewWithMargin:20];
[buttonRow autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:attachmentView withOffset:10];
[buttonRow autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10];
// We use this invisible subview to ensure that the buttons are centered
// horizontally.
UIView *buttonSpacer = [UIView new];
[buttonRow addSubview:buttonSpacer];
// Vertical positioning of this view doesn't matter.
[buttonSpacer autoPinEdgeToSuperviewEdge:ALEdgeTop];
[buttonSpacer autoSetDimension:ALDimensionWidth toSize:ScaleFromIPhone5To7Plus(20, 30)];
[buttonSpacer autoSetDimension:ALDimensionHeight toSize:0];
[buttonSpacer autoHCenterInSuperview];
UIView *cancelButton = [self createAttachmentApprovalButton:[CommonStrings cancelButton]
color:[UIColor ows_destructiveRedColor]
selector:@selector(attachmentApprovalCancelPressed)];
[buttonRow addSubview:cancelButton];
[cancelButton autoPinHeightToSuperview];
[cancelButton autoPinEdge:ALEdgeRight toEdge:ALEdgeLeft ofView:buttonSpacer];
UIView *sendButton =
[self createAttachmentApprovalButton:NSLocalizedString(
@"ATTACHMENT_APPROVAL_SEND_BUTTON", comment
: @"Label for 'send' button in the 'attachment approval' dialog.")
color:[UIColor colorWithRGBHex:0x2ecc71]
selector:@selector(attachmentApprovalSendPressed)];
[buttonRow addSubview:sendButton];
[sendButton autoPinHeightToSuperview];
[sendButton autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:buttonSpacer];
[self.contentView addSubview:attachmentView];
UIView *cancelButtonWrapper = [UIView containerView];
[cancelButtonWrapper
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(cancelButtonWrapperTapped:)]];
UIView *_Nullable cancelButtonSuperview = [self.attachmentView contentView];
if (cancelButtonSuperview) {
cancelButtonSuperview.layer.borderColor = self.inputTextView.layer.borderColor;
cancelButtonSuperview.layer.borderWidth = self.inputTextView.layer.borderWidth;
cancelButtonSuperview.layer.cornerRadius = self.inputTextView.layer.cornerRadius;
cancelButtonSuperview.clipsToBounds = YES;
} else {
cancelButtonSuperview = self.attachmentView;
}
[cancelButtonSuperview addSubview:cancelButtonWrapper];
[cancelButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop];
[cancelButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight];
UIImage *cancelIcon = [UIImage imageNamed:@"cancel-cross-white"];
OWSAssert(cancelIcon);
UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
[cancelButton setImage:cancelIcon forState:UIControlStateNormal];
[cancelButton setBackgroundColor:[UIColor ows_materialBlueColor]];
OWSAssert(cancelIcon.size.width == cancelIcon.size.height);
CGFloat cancelIconSize = MIN(cancelIcon.size.width, cancelIcon.size.height);
CGFloat cancelIconInset = round(cancelIconSize * 0.35f);
[cancelButton
setContentEdgeInsets:UIEdgeInsetsMake(cancelIconInset, cancelIconInset, cancelIconInset, cancelIconInset)];
CGFloat cancelButtonRadius = cancelIconInset + cancelIconSize * 0.5f;
cancelButton.layer.cornerRadius = cancelButtonRadius;
CGFloat cancelButtonInset = 10.f;
[cancelButton addTarget:self
action:@selector(attachmentApprovalCancelPressed)
forControlEvents:UIControlEventTouchUpInside];
[cancelButtonWrapper addSubview:cancelButton];
[cancelButton autoPinWidthToSuperviewWithMargin:cancelButtonInset];
[cancelButton autoPinHeightToSuperviewWithMargin:cancelButtonInset];
CGFloat cancelButtonSize = cancelIconSize + 2 * cancelIconInset;
[cancelButton autoSetDimension:ALDimensionWidth toSize:cancelButtonSize];
[cancelButton autoSetDimension:ALDimensionHeight toSize:cancelButtonSize];
[self ensureContentConstraints];
}
- (UIView *)createAttachmentApprovalButton:(NSString *)title color:(UIColor *)color selector:(SEL)selector
- (void)cancelButtonWrapperTapped:(UIGestureRecognizer *)sender
{
const CGFloat buttonWidth = ScaleFromIPhone5To7Plus(110, 140);
const CGFloat buttonHeight = ScaleFromIPhone5To7Plus(35, 45);
return [OWSFlatButton buttonWithTitle:title
titleColor:[UIColor whiteColor]
backgroundColor:color
width:buttonWidth
height:buttonHeight
target:self
selector:selector];
if (sender.state == UIGestureRecognizerStateRecognized) {
[self attachmentApprovalCancelPressed];
}
}
- (void)attachmentApprovalCancelPressed

@ -277,6 +277,11 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
}
[self updateBarButtonItems];
dispatch_async(dispatch_get_main_queue(), ^{
TSThread *thread = [self threadForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
[self presentThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO];
});
}
- (void)updateBarButtonItems

@ -30,6 +30,8 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
var audioProgressSeconds: CGFloat = 0
var audioDurationSeconds: CGFloat = 0
var contentView: UIView?
// MARK: Initializers
@available(*, unavailable, message:"use attachment: constructor instead.")
@ -49,6 +51,10 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
createViews()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: View Lifecycle
func viewWillAppear(_ animated: Bool) {
@ -154,19 +160,31 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
createGenericPreview()
return
}
guard image.size.width > 0 && image.size.height > 0 else {
createGenericPreview()
return
}
let animatedImageView = YYAnimatedImageView()
animatedImageView.image = image
animatedImageView.contentMode = .scaleAspectFit
self.addSubview(animatedImageView)
animatedImageView.autoPinToSuperviewEdges()
let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view:animatedImageView, aspectRatio:aspectRatio)
contentView = animatedImageView
}
private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) {
self.addSubview(view)
view.autoCenterInSuperview()
view.autoPin(toAspectRatio:aspectRatio)
view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
}
private func createImagePreview() {
var image = attachment.image
if image == nil {
image = UIImage(data: attachment.data)
guard let image = attachment.image() else {
createGenericPreview()
return
}
guard image != nil else {
guard image.size.width > 0 && image.size.height > 0 else {
createGenericPreview()
return
}
@ -174,28 +192,35 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
let imageView = UIImageView(image: image)
imageView.layer.minificationFilter = kCAFilterTrilinear
imageView.layer.magnificationFilter = kCAFilterTrilinear
imageView.contentMode = .scaleAspectFit
self.addSubview(imageView)
imageView.autoPinToSuperviewEdges()
let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view:imageView, aspectRatio:aspectRatio)
contentView = imageView
}
private func createVideoPreview() {
guard let dataUrl = attachment.dataUrl else {
guard let image = attachment.videoPreview() else {
createGenericPreview()
return
}
guard let videoPlayer = MPMoviePlayerController(contentURL: dataUrl) else {
guard image.size.width > 0 && image.size.height > 0 else {
createGenericPreview()
return
}
videoPlayer.prepareToPlay()
videoPlayer.controlStyle = .default
videoPlayer.shouldAutoplay = false
let imageView = UIImageView(image: image)
imageView.layer.minificationFilter = kCAFilterTrilinear
imageView.layer.magnificationFilter = kCAFilterTrilinear
let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view:imageView, aspectRatio:aspectRatio)
contentView = imageView
self.addSubview(videoPlayer.view)
self.videoPlayer = videoPlayer
videoPlayer.view.autoPinToSuperviewEdges()
let videoPlayIcon = UIImage(named:"play_button")
let videoPlayButton = UIImageView(image:videoPlayIcon)
imageView.addSubview(videoPlayButton)
videoPlayButton.autoCenterInSuperview()
imageView.isUserInteractionEnabled = true
imageView.addGestureRecognizer(UITapGestureRecognizer(target:self, action:#selector(videoTapped)))
}
private func createGenericPreview() {
@ -365,4 +390,51 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
audioPlayButton?.setImage(image, for: .normal)
audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue()
}
// MARK: - Video Playback
func videoTapped(sender: UIGestureRecognizer) {
guard let dataUrl = attachment.dataUrl else {
return
}
guard sender.state == .recognized else {
return
}
guard let videoPlayer = MPMoviePlayerController(contentURL: dataUrl) else {
return
}
videoPlayer.prepareToPlay()
NotificationCenter.default.addObserver(forName: .MPMoviePlayerWillExitFullscreen, object: nil, queue: nil) { [weak self] _ in
self?.moviePlayerWillExitFullscreen()
}
NotificationCenter.default.addObserver(forName: .MPMoviePlayerDidExitFullscreen, object: nil, queue: nil) { [weak self] _ in
self?.moviePlayerDidExitFullscreen()
}
videoPlayer.controlStyle = .default
videoPlayer.shouldAutoplay = true
self.addSubview(videoPlayer.view)
videoPlayer.view.frame = self.bounds
self.videoPlayer = videoPlayer
videoPlayer.view.autoPinToSuperviewEdges()
ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(true)
videoPlayer.setFullscreen(true, animated:false)
}
private func moviePlayerWillExitFullscreen() {
clearVideoPlayer()
}
private func moviePlayerDidExitFullscreen() {
clearVideoPlayer()
}
private func clearVideoPlayer() {
videoPlayer?.stop()
videoPlayer?.view.removeFromSuperview()
videoPlayer = nil
ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(false)
}
}

@ -200,6 +200,9 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
DDLogError(@"%@ %zd %@", self.tag, self.attachmentIds.count, self.attachmentIds.firstObject);
[DDLog flushLog];
if (!(self.groupMetaMessage == TSGroupMessageDeliver || self.groupMetaMessage == TSGroupMessageNone)) {
DDLogDebug(@"%@ Skipping save for group meta message.", self.tag);
return;

@ -297,6 +297,7 @@ NS_ASSUME_NONNULL_BEGIN
DataSourcePath *instance = [DataSourcePath new];
instance.filePath = filePath;
OWSAssert(!instance.shouldDeleteOnDeallocation);
return instance;
}

Loading…
Cancel
Save