From 3a74bc21b2ffd028db0dadc384b45ed54c7aee93 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Tue, 4 May 2021 08:18:39 -0700 Subject: [PATCH] Re-style voice memo recording view --- Signal.xcodeproj/project.pbxproj | 4 + .../mic-outline-64.imageset/Contents.json | 23 -- .../mic-outline-64@1x.png | Bin 1294 -> 0 bytes .../mic-outline-64@2x.png | Bin 3037 -> 0 bytes .../mic-outline-64@3x.png | Bin 4722 -> 0 bytes .../mic-solid-36.imageset/Contents.json | 12 + .../mic-solid-36.imageset/mic-solid-36.pdf | 94 +++++++ .../ConversationInputToolbar.h | 4 + .../ConversationInputToolbar.m | 263 ++++++++++-------- ...versationViewController+VoiceMessage.swift | 20 +- .../ConversationViewController.h | 3 +- Signal/src/views/VoiceMessageTooltip.swift | 33 +++ Signal/src/views/VoiceNoteLock.swift | 23 +- .../translations/en.lproj/Localizable.strings | 12 +- .../environment/OWSAudioSession.swift | 3 + 15 files changed, 330 insertions(+), 164 deletions(-) delete mode 100644 Signal/Images.xcassets/mic-outline-64.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@1x.png delete mode 100644 Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@2x.png delete mode 100644 Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@3x.png create mode 100644 Signal/Images.xcassets/mic-solid-36.imageset/Contents.json create mode 100644 Signal/Images.xcassets/mic-solid-36.imageset/mic-solid-36.pdf create mode 100644 Signal/src/views/VoiceMessageTooltip.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index fff97de3e5..4d489b8325 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -785,6 +785,7 @@ 88928A45263A2781009C9B30 /* state-change_confirm-up.caf in Resources */ = {isa = PBXBuildFile; fileRef = 88928A43263A2781009C9B30 /* state-change_confirm-up.caf */; }; 88928A71264099EC009C9B30 /* ConversationViewController+VoiceMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88928A70264099EC009C9B30 /* ConversationViewController+VoiceMessage.swift */; }; 88928A732640DC9B009C9B30 /* VoiceMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88928A722640DC9B009C9B30 /* VoiceMessageModel.swift */; }; + 88928A7526418904009C9B30 /* VoiceMessageTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88928A7426418904009C9B30 /* VoiceMessageTooltip.swift */; }; 889C8A8D248831AC00C3D3D8 /* BlurTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889C8A8C248831AC00C3D3D8 /* BlurTooltip.swift */; }; 88A357B923639384009D6B9A /* MemberActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A357B823639384009D6B9A /* MemberActionSheet.swift */; }; 88A4CC09246CB0960082211F /* OnboardingTransferChoiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A4CC08246CB0960082211F /* OnboardingTransferChoiceViewController.swift */; }; @@ -1928,6 +1929,7 @@ 88928A5A263F0DEC009C9B30 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = translations/hi.lproj/InfoPlist.strings; sourceTree = ""; }; 88928A70264099EC009C9B30 /* ConversationViewController+VoiceMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+VoiceMessage.swift"; sourceTree = ""; }; 88928A722640DC9B009C9B30 /* VoiceMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageModel.swift; sourceTree = ""; }; + 88928A7426418904009C9B30 /* VoiceMessageTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageTooltip.swift; sourceTree = ""; }; 889C8A8C248831AC00C3D3D8 /* BlurTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurTooltip.swift; sourceTree = ""; }; 88A357B823639384009D6B9A /* MemberActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberActionSheet.swift; sourceTree = ""; }; 88A358252363FF63009D6B9A /* mr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mr; path = translations/mr.lproj/Localizable.strings; sourceTree = ""; }; @@ -3442,6 +3444,7 @@ 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */, 32AC5CE6255B51E900829BD8 /* JoinGroupCallPill.swift */, 4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */, + 88928A7426418904009C9B30 /* VoiceMessageTooltip.swift */, ); name = Views; path = views; @@ -5176,6 +5179,7 @@ 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, 88ABB8BD25352B8300229EAA /* GroupCallMemberSheet.swift in Sources */, 88A4CC0E246CD6CC0082211F /* OnboardingTransferProgressViewController.swift in Sources */, + 88928A7526418904009C9B30 /* VoiceMessageTooltip.swift in Sources */, 4C0C36F8226647FE0083F19A /* ThreadMapping.swift in Sources */, 34B14D8D24F02A9600CC3A9A /* GroupLinkViewController.swift in Sources */, 34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */, diff --git a/Signal/Images.xcassets/mic-outline-64.imageset/Contents.json b/Signal/Images.xcassets/mic-outline-64.imageset/Contents.json deleted file mode 100644 index c13e29483f..0000000000 --- a/Signal/Images.xcassets/mic-outline-64.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "mic-outline-64@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "mic-outline-64@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "mic-outline-64@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@1x.png b/Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@1x.png deleted file mode 100644 index eec5037a3c062bf41b83caa4d93f885a02a33f38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1294 zcmV+p1@ZccP)-Kvq%i=s+$4#Fv0OF*E-P-=;AB zB1b{7@@dS=Z5;Z{7#xA{EO61yFi~d4dQpy3!jYKtzU+1aVq%pU#{(c59e)6mBXfHC)Zrd28DIN!BPpU+ijp z?d&Y3uTkiALD38FikabUuI=v|JChn}+%d_~{ey93SBqYNmkgEPjha)0A&F)ORJi!HXx;39TAYRoNi;j4!o{ye>t+Yk;xtr9 zqS*lzE`BXqr`iGhG_?&}3OE%rgL}p|&FFd6>860~(RRb7+=Zw zTjOsUJFEH@iP7X#!}gFZ4tBh2ffVq6l6C=n@z-4OZ?GR5-}^?-s@fW=J?;v54)!Fu z(3~dQZ4P(@BH7_fGB6$ua=Z&>`1UJg`<(VU>YcZb zui4k8p`O=S+2@lOTn_%WLFoH|{W|k$ZCf$m0I;H3|9LQqZ=Vf9&-@%RmP{`vjZT=x z579?`EI$1yJTY-nZ-9@${FwOpVN5YrRAYTiyxu-h??_Jgn9^)Iu8g{d<7aWr7~2T$ zyI7f;)D>_tKzf`g_k$j_kGg~vadD;jXnxEQ*c#W2m=)kB7n{UX&f7g|ZgmBY#l@BC zgTP|&havJ0`l`65_$PzS;AhZJ6r=Zr?8Qx|EdEtMy&@5Z=<2i+msH91q9l#i(dejY zre}C-xjg>X__|bbUdQnsSOlU6oqAF7S1?9y&zB|1eV3RofPFr=o}AC|RU?qv1+3_U zX>-wRR1??_J@wpGbf47qATb3T1YSPH@p9^dE8=DV7bSHTo{S)vbILG(;HI5ToSd8N!lH*m)HtkPhRq-sXZ?gcU zE$x65Zg#+coT$=hKKDDI%DbuW&1to|aW`tS0%fnjQ|5^Ta`L@$4FCWD07*qoM6N<$ Ef`IpdYXATM diff --git a/Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@2x.png b/Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@2x.png deleted file mode 100644 index 5a0970dd2de3f378eb521c5d7e8da58f2c1b89d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3037 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 Vwf{?`o%MBqnGwRU(!k^K{{SblyQcsE diff --git a/Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@3x.png b/Signal/Images.xcassets/mic-outline-64.imageset/mic-outline-64@3x.png deleted file mode 100644 index ae66682fef2e92d83566025d79a41a529ad54029..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4722 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 +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 6.000000 1.500000 cm +1.000000 1.000000 1.000000 scn +12.000000 10.500000 m +10.209788 10.500000 8.492900 11.211159 7.227030 12.477030 c +5.961159 13.742901 5.250000 15.459789 5.250000 17.250000 c +5.250000 26.250000 l +5.250000 28.040211 5.961159 29.757101 7.227030 31.022970 c +8.492900 32.288841 10.209788 33.000000 12.000000 33.000000 c +12.000000 33.000000 l +13.790212 33.000000 15.507100 32.288841 16.772970 31.022970 c +18.038841 29.757101 18.750000 28.040211 18.750000 26.250000 c +18.750000 17.250000 l +18.750000 15.459789 18.038841 13.742901 16.772970 12.477030 c +15.507100 11.211159 13.790212 10.500000 12.000000 10.500000 c +12.000000 10.500000 l +h +24.000000 17.250000 m +21.750000 17.250000 l +21.750000 14.664139 20.722771 12.184189 18.894291 10.355709 c +17.065811 8.527229 14.585861 7.500000 12.000000 7.500000 c +9.414139 7.500000 6.934188 8.527229 5.105709 10.355709 c +3.277229 12.184189 2.250000 14.664139 2.250000 17.250000 c +0.000000 17.250000 l +0.004423 14.263853 1.120677 11.386356 3.131173 9.178417 c +5.141669 6.970476 7.902320 5.590326 10.875000 5.306999 c +10.875000 0.000000 l +13.125000 0.000000 l +13.125000 5.306999 l +16.097679 5.590326 18.858332 6.970476 20.868828 9.178417 c +22.879324 11.386356 23.995577 14.263853 24.000000 17.250000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1326 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 36.000000 36.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001416 00000 n +0000001439 00000 n +0000001612 00000 n +0000001686 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1745 +%%EOF \ No newline at end of file diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index 74fe90190a..83085ad574 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -117,6 +117,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)cancelVoiceMemoIfNecessary; +- (void)showVoiceMemoTooltip; + +- (void)removeVoiceMemoTooltip; + #pragma mark - @property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index a3bb31370f..cee8a20c06 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN -typedef NS_CLOSED_ENUM(NSUInteger, VoiceMemoRecordingState){ +typedef NS_CLOSED_ENUM(NSUInteger, VoiceMemoRecordingState) { VoiceMemoRecordingState_Idle, VoiceMemoRecordingState_RecordingHeld, VoiceMemoRecordingState_RecordingLocked @@ -78,9 +78,10 @@ const CGFloat kMaxIPadTextViewHeight = 142; #pragma mark - Voice Memo Recording UI -@property (nonatomic, nullable) UIView *voiceMemoUI; @property (nonatomic, nullable) VoiceMemoLockView *voiceMemoLockView; -@property (nonatomic, nullable) UIView *voiceMemoContentView; +@property (nonatomic, readonly) UIView *voiceMemoContentViewLeftSpacer; +@property (nonatomic, readonly) UIView *voiceMemoContentViewRightSpacer; +@property (nonatomic, readonly) UIView *voiceMemoContentView; @property (nonatomic) NSDate *voiceMemoStartTime; @property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer; @property (nonatomic) UIGestureRecognizer *voiceMemoGestureRecognizer; @@ -90,6 +91,7 @@ const CGFloat kMaxIPadTextViewHeight = 142; @property (nonatomic, readonly) BOOL isRecordingVoiceMemo; @property (nonatomic) VoiceMemoRecordingState voiceMemoRecordingState; @property (nonatomic) CGPoint voiceMemoGestureStartLocation; +@property (nonatomic, nullable, weak) UIView *voiceMemoTooltip; @property (nonatomic, nullable) NSArray *layoutContraints; @property (nonatomic) UIEdgeInsets receivedSafeAreaInsets; @property (nonatomic, nullable) InputLinkPreview *inputLinkPreview; @@ -248,6 +250,8 @@ const CGFloat kMaxIPadTextViewHeight = 142; _voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.voiceMemoButton.accessibilityLabel = NSLocalizedString(@"INPUT_TOOLBAR_VOICE_MEMO_BUTTON_ACCESSIBILITY_LABEL", @"accessibility label for the button which records voice memos"); + self.voiceMemoButton.accessibilityHint = NSLocalizedString(@"INPUT_TOOLBAR_VOICE_MEMO_BUTTON_ACCESSIBILITY_HINT", + @"accessibility hint for the button which records voice memos"); UIImage *micIcon = [Theme iconImage:ThemeIconMicButton]; [self.voiceMemoButton setTemplateImage:micIcon tintColor:Theme.primaryIconColor]; [self.voiceMemoButton autoSetDimensionsToSize:CGSizeMake(40, kMinToolbarItemHeight)]; @@ -292,6 +296,23 @@ const CGFloat kMaxIPadTextViewHeight = 142; self.linkPreviewWrapper.backgroundColor = Theme.conversationInputBackgroundColor; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _linkPreviewWrapper); + _voiceMemoContentView = [UIView containerView]; + self.voiceMemoContentView.hidden = YES; + [self.voiceMemoContentView setContentHuggingHorizontalLow]; + [self.voiceMemoContentView setCompressionResistanceHorizontalLow]; + [self.voiceMemoContentView autoSetDimension:ALDimensionHeight toSize:kMinTextViewHeight]; + self.voiceMemoContentView.backgroundColor = Theme.conversationInputBackgroundColor; + SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _voiceMemoContentView); + + _voiceMemoContentViewLeftSpacer = [UIView containerView]; + self.voiceMemoContentViewLeftSpacer.hidden = YES; + [self.voiceMemoContentViewLeftSpacer autoSetDimension:ALDimensionHeight toSize:kMinToolbarItemHeight]; + [self.voiceMemoContentViewLeftSpacer autoSetDimension:ALDimensionWidth toSize:16]; + _voiceMemoContentViewRightSpacer = [UIView containerView]; + self.voiceMemoContentViewRightSpacer.hidden = YES; + [self.voiceMemoContentViewRightSpacer autoSetDimension:ALDimensionHeight toSize:kMinToolbarItemHeight]; + [self.voiceMemoContentViewRightSpacer autoSetDimension:ALDimensionWidth toSize:16]; + // V Stack UIStackView *vStack = [[UIStackView alloc] initWithArrangedSubviews:@[ self.quotedReplyWrapper, self.linkPreviewWrapper, self.inputTextView ]]; @@ -300,6 +321,9 @@ const CGFloat kMaxIPadTextViewHeight = 142; [vStack setContentHuggingHorizontalLow]; [vStack setCompressionResistanceHorizontalLow]; + [vStack addSubview:self.voiceMemoContentView]; + [self.voiceMemoContentView autoPinEdgesToEdgesOfView:self.inputTextView]; + for (UIView *button in @[ self.cameraButton, self.attachmentButton, self.stickerButton, self.voiceMemoButton, self.sendButton ]) { [button setContentHuggingHorizontalHigh]; @@ -326,6 +350,7 @@ const CGFloat kMaxIPadTextViewHeight = 142; // Media Stack UIStackView *mediaAndSendStack = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.voiceMemoContentViewRightSpacer, self.sendButton, self.cameraButton, self.voiceMemoButton, @@ -339,6 +364,7 @@ const CGFloat kMaxIPadTextViewHeight = 142; // H Stack UIStackView *hStack = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.voiceMemoContentViewLeftSpacer, self.attachmentButton, vStackRoundingOffsetView, mediaAndSendStack, @@ -608,18 +634,42 @@ const CGFloat kMaxIPadTextViewHeight = 142; // NOTE: We use untrimmedText, so that the sticker button disappears // even if the user just enters whitespace. BOOL hasTextInput = self.inputTextView.untrimmedText.length > 0; + BOOL isRecordingVoiceMemo = self.isRecordingVoiceMemo; // We used trimmed text for determining all the other button visibility. BOOL hasNonWhitespaceTextInput = self.inputTextView.trimmedText.length > 0; ensureViewHiddenState(self.attachmentButton, NO); - if (hasNonWhitespaceTextInput) { + if (isRecordingVoiceMemo) { + BOOL hideSendButton = self.voiceMemoRecordingState == VoiceMemoRecordingState_RecordingHeld; + ensureViewHiddenState(self.linkPreviewWrapper, YES); + ensureViewHiddenState(self.quotedReplyWrapper, YES); + ensureViewHiddenState(self.voiceMemoContentView, NO); + ensureViewHiddenState(self.voiceMemoContentViewLeftSpacer, NO); + ensureViewHiddenState(self.voiceMemoContentViewRightSpacer, !hideSendButton); + ensureViewHiddenState(self.cameraButton, YES); + ensureViewHiddenState(self.voiceMemoButton, YES); + ensureViewHiddenState(self.sendButton, hideSendButton); + ensureViewHiddenState(self.attachmentButton, YES); + } else if (hasNonWhitespaceTextInput) { + ensureViewHiddenState(self.linkPreviewWrapper, NO); + ensureViewHiddenState(self.quotedReplyWrapper, NO); + ensureViewHiddenState(self.voiceMemoContentView, YES); + ensureViewHiddenState(self.voiceMemoContentViewLeftSpacer, YES); + ensureViewHiddenState(self.voiceMemoContentViewRightSpacer, YES); ensureViewHiddenState(self.cameraButton, YES); ensureViewHiddenState(self.voiceMemoButton, YES); ensureViewHiddenState(self.sendButton, NO); + ensureViewHiddenState(self.attachmentButton, NO); } else { + ensureViewHiddenState(self.linkPreviewWrapper, NO); + ensureViewHiddenState(self.quotedReplyWrapper, NO); + ensureViewHiddenState(self.voiceMemoContentView, YES); + ensureViewHiddenState(self.voiceMemoContentViewLeftSpacer, YES); + ensureViewHiddenState(self.voiceMemoContentViewRightSpacer, YES); ensureViewHiddenState(self.cameraButton, NO); ensureViewHiddenState(self.voiceMemoButton, NO); ensureViewHiddenState(self.sendButton, YES); + ensureViewHiddenState(self.attachmentButton, NO); } // If the layout has changed, update the layout @@ -632,7 +682,7 @@ const CGFloat kMaxIPadTextViewHeight = 142; } void (^updateBlock)(void) = ^{ - BOOL hideStickerButton = hasTextInput || self.quotedReply != nil; + BOOL hideStickerButton = hasTextInput || isRecordingVoiceMemo || self.quotedReply != nil; ensureViewHiddenState(self.stickerButton, hideStickerButton); if (!hideStickerButton) { self.stickerButton.imageView.tintColor @@ -783,6 +833,14 @@ const CGFloat kMaxIPadTextViewHeight = 142; } } +- (void)setVoiceMemoRecordingState:(VoiceMemoRecordingState)voiceMemoRecordingState +{ + if (voiceMemoRecordingState != _voiceMemoRecordingState) { + _voiceMemoRecordingState = voiceMemoRecordingState; + [self ensureButtonVisibilityWithIsAnimated:YES doLayout:YES]; + } +} + #pragma mark - Voice Memo - (BOOL)isRecordingVoiceMemo @@ -800,37 +858,24 @@ const CGFloat kMaxIPadTextViewHeight = 142; { OWSAssertIsOnMainThread(); + [self removeVoiceMemoTooltip]; + self.voiceMemoStartTime = [NSDate date]; - [self.voiceMemoUI removeFromSuperview]; + [self.voiceMemoRedRecordingCircle removeFromSuperview]; [self.voiceMemoLockView removeFromSuperview]; - self.voiceMemoUI = [UIView new]; - self.voiceMemoUI.backgroundColor = Theme.toolbarBackgroundColor; - [self addSubview:self.voiceMemoUI]; - [self.voiceMemoUI autoPinEdgesToSuperviewEdges]; - SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _voiceMemoUI); - - self.voiceMemoContentView = [UIView new]; - [self.voiceMemoUI addSubview:self.voiceMemoContentView]; - [self.voiceMemoContentView autoPinEdgesToSuperviewMargins]; + [self.voiceMemoContentView removeAllSubviews]; self.recordingLabel = [UILabel new]; - self.recordingLabel.textColor = UIColor.ows_accentRedColor; - self.recordingLabel.font = [UIFont ows_semiboldFontWithSize:14.f]; + self.recordingLabel.textColor = Theme.primaryTextColor; + self.recordingLabel.font = UIFont.ows_dynamicTypeBodyClampedFont.ows_medium.ows_monospaced; [self.voiceMemoContentView addSubview:self.recordingLabel]; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _recordingLabel); - VoiceMemoLockView *voiceMemoLockView = [VoiceMemoLockView new]; - self.voiceMemoLockView = voiceMemoLockView; - [self addSubview:voiceMemoLockView]; - [voiceMemoLockView autoPinEdgeToSuperviewMargin:ALEdgeRight]; - [voiceMemoLockView autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:self.voiceMemoContentView]; - [voiceMemoLockView setCompressionResistanceHigh]; - [self updateVoiceMemo]; - UIImage *icon = [UIImage imageNamed:@"mic-outline-24"]; + UIImage *icon = [UIImage imageNamed:@"mic-solid-24"]; OWSAssertDebug(icon); UIImageView *imageView = [[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; @@ -840,70 +885,59 @@ const CGFloat kMaxIPadTextViewHeight = 142; NSMutableAttributedString *cancelString = [NSMutableAttributedString new]; const CGFloat cancelArrowFontSize = ScaleFromIPhone5To7Plus(18.4, 20.f); - const CGFloat cancelFontSize = ScaleFromIPhone5To7Plus(14.f, 16.f); NSString *arrowHead = @"\uf104"; [cancelString append:arrowHead attributes:@{ NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize], - NSForegroundColorAttributeName : UIColor.ows_accentRedColor, + NSForegroundColorAttributeName : Theme.secondaryTextAndIconColor, NSBaselineOffsetAttributeName : @(-1.f), }]; [cancelString append:@" " attributes:@{ NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize], - NSForegroundColorAttributeName : UIColor.ows_accentRedColor, - NSBaselineOffsetAttributeName : @(-1.f), - }]; - [cancelString append:NSLocalizedString(@"VOICE_MESSAGE_CANCEL_INSTRUCTIONS", - @"Indicates how to cancel a voice message.") - attributes:@{ - NSFontAttributeName : [UIFont ows_semiboldFontWithSize:cancelFontSize], - NSForegroundColorAttributeName : UIColor.ows_accentRedColor, - }]; - [cancelString append:@" " - attributes:@{ - NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize], - NSForegroundColorAttributeName : UIColor.ows_accentRedColor, - NSBaselineOffsetAttributeName : @(-1.f), - }]; - [cancelString append:arrowHead - attributes:@{ - NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize], - NSForegroundColorAttributeName : UIColor.ows_accentRedColor, + NSForegroundColorAttributeName : Theme.secondaryTextAndIconColor, NSBaselineOffsetAttributeName : @(-1.f), }]; + [cancelString + append:NSLocalizedString(@"VOICE_MESSAGE_CANCEL_INSTRUCTIONS", @"Indicates how to cancel a voice message.") + attributes:@{ + NSFontAttributeName : [UIFont ows_dynamicTypeSubheadlineClampedFont], + NSForegroundColorAttributeName : Theme.secondaryTextAndIconColor, + }]; UILabel *cancelLabel = [UILabel new]; + cancelLabel.textAlignment = NSTextAlignmentRight; self.voiceMemoCancelLabel = cancelLabel; cancelLabel.attributedText = cancelString; [self.voiceMemoContentView addSubview:cancelLabel]; - const CGFloat kRedCircleSize = 100.f; + const CGFloat kRedCircleSize = 80.f; UIView *redCircleView = [[OWSCircleView alloc] initWithDiameter:kRedCircleSize]; self.voiceMemoRedRecordingCircle = redCircleView; redCircleView.backgroundColor = UIColor.ows_accentRedColor; - [self.voiceMemoContentView addSubview:redCircleView]; - [redCircleView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.voiceMemoButton]; - [redCircleView autoAlignAxis:ALAxisVertical toSameAxisOfView:self.voiceMemoButton]; + [self addSubview:redCircleView]; + [redCircleView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.voiceMemoContentView]; + [redCircleView autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:12]; - UIImage *whiteIcon = [UIImage imageNamed:@"mic-outline-64"]; + UIImage *whiteIcon = [UIImage imageNamed:@"mic-solid-36"]; OWSAssertDebug(whiteIcon); UIImageView *whiteIconView = [[UIImageView alloc] initWithImage:whiteIcon]; [redCircleView addSubview:whiteIconView]; [whiteIconView autoCenterInSuperview]; [imageView autoVCenterInSuperview]; - [imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:10.f]; + [imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:12.f]; [self.recordingLabel autoVCenterInSuperview]; - [self.recordingLabel autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:imageView withOffset:5.f]; + [self.recordingLabel autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:imageView withOffset:8.f]; [cancelLabel autoVCenterInSuperview]; - [cancelLabel autoHCenterInSuperview]; - [self.voiceMemoUI layoutIfNeeded]; + [cancelLabel autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:72.f]; + [cancelLabel autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.recordingLabel]; - // 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; + VoiceMemoLockView *voiceMemoLockView = [VoiceMemoLockView new]; + self.voiceMemoLockView = voiceMemoLockView; + [self addSubview:voiceMemoLockView]; + [voiceMemoLockView autoAlignAxis:ALAxisVertical toSameAxisOfView:redCircleView]; + [voiceMemoLockView autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:redCircleView]; + [voiceMemoLockView setCompressionResistanceHigh]; voiceMemoLockView.transform = CGAffineTransformMakeScale(0.0, 0.0); [voiceMemoLockView layoutIfNeeded]; @@ -915,36 +949,18 @@ const CGFloat kMaxIPadTextViewHeight = 142; } completion:nil]; - [UIView animateWithDuration:0.35f - delay:0.f - options:UIViewAnimationOptionCurveEaseOut - animations:^{ - cancelLabel.frame = cancelLabelEndFrame; - } - completion:nil]; + redCircleView.transform = CGAffineTransformMakeScale(0.0, 0.0); + [UIView animateWithDuration:0.1f animations:^{ redCircleView.transform = CGAffineTransformIdentity; }]; // Pulse the icon. - imageView.layer.opacity = 1.f; + imageView.alpha = 1.f; [UIView animateWithDuration:0.5f delay:0.2f options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionCurveEaseIn - animations:^{ - imageView.layer.opacity = 0.f; - } + animations:^{ imageView.alpha = 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 invalidate]; self.voiceMemoUpdateTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.1f @@ -960,33 +976,29 @@ const CGFloat kMaxIPadTextViewHeight = 142; self.voiceMemoRecordingState = VoiceMemoRecordingState_Idle; - UIView *oldVoiceMemoUI = self.voiceMemoUI; + UIView *oldVoiceMemoRedRecordingCircle = self.voiceMemoRedRecordingCircle; UIView *oldVoiceMemoLockView = self.voiceMemoLockView; - self.voiceMemoUI = nil; self.voiceMemoCancelLabel = nil; self.voiceMemoRedRecordingCircle = nil; - self.voiceMemoContentView = nil; self.voiceMemoLockView = nil; self.recordingLabel = nil; [self.voiceMemoUpdateTimer invalidate]; self.voiceMemoUpdateTimer = nil; - [oldVoiceMemoUI.layer removeAllAnimations]; - if (animated) { - [UIView animateWithDuration:0.35f + [UIView animateWithDuration:0.2f animations:^{ - oldVoiceMemoUI.layer.opacity = 0.f; - oldVoiceMemoLockView.layer.opacity = 0.f; + oldVoiceMemoRedRecordingCircle.alpha = 0.f; + oldVoiceMemoLockView.alpha = 0.f; } completion:^(BOOL finished) { - [oldVoiceMemoUI removeFromSuperview]; + [oldVoiceMemoRedRecordingCircle removeFromSuperview]; [oldVoiceMemoLockView removeFromSuperview]; }]; } else { - [oldVoiceMemoUI removeFromSuperview]; + [oldVoiceMemoRedRecordingCircle removeFromSuperview]; [oldVoiceMemoLockView removeFromSuperview]; } } @@ -995,18 +1007,7 @@ const CGFloat kMaxIPadTextViewHeight = 142; { __weak __typeof(self) weakSelf = self; - UIButton *sendVoiceMemoButton = [[OWSButton alloc] initWithBlock:^{ - [weakSelf.inputToolbarDelegate voiceMemoGestureDidComplete]; - }]; - [sendVoiceMemoButton setTitle:MessageStrings.sendButton forState:UIControlStateNormal]; - [sendVoiceMemoButton setTitleColor:UIColor.ows_accentBlueColor forState:UIControlStateNormal]; - sendVoiceMemoButton.alpha = 0; - [self.voiceMemoContentView addSubview:sendVoiceMemoButton]; - [sendVoiceMemoButton autoPinEdgeToSuperviewMargin:ALEdgeRight withInset:10.f]; - [sendVoiceMemoButton autoVCenterInSuperview]; - [sendVoiceMemoButton setCompressionResistanceHigh]; - [sendVoiceMemoButton setContentHuggingHigh]; - SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, sendVoiceMemoButton); + [ImpactHapticFeedback impactOccuredWithStyle:UIImpactFeedbackStyleMedium]; UIButton *cancelButton = [[OWSButton alloc] initWithBlock:^{ [weakSelf.inputToolbarDelegate voiceMemoGestureDidCancel]; @@ -1014,7 +1015,8 @@ const CGFloat kMaxIPadTextViewHeight = 142; [cancelButton setTitle:CommonStrings.cancelButton forState:UIControlStateNormal]; [cancelButton setTitleColor:UIColor.ows_accentRedColor forState:UIControlStateNormal]; cancelButton.alpha = 0; - cancelButton.titleLabel.textAlignment = NSTextAlignmentCenter; + cancelButton.titleLabel.textAlignment = NSTextAlignmentRight; + cancelButton.titleLabel.font = UIFont.ows_dynamicTypeBodyClampedFont.ows_medium; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, cancelButton); [self.voiceMemoContentView addSubview:cancelButton]; @@ -1025,29 +1027,23 @@ const CGFloat kMaxIPadTextViewHeight = 142; forConstraints:^{ [cancelButton autoHCenterInSuperview]; }]; + [cancelButton autoPinEdgeToSuperviewMargin:ALEdgeRight withInset:20]; [cancelButton autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.recordingLabel withOffset:4 relation:NSLayoutRelationGreaterThanOrEqual]; - [cancelButton autoPinEdge:ALEdgeLeft - toEdge:ALEdgeRight - ofView:sendVoiceMemoButton - withOffset:-4 - relation:NSLayoutRelationLessThanOrEqual]; [cancelButton autoVCenterInSuperview]; + [self.voiceMemoCancelLabel removeFromSuperview]; [self.voiceMemoContentView layoutIfNeeded]; - [UIView animateWithDuration:0.35 + [UIView animateWithDuration:0.2f animations:^{ - self.voiceMemoCancelLabel.alpha = 0; self.voiceMemoRedRecordingCircle.alpha = 0; - self.voiceMemoLockView.transform = CGAffineTransformMakeScale(0, 0); + self.voiceMemoLockView.alpha = 0; cancelButton.alpha = 1.0; - sendVoiceMemoButton.alpha = 1.0; } completion:^(BOOL finished) { - [self.voiceMemoCancelLabel removeFromSuperview]; [self.voiceMemoRedRecordingCircle removeFromSuperview]; [self.voiceMemoLockView removeFromSuperview]; }]; @@ -1059,7 +1055,7 @@ const CGFloat kMaxIPadTextViewHeight = 142; // Fade out the voice message views as the cancel gesture // proceeds as feedback. - self.voiceMemoContentView.layer.opacity = MAX(0.f, MIN(1.f, 1.f - (float)cancelAlpha)); + self.voiceMemoCancelLabel.alpha = MAX(0.f, MIN(1.f, 1.f - (float)cancelAlpha)); } - (void)updateVoiceMemo @@ -1078,13 +1074,46 @@ const CGFloat kMaxIPadTextViewHeight = 142; } } +- (void)showVoiceMemoTooltip +{ + if (self.voiceMemoTooltip) { + return; + } + + __weak ConversationInputToolbar *weakSelf = self; + UIView *tooltip = [VoiceMessageTooltip presentFromView:self + widthReferenceView:self + tailReferenceView:self.voiceMemoButton + wasTappedBlock:^{ [weakSelf removeVoiceMemoTooltip]; }]; + self.voiceMemoTooltip = tooltip; + + const CGFloat tooltipDurationSeconds = 3.f; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(tooltipDurationSeconds * NSEC_PER_SEC)), + dispatch_get_main_queue(), + ^{ [weakSelf removeVoiceMemoTooltip]; }); +} + +- (void)removeVoiceMemoTooltip +{ + UIView *voiceMemoTooltip = self.voiceMemoTooltip; + self.voiceMemoTooltip = nil; + + [UIView animateWithDuration:0.2 + animations:^{ voiceMemoTooltip.alpha = 0; } + completion:^(BOOL finished) { [voiceMemoTooltip removeFromSuperview]; }]; +} + #pragma mark - Event Handlers - (void)sendButtonPressed { OWSAssertDebug(self.inputToolbarDelegate); - [self.inputToolbarDelegate sendButtonPressed]; + if (self.isRecordingVoiceMemo) { + [self.inputToolbarDelegate voiceMemoGestureDidComplete]; + } else { + [self.inputToolbarDelegate sendButtonPressed]; + } } - (void)cameraButtonPressed diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController+VoiceMessage.swift b/Signal/src/ViewControllers/ConversationView/ConversationViewController+VoiceMessage.swift index 7bfd8bbc82..5d71ededb8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController+VoiceMessage.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController+VoiceMessage.swift @@ -12,7 +12,6 @@ extension ConversationViewController { // Cancel any ongoing audio playback. cvAudioPlayer.stopAll() - SelectionHapticFeedback().selectionChanged() inputToolbar?.showVoiceMemoUI() let voiceMessageModel = VoiceMessageModel(thread: thread) @@ -38,6 +37,8 @@ extension ConversationViewController { return owsFailDebug("Unexpectedly missing voice message model") } + ImpactHapticFeedback.impactOccured(style: .light) + do { try voiceMessageModel.startRecording() } catch { @@ -50,7 +51,13 @@ extension ConversationViewController { func cancelRecordingVoiceMessage() { AssertIsOnMainThread() - viewState.currentVoiceMessageModel?.stopRecording() + defer { viewState.currentVoiceMessageModel = nil } + guard let voiceMessageModel = viewState.currentVoiceMessageModel else { return } + + voiceMessageModel.stopRecording() + + NotificationHapticFeedback().notificationOccurred(.warning) + clearVoiceMessageDraft() viewState.currentVoiceMessageModel = nil inputToolbar?.cancelVoiceMemoIfNecessary() @@ -66,17 +73,14 @@ extension ConversationViewController { let duration = voiceMessageModel.stopRecording() - SelectionHapticFeedback().selectionChanged() - guard duration >= Self.minimumVoiceMessageDuration else { - presentToast(text: NSLocalizedString( - "VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE", - comment: "Message for the alert indicating the 'voice message' needs to be held to be held down to record." - )) + inputToolbar?.showVoiceMemoTooltip() cancelRecordingVoiceMessage() return } + ImpactHapticFeedback.impactOccured(style: .medium) + if sendImmediately { inputToolbar?.hideVoiceMemoUI(true) sendVoiceMessageModel(voiceMessageModel) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h index 05a346c4d3..1a7c896f49 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.h @@ -92,7 +92,8 @@ typedef NS_CLOSED_ENUM(NSUInteger, ConversationUIMode) { @property (nonatomic, readonly) ConversationViewLayout *layout; @property (nonatomic, readonly) CVViewState *viewState; -- (void)tryToSendAttachments:(NSArray *)attachments messageBody:(MessageBody *_Nullable)messageBody NS_SWIFT_NAME(tryToSendAttachments(_:messageBody:)); +- (void)tryToSendAttachments:(NSArray *)attachments + messageBody:(MessageBody *_Nullable)messageBody NS_SWIFT_NAME(tryToSendAttachments(_:messageBody:)); - (void)updateBarButtonItems; - (void)ensureBannerState; diff --git a/Signal/src/views/VoiceMessageTooltip.swift b/Signal/src/views/VoiceMessageTooltip.swift new file mode 100644 index 0000000000..529f2b1747 --- /dev/null +++ b/Signal/src/views/VoiceMessageTooltip.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class VoiceMessageTooltip: TooltipView { + @objc + public class func present(fromView: UIView, + widthReferenceView: UIView, + tailReferenceView: UIView, + wasTappedBlock: (() -> Void)?) -> VoiceMessageTooltip { + return VoiceMessageTooltip(fromView: fromView, widthReferenceView: widthReferenceView, tailReferenceView: tailReferenceView, wasTappedBlock: wasTappedBlock) + } + + public override func bubbleContentView() -> UIView { + let label = UILabel() + label.text = NSLocalizedString( + "VOICE_MESSAGE_TOO_SHORT_TOOLTIP", + comment: "Message for the tooltip indicating the 'voice message' needs to be held to be held down to record." + ) + label.font = UIFont.ows_dynamicTypeBodyClamped + label.textColor = Theme.primaryTextColor + + return horizontalStack(forSubviews: [label]) + } + + public override var bubbleColor: UIColor { Theme.backgroundColor } + public override var bubbleHSpacing: CGFloat { 8 } + + public override var tailDirection: TooltipView.TailDirection { .down } +} diff --git a/Signal/src/views/VoiceNoteLock.swift b/Signal/src/views/VoiceNoteLock.swift index 594a764a34..3e7632ff42 100644 --- a/Signal/src/views/VoiceNoteLock.swift +++ b/Signal/src/views/VoiceNoteLock.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation @@ -9,8 +9,8 @@ public class VoiceMemoLockView: UIView { private var offsetConstraint: NSLayoutConstraint! - private let offsetFromToolbar: CGFloat = 40 - private let backgroundViewInitialHeight: CGFloat = 80 + private let offsetFromToolbar: CGFloat = 42 + private let backgroundViewInitialHeight: CGFloat = 72 private var chevronTravel: CGFloat { return -1 * (backgroundViewInitialHeight - 50) } @@ -29,7 +29,7 @@ public class VoiceMemoLockView: UIView { // 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) + backgroundView.layoutMargins = UIEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) lockIconView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom) chevronView.autoPinEdges(toSuperviewMarginsExcludingEdge: .top) @@ -49,9 +49,9 @@ public class VoiceMemoLockView: UIView { // MARK: - Subviews private lazy var lockIconView: UIImageView = { - let imageTemplate = #imageLiteral(resourceName: "ic_lock_outline").withRenderingMode(.alwaysTemplate) + let imageTemplate = #imageLiteral(resourceName: "lock-solid-24").withRenderingMode(.alwaysTemplate) let imageView = UIImageView(image: imageTemplate) - imageView.tintColor = .ows_accentRed + imageView.tintColor = Theme.isDarkThemeEnabled ? .ows_gray15 : .ows_gray75 imageView.autoSetDimensions(to: CGSize(square: 24)) return imageView }() @@ -59,7 +59,7 @@ public class VoiceMemoLockView: UIView { private lazy var chevronView: UIView = { let label = UILabel() label.text = "\u{2303}" - label.textColor = .ows_accentRed + label.textColor = Theme.ternaryTextColor label.textAlignment = .center return label }() @@ -67,13 +67,18 @@ public class VoiceMemoLockView: UIView { private lazy var backgroundView: UIView = { let view = UIView() - let width: CGFloat = 36 + let width: CGFloat = 40 view.autoSetDimension(.width, toSize: width) - view.backgroundColor = Theme.scrollButtonBackgroundColor + view.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray02 view.layer.cornerRadius = width / 2 view.layer.borderColor = Theme.washColor.cgColor view.layer.borderWidth = CGHairlineWidth() + view.layer.shadowOffset = CGSize(width: 0, height: 4) + view.layer.shadowRadius = 12 + view.layer.shadowOpacity = 0.3 + view.layer.shadowColor = UIColor.black.cgColor + return view }() diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 063ef8f2d0..b5a18f50fc 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2590,6 +2590,9 @@ /* accessibility label for the button which shows the sticker picker */ "INPUT_TOOLBAR_STICKER_BUTTON_ACCESSIBILITY_LABEL" = "Stickers"; +/* accessibility hint for the button which records voice memos */ +"INPUT_TOOLBAR_VOICE_MEMO_BUTTON_ACCESSIBILITY_HINT" = "Double tap and hold to record. Release to send or slide left to cancel. Double tap and slide up to lock recording."; + /* accessibility label for the button which records voice memos */ "INPUT_TOOLBAR_VOICE_MEMO_BUTTON_ACCESSIBILITY_LABEL" = "Voice Message"; @@ -5858,16 +5861,13 @@ "VIEW_ONCE_OUTGOING_TOAST" = "Outgoing view-once media files are automatically removed after they are sent."; /* Indicates how to cancel a voice message. */ -"VOICE_MESSAGE_CANCEL_INSTRUCTIONS" = "Slide to Cancel"; +"VOICE_MESSAGE_CANCEL_INSTRUCTIONS" = "Slide to cancel"; /* Filename for voice messages. */ "VOICE_MESSAGE_FILE_NAME" = "Voice Message"; -/* Message for the alert indicating the 'voice message' needs to be held to be held down to record. */ -"VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE" = "Tap and hold to record a voice message."; - -/* Title for the alert indicating the 'voice message' needs to be held to be held down to record. */ -"VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE" = "Voice Message"; +/* Message for the tooltip indicating the 'voice message' needs to be held to be held down to record. */ +"VOICE_MESSAGE_TOO_SHORT_TOOLTIP" = "Press and hold to record."; /* 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."; diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index fe28cd40b1..6ef3068565 100644 --- a/SignalMessaging/environment/OWSAudioSession.swift +++ b/SignalMessaging/environment/OWSAudioSession.swift @@ -144,6 +144,9 @@ public class OWSAudioSession: NSObject { } else if aggregateBehaviors.contains(.playAndRecord) { assert(avAudioSession.recordPermission == .granted) try avAudioSession.setCategory(.record) + if #available(iOS 13, *) { + try avAudioSession.setAllowHapticsAndSystemSoundsDuringRecording(true) + } } else if aggregateBehaviors.contains(.audioMessagePlayback) { if self.device.proximityState { Logger.debug("proximityState: true")