From cbacda87cab401a3945f9f990265b1db26cd6300 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 25 Jun 2018 16:20:28 -0400 Subject: [PATCH] Introduce message cell footer view. --- Signal.xcodeproj/project.pbxproj | 14 +- .../Cells/OWSExpirableMessageView.h | 5 - .../Cells/OWSExpirationTimerView.h | 25 --- .../Cells/OWSExpirationTimerView.m | 192 ------------------ .../Cells/OWSMessageBubbleView.m | 38 ++++ .../ConversationView/Cells/OWSMessageCell.m | 5 +- .../Cells/OWSMessageFooterView.h | 17 ++ .../Cells/OWSMessageFooterView.m | 150 ++++++++++++++ .../ConversationView/ConversationViewItem.h | 1 + 9 files changed, 213 insertions(+), 234 deletions(-) delete mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSExpirableMessageView.h delete mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.h delete mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index d5125205d..6ff0d9e95 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -203,7 +203,6 @@ 34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0721F8678AA0066283D /* ConversationViewLayout.m */; }; 34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0971F867BFC0066283D /* ConversationViewCell.m */; }; 34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */; }; - 34D1F0AC1F867BFC0066283D /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */; }; 34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */; }; 34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */; }; 34D1F0B11F867BFC0066283D /* OWSUnreadIndicatorCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */; }; @@ -225,6 +224,7 @@ 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; }; 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */; }; 34D920E220DD39EA00D51158 /* ConversationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D920E120DD39E900D51158 /* ConversationStyle.swift */; }; + 34D920E720E179C200D51158 /* OWSMessageFooterView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D920E620E179C200D51158 /* OWSMessageFooterView.m */; }; 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 */; }; @@ -843,9 +843,6 @@ 34D1F0971F867BFC0066283D /* ConversationViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewCell.m; sourceTree = ""; }; 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactOffersCell.h; sourceTree = ""; }; 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactOffersCell.m; sourceTree = ""; }; - 34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirableMessageView.h; sourceTree = ""; }; - 34D1F09D1F867BFC0066283D /* OWSExpirationTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirationTimerView.h; sourceTree = ""; }; - 34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSExpirationTimerView.m; sourceTree = ""; }; 34D1F0A11F867BFC0066283D /* OWSMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageCell.h; sourceTree = ""; }; 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageCell.m; sourceTree = ""; }; 34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSystemMessageCell.h; sourceTree = ""; }; @@ -883,6 +880,8 @@ 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 /* ConversationStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationStyle.swift; sourceTree = ""; }; + 34D920E520E179C100D51158 /* OWSMessageFooterView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageFooterView.h; sourceTree = ""; }; + 34D920E620E179C200D51158 /* OWSMessageFooterView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageFooterView.m; 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 = ""; }; @@ -1751,15 +1750,14 @@ 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */, 34CA63192097806E00E526A0 /* OWSContactShareView.h */, 34CA631A2097806E00E526A0 /* OWSContactShareView.m */, - 34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */, - 34D1F09D1F867BFC0066283D /* OWSExpirationTimerView.h */, - 34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */, 34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */, 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */, 3496744B2076768600080B5F /* OWSMessageBubbleView.h */, 3496744C2076768700080B5F /* OWSMessageBubbleView.m */, 34D1F0A11F867BFC0066283D /* OWSMessageCell.h */, 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */, + 34D920E520E179C100D51158 /* OWSMessageFooterView.h */, + 34D920E620E179C200D51158 /* OWSMessageFooterView.m */, 34DBF000206BD5A400025978 /* OWSMessageTextView.h */, 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */, 34277A5D20751BDC006049F2 /* OWSQuotedMessageView.h */, @@ -3232,7 +3230,6 @@ 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */, 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */, 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */, - 34D1F0AC1F867BFC0066283D /* OWSExpirationTimerView.m in Sources */, 76EB063A18170B33006006FC /* FunctionalUtil.m in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, 45B27B862037FFB400A539DF /* DebugUIFileBrowser.swift in Sources */, @@ -3271,6 +3268,7 @@ 4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */, 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */, 34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */, + 34D920E720E179C200D51158 /* OWSMessageFooterView.m in Sources */, 348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */, 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, 457C87B82032645C008D52D6 /* DebugUINotifications.swift in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirableMessageView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirableMessageView.h deleted file mode 100644 index ad9727487..000000000 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirableMessageView.h +++ /dev/null @@ -1,5 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -// TODO: diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.h deleted file mode 100644 index 689fb1ab6..000000000 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -extern const CGFloat kExpirationTimerViewSize; - -@interface OWSExpirationTimerView : UIView - -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; -- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; -- (instancetype)initWithExpiration:(uint64_t)expirationTimestamp - initialDurationSeconds:(uint32_t)initialDurationSeconds NS_DESIGNATED_INITIALIZER; - -- (void)ensureAnimations; - -- (void)clearAnimations; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m deleted file mode 100644 index 4cd15ca41..000000000 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSExpirationTimerView.m +++ /dev/null @@ -1,192 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSExpirationTimerView.h" -#import "ConversationViewController.h" -#import "NSDate+OWS.h" -#import "OWSMath.h" -#import "UIColor+OWS.h" -#import "UIView+OWS.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -const CGFloat kExpirationTimerViewSize = 16.f; - -@interface OWSExpirationTimerView () - -@property (nonatomic) uint32_t initialDurationSeconds; -@property (nonatomic) uint64_t expirationTimestamp; - -@property (nonatomic, readonly) UIImageView *emptyHourglassImageView; -@property (nonatomic, readonly) UIImageView *fullHourglassImageView; -@property (nonatomic, nullable) CAGradientLayer *maskLayer; -@property (nonatomic, nullable) NSTimer *animationTimer; - -@end - -#pragma mark - - -@implementation OWSExpirationTimerView - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (instancetype)initWithExpiration:(uint64_t)expirationTimestamp initialDurationSeconds:(uint32_t)initialDurationSeconds -{ - self = [super initWithFrame:CGRectZero]; - if (!self) { - return self; - } - - self.expirationTimestamp = expirationTimestamp; - self.initialDurationSeconds = initialDurationSeconds; - - [self commonInit]; - - return self; -} - -- (void)commonInit -{ - self.clipsToBounds = YES; - - UIImage *hourglassEmptyImage = [[UIImage imageNamed:@"ic_hourglass_empty"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - UIImage *hourglassFullImage = [[UIImage imageNamed:@"ic_hourglass_full"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - _emptyHourglassImageView = [[UIImageView alloc] initWithImage:hourglassEmptyImage]; - self.emptyHourglassImageView.tintColor = [UIColor lightGrayColor]; - [self addSubview:self.emptyHourglassImageView]; - - _fullHourglassImageView = [[UIImageView alloc] initWithImage:hourglassFullImage]; - self.fullHourglassImageView.tintColor = [UIColor lightGrayColor]; - [self addSubview:self.fullHourglassImageView]; - - [self.emptyHourglassImageView autoPinHeightToSuperviewWithMargin:2.f]; - [self.emptyHourglassImageView autoHCenterInSuperview]; - [self.emptyHourglassImageView autoPinToSquareAspectRatio]; - [self.fullHourglassImageView autoPinHeightToSuperviewWithMargin:2.f]; - [self.fullHourglassImageView autoHCenterInSuperview]; - [self.fullHourglassImageView autoPinToSquareAspectRatio]; - [self autoSetDimension:ALDimensionWidth toSize:kExpirationTimerViewSize]; - [self autoSetDimension:ALDimensionHeight toSize:kExpirationTimerViewSize]; -} - -- (void)clearAnimations -{ - [self.layer removeAllAnimations]; - [self.maskLayer removeAllAnimations]; - [self.maskLayer removeFromSuperlayer]; - self.maskLayer = nil; - [self.fullHourglassImageView.layer.mask removeFromSuperlayer]; - self.fullHourglassImageView.layer.mask = nil; - self.layer.opacity = 1.f; - self.emptyHourglassImageView.hidden = YES; - self.fullHourglassImageView.hidden = YES; - [self.animationTimer invalidate]; - self.animationTimer = nil; -} - -- (void)setFrame:(CGRect)frame { - BOOL sizeDidChange = CGSizeEqualToSize(self.frame.size, frame.size); - [super setFrame:frame]; - if (sizeDidChange) { - [self ensureAnimations]; - } -} - -- (void)setBounds:(CGRect)bounds { - BOOL sizeDidChange = CGSizeEqualToSize(self.bounds.size, bounds.size); - [super setBounds:bounds]; - if (sizeDidChange) { - [self ensureAnimations]; - } -} - -- (void)ensureAnimations -{ - OWSAssertIsOnMainThread(); - - CGFloat secondsLeft = MAX(0, (self.expirationTimestamp - [NSDate ows_millisecondTimeStamp]) / 1000.f); - - [self clearAnimations]; - - const NSTimeInterval kBlinkAnimationDurationSeconds = 2; - - if (self.expirationTimestamp == 0) { - // If message hasn't started expiring yet, just show the full hourglass. - self.fullHourglassImageView.hidden = NO; - return; - } else if (secondsLeft <= kBlinkAnimationDurationSeconds + 0.1f) { - // If message has expired, just show the blinking empty hourglass. - self.emptyHourglassImageView.hidden = NO; - - // Flashing animation. - [UIView animateWithDuration:0.5f - delay:0.f - options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat - animations:^{ - self.layer.opacity = 0.f; - } - completion:^(BOOL finished) { - self.layer.opacity = 1.f; - }]; - return; - } - - self.emptyHourglassImageView.hidden = NO; - self.fullHourglassImageView.hidden = NO; - - CAGradientLayer *maskLayer = [CAGradientLayer new]; - maskLayer.anchorPoint = CGPointZero; - maskLayer.frame = self.fullHourglassImageView.bounds; - self.maskLayer = maskLayer; - self.fullHourglassImageView.layer.mask = maskLayer; - - // Blur the top of the mask a bit with gradient - maskLayer.colors = @[ (id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor ]; - maskLayer.startPoint = CGPointMake(0.5f, 0.f); - // Use a mask that is 20% tall to soften the edge of the animation. - const CGFloat kMaskEdgeFraction = 0.2f; - maskLayer.endPoint = CGPointMake(0.5f, kMaskEdgeFraction); - - NSTimeInterval timeUntilFlashing = MAX(0, secondsLeft - kBlinkAnimationDurationSeconds); - - if (self.initialDurationSeconds == 0) { - OWSFail(@"initialDurationSeconds was unexpectedly 0"); - return; - } - - CGFloat ratioRemaining = (CGFloat)secondsLeft / (CGFloat)self.initialDurationSeconds; - CGFloat ratioComplete = CGFloatClamp((CGFloat)1.0 - ratioRemaining, 0, 1.0); - CGPoint startPosition = CGPointMake(0, self.fullHourglassImageView.height * ratioComplete); - - // We offset the bottom slightly to make sure the duration of the perceived animation is correct. - // We're accounting for: - // - the bottom pixel of the two images is the outline of the hourglass. Because the outline is identical in the full vs empty hourglass this wouldn't be perceptible. - // - the top pixel is not visible due to our softening gradient layer. - CGPoint endPosition = CGPointMake(0, self.fullHourglassImageView.height - 2); - - maskLayer.position = startPosition; - [CATransaction begin]; - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; - animation.duration = timeUntilFlashing; - animation.fromValue = [NSValue valueWithCGPoint:startPosition]; - animation.toValue = [NSValue valueWithCGPoint:endPosition]; - [maskLayer addAnimation:animation forKey:@"slideAnimation"]; - maskLayer.position = endPosition; // don't snap back - [CATransaction commit]; - - self.animationTimer = [NSTimer weakScheduledTimerWithTimeInterval:timeUntilFlashing - target:self - selector:@selector(ensureAnimations) - userInfo:nil - repeats:NO]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 87b47b9d4..83ea4db32 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -10,6 +10,7 @@ #import "OWSBubbleView.h" #import "OWSContactShareView.h" #import "OWSGenericAttachmentView.h" +#import "OWSMessageFooterView.h" #import "OWSMessageTextView.h" #import "OWSQuotedMessageView.h" #import "Signal-Swift.h" @@ -36,6 +37,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) NSMutableArray *viewConstraints; +@property (nonatomic) OWSMessageFooterView *footerView; + @end @implementation OWSMessageBubbleView @@ -73,6 +76,8 @@ NS_ASSUME_NONNULL_BEGIN self.bodyTextView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); self.bodyTextView.hidden = YES; + + self.footerView = [OWSMessageFooterView new]; } - (OWSMessageTextView *)newTextView @@ -436,6 +441,20 @@ NS_ASSUME_NONNULL_BEGIN bottomMargin = textInsets.bottom; } + OWSMessageFooterView *footerView = self.footerView; + [footerView configureWithConversationViewItem:self.viewItem]; + if (self.footerView) { + [self.bubbleView addSubview:self.footerView]; + [self.viewConstraints addObjectsFromArray:@[ + [tapForMoreLabel autoPinLeadingToSuperviewMarginWithInset:textInsets.leading], + [tapForMoreLabel autoPinTrailingToSuperviewMarginWithInset:textInsets.trailing], + [tapForMoreLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview], + [tapForMoreLabel autoSetDimension:ALDimensionHeight toSize:self.tapForMoreHeight], + ]]; + lastSubview = tapForMoreLabel; + bottomMargin = textInsets.bottom; + } + OWSAssert(lastSubview); [self.viewConstraints addObjectsFromArray:@[ [lastSubview autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:bottomMargin], @@ -1007,11 +1026,23 @@ NS_ASSUME_NONNULL_BEGIN cellSize.height += self.tapForMoreHeight; } + if (self.hasFooter) { + CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem]; + cellSize.width = MAX(cellSize.width, footerSize.width); + cellSize.height += self.footerVSpacing + footerSize.height; + } + cellSize = CGSizeCeil(cellSize); return cellSize; } +- (BOOL)hasFooter +{ + // TODO: + return YES; +} + - (UIFont *)tapForMoreFont { return UIFont.ows_dynamicTypeCaption1Font; @@ -1022,6 +1053,11 @@ NS_ASSUME_NONNULL_BEGIN return (CGFloat)ceil([self tapForMoreFont].lineHeight * 1.25); } +- (CGFloat)footerVSpacing +{ + return 10.f; +} + #pragma mark - - (UIColor *)bodyTextColor @@ -1082,6 +1118,8 @@ NS_ASSUME_NONNULL_BEGIN [self.quotedMessageView removeFromSuperview]; self.quotedMessageView = nil; + + [self.footerView removeFromSuperview]; } #pragma mark - Gestures diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 176a41725..26a4a2be9 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -4,7 +4,6 @@ #import "OWSMessageCell.h" #import "OWSContactAvatarBuilder.h" -#import "OWSExpirationTimerView.h" #import "OWSMessageBubbleView.h" #import "Signal-Swift.h" @@ -24,10 +23,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) OWSMessageBubbleView *messageBubbleView; @property (nonatomic) UILabel *dateHeaderLabel; -@property (nonatomic) UIView *footerView; @property (nonatomic) AvatarImageView *avatarView; -@property (nonatomic) UILabel *footerLabel; -@property (nonatomic, nullable) OWSExpirationTimerView *expirationTimerView; +@property (nonatomic) OWSMessageFooterView *footerView2; @property (nonatomic, nullable) NSMutableArray *viewConstraints; @property (nonatomic) BOOL isPresentingMenuController; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.h new file mode 100644 index 000000000..e837d6ad8 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +@class ConversationViewItem; + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSMessageFooterView : UIView + +- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem; + +- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m new file mode 100644 index 000000000..a89e34c70 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m @@ -0,0 +1,150 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageFooterView.h" +#import "DateUtil.h" +#import "OWSExpirationTimerView.h" +#import "Signal-Swift.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSMessageFooterView () + +@property (nonatomic) UILabel *timestampLabel; +@property (nonatomic) UILabel *statusLabel; +@property (nonatomic) UIView *statusIndicatorView; + +@end + +@implementation OWSMessageFooterView + +// `[UIView init]` invokes `[self initWithFrame:...]`. +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self commontInit]; + } + + return self; +} + +- (void)commontInit +{ + // Ensure only called once. + OWSAssert(!self.timestampLabel); + + self.layoutMargins = UIEdgeInsetsZero; + + self.timestampLabel = [UILabel new]; + // TODO: Color + self.timestampLabel.textColor = [UIColor lightGrayColor]; + + self.statusLabel = [UILabel new]; + // TODO: Color + self.statusLabel.textColor = [UIColor lightGrayColor]; + + self.statusIndicatorView = [UIView new]; + [self.statusIndicatorView autoSetDimension:ALDimensionWidth toSize:self.statusIndicatorSize]; + [self.statusIndicatorView autoSetDimension:ALDimensionHeight toSize:self.statusIndicatorSize]; + self.statusIndicatorView.layer.cornerRadius = self.statusIndicatorSize * 0.5f; + + // TODO: Review constant with Myles.0 + UIStackView *statusStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.statusLabel, + self.statusIndicatorView, + ]]; + statusStackView.axis = UILayoutConstraintAxisHorizontal; + statusStackView.spacing = self.hSpacing; + + [self.timestampLabel autoPinEdgeToSuperviewEdge:ALEdgeLeading]; + [statusStackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; + [self.timestampLabel autoVCenterInSuperview]; + [statusStackView autoVCenterInSuperview]; + [self.timestampLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; + [self.timestampLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom + withInset:0 + relation:NSLayoutRelationGreaterThanOrEqual]; + [statusStackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; + [statusStackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; + [statusStackView autoPinEdge:ALEdgeLeading + toEdge:ALEdgeTrailing + ofView:self.timestampLabel + withOffset:self.hSpacing + relation:NSLayoutRelationGreaterThanOrEqual]; +} + +- (void)configureFonts +{ + self.timestampLabel.font = UIFont.ows_dynamicTypeCaption2Font; + self.statusLabel.font = UIFont.ows_dynamicTypeCaption2Font; +} + +- (CGFloat)statusIndicatorSize +{ + // TODO: Review constant. + return 20.f; +} + +- (CGFloat)hSpacing +{ + // TODO: Review constant. + return 10.f; +} + +#pragma mark - Load + +- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem +{ + OWSAssert(viewItem); + + [self configureLabelsWithConversationViewItem:viewItem]; + ; + + // TODO: + self.statusIndicatorView.backgroundColor = [UIColor ows_materialBlueColor]; +} + +- (void)configureLabelsWithConversationViewItem:(ConversationViewItem *)viewItem +{ + OWSAssert(viewItem); + + [self configureFonts]; + + // TODO: Correct text. + self.timestampLabel.text = + [DateUtil formatPastTimestampRelativeToNow:viewItem.interaction.timestamp isRTL:CurrentAppContext().isRTL]; + self.statusLabel.text = [self messageStatusTextForConversationViewItem:viewItem]; +} + +- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem +{ + OWSAssert(viewItem); + + [self configureLabelsWithConversationViewItem:viewItem]; + ; + + CGSize result = CGSizeZero; + result.height + = MAX(self.timestampLabel.font.lineHeight, MAX(self.statusLabel.font.lineHeight, self.statusIndicatorSize)); + result.width = ([self.timestampLabel sizeThatFits:CGSizeZero].width + + [self.statusLabel sizeThatFits:CGSizeZero].width + self.statusIndicatorSize + self.hSpacing * 2.f); + return CGSizeCeil(result); +} + +- (nullable NSString *)messageStatusTextForConversationViewItem:(ConversationViewItem *)viewItem +{ + OWSAssert(viewItem); + if (viewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage) { + return nil; + } + + TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction; + NSString *statusMessage = + [MessageRecipientStatusUtils receiptMessageWithOutgoingMessage:outgoingMessage referenceView:self]; + return statusMessage; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 6293589ee..4c6694124 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -55,6 +55,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic, readonly) BOOL hasQuotedText; @property (nonatomic) BOOL shouldShowDate; +// TODO: Consider renaming to shouldHideFooter. @property (nonatomic) BOOL shouldHideRecipientStatus; // Used to suppress "group sender" avatars. @property (nonatomic) BOOL shouldHideAvatar;