From d29ce740cb668d4954e6e52889ddecf280c83415 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Tue, 5 Feb 2019 22:36:03 -0700 Subject: [PATCH] Voice Note Lock --- Signal.xcodeproj/project.pbxproj | 4 + .../ConversationInputToolbar.h | 12 +- .../ConversationInputToolbar.m | 197 ++++++++++++++++-- .../ConversationScrollButton.m | 7 +- .../ConversationViewController.m | 14 +- Signal/src/views/VoiceNoteLock.swift | 80 +++++++ SignalMessaging/appearance/Theme.h | 4 +- SignalMessaging/appearance/Theme.m | 8 +- 8 files changed, 293 insertions(+), 33 deletions(-) create mode 100644 Signal/src/views/VoiceNoteLock.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 2ed9d2c97..ca78b916a 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -437,6 +437,7 @@ 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */; }; 45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */; }; 4AC4EA13C8A444455DAB351F /* Pods_SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */; }; + 4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */; }; 4C04F58421C860C50090D0BB /* MantlePerfTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C04F58321C860C50090D0BB /* MantlePerfTest.swift */; }; 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; }; 4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */; }; @@ -1149,6 +1150,7 @@ 45F659811E1BE77000444429 /* NonCallKitCallUIAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonCallKitCallUIAdaptee.swift; sourceTree = ""; }; 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = ""; }; 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = ""; }; + 4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceNoteLock.swift; sourceTree = ""; }; 4C04F58321C860C50090D0BB /* MantlePerfTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MantlePerfTest.swift; path = Models/MantlePerfTest.swift; sourceTree = ""; }; 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticFeedback.swift; path = UserInterface/HapticFeedback.swift; sourceTree = ""; }; 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = ""; }; @@ -2330,6 +2332,7 @@ 450D19111F85236600970622 /* RemoteVideoView.h */, 450D19121F85236600970622 /* RemoteVideoView.m */, 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */, + 4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */, ); name = Views; path = views; @@ -3394,6 +3397,7 @@ 34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */, 34DC9BD921543E0C00FDDCEC /* DebugContactsUtils.m in Sources */, 34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */, + 4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */, 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */, 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */, 3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index adebf8e83..197fc18ff 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -19,11 +19,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)voiceMemoGestureDidStart; -- (void)voiceMemoGestureDidEnd; +- (void)voiceMemoGestureDidLock; + +- (void)voiceMemoGestureDidComplete; - (void)voiceMemoGestureDidCancel; -- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha; +- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha; @end @@ -55,10 +57,12 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateFontSizes; - (void)updateLayoutWithSafeAreaInsets:(UIEdgeInsets)safeAreaInsets; +- (void)ensureTextViewHeight; #pragma mark - Voice Memo -- (void)ensureTextViewHeight; +- (void)lockVoiceMemoUI; + - (void)showVoiceMemoUI; - (void)hideVoiceMemoUI:(BOOL)animated; @@ -67,6 +71,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)cancelVoiceMemoIfNecessary; +#pragma mark - + @property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply; @property (nonatomic, nullable, readonly) OWSLinkPreviewDraft *linkPreviewDraft; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 18ce9b35e..e4eed46d0 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -20,6 +20,12 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_CLOSED_ENUM(NSUInteger, VoiceMemoRecordingState){ + VoiceMemoRecordingState_Idle, + VoiceMemoRecordingState_RecordingHeld, + VoiceMemoRecordingState_RecordingLocked +}; + static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext; const CGFloat kMinTextViewHeight = 36; @@ -62,11 +68,16 @@ const CGFloat kMaxTextViewHeight = 98; #pragma mark - Voice Memo Recording UI @property (nonatomic, nullable) UIView *voiceMemoUI; +@property (nonatomic, nullable) VoiceMemoLockView *voiceMemoLockView; @property (nonatomic, nullable) UIView *voiceMemoContentView; @property (nonatomic) NSDate *voiceMemoStartTime; @property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer; +@property (nonatomic) UIGestureRecognizer *voiceMemoGestureRecognizer; +@property (nonatomic, nullable) UILabel *voiceMemoCancelLabel; +@property (nonatomic, nullable) UIView *voiceMemoRedRecordingCircle; @property (nonatomic, nullable) UILabel *recordingLabel; -@property (nonatomic) BOOL isRecordingVoiceMemo; +@property (nonatomic, readonly) BOOL isRecordingVoiceMemo; +@property (nonatomic) VoiceMemoRecordingState voiceMemoRecordingState; @property (nonatomic) CGPoint voiceMemoGestureStartLocation; @property (nonatomic, nullable) NSArray *layoutContraints; @property (nonatomic) UIEdgeInsets receivedSafeAreaInsets; @@ -164,6 +175,7 @@ const CGFloat kMaxTextViewHeight = 98; UILongPressGestureRecognizer *longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; longPressGestureRecognizer.minimumPressDuration = 0; + self.voiceMemoGestureRecognizer = longPressGestureRecognizer; [self.voiceMemoButton addGestureRecognizer:longPressGestureRecognizer]; self.userInteractionEnabled = YES; @@ -435,18 +447,25 @@ const CGFloat kMaxTextViewHeight = 98; case UIGestureRecognizerStateFailed: if (self.isRecordingVoiceMemo) { // Cancel voice message if necessary. - self.isRecordingVoiceMemo = NO; + self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle; [self.inputToolbarDelegate voiceMemoGestureDidCancel]; } break; case UIGestureRecognizerStateBegan: - if (self.isRecordingVoiceMemo) { - // Cancel voice message if necessary. - self.isRecordingVoiceMemo = NO; - [self.inputToolbarDelegate voiceMemoGestureDidCancel]; + switch (self.voiceMemoRecordingState) { + case VoiceMemoRecordingState_Idle: + break; + case VoiceMemoRecordingState_RecordingHeld: + OWSFailDebug(@"while recording held, shouldn't be possible to restart gesture."); + [self.inputToolbarDelegate voiceMemoGestureDidCancel]; + break; + case VoiceMemoRecordingState_RecordingLocked: + OWSFailDebug(@"once locked, shouldn't be possible to interact with gesture."); + [self.inputToolbarDelegate voiceMemoGestureDidCancel]; + break; } // Start voice message. - self.isRecordingVoiceMemo = YES; + self.voiceMemoRecordingState = VoiceMemoRecordingState_RecordingHeld; self.voiceMemoGestureStartLocation = [sender locationInView:self]; [self.inputToolbarDelegate voiceMemoGestureDidStart]; break; @@ -457,25 +476,62 @@ const CGFloat kMaxTextViewHeight = 98; // For LTR/RTL, swiping in either direction will cancel. // This is okay because there's only space on screen to perform the // gesture in one direction. - CGFloat offset = fabs(self.voiceMemoGestureStartLocation.x - location.x); + CGFloat xOffset = fabs(self.voiceMemoGestureStartLocation.x - location.x); // The lower this value, the easier it is to cancel by accident. // The higher this value, the harder it is to cancel. const CGFloat kCancelOffsetPoints = 100.f; - CGFloat cancelAlpha = offset / kCancelOffsetPoints; + CGFloat cancelAlpha = xOffset / kCancelOffsetPoints; BOOL isCancelled = cancelAlpha >= 1.f; if (isCancelled) { - self.isRecordingVoiceMemo = NO; + self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle; [self.inputToolbarDelegate voiceMemoGestureDidCancel]; + break; } else { - [self.inputToolbarDelegate voiceMemoGestureDidChange:cancelAlpha]; + [self.inputToolbarDelegate voiceMemoGestureDidUpdateCancelWithRatioComplete:cancelAlpha]; + } + + CGFloat yOffset = fabs(self.voiceMemoGestureStartLocation.y - location.y); + + // require a certain threshold before we consider the user to be + // interacting with the lock ui, otherwise there's perceptible wobble + // of the lock slider even when the user isn't intended to interact with it. + const CGFloat kLockThresholdPoints = 20.f; + const CGFloat kLockOffsetPoints = 80.f; + CGFloat yOffsetBeyondThreshold = MAX(yOffset - kLockThresholdPoints, 0); + CGFloat lockAlpha = yOffsetBeyondThreshold / kLockOffsetPoints; + BOOL isLocked = lockAlpha >= 1.f; + if (isLocked) { + switch (self.voiceMemoRecordingState) { + case VoiceMemoRecordingState_RecordingHeld: + self.voiceMemoRecordingState = VoiceMemoRecordingState_RecordingLocked; + [self.inputToolbarDelegate voiceMemoGestureDidLock]; + [self.inputToolbarDelegate voiceMemoGestureDidUpdateCancelWithRatioComplete:0]; + break; + case VoiceMemoRecordingState_RecordingLocked: + // already locked + break; + case VoiceMemoRecordingState_Idle: + OWSFailDebug(@"failure: unexpeceted idle state"); + [self.inputToolbarDelegate voiceMemoGestureDidCancel]; + break; + } + } else { + [self.voiceMemoLockView updateWithRatioComplete:lockAlpha]; } } break; case UIGestureRecognizerStateEnded: - if (self.isRecordingVoiceMemo) { - // End voice message. - self.isRecordingVoiceMemo = NO; - [self.inputToolbarDelegate voiceMemoGestureDidEnd]; + switch (self.voiceMemoRecordingState) { + case VoiceMemoRecordingState_Idle: + break; + case VoiceMemoRecordingState_RecordingHeld: + // End voice message. + self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle; + [self.inputToolbarDelegate voiceMemoGestureDidComplete]; + break; + case VoiceMemoRecordingState_RecordingLocked: + // Continue recording. + break; } break; } @@ -483,6 +539,17 @@ const CGFloat kMaxTextViewHeight = 98; #pragma mark - Voice Memo +- (BOOL)isRecordingVoiceMemo +{ + switch (self.voiceMemoRecordingState) { + case VoiceMemoRecordingState_Idle: + return NO; + case VoiceMemoRecordingState_RecordingHeld: + case VoiceMemoRecordingState_RecordingLocked: + return YES; + } +} + - (void)showVoiceMemoUI { OWSAssertIsOnMainThread(); @@ -490,9 +557,9 @@ const CGFloat kMaxTextViewHeight = 98; self.voiceMemoStartTime = [NSDate date]; [self.voiceMemoUI removeFromSuperview]; + [self.voiceMemoLockView removeFromSuperview]; self.voiceMemoUI = [UIView new]; - self.voiceMemoUI.userInteractionEnabled = NO; self.voiceMemoUI.backgroundColor = Theme.toolbarBackgroundColor; [self addSubview:self.voiceMemoUI]; self.voiceMemoUI.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height); @@ -505,6 +572,14 @@ const CGFloat kMaxTextViewHeight = 98; self.recordingLabel.textColor = [UIColor ows_destructiveRedColor]; self.recordingLabel.font = [UIFont ows_mediumFontWithSize:14.f]; [self.voiceMemoContentView addSubview:self.recordingLabel]; + + VoiceMemoLockView *voiceMemoLockView = [VoiceMemoLockView new]; + self.voiceMemoLockView = voiceMemoLockView; + [self addSubview:voiceMemoLockView]; + [voiceMemoLockView autoPinTrailingToSuperviewMargin]; + [voiceMemoLockView autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:self.voiceMemoContentView]; + [voiceMemoLockView setCompressionResistanceHigh]; + [self updateVoiceMemo]; UIImage *icon = [UIImage imageNamed:@"voice-memo-button"]; @@ -512,6 +587,7 @@ const CGFloat kMaxTextViewHeight = 98; UIImageView *imageView = [[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; imageView.tintColor = [UIColor ows_destructiveRedColor]; + [imageView setContentHuggingHigh]; [self.voiceMemoContentView addSubview:imageView]; NSMutableAttributedString *cancelString = [NSMutableAttributedString new]; @@ -559,11 +635,13 @@ const CGFloat kMaxTextViewHeight = 98; NSBaselineOffsetAttributeName : @(-1.f), }]]; UILabel *cancelLabel = [UILabel new]; + self.voiceMemoCancelLabel = cancelLabel; cancelLabel.attributedText = cancelString; [self.voiceMemoContentView addSubview:cancelLabel]; const CGFloat kRedCircleSize = 100.f; UIView *redCircleView = [UIView new]; + self.voiceMemoRedRecordingCircle = redCircleView; redCircleView.backgroundColor = [UIColor ows_destructiveRedColor]; redCircleView.layer.cornerRadius = kRedCircleSize * 0.5f; [redCircleView autoSetDimension:ALDimensionWidth toSize:kRedCircleSize]; @@ -593,6 +671,17 @@ const CGFloat kMaxTextViewHeight = 98; cancelLabelStartFrame.origin.x = (CurrentAppContext().isRTL ? -self.voiceMemoUI.bounds.size.width : self.voiceMemoUI.bounds.size.width); cancelLabel.frame = cancelLabelStartFrame; + + voiceMemoLockView.transform = CGAffineTransformMakeScale(0.0, 0.0); + [voiceMemoLockView layoutIfNeeded]; + [UIView animateWithDuration:0.2f + delay:1.f + options:0 + animations:^{ + voiceMemoLockView.transform = CGAffineTransformIdentity; + } + completion:nil]; + [UIView animateWithDuration:0.35f delay:0.f options:UIViewAnimationOptionCurveEaseOut @@ -636,11 +725,19 @@ const CGFloat kMaxTextViewHeight = 98; { OWSAssertIsOnMainThread(); + self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle; + UIView *oldVoiceMemoUI = self.voiceMemoUI; + UIView *oldVoiceMemoLockView = self.voiceMemoLockView; + self.voiceMemoUI = nil; + self.voiceMemoCancelLabel = nil; + self.voiceMemoRedRecordingCircle = nil; self.voiceMemoContentView = nil; + self.voiceMemoLockView = nil; self.recordingLabel = nil; - NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer; + + [self.voiceMemoUpdateTimer invalidate]; self.voiceMemoUpdateTimer = nil; [oldVoiceMemoUI.layer removeAllAnimations]; @@ -649,17 +746,77 @@ const CGFloat kMaxTextViewHeight = 98; [UIView animateWithDuration:0.35f animations:^{ oldVoiceMemoUI.layer.opacity = 0.f; + oldVoiceMemoLockView.layer.opacity = 0.f; } completion:^(BOOL finished) { [oldVoiceMemoUI removeFromSuperview]; - [voiceMemoUpdateTimer invalidate]; + [oldVoiceMemoLockView removeFromSuperview]; }]; } else { [oldVoiceMemoUI removeFromSuperview]; - [voiceMemoUpdateTimer invalidate]; + [oldVoiceMemoLockView removeFromSuperview]; } } +- (void)lockVoiceMemoUI +{ + __weak __typeof(self) weakSelf = self; + + UIButton *sendVoiceMemoButton = [[OWSButton alloc] initWithBlock:^{ + [weakSelf.inputToolbarDelegate voiceMemoGestureDidComplete]; + }]; + [sendVoiceMemoButton setTitle:MessageStrings.sendButton forState:UIControlStateNormal]; + [sendVoiceMemoButton setTitleColor:UIColor.ows_signalBlueColor forState:UIControlStateNormal]; + sendVoiceMemoButton.alpha = 0; + [self.voiceMemoContentView addSubview:sendVoiceMemoButton]; + [sendVoiceMemoButton autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeLeading]; + [sendVoiceMemoButton setCompressionResistanceHigh]; + [sendVoiceMemoButton setContentHuggingHigh]; + + UIButton *cancelButton = [[OWSButton alloc] initWithBlock:^{ + [weakSelf.inputToolbarDelegate voiceMemoGestureDidCancel]; + }]; + [cancelButton setTitle:CommonStrings.cancelButton forState:UIControlStateNormal]; + [cancelButton setTitleColor:UIColor.ows_destructiveRedColor forState:UIControlStateNormal]; + cancelButton.alpha = 0; + cancelButton.titleLabel.textAlignment = NSTextAlignmentCenter; + + [self.voiceMemoContentView addSubview:cancelButton]; + OWSAssert(self.recordingLabel != nil); + [self.recordingLabel setContentHuggingHigh]; + + [NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow + forConstraints:^{ + [cancelButton autoHCenterInSuperview]; + }]; + [cancelButton autoPinEdge:ALEdgeLeading + toEdge:ALEdgeTrailing + ofView:self.recordingLabel + withOffset:4 + relation:NSLayoutRelationGreaterThanOrEqual]; + [cancelButton autoPinEdge:ALEdgeTrailing + toEdge:ALEdgeLeading + ofView:sendVoiceMemoButton + withOffset:-4 + relation:NSLayoutRelationLessThanOrEqual]; + [cancelButton autoVCenterInSuperview]; + + [self.voiceMemoContentView layoutIfNeeded]; + [UIView animateWithDuration:0.35 + animations:^{ + self.voiceMemoCancelLabel.alpha = 0; + self.voiceMemoRedRecordingCircle.alpha = 0; + self.voiceMemoLockView.transform = CGAffineTransformMakeScale(0, 0); + cancelButton.alpha = 1.0; + sendVoiceMemoButton.alpha = 1.0; + } + completion:^(BOOL finished) { + [self.voiceMemoCancelLabel removeFromSuperview]; + [self.voiceMemoRedRecordingCircle removeFromSuperview]; + [self.voiceMemoLockView removeFromSuperview]; + }]; +} + - (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha { OWSAssertIsOnMainThread(); @@ -681,7 +838,7 @@ const CGFloat kMaxTextViewHeight = 98; - (void)cancelVoiceMemoIfNecessary { if (self.isRecordingVoiceMemo) { - self.isRecordingVoiceMemo = NO; + self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle; } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m index e06fe01a5..8b1c203a2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationScrollButton.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "ConversationScrollButton.h" @@ -86,12 +86,9 @@ NS_ASSUME_NONNULL_BEGIN if (self.hasUnreadMessages) { foregroundColor = UIColor.whiteColor; backgroundColor = UIColor.ows_materialBlueColor; - } else if (Theme.isDarkThemeEnabled) { - foregroundColor = UIColor.ows_materialBlueColor; - backgroundColor = [UIColor colorWithWhite:0.25f alpha:1.f]; } else { foregroundColor = UIColor.ows_materialBlueColor; - backgroundColor = [UIColor colorWithWhite:0.95f alpha:1.f]; + backgroundColor = Theme.scrollButtonBackgroundColor; } const CGFloat circleSize = self.class.circleSize; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index a14ddbd68..a40183cd2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4033,17 +4033,25 @@ typedef enum : NSUInteger { [self requestRecordingVoiceMemo]; } -- (void)voiceMemoGestureDidEnd +- (void)voiceMemoGestureDidComplete { OWSAssertIsOnMainThread(); - OWSLogInfo(@"voiceMemoGestureDidEnd"); + OWSLogInfo(@""); [self.inputToolbar hideVoiceMemoUI:YES]; [self endRecordingVoiceMemo]; AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); } +- (void)voiceMemoGestureDidLock +{ + OWSAssertIsOnMainThread(); + OWSLogInfo(@""); + + [self.inputToolbar lockVoiceMemoUI]; +} + - (void)voiceMemoGestureDidCancel { OWSAssertIsOnMainThread(); @@ -4055,7 +4063,7 @@ typedef enum : NSUInteger { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); } -- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha +- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha { OWSAssertIsOnMainThread(); diff --git a/Signal/src/views/VoiceNoteLock.swift b/Signal/src/views/VoiceNoteLock.swift new file mode 100644 index 000000000..16a1210b8 --- /dev/null +++ b/Signal/src/views/VoiceNoteLock.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class VoiceMemoLockView: UIView { + + private var offsetConstraint: NSLayoutConstraint! + + private let offsetFromToolbar: CGFloat = 40 + private let backgroundViewInitialHeight: CGFloat = 80 + private var chevronTravel: CGFloat { + return -1 * (backgroundViewInitialHeight - 50) + } + + @objc + public override init(frame: CGRect) { + super.init(frame: frame) + addSubview(backgroundView) + backgroundView.addSubview(lockIconView) + backgroundView.addSubview(chevronView) + + layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: offsetFromToolbar, trailing: 0) + + backgroundView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom) + self.offsetConstraint = backgroundView.autoPinEdge(toSuperviewMargin: .bottom) + // we anchor the top so that the bottom "slides up" to meet it as the user slides the lock + backgroundView.autoPinEdge(.top, to: .bottom, of: self, withOffset: -offsetFromToolbar - backgroundViewInitialHeight) + + backgroundView.layoutMargins = UIEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6) + + lockIconView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom) + chevronView.autoPinEdges(toSuperviewMarginsExcludingEdge: .top) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - + + @objc + public func update(ratioComplete: CGFloat) { + offsetConstraint.constant = CGFloatLerp(0, chevronTravel, ratioComplete) + } + + // MARK: - Subviews + + private lazy var lockIconView: UIImageView = { + let imageTemplate = #imageLiteral(resourceName: "ic_lock_outline").withRenderingMode(.alwaysTemplate) + let imageView = UIImageView(image: imageTemplate) + imageView.tintColor = .ows_destructiveRed + imageView.autoSetDimensions(to: CGSize(width: 24, height: 24)) + return imageView + }() + + private lazy var chevronView: UIView = { + let label = UILabel() + label.text = "\u{2303}" + label.textColor = .ows_destructiveRed + label.textAlignment = .center + return label + }() + + private lazy var backgroundView: UIView = { + let view = UIView() + + let width: CGFloat = 36 + view.autoSetDimension(.width, toSize: width) + view.backgroundColor = Theme.scrollButtonBackgroundColor + view.layer.cornerRadius = width / 2 + view.layer.borderColor = Theme.offBackgroundColor.cgColor + view.layer.borderWidth = CGHairlineWidth() + + return view + }() + +} diff --git a/SignalMessaging/appearance/Theme.h b/SignalMessaging/appearance/Theme.h index 1f3315350..0d2851232 100644 --- a/SignalMessaging/appearance/Theme.h +++ b/SignalMessaging/appearance/Theme.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "UIColor+OWS.h" @@ -61,6 +61,8 @@ extern NSString *const ThemeDidChangeNotification; @property (class, readonly, nonatomic) UIColor *toastForegroundColor; @property (class, readonly, nonatomic) UIColor *toastBackgroundColor; +@property (class, readonly, nonatomic) UIColor *scrollButtonBackgroundColor; + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/appearance/Theme.m b/SignalMessaging/appearance/Theme.m index 9313f8478..b0d66e3bf 100644 --- a/SignalMessaging/appearance/Theme.m +++ b/SignalMessaging/appearance/Theme.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "Theme.h" @@ -194,6 +194,12 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled"; return (Theme.isDarkThemeEnabled ? UIColor.ows_gray75Color : UIColor.ows_gray60Color); } ++ (UIColor *)scrollButtonBackgroundColor +{ + return Theme.isDarkThemeEnabled ? [UIColor colorWithWhite:0.25f alpha:1.f] + : [UIColor colorWithWhite:0.95f alpha:1.f]; +} + @end NS_ASSUME_NONNULL_END