Merge branch 'charlesmchen/footerView'

pull/1/head
Matthew Chen 7 years ago
commit 5e676c13d2

@ -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 = "<group>"; };
34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactOffersCell.h; sourceTree = "<group>"; };
34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactOffersCell.m; sourceTree = "<group>"; };
34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirableMessageView.h; sourceTree = "<group>"; };
34D1F09D1F867BFC0066283D /* OWSExpirationTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirationTimerView.h; sourceTree = "<group>"; };
34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSExpirationTimerView.m; sourceTree = "<group>"; };
34D1F0A11F867BFC0066283D /* OWSMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageCell.h; sourceTree = "<group>"; };
34D1F0A21F867BFC0066283D /* OWSMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageCell.m; sourceTree = "<group>"; };
34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSystemMessageCell.h; sourceTree = "<group>"; };
@ -883,6 +880,8 @@
34D8C02A1ED3685800188D7C /* DebugUIContacts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIContacts.m; sourceTree = "<group>"; };
34D913491F62D4A500722898 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalAttachment.swift; sourceTree = "<group>"; };
34D920E120DD39E900D51158 /* ConversationStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationStyle.swift; sourceTree = "<group>"; };
34D920E520E179C100D51158 /* OWSMessageFooterView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageFooterView.h; sourceTree = "<group>"; };
34D920E620E179C200D51158 /* OWSMessageFooterView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageFooterView.m; sourceTree = "<group>"; };
34D99C8A1F27B13B00D284D6 /* OWSViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSViewController.h; sourceTree = "<group>"; };
34D99C8B1F27B13B00D284D6 /* OWSViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSViewController.m; sourceTree = "<group>"; };
34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAnalytics.swift; sourceTree = "<group>"; };
@ -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 */,

@ -1,26 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_hourglass_empty.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_hourglass_empty@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_hourglass_empty@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

@ -1,26 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_hourglass_full.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_hourglass_full@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_hourglass_full@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

@ -1,5 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
// TODO:

@ -1,25 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <UIKit/UIKit.h>
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

@ -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 <QuartzCore/QuartzCore.h>
#import <SignalServiceKit/NSTimer+OWS.h>
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

@ -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"
@ -22,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) OWSBubbleView *bubbleView;
@property (nonatomic) UIStackView *stackView;
@property (nonatomic) OWSMessageTextView *bodyTextView;
@property (nonatomic, nullable) UIView *quotedMessageView;
@ -36,6 +39,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
@property (nonatomic) OWSMessageFooterView *footerView;
@end
@implementation OWSMessageBubbleView
@ -68,11 +73,19 @@ NS_ASSUME_NONNULL_BEGIN
[self addSubview:self.bubbleView];
[self.bubbleView autoPinEdgesToSuperviewEdges];
self.stackView = [UIStackView new];
self.stackView.axis = UILayoutConstraintAxisVertical;
self.stackView.alignment = UIStackViewAlignmentFill;
[self addSubview:self.stackView];
[self.stackView autoPinEdgesToSuperviewEdges];
self.bodyTextView = [self newTextView];
// Setting dataDetectorTypes is expensive. Do it just once.
self.bodyTextView.dataDetectorTypes
= (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent);
self.bodyTextView.hidden = YES;
self.footerView = [OWSMessageFooterView new];
}
- (OWSMessageTextView *)newTextView
@ -243,6 +256,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
CGSize quotedMessageContentSize = [self quotedMessageSize];
// TODO:
CGSize bodyMediaContentSize = [self bodyMediaSize];
CGSize bodyTextContentSize = [self bodyTextSizeWithIncludeMargins:NO];
@ -255,12 +269,7 @@ NS_ASSUME_NONNULL_BEGIN
self.bubbleView.bubbleColor = nil;
}
UIView *_Nullable lastSubview = nil;
CGFloat bottomMargin = 0;
if (self.isQuotedReply) {
OWSAssert(!lastSubview);
BOOL isOutgoing = [self.viewItem.interaction isKindOfClass:TSOutgoingMessage.class];
DisplayableText *_Nullable displayableQuotedText
= (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
@ -273,24 +282,10 @@ NS_ASSUME_NONNULL_BEGIN
self.quotedMessageView = quotedMessageView;
[quotedMessageView createContents];
[self.bubbleView addSubview:quotedMessageView];
[self.viewConstraints addObjectsFromArray:@[
[quotedMessageView autoPinLeadingToSuperviewMargin],
[quotedMessageView autoPinTrailingToSuperviewMargin],
]];
[self.stackView addArrangedSubview:quotedMessageView];
[self.viewConstraints
addObject:[quotedMessageView autoSetDimension:ALDimensionHeight toSize:quotedMessageContentSize.height]];
if (lastSubview) {
[self.viewConstraints
addObject:[quotedMessageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview]];
} else {
[self.viewConstraints addObject:[quotedMessageView autoPinEdgeToSuperviewEdge:ALEdgeTop]];
}
lastSubview = quotedMessageView;
bottomMargin = 0;
[self.bubbleView addPartnerView:quotedMessageView.boundsStrokeView];
}
@ -344,34 +339,7 @@ NS_ASSUME_NONNULL_BEGIN
bodyMediaView.layer.opacity = 0.75f;
}
[self.bubbleView addSubview:bodyMediaView];
// This layout can lead to extreme cropping of media content,
// e.g. a very tall portrait image + long caption. The media
// view will have "max width", so the image will be cropped to
// roughly a square.
// TODO: Myles is considering alternatives.
[self.viewConstraints addObjectsFromArray:@[
[bodyMediaView autoPinLeadingToSuperviewMarginWithInset:0],
[bodyMediaView autoPinTrailingToSuperviewMarginWithInset:0],
]];
// We need constraints to control the vertical sizing of the media view, but we use
// lower priority so that when a message only contains media it uses the exact bounds of
// the message view.
[NSLayoutConstraint
autoSetPriority:UILayoutPriorityDefaultLow
forConstraints:^{
[self.viewConstraints
addObject:[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaContentSize.height]];
}];
if (lastSubview) {
[self.viewConstraints
addObject:[bodyMediaView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview withOffset:0]];
} else {
[self.viewConstraints addObject:[bodyMediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0]];
}
lastSubview = bodyMediaView;
bottomMargin = 0;
[self.stackView addArrangedSubview:bodyMediaView];
BOOL shouldStrokeMediaView = ([bodyMediaView isKindOfClass:[UIImageView class]] ||
[bodyMediaView isKindOfClass:[OWSContactShareView class]]);
@ -390,56 +358,67 @@ NS_ASSUME_NONNULL_BEGIN
}
}
OWSDirectionalEdgeInsets *textInsets = self.conversationStyle.textInsets;
OWSAssert(textInsets);
UIStackView *_Nullable textStackView = nil;
OWSMessageTextView *_Nullable bodyTextView = nil;
// We render malformed messages as "empty text" messages,
// so create a text view if there is no body media view.
if (self.hasBodyText || !bodyMediaView) {
OWSMessageTextView *_Nullable bodyTextView = nil;
bodyTextView = [self configureBodyTextView];
}
if (bodyTextView) {
[self.bubbleView addSubview:bodyTextView];
textStackView = [UIStackView new];
textStackView.axis = UILayoutConstraintAxisVertical;
textStackView.alignment = UIStackViewAlignmentFill;
// TODO: Review
textStackView.spacing = self.textViewVSpacing;
textStackView.layoutMarginsRelativeArrangement = YES;
textStackView.layoutMargins = UIEdgeInsetsMake(self.conversationStyle.textInsetTop,
self.conversationStyle.textInsetHorizontal,
self.conversationStyle.textInsetBottom,
self.conversationStyle.textInsetHorizontal);
[self.stackView addArrangedSubview:textStackView];
[textStackView addArrangedSubview:bodyTextView];
[self.viewConstraints addObjectsFromArray:@[
[bodyTextView autoPinLeadingToSuperviewMarginWithInset:textInsets.leading],
[bodyTextView autoPinTrailingToSuperviewMarginWithInset:textInsets.trailing],
[bodyTextView autoSetDimension:ALDimensionWidth toSize:bodyTextContentSize.width],
[bodyTextView autoSetDimension:ALDimensionHeight toSize:bodyTextContentSize.height],
]];
if (lastSubview) {
[self.viewConstraints addObject:[bodyTextView autoPinEdge:ALEdgeTop
toEdge:ALEdgeBottom
ofView:lastSubview
withOffset:textInsets.top]];
} else {
[self.viewConstraints
addObject:[bodyTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textInsets.top]];
UIView *_Nullable tapForMoreLabel = [self createTapForMoreLabelIfNecessary];
if (tapForMoreLabel) {
[textStackView addArrangedSubview:tapForMoreLabel];
[self.viewConstraints addObjectsFromArray:@[
[tapForMoreLabel autoSetDimension:ALDimensionHeight toSize:self.tapForMoreHeight],
]];
}
lastSubview = bodyTextView;
bottomMargin = textInsets.bottom;
}
UIView *_Nullable tapForMoreLabel = [self createTapForMoreLabelIfNecessary];
if (tapForMoreLabel) {
OWSAssert(lastSubview);
OWSAssert(lastSubview == bodyTextView);
[self.bubbleView addSubview:tapForMoreLabel];
OWSMessageFooterView *footerView = self.footerView;
[footerView configureWithConversationViewItem:self.viewItem];
if (textStackView) {
// Display footer below text.
[textStackView addArrangedSubview:self.footerView];
[self.footerView setHasShadows:NO viewItem:self.viewItem];
} else if (bodyMediaView) {
// Display footer over media.
[bodyMediaView addSubview:footerView];
bodyMediaView.layoutMargins = UIEdgeInsetsZero;
[self.viewConstraints addObjectsFromArray:@[
[tapForMoreLabel autoPinLeadingToSuperviewMarginWithInset:textInsets.leading],
[tapForMoreLabel autoPinTrailingToSuperviewMarginWithInset:textInsets.trailing],
[tapForMoreLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview],
[tapForMoreLabel autoSetDimension:ALDimensionHeight toSize:self.tapForMoreHeight],
[footerView autoPinLeadingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
[footerView autoPinTrailingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
[footerView autoPinBottomToSuperviewMarginWithInset:self.conversationStyle.textInsetBottom],
]];
lastSubview = tapForMoreLabel;
bottomMargin = textInsets.bottom;
[self.footerView setHasShadows:YES viewItem:self.viewItem];
} else {
OWSFail(@"%@ could not display footer.", self.logTag);
}
OWSAssert(lastSubview);
[self.viewConstraints addObjectsFromArray:@[
[lastSubview autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:bottomMargin],
]];
if (textStackView) {
CGSize bubbleSize = [self measureSize];
[self.viewConstraints addObjectsFromArray:@[
[self autoSetDimension:ALDimensionWidth toSize:bubbleSize.width relation:NSLayoutRelationLessThanOrEqual],
]];
}
}
// We now eagerly create our view hierarchy (to do this exactly once per cell usage)
@ -479,6 +458,11 @@ NS_ASSUME_NONNULL_BEGIN
return cellMedia;
}
- (CGFloat)textViewVSpacing
{
return 5.f;
}
#pragma mark - Load / Unload
- (void)loadContent
@ -862,10 +846,7 @@ NS_ASSUME_NONNULL_BEGIN
return CGSizeZero;
}
OWSDirectionalEdgeInsets *textInsets = self.conversationStyle.textInsets;
OWSAssert(textInsets);
CGFloat hMargins = textInsets.leading + textInsets.trailing;
CGFloat hMargins = self.conversationStyle.textInsetHorizontal * 2;
const int maxTextWidth = (int)floor(self.conversationStyle.maxMessageWidth - hMargins);
@ -876,7 +857,7 @@ NS_ASSUME_NONNULL_BEGIN
if (includeMargins) {
result.width += hMargins;
result.height += textInsets.top + textInsets.bottom;
result.height += (self.conversationStyle.textInsetTop + self.conversationStyle.textInsetBottom);
}
return CGSizeCeil(result);
@ -1004,7 +985,15 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(cellSize.width > 0 && cellSize.height > 0);
if (self.hasTapForMore) {
cellSize.height += self.tapForMoreHeight;
cellSize.height += self.tapForMoreHeight + self.textViewVSpacing;
}
// TODO: Update this to reflect generic attachment, downloading attachments and
// contact shares.
if (self.hasFooter && self.hasBodyText) {
CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem];
cellSize.width = MAX(cellSize.width, footerSize.width + self.conversationStyle.textInsetHorizontal * 2);
cellSize.height += self.textViewVSpacing + footerSize.height;
}
cellSize = CGSizeCeil(cellSize);
@ -1012,6 +1001,12 @@ NS_ASSUME_NONNULL_BEGIN
return cellSize;
}
- (BOOL)hasFooter
{
// TODO:
return YES;
}
- (UIFont *)tapForMoreFont
{
return UIFont.ows_dynamicTypeCaption1Font;
@ -1082,6 +1077,12 @@ NS_ASSUME_NONNULL_BEGIN
[self.quotedMessageView removeFromSuperview];
self.quotedMessageView = nil;
[self.footerView removeFromSuperview];
for (UIView *subview in self.stackView.subviews) {
[subview removeFromSuperview];
}
}
#pragma mark - Gestures

@ -4,7 +4,6 @@
#import "OWSMessageCell.h"
#import "OWSContactAvatarBuilder.h"
#import "OWSExpirationTimerView.h"
#import "OWSMessageBubbleView.h"
#import "Signal-Swift.h"
@ -24,10 +23,7 @@ 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, nullable) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
@property (nonatomic) BOOL isPresentingMenuController;
@ -59,20 +55,12 @@ NS_ASSUME_NONNULL_BEGIN
self.messageBubbleView = [OWSMessageBubbleView new];
[self.contentView addSubview:self.messageBubbleView];
self.footerView = [UIView containerView];
[self.contentView addSubview:self.footerView];
self.dateHeaderLabel = [UILabel new];
self.dateHeaderLabel.font = self.dateHeaderDateFont;
self.dateHeaderLabel.textAlignment = NSTextAlignmentCenter;
self.dateHeaderLabel.textColor = [UIColor lightGrayColor];
[self.contentView addSubview:self.dateHeaderLabel];
self.footerLabel = [UILabel new];
self.footerLabel.font = UIFont.ows_dynamicTypeCaption2Font;
self.footerLabel.textColor = [UIColor lightGrayColor];
[self.footerView addSubview:self.footerLabel];
self.avatarView = [[AvatarImageView alloc] init];
[self.contentView addSubview:self.avatarView];
[self.avatarView autoSetDimension:ALDimensionWidth toSize:self.avatarSize];
@ -80,12 +68,10 @@ NS_ASSUME_NONNULL_BEGIN
// Hide these views by default.
self.dateHeaderLabel.hidden = YES;
self.footerLabel.hidden = YES;
self.avatarView.hidden = YES;
[self.messageBubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel];
[self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self.messageBubbleView autoPinBottomToSuperviewMarginWithInset:0];
self.contentView.userInteractionEnabled = YES;
@ -161,7 +147,6 @@ NS_ASSUME_NONNULL_BEGIN
// Update label fonts to honor dynamic type size.
self.dateHeaderLabel.font = self.dateHeaderDateFont;
self.footerLabel.font = UIFont.ows_dynamicTypeCaption2Font;
if (self.isIncoming) {
[self.viewConstraints addObjectsFromArray:@[
@ -182,7 +167,6 @@ NS_ASSUME_NONNULL_BEGIN
}
[self updateDateHeader];
[self updateFooter];
if ([self updateAvatarView]) {
CGFloat avatarBottomMargin = round(self.conversationStyle.lastTextLineAxis - self.avatarSize * 0.5f);
@ -278,132 +262,6 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (BOOL)shouldShowFooter
{
BOOL shouldShowFooter = NO;
if (self.message.shouldStartExpireTimer) {
shouldShowFooter = YES;
} else if (self.isOutgoing) {
shouldShowFooter = !self.viewItem.shouldHideRecipientStatus;
} else if (self.viewItem.isGroupThread) {
shouldShowFooter = YES;
} else {
shouldShowFooter = NO;
}
return shouldShowFooter;
}
- (CGFloat)footerHeight
{
if (!self.shouldShowFooter) {
return 0.f;
}
return ceil(MAX(kExpirationTimerViewSize, self.footerLabel.font.lineHeight));
}
- (CGFloat)footerVSpacing
{
return 0.f;
}
- (void)updateFooter
{
OWSAssert(self.conversationStyle);
OWSAssert(self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage
|| self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage);
TSMessage *message = self.message;
BOOL hasExpirationTimer = message.shouldStartExpireTimer;
NSAttributedString *attributedText = nil;
if (self.isOutgoing) {
if (!self.viewItem.shouldHideRecipientStatus || hasExpirationTimer) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)message;
NSString *statusMessage =
[MessageRecipientStatusUtils receiptMessageWithOutgoingMessage:outgoingMessage referenceView:self];
attributedText = [[NSAttributedString alloc] initWithString:statusMessage attributes:@{}];
}
} else if (self.viewItem.isGroupThread) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.viewItem.interaction;
attributedText = [self.delegate attributedContactOrProfileNameForPhoneIdentifier:incomingMessage.authorId];
}
if (!hasExpirationTimer &&
!attributedText) {
self.footerLabel.hidden = YES;
[self.viewConstraints addObjectsFromArray:@[
[self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.messageBubbleView],
[self.footerView autoSetDimension:ALDimensionHeight toSize:0],
]];
return;
}
[self.viewConstraints addObjectsFromArray:@[
(self.isIncoming
? [self.footerView autoPinLeadingToSuperviewMarginWithInset:self.conversationStyle.gutterLeading]
: [self.footerView autoPinTrailingToSuperviewMarginWithInset:self.conversationStyle.gutterTrailing]),
]];
[self.viewConstraints addObject:[self.footerView autoPinEdge:ALEdgeTop
toEdge:ALEdgeBottom
ofView:self.messageBubbleView
withOffset:self.footerVSpacing]];
if (hasExpirationTimer) {
uint64_t expirationTimestamp = message.expiresAt;
uint32_t expiresInSeconds = message.expiresInSeconds;
self.expirationTimerView = [[OWSExpirationTimerView alloc] initWithExpiration:expirationTimestamp
initialDurationSeconds:expiresInSeconds];
[self.footerView addSubview:self.expirationTimerView];
}
if (attributedText) {
self.footerLabel.attributedText = attributedText;
self.footerLabel.hidden = NO;
}
// Footer labels can extend past the message bubble, but
// 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.conversationStyle.maxFooterWidth;
if (hasExpirationTimer &&
attributedText) {
[self.viewConstraints addObjectsFromArray:@[
[self.expirationTimerView autoVCenterInSuperview],
[self.footerLabel autoVCenterInSuperview],
(self.isIncoming ? [self.expirationTimerView autoPinLeadingToSuperviewMargin]
: [self.expirationTimerView autoPinTrailingToSuperviewMargin]),
(self.isIncoming ? [self.footerLabel autoPinLeadingToTrailingEdgeOfView:self.expirationTimerView]
: [self.footerLabel autoPinTrailingToLeadingEdgeOfView:self.expirationTimerView]),
[self.footerLabel autoSetDimension:ALDimensionWidth
toSize:maxFooterLabelWidth
relation:NSLayoutRelationLessThanOrEqual],
[self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight],
]];
} else if (hasExpirationTimer) {
[self.viewConstraints addObjectsFromArray:@[
[self.expirationTimerView autoVCenterInSuperview],
(self.isIncoming ? [self.expirationTimerView autoPinLeadingToSuperviewMargin]
: [self.expirationTimerView autoPinTrailingToSuperviewMargin]),
[self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight],
]];
} else if (attributedText) {
[self.viewConstraints addObjectsFromArray:@[
[self.footerLabel autoVCenterInSuperview],
(self.isIncoming ? [self.footerLabel autoPinLeadingToSuperviewMargin]
: [self.footerLabel autoPinTrailingToSuperviewMargin]),
[self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight],
[self.footerLabel autoSetDimension:ALDimensionWidth
toSize:maxFooterLabelWidth
relation:NSLayoutRelationLessThanOrEqual],
]];
} else {
OWSFail(@"%@ Cell unexpectedly has neither expiration timer nor footer text.", self.logTag);
}
}
- (UIFont *)dateHeaderDateFont
{
return UIFont.ows_dynamicTypeCaption1Font.ows_mediumWeight;
@ -501,10 +359,6 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(cellSize.width > 0 && cellSize.height > 0);
cellSize.height += self.dateHeaderHeight;
if (self.shouldShowFooter) {
cellSize.height += self.footerVSpacing;
cellSize.height += self.footerHeight;
}
cellSize = CGSizeCeil(cellSize);
@ -535,15 +389,9 @@ NS_ASSUME_NONNULL_BEGIN
self.dateHeaderLabel.text = nil;
self.dateHeaderLabel.hidden = YES;
self.footerLabel.text = nil;
self.footerLabel.hidden = YES;
self.avatarView.image = nil;
self.avatarView.hidden = YES;
[self.expirationTimerView clearAnimations];
[self.expirationTimerView removeFromSuperview];
self.expirationTimerView = nil;
[self hideMenuControllerIfNecessary];
[[NSNotificationCenter defaultCenter] removeObserver:self];
@ -562,15 +410,7 @@ NS_ASSUME_NONNULL_BEGIN
[self ensureMediaLoadState];
if (isCellVisible) {
if (self.message.shouldStartExpireTimer) {
[self.expirationTimerView ensureAnimations];
} else {
[self.expirationTimerView clearAnimations];
}
} else {
[self.expirationTimerView clearAnimations];
if (!isCellVisible) {
[self hideMenuControllerIfNecessary];
}
}

@ -0,0 +1,19 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
@class ConversationViewItem;
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageFooterView : UIStackView
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem;
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem;
- (void)setHasShadows:(BOOL)hasShadows viewItem:(ConversationViewItem *)viewItem;
@end
NS_ASSUME_NONNULL_END

@ -0,0 +1,182 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageFooterView.h"
#import "DateUtil.h"
#import "Signal-Swift.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageFooterView ()
@property (nonatomic) UILabel *timestampLabel;
@property (nonatomic) UIView *spacerView;
@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.axis = UILayoutConstraintAxisHorizontal;
self.spacing = self.hSpacing;
self.alignment = UIStackViewAlignmentCenter;
self.timestampLabel = [UILabel new];
// TODO: Color
self.timestampLabel.textColor = [UIColor lightGrayColor];
[self addArrangedSubview:self.timestampLabel];
self.spacerView = [UIView new];
[self.spacerView setContentHuggingLow];
[self addArrangedSubview:self.spacerView];
self.statusLabel = [UILabel new];
// TODO: Color
self.statusLabel.textColor = [UIColor lightGrayColor];
[self addArrangedSubview:self.statusLabel];
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;
[self addArrangedSubview:self.statusIndicatorView];
}
- (void)configureFonts
{
self.timestampLabel.font = UIFont.ows_dynamicTypeCaption2Font;
self.statusLabel.font = UIFont.ows_dynamicTypeCaption2Font;
}
- (CGFloat)statusIndicatorSize
{
// TODO: Review constant.
return 12.f;
}
- (CGFloat)hSpacing
{
// TODO: Review constant.
return 8.f;
}
#pragma mark - Load
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem
{
OWSAssert(viewItem);
[self configureLabelsWithConversationViewItem:viewItem];
// TODO:
self.statusIndicatorView.backgroundColor = [UIColor orangeColor];
BOOL isOutgoing = (viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage);
for (UIView *subview in @[
self.spacerView,
self.statusLabel,
self.statusIndicatorView,
]) {
subview.hidden = !isOutgoing;
}
}
- (void)configureLabelsWithConversationViewItem:(ConversationViewItem *)viewItem
{
OWSAssert(viewItem);
[self configureFonts];
self.timestampLabel.text = [DateUtil formatTimestampShort:viewItem.interaction.timestamp];
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));
if (viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
result.width = ([self.timestampLabel sizeThatFits:CGSizeZero].width +
[self.statusLabel sizeThatFits:CGSizeZero].width + self.statusIndicatorSize + self.hSpacing * 3.f);
} else {
result.width = [self.timestampLabel sizeThatFits:CGSizeZero].width;
}
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;
}
#pragma mark - Shadows
- (void)setHasShadows:(BOOL)hasShadows viewItem:(ConversationViewItem *)viewItem
{
// TODO: Constants
for (UIView *subview in @[
self.timestampLabel,
self.statusLabel,
self.statusIndicatorView,
]) {
if (hasShadows) {
subview.layer.shadowColor = [UIColor blackColor].CGColor;
subview.layer.shadowOpacity = 0.35f;
subview.layer.shadowOffset = CGSizeZero;
subview.layer.shadowRadius = 0.5f;
} else {
subview.layer.shadowColor = nil;
subview.layer.shadowOpacity = 0.f;
subview.layer.shadowOffset = CGSizeZero;
subview.layer.shadowRadius = 0.f;
}
}
UIColor *textColor;
if (hasShadows) {
textColor = [UIColor whiteColor];
} else if (viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage) {
// TODO:
textColor = [UIColor lightGrayColor];
} else {
textColor = [UIColor whiteColor];
}
self.timestampLabel.textColor = textColor;
self.statusLabel.textColor = textColor;
}
@end
NS_ASSUME_NONNULL_END

@ -4,34 +4,6 @@
import Foundation
@objc
public class OWSDirectionalEdgeInsets: NSObject {
@objc public let leading: CGFloat
@objc public let trailing: CGFloat
@objc public let top: CGFloat
@objc public let bottom: CGFloat
@objc
public required init(top: CGFloat = 0,
leading: CGFloat = 0,
bottom: CGFloat = 0,
trailing: CGFloat = 0) {
self.leading = leading
self.trailing = trailing
self.top = top
self.bottom = bottom
super.init()
}
static var zero = OWSDirectionalEdgeInsets(top: 0,
leading: 0,
bottom: 0,
trailing: 0)
}
@objc
public class ConversationStyle: NSObject {
@ -70,7 +42,9 @@ public class ConversationStyle: NSObject {
// message status inside the message bubbles.
@objc public var maxFooterWidth: CGFloat = 0
@objc public var textInsets = OWSDirectionalEdgeInsets.zero
@objc public var textInsetTop: CGFloat = 0
@objc public var textInsetBottom: CGFloat = 0
@objc public var textInsetHorizontal: CGFloat = 0
// We want to align "group sender" avatars with the v-center of the
// "last line" of the message body text - or where it would be for
@ -130,16 +104,13 @@ public class ConversationStyle: NSObject {
let messageTextFont = UIFont.ows_dynamicTypeBody
// Don't include the distance from the "cap height" to the top of the UILabel
// in the top margin.
let textInsetTop = max(0, 12 - (messageTextFont.ascender - messageTextFont.capHeight))
textInsetTop = max(0, 12 - (messageTextFont.ascender - messageTextFont.capHeight))
// Don't include the distance from the "baseline" to the bottom of the UILabel
// (e.g. the descender) in the top margin. Note that UIFont.descender is a
// negative value.
let textInsetBottom = max(0, 12 - abs(messageTextFont.descender))
textInsetBottom = max(0, 12 - abs(messageTextFont.descender))
textInsetHorizontal = 12
textInsets = OWSDirectionalEdgeInsets(top: textInsetTop,
leading: 12,
bottom: textInsetBottom,
trailing: 12)
lastTextLineAxis = CGFloat(round(12 + messageTextFont.capHeight * 0.5))
}
}

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

@ -342,18 +342,7 @@ NS_ASSUME_NONNULL_BEGIN
return @"";
}
NSString *dateTimeString;
if (![DateUtil dateIsThisYear:date]) {
dateTimeString = [[DateUtil dateFormatter] stringFromDate:date];
} else if ([DateUtil dateIsOlderThanOneWeek:date]) {
dateTimeString = [[DateUtil monthAndDayFormatter] stringFromDate:date];
} else if ([DateUtil dateIsOlderThanToday:date]) {
dateTimeString = [[DateUtil shortDayOfWeekFormatter] stringFromDate:date];
} else {
dateTimeString = [[DateUtil timeFormatter] stringFromDate:date];
}
return dateTimeString.uppercaseString;
return [DateUtil formatDateShort:date];
}
#pragma mark - Constants

@ -20,6 +20,9 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp
isRTL:(BOOL)isRTL NS_SWIFT_NAME(formatPastTimestampRelativeToNow(_:isRTL:));
+ (NSString *)formatTimestampShort:(uint64_t)timestamp;
+ (NSString *)formatDateShort:(NSDate *)date;
@end
NS_ASSUME_NONNULL_END

@ -165,6 +165,29 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE";
isRTL:isRTL];
}
+ (NSString *)formatTimestampShort:(uint64_t)timestamp
{
return [self formatDateShort:[NSDate ows_dateWithMillisecondsSince1970:timestamp]];
}
+ (NSString *)formatDateShort:(NSDate *)date
{
OWSAssert(date);
NSString *dateTimeString;
if (![DateUtil dateIsThisYear:date]) {
dateTimeString = [[DateUtil dateFormatter] stringFromDate:date];
} else if ([DateUtil dateIsOlderThanOneWeek:date]) {
dateTimeString = [[DateUtil monthAndDayFormatter] stringFromDate:date];
} else if ([DateUtil dateIsOlderThanToday:date]) {
dateTimeString = [[DateUtil shortDayOfWeekFormatter] stringFromDate:date];
} else {
dateTimeString = [[DateUtil timeFormatter] stringFromDate:date];
}
return dateTimeString.uppercaseString;
}
@end
NS_ASSUME_NONNULL_END

Loading…
Cancel
Save