diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 7facedb81..f02203de2 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -224,6 +224,7 @@ 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0241ED3673300188D7C /* DebugUIMessages.m */; }; 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; }; 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */; }; + 34D920E220DD39EA00D51158 /* ConversationLayoutInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D920E120DD39E900D51158 /* ConversationLayoutInfo.swift */; }; 34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */; }; 34DB0BED2011548B007B313F /* OWSDatabaseConverterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DB0BEC2011548B007B313F /* OWSDatabaseConverterTest.m */; }; 34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */; }; @@ -881,6 +882,7 @@ 34D8C0291ED3685800188D7C /* DebugUIContacts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIContacts.h; sourceTree = ""; }; 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIContacts.m; sourceTree = ""; }; 34D913491F62D4A500722898 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalAttachment.swift; sourceTree = ""; }; + 34D920E120DD39E900D51158 /* ConversationLayoutInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationLayoutInfo.swift; sourceTree = ""; }; 34D99C8A1F27B13B00D284D6 /* OWSViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSViewController.h; sourceTree = ""; }; 34D99C8B1F27B13B00D284D6 /* OWSViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSViewController.m; sourceTree = ""; }; 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAnalytics.swift; sourceTree = ""; }; @@ -1496,10 +1498,12 @@ 34D1F0951F867BFC0066283D /* Cells */, 34D1F0B21F86D31D0066283D /* ConversationCollectionView.h */, 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */, + 45DDA6232090CEB500DE97F8 /* ConversationHeaderView.swift */, 34D1F0671F8678AA0066283D /* ConversationInputTextView.h */, 34D1F0681F8678AA0066283D /* ConversationInputTextView.m */, 34D1F0691F8678AA0066283D /* ConversationInputToolbar.h */, 34D1F06A1F8678AA0066283D /* ConversationInputToolbar.m */, + 34D920E120DD39E900D51158 /* ConversationLayoutInfo.swift */, 343A65971FC4CFE7000477A1 /* ConversationScrollButton.h */, 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */, 34D1F06D1F8678AA0066283D /* ConversationViewController.h */, @@ -1508,7 +1512,6 @@ 34D1F0701F8678AA0066283D /* ConversationViewItem.m */, 34D1F0711F8678AA0066283D /* ConversationViewLayout.h */, 34D1F0721F8678AA0066283D /* ConversationViewLayout.m */, - 45DDA6232090CEB500DE97F8 /* ConversationHeaderView.swift */, ); path = ConversationView; sourceTree = ""; @@ -3310,6 +3313,7 @@ 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, 45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */, 34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */, + 34D920E220DD39EA00D51158 /* ConversationLayoutInfo.swift in Sources */, 458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */, 45DDA6242090CEB500DE97F8 /* ConversationHeaderView.swift in Sources */, 45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 097669b62..9438fc35e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -4,6 +4,7 @@ NS_ASSUME_NONNULL_BEGIN +@class ConversationLayoutInfo; @class ConversationViewCell; @class ConversationViewItem; @class OWSContactOffersInteraction; @@ -69,12 +70,11 @@ NS_ASSUME_NONNULL_BEGIN // * Users enters another view (e.g. conversation settings view, call screen, etc.). @property (nonatomic) BOOL isCellVisible; -// The width of the collection view. -@property (nonatomic) int contentWidth; +@property (nonatomic, nullable) ConversationLayoutInfo *layoutInfo; - (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction; -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth; +- (CGSize)cellSize; @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m index e82863d8c..9ff980098 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m @@ -16,17 +16,18 @@ NS_ASSUME_NONNULL_BEGIN self.viewItem = nil; self.delegate = nil; self.isCellVisible = NO; - self.contentWidth = 0; + self.layoutInfo = nil; } - (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction { - OWSFail(@"%@ This method should be overridden.", self.logTag); + OWS_ABSTRACT_METHOD(); } -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth +- (CGSize)cellSize { - OWSFail(@"%@ This method should be overridden.", self.logTag); + OWS_ABSTRACT_METHOD(); + return CGSizeZero; } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m index 89af63251..6b022dad5 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m @@ -4,6 +4,7 @@ #import "OWSContactOffersCell.h" #import "ConversationViewItem.h" +#import "Signal-Swift.h" #import #import #import @@ -38,6 +39,11 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(!self.titleLabel); + self.preservesSuperviewLayoutMargins = NO; + self.contentView.preservesSuperviewLayoutMargins = NO; + self.layoutMargins = UIEdgeInsetsZero; + self.contentView.layoutMargins = UIEdgeInsetsZero; + // [self setTranslatesAutoresizingMaskIntoConstraints:NO]; self.titleLabel = [UILabel new]; @@ -169,15 +175,17 @@ NS_ASSUME_NONNULL_BEGIN layoutButton(self.blockButton, interaction.hasBlockOffer); } -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth +- (CGSize)cellSize { + OWSAssert(self.layoutInfo); + OWSAssert(self.layoutInfo.viewWidth > 0); OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]); OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction; // TODO: Should we use viewWidth? - CGSize result = CGSizeMake(viewWidth, 0); + CGSize result = CGSizeMake(self.layoutInfo.viewWidth, 0); result.height += self.topVMargin; result.height += self.bottomVMargin; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h index 5def0ab37..67131839b 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h @@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN @class ContactShareViewModel; +@class ConversationLayoutInfo; @class ConversationViewItem; @class OWSContact; @class OWSQuotedReplyModel; @@ -53,11 +54,13 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { @end +#pragma mark - + @interface OWSMessageBubbleView : UIView @property (nonatomic, nullable) ConversationViewItem *viewItem; -@property (nonatomic) int contentWidth; +@property (nonatomic) ConversationLayoutInfo *layoutInfo; @property (nonatomic) NSCache *cellMediaCache; @@ -78,7 +81,7 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { - (void)loadContent; - (void)unloadContent; -- (CGSize)sizeForContentWidth:(int)contentWidth; +- (CGSize)measureSize; - (void)prepareForReuse; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 017baa740..5b63a8638 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -237,14 +237,14 @@ NS_ASSUME_NONNULL_BEGIN - (void)configureViews { + OWSAssert(self.layoutInfo); OWSAssert(self.viewItem); OWSAssert(self.viewItem.interaction); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); - OWSAssert(self.contentWidth > 0); - CGSize quotedMessageContentSize = [self quotedMessageSizeForContentWidth:self.contentWidth includeMargins:NO]; - CGSize bodyMediaContentSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; - CGSize bodyTextContentSize = [self bodyTextSizeForContentWidth:self.contentWidth includeMargins:NO]; + CGSize quotedMessageContentSize = [self quotedMessageSize]; + CGSize bodyMediaContentSize = [self bodyMediaSize]; + CGSize bodyTextContentSize = [self bodyTextSize:NO]; self.bubbleView.isOutgoing = self.isOutgoing; self.bubbleView.hideTail = self.viewItem.shouldHideBubbleTail && !self.alwaysShowBubbleTail; @@ -855,18 +855,18 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Measurement // Size of "message body" text, not quoted reply text. -- (CGSize)bodyTextSizeForContentWidth:(int)contentWidth includeMargins:(BOOL)includeMargins +- (CGSize)bodyTextSize:(BOOL)includeMargins { + OWSAssert(self.layoutInfo); + OWSAssert(self.layoutInfo.maxMessageWidth > 0); + if (!self.hasBodyText) { return CGSizeZero; } - BOOL isRTL = self.isRTL; - CGFloat leftMargin = isRTL ? self.textTrailingMargin : self.textLeadingMargin; - CGFloat rightMargin = isRTL ? self.textLeadingMargin : self.textTrailingMargin; + CGFloat hMargins = self.textTrailingMargin + self.textLeadingMargin; - const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; - const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); + const int maxTextWidth = (int)floor(self.layoutInfo.maxMessageWidth - hMargins); OWSMessageTextView *bodyTextView = [self configureBodyTextView]; CGSize textSize = CGSizeCeil([bodyTextView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); @@ -874,22 +874,27 @@ NS_ASSUME_NONNULL_BEGIN CGSize result = textSize; if (includeMargins) { - result.width += leftMargin + rightMargin; + result.width += hMargins; result.height += self.textTopMargin + self.textBottomMargin; } return CGSizeCeil(result); } -- (CGSize)bodyMediaSizeForContentWidth:(int)contentWidth +- (CGSize)bodyMediaSize { - const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; + OWSAssert(self.layoutInfo); + OWSAssert(self.layoutInfo.maxMessageWidth > 0); + + CGFloat maxMessageWidth = self.layoutInfo.maxMessageWidth; + CGSize result = CGSizeZero; switch (self.cellType) { case OWSMessageCellType_Unknown: case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: { - return CGSizeZero; + result = CGSizeZero; + break; } case OWSMessageCellType_StillImage: case OWSMessageCellType_AnimatedImage: @@ -926,28 +931,32 @@ NS_ASSUME_NONNULL_BEGIN } return CGSizeRound(CGSizeMake(mediaWidth, mediaHeight)); + break; } case OWSMessageCellType_Audio: - return CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight); + result = CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight); + break; case OWSMessageCellType_GenericAttachment: - return CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]); + result = CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]); + break; case OWSMessageCellType_DownloadingAttachment: - return CGSizeMake(200, 90); + result = CGSizeMake(200, 90); + break; case OWSMessageCellType_ContactShare: OWSAssert(self.viewItem.contactShare); - return CGSizeMake( + result = CGSizeMake( maxMessageWidth, [OWSContactShareView bubbleHeightForContactShare:self.viewItem.contactShare]); + break; } -} -- (int)maxMessageWidthForContentWidth:(int)contentWidth -{ - return (int)floor(contentWidth * 0.8f); + return CGSizeCeil(result); } -- (CGSize)quotedMessageSizeForContentWidth:(int)contentWidth includeMargins:(BOOL)includeMargins +- (CGSize)quotedMessageSize { + OWSAssert(self.layoutInfo); + OWSAssert(self.layoutInfo.maxMessageWidth > 0); OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); @@ -963,31 +972,28 @@ NS_ASSUME_NONNULL_BEGIN [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply displayableQuotedText:displayableQuotedText isOutgoing:isOutgoing]; - const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; - CGSize result = [quotedMessageView sizeForMaxWidth:maxMessageWidth - kBubbleThornSideInset]; - if (includeMargins) { - result.width += kBubbleThornSideInset; - } - - return result; + CGSize result = [quotedMessageView sizeForMaxWidth:self.layoutInfo.maxMessageWidth]; + return CGSizeCeil(result); } -- (CGSize)sizeForContentWidth:(int)contentWidth +- (CGSize)measureSize { + OWSAssert(self.layoutInfo); + OWSAssert(self.layoutInfo.viewWidth > 0); OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); CGSize cellSize = CGSizeZero; - CGSize quotedMessageSize = [self quotedMessageSizeForContentWidth:contentWidth includeMargins:YES]; + CGSize quotedMessageSize = [self quotedMessageSize]; cellSize.width = MAX(cellSize.width, quotedMessageSize.width); cellSize.height += quotedMessageSize.height; - CGSize mediaContentSize = [self bodyMediaSizeForContentWidth:contentWidth]; + CGSize mediaContentSize = [self bodyMediaSize]; cellSize.width = MAX(cellSize.width, mediaContentSize.width); cellSize.height += mediaContentSize.height; - CGSize textContentSize = [self bodyTextSizeForContentWidth:contentWidth includeMargins:YES]; + CGSize textContentSize = [self bodyTextSize:YES]; cellSize.width = MAX(cellSize.width, textContentSize.width); cellSize.height += textContentSize.height; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 3ad7f432a..b96ab982e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -51,6 +51,9 @@ NS_ASSUME_NONNULL_BEGIN // Ensure only called once. OWSAssert(!self.messageBubbleView); + self.preservesSuperviewLayoutMargins = NO; + self.contentView.preservesSuperviewLayoutMargins = NO; + _viewConstraints = [NSMutableArray new]; self.layoutMargins = UIEdgeInsetsZero; @@ -97,6 +100,13 @@ NS_ASSUME_NONNULL_BEGIN [self addGestureRecognizer:panGesture]; } +- (void)setLayoutInfo:(nullable ConversationLayoutInfo *)layoutInfo +{ + [super setLayoutInfo:layoutInfo]; + + self.messageBubbleView.layoutInfo = layoutInfo; +} + + (NSString *)cellReuseIdentifier { return NSStringFromClass([self class]); @@ -152,14 +162,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction { + OWSAssert(self.layoutInfo); OWSAssert(self.viewItem); OWSAssert(self.viewItem.interaction); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); - OWSAssert(self.contentWidth > 0); OWSAssert(self.messageBubbleView); self.messageBubbleView.viewItem = self.viewItem; - self.messageBubbleView.contentWidth = self.contentWidth; self.messageBubbleView.cellMediaCache = self.delegate.cellMediaCache; [self.messageBubbleView configureViews]; [self.messageBubbleView loadContent]; @@ -209,7 +218,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateDateHeader { - OWSAssert(self.contentWidth > 0); + OWSAssert(self.layoutInfo); static NSDateFormatter *dateHeaderDateFormatter = nil; static NSDateFormatter *dateHeaderTimeFormatter = nil; @@ -256,11 +265,9 @@ NS_ASSUME_NONNULL_BEGIN self.dateHeaderLabel.hidden = NO; [self.viewConstraints addObjectsFromArray:@[ - // Date headers should be visually centered within the conversation view, - // so they need to extend outside the cell's boundaries. - [self.dateHeaderLabel autoSetDimension:ALDimensionWidth toSize:self.contentWidth], - (self.isIncoming ? [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeLeading] - : [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]), + // TODO: Are data headers symmetric or are they asymmetric? gutters are asymmetric? + [self.dateHeaderLabel autoPinLeadingToSuperviewMarginWithInset:self.layoutInfo.gutterLeading], + [self.dateHeaderLabel autoPinTrailingToSuperviewMarginWithInset:self.layoutInfo.gutterTrailing], [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop], [self.dateHeaderLabel autoSetDimension:ALDimensionHeight toSize:self.dateHeaderHeight], ]]; @@ -362,7 +369,7 @@ NS_ASSUME_NONNULL_BEGIN // we want to leave spaces for an expiration timer and // include padding so that they still visually "cling" to the // appropriate incoming/outgoing edge. - const CGFloat maxFooterLabelWidth = self.contentWidth - 100; + const CGFloat maxFooterLabelWidth = self.layoutInfo.maxFooterWidth; if (hasExpirationTimer && attributedText) { [self.viewConstraints addObjectsFromArray:@[ @@ -411,16 +418,18 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Measurement -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth + +- (CGSize)cellSize { + OWSAssert(self.layoutInfo); + OWSAssert(self.layoutInfo.viewWidth > 0); OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); OWSAssert(self.messageBubbleView); self.messageBubbleView.viewItem = self.viewItem; - self.messageBubbleView.contentWidth = self.contentWidth; self.messageBubbleView.cellMediaCache = self.delegate.cellMediaCache; - CGSize messageBubbleSize = [self.messageBubbleView sizeForContentWidth:contentWidth]; + CGSize messageBubbleSize = [self.messageBubbleView measureSize]; CGSize cellSize = messageBubbleSize; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m index 2a3e4e9f0..ef28933de 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m @@ -4,6 +4,7 @@ #import "OWSSystemMessageCell.h" #import "ConversationViewItem.h" +#import "Signal-Swift.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" @@ -22,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIImageView *imageView; @property (nonatomic) UILabel *titleLabel; +@property (nonatomic) UIStackView *stackView; +@property (nonatomic) NSArray *layoutConstraints; @end @@ -43,19 +46,31 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(!self.imageView); - [self setTranslatesAutoresizingMaskIntoConstraints:NO]; + self.preservesSuperviewLayoutMargins = NO; + self.contentView.preservesSuperviewLayoutMargins = NO; + self.layoutMargins = UIEdgeInsetsZero; + self.contentView.layoutMargins = UIEdgeInsetsZero; self.backgroundColor = [UIColor whiteColor]; self.imageView = [UIImageView new]; - [self.contentView addSubview:self.imageView]; + [self.imageView autoSetDimension:ALDimensionWidth toSize:self.iconSize]; + [self.imageView autoSetDimension:ALDimensionHeight toSize:self.iconSize]; + [self.imageView setContentHuggingHigh]; self.titleLabel = [UILabel new]; self.titleLabel.textColor = [UIColor colorWithRGBHex:0x403e3b]; - self.titleLabel.font = [self titleFont]; self.titleLabel.numberOfLines = 0; self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; - [self.contentView addSubview:self.titleLabel]; + + self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.imageView, + self.titleLabel, + ]]; + self.stackView.axis = UILayoutConstraintAxisHorizontal; + self.stackView.spacing = self.hSpacing; + self.stackView.alignment = UIStackViewAlignmentCenter; + [self.contentView addSubview:self.stackView]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; @@ -66,6 +81,12 @@ NS_ASSUME_NONNULL_BEGIN [self addGestureRecognizer:longPress]; } +- (void)configureFonts +{ + // Update cell to reflect changes in dynamic text. + self.titleLabel.font = UIFont.ows_dynamicTypeFootnoteFont; +} + + (NSString *)cellReuseIdentifier { return NSStringFromClass([self class]); @@ -73,6 +94,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction { + OWSAssert(self.layoutInfo); OWSAssert(self.viewItem); TSInteraction *interaction = self.viewItem.interaction; @@ -81,9 +103,27 @@ NS_ASSUME_NONNULL_BEGIN self.imageView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; self.imageView.tintColor = [self iconColorForInteraction:interaction]; self.titleLabel.textColor = [self textColor]; - [self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction]; - - [self setNeedsLayout]; + [self applyTitleForInteraction:interaction label:self.titleLabel]; + + CGSize titleSize = [self titleSize]; + + [NSLayoutConstraint deactivateConstraints:self.layoutConstraints]; + self.layoutConstraints = @[ + [self.titleLabel autoSetDimension:ALDimensionWidth toSize:titleSize.width], + [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin], + [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin], + [self.stackView autoHCenterInSuperview], + [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading + withInset:self.layoutInfo.fullWidthGutterLeading + self.hMargin + relation:NSLayoutRelationGreaterThanOrEqual], + [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing + withInset:self.layoutInfo.fullWidthGutterTrailing + self.hMargin + relation:NSLayoutRelationGreaterThanOrEqual], + ]; + + [self.contentView logFrameLaterWithLabel:@"contentView"]; + [self.stackView logFrameLaterWithLabel:@"stackView"]; + [self.titleLabel logFrameLaterWithLabel:@"titleLabel"]; } - (UIColor *)textColor @@ -162,19 +202,17 @@ NS_ASSUME_NONNULL_BEGIN - (void)applyTitleForInteraction:(TSInteraction *)interaction label:(UILabel *)label - transaction:(YapDatabaseReadTransaction *)transaction { OWSAssert(interaction); OWSAssert(label); - // Update cell to reflect changes in dynamic text. - self.titleLabel.font = [self titleFont]; + [self configureFonts]; // TODO: Should we move the copy generation into this view? if ([interaction isKindOfClass:[TSErrorMessage class]]) { TSErrorMessage *errorMessage = (TSErrorMessage *)interaction; - label.text = [errorMessage previewTextWithTransaction:transaction]; + label.text = [errorMessage previewText]; } else if ([interaction isKindOfClass:[TSInfoMessage class]]) { TSInfoMessage *infoMessage = (TSInfoMessage *)interaction; if ([infoMessage isKindOfClass:[OWSVerificationStateChangeMessage class]]) { @@ -199,22 +237,17 @@ NS_ASSUME_NONNULL_BEGIN @"another device. Embeds {{user's name or phone number}}."))); label.text = [NSString stringWithFormat:titleFormat, displayName]; } else { - label.text = [infoMessage previewTextWithTransaction:transaction]; + label.text = [infoMessage previewText]; } } else if ([interaction isKindOfClass:[TSCall class]]) { TSCall *call = (TSCall *)interaction; - label.text = [call previewTextWithTransaction:transaction]; + label.text = [call previewText]; } else { OWSFail(@"Unknown interaction type: %@", [interaction class]); label.text = nil; } } -- (UIFont *)titleFont -{ - return UIFont.ows_dynamicTypeFootnoteFont; -} - - (CGFloat)hMargin { return 25.f; @@ -240,48 +273,30 @@ NS_ASSUME_NONNULL_BEGIN return 20.f; } -- (void)layoutSubviews +- (CGSize)titleSize { - [super layoutSubviews]; - - CGFloat maxTitleWidth = (self.contentView.width - ([self hMargin] * 2.f + [self hSpacing] + [self iconSize])); - CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)]; - - CGFloat contentWidth = ([self iconSize] + [self hSpacing] + titleSize.width); - - CGFloat contentLeft = round((self.contentView.width - contentWidth) * 0.5f); - CGFloat imageLeft = ([self isRTL] ? round(contentLeft + contentWidth - [self iconSize]) : contentLeft); - CGFloat titleLeft = ([self isRTL] ? contentLeft : round(imageLeft + [self iconSize] + [self hSpacing])); - - self.imageView.frame = CGRectMake( - imageLeft, round((self.contentView.height - [self iconSize]) * 0.5f), [self iconSize], [self iconSize]); + OWSAssert(self.layoutInfo); + OWSAssert(self.viewItem); - self.titleLabel.frame = CGRectMake(titleLeft, - round((self.contentView.height - titleSize.height) * 0.5f), - ceil(titleSize.width + 1.f), - ceil(titleSize.height + 1.f)); + CGFloat maxTitleWidth + = (CGFloat)floor(self.layoutInfo.fullWidthContentWidth - (2 * self.hMargin + self.iconSize + self.hSpacing)); + return [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)]; } -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth +- (CGSize)cellSize { + OWSAssert(self.layoutInfo); OWSAssert(self.viewItem); TSInteraction *interaction = self.viewItem.interaction; - CGSize result = CGSizeMake(contentWidth, 0); - result.height += self.topVMargin; - result.height += self.bottomVMargin; - - // FIXME pass in transaction from the uiDBConnection. - [[TSYapDatabaseObject dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { - [self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction]; - }]; + CGSize result = CGSizeMake(self.layoutInfo.viewWidth, 0); - CGFloat maxTitleWidth = (contentWidth - ([self hMargin] * 2.f + [self hSpacing] + [self iconSize])); - CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)]; + [self applyTitleForInteraction:interaction label:self.titleLabel]; + CGSize titleSize = [self titleSize]; CGFloat contentHeight = ceil(MAX([self iconSize], titleSize.height)); - result.height += contentHeight; + result.height = (contentHeight + self.topVMargin + self.bottomVMargin); return result; } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m index 780147697..32426b216 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m @@ -4,6 +4,7 @@ #import "OWSUnreadIndicatorCell.h" #import "ConversationViewItem.h" +#import "Signal-Swift.h" #import #import #import @@ -15,12 +16,14 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) TSUnreadIndicatorInteraction *interaction; -@property (nonatomic) UIView *bannerView; -@property (nonatomic) UIView *bannerTopHighlightView; -@property (nonatomic) UIView *bannerBottomHighlightView1; -@property (nonatomic) UIView *bannerBottomHighlightView2; +// TODO: +//@property (nonatomic) UIView *bannerView; +//@property (nonatomic) UIView *bannerTopHighlightView; +//@property (nonatomic) UIView *bannerBottomHighlightView1; +//@property (nonatomic) UIView *bannerBottomHighlightView2; @property (nonatomic) UILabel *titleLabel; -@property (nonatomic) UILabel *subtitleLabel; +//@property (nonatomic) UILabel *subtitleLabel; +@property (nonatomic) NSArray *layoutConstraints; @end @@ -40,41 +43,64 @@ NS_ASSUME_NONNULL_BEGIN - (void)commontInit { - OWSAssert(!self.bannerView); + OWSAssert(!self.titleLabel); - [self setTranslatesAutoresizingMaskIntoConstraints:NO]; + self.preservesSuperviewLayoutMargins = NO; + self.contentView.preservesSuperviewLayoutMargins = NO; + self.layoutMargins = UIEdgeInsetsZero; + self.contentView.layoutMargins = UIEdgeInsetsZero; - self.backgroundColor = [UIColor whiteColor]; + // self.backgroundColor = [UIColor whiteColor]; - self.bannerView = [UIView new]; - self.bannerView.backgroundColor = [UIColor colorWithRGBHex:0xf6eee3]; - [self.contentView addSubview:self.bannerView]; + // self.bannerView = [UIView new]; + // self.bannerView.backgroundColor = [UIColor colorWithRGBHex:0xf6eee3]; + // [self.contentView addSubview:self.bannerView]; + // + // self.bannerTopHighlightView = [UIView new]; + // self.bannerTopHighlightView.backgroundColor = [UIColor colorWithRGBHex:0xf9f3eb]; + // [self.bannerView addSubview:self.bannerTopHighlightView]; + // + // self.bannerBottomHighlightView1 = [UIView new]; + // self.bannerBottomHighlightView1.backgroundColor = [UIColor colorWithRGBHex:0xd1c6b8]; + // [self.bannerView addSubview:self.bannerBottomHighlightView1]; + // + // self.bannerBottomHighlightView2 = [UIView new]; + // self.bannerBottomHighlightView2.backgroundColor = [UIColor colorWithRGBHex:0xdbcfc0]; + // [self.bannerView addSubview:self.bannerBottomHighlightView2]; - self.bannerTopHighlightView = [UIView new]; - self.bannerTopHighlightView.backgroundColor = [UIColor colorWithRGBHex:0xf9f3eb]; - [self.bannerView addSubview:self.bannerTopHighlightView]; + self.titleLabel = [UILabel new]; + self.titleLabel.textColor = [UIColor colorWithRGBHex:0x403e3b]; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + [self.contentView addSubview:self.titleLabel]; + + // self.subtitleLabel = [UILabel new]; + // self.subtitleLabel.textColor = [UIColor ows_infoMessageBorderColor]; + // self.subtitleLabel.font = [self subtitleFont]; + // // The subtitle may wrap to a second line. + // self.subtitleLabel.numberOfLines = 0; + // self.subtitleLabel.lineBreakMode = NSLineBreakByWordWrapping; + // self.subtitleLabel.textAlignment = NSTextAlignmentCenter; + // [self.contentView addSubview:self.subtitleLabel]; + + [self configureFonts]; +} - self.bannerBottomHighlightView1 = [UIView new]; - self.bannerBottomHighlightView1.backgroundColor = [UIColor colorWithRGBHex:0xd1c6b8]; - [self.bannerView addSubview:self.bannerBottomHighlightView1]; +- (void)configureFonts +{ + // Update cell to reflect changes in dynamic text. + self.titleLabel.font = UIFont.ows_dynamicTypeBodyFont; + // self.subtitleLabel.font = [self subtitleFont]; - self.bannerBottomHighlightView2 = [UIView new]; - self.bannerBottomHighlightView2.backgroundColor = [UIColor colorWithRGBHex:0xdbcfc0]; - [self.bannerView addSubview:self.bannerBottomHighlightView2]; - self.titleLabel = [UILabel new]; - self.titleLabel.textColor = [UIColor colorWithRGBHex:0x403e3b]; - self.titleLabel.font = [self titleFont]; - [self.bannerView addSubview:self.titleLabel]; - - self.subtitleLabel = [UILabel new]; - self.subtitleLabel.textColor = [UIColor ows_infoMessageBorderColor]; - self.subtitleLabel.font = [self subtitleFont]; - // The subtitle may wrap to a second line. - self.subtitleLabel.numberOfLines = 0; - self.subtitleLabel.lineBreakMode = NSLineBreakByWordWrapping; - self.subtitleLabel.textAlignment = NSTextAlignmentCenter; - [self.contentView addSubview:self.subtitleLabel]; + // - (UIFont *)titleFont + // { + // return UIFont.ows_dynamicTypeBodyFont; + // } + // + // - (UIFont *)subtitleFont + // { + // return UIFont.ows_dynamicTypeCaption1Font; + // } } + (NSString *)cellReuseIdentifier @@ -84,31 +110,29 @@ NS_ASSUME_NONNULL_BEGIN - (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction { + OWSAssert(self.layoutInfo); OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]); + [self configureFonts]; + TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)self.viewItem.interaction; self.titleLabel.text = [self titleForInteraction:interaction]; - self.subtitleLabel.text = [self subtitleForInteraction:interaction]; - - // Update cell to reflect changes in dynamic text. - self.titleLabel.font = [self titleFont]; - self.subtitleLabel.font = [self subtitleFont]; + // self.subtitleLabel.text = [self subtitleForInteraction:interaction]; self.backgroundColor = [UIColor whiteColor]; - [self setNeedsLayout]; -} + [NSLayoutConstraint deactivateConstraints:self.layoutConstraints]; + self.layoutConstraints = @[ + // TODO: Constants. + [self.titleLabel autoVCenterInSuperview], + [self.titleLabel autoPinLeadingToSuperviewMarginWithInset:self.layoutInfo.fullWidthGutterLeading], + [self.titleLabel autoPinTrailingToSuperviewMarginWithInset:self.layoutInfo.fullWidthGutterTrailing], + ]; -- (UIFont *)titleFont -{ - return UIFont.ows_dynamicTypeBodyFont; -} - -- (UIFont *)subtitleFont -{ - return UIFont.ows_dynamicTypeCaption1Font; + // TODO: + // [self setNeedsLayout]; } - (NSString *)titleForInteraction:(TSUnreadIndicatorInteraction *)interaction @@ -117,126 +141,130 @@ NS_ASSUME_NONNULL_BEGIN .uppercaseString; } -- (NSString *)subtitleForInteraction:(TSUnreadIndicatorInteraction *)interaction -{ - if (!interaction.hasMoreUnseenMessages) { - return nil; - } - NSString *subtitleFormat = (interaction.missingUnseenSafetyNumberChangeCount > 0 - ? NSLocalizedString(@"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES_FORMAT", - @"Messages that indicates that there are more unseen messages that be revealed by tapping the 'load " - @"earlier messages' button. Embeds {{the name of the 'load earlier messages' button}}") - : NSLocalizedString( - @"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES_AND_SAFETY_NUMBER_CHANGES_FORMAT", - @"Messages that indicates that there are more unseen messages including safety number changes that " - @"be revealed by tapping the 'load earlier messages' button. Embeds {{the name of the 'load earlier " - @"messages' button}}.")); - NSString *loadMoreButtonName = NSLocalizedString( - @"load_earlier_messages", @"Label for button that loads more messages in conversation view."); - return [NSString stringWithFormat:subtitleFormat, loadMoreButtonName]; -} - -- (CGFloat)subtitleHMargin -{ - return 20.f; -} - -- (CGFloat)subtitleVSpacing -{ - return 3.f; -} - -- (CGFloat)titleInnerHMargin -{ - return 10.f; -} - -- (CGFloat)titleVMargin -{ - return 5.5f; -} - -- (CGFloat)topVMargin -{ - return 5.f; -} - -- (CGFloat)bottomVMargin -{ - return 5.f; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - [self.titleLabel sizeToFit]; - - // It's a bit of a hack, but we use a view that extends _outside_ the cell's bounds - // to draw its background, since we want the background to extend to the edges of the - // collection view. - // - // This layout logic assumes that the cell insets are symmetrical and can be deduced - // from the cell frame. - CGRect bannerViewFrame = CGRectMake(-self.left, - round(self.topVMargin), - round(self.width + self.left * 2.f), - round(self.titleLabel.height + self.titleVMargin * 2.f)); - self.bannerView.frame = [self convertRect:bannerViewFrame toView:self.contentView]; - - // The highlights should be 1px (not 1pt), so adapt their thickness to - // the device resolution. - CGFloat kHighlightThickness = 1.f / [UIScreen mainScreen].scale; - self.bannerTopHighlightView.frame = CGRectMake(0, 0, self.bannerView.width, kHighlightThickness); - self.bannerBottomHighlightView1.frame - = CGRectMake(0, self.bannerView.height - kHighlightThickness * 2.f, self.bannerView.width, kHighlightThickness); - self.bannerBottomHighlightView2.frame - = CGRectMake(0, self.bannerView.height - kHighlightThickness * 1.f, self.bannerView.width, kHighlightThickness); - - [self.titleLabel centerOnSuperview]; - - if (self.subtitleLabel.text.length > 0) { - CGSize subtitleSize = [self.subtitleLabel - sizeThatFits:CGSizeMake(self.contentView.width - [self subtitleHMargin] * 2.f, CGFLOAT_MAX)]; - self.subtitleLabel.frame = CGRectMake(round((self.contentView.width - subtitleSize.width) * 0.5f), - round(self.bannerView.bottom + self.subtitleVSpacing), - ceil(subtitleSize.width), - ceil(subtitleSize.height)); - } -} - -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth +//- (NSString *)subtitleForInteraction:(TSUnreadIndicatorInteraction *)interaction +//{ +// if (!interaction.hasMoreUnseenMessages) { +// return nil; +// } +// NSString *subtitleFormat = (interaction.missingUnseenSafetyNumberChangeCount > 0 +// ? NSLocalizedString(@"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES_FORMAT", +// @"Messages that indicates that there are more unseen messages that be revealed by tapping the 'load +// " +// @"earlier messages' button. Embeds {{the name of the 'load earlier messages' button}}") +// : NSLocalizedString( +// @"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES_AND_SAFETY_NUMBER_CHANGES_FORMAT", +// @"Messages that indicates that there are more unseen messages including safety number changes that " +// @"be revealed by tapping the 'load earlier messages' button. Embeds {{the name of the 'load earlier +// " +// @"messages' button}}.")); +// NSString *loadMoreButtonName = NSLocalizedString( +// @"load_earlier_messages", @"Label for button that loads more messages in conversation view."); +// return [NSString stringWithFormat:subtitleFormat, loadMoreButtonName]; +//} + +//- (CGFloat)subtitleHMargin +//{ +// return 20.f; +//} +// +//- (CGFloat)subtitleVSpacing +//{ +// return 3.f; +//} +// +//- (CGFloat)titleInnerHMargin +//{ +// return 10.f; +//} +// +//- (CGFloat)titleVMargin +//{ +// return 5.5f; +//} +// +//- (CGFloat)topVMargin +//{ +// return 5.f; +//} +// +//- (CGFloat)bottomVMargin +//{ +// return 5.f; +//} + +//- (void)layoutSubviews +//{ +// [super layoutSubviews]; +// +// [self.titleLabel sizeToFit]; +// +// // It's a bit of a hack, but we use a view that extends _outside_ the cell's bounds +// // to draw its background, since we want the background to extend to the edges of the +// // collection view. +// // +// // This layout logic assumes that the cell insets are symmetrical and can be deduced +// // from the cell frame. +// CGRect bannerViewFrame = CGRectMake(-self.left, +// round(self.topVMargin), +// round(self.width + self.left * 2.f), +// round(self.titleLabel.height + self.titleVMargin * 2.f)); +// self.bannerView.frame = [self convertRect:bannerViewFrame toView:self.contentView]; +// +// // The highlights should be 1px (not 1pt), so adapt their thickness to +// // the device resolution. +// CGFloat kHighlightThickness = 1.f / [UIScreen mainScreen].scale; +// self.bannerTopHighlightView.frame = CGRectMake(0, 0, self.bannerView.width, kHighlightThickness); +// self.bannerBottomHighlightView1.frame +// = CGRectMake(0, self.bannerView.height - kHighlightThickness * 2.f, self.bannerView.width, +// kHighlightThickness); +// self.bannerBottomHighlightView2.frame +// = CGRectMake(0, self.bannerView.height - kHighlightThickness * 1.f, self.bannerView.width, +// kHighlightThickness); +// +// [self.titleLabel centerOnSuperview]; +// +// if (self.subtitleLabel.text.length > 0) { +// CGSize subtitleSize = [self.subtitleLabel +// sizeThatFits:CGSizeMake(self.contentView.width - [self subtitleHMargin] * 2.f, CGFLOAT_MAX)]; +// self.subtitleLabel.frame = CGRectMake(round((self.contentView.width - subtitleSize.width) * 0.5f), +// round(self.bannerView.bottom + self.subtitleVSpacing), +// ceil(subtitleSize.width), +// ceil(subtitleSize.height)); +// } +//} + +- (CGSize)cellSize { + OWSAssert(self.layoutInfo); OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]); - TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)self.viewItem.interaction; - - // Update cell to reflect changes in dynamic text. - self.titleLabel.font = [self titleFont]; - self.subtitleLabel.font = [self subtitleFont]; + [self configureFonts]; - // TODO: Should we use viewWidth? - CGSize result = CGSizeMake(viewWidth, 0); - result.height += self.titleVMargin * 2.f; - result.height += self.topVMargin; - result.height += self.bottomVMargin; + // TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)self.viewItem.interaction; - NSString *title = [self titleForInteraction:interaction]; - NSString *subtitle = [self subtitleForInteraction:interaction]; - - self.titleLabel.text = title; - result.height += ceil([self.titleLabel sizeThatFits:CGSizeZero].height); - - if (subtitle.length > 0) { - result.height += self.subtitleVSpacing; - - self.subtitleLabel.text = subtitle; - result.height += ceil( - [self.subtitleLabel sizeThatFits:CGSizeMake(viewWidth - self.subtitleHMargin * 2.f, CGFLOAT_MAX)].height); - } + // TODO: + CGSize result = CGSizeMake(self.layoutInfo.fullWidthContentWidth, self.titleLabel.font.lineHeight + 24.f * 2); + // result.height += self.titleVMargin * 2.f; + // result.height += self.topVMargin; + // result.height += self.bottomVMargin; + // + // NSString *title = [self titleForInteraction:interaction]; + // NSString *subtitle = [self subtitleForInteraction:interaction]; + // + // self.titleLabel.text = title; + // result.height += ceil([self.titleLabel sizeThatFits:CGSizeZero].height); + // + // if (subtitle.length > 0) { + // result.height += self.subtitleVSpacing; + // + // self.subtitleLabel.text = subtitle; + // result.height += ceil( + // [self.subtitleLabel sizeThatFits:CGSizeMake(self.layoutInfo.fullWidthContentWidth - + // self.subtitleHMargin * 2.f, CGFLOAT_MAX)].height); + // } - return result; + return CGSizeCeil(result); } - (void)prepareForReuse diff --git a/Signal/src/ViewControllers/ConversationView/ConversationLayoutInfo.swift b/Signal/src/ViewControllers/ConversationView/ConversationLayoutInfo.swift new file mode 100644 index 000000000..ab026a696 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/ConversationLayoutInfo.swift @@ -0,0 +1,73 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class ConversationLayoutInfo: NSObject { + + private let thread: TSThread + + private let isRTL: Bool + + // The width of the collection view. + @objc public var viewWidth: CGFloat = 0 { + didSet { + SwiftAssertIsOnMainThread(#function) + + updateProperties() + } + } + + @objc public let contentMarginTop: CGFloat = 0 + @objc public let contentMarginBottom: CGFloat = 0 + + @objc public var gutterLeading: CGFloat = 0 + @objc public var gutterTrailing: CGFloat = 0 + // These are the gutters used by "full width" views + // like "date headers" and "unread indicator". + @objc public var fullWidthGutterLeading: CGFloat = 0 + @objc public var fullWidthGutterTrailing: CGFloat = 0 + + // viewWidth - (gutterLeading + gutterTrailing) + @objc public var contentWidth: CGFloat = 0 + + // viewWidth - (gutterfullWidthGutterLeadingLeading + fullWidthGutterTrailing) + // TODO: Is this necessary? + @objc public var fullWidthContentWidth: CGFloat = 0 + + @objc public var maxMessageWidth: CGFloat = 0 + @objc public var maxFooterWidth: CGFloat = 0 + + @objc + public required init(thread: TSThread) { + + self.thread = thread + self.isRTL = CurrentAppContext().isRTL + + super.init() + + updateProperties() + } + + private func updateProperties() { + if thread.isGroupThread() { + gutterLeading = 16 + gutterTrailing = 20 + } else { + gutterLeading = 40 + gutterTrailing = 20 + } + // TODO: Should these be symmetric? + fullWidthGutterLeading = gutterLeading + fullWidthGutterTrailing = gutterTrailing + + contentWidth = viewWidth - (gutterLeading + gutterTrailing) + + fullWidthContentWidth = viewWidth - (fullWidthGutterLeading + fullWidthGutterTrailing) + + maxMessageWidth = floor(contentWidth * 0.8) + maxFooterWidth = floor(contentWidth - 100) + } +} diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 2926a492d..bbfd985e6 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -174,6 +174,7 @@ typedef enum : NSUInteger { @property (nonatomic, readonly) ConversationInputToolbar *inputToolbar; @property (nonatomic, readonly) ConversationCollectionView *collectionView; @property (nonatomic, readonly) ConversationViewLayout *layout; +@property (nonatomic, readonly) ConversationLayoutInfo *layoutInfo; @property (nonatomic) NSArray *viewItems; @property (nonatomic) NSMutableDictionary *viewItemCache; @@ -217,7 +218,6 @@ typedef enum : NSUInteger { @property (nonatomic) UILabel *loadMoreHeader; @property (nonatomic) uint64_t lastVisibleTimestamp; -@property (nonatomic, readonly) BOOL isGroupConversation; @property (nonatomic) BOOL isUserScrolling; @property (nonatomic) NSLayoutConstraint *scrollDownButtonButtomConstraint; @@ -356,6 +356,13 @@ typedef enum : NSUInteger { object:nil]; } +- (BOOL)isGroupConversation +{ + OWSAssert(self.thread); + + return self.thread.isGroupThread; +} + - (void)signalAccountsDidChange:(NSNotification *)notification { OWSAssertIsOnMainThread(); @@ -435,12 +442,12 @@ typedef enum : NSUInteger { OWSAssert(thread); _thread = thread; - _isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]]; self.actionOnOpen = action; self.focusMessageIdOnOpen = focusMessageId; _cellMediaCache = [NSCache new]; // Cache the cell media for ~24 cells. self.cellMediaCache.countLimit = 24; + _layoutInfo = [[ConversationLayoutInfo alloc] initWithThread:thread]; // We need to update the "unread indicator" _before_ we determine the initial range // size, since it depends on where the unread indicator is placed. @@ -526,7 +533,11 @@ typedef enum : NSUInteger { - (void)createContents { - _layout = [ConversationViewLayout new]; + OWSAssert(self.layoutInfo); + + _layout = [[ConversationViewLayout alloc] initWithLayoutInfo:self.layoutInfo]; + self.layoutInfo.viewWidth = self.view.width; + self.layout.delegate = self; // We use the root view bounds as the initial frame for the collection // view so that its contents can be laid out immediately. @@ -982,15 +993,16 @@ typedef enum : NSUInteger { @"numbers of multiple users.") : NSLocalizedString(@"VERIFY_PRIVACY", @"Label for button or row which allows users to verify the safety " - @"number of another user."))style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + @"number of another user.")) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { [weakSelf showNoLongerVerifiedUI]; }]; [actionSheetController addAction:verifyAction]; UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.dismissButton style:UIAlertActionStyleCancel - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [weakSelf resetVerificationStateToDefault]; }]; [actionSheetController addAction:dismissAction]; @@ -1656,7 +1668,7 @@ typedef enum : NSUInteger { - (void)updateDisappearingMessagesConfiguration { - [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { self.disappearingMessagesConfiguration = [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId transaction:transaction]; }]; @@ -1747,7 +1759,7 @@ typedef enum : NSUInteger { UIAlertAction *deleteMessageAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", @"") style:UIAlertActionStyleDestructive - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [message remove]; }]; [actionSheetController addAction:deleteMessageAction]; @@ -1755,17 +1767,17 @@ typedef enum : NSUInteger { UIAlertAction *retryAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"MESSAGES_VIEW_FAILED_DOWNLOAD_RETRY_ACTION", @"Action sheet button text") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { OWSAttachmentsProcessor *processor = [[OWSAttachmentsProcessor alloc] initWithAttachmentPointer:attachmentPointer networkManager:self.networkManager]; [processor fetchAttachmentsForMessage:message primaryStorage:self.primaryStorage - success:^(TSAttachmentStream *_Nonnull attachmentStream) { + success:^(TSAttachmentStream *attachmentStream) { DDLogInfo( @"%@ Successfully redownloaded attachment in thread: %@", self.logTag, message.thread); } - failure:^(NSError *_Nonnull error) { + failure:^(NSError *error) { DDLogWarn(@"%@ Failed to redownload message with error: %@", self.logTag, error); }]; }]; @@ -1787,7 +1799,7 @@ typedef enum : NSUInteger { UIAlertAction *deleteMessageAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", @"") style:UIAlertActionStyleDestructive - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [message remove]; }]; [actionSheetController addAction:deleteMessageAction]; @@ -1795,12 +1807,12 @@ typedef enum : NSUInteger { UIAlertAction *resendMessageAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"SEND_AGAIN_BUTTON", @"") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [self.messageSender enqueueMessage:message success:^{ DDLogInfo(@"%@ Successfully resent failed message.", self.logTag); } - failure:^(NSError *_Nonnull error) { + failure:^(NSError *error) { DDLogWarn(@"%@ Failed to send message with error: %@", self.logTag, error); }]; }]; @@ -1922,7 +1934,7 @@ typedef enum : NSUInteger { UIAlertAction *resetSessionAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"FINGERPRINT_SHRED_KEYMATERIAL_BUTTON", @"") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { if (![self.thread isKindOfClass:[TSContactThread class]]) { // Corrupt Message errors only appear in contact threads. DDLogError(@"%@ Unexpected request to reset session in group thread. Refusing", self.logTag); @@ -1955,7 +1967,7 @@ typedef enum : NSUInteger { UIAlertAction *showSafteyNumberAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"SHOW_SAFETY_NUMBER_ACTION", @"Action sheet item") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { DDLogInfo(@"%@ Remote Key Changed actions: Show fingerprint display", self.logTag); [self showFingerprintWithRecipientId:errorMessage.theirSignalId]; }]; @@ -1964,7 +1976,7 @@ typedef enum : NSUInteger { UIAlertAction *acceptSafetyNumberAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"ACCEPT_NEW_IDENTITY_ACTION", @"Action sheet item") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { DDLogInfo(@"%@ Remote Key Changed actions: Accepted new identity key", self.logTag); // DEPRECATED: we're no longer creating these incoming SN error's per message, @@ -2000,7 +2012,7 @@ typedef enum : NSUInteger { __weak ConversationViewController *weakSelf = self; UIAlertAction *callAction = [UIAlertAction actionWithTitle:[CallStrings callBackAlertCallButton] style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [weakSelf startAudioCall]; }]; [alertController addAction:callAction]; @@ -2044,7 +2056,7 @@ typedef enum : NSUInteger { actionWithTitle:NSLocalizedString( @"BLOCK_OFFER_ACTIONSHEET_BLOCK_ACTION", @"Action sheet that will block an unknown user.") style:UIAlertActionStyleDestructive - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { DDLogInfo(@"%@ Blocking an unknown user.", self.logTag); [self.blockingManager addBlockedPhoneNumber:interaction.recipientId]; // Delete the offers. @@ -2283,20 +2295,20 @@ typedef enum : NSUInteger { [[OWSAttachmentsProcessor alloc] initWithAttachmentPointer:attachmentPointer networkManager:self.networkManager]; - [self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [processor fetchAttachmentsForMessage:nil transaction:transaction - success:^(TSAttachmentStream *_Nonnull attachmentStream) { + success:^(TSAttachmentStream *attachmentStream) { [self.editingDatabaseConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull postSuccessTransaction) { + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) { [message setQuotedMessageThumbnailAttachmentStream:attachmentStream]; [message saveWithTransaction:postSuccessTransaction]; }]; } - failure:^(NSError *_Nonnull error) { + failure:^(NSError *error) { DDLogWarn(@"%@ Failed to redownload thumbnail with error: %@", self.logTag, error); [self.editingDatabaseConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull postSuccessTransaction) { + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *postSuccessTransaction) { [message touchWithTransaction:transaction]; }]; }]; @@ -2443,6 +2455,7 @@ typedef enum : NSUInteger { MessageDetailViewController *view = [[MessageDetailViewController alloc] initWithViewItem:conversationItem message:message + thread:self.thread mode:MessageMetadataViewModeFocusOnMetadata]; [self.navigationController pushViewController:view animated:YES]; } @@ -2452,7 +2465,7 @@ typedef enum : NSUInteger { DDLogDebug(@"%@ user did tap reply", self.logTag); __block OWSQuotedReplyModel *quotedReply; - [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { quotedReply = [OWSQuotedReplyModel quotedReplyForConversationViewItem:conversationItem transaction:transaction]; }]; @@ -3739,7 +3752,7 @@ typedef enum : NSUInteger { UIAlertAction *takeMediaAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"MEDIA_FROM_CAMERA_BUTTON", @"media picker option to take photo or video") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [self takePictureOrVideo]; }]; UIImage *takeMediaImage = [UIImage imageNamed:@"actionsheet_camera_black"]; @@ -3750,7 +3763,7 @@ typedef enum : NSUInteger { UIAlertAction *chooseMediaAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"MEDIA_FROM_LIBRARY_BUTTON", @"media picker option to choose from library") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [self chooseFromLibraryAsMedia]; }]; UIImage *chooseMediaImage = [UIImage imageNamed:@"actionsheet_camera_roll_black"]; @@ -3761,7 +3774,7 @@ typedef enum : NSUInteger { UIAlertAction *gifAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"SELECT_GIF_BUTTON", @"Label for 'select GIF to attach' action sheet button") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [self showGifPicker]; }]; UIImage *gifImage = [UIImage imageNamed:@"actionsheet_gif_black"]; @@ -3773,7 +3786,7 @@ typedef enum : NSUInteger { [UIAlertAction actionWithTitle:NSLocalizedString(@"MEDIA_FROM_DOCUMENT_PICKER_BUTTON", @"action sheet button title when choosing attachment type") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [self showAttachmentDocumentPickerMenu]; }]; UIImage *chooseDocumentImage = [UIImage imageNamed:@"actionsheet_document_black"]; @@ -3786,7 +3799,7 @@ typedef enum : NSUInteger { [UIAlertAction actionWithTitle:NSLocalizedString(@"ATTACHMENT_MENU_CONTACT_BUTTON", @"attachment menu option to send contact") style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { + handler:^(UIAlertAction *action) { [self chooseContactForSending]; }]; UIImage *chooseContactImage = [UIImage imageNamed:@"actionsheet_contact"]; @@ -3948,7 +3961,7 @@ typedef enum : NSUInteger { successCompletion(); } } - failure:^(NSError *_Nonnull error) { + failure:^(NSError *error) { DDLogError(@"%@ Failed to send group avatar update with error: %@", self.logTag, error); }]; } else { @@ -3959,7 +3972,7 @@ typedef enum : NSUInteger { successCompletion(); } } - failure:^(NSError *_Nonnull error) { + failure:^(NSError *error) { DDLogError(@"%@ Failed to send group update with error: %@", self.logTag, error); }]; } @@ -4754,6 +4767,7 @@ typedef enum : NSUInteger { OWSAssertIsOnMainThread(); [self updateLastVisibleTimestamp]; + self.layoutInfo.viewWidth = self.collectionView.width; } #pragma mark - View Items @@ -4799,7 +4813,8 @@ typedef enum : NSUInteger { } else { viewItem = [[ConversationViewItem alloc] initWithInteraction:interaction isGroupThread:isGroupThread - transaction:transaction]; + transaction:transaction + layoutInfo:self.layoutInfo]; } viewItem.row = (NSInteger)row; [viewItems addObject:viewItem]; @@ -4966,9 +4981,9 @@ typedef enum : NSUInteger { OWSMessageCell *messageCell = (OWSMessageCell *)cell; messageCell.messageBubbleView.delegate = self; } - cell.contentWidth = self.layout.contentWidth; + cell.layoutInfo = self.layoutInfo; - [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [cell loadForDisplayWithTransaction:transaction]; }]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index e1911eb5a..bfa67c8a6 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -66,10 +66,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); // previous update. @property (nonatomic) NSInteger previousRow; +@property (nonatomic, readonly) ConversationLayoutInfo *layoutInfo; + - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithInteraction:(TSInteraction *)interaction isGroupThread:(BOOL)isGroupThread - transaction:(YapDatabaseReadTransaction *)transaction; + transaction:(YapDatabaseReadTransaction *)transaction + layoutInfo:(ConversationLayoutInfo *)layoutInfo; - (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView indexPath:(NSIndexPath *)indexPath; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index df71d3664..b080befdd 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -77,7 +77,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) - (instancetype)initWithInteraction:(TSInteraction *)interaction isGroupThread:(BOOL)isGroupThread transaction:(YapDatabaseReadTransaction *)transaction + layoutInfo:(ConversationLayoutInfo *)layoutInfo { + OWSAssert(interaction); + OWSAssert(transaction); + OWSAssert(layoutInfo); + self = [super init]; if (!self) { @@ -86,6 +91,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) _interaction = interaction; _isGroupThread = isGroupThread; + _layoutInfo = layoutInfo; self.row = NSNotFound; self.previousRow = NSNotFound; @@ -172,14 +178,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.cachedCellSize = nil; } -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth +- (CGSize)cellSize { OWSAssertIsOnMainThread(); + OWSAssert(self.layoutInfo); if (!self.cachedCellSize) { ConversationViewCell *_Nullable measurementCell = [self measurementCell]; measurementCell.viewItem = self; - CGSize cellSize = [measurementCell cellSizeForViewWidth:viewWidth contentWidth:contentWidth]; + measurementCell.layoutInfo = self.layoutInfo; + CGSize cellSize = [measurementCell cellSize]; self.cachedCellSize = [NSValue valueWithCGSize:cellSize]; [measurementCell prepareForReuse]; } @@ -252,6 +260,14 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return measurementCell; } +- (CGFloat)vSpacingWithLastLayoutItem:(id)lastLayoutItem +{ + OWSAssert(lastLayoutItem); + + // TODO: + return 0.f; +} + - (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView indexPath:(NSIndexPath *)indexPath { diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h index 822c56ee4..cbebd870e 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h @@ -4,6 +4,7 @@ NS_ASSUME_NONNULL_BEGIN +// TODO: Remove this enum. typedef NS_ENUM(NSInteger, ConversationViewLayoutAlignment) { // We use incoming/outgoing, not left/right to support RTL. ConversationViewLayoutAlignment_Incoming, @@ -12,12 +13,16 @@ typedef NS_ENUM(NSInteger, ConversationViewLayoutAlignment) { ConversationViewLayoutAlignment_Center, }; +@class ConversationLayoutInfo; + @protocol ConversationViewLayoutItem -- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth; +- (CGSize)cellSize; - (ConversationViewLayoutAlignment)layoutAlignment; +- (CGFloat)vSpacingWithLastLayoutItem:(id)lastLayoutItem; + @end #pragma mark - @@ -39,7 +44,11 @@ typedef NS_ENUM(NSInteger, ConversationViewLayoutAlignment) { @property (nonatomic, weak) id delegate; @property (nonatomic, readonly) BOOL hasLayout; @property (nonatomic, readonly) BOOL hasEverHadLayout; -@property (nonatomic, readonly) int contentWidth; +@property (nonatomic, readonly) ConversationLayoutInfo *layoutInfo; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithLayoutInfo:(ConversationLayoutInfo *)layoutInfo; @end diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m index 1835f7e95..104a2fae2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m @@ -3,6 +3,7 @@ // #import "ConversationViewLayout.h" +#import "Signal-Swift.h" #import "UIView+OWS.h" NS_ASSUME_NONNULL_BEGIN @@ -21,18 +22,17 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL hasLayout; @property (nonatomic) BOOL hasEverHadLayout; -@property (nonatomic) int contentWidth; - @end #pragma mark - @implementation ConversationViewLayout -- (instancetype)init +- (instancetype)initWithLayoutInfo:(ConversationLayoutInfo *)layoutInfo { if (self = [super init]) { _itemAttributesMap = [NSMutableDictionary new]; + _layoutInfo = layoutInfo; } return self; @@ -94,48 +94,29 @@ NS_ASSUME_NONNULL_BEGIN // TODO: Remove this log statement after we've reduced the invalidation churn. DDLogVerbose(@"%@ prepareLayout", self.logTag); - const int vInset = 8; - const int hInset = 10; - const int vSpacing = 5; - const int viewWidth = (int)floor(self.collectionView.bounds.size.width); - const int contentWidth = (int)floor(viewWidth - 2 * hInset); - self.contentWidth = contentWidth; + const CGFloat viewWidth = self.layoutInfo.viewWidth; NSArray> *layoutItems = self.delegate.layoutItems; - CGFloat y = vInset + self.delegate.layoutHeaderHeight; + CGFloat y = self.layoutInfo.contentMarginTop + self.delegate.layoutHeaderHeight; CGFloat contentBottom = y; - BOOL isRTL = self.collectionView.isRTL; NSInteger row = 0; + id _Nullable lastLayoutItem = nil; for (id layoutItem in layoutItems) { - CGSize layoutSize = [layoutItem cellSizeForViewWidth:viewWidth contentWidth:contentWidth]; - - layoutSize.width = MIN(viewWidth, floor(layoutSize.width)); - layoutSize.height = floor(layoutSize.height); - CGRect itemFrame; - switch (layoutItem.layoutAlignment) { - case ConversationViewLayoutAlignment_Incoming: - case ConversationViewLayoutAlignment_Outgoing: { - BOOL isIncoming = layoutItem.layoutAlignment == ConversationViewLayoutAlignment_Incoming; - BOOL isLeft = isIncoming ^ isRTL; - if (isLeft) { - itemFrame = CGRectMake(hInset, y, layoutSize.width, layoutSize.height); - } else { - itemFrame - = CGRectMake(viewWidth - (hInset + layoutSize.width), y, layoutSize.width, layoutSize.height); - } - break; - } - case ConversationViewLayoutAlignment_FullWidth: - itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height); - break; - case ConversationViewLayoutAlignment_Center: - itemFrame = CGRectMake( - hInset + round((viewWidth - layoutSize.width) * 0.5f), y, layoutSize.width, layoutSize.height); - break; + if (lastLayoutItem) { + y += [layoutItem vSpacingWithLastLayoutItem:lastLayoutItem]; } + CGSize layoutSize = CGSizeCeil([layoutItem cellSize]); + + // Ensure cell fits within view. + OWSAssert(layoutSize.width <= viewWidth); + layoutSize.width = MIN(viewWidth, layoutSize.width); + + // All cell are "full width" and are responsible for aligning their own content. + CGRect itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height); + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; @@ -143,11 +124,12 @@ NS_ASSUME_NONNULL_BEGIN self.itemAttributesMap[@(row)] = itemAttributes; contentBottom = itemFrame.origin.y + itemFrame.size.height; - y = contentBottom + vSpacing; + y = contentBottom; row++; + lastLayoutItem = layoutItem; } - contentBottom += vInset; + contentBottom += self.layoutInfo.contentMarginBottom; self.contentSize = CGSizeMake(viewWidth, contentBottom); } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 9c1e34b80..b12de896f 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -1963,6 +1963,9 @@ NS_ASSUME_NONNULL_BEGIN [prepareBlocks addObject:replyAssetLoader.prepareBlock]; } + // We don't need to configure ConversationLayoutInfo's view width in this case. + ConversationLayoutInfo *layoutInfo = [[ConversationLayoutInfo alloc] initWithThread:thread]; + return [DebugUIMessagesSingleAction actionWithLabel:label unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { @@ -1980,7 +1983,10 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(messageToQuote); DDLogVerbose(@"%@ %@", self.logTag, label); [DDLog flushLog]; - ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote isGroupThread:thread.isGroupThread transaction:transaction]; + ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote + isGroupThread:thread.isGroupThread + transaction:transaction + layoutInfo:layoutInfo]; quotedMessage = [[OWSQuotedReplyModel quotedReplyForConversationViewItem:viewItem transaction:transaction] buildQuotedMessage]; } else { TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread @@ -1994,7 +2000,10 @@ NS_ASSUME_NONNULL_BEGIN transaction:transaction]; OWSAssert(messageToQuote); - ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote isGroupThread:thread.isGroupThread transaction:transaction]; + ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote + isGroupThread:thread.isGroupThread + transaction:transaction + layoutInfo:layoutInfo]; quotedMessage = [[OWSQuotedReplyModel quotedReplyForConversationViewItem:viewItem transaction:transaction] buildQuotedMessage]; } OWSAssert(quotedMessage); diff --git a/Signal/src/ViewControllers/MediaPageViewController.swift b/Signal/src/ViewControllers/MediaPageViewController.swift index 7c9b89fc9..3f25def71 100644 --- a/Signal/src/ViewControllers/MediaPageViewController.swift +++ b/Signal/src/ViewControllers/MediaPageViewController.swift @@ -479,7 +479,11 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou self.uiDatabaseConnection.read { transaction in let message = galleryItem.message let thread = message.thread(with: transaction) - fetchedItem = ConversationViewItem(interaction: message, isGroupThread: thread.isGroupThread(), transaction: transaction) + let conversationLayoutInfo = ConversationLayoutInfo(thread: thread) + fetchedItem = ConversationViewItem(interaction: message, + isGroupThread: thread.isGroupThread(), + transaction: transaction, + layoutInfo: conversationLayoutInfo) } guard let viewItem = fetchedItem else { diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index ceb30e981..37e648add 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -40,6 +40,8 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele var attachmentStream: TSAttachmentStream? var messageBody: String? + var conversationLayoutInfo: ConversationLayoutInfo + private var contactShareViewHelper: ContactShareViewHelper // MARK: Initializers @@ -50,13 +52,15 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele } @objc - required init(viewItem: ConversationViewItem, message: TSMessage, mode: MessageMetadataViewMode) { + required init(viewItem: ConversationViewItem, message: TSMessage, thread: TSThread, mode: MessageMetadataViewMode) { self.contactsManager = Environment.current().contactsManager self.viewItem = viewItem self.message = message self.mode = mode self.uiDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection() self.contactShareViewHelper = ContactShareViewHelper(contactsManager: contactsManager) + self.conversationLayoutInfo = ConversationLayoutInfo(thread: thread) + super.init(nibName: nil, bundle: nil) contactShareViewHelper.delegate = self @@ -83,6 +87,22 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele object: OWSPrimaryStorage.shared().dbNotificationObject) } + override public func viewWillLayoutSubviews() { + Logger.debug("\(self.logTag) in \(#function)") + + super.viewWillLayoutSubviews() + + self.conversationLayoutInfo.viewWidth = self.view.width() + } + + override public func viewDidLayoutSubviews() { + Logger.debug("\(self.logTag) in \(#function)") + + super.viewDidLayoutSubviews() + + self.conversationLayoutInfo.viewWidth = self.view.width() + } + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -331,7 +351,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele self.messageBubbleView = messageBubbleView messageBubbleView.viewItem = viewItem messageBubbleView.cellMediaCache = NSCache() - messageBubbleView.contentWidth = contentWidth() + messageBubbleView.layoutInfo = self.conversationLayoutInfo messageBubbleView.alwaysShowBubbleTail = true messageBubbleView.configureViews() messageBubbleView.loadContent() @@ -574,10 +594,6 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele // MARK: - Message Bubble Layout - private func contentWidth() -> Int32 { - return Int32(round(self.view.width() - (2 * bubbleViewHMargin))) - } - private func updateMessageBubbleViewLayout() { guard let messageBubbleView = messageBubbleView else { return @@ -589,9 +605,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele return } - messageBubbleView.contentWidth = contentWidth() - - let messageBubbleSize = messageBubbleView.size(forContentWidth: contentWidth()) + let messageBubbleSize = messageBubbleView.measureSize() messageBubbleViewWidthLayoutConstraint.constant = messageBubbleSize.width messageBubbleViewHeightLayoutConstraint.constant = messageBubbleSize.height } diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 311e2d1bc..d84d7414f 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -77,7 +77,10 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); // contents. // // NOTE: the margin values are inverted in RTL layouts. +// +// TODO: Remove this in favor of AppContext.isRTL() - (BOOL)isRTL; + - (NSArray *)autoPinLeadingAndTrailingToSuperviewMargin; - (NSLayoutConstraint *)autoPinLeadingToSuperviewMargin; - (NSLayoutConstraint *)autoPinLeadingToSuperviewMarginWithInset:(CGFloat)margin; @@ -150,6 +153,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height)); } +CG_INLINE CGSize CGSizeFloor(CGSize size) +{ + return CGSizeMake((CGFloat)floor(size.width), (CGFloat)floor(size.height)); +} + CG_INLINE CGSize CGSizeRound(CGSize size) { return CGSizeMake((CGFloat)round(size.width), (CGFloat)round(size.height)); diff --git a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h index 852fa1496..d552268e6 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.h @@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN createdByRemoteName:(nullable NSString *)remoteName createdInExistingGroup:(BOOL)createdInExistingGroup; +- (NSString *)previewText; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m index bcb58ce73..f54d740f1 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m @@ -50,6 +50,11 @@ NS_ASSUME_NONNULL_BEGIN } -(NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self previewText]; +} + +- (NSString *)previewText { if (self.createdInExistingGroup) { OWSAssert(self.configurationIsEnabled && self.configurationDurationSeconds > 0); diff --git a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h index 1d2e62159..01a3b8946 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.h @@ -27,6 +27,9 @@ typedef NS_ENUM(int32_t, TSErrorMessageType) { @interface TSErrorMessage : TSMessage +@property (nonatomic, readonly) TSErrorMessageType errorType; +@property (nullable, nonatomic, readonly) NSString *recipientId; + - (instancetype)initMessageWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread messageBody:(nullable NSString *)body @@ -70,8 +73,7 @@ typedef NS_ENUM(int32_t, TSErrorMessageType) { + (instancetype)nonblockingIdentityChangeInThread:(TSThread *)thread recipientId:(NSString *)recipientId; -@property (nonatomic, readonly) TSErrorMessageType errorType; -@property (nullable, nonatomic, readonly) NSString *recipientId; +- (NSString *)previewText; @end diff --git a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m index 550aa1b92..5c736a58e 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSErrorMessage.m @@ -104,6 +104,11 @@ NSUInteger TSErrorMessageSchemaVersion = 1; } - (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self previewText]; +} + +- (NSString *)previewText { switch (_errorType) { case TSErrorMessageNoSession: diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h index 03d1e7d05..d360a1d2b 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.h @@ -61,6 +61,8 @@ typedef NS_ENUM(NSInteger, TSInfoMessageType) { expiresInSeconds:(uint32_t)expiresInSeconds expireStartedAt:(uint64_t)expireStartedAt NS_UNAVAILABLE; +- (NSString *)previewText; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m index b9a181158..b5b63b1c6 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage.m @@ -112,6 +112,11 @@ NSUInteger TSInfoMessageSchemaVersion = 1; } - (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self previewText]; +} + +- (NSString *)previewText { switch (_messageType) { case TSInfoMessageTypeSessionDidEnd: diff --git a/SignalServiceKit/src/Messages/TSCall.h b/SignalServiceKit/src/Messages/TSCall.h index fdca7d4d1..207290a9a 100644 --- a/SignalServiceKit/src/Messages/TSCall.h +++ b/SignalServiceKit/src/Messages/TSCall.h @@ -35,6 +35,8 @@ typedef enum { - (void)updateCallType:(RPRecentCallType)callType; +- (NSString *)previewText; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/TSCall.m b/SignalServiceKit/src/Messages/TSCall.m index 20d805c09..64d149c3d 100644 --- a/SignalServiceKit/src/Messages/TSCall.m +++ b/SignalServiceKit/src/Messages/TSCall.m @@ -68,6 +68,11 @@ NSUInteger TSCallCurrentSchemaVersion = 1; } - (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self previewText]; +} + +- (NSString *)previewText { // We don't actually use the `transaction` but other sibling classes do. switch (_callType) { diff --git a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m index d953c2048..d30251745 100644 --- a/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m +++ b/SignalServiceKit/src/Network/WebSockets/TSSocketManager.m @@ -629,16 +629,6 @@ NSString *const kNSNotification_SocketManagerStateDidChange = @"kNSNotification_ [socketMessage didFailWithStatusCode:(NSInteger)responseStatus responseData:responseData error:error]; } } - - DDLogVerbose(@"%@ received WebSocket response: %llu, %zd, %@, %zd, %@, %d, %@.", - self.logTag, - (unsigned long long)requestId, - (NSInteger)responseStatus, - responseMessage, - responseData.length, - responseHeaders, - socketMessage != nil, - responseObject); } - (void)failAllPendingSocketMessagesIfNecessary diff --git a/SignalServiceKit/src/Util/OWSAsserts.h b/SignalServiceKit/src/Util/OWSAsserts.h index cc719b424..6752e6eea 100755 --- a/SignalServiceKit/src/Util/OWSAsserts.h +++ b/SignalServiceKit/src/Util/OWSAsserts.h @@ -73,7 +73,7 @@ NS_ASSUME_NONNULL_BEGIN #endif -#define OWS_ABSTRACT_METHOD() OWSFail(@"Method needs to be implemented by subclasses.") +#define OWS_ABSTRACT_METHOD() OWSFail(@"%@ Method needs to be implemented by subclasses.", self.logTag) #pragma mark - Singleton Asserts