Add views to render messages with per-message expiration in conversation view.

This commit is contained in:
Matthew Chen 2019-05-31 17:36:38 -04:00
parent a6280e61c5
commit 08d1592cb1
14 changed files with 771 additions and 8 deletions

View File

@ -162,6 +162,7 @@
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; };
346E35BE224283B100E55D5F /* UIAlertController+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346E35BD224283B000E55D5F /* UIAlertController+OWS.swift */; };
346E9D5421B040B700562252 /* RegistrationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346E9D5321B040B600562252 /* RegistrationController.swift */; };
347137FC22A1BCE800F43A63 /* OWSMessageHiddenView.m in Sources */ = {isa = PBXBuildFile; fileRef = 347137FB22A1BCE800F43A63 /* OWSMessageHiddenView.m */; };
347850311FD7494A007B8332 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; };
347850321FD7494A007B8332 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; };
347850331FD7494A007B8332 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; };
@ -876,6 +877,8 @@
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
346E35BD224283B000E55D5F /* UIAlertController+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+OWS.swift"; sourceTree = "<group>"; };
346E9D5321B040B600562252 /* RegistrationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationController.swift; sourceTree = "<group>"; };
347137FA22A1BCE800F43A63 /* OWSMessageHiddenView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageHiddenView.h; sourceTree = "<group>"; };
347137FB22A1BCE800F43A63 /* OWSMessageHiddenView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageHiddenView.m; sourceTree = "<group>"; };
347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = "<group>"; };
3478505A1FD999D5007B8332 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = translations/et.lproj/Localizable.strings; sourceTree = "<group>"; };
3478505C1FD99A1F007B8332 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = translations/zh_TW.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -2139,6 +2142,8 @@
34D920E620E179C200D51158 /* OWSMessageFooterView.m */,
348570A720F67574004FF32B /* OWSMessageHeaderView.h */,
348570A620F67574004FF32B /* OWSMessageHeaderView.m */,
347137FA22A1BCE800F43A63 /* OWSMessageHiddenView.h */,
347137FB22A1BCE800F43A63 /* OWSMessageHiddenView.m */,
34C2EEB82272244500BCA1D0 /* OWSMessageStickerView.h */,
34C2EEB72272244500BCA1D0 /* OWSMessageStickerView.m */,
34DBF000206BD5A400025978 /* OWSMessageTextView.h */,
@ -3772,6 +3777,7 @@
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
3448E16422135FFA004B052E /* OnboardingPhoneNumberViewController.swift in Sources */,
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
347137FC22A1BCE800F43A63 /* OWSMessageHiddenView.m in Sources */,
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,
45D2AC02204885170033C692 /* OWS2FAReminderViewController.swift in Sources */,
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "play-outline-24@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "play-outline-24@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "play-outline-24@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -339,6 +339,7 @@ private class MockConversationViewItem: NSObject, ConversationViewItem {
var stickerInfo: StickerInfo?
var stickerAttachment: TSAttachmentStream?
var isFailedSticker: Bool = false
var perMessageExpirationHasExpired: Bool = false
override init() {
super.init()

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@ -38,6 +38,7 @@ typedef NS_OPTIONS(NSUInteger, OWSDirectionalRectCorner) {
sharpCorners:(OWSDirectionalRectCorner)sharpCorners;
@property (nonatomic, nullable) UIColor *bubbleColor;
@property (nonatomic, nullable) NSArray<UIColor *> *bubbleGradientColors;
@property (nonatomic) OWSDirectionalRectCorner sharpCorners;

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
@ -40,10 +40,13 @@ UIRectCorner UIRectCornerForOWSDirectionalRectCorner(OWSDirectionalRectCorner co
const CGFloat kOWSMessageCellCornerRadius_Large = 18;
const CGFloat kOWSMessageCellCornerRadius_Small = 4;
#pragma mark -
@interface OWSBubbleView ()
@property (nonatomic) CAShapeLayer *maskLayer;
@property (nonatomic) CAShapeLayer *shapeLayer;
@property (nonatomic) CAGradientLayer *gradientLayer;
@property (nonatomic, readonly) NSMutableArray<id<OWSBubbleViewPartner>> *partnerViews;
@ -64,6 +67,11 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
self.shapeLayer.hidden = YES;
self.gradientLayer = [CAGradientLayer new];
[self.layer addSublayer:self.gradientLayer];
self.gradientLayer.hidden = YES;
self.maskLayer = [CAShapeLayer new];
self.layer.mask = self.maskLayer;
@ -118,17 +126,22 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
- (void)setBubbleColor:(nullable UIColor *)bubbleColor
{
OWSAssertIsOnMainThread();
_bubbleColor = bubbleColor;
_bubbleGradientColors = nil;
[self updateLayers];
}
// Prevent the shape layer from animating changes.
[CATransaction begin];
[CATransaction setDisableActions:YES];
- (void)setBubbleGradientColors:(nullable NSArray<UIColor *> *)bubbleGradientColors
{
OWSAssertIsOnMainThread();
self.shapeLayer.fillColor = bubbleColor.CGColor;
_bubbleColor = nil;
_bubbleGradientColors = bubbleGradientColors;
[CATransaction commit];
[self updateLayers];
}
- (void)setSharpCorners:(OWSDirectionalRectCorner)sharpCorners
@ -146,6 +159,9 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
if (!self.shapeLayer) {
return;
}
if (!self.gradientLayer) {
return;
}
UIBezierPath *bezierPath = [self maskPath];
@ -154,12 +170,32 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
[CATransaction setDisableActions:YES];
self.shapeLayer.fillColor = self.bubbleColor.CGColor;
self.shapeLayer.hidden = self.bubbleColor == nil;
self.shapeLayer.path = bezierPath.CGPath;
NSArray *bubbleGradientCGColors = self.bubbleGradientCGColors;
self.gradientLayer.colors = bubbleGradientCGColors;
self.gradientLayer.hidden = bubbleGradientCGColors.count < 1;
// Currently this view only supports linear (or axial) gradients
// from the top-left to bottom-right.
self.gradientLayer.startPoint = CGPointMake(0, 1);
self.gradientLayer.endPoint = CGPointMake(1, 0);
self.gradientLayer.frame = self.bounds;
self.maskLayer.path = bezierPath.CGPath;
[CATransaction commit];
}
- (NSArray *)bubbleGradientCGColors
{
NSMutableArray *result = [NSMutableArray new];
for (UIColor *color in self.bubbleGradientColors) {
[result addObject:(id)color.CGColor];
}
return result;
}
- (UIBezierPath *)maskPath
{
return [self.class maskPathForSize:self.bounds.size sharpCorners:self.sharpCorners];

View File

@ -6,6 +6,7 @@
#import "OWSContactAvatarBuilder.h"
#import "OWSMessageBubbleView.h"
#import "OWSMessageHeaderView.h"
#import "OWSMessageHiddenView.h"
#import "OWSMessageStickerView.h"
#import "Signal-Swift.h"

View File

@ -0,0 +1,71 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageView.h"
NS_ASSUME_NONNULL_BEGIN
//@class ContactShareViewModel;
//@class OWSContact;
//@class OWSLinkPreview;
//@class OWSQuotedReplyModel;
//@class StickerPackInfo;
//@class TSAttachmentPointer;
//@class TSAttachmentStream;
//@class TSOutgoingMessage;
@protocol OWSMessageHiddenViewDelegate
//- (void)didTapImageViewItem:(id<ConversationViewItem>)viewItem
// attachmentStream:(TSAttachmentStream *)attachmentStream
// imageView:(UIView *)imageView;
//
//- (void)didTapVideoViewItem:(id<ConversationViewItem>)viewItem
// attachmentStream:(TSAttachmentStream *)attachmentStream
// imageView:(UIView *)imageView;
//
//- (void)didTapAudioViewItem:(id<ConversationViewItem>)viewItem attachmentStream:(TSAttachmentStream
//*)attachmentStream;
//
//- (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem;
//
//- (void)didTapFailedIncomingAttachment:(id<ConversationViewItem>)viewItem;
//
//- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply;
//- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem
// quotedReply:(OWSQuotedReplyModel *)quotedReply
// failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer;
//
//- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview;
//
//- (void)didTapContactShareViewItem:(id<ConversationViewItem>)viewItem;
//
//- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
// NS_SWIFT_NAME(didTapSendMessage(toContactShare:));
//- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
// NS_SWIFT_NAME(didTapSendInvite(toContactShare:));
//- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
// NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
//
//- (void)didTapStickerPack:(StickerPackInfo *)stickerPackInfo NS_SWIFT_NAME(didTapStickerPack(_:));
//
//@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
@end
#pragma mark -
@interface OWSMessageHiddenView : OWSMessageView
@property (nonatomic, weak) id<OWSMessageHiddenViewDelegate> delegate;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,581 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageHiddenView.h"
#import "AttachmentUploadView.h"
#import "ConversationViewItem.h"
//#import "OWSAudioMessageView.h"
#import "OWSBubbleShapeView.h"
#import "OWSBubbleView.h"
//#import "OWSContactShareButtonsView.h"
//#import "OWSContactShareView.h"
//#import "OWSGenericAttachmentView.h"
#import "OWSLabel.h"
#import "OWSMessageFooterView.h"
//#import "OWSMessageTextView.h"
//#import "OWSQuotedMessageView.h"
#import "Signal-Swift.h"
#import "UIColor+OWS.h"
#import <SignalMessaging/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageHiddenView ()
@property (nonatomic) OWSBubbleView *bubbleView;
@property (nonatomic) UIStackView *vStackView;
@property (nonatomic) UIStackView *hStackView;
@property (nonatomic) UIImageView *iconView;
@property (nonatomic) UILabel *label;
//@property (nonatomic) UIView *senderNameContainer;
//
//@property (nonatomic) OWSMessageTextView *bodyTextView;
//
//@property (nonatomic, nullable) UIView *quotedMessageView;
//
//@property (nonatomic, nullable) UIView *bodyMediaView;
//
//@property (nonatomic) LinkPreviewView *linkPreviewView;
//
//// Should lazy-load expensive view contents (images, etc.).
//// Should do nothing if view is already loaded.
//@property (nonatomic, nullable) dispatch_block_t loadCellContentBlock;
//// Should unload all expensive view contents (images, etc.).
//@property (nonatomic, nullable) dispatch_block_t unloadCellContentBlock;
@property (nonatomic, nullable) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
@property (nonatomic) OWSMessageFooterView *footerView;
@end
#pragma mark -
@implementation OWSMessageHiddenView
#pragma mark - Dependencies
- (OWSAttachmentDownloads *)attachmentDownloads
{
return SSKEnvironment.shared.attachmentDownloads;
}
//- (TSAccountManager *)tsAccountManager
//{
// OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
//
// return SSKEnvironment.shared.tsAccountManager;
//}
#pragma mark -
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self) {
return self;
}
[self commontInit];
return self;
}
- (void)commontInit
{
// Ensure only called once.
OWSAssertDebug(!self.vStackView);
_viewConstraints = [NSMutableArray new];
self.layoutMargins = UIEdgeInsetsZero;
self.userInteractionEnabled = YES;
self.bubbleView = [OWSBubbleView new];
self.bubbleView.layoutMargins = UIEdgeInsetsZero;
[self addSubview:self.bubbleView];
[self.bubbleView autoPinEdgesToSuperviewEdges];
self.iconView = [UIImageView new];
[self.iconView setContentHuggingHorizontalHigh];
[self.iconView setCompressionResistanceHorizontalHigh];
[self.iconView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[self.iconView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
self.label = [OWSLabel new];
self.hStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.iconView,
self.label,
]];
self.hStackView.axis = UILayoutConstraintAxisHorizontal;
self.hStackView.spacing = self.contentHSpacing;
self.hStackView.alignment = UIStackViewAlignmentCenter;
self.vStackView = [UIStackView new];
self.vStackView.axis = UILayoutConstraintAxisVertical;
self.footerView = [OWSMessageFooterView new];
}
#pragma mark - Convenience Accessors
- (OWSMessageCellType)cellType
{
return self.viewItem.messageCellType;
}
- (TSMessage *)message
{
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
return (TSMessage *)self.viewItem.interaction;
}
- (BOOL)isIncoming
{
return self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage;
}
- (BOOL)isOutgoing
{
return self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage;
}
- (BOOL)perMessageExpirationHasExpired
{
return self.viewItem.perMessageExpirationHasExpired;
}
#pragma mark - Load
- (void)configureViews
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.viewItem);
OWSAssertDebug(self.viewItem.interaction);
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
[self.bubbleView addSubview:self.vStackView];
[self.viewConstraints addObjectsFromArray:[self.vStackView autoPinEdgesToSuperviewEdges]];
NSMutableArray<UIView *> *textViews = [NSMutableArray new];
// TODO:
[self addProgressViewsIfNecessary:self.overlayHostView shouldShowDownloadProgress:NO];
[self configureIconView];
[self configureLabel];
[textViews addObject:self.hStackView];
if (self.viewItem.shouldHideFooter) {
// Do nothing.
} else {
[self.footerView configureWithConversationViewItem:self.viewItem
conversationStyle:self.conversationStyle
isIncoming:self.isIncoming
isOverlayingMedia:NO
isOutsideBubble:NO];
[textViews addObject:self.footerView];
}
[self insertAnyTextViewsIntoStackView:textViews];
CGSize bubbleSize = [self measureSize];
[self.viewConstraints addObjectsFromArray:@[
[self autoSetDimension:ALDimensionWidth toSize:bubbleSize.width],
]];
[self updateBubbleColor];
[self configureBubbleRounding];
}
- (CGFloat)iconSize
{
return 24.f;
}
- (OWSDirectionalRectCorner)sharpCorners
{
OWSDirectionalRectCorner sharpCorners = 0;
if (!self.viewItem.isFirstInCluster) {
sharpCorners = sharpCorners
| (self.isIncoming ? OWSDirectionalRectCornerTopLeading : OWSDirectionalRectCornerTopTrailing);
}
if (!self.viewItem.isLastInCluster) {
sharpCorners = sharpCorners
| (self.isIncoming ? OWSDirectionalRectCornerBottomLeading : OWSDirectionalRectCornerBottomTrailing);
}
return sharpCorners;
}
- (OWSDirectionalRectCorner)sharpCornersForQuotedMessage
{
if (self.viewItem.senderName) {
return OWSDirectionalRectCornerAllCorners;
} else {
return self.sharpCorners | OWSDirectionalRectCornerBottomLeading | OWSDirectionalRectCornerBottomTrailing;
}
}
- (void)configureBubbleRounding
{
self.bubbleView.sharpCorners = self.sharpCorners;
}
- (void)updateBubbleColor
{
if (self.perMessageExpirationHasExpired) {
self.bubbleView.bubbleColor = UIColor.ows_gray15Color;
} else if (self.isIncoming) {
self.bubbleView.bubbleGradientColors = @[
[UIColor colorWithRGBHex:0xD0D1D1],
UIColor.ows_gray05Color,
];
} else {
self.bubbleView.bubbleGradientColors = @[
[UIColor colorWithRGBHex:0x1C7ECD],
UIColor.ows_signalBlueColor,
];
}
}
- (CGFloat)contentHSpacing
{
return 6.f;
}
- (UIColor *)contentForegroundColor
{
if (self.perMessageExpirationHasExpired) {
return UIColor.ows_gray60Color;
} else if (self.isIncoming) {
return UIColor.ows_gray90Color;
} else {
return UIColor.ows_whiteColor;
}
}
- (UIView *)overlayHostView
{
return self.hStackView;
}
- (UIColor *)overlayBackgroundColor
{
if (self.perMessageExpirationHasExpired) {
return UIColor.ows_gray15Color;
} else if (self.isIncoming) {
return UIColor.ows_gray05Color;
} else {
return UIColor.ows_signalBlueColor;
}
}
- (BOOL)hasBottomFooter
{
return !self.viewItem.shouldHideFooter;
}
- (BOOL)insertAnyTextViewsIntoStackView:(NSArray<UIView *> *)textViews
{
if (textViews.count < 1) {
return NO;
}
UIStackView *textStackView = [[UIStackView alloc] initWithArrangedSubviews:textViews];
textStackView.axis = UILayoutConstraintAxisVertical;
textStackView.spacing = self.textViewVSpacing;
textStackView.layoutMarginsRelativeArrangement = YES;
textStackView.layoutMargins = UIEdgeInsetsMake(self.conversationStyle.textInsetTop,
self.conversationStyle.textInsetHorizontal,
self.conversationStyle.textInsetBottom,
self.conversationStyle.textInsetHorizontal);
[self.vStackView addArrangedSubview:textStackView];
return YES;
}
- (CGFloat)textViewVSpacing
{
return 2.f;
}
#pragma mark - Load / Unload
- (void)loadContent
{
// Do nothing.
}
- (void)unloadContent
{
// Do nothing.
}
#pragma mark - Subviews
- (void)configureLabel
{
OWSAssertDebug(self.label);
self.label.textColor = self.contentForegroundColor;
self.label.font = UIFont.ows_dynamicTypeSubheadlineFont.ows_mediumWeight;
self.label.text
= (self.perMessageExpirationHasExpired ? NSLocalizedString(@"PER_MESSAGE_EXPIRATION_VIEWED",
@"Label for messages with per-message expiration indicating that "
@"user has viewed the message's contents.")
: NSLocalizedString(@"PER_MESSAGE_EXPIRATION_TAP_TO_VIEW",
@"Label for messages with per-message expiration indicating that "
@"user can tap to view the message's contents."));
self.label.lineBreakMode = NSLineBreakByTruncatingTail;
}
- (void)configureIconView
{
OWSAssertDebug(self.iconView);
[self.iconView setTemplateImageName:(self.perMessageExpirationHasExpired ? @"play-outline-24" : @"play-filled-24")
tintColor:self.contentForegroundColor];
}
- (void)addProgressViewsIfNecessary:(UIView *)bodyMediaView shouldShowDownloadProgress:(BOOL)shouldShowDownloadProgress
{
if (self.viewItem.attachmentStream) {
[self addUploadViewIfNecessary:bodyMediaView];
} else if (self.viewItem.attachmentPointer) {
[self addDownloadViewIfNecessary:bodyMediaView shouldShowDownloadProgress:(BOOL)shouldShowDownloadProgress];
}
}
- (void)addUploadViewIfNecessary:(UIView *)bodyMediaView
{
OWSAssertDebug(self.viewItem.attachmentStream);
if (!self.isOutgoing) {
return;
}
if (self.viewItem.attachmentStream.isUploaded) {
return;
}
AttachmentUploadView *uploadView = [[AttachmentUploadView alloc] initWithAttachment:self.viewItem.attachmentStream];
[self.bubbleView addSubview:uploadView];
[uploadView autoPinEdgesToSuperviewEdges];
[uploadView setContentHuggingLow];
[uploadView setCompressionResistanceLow];
}
- (void)addDownloadViewIfNecessary:(UIView *)bodyMediaView shouldShowDownloadProgress:(BOOL)shouldShowDownloadProgress
{
OWSAssertDebug(self.viewItem.attachmentPointer);
switch (self.viewItem.attachmentPointer.state) {
case TSAttachmentPointerStateFailed:
[self addTapToRetryView:self.overlayHostView];
return;
case TSAttachmentPointerStateEnqueued:
case TSAttachmentPointerStateDownloading:
break;
}
switch (self.viewItem.attachmentPointer.pointerType) {
case TSAttachmentPointerTypeRestoring:
// TODO: Show "restoring" indicator and possibly progress.
return;
case TSAttachmentPointerTypeUnknown:
case TSAttachmentPointerTypeIncoming:
break;
}
if (!shouldShowDownloadProgress) {
return;
}
NSString *_Nullable uniqueId = self.viewItem.attachmentPointer.uniqueId;
if (uniqueId.length < 1) {
OWSFailDebug(@"Missing uniqueId.");
return;
}
if ([self.attachmentDownloads downloadProgressForAttachmentId:uniqueId] == nil) {
OWSFailDebug(@"Missing download progress.");
return;
}
UIView *overlayView = [UIView new];
overlayView.backgroundColor = [self.overlayBackgroundColor colorWithAlphaComponent:0.5];
[bodyMediaView addSubview:overlayView];
[overlayView autoPinEdgesToSuperviewEdges];
[overlayView setContentHuggingLow];
[overlayView setCompressionResistanceLow];
MediaDownloadView *downloadView =
[[MediaDownloadView alloc] initWithAttachmentId:uniqueId radius:self.conversationStyle.maxMessageWidth * 0.1f];
bodyMediaView.layer.opacity = 0.5f;
[self.bubbleView addSubview:downloadView];
[downloadView autoPinEdgesToSuperviewEdges];
[downloadView setContentHuggingLow];
[downloadView setCompressionResistanceLow];
}
// TODO:
- (void)addTapToRetryView:(UIView *)bodyMediaView
{
OWSAssertDebug(self.viewItem.attachmentPointer);
// Hide the body media view, replace with "tap to retry" indicator.
UILabel *label = [UILabel new];
label.text = NSLocalizedString(
@"ATTACHMENT_DOWNLOADING_STATUS_FAILED", @"Status label when an attachment download has failed.");
label.font = UIFont.ows_dynamicTypeBodyFont;
label.textColor = Theme.secondaryColor;
label.numberOfLines = 0;
label.lineBreakMode = NSLineBreakByWordWrapping;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = self.overlayBackgroundColor;
[bodyMediaView addSubview:label];
[label autoPinEdgesToSuperviewMargins];
[label setContentHuggingLow];
[label setCompressionResistanceLow];
}
#pragma mark - Measurement
- (CGSize)contentSize
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
CGFloat hMargins = self.conversationStyle.textInsetHorizontal * 2;
const int maxTextWidth
= (int)floor(self.conversationStyle.maxMessageWidth - (hMargins + self.contentHSpacing + self.iconSize));
[self configureLabel];
CGSize result = CGSizeCeil([self.label sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
result.width += self.contentHSpacing + self.iconSize;
result.width = MIN(result.width, self.conversationStyle.maxMessageWidth);
result.height = MAX(result.height, self.iconSize);
return result;
}
- (CGSize)measureSize
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
OWSAssertDebug(self.viewItem);
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
CGSize cellSize = CGSizeZero;
[self configureBubbleRounding];
NSMutableArray<NSValue *> *textViewSizes = [NSMutableArray new];
CGSize contentSize = self.contentSize;
[textViewSizes addObject:[NSValue valueWithCGSize:contentSize]];
if (self.hasBottomFooter) {
CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem];
footerSize.width = MIN(footerSize.width, self.conversationStyle.maxMessageWidth);
[textViewSizes addObject:[NSValue valueWithCGSize:footerSize]];
}
if (textViewSizes.count > 0) {
CGSize groupSize = [self sizeForTextViewGroup:textViewSizes];
cellSize.width = MAX(cellSize.width, groupSize.width);
cellSize.height += groupSize.height;
}
// Make sure the bubble is always wide enough to complete it's bubble shape.
cellSize.width = MAX(cellSize.width, self.bubbleView.minWidth);
OWSAssertDebug(cellSize.width > 0 && cellSize.height > 0);
cellSize = CGSizeCeil(cellSize);
OWSAssertDebug(cellSize.width <= self.conversationStyle.maxMessageWidth);
cellSize.width = MIN(cellSize.width, self.conversationStyle.maxMessageWidth);
return cellSize;
}
- (CGSize)sizeForTextViewGroup:(NSArray<NSValue *> *)textViewSizes
{
OWSAssertDebug(textViewSizes);
OWSAssertDebug(textViewSizes.count > 0);
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
CGSize result = CGSizeZero;
for (NSValue *size in textViewSizes) {
result.width = MAX(result.width, size.CGSizeValue.width);
result.height += size.CGSizeValue.height;
}
result.height += self.textViewVSpacing * (textViewSizes.count - 1);
result.height += (self.conversationStyle.textInsetTop + self.conversationStyle.textInsetBottom);
result.width += self.conversationStyle.textInsetHorizontal * 2;
return result;
}
#pragma mark -
- (void)prepareForReuse
{
[NSLayoutConstraint deactivateConstraints:self.viewConstraints];
self.viewConstraints = [NSMutableArray new];
self.delegate = nil;
self.label.text = nil;
self.iconView.image = nil;
self.bubbleView.bubbleColor = nil;
[self.bubbleView clearPartnerViews];
for (UIView *subview in self.bubbleView.subviews) {
[subview removeFromSuperview];
}
[self.footerView removeFromSuperview];
[self.footerView prepareForReuse];
for (UIView *subview in self.vStackView.subviews) {
[subview removeFromSuperview];
}
for (UIView *subview in self.subviews) {
if (subview != self.bubbleView) {
[subview removeFromSuperview];
}
}
}
#pragma mark - Gestures
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
{
OWSAssertDebug(self.delegate);
if (sender.state != UIGestureRecognizerStateRecognized) {
OWSLogVerbose(@"Ignoring tap on message: %@", self.viewItem.interaction.debugDescription);
return;
}
// TODO:
}
- (OWSMessageGestureLocation)gestureLocationForLocation:(CGPoint)locationInMessageBubble
{
return OWSMessageGestureLocation_Default;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -132,6 +132,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@property (nonatomic, readonly, nullable) StickerInfo *stickerInfo;
@property (nonatomic, readonly, nullable) TSAttachmentStream *stickerAttachment;
@property (nonatomic, readonly) BOOL isFailedSticker;
@property (nonatomic, readonly) BOOL perMessageExpirationHasExpired;
@property (nonatomic, readonly, nullable) NSString *systemMessageText;

View File

@ -99,6 +99,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
@property (nonatomic, nullable) StickerInfo *stickerInfo;
@property (nonatomic, nullable) TSAttachmentStream *stickerAttachment;
@property (nonatomic) BOOL isFailedSticker;
@property (nonatomic) BOOL perMessageExpirationHasExpired;
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
@property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer;
@property (nonatomic, nullable) ContactShareViewModel *contactShare;
@ -171,6 +172,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
self.stickerInfo = nil;
self.stickerAttachment = nil;
self.isFailedSticker = NO;
self.perMessageExpirationHasExpired = NO;
self.contactShare = nil;
self.systemMessageText = nil;
self.authorConversationColorName = nil;
@ -379,6 +381,17 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
[self clearCachedLayoutState];
}
- (void)setPerMessageExpirationHasExpired:(BOOL)perMessageExpirationHasExpired
{
if (_perMessageExpirationHasExpired == perMessageExpirationHasExpired) {
return;
}
_perMessageExpirationHasExpired = perMessageExpirationHasExpired;
[self clearCachedLayoutState];
}
- (void)clearCachedLayoutState
{
self.cachedCellSize = nil;
@ -673,7 +686,30 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
TSMessage *message = (TSMessage *)self.interaction;
if (message.hasPerMessageExpiration) {
self.messageCellType = OWSMessageCellType_PerMessageExpiration;
if (transaction.transitional_yapReadTransaction) {
NSArray<TSAttachment *> *mediaAttachments =
[message mediaAttachmentsWithTransaction:transaction.transitional_yapReadTransaction];
// TODO: We currently only support single attachments for messages
// with per-message expiration.
TSAttachment *_Nullable mediaAttachment = mediaAttachments.firstObject;
if ([mediaAttachment isKindOfClass:[TSAttachmentPointer class]]) {
self.messageCellType = OWSMessageCellType_PerMessageExpiration;
self.perMessageExpirationHasExpired = message.perMessageExpirationHasExpired;
self.attachmentPointer = (TSAttachmentPointer *)mediaAttachment;
return;
} else if ([mediaAttachment isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)mediaAttachment;
if (attachmentStream.isValidVisualMedia) {
self.messageCellType = OWSMessageCellType_PerMessageExpiration;
self.perMessageExpirationHasExpired = message.perMessageExpirationHasExpired;
self.attachmentStream = attachmentStream;
return;
}
}
}
OWSFailDebug(@"Invalid media for message with per-message expiration.");
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}

View File

@ -1637,6 +1637,12 @@
/* Navbar title for for PDF view. */
"PDF_VIEW_TITLE" = "PDF";
/* Label for messages with per-message expiration indicating that user can tap to view the message's contents. */
"PER_MESSAGE_EXPIRATION_TAP_TO_VIEW" = "Tap to view";
/* Label for messages with per-message expiration indicating that user has viewed the message's contents. */
"PER_MESSAGE_EXPIRATION_VIEWED" = "Viewed";
/* A format for a label showing an example phone number. Embeds {{the example phone number}}. */
"PHONE_NUMBER_EXAMPLE_FORMAT" = "Example: %@";