Re-style voice memo recording view

This commit is contained in:
Nora Trapp 2021-05-04 08:18:39 -07:00 committed by Matthew Chen
parent a978e453bc
commit 3a74bc21b2
15 changed files with 330 additions and 164 deletions

View File

@ -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 = "<group>"; };
88928A70264099EC009C9B30 /* ConversationViewController+VoiceMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+VoiceMessage.swift"; sourceTree = "<group>"; };
88928A722640DC9B009C9B30 /* VoiceMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageModel.swift; sourceTree = "<group>"; };
88928A7426418904009C9B30 /* VoiceMessageTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageTooltip.swift; sourceTree = "<group>"; };
889C8A8C248831AC00C3D3D8 /* BlurTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurTooltip.swift; sourceTree = "<group>"; };
88A357B823639384009D6B9A /* MemberActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberActionSheet.swift; sourceTree = "<group>"; };
88A358252363FF63009D6B9A /* mr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mr; path = translations/mr.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "mic-solid-36.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,94 @@
%PDF-1.7
1 0 obj
<< >>
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

View File

@ -117,6 +117,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)cancelVoiceMemoIfNecessary;
- (void)showVoiceMemoTooltip;
- (void)removeVoiceMemoTooltip;
#pragma mark -
@property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply;

View File

@ -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<NSLayoutConstraint *> *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

View File

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

View File

@ -92,7 +92,8 @@ typedef NS_CLOSED_ENUM(NSUInteger, ConversationUIMode) {
@property (nonatomic, readonly) ConversationViewLayout *layout;
@property (nonatomic, readonly) CVViewState *viewState;
- (void)tryToSendAttachments:(NSArray<SignalAttachment *> *)attachments messageBody:(MessageBody *_Nullable)messageBody NS_SWIFT_NAME(tryToSendAttachments(_:messageBody:));
- (void)tryToSendAttachments:(NSArray<SignalAttachment *> *)attachments
messageBody:(MessageBody *_Nullable)messageBody NS_SWIFT_NAME(tryToSendAttachments(_:messageBody:));
- (void)updateBarButtonItems;
- (void)ensureBannerState;

View File

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

View File

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

View File

@ -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.";

View File

@ -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")