From 45c8695ab4b4e69d0b6558be9ced7d1834d0974a Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Thu, 4 May 2017 21:21:27 -0400 Subject: [PATCH] Sketch out the voice memo UI. // FREEBIE --- .../voice-memo-button.imageset/Contents.json | 23 + .../voice-memo-button-25.png | Bin 0 -> 1496 bytes .../voice-memo-button-50.png | Bin 0 -> 1766 bytes .../voice-memo-button-75.png | Bin 0 -> 2124 bytes .../ViewControllers/MessagesViewController.m | 464 +++++++++++++++++- .../translations/en.lproj/Localizable.strings | 3 + 6 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 Signal/Images.xcassets/voice-memo-button.imageset/Contents.json create mode 100644 Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-25.png create mode 100644 Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-50.png create mode 100644 Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-75.png diff --git a/Signal/Images.xcassets/voice-memo-button.imageset/Contents.json b/Signal/Images.xcassets/voice-memo-button.imageset/Contents.json new file mode 100644 index 000000000..960c724fb --- /dev/null +++ b/Signal/Images.xcassets/voice-memo-button.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "voice-memo-button-25.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "voice-memo-button-50.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "voice-memo-button-75.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-25.png b/Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-25.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7221dc46a1d2359309dbed2eb287c137656841 GIT binary patch literal 1496 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIEa{HEjtmSN`?>!lvI6;>1s;*b z3=De8Ak0{?)V>U;MkO;Oq9nrC$0|8LS1&OoKPgqOBDVmjnt{Ql!V1XDO)W`OsL0L9 zE4HezRRXK90<uBE`br95B_-LmN)f&R3eNdOsS2igCVB=+c3cVy3N}S4X;wilZcyz& zo~=?wNlAf~zJ7Umxn8-kUVc%!zM-Y1rM`iYzLAkGP=#)BWnM{Qg>GK4GQ<#=IWDQi z$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;|P#+|tZ>VRWk4;-@MJ5hy zAQ_z6Qj+1mDkv?=0sAQ>SwA%=H8(Y{q!_5r5UX{-u!U<xab!he0mL$JAVU0R19G`l zP-=00PAMn|Lh~|9?2K%{9zqvJga|~JbAE0?QEG89P@A18R12~gy7~x&HFm}Z22dTy zqUbvOi!y;;O$0g2&Jd~|Sqxo$Bvu=cMUixX0^ceivm!Mo!X*<Jq+ov-0X>3M0$rnX zeolT-a6w{nD#&GEEl5J>s=?Mo;<5%w0!d>^vQ=?uQdVkm2~vCkV-1}8of6aak%VpZ zv8uKKrUfhC{FKbJN|(fvR68RBLvt$wQ!4`#1qj2$#6}-Y4U!8$GR{S*i6!|(A^G_^ zc3@xRg18FCdS(!v$nsFFHu|7^gp{u!nHDSx%(Zr0K%KA>z>e#quJL6C21YMW7sn6} z-nUZ>y__9IT-CLd4ju~HAjs`_f-6VYg<}WXLABQc(b0}9*Au2{^|o3I9AsUwl!db^ z!zTQ`$^7SMg7?Mh#iTvkYyI8t+5h=d(ta$LzH#LHsTGy4Up)N5u-xrzXtrqjj;9Sa ztHch)cuc+2w)k^apU%>Z4!`>?;n`XHu3Z%He}46!$fnclRA0=EGX1(lb5YqW<+Ak$ z%8$E(d19YV<-HG22?oiC8RhS`?(<%k+N=Ci@2b<g2`8VNEe!Cr&Foq+H)-Ns?hl;b zr2W*_h<@S7m)aYzdp$tjxpnTLje*5iuS{OnQL|q4qTN|tjmPUa)q9vLxb@e5dn~|t z@tN!n_W4^GP8V>vM_m!%o!B2YiP?Bh!k-P5r5BugotAx9<74-b=Z$n-y??@=3D;-t xci0;l>azZ|%<|kBUw>U%|4?zx!n@KxSQ>3izApXoZ91r|^>p=fS?83{1OPYX`gQ;S literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-50.png b/Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-50.png new file mode 100644 index 0000000000000000000000000000000000000000..c04acfac7680b9107f6f0e47bc79c70094595078 GIT binary patch literal 1766 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-mUKs7M+SzC{oH>NS%G}c0*}aI z1_nK45N51cYF`FaqmmgCQ4-<nW0jnrtCyIPpOmUsky`*%&A?z&VFhI7rj{fsROII5 z6<bx<DuGp40ofp7eI*63l9Fs&r3l{u1?T*tR0UH#6FmbZJ1zwU1)HLjG^-#NH>h?X z&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6GOr}DLN~8i8Da>`9GBGM z<f6=ilFa-(1(3OkN%^HEwn|DMi{Jp_&XUYDgiU$HP;UV}s1K6SH`FuG$EGc{A`^!; zkPJ?3DamkO6_gg`fc=z`te={bnwy$eQVi5*h}Ak^*upiUII<$K0Ad+95FviE0lC~N zD7830rxX+fp?R4lc1AW}51|VqLIk4AIX}0cD7Cm4sLjq4ss&jLU3~<?8arbH1E>yU zQFI;tMVUaaCW0JhX9!h~EQYQ=5~~f!qDVSGfo~O%S&^C(;gSgqQiwl*&cZ5zuF*L^ zC%-7TATc==<T9`pBq4OwU~3|AS%V~jq%kGgs<<>MD>b<UDZYTQ22TA>iRt=C!Z!L? zRoej5f|YN6N@iN6OJYf?osof|xs`#bm4S%@gkb`VH#9X!E&$0m7o{ea<QIkH=jYgg zeUS^|Dj4gTL3AR^L$%uIgYpqlzJg?0uqZIs+HnDO!b$);uA@uR7cej|MR>Y6hGek5 zoxa;!)=}c<dj2gOZql8xDorexs#p)!I|Q-oym9hyICAXTsbg0%laj?bA|h@^a4VUK zajpz`!<5-{BcrFP?j2uwM8vmubMxOjKRa-9=G?#cjIB3EuXa7c1%WD}GeR~i-dnQ7 zUh|tsnCM&ik9N+-Dy|7f1W&oivg!MY64N;CUD}4y72o6}Q_fz<Pt!EqvgBE$u-Qb% zYdqIFuXwwJxox|!rc_C}RZf~IHAOng()P7W(VY-`8R3=tjy<)BIDf){^LuIv_t_}* zpcfmo8ZWq{iQL%YSa9P&*zp3j^S9@HSN^(rdBr{3O?*J1(;RarT35C$<k%)W%Ois+ z?U$k8<{&A_F5^jJmo^DFZflw{^~_F-Cu{lsouA#R-_4t|wdaCM+0&m>pZ`7bTD-J2 zGAChm@&l89x_!$9&#d~Z{!F$hPew9@ao3F(FHWAD^g~gvn{WQrLdNG?PgpGI=B@AC zm7be>|MczFA2wgDR+I%lkv*YyNaFki#up2^dlkO0W~!fJ*{c3eRNS?Rz4(#!M_%(o z{)O%~PuvcCFE8||(Z1Iudz{<#z^5N-eP)8ZMkf|E=Y=OaU3}BM{|LX5exX>;M;^(P zi-wIyFMVQuxc>d(9ZH-2{WN(l%b@SN?WXs~?j5TC#C;Ryc5r7!%&j`*|MEfj*5jJ% zFK?We<-rifwVgNY<q}!DtcCM6Ld6zLd8<Cz!+P0XUG}+O*KJ+*)j)OE<+)D{&%0W9 z%sQ@J=YNzpUuQ+Ot#Zw@4R3!vXTBevpqMGQevWz0QNfK&5V$wCk)8Q)?bGGb(p{jM N$J5o%Wt~$(69CSDb-Dlm literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-75.png b/Signal/Images.xcassets/voice-memo-button.imageset/voice-memo-button-75.png new file mode 100644 index 0000000000000000000000000000000000000000..775e470fdbfb39559fd531e8ded4cf624a2b234e GIT binary patch literal 2124 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 Iol`;+07zOOQvd(} literal 0 HcmV?d00001 diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index 0c673705e..53b67f86d 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -12,6 +12,7 @@ #import "FingerprintViewController.h" #import "FullImageViewController.h" #import "NSDate+millisecondTimeStamp.h" +#import "NSTimer+OWS.h" #import "NewGroupViewController.h" #import "OWSAudioAttachmentPlayer.h" #import "OWSCall.h" @@ -96,20 +97,322 @@ typedef enum : NSUInteger { - (void)didPasteAttachment:(SignalAttachment * _Nullable)attachment; +- (void)didStartVoiceMemo; + +- (void)didEndVoiceMemo; + +- (void)didCancelVoiceMemo; + @end #pragma mark - -@interface OWSMessagesComposerTextView () +@interface OWSMessagesComposerTextView () <UITextViewDelegate> @property (weak, nonatomic) id<OWSTextViewPasteDelegate> textViewPasteDelegate; +@property (nonatomic) BOOL shouldShowVoiceMemoButton; + +@property (nonatomic) UIView *voiceMemoButton; + +// This view serves as its own delegate but also needs to forward delegate events +// to JSQ. +@property (weak, nonatomic) id<UITextViewDelegate> jsqDelegate; + +@property (nonatomic) BOOL isRecordingVoiceMemo; + @end #pragma mark - @implementation OWSMessagesComposerTextView +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + [self commonInit]; + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + if (!self) { + return self; + } + + [self commonInit]; + + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (!self) { + return self; + } + + [self commonInit]; + + return self; +} + +- (void)commonInit +{ + self.delegate = self; + [self ensureShouldShowVoiceMemoButton]; +} + +- (void)setDelegate:(id<UITextViewDelegate>)delegate +{ + if (delegate == self) { + [super setDelegate:delegate]; + } else { + self.jsqDelegate = delegate; + } +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textViewShouldBeginEditing:(UITextView *)textView +{ + if ([self.jsqDelegate respondsToSelector:@selector(textViewShouldBeginEditing:)]) { + return [self.jsqDelegate textViewShouldBeginEditing:textView]; + } + return YES; +} + +- (BOOL)textViewShouldEndEditing:(UITextView *)textView +{ + if ([self.jsqDelegate respondsToSelector:@selector(textViewShouldEndEditing:)]) { + return [self.jsqDelegate textViewShouldEndEditing:textView]; + } + return YES; +} + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + if ([self.jsqDelegate respondsToSelector:@selector(textViewDidBeginEditing:)]) { + [self.jsqDelegate textViewDidBeginEditing:textView]; + } +} + +- (void)textViewDidEndEditing:(UITextView *)textView +{ + if ([self.jsqDelegate respondsToSelector:@selector(textViewDidEndEditing:)]) { + [self.jsqDelegate textViewDidEndEditing:textView]; + } +} + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + if ([self.jsqDelegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { + return [self.jsqDelegate textView:textView shouldChangeTextInRange:range replacementText:text]; + } + return YES; +} + +- (void)textViewDidChange:(UITextView *)textView +{ + if ([self.jsqDelegate respondsToSelector:@selector(textViewDidChange:)]) { + [self.jsqDelegate textViewDidChange:textView]; + } + + [self ensureShouldShowVoiceMemoButton]; +} + +- (void)textViewDidChangeSelection:(UITextView *)textView +{ + if ([self.jsqDelegate respondsToSelector:@selector(textViewDidChangeSelection:)]) { + [self.jsqDelegate textViewDidChangeSelection:textView]; + } +} + +- (BOOL)textView:(UITextView *)textView + shouldInteractWithURL:(NSURL *)URL + inRange:(NSRange)characterRange + interaction:(UITextItemInteraction)interaction +{ + if ([self.jsqDelegate respondsToSelector:@selector(textView:shouldInteractWithURL:inRange:interaction:)]) { + return [self.jsqDelegate textView:textView + shouldInteractWithURL:URL + inRange:characterRange + interaction:interaction]; + } + return YES; +} + +- (BOOL)textView:(UITextView *)textView + shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment + inRange:(NSRange)characterRange + interaction:(UITextItemInteraction)interaction +{ + if ([self.jsqDelegate + respondsToSelector:@selector(textView:shouldInteractWithTextAttachment:inRange:interaction:)]) { + return [self.jsqDelegate textView:textView + shouldInteractWithTextAttachment:textAttachment + inRange:characterRange + interaction:interaction]; + } + return YES; +} + +- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange +{ + if ([self.jsqDelegate respondsToSelector:@selector(textView:shouldInteractWithURL:inRange:)]) { + return [self.jsqDelegate textView:textView shouldInteractWithURL:URL inRange:characterRange]; + } + return YES; +} + +- (BOOL)textView:(UITextView *)textView + shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment + inRange:(NSRange)characterRange +{ + if ([self.jsqDelegate respondsToSelector:@selector(textView:shouldInteractWithTextAttachment:inRange:)]) { + return + [self.jsqDelegate textView:textView shouldInteractWithTextAttachment:textAttachment inRange:characterRange]; + } + return YES; +} + +- (void)ensureShouldShowVoiceMemoButton +{ + self.shouldShowVoiceMemoButton = self.text.length < 1; +} + +- (void)setShouldShowVoiceMemoButton:(BOOL)shouldShowVoiceMemoButton +{ + if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) { + return; + } + + _shouldShowVoiceMemoButton = shouldShowVoiceMemoButton; + + [self ensureVoiceMemoButton]; +} + +- (CGFloat)voiceMemoButtonSize +{ + return 25; +} + +- (void)ensureVoiceMemoButton +{ + if (!self.superview) { + return; + } + + if (self.shouldShowVoiceMemoButton) { + [self.voiceMemoButton removeFromSuperview]; + self.voiceMemoButton = nil; + + UIView *button = [UIView new]; + button.frame = CGRectMake(0, 0, self.voiceMemoButtonSize, self.voiceMemoButtonSize); + [button addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(handleLongPress:)]]; + button.userInteractionEnabled = YES; + + UIImage *icon = [UIImage imageNamed:@"voice-memo-button"]; + OWSAssert(icon); + UIImageView *imageView = [[UIImageView alloc] initWithImage:icon]; + imageView.layer.opacity = 0.8f; + [button addSubview:imageView]; + + self.voiceMemoButton = button; + [self addSubview:button]; + [self layoutVoiceMemoButton]; + } else { + [self.voiceMemoButton removeFromSuperview]; + self.voiceMemoButton = nil; + } +} + +- (void)ensureSubviews +{ + [self ensureVoiceMemoButton]; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + [self layoutVoiceMemoButton]; +} + +- (void)setBounds:(CGRect)bounds +{ + [super setBounds:bounds]; + + [self layoutVoiceMemoButton]; +} + +- (void)setCenter:(CGPoint)center +{ + [super setCenter:center]; + + [self layoutVoiceMemoButton]; +} + +- (void)layoutVoiceMemoButton +{ + if (!self.voiceMemoButton) { + return; + } + CGRect buttonFrame = CGRectMake(floor(self.frame.size.width - (self.voiceMemoButtonSize + 5)), + floor((self.frame.size.height - self.voiceMemoButtonSize) * 0.5f), + self.voiceMemoButtonSize, + self.voiceMemoButtonSize); + buttonFrame = [self.voiceMemoButton.superview convertRect:buttonFrame fromView:self]; + self.voiceMemoButton.frame = buttonFrame; + [self.voiceMemoButton.superview bringSubviewToFront:self.voiceMemoButton]; +} + +- (void)handleLongPress:(UIGestureRecognizer *)sender +{ + switch (sender.state) { + case UIGestureRecognizerStatePossible: + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateFailed: + if (self.isRecordingVoiceMemo) { + self.isRecordingVoiceMemo = NO; + [self.textViewPasteDelegate didCancelVoiceMemo]; + } + break; + case UIGestureRecognizerStateBegan: + if (self.isRecordingVoiceMemo) { + self.isRecordingVoiceMemo = NO; + [self.textViewPasteDelegate didCancelVoiceMemo]; + } + self.isRecordingVoiceMemo = YES; + [self.textViewPasteDelegate didStartVoiceMemo]; + break; + case UIGestureRecognizerStateChanged: + // TODO: + break; + case UIGestureRecognizerStateEnded: + if (self.isRecordingVoiceMemo) { + self.isRecordingVoiceMemo = NO; + [self.textViewPasteDelegate didEndVoiceMemo]; + } + break; + } +} + +- (void)cancelVoiceMemoIfNecessary +{ + if (self.isRecordingVoiceMemo) { + self.isRecordingVoiceMemo = NO; + [self.textViewPasteDelegate didCancelVoiceMemo]; + } +} + - (BOOL)canBecomeFirstResponder { return YES; } @@ -159,6 +462,20 @@ typedef enum : NSUInteger { #pragma mark - +@interface OWSMessagesInputToolbar () + +@property (nonatomic) UIView *voiceMemoUI; + +@property (nonatomic) NSDate *voiceMemoStartTime; + +@property (nonatomic) NSTimer *voiceMemoUpdateTimer; + +@property (nonatomic) UILabel *recordingLabel; + +@end + +#pragma mark - + @implementation OWSMessagesInputToolbar - (JSQMessagesToolbarContentView *)loadToolbarContentView { @@ -170,6 +487,126 @@ typedef enum : NSUInteger { return view; } +- (CGFloat)voiceMemoButtonSize +{ + return 25; +} + +- (void)showVoiceMemoUI +{ + self.voiceMemoStartTime = [NSDate date]; + + [self.voiceMemoUI removeFromSuperview]; + + self.voiceMemoUI = [UIView new]; + self.voiceMemoUI.userInteractionEnabled = NO; + self.voiceMemoUI.backgroundColor = [UIColor whiteColor]; + [self addSubview:self.voiceMemoUI]; + self.voiceMemoUI.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height); + + self.recordingLabel = [UILabel new]; + self.recordingLabel.textColor = [UIColor ows_materialBlueColor]; + self.recordingLabel.font = [UIFont ows_mediumFontWithSize:14.f]; + [self.voiceMemoUI addSubview:self.recordingLabel]; + [self updateVoiceMemo]; + + UIImage *icon = [UIImage imageNamed:@"voice-memo-button"]; + OWSAssert(icon); + UIImageView *imageView = + [[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; + imageView.tintColor = [UIColor ows_materialBlueColor]; + // imageView.layer.opacity = 0.8f; + [self.voiceMemoUI addSubview:imageView]; + + UILabel *cancelLabel = [UILabel new]; + cancelLabel.textColor = [UIColor ows_destructiveRedColor]; + cancelLabel.font = [UIFont ows_mediumFontWithSize:14.f]; + cancelLabel.text = NSLocalizedString(@"VOICE_MEMO_CANCEL_INSTRUCTIONS", @"Indicates how to cancel a voice memo."); + [self.voiceMemoUI addSubview:cancelLabel]; + + [imageView autoVCenterInSuperview]; + [imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:10]; + [self.recordingLabel autoVCenterInSuperview]; + [self.recordingLabel autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:imageView withOffset:5.f]; + [cancelLabel autoVCenterInSuperview]; + [cancelLabel autoHCenterInSuperview]; + [self.voiceMemoUI setNeedsLayout]; + [self.voiceMemoUI layoutSubviews]; + + // Slide in the "slide to cancel" label. + CGRect cancelLabelStartFrame = cancelLabel.frame; + CGRect cancelLabelEndFrame = cancelLabel.frame; + cancelLabelStartFrame.origin.x = self.voiceMemoUI.bounds.size.width; + cancelLabel.frame = cancelLabelStartFrame; + [UIView animateWithDuration:0.35f + delay:0.f + options:UIViewAnimationOptionCurveEaseOut + animations:^{ + cancelLabel.frame = cancelLabelEndFrame; + } + completion:nil]; + + // Pulse the icon. + imageView.layer.opacity = 1.f; + [UIView animateWithDuration:0.5f + delay:0.2f + options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse + | UIViewAnimationOptionCurveEaseIn + animations:^{ + imageView.layer.opacity = 0.f; + } + completion:nil]; + + // Fade in the view. + self.voiceMemoUI.layer.opacity = 0.f; + [UIView animateWithDuration:0.2f + animations:^{ + self.voiceMemoUI.layer.opacity = 1.f; + } + completion:^(BOOL finished) { + if (finished) { + self.voiceMemoUI.layer.opacity = 1.f; + } + }]; + + self.voiceMemoUpdateTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.1f + target:self + selector:@selector(updateVoiceMemo) + userInfo:nil + repeats:YES]; +} + +- (void)hideVoiceMemoUI:(BOOL)animated +{ + UIView *voiceMemoUI = self.voiceMemoUI; + self.voiceMemoUI = nil; + NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer; + self.voiceMemoUpdateTimer = nil; + + [self.voiceMemoUI.layer removeAllAnimations]; + + if (animated) { + [UIView animateWithDuration:0.35f + animations:^{ + voiceMemoUI.layer.opacity = 0.f; + } + completion:^(BOOL finished) { + [voiceMemoUI removeFromSuperview]; + [voiceMemoUpdateTimer invalidate]; + }]; + } else { + [voiceMemoUI removeFromSuperview]; + [voiceMemoUpdateTimer invalidate]; + } +} + +- (void)updateVoiceMemo +{ + NSTimeInterval durationSeconds = fabs([self.voiceMemoStartTime timeIntervalSinceNow]); + self.recordingLabel.text = [ViewControllerUtils formatDurationSeconds:(long)round(durationSeconds)]; + [self.recordingLabel sizeToFit]; +} + @end #pragma mark - @@ -561,6 +998,8 @@ typedef enum : NSUInteger { [self ensureBlockStateIndicator]; [self resetContentAndLayout]; + + [((OWSMessagesComposerTextView *)self.inputToolbar.contentView.textView)ensureSubviews]; } - (void)resetContentAndLayout @@ -749,6 +1188,8 @@ typedef enum : NSUInteger { [self cancelReadTimer]; [self saveDraft]; + + [((OWSMessagesComposerTextView *)self.inputToolbar.contentView.textView)cancelVoiceMemoIfNecessary]; } - (void)startExpirationTimerAnimations @@ -3037,6 +3478,27 @@ typedef enum : NSUInteger { completion:nil]; } +- (void)didStartVoiceMemo +{ + DDLogError(@"didStartVoiceMemo"); + + [((OWSMessagesInputToolbar *)self.inputToolbar)showVoiceMemoUI]; +} + +- (void)didEndVoiceMemo +{ + DDLogError(@"didEndVoiceMemo"); + + [((OWSMessagesInputToolbar *)self.inputToolbar) hideVoiceMemoUI:YES]; +} + +- (void)didCancelVoiceMemo +{ + DDLogError(@"didCancelVoiceMemo"); + + [((OWSMessagesInputToolbar *)self.inputToolbar) hideVoiceMemoUI:NO]; +} + #pragma mark - UIScrollViewDelegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 40a9df2c0..0a66c1f7e 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1280,6 +1280,9 @@ /* table cell label in conversation settings */ "VERIFY_PRIVACY" = "Verify Safety Number"; +/* Indicates how to cancel a voice memo. */ +"VOICE_MEMO_CANCEL_INSTRUCTIONS" = "Slide To Cancel"; + /* Activity indicator title, shown upon returning to the device manager, until you complete the provisioning process on desktop */ "WAITING_TO_COMPLETE_DEVICE_LINK_TEXT" = "Complete setup on Signal Desktop.";