diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b2dbb37ea..f0c29f4b5 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -188,6 +188,7 @@ 34C6B0AB1FA0E46F00D35993 /* test-mp3.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0A71FA0E46F00D35993 /* test-mp3.mp3 */; }; 34C6B0AC1FA0E46F00D35993 /* test-mp4.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0A81FA0E46F00D35993 /* test-mp4.mp4 */; }; 34C6B0AE1FA0E4AA00D35993 /* test-jpg.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0AD1FA0E4AA00D35993 /* test-jpg.jpg */; }; + 34CA631B2097806F00E526A0 /* OWSContactShareView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CA631A2097806E00E526A0 /* OWSContactShareView.m */; }; 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */; }; 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; }; 34CF0787203E6B78005C4D61 /* busy_tone_ansi.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0783203E6B77005C4D61 /* busy_tone_ansi.caf */; }; @@ -802,6 +803,8 @@ 34C6B0AD1FA0E4AA00D35993 /* test-jpg.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "test-jpg.jpg"; sourceTree = ""; }; 34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDetailViewController.swift; sourceTree = ""; }; 34CA1C281F7164F700E51C51 /* MediaMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageView.swift; sourceTree = ""; }; + 34CA63192097806E00E526A0 /* OWSContactShareView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactShareView.h; sourceTree = ""; }; + 34CA631A2097806E00E526A0 /* OWSContactShareView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareView.m; sourceTree = ""; }; 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppUpdateNag.h; sourceTree = ""; }; 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = ""; }; 34CE88E51F2FB9A10098030F /* ProfileViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileViewController.h; sourceTree = ""; }; @@ -1711,6 +1714,8 @@ 34DBF001206BD5A500025978 /* OWSBubbleView.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */, + 34CA63192097806E00E526A0 /* OWSContactShareView.h */, + 34CA631A2097806E00E526A0 /* OWSContactShareView.m */, 34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */, 34D1F09D1F867BFC0066283D /* OWSExpirationTimerView.h */, 34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */, @@ -3276,6 +3281,7 @@ 34A910601FFEB114000C4745 /* OWSBackup.m in Sources */, 34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */, 45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */, + 34CA631B2097806F00E526A0 /* OWSContactShareView.m in Sources */, 34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */, B90418E6183E9DD40038554A /* DateUtil.m in Sources */, 340FC8BD204DAC8D007AEB0F /* ShowGroupMembersViewController.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h new file mode 100644 index 000000000..4fd824a3a --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class OWSContactShare; + +@interface OWSContactShareView : UIView + +- (instancetype)initWithContactShare:(OWSContactShare *)contactShare + contactShareName:(NSString *)contactShareName + isIncoming:(BOOL)isIncoming; + +- (void)createContents; + ++ (CGFloat)bubbleHeight; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m new file mode 100644 index 000000000..97f40be75 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m @@ -0,0 +1,178 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSContactShareView.h" +#import "UIColor+JSQMessages.h" +#import "UIColor+OWS.h" +#import "UIFont+OWS.h" +#import "UIView+OWS.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSContactShareView () + +@property (nonatomic) OWSContactShare *contactShare; +@property (nonatomic) NSString *contactShareName; +@property (nonatomic) BOOL isIncoming; + +@end + +#pragma mark - + +@implementation OWSContactShareView + +- (instancetype)initWithContactShare:(OWSContactShare *)contactShare + contactShareName:(NSString *)contactShareName + isIncoming:(BOOL)isIncoming +{ + self = [super init]; + + if (self) { + _contactShare = contactShare; + _contactShareName = contactShareName; + _isIncoming = isIncoming; + } + + return self; +} + +#pragma mark - JSQMessageMediaData protocol + +- (CGFloat)iconHMargin +{ + return 12.f; +} + +- (CGFloat)iconHSpacing +{ + return 10.f; +} + ++ (CGFloat)iconVMargin +{ + return 12.f; +} + +- (CGFloat)iconVMargin +{ + return [OWSContactShareView iconVMargin]; +} + ++ (CGFloat)bubbleHeight +{ + return self.iconSize + self.iconVMargin * 2; +} + +- (CGFloat)bubbleHeight +{ + return [OWSContactShareView bubbleHeight]; +} + ++ (CGFloat)iconSize +{ + return 44.f; +} + +- (CGFloat)iconSize +{ + return [OWSContactShareView iconSize]; +} + +- (CGFloat)vMargin +{ + return 10.f; +} + +- (UIColor *)bubbleBackgroundColor +{ + return self.isIncoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_materialBlueColor]; +} + +- (void)createContents +{ + self.backgroundColor = [UIColor colorWithRGBHex:0xefeff4]; + self.layoutMargins = UIEdgeInsetsZero; + + // TODO: Verify that this layout works in RTL. + const CGFloat kBubbleTailWidth = 6.f; + + UIView *contentView = [UIView containerView]; + [self addSubview:contentView]; + [contentView autoPinLeadingToSuperviewMarginWithInset:self.isIncoming ? kBubbleTailWidth : 0.f]; + [contentView autoPinTrailingToSuperviewMarginWithInset:self.isIncoming ? 0.f : kBubbleTailWidth]; + [contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.vMargin]; + [contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.vMargin]; + + UIView *iconCircleView = [UIView containerView]; + iconCircleView.backgroundColor = [UIColor colorWithRGBHex:0x00ffff]; + iconCircleView.layer.cornerRadius = self.iconSize * 0.5f; + [iconCircleView autoSetDimension:ALDimensionWidth toSize:self.iconSize]; + [iconCircleView autoSetDimension:ALDimensionHeight toSize:self.iconSize]; + [iconCircleView setCompressionResistanceHigh]; + [iconCircleView setContentHuggingHigh]; + + // TODO: Use avatar, if present and downloaded. else default. + UIImage *image = [UIImage imageNamed:@"attachment_file"]; + OWSAssert(image); + UIImageView *imageView = [UIImageView new]; + imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + imageView.tintColor = self.bubbleBackgroundColor; + [iconCircleView addSubview:imageView]; + [imageView autoCenterInSuperview]; + + UILabel *topLabel = [UILabel new]; + topLabel.text = self.contactShareName; + topLabel.textColor = [UIColor blackColor]; + topLabel.lineBreakMode = NSLineBreakByTruncatingTail; + topLabel.font = [UIFont ows_dynamicTypeBodyFont]; + + UIStackView *labelsView = [UIStackView new]; + labelsView.axis = UILayoutConstraintAxisVertical; + labelsView.spacing = 2; + [labelsView addArrangedSubview:topLabel]; + + // TODO: Should we just try to show the _first_ phone number? + // What about email? + // What if the second phone number is a signal account? + NSString *_Nullable firstPhoneNumber = self.contactShare.phoneNumbers.firstObject.phoneNumber; + if (firstPhoneNumber.length > 0) { + UILabel *bottomLabel = [UILabel new]; + bottomLabel.text = firstPhoneNumber; + // TODO: + bottomLabel.textColor = [UIColor ows_darkGrayColor]; + bottomLabel.lineBreakMode = NSLineBreakByTruncatingTail; + bottomLabel.font = [UIFont ows_dynamicTypeCaption1Font]; + [labelsView addArrangedSubview:bottomLabel]; + } + + UIImage *disclosureImage = + [UIImage imageNamed:(self.isRTL ? @"system_disclosure_indicator_rtl" : @"system_disclosure_indicator")]; + OWSAssert(disclosureImage); + UIImageView *disclosureImageView = [UIImageView new]; + disclosureImageView.image = [disclosureImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + disclosureImageView.tintColor = [UIColor blackColor]; + [disclosureImageView setCompressionResistanceHigh]; + [disclosureImageView setContentHuggingHigh]; + + UIStackView *stackView = [UIStackView new]; + stackView.axis = UILayoutConstraintAxisHorizontal; + stackView.spacing = self.iconHSpacing; + stackView.alignment = UIStackViewAlignmentCenter; + [contentView addSubview:stackView]; + [stackView autoPinLeadingToSuperviewMarginWithInset:self.iconHMargin]; + [stackView autoPinTrailingToSuperviewMarginWithInset:self.iconHMargin]; + [stackView autoVCenterInSuperview]; + // NOTE: It's critical that we pin to the superview top and bottom _edge_ and not _margin_. + [stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; + [stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; + + [stackView addArrangedSubview:iconCircleView]; + [stackView addArrangedSubview:labelsView]; + [stackView addArrangedSubview:disclosureImageView]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 3e05346ce..f29db24c9 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 "OWSBubbleStrokeView.h" #import "OWSBubbleView.h" +#import "OWSContactShareView.h" #import "OWSGenericAttachmentView.h" #import "OWSMessageTextView.h" #import "OWSQuotedMessageView.h" @@ -194,7 +195,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - -- (BOOL)hasNonImageBodyContent +- (BOOL)hasBubbleBackground { switch (self.cellType) { case OWSMessageCellType_Unknown: @@ -202,6 +203,7 @@ NS_ASSUME_NONNULL_BEGIN case OWSMessageCellType_OversizeTextMessage: case OWSMessageCellType_GenericAttachment: case OWSMessageCellType_DownloadingAttachment: + case OWSMessageCellType_ShareContact: return YES; case OWSMessageCellType_StillImage: case OWSMessageCellType_AnimatedImage: @@ -226,6 +228,8 @@ NS_ASSUME_NONNULL_BEGIN case OWSMessageCellType_Video: // Is there a caption? return self.hasBodyText; + case OWSMessageCellType_ShareContact: + return NO; } } @@ -245,7 +249,7 @@ NS_ASSUME_NONNULL_BEGIN self.bubbleView.isOutgoing = self.isOutgoing; self.bubbleView.hideTail = self.viewItem.shouldHideBubbleTail && !self.alwaysShowBubbleTail; - if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && self.hasNonImageBodyContent) { + if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && self.hasBubbleBackground) { TSMessage *message = (TSMessage *)self.viewItem.interaction; self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; } else { @@ -327,6 +331,11 @@ NS_ASSUME_NONNULL_BEGIN bodyMediaView = [self loadViewForDownloadingAttachment]; bodyMediaViewHasGreedyWidth = YES; break; + case OWSMessageCellType_ShareContact: + // TODO: + bodyMediaView = [self loadViewForShareContact]; + bodyMediaViewHasGreedyWidth = YES; + break; } if (bodyMediaView) { @@ -784,6 +793,28 @@ NS_ASSUME_NONNULL_BEGIN return customView; } +- (UIView *)loadViewForShareContact +{ + OWSAssert(self.viewItem.contactShare); + OWSAssert(self.viewItem.contactShareName.length > 0); + + OWSContactShareView *contactShareView = + [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare + contactShareName:self.viewItem.contactShareName + isIncoming:self.isIncoming]; + [contactShareView createContents]; + // [self addAttachmentUploadViewIfNecessary:attachmentView]; + + self.loadCellContentBlock = ^{ + // Do nothing. + }; + self.unloadCellContentBlock = ^{ + // Do nothing. + }; + + return contactShareView; +} + - (void)addAttachmentUploadViewIfNecessary:(UIView *)attachmentView { [self addAttachmentUploadViewIfNecessary:attachmentView @@ -902,6 +933,8 @@ NS_ASSUME_NONNULL_BEGIN return CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]); case OWSMessageCellType_DownloadingAttachment: return CGSizeMake(200, 90); + case OWSMessageCellType_ShareContact: + return CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]); } } @@ -1022,6 +1055,10 @@ NS_ASSUME_NONNULL_BEGIN if (self.cellType == OWSMessageCellType_DownloadingAttachment) { return NO; } + if (self.cellType == OWSMessageCellType_ShareContact) { + // TODO: Handle this case. + return NO; + } if (!self.attachmentStream) { return NO; } @@ -1168,6 +1205,9 @@ NS_ASSUME_NONNULL_BEGIN } break; } + case OWSMessageCellType_ShareContact: + // TODO: + break; } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index dd175445e..78170e225 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -17,6 +17,7 @@ typedef NS_ENUM(NSInteger, OWSMessageCellType) { OWSMessageCellType_Video, OWSMessageCellType_GenericAttachment, OWSMessageCellType_DownloadingAttachment, + OWSMessageCellType_ShareContact, }; NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @@ -26,6 +27,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @class ConversationViewCell; @class DisplayableText; @class OWSAudioMessageView; +@class OWSContactShare; @class OWSQuotedReplyModel; @class TSAttachmentPointer; @class TSAttachmentStream; @@ -101,6 +103,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); // if a load has previously failed. @property (nonatomic) BOOL didCellMediaFailToLoad; +- (nullable OWSContactShare *)contactShare; +- (nullable NSString *)contactShareName; + #pragma mark - UIMenuController - (NSArray *)textMenuControllerItems; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 4070f192a..1404d99bb 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -11,6 +11,7 @@ #import "Signal-Swift.h" #import #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -36,6 +37,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return @"OWSMessageCellType_DownloadingAttachment"; case OWSMessageCellType_Unknown: return @"OWSMessageCellType_Unknown"; + case OWSMessageCellType_ShareContact: + return @"OWSMessageCellType_ShareContact"; } } @@ -62,6 +65,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @property (nonatomic, readonly, nullable) NSString *quotedRecipientId; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; +@property (nonatomic, nullable) OWSContactShare *contactShare; +@property (nonatomic, nullable) NSString *contactShareName; @property (nonatomic) CGSize mediaSize; @end @@ -401,6 +406,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.hasViewState = YES; TSMessage *message = (TSMessage *)self.interaction; + if (message.contactShare) { + // TODO: Format contact share name. + NSString *contactShareName = @"Alice"; + if (contactShareName.length > 0) { + self.contactShare = message.contactShare; + self.contactShareName = contactShareName; + self.messageCellType = OWSMessageCellType_ShareContact; + return; + } + } TSAttachment *_Nullable attachment = [self firstAttachmentIfAnyOfMessage:message transaction:transaction]; if (attachment) { if ([attachment isKindOfClass:[TSAttachmentStream class]]) { @@ -712,6 +727,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSFail(@"%@ No text to copy", self.logTag); break; } + case OWSMessageCellType_ShareContact: { + // TODO: Implement copy contact. + OWSFail(@"%@ Not implemented yet", self.logTag); + break; + } } } @@ -720,7 +740,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) switch (self.messageCellType) { case OWSMessageCellType_Unknown: case OWSMessageCellType_TextMessage: - case OWSMessageCellType_OversizeTextMessage: { + case OWSMessageCellType_OversizeTextMessage: + case OWSMessageCellType_ShareContact: { OWSFail(@"%@ No media to copy", self.logTag); break; } @@ -802,6 +823,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_Unknown: case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: + case OWSMessageCellType_ShareContact: return NO; case OWSMessageCellType_StillImage: case OWSMessageCellType_AnimatedImage: @@ -824,6 +846,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_Unknown: case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: + case OWSMessageCellType_ShareContact: OWSFail(@"%@ Cannot save text data.", self.logTag); break; case OWSMessageCellType_StillImage: @@ -879,6 +902,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_Unknown: case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: + case OWSMessageCellType_ShareContact: return NO; case OWSMessageCellType_StillImage: case OWSMessageCellType_AnimatedImage: