diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c79735bc04..f43e4348a2 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 346E35BD224283B000E55D5F /* UIAlertController+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+OWS.swift"; sourceTree = ""; }; 346E9D5321B040B600562252 /* RegistrationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationController.swift; sourceTree = ""; }; + 347137FA22A1BCE800F43A63 /* OWSMessageHiddenView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageHiddenView.h; sourceTree = ""; }; + 347137FB22A1BCE800F43A63 /* OWSMessageHiddenView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageHiddenView.m; sourceTree = ""; }; 347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = ""; }; 3478505A1FD999D5007B8332 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = translations/et.lproj/Localizable.strings; sourceTree = ""; }; 3478505C1FD99A1F007B8332 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = translations/zh_TW.lproj/Localizable.strings; sourceTree = ""; }; @@ -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 */, diff --git a/Signal/Images.xcassets/play-outline-24.imageset/Contents.json b/Signal/Images.xcassets/play-outline-24.imageset/Contents.json new file mode 100644 index 0000000000..1590f74ec2 --- /dev/null +++ b/Signal/Images.xcassets/play-outline-24.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@1x.png b/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@1x.png new file mode 100644 index 0000000000..d0edeb1e7d Binary files /dev/null and b/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@1x.png differ diff --git a/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@2x.png b/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@2x.png new file mode 100644 index 0000000000..9b81241694 Binary files /dev/null and b/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@2x.png differ diff --git a/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@3x.png b/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@3x.png new file mode 100644 index 0000000000..c558b72771 Binary files /dev/null and b/Signal/Images.xcassets/play-outline-24.imageset/play-outline-24@3x.png differ diff --git a/Signal/src/ViewControllers/ColorPickerViewController.swift b/Signal/src/ViewControllers/ColorPickerViewController.swift index 0f93d14576..9263e1bee2 100644 --- a/Signal/src/ViewControllers/ColorPickerViewController.swift +++ b/Signal/src/ViewControllers/ColorPickerViewController.swift @@ -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() diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h index 5119d3c1a8..d6dd8da837 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -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 *bubbleGradientColors; @property (nonatomic) OWSDirectionalRectCorner sharpCorners; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index 14ee8e4360..7f09bcfc37 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -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> *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 *)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]; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index f227d078a0..b3f44e5638 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -6,6 +6,7 @@ #import "OWSContactAvatarBuilder.h" #import "OWSMessageBubbleView.h" #import "OWSMessageHeaderView.h" +#import "OWSMessageHiddenView.h" #import "OWSMessageStickerView.h" #import "Signal-Swift.h" diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.h new file mode 100644 index 0000000000..a08714d2f7 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.h @@ -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)viewItem +// attachmentStream:(TSAttachmentStream *)attachmentStream +// imageView:(UIView *)imageView; +// +//- (void)didTapVideoViewItem:(id)viewItem +// attachmentStream:(TSAttachmentStream *)attachmentStream +// imageView:(UIView *)imageView; +// +//- (void)didTapAudioViewItem:(id)viewItem attachmentStream:(TSAttachmentStream +//*)attachmentStream; +// +//- (void)didTapTruncatedTextMessage:(id)conversationItem; +// +//- (void)didTapFailedIncomingAttachment:(id)viewItem; +// +//- (void)didTapConversationItem:(id)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply; +//- (void)didTapConversationItem:(id)viewItem +// quotedReply:(OWSQuotedReplyModel *)quotedReply +// failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer; +// +//- (void)didTapConversationItem:(id)viewItem linkPreview:(OWSLinkPreview *)linkPreview; +// +//- (void)didTapContactShareViewItem:(id)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 delegate; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.m new file mode 100644 index 0000000000..9955af59d9 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.m @@ -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 + +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 *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 *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 *)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 *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 *)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 diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 6cb2b579c7..cbbfaaa2ed 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -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; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index fc9be718b3..24b31b88bd 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -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 *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; } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index fb7001c0f3..8eb3465dbc 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -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: %@";