Voice Note Lock

This commit is contained in:
Michael Kirk 2019-02-05 22:36:03 -07:00
parent a566145d5b
commit d29ce740cb
8 changed files with 293 additions and 33 deletions

View File

@ -437,6 +437,7 @@
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */; }; 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */; };
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.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 */; }; 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 */; }; 4C04F58421C860C50090D0BB /* MantlePerfTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C04F58321C860C50090D0BB /* MantlePerfTest.swift */; };
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; }; 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.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 = "<group>"; }; 45F659811E1BE77000444429 /* NonCallKitCallUIAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonCallKitCallUIAdaptee.swift; sourceTree = "<group>"; };
45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = "<group>"; }; 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = "<group>"; };
45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = "<group>"; }; 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = "<group>"; };
4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceNoteLock.swift; sourceTree = "<group>"; };
4C04F58321C860C50090D0BB /* MantlePerfTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MantlePerfTest.swift; path = Models/MantlePerfTest.swift; sourceTree = "<group>"; }; 4C04F58321C860C50090D0BB /* MantlePerfTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MantlePerfTest.swift; path = Models/MantlePerfTest.swift; sourceTree = "<group>"; };
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticFeedback.swift; path = UserInterface/HapticFeedback.swift; sourceTree = "<group>"; }; 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticFeedback.swift; path = UserInterface/HapticFeedback.swift; sourceTree = "<group>"; };
4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = "<group>"; }; 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = "<group>"; };
@ -2330,6 +2332,7 @@
450D19111F85236600970622 /* RemoteVideoView.h */, 450D19111F85236600970622 /* RemoteVideoView.h */,
450D19121F85236600970622 /* RemoteVideoView.m */, 450D19121F85236600970622 /* RemoteVideoView.m */,
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */, 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */,
); );
name = Views; name = Views;
path = views; path = views;
@ -3394,6 +3397,7 @@
34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */, 34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */,
34DC9BD921543E0C00FDDCEC /* DebugContactsUtils.m in Sources */, 34DC9BD921543E0C00FDDCEC /* DebugContactsUtils.m in Sources */,
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */, 34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */,
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */, 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */, 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */, 3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */,

View File

@ -19,11 +19,13 @@ NS_ASSUME_NONNULL_BEGIN
- (void)voiceMemoGestureDidStart; - (void)voiceMemoGestureDidStart;
- (void)voiceMemoGestureDidEnd; - (void)voiceMemoGestureDidLock;
- (void)voiceMemoGestureDidComplete;
- (void)voiceMemoGestureDidCancel; - (void)voiceMemoGestureDidCancel;
- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha; - (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
@end @end
@ -55,10 +57,12 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateFontSizes; - (void)updateFontSizes;
- (void)updateLayoutWithSafeAreaInsets:(UIEdgeInsets)safeAreaInsets; - (void)updateLayoutWithSafeAreaInsets:(UIEdgeInsets)safeAreaInsets;
- (void)ensureTextViewHeight;
#pragma mark - Voice Memo #pragma mark - Voice Memo
- (void)ensureTextViewHeight; - (void)lockVoiceMemoUI;
- (void)showVoiceMemoUI; - (void)showVoiceMemoUI;
- (void)hideVoiceMemoUI:(BOOL)animated; - (void)hideVoiceMemoUI:(BOOL)animated;
@ -67,6 +71,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)cancelVoiceMemoIfNecessary; - (void)cancelVoiceMemoIfNecessary;
#pragma mark -
@property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply; @property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply;
@property (nonatomic, nullable, readonly) OWSLinkPreviewDraft *linkPreviewDraft; @property (nonatomic, nullable, readonly) OWSLinkPreviewDraft *linkPreviewDraft;

View File

@ -20,6 +20,12 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
typedef NS_CLOSED_ENUM(NSUInteger, VoiceMemoRecordingState){
VoiceMemoRecordingState_Idle,
VoiceMemoRecordingState_RecordingHeld,
VoiceMemoRecordingState_RecordingLocked
};
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext; static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
const CGFloat kMinTextViewHeight = 36; const CGFloat kMinTextViewHeight = 36;
@ -62,11 +68,16 @@ const CGFloat kMaxTextViewHeight = 98;
#pragma mark - Voice Memo Recording UI #pragma mark - Voice Memo Recording UI
@property (nonatomic, nullable) UIView *voiceMemoUI; @property (nonatomic, nullable) UIView *voiceMemoUI;
@property (nonatomic, nullable) VoiceMemoLockView *voiceMemoLockView;
@property (nonatomic, nullable) UIView *voiceMemoContentView; @property (nonatomic, nullable) UIView *voiceMemoContentView;
@property (nonatomic) NSDate *voiceMemoStartTime; @property (nonatomic) NSDate *voiceMemoStartTime;
@property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer; @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, nullable) UILabel *recordingLabel;
@property (nonatomic) BOOL isRecordingVoiceMemo; @property (nonatomic, readonly) BOOL isRecordingVoiceMemo;
@property (nonatomic) VoiceMemoRecordingState voiceMemoRecordingState;
@property (nonatomic) CGPoint voiceMemoGestureStartLocation; @property (nonatomic) CGPoint voiceMemoGestureStartLocation;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *layoutContraints; @property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *layoutContraints;
@property (nonatomic) UIEdgeInsets receivedSafeAreaInsets; @property (nonatomic) UIEdgeInsets receivedSafeAreaInsets;
@ -164,6 +175,7 @@ const CGFloat kMaxTextViewHeight = 98;
UILongPressGestureRecognizer *longPressGestureRecognizer = UILongPressGestureRecognizer *longPressGestureRecognizer =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPressGestureRecognizer.minimumPressDuration = 0; longPressGestureRecognizer.minimumPressDuration = 0;
self.voiceMemoGestureRecognizer = longPressGestureRecognizer;
[self.voiceMemoButton addGestureRecognizer:longPressGestureRecognizer]; [self.voiceMemoButton addGestureRecognizer:longPressGestureRecognizer];
self.userInteractionEnabled = YES; self.userInteractionEnabled = YES;
@ -435,18 +447,25 @@ const CGFloat kMaxTextViewHeight = 98;
case UIGestureRecognizerStateFailed: case UIGestureRecognizerStateFailed:
if (self.isRecordingVoiceMemo) { if (self.isRecordingVoiceMemo) {
// Cancel voice message if necessary. // Cancel voice message if necessary.
self.isRecordingVoiceMemo = NO; self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
[self.inputToolbarDelegate voiceMemoGestureDidCancel]; [self.inputToolbarDelegate voiceMemoGestureDidCancel];
} }
break; break;
case UIGestureRecognizerStateBegan: case UIGestureRecognizerStateBegan:
if (self.isRecordingVoiceMemo) { switch (self.voiceMemoRecordingState) {
// Cancel voice message if necessary. case VoiceMemoRecordingState_Idle:
self.isRecordingVoiceMemo = NO; break;
[self.inputToolbarDelegate voiceMemoGestureDidCancel]; 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. // Start voice message.
self.isRecordingVoiceMemo = YES; self.voiceMemoRecordingState = VoiceMemoRecordingState_RecordingHeld;
self.voiceMemoGestureStartLocation = [sender locationInView:self]; self.voiceMemoGestureStartLocation = [sender locationInView:self];
[self.inputToolbarDelegate voiceMemoGestureDidStart]; [self.inputToolbarDelegate voiceMemoGestureDidStart];
break; break;
@ -457,25 +476,62 @@ const CGFloat kMaxTextViewHeight = 98;
// For LTR/RTL, swiping in either direction will cancel. // For LTR/RTL, swiping in either direction will cancel.
// This is okay because there's only space on screen to perform the // This is okay because there's only space on screen to perform the
// gesture in one direction. // 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 lower this value, the easier it is to cancel by accident.
// The higher this value, the harder it is to cancel. // The higher this value, the harder it is to cancel.
const CGFloat kCancelOffsetPoints = 100.f; const CGFloat kCancelOffsetPoints = 100.f;
CGFloat cancelAlpha = offset / kCancelOffsetPoints; CGFloat cancelAlpha = xOffset / kCancelOffsetPoints;
BOOL isCancelled = cancelAlpha >= 1.f; BOOL isCancelled = cancelAlpha >= 1.f;
if (isCancelled) { if (isCancelled) {
self.isRecordingVoiceMemo = NO; self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
[self.inputToolbarDelegate voiceMemoGestureDidCancel]; [self.inputToolbarDelegate voiceMemoGestureDidCancel];
break;
} else { } 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; break;
case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateEnded:
if (self.isRecordingVoiceMemo) { switch (self.voiceMemoRecordingState) {
// End voice message. case VoiceMemoRecordingState_Idle:
self.isRecordingVoiceMemo = NO; break;
[self.inputToolbarDelegate voiceMemoGestureDidEnd]; case VoiceMemoRecordingState_RecordingHeld:
// End voice message.
self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
[self.inputToolbarDelegate voiceMemoGestureDidComplete];
break;
case VoiceMemoRecordingState_RecordingLocked:
// Continue recording.
break;
} }
break; break;
} }
@ -483,6 +539,17 @@ const CGFloat kMaxTextViewHeight = 98;
#pragma mark - Voice Memo #pragma mark - Voice Memo
- (BOOL)isRecordingVoiceMemo
{
switch (self.voiceMemoRecordingState) {
case VoiceMemoRecordingState_Idle:
return NO;
case VoiceMemoRecordingState_RecordingHeld:
case VoiceMemoRecordingState_RecordingLocked:
return YES;
}
}
- (void)showVoiceMemoUI - (void)showVoiceMemoUI
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
@ -490,9 +557,9 @@ const CGFloat kMaxTextViewHeight = 98;
self.voiceMemoStartTime = [NSDate date]; self.voiceMemoStartTime = [NSDate date];
[self.voiceMemoUI removeFromSuperview]; [self.voiceMemoUI removeFromSuperview];
[self.voiceMemoLockView removeFromSuperview];
self.voiceMemoUI = [UIView new]; self.voiceMemoUI = [UIView new];
self.voiceMemoUI.userInteractionEnabled = NO;
self.voiceMemoUI.backgroundColor = Theme.toolbarBackgroundColor; self.voiceMemoUI.backgroundColor = Theme.toolbarBackgroundColor;
[self addSubview:self.voiceMemoUI]; [self addSubview:self.voiceMemoUI];
self.voiceMemoUI.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height); 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.textColor = [UIColor ows_destructiveRedColor];
self.recordingLabel.font = [UIFont ows_mediumFontWithSize:14.f]; self.recordingLabel.font = [UIFont ows_mediumFontWithSize:14.f];
[self.voiceMemoContentView addSubview:self.recordingLabel]; [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]; [self updateVoiceMemo];
UIImage *icon = [UIImage imageNamed:@"voice-memo-button"]; UIImage *icon = [UIImage imageNamed:@"voice-memo-button"];
@ -512,6 +587,7 @@ const CGFloat kMaxTextViewHeight = 98;
UIImageView *imageView = UIImageView *imageView =
[[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; [[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
imageView.tintColor = [UIColor ows_destructiveRedColor]; imageView.tintColor = [UIColor ows_destructiveRedColor];
[imageView setContentHuggingHigh];
[self.voiceMemoContentView addSubview:imageView]; [self.voiceMemoContentView addSubview:imageView];
NSMutableAttributedString *cancelString = [NSMutableAttributedString new]; NSMutableAttributedString *cancelString = [NSMutableAttributedString new];
@ -559,11 +635,13 @@ const CGFloat kMaxTextViewHeight = 98;
NSBaselineOffsetAttributeName : @(-1.f), NSBaselineOffsetAttributeName : @(-1.f),
}]]; }]];
UILabel *cancelLabel = [UILabel new]; UILabel *cancelLabel = [UILabel new];
self.voiceMemoCancelLabel = cancelLabel;
cancelLabel.attributedText = cancelString; cancelLabel.attributedText = cancelString;
[self.voiceMemoContentView addSubview:cancelLabel]; [self.voiceMemoContentView addSubview:cancelLabel];
const CGFloat kRedCircleSize = 100.f; const CGFloat kRedCircleSize = 100.f;
UIView *redCircleView = [UIView new]; UIView *redCircleView = [UIView new];
self.voiceMemoRedRecordingCircle = redCircleView;
redCircleView.backgroundColor = [UIColor ows_destructiveRedColor]; redCircleView.backgroundColor = [UIColor ows_destructiveRedColor];
redCircleView.layer.cornerRadius = kRedCircleSize * 0.5f; redCircleView.layer.cornerRadius = kRedCircleSize * 0.5f;
[redCircleView autoSetDimension:ALDimensionWidth toSize:kRedCircleSize]; [redCircleView autoSetDimension:ALDimensionWidth toSize:kRedCircleSize];
@ -593,6 +671,17 @@ const CGFloat kMaxTextViewHeight = 98;
cancelLabelStartFrame.origin.x cancelLabelStartFrame.origin.x
= (CurrentAppContext().isRTL ? -self.voiceMemoUI.bounds.size.width : self.voiceMemoUI.bounds.size.width); = (CurrentAppContext().isRTL ? -self.voiceMemoUI.bounds.size.width : self.voiceMemoUI.bounds.size.width);
cancelLabel.frame = cancelLabelStartFrame; 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 [UIView animateWithDuration:0.35f
delay:0.f delay:0.f
options:UIViewAnimationOptionCurveEaseOut options:UIViewAnimationOptionCurveEaseOut
@ -636,11 +725,19 @@ const CGFloat kMaxTextViewHeight = 98;
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
UIView *oldVoiceMemoUI = self.voiceMemoUI; UIView *oldVoiceMemoUI = self.voiceMemoUI;
UIView *oldVoiceMemoLockView = self.voiceMemoLockView;
self.voiceMemoUI = nil; self.voiceMemoUI = nil;
self.voiceMemoCancelLabel = nil;
self.voiceMemoRedRecordingCircle = nil;
self.voiceMemoContentView = nil; self.voiceMemoContentView = nil;
self.voiceMemoLockView = nil;
self.recordingLabel = nil; self.recordingLabel = nil;
NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer;
[self.voiceMemoUpdateTimer invalidate];
self.voiceMemoUpdateTimer = nil; self.voiceMemoUpdateTimer = nil;
[oldVoiceMemoUI.layer removeAllAnimations]; [oldVoiceMemoUI.layer removeAllAnimations];
@ -649,17 +746,77 @@ const CGFloat kMaxTextViewHeight = 98;
[UIView animateWithDuration:0.35f [UIView animateWithDuration:0.35f
animations:^{ animations:^{
oldVoiceMemoUI.layer.opacity = 0.f; oldVoiceMemoUI.layer.opacity = 0.f;
oldVoiceMemoLockView.layer.opacity = 0.f;
} }
completion:^(BOOL finished) { completion:^(BOOL finished) {
[oldVoiceMemoUI removeFromSuperview]; [oldVoiceMemoUI removeFromSuperview];
[voiceMemoUpdateTimer invalidate]; [oldVoiceMemoLockView removeFromSuperview];
}]; }];
} else { } else {
[oldVoiceMemoUI removeFromSuperview]; [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 - (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
@ -681,7 +838,7 @@ const CGFloat kMaxTextViewHeight = 98;
- (void)cancelVoiceMemoIfNecessary - (void)cancelVoiceMemoIfNecessary
{ {
if (self.isRecordingVoiceMemo) { if (self.isRecordingVoiceMemo) {
self.isRecordingVoiceMemo = NO; self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle;
} }
} }

View File

@ -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" #import "ConversationScrollButton.h"
@ -86,12 +86,9 @@ NS_ASSUME_NONNULL_BEGIN
if (self.hasUnreadMessages) { if (self.hasUnreadMessages) {
foregroundColor = UIColor.whiteColor; foregroundColor = UIColor.whiteColor;
backgroundColor = UIColor.ows_materialBlueColor; backgroundColor = UIColor.ows_materialBlueColor;
} else if (Theme.isDarkThemeEnabled) {
foregroundColor = UIColor.ows_materialBlueColor;
backgroundColor = [UIColor colorWithWhite:0.25f alpha:1.f];
} else { } else {
foregroundColor = UIColor.ows_materialBlueColor; foregroundColor = UIColor.ows_materialBlueColor;
backgroundColor = [UIColor colorWithWhite:0.95f alpha:1.f]; backgroundColor = Theme.scrollButtonBackgroundColor;
} }
const CGFloat circleSize = self.class.circleSize; const CGFloat circleSize = self.class.circleSize;

View File

@ -4033,17 +4033,25 @@ typedef enum : NSUInteger {
[self requestRecordingVoiceMemo]; [self requestRecordingVoiceMemo];
} }
- (void)voiceMemoGestureDidEnd - (void)voiceMemoGestureDidComplete
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
OWSLogInfo(@"voiceMemoGestureDidEnd"); OWSLogInfo(@"");
[self.inputToolbar hideVoiceMemoUI:YES]; [self.inputToolbar hideVoiceMemoUI:YES];
[self endRecordingVoiceMemo]; [self endRecordingVoiceMemo];
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
} }
- (void)voiceMemoGestureDidLock
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
[self.inputToolbar lockVoiceMemoUI];
}
- (void)voiceMemoGestureDidCancel - (void)voiceMemoGestureDidCancel
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();
@ -4055,7 +4063,7 @@ typedef enum : NSUInteger {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
} }
- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha - (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha
{ {
OWSAssertIsOnMainThread(); OWSAssertIsOnMainThread();

View File

@ -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
}()
}

View File

@ -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" #import "UIColor+OWS.h"
@ -61,6 +61,8 @@ extern NSString *const ThemeDidChangeNotification;
@property (class, readonly, nonatomic) UIColor *toastForegroundColor; @property (class, readonly, nonatomic) UIColor *toastForegroundColor;
@property (class, readonly, nonatomic) UIColor *toastBackgroundColor; @property (class, readonly, nonatomic) UIColor *toastBackgroundColor;
@property (class, readonly, nonatomic) UIColor *scrollButtonBackgroundColor;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View File

@ -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" #import "Theme.h"
@ -194,6 +194,12 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
return (Theme.isDarkThemeEnabled ? UIColor.ows_gray75Color : UIColor.ows_gray60Color); 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 @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END