diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 766001427..d19645831 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 2AE2882E4C2B96BFFF9EE27C /* Pods_SignalShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F94C85CB0B235DA37F68ED0 /* Pods_SignalShareExtension.framework */; }; + 3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */; }; 34074F61203D0CBE004596AE /* OWSSounds.m in Sources */ = {isa = PBXBuildFile; fileRef = 34074F5F203D0CBD004596AE /* OWSSounds.m */; }; 34074F62203D0CBE004596AE /* OWSSounds.h in Headers */ = {isa = PBXBuildFile; fileRef = 34074F60203D0CBE004596AE /* OWSSounds.h */; settings = {ATTRIBUTES = (Public, ); }; }; 340B02BA1FA0D6C700F9CFEC /* ConversationViewItemTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 340B02B91FA0D6C700F9CFEC /* ConversationViewItemTest.m */; }; @@ -567,6 +568,8 @@ 3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SelectThreadViewController.m; sourceTree = ""; }; 3400C7971EAFB772008A8584 /* ThreadViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadViewHelper.h; sourceTree = ""; }; 3400C7981EAFB772008A8584 /* ThreadViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadViewHelper.m; sourceTree = ""; }; + 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareButtonsView.m; sourceTree = ""; }; + 3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactShareButtonsView.h; sourceTree = ""; }; 34074F5F203D0CBD004596AE /* OWSSounds.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSounds.m; sourceTree = ""; }; 34074F60203D0CBE004596AE /* OWSSounds.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSounds.h; sourceTree = ""; }; 340B02B61F9FD31800F9CFEC /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = translations/he.lproj/Localizable.strings; sourceTree = ""; }; @@ -1750,6 +1753,8 @@ 34DBF001206BD5A500025978 /* OWSBubbleView.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */, + 3403B95C20EA9527001A1F44 /* OWSContactShareButtonsView.h */, + 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */, 34CA63192097806E00E526A0 /* OWSContactShareView.h */, 34CA631A2097806E00E526A0 /* OWSContactShareView.m */, 34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */, @@ -3237,6 +3242,7 @@ 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, 45B27B862037FFB400A539DF /* DebugUIFileBrowser.swift in Sources */, 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */, + 3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */, 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */, 34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */, 340FC8CD20518C77007AEB0F /* OWSBackupJob.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareButtonsView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareButtonsView.h new file mode 100644 index 000000000..6c4b9b666 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareButtonsView.h @@ -0,0 +1,35 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class ContactShareViewModel; + +@protocol OWSContactShareButtonsViewDelegate + +- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare; +- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare; +- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare; + +@end + +#pragma mark - + +@interface OWSContactShareButtonsView : UIView + +- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare + delegate:(id)delegate; + +- (void)createContents; + ++ (CGFloat)bubbleHeight; + +// Returns YES IFF the tap was handled. +- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender; + ++ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareButtonsView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareButtonsView.m new file mode 100644 index 000000000..fe566f6c8 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareButtonsView.m @@ -0,0 +1,161 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSContactShareButtonsView.h" +#import "Signal-Swift.h" +#import "UIColor+OWS.h" +#import "UIFont+OWS.h" +#import "UIView+OWS.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSContactShareButtonsView () + +@property (nonatomic, readonly) ContactShareViewModel *contactShare; +@property (nonatomic, weak) id delegate; + +@property (nonatomic, readonly) OWSContactsManager *contactsManager; + +@property (nonatomic, nullable) UIView *buttonView; + +@end + +#pragma mark - + +@implementation OWSContactShareButtonsView + +- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare + delegate:(id)delegate +{ + self = [super init]; + + if (self) { + _delegate = delegate; + _contactShare = contactShare; + _contactsManager = [Environment current].contactsManager; + } + + return self; +} + +#pragma mark - + ++ (BOOL)hasSendTextButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(contactShare); + OWSAssert(contactsManager); + + return [contactShare systemContactsWithSignalAccountPhoneNumbers:contactsManager].count > 0; +} + ++ (BOOL)hasInviteButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(contactShare); + OWSAssert(contactsManager); + + return [contactShare systemContactPhoneNumbers:contactsManager].count > 0; +} + ++ (BOOL)hasAddToContactsButton:(ContactShareViewModel *)contactShare +{ + OWSAssert(contactShare); + + return [contactShare e164PhoneNumbers].count > 0; +} + ++ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare +{ + OWSAssert(contactShare); + + OWSContactsManager *contactsManager = [Environment current].contactsManager; + + return [self hasAnyButton:contactShare contactsManager:contactsManager]; +} + ++ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(contactShare); + + return ([self hasSendTextButton:contactShare contactsManager:contactsManager] || + [self hasInviteButton:contactShare contactsManager:contactsManager] || + [self hasAddToContactsButton:contactShare]); +} + ++ (CGFloat)bubbleHeight +{ + return self.buttonHeight; +} + ++ (CGFloat)buttonHeight +{ + return MAX(44.f, self.buttonFont.lineHeight + self.buttonVMargin * 2); +} + ++ (UIFont *)buttonFont +{ + return [UIFont ows_dynamicTypeBodyFont].ows_mediumWeight; +} + ++ (CGFloat)buttonVMargin +{ + return 5; +} + +- (void)createContents +{ + OWSAssert([OWSContactShareButtonsView hasAnyButton:self.contactShare contactsManager:self.contactsManager]); + + self.layoutMargins = UIEdgeInsetsZero; + self.backgroundColor = [UIColor ows_light02Color]; + + UILabel *label = [UILabel new]; + self.buttonView = label; + if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) { + label.text = NSLocalizedString(@"ACTION_SEND_MESSAGE", @"Label for 'sent message' button in contact view."); + } else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) { + label.text = NSLocalizedString(@"ACTION_INVITE", @"Label for 'invite' button in contact view."); + } else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) { + label.text = NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER", + @"Message shown in conversation view that offers to add an unknown user to your phone's contacts."); + } else { + OWSFail(@"%@ unexpected button state.", self.logTag); + } + label.font = OWSContactShareButtonsView.buttonFont; + label.textColor = UIColor.ows_materialBlueColor; + label.textAlignment = NSTextAlignmentCenter; + [self addSubview:label]; + [label autoPinToSuperviewEdges]; + [label autoSetDimension:ALDimensionHeight toSize:OWSContactShareButtonsView.buttonHeight]; +} + +- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender +{ + if (!self.buttonView) { + return NO; + } + CGPoint location = [sender locationInView:self.buttonView]; + if (!CGRectContainsPoint(self.buttonView.bounds, location)) { + return NO; + } + + if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) { + [self.delegate didTapSendMessageToContactShare:self.contactShare]; + } else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) { + [self.delegate didTapSendInviteToContactShare:self.contactShare]; + } else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) { + [self.delegate didTapShowAddToContactUIForContactShare:self.contactShare]; + } else { + OWSFail(@"%@ unexpected button tap.", self.logTag); + } + + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h index 3325e21a4..c374c9fcf 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h @@ -5,31 +5,14 @@ NS_ASSUME_NONNULL_BEGIN @class ContactShareViewModel; -@class OWSContact; -@class OWSContactsManager; - -@protocol OWSContactShareViewDelegate - -- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare; -- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare; -- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare; - -@end - -#pragma mark - @interface OWSContactShareView : UIView -- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare - isIncoming:(BOOL)isIncoming - delegate:(id)delegate; +- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare isIncoming:(BOOL)isIncoming; - (void)createContents; -+ (CGFloat)bubbleHeightForContactShare:(ContactShareViewModel *)contactShare; - -// Returns YES IFF the tap was handled. -- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender; ++ (CGFloat)bubbleHeight; @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m index c5d5e4fc4..3b51fd810 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m @@ -18,13 +18,10 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSContactShareView () @property (nonatomic, readonly) ContactShareViewModel *contactShare; -@property (nonatomic, weak) id delegate; @property (nonatomic, readonly) BOOL isIncoming; @property (nonatomic, readonly) OWSContactsManager *contactsManager; -@property (nonatomic, nullable) UIView *buttonView; - @end #pragma mark - @@ -33,12 +30,10 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare isIncoming:(BOOL)isIncoming - delegate:(id)delegate { self = [super init]; if (self) { - _delegate = delegate; _contactShare = contactShare; _isIncoming = isIncoming; _contactsManager = [Environment current].contactsManager; @@ -54,80 +49,29 @@ NS_ASSUME_NONNULL_BEGIN return 12.f; } -- (CGFloat)iconHSpacing -{ - return 8.f; -} - -+ (CGFloat)iconVMargin -{ - return 12.f; -} - -- (CGFloat)iconVMargin -{ - return [OWSContactShareView iconVMargin]; -} - -+ (BOOL)hasSendTextButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager ++ (CGFloat)vMargin { - OWSAssert(contactShare); - OWSAssert(contactsManager); - - return [contactShare systemContactsWithSignalAccountPhoneNumbers:contactsManager].count > 0; + return 0.f; } -+ (BOOL)hasInviteButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager -{ - OWSAssert(contactShare); - OWSAssert(contactsManager); - - return [contactShare systemContactPhoneNumbers:contactsManager].count > 0; -} - -+ (BOOL)hasAddToContactsButton:(ContactShareViewModel *)contactShare -{ - OWSAssert(contactShare); - - return [contactShare e164PhoneNumbers].count > 0; -} - - -+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager +- (CGFloat)iconHSpacing { - OWSAssert(contactShare); - - return ([self hasSendTextButton:contactShare contactsManager:contactsManager] || - [self hasInviteButton:contactShare contactsManager:contactsManager] || - [self hasAddToContactsButton:contactShare]); + return 8.f; } -+ (CGFloat)bubbleHeightForContactShare:(ContactShareViewModel *)contactShare ++ (CGFloat)bubbleHeight { - OWSAssert(contactShare); - - OWSContactsManager *contactsManager = [Environment current].contactsManager; - - if ([self hasAnyButton:contactShare contactsManager:contactsManager]) { - return self.contentHeight + self.buttonHeight; - } else { - return self.contentHeight; - } + return self.contentHeight; } + (CGFloat)contentHeight { CGFloat labelsHeight = (self.nameFont.lineHeight + self.labelsVSpacing + self.subtitleFont.lineHeight); CGFloat contentHeight = MAX(self.iconSize, labelsHeight); - contentHeight += self.iconVMargin * 2; + contentHeight += OWSContactShareView.vMargin * 2; return contentHeight; } -+ (CGFloat)buttonHeight -{ - return MAX(44.f, self.buttonFont.lineHeight + self.buttonVMargin * 2); -} - + (CGFloat)iconSize { return 48.f; @@ -138,11 +82,6 @@ NS_ASSUME_NONNULL_BEGIN return [OWSContactShareView iconSize]; } -- (CGFloat)vMargin -{ - return 10.f; -} - + (UIFont *)nameFont { return [UIFont ows_dynamicTypeBodyFont]; @@ -158,16 +97,6 @@ NS_ASSUME_NONNULL_BEGIN return 2; } -+ (UIFont *)buttonFont -{ - return [UIFont ows_dynamicTypeBodyFont]; -} - -+ (CGFloat)buttonVMargin -{ - return 5; -} - - (void)createContents { self.layoutMargins = UIEdgeInsetsZero; @@ -220,61 +149,13 @@ NS_ASSUME_NONNULL_BEGIN hStackView.spacing = self.iconHSpacing; hStackView.alignment = UIStackViewAlignmentCenter; hStackView.layoutMarginsRelativeArrangement = YES; - hStackView.layoutMargins = UIEdgeInsetsMake(self.vMargin, self.hMargin, self.vMargin, self.hMargin); + hStackView.layoutMargins + = UIEdgeInsetsMake(OWSContactShareView.vMargin, self.hMargin, OWSContactShareView.vMargin, self.hMargin); [hStackView addArrangedSubview:avatarView]; [hStackView addArrangedSubview:labelsView]; [hStackView addArrangedSubview:disclosureImageView]; - - UIStackView *vStackView = [UIStackView new]; - vStackView.axis = UILayoutConstraintAxisVertical; - vStackView.spacing = 0; - [self addSubview:vStackView]; - [vStackView autoPinToSuperviewEdges]; - [vStackView addArrangedSubview:hStackView]; - - if ([OWSContactShareView hasAnyButton:self.contactShare contactsManager:self.contactsManager]) { - UILabel *label = [UILabel new]; - self.buttonView = label; - if ([OWSContactShareView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) { - label.text = NSLocalizedString(@"ACTION_SEND_MESSAGE", @"Label for 'sent message' button in contact view."); - } else if ([OWSContactShareView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) { - label.text = NSLocalizedString(@"ACTION_INVITE", @"Label for 'invite' button in contact view."); - } else if ([OWSContactShareView hasAddToContactsButton:self.contactShare]) { - label.text = NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER", - @"Message shown in conversation view that offers to add an unknown user to your phone's contacts."); - } else { - OWSFail(@"%@ unexpected button state.", self.logTag); - } - label.font = OWSContactShareView.buttonFont; - label.textColor = UIColor.ows_materialBlueColor; - label.textAlignment = NSTextAlignmentCenter; - label.backgroundColor = [UIColor whiteColor]; - [vStackView addArrangedSubview:label]; - [label autoSetDimension:ALDimensionHeight toSize:OWSContactShareView.buttonHeight]; - } -} - -- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender -{ - if (!self.buttonView) { - return NO; - } - CGPoint location = [sender locationInView:self.buttonView]; - if (!CGRectContainsPoint(self.buttonView.bounds, location)) { - return NO; - } - - if ([OWSContactShareView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) { - [self.delegate didTapSendMessageToContactShare:self.contactShare]; - } else if ([OWSContactShareView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) { - [self.delegate didTapSendInviteToContactShare:self.contactShare]; - } else if ([OWSContactShareView hasAddToContactsButton:self.contactShare]) { - [self.delegate didTapShowAddToContactUIForContactShare:self.contactShare]; - } else { - OWSFail(@"%@ unexpected button tap.", self.logTag); - } - - return YES; + [self addSubview:hStackView]; + [hStackView autoPinToSuperviewEdges]; } @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 645118317..cd14f1f0f 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -8,6 +8,7 @@ #import "OWSAudioMessageView.h" #import "OWSBubbleShapeView.h" #import "OWSBubbleView.h" +#import "OWSContactShareButtonsView.h" #import "OWSContactShareView.h" #import "OWSGenericAttachmentView.h" #import "OWSMessageFooterView.h" @@ -19,19 +20,10 @@ NS_ASSUME_NONNULL_BEGIN -@interface OWSMessageBubbleView () +@interface OWSMessageBubbleView () @property (nonatomic) OWSBubbleView *bubbleView; -// TODO: We may only end up using a single shadow. -@property (nonatomic) OWSBubbleShapeView *mediaShadowView1; - -@property (nonatomic) OWSBubbleShapeView *mediaShadowView2; - -@property (nonatomic) OWSBubbleShapeView *mediaClipView; - -@property (nonatomic) OWSBubbleShapeView *bubbleStrokeView; - @property (nonatomic) UIStackView *stackView; @property (nonatomic) UILabel *senderNameLabel; @@ -52,8 +44,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) OWSMessageFooterView *footerView; +@property (nonatomic, nullable) OWSContactShareButtonsView *contactShareButtonsView; + @end +#pragma mark - + @implementation OWSMessageBubbleView - (instancetype)initWithFrame:(CGRect)frame @@ -84,11 +80,6 @@ NS_ASSUME_NONNULL_BEGIN [self addSubview:self.bubbleView]; [self.bubbleView autoPinEdgesToSuperviewEdges]; - self.mediaShadowView1 = [OWSBubbleShapeView bubbleShadowView]; - self.mediaShadowView2 = [OWSBubbleShapeView bubbleShadowView]; - self.mediaClipView = [OWSBubbleShapeView bubbleClipView]; - self.bubbleStrokeView = [OWSBubbleShapeView bubbleDrawView]; - self.stackView = [UIStackView new]; self.stackView.axis = UILayoutConstraintAxisVertical; @@ -356,53 +347,64 @@ NS_ASSUME_NONNULL_BEGIN } if (self.hasBodyMediaWithThumbnail) { - // The "body media" view casts a shadow "downward" onto adjacent views, // so we use a "proxy" view to take its place within the v-stack // view and then insert the body media view above its proxy so that // it floats above the other content of the bubble view. - UIView *bodyProxyView = [UIView new]; - [self.stackView addArrangedSubview:bodyProxyView]; + UIView *proxyView = [UIView new]; + [self.stackView addArrangedSubview:proxyView]; + + // TODO: We may only end up using a single shadow. + OWSBubbleShapeView *shadowView1 = [OWSBubbleShapeView bubbleShadowView]; + OWSBubbleShapeView *shadowView2 = [OWSBubbleShapeView bubbleShadowView]; + OWSBubbleShapeView *clipView = [OWSBubbleShapeView bubbleClipView]; - [self addSubview:self.mediaShadowView1]; - [self addSubview:self.mediaShadowView2]; - [self addSubview:self.mediaClipView]; + [self addSubview:shadowView1]; + [self addSubview:shadowView2]; + [self addSubview:clipView]; - [self.viewConstraints addObjectsFromArray:[self.mediaShadowView1 autoPinToEdgesOfView:bodyProxyView]]; - [self.viewConstraints addObjectsFromArray:[self.mediaShadowView2 autoPinToEdgesOfView:bodyProxyView]]; - [self.viewConstraints addObjectsFromArray:[self.mediaClipView autoPinToEdgesOfView:bodyProxyView]]; + [self.viewConstraints addObjectsFromArray:[shadowView1 autoPinToEdgesOfView:proxyView]]; + [self.viewConstraints addObjectsFromArray:[shadowView2 autoPinToEdgesOfView:proxyView]]; + [self.viewConstraints addObjectsFromArray:[clipView autoPinToEdgesOfView:proxyView]]; - [self.mediaClipView addSubview:bodyMediaView]; + [clipView addSubview:bodyMediaView]; [self.viewConstraints addObjectsFromArray:[bodyMediaView autoPinToSuperviewEdges]]; - [self.bubbleView addPartnerView:self.mediaShadowView1]; - [self.bubbleView addPartnerView:self.mediaShadowView2]; - [self.bubbleView addPartnerView:self.mediaClipView]; + [self.bubbleView addPartnerView:shadowView1]; + [self.bubbleView addPartnerView:shadowView2]; + [self.bubbleView addPartnerView:clipView]; // TODO: Consider only using a single shadow for perf. - self.mediaShadowView1.fillColor = self.bubbleColor; - self.mediaShadowView1.layer.shadowColor = [UIColor blackColor].CGColor; - self.mediaShadowView1.layer.shadowOpacity = 0.2f; - self.mediaShadowView1.layer.shadowOffset = CGSizeMake(0.f, 4.f); - self.mediaShadowView1.layer.shadowRadius = 20.f; - - self.mediaShadowView2.fillColor = self.bubbleColor; - self.mediaShadowView2.layer.shadowColor = [UIColor blackColor].CGColor; - self.mediaShadowView2.layer.shadowOpacity = 0.08f; - self.mediaShadowView2.layer.shadowOffset = CGSizeZero; - self.mediaShadowView2.layer.shadowRadius = 4.f; + shadowView1.fillColor = self.bubbleColor; + shadowView1.layer.shadowColor = [UIColor blackColor].CGColor; + shadowView1.layer.shadowOpacity = 0.2f; + shadowView1.layer.shadowOffset = CGSizeMake(0.f, 4.f); + shadowView1.layer.shadowRadius = 20.f; + + shadowView2.fillColor = self.bubbleColor; + shadowView2.layer.shadowColor = [UIColor blackColor].CGColor; + shadowView2.layer.shadowOpacity = 0.08f; + shadowView2.layer.shadowOffset = CGSizeZero; + shadowView2.layer.shadowRadius = 4.f; } else { OWSAssert(self.cellType == OWSMessageCellType_ContactShare); + if (self.contactShareHasSpacerTop) { + UIView *spacerView = [UIView containerView]; + [spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing]; + [spacerView setCompressionResistanceHigh]; + [self.stackView addArrangedSubview:spacerView]; + } + [self.stackView addArrangedSubview:bodyMediaView]; - // TODO: Constants. - self.bubbleStrokeView.strokeColor = [UIColor lightGrayColor]; - self.bubbleStrokeView.strokeThickness = 1.f; - [self.bubbleView addSubview:self.bubbleStrokeView]; - [self.viewConstraints addObjectsFromArray:[self.bubbleStrokeView autoPinToSuperviewEdges]]; - [self.bubbleView addPartnerView:self.bubbleStrokeView]; + if (self.contactShareHasSpacerBottom) { + UIView *spacerView = [UIView containerView]; + [spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing]; + [spacerView setCompressionResistanceHigh]; + [self.stackView addArrangedSubview:spacerView]; + } } } else { [textViews addObject:bodyMediaView]; @@ -483,15 +485,82 @@ NS_ASSUME_NONNULL_BEGIN addObject:[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaSize.CGSizeValue.height]]; } - [self updateBubbleColor]; + [self insertContactShareButtonsIfNecessary]; - // If we're stroking the bubble edge, ensure the stroke - // view is in front of its peers to prevent it from being occluded. - [self.bubbleStrokeView.superview bringSubviewToFront:self.bubbleStrokeView]; + [self updateBubbleColor]; [self configureBubbleRounding]; } +- (void)insertContactShareButtonsIfNecessary +{ + if (self.cellType != OWSMessageCellType_ContactShare) { + return; + } + + if (![OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) { + return; + } + + OWSAssert(self.viewItem.contactShare); + + OWSContactShareButtonsView *buttonsView = + [[OWSContactShareButtonsView alloc] initWithContactShare:self.viewItem.contactShare delegate:self]; + [buttonsView createContents]; + + NSValue *_Nullable actionButtonsSize = [self actionButtonsSize]; + OWSAssert(actionButtonsSize); + [self.viewConstraints addObjectsFromArray:@[ + [buttonsView autoSetDimension:ALDimensionHeight toSize:actionButtonsSize.CGSizeValue.height], + ]]; + + // The "body media" view casts a shadow "downward" onto adjacent views, + // so we use a "proxy" view to take its place within the v-stack + // view and then insert the body media view above its proxy so that + // it floats above the other content of the bubble view. + + UIView *proxyView = [UIView new]; + [self.stackView addArrangedSubview:proxyView]; + + // TODO: We may only end up using a single shadow. + OWSBubbleShapeView *shadowView = [OWSBubbleShapeView bubbleShadowView]; + OWSBubbleShapeView *clipView = [OWSBubbleShapeView bubbleClipView]; + + [self addSubview:shadowView]; + [self addSubview:clipView]; + + [self.viewConstraints addObjectsFromArray:[shadowView autoPinToEdgesOfView:proxyView]]; + [self.viewConstraints addObjectsFromArray:[clipView autoPinToEdgesOfView:proxyView]]; + + [clipView addSubview:buttonsView]; + [self.viewConstraints addObjectsFromArray:[buttonsView autoPinToSuperviewEdges]]; + + [self.bubbleView addPartnerView:shadowView]; + [self.bubbleView addPartnerView:clipView]; + + OWSAssert(buttonsView.backgroundColor); + shadowView.fillColor = buttonsView.backgroundColor; + shadowView.layer.shadowColor = [UIColor blackColor].CGColor; + shadowView.layer.shadowOpacity = 0.12f; + shadowView.layer.shadowOffset = CGSizeMake(0.f, 0.f); + shadowView.layer.shadowRadius = 1.f; +} + +- (BOOL)contactShareHasSpacerTop +{ + return (self.cellType == OWSMessageCellType_ContactShare && (self.isQuotedReply || !self.shouldShowSenderName)); +} + +- (BOOL)contactShareHasSpacerBottom +{ + return (self.cellType == OWSMessageCellType_ContactShare && !self.hasBottomFooter); +} + +- (CGFloat)contactShareVSpacing +{ + return 12.f; +} + - (void)configureBubbleRounding { self.bubbleView.useSmallCorners_Top @@ -968,9 +1037,8 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(self.viewItem.contactShare); - OWSContactShareView *contactShareView = [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare - isIncoming:self.isIncoming - delegate:self]; + OWSContactShareView *contactShareView = + [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare isIncoming:self.isIncoming]; [contactShareView createContents]; // TODO: Should we change appearance if contact avatar is uploading? @@ -1115,8 +1183,7 @@ NS_ASSUME_NONNULL_BEGIN case OWSMessageCellType_ContactShare: OWSAssert(self.viewItem.contactShare); - result = CGSizeMake( - maxMessageWidth, [OWSContactShareView bubbleHeightForContactShare:self.viewItem.contactShare]); + result = CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]); break; } @@ -1165,6 +1232,23 @@ NS_ASSUME_NONNULL_BEGIN return [NSValue valueWithCGSize:result]; } +- (nullable NSValue *)actionButtonsSize +{ + OWSAssert(self.conversationStyle); + OWSAssert(self.conversationStyle.maxMessageWidth > 0); + + if (self.cellType == OWSMessageCellType_ContactShare) { + OWSAssert(self.viewItem.contactShare); + + if ([OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) { + CGSize buttonsSize = CGSizeCeil( + CGSizeMake(self.conversationStyle.maxMessageWidth, [OWSContactShareButtonsView bubbleHeight])); + return [NSValue valueWithCGSize:buttonsSize]; + } + } + return nil; +} + - (CGSize)measureSize { OWSAssert(self.conversationStyle); @@ -1201,6 +1285,13 @@ NS_ASSUME_NONNULL_BEGIN [textViewSizes addObject:bodyMediaSize]; bodyMediaSize = nil; } + + if (self.contactShareHasSpacerTop) { + cellSize.height += self.contactShareVSpacing; + } + if (self.contactShareHasSpacerBottom) { + cellSize.height += self.contactShareVSpacing; + } } if (bodyMediaSize || quotedMessageSize) { @@ -1241,6 +1332,12 @@ NS_ASSUME_NONNULL_BEGIN cellSize.height += self.tapForMoreHeight + self.textViewVSpacing; } + NSValue *_Nullable actionButtonsSize = [self actionButtonsSize]; + if (actionButtonsSize) { + cellSize.width = MAX(cellSize.width, actionButtonsSize.CGSizeValue.width); + cellSize.height += actionButtonsSize.CGSizeValue.height; + } + cellSize = CGSizeCeil(cellSize); OWSAssert(cellSize.width <= self.conversationStyle.maxMessageWidth); @@ -1340,17 +1437,20 @@ NS_ASSUME_NONNULL_BEGIN [self.quotedMessageView removeFromSuperview]; self.quotedMessageView = nil; - [self.mediaShadowView1 removeFromSuperview]; - [self.mediaShadowView2 removeFromSuperview]; - [self.mediaClipView removeFromSuperview]; - [self.bubbleStrokeView removeFromSuperview]; - [self.footerView removeFromSuperview]; [self.footerView prepareForReuse]; for (UIView *subview in self.stackView.subviews) { [subview removeFromSuperview]; } + for (UIView *subview in self.subviews) { + if (subview != self.bubbleView) { + [subview removeFromSuperview]; + } + } + + [self.contactShareButtonsView removeFromSuperview]; + self.contactShareButtonsView = nil; } #pragma mark - Gestures @@ -1381,9 +1481,8 @@ NS_ASSUME_NONNULL_BEGIN } } - if ([self.bodyMediaView isKindOfClass:[OWSContactShareView class]]) { - OWSContactShareView *contactShareView = (OWSContactShareView *)self.bodyMediaView; - if ([contactShareView handleTapGesture:sender]) { + if (self.contactShareButtonsView) { + if ([self.contactShareButtonsView handleTapGesture:sender]) { return; } } @@ -1508,7 +1607,7 @@ NS_ASSUME_NONNULL_BEGIN failedThumbnailDownloadAttachmentPointer:attachmentPointer]; } -#pragma mark - OWSContactShareViewDelegate +#pragma mark - OWSContactShareButtonsViewDelegate - (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare { diff --git a/SignalServiceKit/src/Messages/Interactions/OWSContact.m b/SignalServiceKit/src/Messages/Interactions/OWSContact.m index 9041dcea2..a05c514b8 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSContact.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSContact.m @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN // NOTE: When changing the value of this feature flag, you also need // to update the filtering in the SAE's info.plist. -BOOL kIsSendingContactSharesEnabled = NO; +BOOL kIsSendingContactSharesEnabled = YES; NSString *NSStringForContactPhoneType(OWSContactPhoneType value) {