Tweak sender names.

This commit is contained in:
Matthew Chen 2018-06-26 16:04:09 -04:00
parent 538194aba7
commit 966e6a1156
7 changed files with 246 additions and 118 deletions

View File

@ -25,6 +25,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) UIStackView *stackView;
@property (nonatomic) UILabel *senderNameLabel;
@property (nonatomic) OWSMessageTextView *bodyTextView;
@property (nonatomic, nullable) UIView *quotedMessageView;
@ -77,6 +79,8 @@ NS_ASSUME_NONNULL_BEGIN
self.stackView.axis = UILayoutConstraintAxisVertical;
self.stackView.alignment = UIStackViewAlignmentFill;
self.senderNameLabel = [UILabel new];
self.bodyTextView = [self newTextView];
// Setting dataDetectorTypes is expensive. Do it just once.
self.bodyTextView.dataDetectorTypes
@ -206,24 +210,6 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark -
- (BOOL)hasBubbleBackground
{
switch (self.cellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
case OWSMessageCellType_GenericAttachment:
case OWSMessageCellType_DownloadingAttachment:
case OWSMessageCellType_ContactShare:
return YES;
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Audio:
case OWSMessageCellType_Video:
return self.hasBodyText;
}
}
- (BOOL)hasBodyTextContent
{
switch (self.cellType) {
@ -260,17 +246,18 @@ NS_ASSUME_NONNULL_BEGIN
[self.bubbleView addSubview:self.stackView];
[self.viewConstraints addObjectsFromArray:[self.stackView autoPinEdgesToSuperviewEdges]];
NSMutableArray<UIView *> *textViews = [NSMutableArray new];
if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && self.hasBubbleBackground) {
TSMessage *message = (TSMessage *)self.viewItem.interaction;
self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message];
} else {
// Media-only messages should have no background color; they will fill the bubble's bounds
// and we don't want artifacts at the edges.
self.bubbleView.bubbleColor = nil;
if (self.shouldShowSenderName) {
[self configureSenderNameLabel];
[textViews addObject:self.senderNameLabel];
}
if (self.isQuotedReply) {
// Flush any pending "text" subviews.
[self insertTextViewsIntoStackViewIfNecessary:textViews];
[textViews removeAllObjects];
BOOL isOutgoing = [self.viewItem.interaction isKindOfClass:TSOutgoingMessage.class];
DisplayableText *_Nullable displayableQuotedText
= (self.viewItem.hasQuotedText ? self.viewItem.displayableQuotedText : nil);
@ -328,10 +315,17 @@ NS_ASSUME_NONNULL_BEGIN
break;
}
BOOL shouldFooterOverlayMedia = NO;
if (bodyMediaView) {
OWSAssert(self.loadCellContentBlock);
OWSAssert(self.unloadCellContentBlock);
shouldFooterOverlayMedia = self.canFooterOverlayMedia;
// Flush any pending "text" subviews.
[self insertTextViewsIntoStackViewIfNecessary:textViews];
[textViews removeAllObjects];
bodyMediaView.clipsToBounds = YES;
self.bodyMediaView = bodyMediaView;
@ -359,70 +353,122 @@ NS_ASSUME_NONNULL_BEGIN
}
}
UIStackView *_Nullable textStackView = 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];
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 configureBodyTextView];
[textViews addObject:self.bodyTextView];
[self.viewConstraints addObjectsFromArray:@[
[bodyTextView autoSetDimension:ALDimensionHeight toSize:bodyTextContentSize.height],
[self.bodyTextView autoSetDimension:ALDimensionHeight toSize:bodyTextContentSize.height],
]];
UIView *_Nullable tapForMoreLabel = [self createTapForMoreLabelIfNecessary];
if (tapForMoreLabel) {
[textStackView addArrangedSubview:tapForMoreLabel];
[textViews addObject:tapForMoreLabel];
[self.viewConstraints addObjectsFromArray:@[
[tapForMoreLabel autoSetDimension:ALDimensionHeight toSize:self.tapForMoreHeight],
]];
}
}
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) {
if (self.viewItem.shouldHideFooter) {
// Do nothing.
} else if (shouldFooterOverlayMedia) {
OWSAssert(bodyMediaView);
// Display footer over media.
[bodyMediaView addSubview:footerView];
[self.footerView configureWithConversationViewItem:self.viewItem hasShadows:YES];
[bodyMediaView addSubview:self.footerView];
bodyMediaView.layoutMargins = UIEdgeInsetsZero;
[self.viewConstraints addObjectsFromArray:@[
[footerView autoPinLeadingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
[footerView autoPinTrailingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
[footerView autoPinBottomToSuperviewMarginWithInset:self.conversationStyle.textInsetBottom],
[self.footerView autoPinLeadingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
[self.footerView autoPinTrailingToSuperviewMarginWithInset:self.conversationStyle.textInsetHorizontal],
[self.footerView autoPinBottomToSuperviewMarginWithInset:self.conversationStyle.textInsetBottom],
]];
[self.footerView setHasShadows:YES viewItem:self.viewItem];
} else {
OWSFail(@"%@ could not display footer.", self.logTag);
// Display footer at bottom of message bubble.
[self.footerView configureWithConversationViewItem:self.viewItem hasShadows:NO];
[textViews addObject:self.footerView];
}
if (textStackView) {
CGSize bubbleSize = [self measureSize];
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired
forConstraints:^{
[self.viewConstraints addObjectsFromArray:@[
[self autoSetDimension:ALDimensionWidth toSize:bubbleSize.width],
]];
}];
[self insertTextViewsIntoStackViewIfNecessary:textViews];
CGSize bubbleSize = [self measureSize];
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired
forConstraints:^{
[self.viewConstraints addObjectsFromArray:@[
[self autoSetDimension:ALDimensionWidth toSize:bubbleSize.width],
]];
}];
[self updateBubbleColorWithBodyMediaView:bodyMediaView];
}
- (void)updateBubbleColorWithBodyMediaView:(nullable UIView *)bodyMediaView
{
BOOL hasOnlyBodyMediaView = NO;
switch (self.cellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
case OWSMessageCellType_GenericAttachment:
case OWSMessageCellType_DownloadingAttachment:
case OWSMessageCellType_ContactShare:
break;
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Audio:
case OWSMessageCellType_Video:
hasOnlyBodyMediaView = (bodyMediaView && self.stackView.subviews.count == 1);
break;
}
if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && !hasOnlyBodyMediaView) {
TSMessage *message = (TSMessage *)self.viewItem.interaction;
self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message];
} else {
// Media-only messages should have no background color; they will fill the bubble's bounds
// and we don't want artifacts at the edges.
self.bubbleView.bubbleColor = nil;
}
}
- (BOOL)canFooterOverlayMedia
{
switch (self.cellType) {
case OWSMessageCellType_Unknown:
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
return NO;
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
case OWSMessageCellType_Video:
return YES;
case OWSMessageCellType_Audio:
case OWSMessageCellType_GenericAttachment:
case OWSMessageCellType_DownloadingAttachment:
case OWSMessageCellType_ContactShare:
return NO;
}
}
- (void)insertTextViewsIntoStackViewIfNecessary:(NSArray<UIView *> *)textViews
{
if (textViews.count < 1) {
return;
}
UIStackView *textStackView = [[UIStackView alloc] initWithArrangedSubviews:textViews];
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];
}
// We now eagerly create our view hierarchy (to do this exactly once per cell usage)
@ -464,7 +510,7 @@ NS_ASSUME_NONNULL_BEGIN
- (CGFloat)textViewVSpacing
{
return 5.f;
return 2.f;
}
#pragma mark - Load / Unload
@ -485,7 +531,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Subviews
- (OWSMessageTextView *)configureBodyTextView
- (void)configureBodyTextView
{
OWSAssert(self.hasBodyText);
@ -501,7 +547,6 @@ NS_ASSUME_NONNULL_BEGIN
textColor:self.bodyTextColor
font:self.textMessageFont
shouldIgnoreEvents:shouldIgnoreEvents];
return self.bodyTextView;
}
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
@ -525,6 +570,22 @@ NS_ASSUME_NONNULL_BEGIN
textView.text = text;
}
- (BOOL)shouldShowSenderName
{
return self.viewItem.senderName.length > 0;
}
- (void)configureSenderNameLabel
{
OWSAssert(self.senderNameLabel);
OWSAssert(self.shouldShowSenderName);
self.senderNameLabel.text = self.viewItem.senderName.uppercaseString;
self.senderNameLabel.textColor = self.bodyTextColor;
self.senderNameLabel.font = UIFont.ows_dynamicTypeCaption2Font;
self.senderNameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
}
- (BOOL)hasTapForMore
{
if (!self.hasBodyText) {
@ -854,9 +915,9 @@ NS_ASSUME_NONNULL_BEGIN
const int maxTextWidth = (int)floor(self.conversationStyle.maxMessageWidth - hMargins);
OWSMessageTextView *bodyTextView = [self configureBodyTextView];
[self configureBodyTextView];
const int kMaxIterations = 5;
CGSize result = [bodyTextView compactSizeThatFitsMaxWidth:maxTextWidth maxIterations:kMaxIterations];
CGSize result = [self.bodyTextView compactSizeThatFitsMaxWidth:maxTextWidth maxIterations:kMaxIterations];
if (includeMargins) {
result.width += hMargins;
@ -961,6 +1022,34 @@ NS_ASSUME_NONNULL_BEGIN
return CGSizeCeil(result);
}
- (CGSize)senderNameSizeWithBodyMediaSize:(CGSize)bodyMediaSize includeMargins:(BOOL)includeMargins
{
OWSAssert(self.conversationStyle);
OWSAssert(self.conversationStyle.maxMessageWidth > 0);
if (!self.shouldShowSenderName) {
return CGSizeZero;
}
CGFloat hMargins = self.conversationStyle.textInsetHorizontal * 2;
const int maxTextWidth = (int)floor(self.conversationStyle.maxMessageWidth - hMargins);
[self configureSenderNameLabel];
CGSize result = CGSizeCeil([self.senderNameLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
BOOL hasSeparateTextStackView = (self.isQuotedReply || bodyMediaSize.width > 0 || bodyMediaSize.height > 0);
if (includeMargins) {
result.width += hMargins;
if (hasSeparateTextStackView) {
result.height += (self.conversationStyle.textInsetTop + self.conversationStyle.textInsetBottom);
} else {
result.height += self.textViewVSpacing;
}
}
return result;
}
- (CGSize)measureSize
{
OWSAssert(self.conversationStyle);
@ -970,13 +1059,20 @@ NS_ASSUME_NONNULL_BEGIN
CGSize cellSize = CGSizeZero;
// TODO: Reflect "sender name" and "footer" layout.
// shouldFooterOverlayMedia = self.canFooterOverlayMedia;
CGSize quotedMessageSize = [self quotedMessageSize];
cellSize.width = MAX(cellSize.width, quotedMessageSize.width);
cellSize.height += quotedMessageSize.height;
CGSize mediaContentSize = [self bodyMediaSize];
cellSize.width = MAX(cellSize.width, mediaContentSize.width);
cellSize.height += mediaContentSize.height;
CGSize bodyMediaSize = [self bodyMediaSize];
cellSize.width = MAX(cellSize.width, bodyMediaSize.width);
cellSize.height += bodyMediaSize.height;
CGSize senderNameSize = [self senderNameSizeWithBodyMediaSize:bodyMediaSize includeMargins:YES];
cellSize.width = MAX(cellSize.width, senderNameSize.width);
cellSize.height += senderNameSize.height;
CGSize textContentSize = [self bodyTextSizeWithIncludeMargins:YES];
cellSize.width = MAX(cellSize.width, textContentSize.width);
@ -993,10 +1089,13 @@ NS_ASSUME_NONNULL_BEGIN
// TODO: Update this to reflect generic attachment, downloading attachments and
// contact shares.
if (self.hasFooter && self.hasBodyText) {
if (!self.viewItem.shouldHideFooter && !self.canFooterOverlayMedia) {
CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem];
cellSize.width = MAX(cellSize.width, footerSize.width + self.conversationStyle.textInsetHorizontal * 2);
cellSize.height += self.textViewVSpacing + footerSize.height;
if (!self.hasBodyText) {
cellSize.height += (self.conversationStyle.textInsetTop + self.conversationStyle.textInsetBottom);
}
}
cellSize = CGSizeCeil(cellSize);
@ -1004,12 +1103,6 @@ NS_ASSUME_NONNULL_BEGIN
return cellSize;
}
- (BOOL)hasFooter
{
// TODO:
return YES;
}
- (UIFont *)tapForMoreFont
{
return UIFont.ows_dynamicTypeCaption1Font;

View File

@ -15,12 +15,6 @@ NS_ASSUME_NONNULL_BEGIN
// The non-nullable properties are so frequently used that it's easier
// to always keep one around.
// The cell's contentView contains:
//
// * MessageView (message)
// * dateHeaderLabel (above message)
// * footerView (below message)
@property (nonatomic) OWSMessageBubbleView *messageBubbleView;
@property (nonatomic) UIView *dateHeaderView;
@property (nonatomic) UIView *dateStrokeView;
@ -282,13 +276,15 @@ NS_ASSUME_NONNULL_BEGIN
// Returns YES IFF the avatar view is appropriate and configured.
- (BOOL)updateAvatarView
{
if (!self.viewItem.shouldShowSenderAvatar) {
return NO;
}
if (!self.viewItem.isGroupThread) {
OWSFail(@"%@ not a group thread.", self.logTag);
return NO;
}
if (self.viewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) {
return NO;
}
if (self.viewItem.shouldHideAvatar) {
OWSFail(@"%@ not an incoming message.", self.logTag);
return NO;
}
@ -322,13 +318,15 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSAssertIsOnMainThread();
if (!self.viewItem.shouldShowSenderAvatar) {
return;
}
if (!self.viewItem.isGroupThread) {
OWSFail(@"%@ not a group thread.", self.logTag);
return;
}
if (self.viewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) {
return;
}
if (self.viewItem.shouldHideAvatar) {
OWSFail(@"%@ not an incoming message.", self.logTag);
return;
}

View File

@ -8,12 +8,10 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageFooterView : UIStackView
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem;
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem hasShadows:(BOOL)hasShadows;
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem;
- (void)setHasShadows:(BOOL)hasShadows viewItem:(ConversationViewItem *)viewItem;
@end
NS_ASSUME_NONNULL_END

View File

@ -81,7 +81,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Load
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem hasShadows:(BOOL)hasShadows
{
OWSAssert(viewItem);
@ -98,6 +98,8 @@ NS_ASSUME_NONNULL_BEGIN
]) {
subview.hidden = !isOutgoing;
}
[self setHasShadows:hasShadows viewItem:viewItem];
}
- (void)configureLabelsWithConversationViewItem:(ConversationViewItem *)viewItem

View File

@ -4861,12 +4861,15 @@ typedef enum : NSUInteger {
}
// Update the "shouldShowDate" property of the view items.
//
// First iterate in reverse order.
OWSInteractionType lastInteractionType = OWSInteractionType_Unknown;
MessageReceiptStatus lastReceiptStatus = MessageReceiptStatusUploading;
NSString *_Nullable lastIncomingSenderId = nil;
for (ConversationViewItem *viewItem in viewItems.reverseObjectEnumerator) {
BOOL shouldHideRecipientStatus = NO;
BOOL shouldHideAvatar = NO;
BOOL shouldShowSenderAvatar = NO;
BOOL shouldHideFooter = NO;
OWSInteractionType interactionType = viewItem.interaction.interactionType;
if (interactionType == OWSInteractionType_OutgoingMessage) {
@ -4875,27 +4878,51 @@ typedef enum : NSUInteger {
[MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage
referenceView:self.view];
if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
// always show "failed to send" status
shouldHideRecipientStatus = NO;
} else {
shouldHideRecipientStatus
= (interactionType == lastInteractionType && receiptStatus == lastReceiptStatus);
}
// Always show "failed to send" status.
shouldHideFooter = (interactionType == lastInteractionType && receiptStatus == lastReceiptStatus
&& outgoingMessage.messageState != TSOutgoingMessageStateFailed);
lastReceiptStatus = receiptStatus;
} else if (interactionType == OWSInteractionType_IncomingMessage) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)viewItem.interaction;
NSString *incomingSenderId = incomingMessage.authorId;
OWSAssert(incomingSenderId.length > 0);
shouldHideAvatar = (interactionType == lastInteractionType &&
BOOL isCollapsed = (interactionType == lastInteractionType &&
[NSObject isNullableObject:lastIncomingSenderId equalTo:incomingSenderId]);
lastIncomingSenderId = incomingSenderId;
shouldShowSenderAvatar = viewItem.isGroupThread && !isCollapsed;
}
lastInteractionType = interactionType;
viewItem.shouldHideRecipientStatus = shouldHideRecipientStatus;
viewItem.shouldHideAvatar = shouldHideAvatar;
viewItem.shouldShowSenderAvatar = shouldShowSenderAvatar;
viewItem.shouldHideFooter = shouldHideFooter;
}
// Iterate again in forward order.
lastInteractionType = OWSInteractionType_Unknown;
lastReceiptStatus = MessageReceiptStatusUploading;
lastIncomingSenderId = nil;
for (ConversationViewItem *viewItem in viewItems) {
NSString *_Nullable senderName = nil;
OWSInteractionType interactionType = viewItem.interaction.interactionType;
if (interactionType == OWSInteractionType_IncomingMessage) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)viewItem.interaction;
NSString *incomingSenderId = incomingMessage.authorId;
OWSAssert(incomingSenderId.length > 0);
BOOL isCollapsed = (interactionType == lastInteractionType &&
[NSObject isNullableObject:lastIncomingSenderId equalTo:incomingSenderId]);
lastIncomingSenderId = incomingSenderId;
if (viewItem.isGroupThread && !isCollapsed) {
senderName = [self.contactsManager displayNameForPhoneIdentifier:incomingSenderId];
}
}
lastInteractionType = interactionType;
viewItem.senderName = senderName;
}
self.viewItems = viewItems;

View File

@ -55,10 +55,9 @@ 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;
@property (nonatomic) BOOL shouldShowSenderAvatar;
@property (nonatomic, nullable) NSString *senderName;
@property (nonatomic) BOOL shouldHideFooter;
@property (nonatomic, readonly) ConversationStyle *conversationStyle;

View File

@ -149,24 +149,35 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
[self clearCachedLayoutState];
}
- (void)setShouldHideRecipientStatus:(BOOL)shouldHideRecipientStatus
- (void)setShouldShowSenderAvatar:(BOOL)shouldShowSenderAvatar
{
if (_shouldHideRecipientStatus == shouldHideRecipientStatus) {
if (_shouldShowSenderAvatar == shouldShowSenderAvatar) {
return;
}
_shouldHideRecipientStatus = shouldHideRecipientStatus;
_shouldShowSenderAvatar = shouldShowSenderAvatar;
[self clearCachedLayoutState];
}
- (void)setShouldHideAvatar:(BOOL)shouldHideAvatar
- (void)setSenderName:(nullable NSString *)senderName
{
if (_shouldHideAvatar == shouldHideAvatar) {
if ([NSObject isNullableObject:senderName equalTo:_senderName]) {
return;
}
_shouldHideAvatar = shouldHideAvatar;
_senderName = senderName;
[self clearCachedLayoutState];
}
- (void)setShouldHideFooter:(BOOL)shouldHideFooter
{
if (_shouldHideFooter == shouldHideFooter) {
return;
}
_shouldHideFooter = shouldHideFooter;
[self clearCachedLayoutState];
}