From 369118b045d95f4b68af99c7914bc00d7f5207c3 Mon Sep 17 00:00:00 2001 From: Max Radermacher Date: Fri, 1 Jul 2022 09:51:27 -0700 Subject: [PATCH] Add support for replying to gift messages --- .../gift-thumbnail.imageset/Contents.json | 12 ++++ .../gift-thumbnail.pdf | Bin 0 -> 24969 bytes .../CV/CVItemViewModelImpl.swift | 6 ++ .../Cells/OWSQuotedMessageView.m | 32 ++++++++- .../Cells/QuotedMessageView.swift | 65 ++++++++++++++++-- .../translations/en.lproj/Localizable.strings | 3 + SignalServiceKit/protobuf/SignalService.proto | 6 ++ .../Messages/Interactions/TSOutgoingMessage.m | 11 ++- .../Messages/Interactions/TSQuotedMessage.h | 5 +- .../Messages/Interactions/TSQuotedMessage.m | 33 +++++++-- .../src/Protos/Generated/SSKProto.swift | 50 ++++++++++++++ .../Protos/Generated/SignalService.pb.swift | 54 +++++++++++++++ SignalUI/Categories/UIView+SignalUI.swift | 59 ++++++++++++++-- SignalUI/ViewModels/CVItemViewModel.h | 3 +- SignalUI/ViewModels/OWSQuotedReplyModel.h | 1 + SignalUI/ViewModels/OWSQuotedReplyModel.m | 46 ++++++++++--- 16 files changed, 355 insertions(+), 31 deletions(-) create mode 100644 Signal/Images.xcassets/gift-thumbnail.imageset/Contents.json create mode 100644 Signal/Images.xcassets/gift-thumbnail.imageset/gift-thumbnail.pdf diff --git a/Signal/Images.xcassets/gift-thumbnail.imageset/Contents.json b/Signal/Images.xcassets/gift-thumbnail.imageset/Contents.json new file mode 100644 index 0000000000..937ead4154 --- /dev/null +++ b/Signal/Images.xcassets/gift-thumbnail.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "gift-thumbnail.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/gift-thumbnail.imageset/gift-thumbnail.pdf b/Signal/Images.xcassets/gift-thumbnail.imageset/gift-thumbnail.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d8897ce3b7b74e2c97a813b7033dd3669ab06d11 GIT binary patch literal 24969 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 0 - && ![OWSMimeTypeOversizeTextMessage isEqualToString:self.quotedMessage.contentType]); + if (self.quotedMessage.contentType.length > 0 + && ![OWSMimeTypeOversizeTextMessage isEqualToString:self.quotedMessage.contentType]) { + return YES; + } + if (self.quotedMessage.isGiftBadge) { + return YES; + } + return NO; } - (BOOL)hasQuotedAttachmentThumbnailImage @@ -236,6 +242,16 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3; [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapFailedThumbnailDownload:)]; [quotedAttachmentView addGestureRecognizer:tapGesture]; quotedAttachmentView.userInteractionEnabled = YES; + } else if (self.quotedMessage.isGiftBadge) { + UIImage *giftIcon = [UIImage imageNamed:@"gift-thumbnail"]; + UIImageView *contentImageView = [self imageViewForImage:giftIcon]; + contentImageView.contentMode = UIViewContentModeScaleAspectFit; + + UIView *wrapper = [UIView transparentContainer]; + [wrapper addSubview:contentImageView]; + [contentImageView autoCenterInSuperview]; + [contentImageView autoSetDimension:ALDimensionWidth toSize:self.quotedAttachmentSize]; + quotedAttachmentView = wrapper; } else { // TODO: Should we overlay the file extension like we do with CVComponentGenericAttachment UIImage *contentIcon = [UIImage imageNamed:@"generic-attachment"]; @@ -416,6 +432,12 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3; NSFontAttributeName : self.filenameFont, NSForegroundColorAttributeName : self.filenameTextColor, }]; + } else if (self.quotedMessage.isGiftBadge) { + attributedText = [[NSAttributedString alloc] initWithString:[self giftTypeForSnippet] + attributes:@{ + NSFontAttributeName : self.fileTypeFont, + NSForegroundColorAttributeName : self.fileTypeTextColor, + }]; } else { attributedText = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"QUOTED_REPLY_TYPE_ATTACHMENT", @@ -480,6 +502,12 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3; return nil; } +- (nullable NSString *)giftTypeForSnippet +{ + return NSLocalizedString( + @"BADGE_GIFTING_REPLY", @"Shown when you're replying to a gift message to indicate that it contains a gift."); +} + - (BOOL)isAudioAttachment { // TODO: Are we going to use the filename? For all mimetypes? diff --git a/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift b/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift index 32c09c1b29..9dc3bbae5e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/QuotedMessageView.swift @@ -3,6 +3,7 @@ // import Foundation +import UIKit @objc public protocol QuotedMessageViewDelegate { @@ -169,7 +170,7 @@ public class QuotedMessageView: ManualStackViewWithLayer { } var hasQuotedThumbnail: Bool { - contentTypeWithThumbnail != nil || quotedReplyModel.thumbnailViewFactory != nil + contentTypeWithThumbnail != nil || quotedReplyModel.thumbnailViewFactory != nil || quotedReplyModel.isGiftBadge } var hasReaction: Bool { @@ -273,6 +274,15 @@ public class QuotedMessageView: ManualStackViewWithLayer { .font: filenameFont, .foregroundColor: filenameTextColor ]) + } else if self.quotedReplyModel.isGiftBadge { + attributedText = NSAttributedString( + string: NSLocalizedString( + "BADGE_GIFTING_REPLY", + comment: "Shown when you're replying to a gift message to indicate that it contains a gift." + ), + // This appears in the same context as fileType, so use the same font/color. + attributes: [.font: self.fileTypeFont, .foregroundColor: self.fileTypeTextColor] + ) } else { let string = NSLocalizedString("QUOTED_REPLY_TYPE_ATTACHMENT", comment: "Indicates this message is a quoted reply to an attachment of unknown type.") @@ -349,12 +359,13 @@ public class QuotedMessageView: ManualStackViewWithLayer { } } + private static let sharpCornerRadius: CGFloat = 4 + private static let wideCornerRadius: CGFloat = 10 + private func createBubbleView(sharpCorners: OWSDirectionalRectCorner, conversationStyle: ConversationStyle, configurator: Configurator, componentDelegate: CVComponentDelegate) -> ManualLayoutView { - let sharpCornerRadius: CGFloat = 4 - let wideCornerRadius: CGFloat = 10 // Background chatColorView.configure(value: conversationStyle.bubbleChatColorOutgoing, @@ -376,7 +387,7 @@ public class QuotedMessageView: ManualStackViewWithLayer { // Mask & Rounding if sharpCorners.isEmpty || sharpCorners.contains(.allCorners) { bubbleView.layer.maskedCorners = .all - bubbleView.layer.cornerRadius = sharpCorners.isEmpty ? wideCornerRadius : sharpCornerRadius + bubbleView.layer.cornerRadius = sharpCorners.isEmpty ? Self.wideCornerRadius : Self.sharpCornerRadius } else { // Slow path. CA isn't optimized to handle corners of multiple radii // Let's do it by hand with a CAShapeLayer @@ -385,8 +396,8 @@ public class QuotedMessageView: ManualStackViewWithLayer { let sharpCorners = UIView.uiRectCorner(forOWSDirectionalRectCorner: sharpCorners) let bezierPath = UIBezierPath.roundedRect(view.bounds, sharpCorners: sharpCorners, - sharpCornerRadius: sharpCornerRadius, - wideCornerRadius: wideCornerRadius) + sharpCornerRadius: Self.sharpCornerRadius, + wideCornerRadius: Self.wideCornerRadius) maskLayer.path = bezierPath.cgPath } bubbleView.layer.mask = maskLayer @@ -443,6 +454,7 @@ public class QuotedMessageView: ManualStackViewWithLayer { // some performance cost. quotedImageView.layer.minificationFilter = .trilinear quotedImageView.layer.magnificationFilter = .trilinear + quotedImageView.layer.mask = nil func tryToLoadThumbnailImage() -> UIImage? { guard let contentType = configurator.contentTypeWithThumbnail, @@ -490,6 +502,47 @@ public class QuotedMessageView: ManualStackViewWithLayer { action: #selector(didTapFailedThumbnailDownload))) wrapper.isUserInteractionEnabled = true + return wrapper + } else if quotedReplyModel.isGiftBadge { + quotedImageView.image = UIImage(named: "gift-thumbnail") + quotedImageView.contentMode = .scaleAspectFit + quotedImageView.clipsToBounds = false + + let wrapper = ManualLayoutViewWithLayer(name: "giftBadgeWrapper") + wrapper.addSubviewToFillSuperviewEdges(quotedImageView) + + // For outgoing replies to gift messages, the wrapping image is blue, and + // the bubble can be the same shade of blue. This looks odd, so add a 1pt + // white border in that case. + if configurator.isOutgoing && !configurator.isForPreview { + // The gift badge needs to know which corners to round, which depends on + // whether or not there's adjacent content in the parent container. We care + // about "edges that are against the rounded parent edges", and then we + // round the corners at the intersection of those edges. For example, in + // the common case, we'll be pressing against the top, trailing, and bottom + // edges, so we round the .topTrailing and .bottomTrailing corners. + var eligibleCorners: OWSDirectionalRectCorner = [.topTrailing, .bottomTrailing] + if quotedReplyModel.isRemotelySourced { + eligibleCorners.remove(.bottomTrailing) + } + let maskLayer = CAShapeLayer() + quotedImageView.addLayoutBlock { view in + let maskRect = view.bounds.insetBy(dx: 1, dy: 1) + maskLayer.path = UIBezierPath.roundedRect( + maskRect, + sharpCorners: UIView.uiRectCorner( + forOWSDirectionalRectCorner: sharpCorners.intersection(eligibleCorners) + ), + sharpCornerRadius: Self.sharpCornerRadius, + wideCorners: UIView.uiRectCorner( + forOWSDirectionalRectCorner: eligibleCorners.subtracting(sharpCorners) + ), + wideCornerRadius: Self.wideCornerRadius + ).cgPath + } + quotedImageView.layer.mask = maskLayer + wrapper.backgroundColor = .ows_white + } return wrapper } else { // TODO: Should we overlay the file extension like we do with CVComponentGenericAttachment diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index e6cfd7a1e4..4bb48fce6a 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -466,6 +466,9 @@ /* Label for a button to see details about a gift you've already redeemed. The text is shown next to a checkmark. */ "BADGE_GIFTING_REDEEMED" = "Redeemed"; +/* Shown when you're replying to a gift message to indicate that it contains a gift. */ +"BADGE_GIFTING_REPLY" = "Gift"; + /* When gifting a badge, shows how long the badge lasts. Embeds {formatted duration}. */ "BADGE_GIFTING_ROW_DURATION" = "Lasts %@"; diff --git a/SignalServiceKit/protobuf/SignalService.proto b/SignalServiceKit/protobuf/SignalService.proto index f6d206097f..3651371850 100644 --- a/SignalServiceKit/protobuf/SignalService.proto +++ b/SignalServiceKit/protobuf/SignalService.proto @@ -201,6 +201,11 @@ message DataMessage { } message Quote { + enum Type { + NORMAL = 0; + GIFT_BADGE = 1; + } + message QuotedAttachment { optional string contentType = 1; optional string fileName = 2; @@ -214,6 +219,7 @@ message DataMessage { optional string text = 3; repeated QuotedAttachment attachments = 4; repeated BodyRange bodyRanges = 6; + optional Type type = 7; } message Contact { diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index 7e984ed25b..804fd48cfb 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -1453,7 +1453,9 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1; BOOL hasQuotedText = NO; BOOL hasQuotedAttachment = NO; - if (self.quotedMessage.body.length > 0) { + BOOL hasQuotedGiftBadge = NO; + + if (quotedMessage.body.length > 0) { hasQuotedText = YES; [quoteBuilder setText:quotedMessage.body]; @@ -1489,7 +1491,12 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1; hasQuotedAttachment = YES; } - if (hasQuotedText || hasQuotedAttachment) { + if (quotedMessage.isGiftBadge) { + [quoteBuilder setType:SSKProtoDataMessageQuoteTypeGiftBadge]; + hasQuotedGiftBadge = YES; + } + + if (hasQuotedText || hasQuotedAttachment || hasQuotedGiftBadge) { return quoteBuilder; } else { OWSFailDebug(@"Invalid quoted message data."); diff --git a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h index ea9828b909..a0f8c446d5 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h @@ -34,6 +34,8 @@ typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) { @property (nullable, nonatomic, readonly) NSString *body; @property (nonatomic, readonly, nullable) MessageBodyRanges *bodyRanges; +@property (nonatomic, readonly) BOOL isGiftBadge; + #pragma mark - Attachments @property (nonatomic, readonly) BOOL hasAttachment; @@ -62,7 +64,8 @@ typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) { authorAddress:(SignalServiceAddress *)authorAddress body:(nullable NSString *)body bodyRanges:(nullable MessageBodyRanges *)bodyRanges - quotedAttachmentForSending:(nullable TSAttachment *)attachment; + quotedAttachmentForSending:(nullable TSAttachment *)attachment + isGiftBadge:(BOOL)isGiftBadge; // used when receiving quoted messages + (nullable instancetype)quotedMessageForDataMessage:(SSKProtoDataMessage *)dataMessage diff --git a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m index 431a8119cc..85388db45f 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.m @@ -146,6 +146,7 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { bodyRanges:(nullable MessageBodyRanges *)bodyRanges bodySource:(TSQuotedMessageContentSource)bodySource receivedQuotedAttachmentInfo:(nullable OWSAttachmentInfo *)attachmentInfo + isGiftBadge:(BOOL)isGiftBadge { OWSAssertDebug(timestamp > 0); OWSAssertDebug(authorAddress.isValid); @@ -161,6 +162,7 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { _bodyRanges = bodyRanges; _bodySource = bodySource; _quotedAttachment = attachmentInfo; + _isGiftBadge = isGiftBadge; return self; } @@ -170,6 +172,7 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { body:(nullable NSString *)body bodyRanges:(nullable MessageBodyRanges *)bodyRanges quotedAttachmentForSending:(nullable TSAttachmentStream *)attachment + isGiftBadge:(BOOL)isGiftBadge { OWSAssertDebug(timestamp > 0); OWSAssertDebug(authorAddress.isValid); @@ -185,6 +188,7 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { _bodyRanges = bodyRanges; _bodySource = TSQuotedMessageContentSourceLocal; _quotedAttachment = attachment ? [[OWSAttachmentInfo alloc] initWithOriginalAttachmentStream:attachment] : nil; + _isGiftBadge = isGiftBadge; return self; } @@ -292,12 +296,14 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { body:body bodyRanges:nil bodySource:TSQuotedMessageContentSourceLocal - receivedQuotedAttachmentInfo:nil]; + receivedQuotedAttachmentInfo:nil + isGiftBadge:NO]; } NSString *_Nullable body = nil; MessageBodyRanges *_Nullable bodyRanges = nil; OWSAttachmentInfo *attachmentInfo = nil; + BOOL isGiftBadge = NO; if (quotedMessage.body.length > 0) { body = quotedMessage.body; @@ -309,6 +315,8 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { body = [@"👤 " stringByAppendingString:quotedMessage.contactShare.name.displayName]; } else if (quotedMessage.storyReactionEmoji.length > 0) { body = quotedMessage.storyReactionEmoji; + } else if (quotedMessage.giftBadge != nil) { + isGiftBadge = YES; } SSKProtoDataMessageQuoteQuotedAttachment *_Nullable firstAttachmentProto = proto.attachments.firstObject; @@ -344,8 +352,8 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { } } - if (body.length == 0 && !attachmentInfo) { - OWSFailDebug(@"quoted message has neither text nor attachment"); + if (body.length == 0 && !attachmentInfo && !isGiftBadge) { + OWSFailDebug(@"quoted message has no content"); return nil; } @@ -364,7 +372,8 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { body:body bodyRanges:bodyRanges bodySource:TSQuotedMessageContentSourceLocal - receivedQuotedAttachmentInfo:attachmentInfo]; + receivedQuotedAttachmentInfo:attachmentInfo + isGiftBadge:isGiftBadge]; } /// Builds a remote message from the proto payload @@ -373,6 +382,19 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { + (nullable TSQuotedMessage *)remoteQuotedMessageFromQuoteProto:(SSKProtoDataMessageQuote *)proto transaction:(SDSAnyWriteTransaction *)transaction { + // This is untrusted content from other users that may not be well-formed. + // The GiftBadge type has no content/attachments, so don't read those + // fields if the type is GiftBadge. + if (proto.hasType && (proto.unwrappedType == SSKProtoDataMessageQuoteTypeGiftBadge)) { + return [[TSQuotedMessage alloc] initWithTimestamp:proto.id + authorAddress:proto.authorAddress + body:nil + bodyRanges:nil + bodySource:TSQuotedMessageContentSourceRemote + receivedQuotedAttachmentInfo:nil + isGiftBadge:YES]; + } + NSString *_Nullable body = nil; MessageBodyRanges *_Nullable bodyRanges = nil; OWSAttachmentInfo *attachmentInfo = nil; @@ -409,7 +431,8 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { body:body bodyRanges:bodyRanges bodySource:TSQuotedMessageContentSourceRemote - receivedQuotedAttachmentInfo:attachmentInfo]; + receivedQuotedAttachmentInfo:attachmentInfo + isGiftBadge:NO]; } else { OWSFailDebug(@"Failed to construct a valid quoted message from remote proto content"); return nil; diff --git a/SignalServiceKit/src/Protos/Generated/SSKProto.swift b/SignalServiceKit/src/Protos/Generated/SSKProto.swift index 5c4f9636e5..0cba3bc766 100644 --- a/SignalServiceKit/src/Protos/Generated/SSKProto.swift +++ b/SignalServiceKit/src/Protos/Generated/SSKProto.swift @@ -4004,6 +4004,28 @@ extension SSKProtoDataMessageQuoteQuotedAttachmentBuilder { #endif +// MARK: - SSKProtoDataMessageQuoteType + +@objc +public enum SSKProtoDataMessageQuoteType: Int32 { + case normal = 0 + case giftBadge = 1 +} + +private func SSKProtoDataMessageQuoteTypeWrap(_ value: SignalServiceProtos_DataMessage.Quote.TypeEnum) -> SSKProtoDataMessageQuoteType { + switch value { + case .normal: return .normal + case .giftBadge: return .giftBadge + } +} + +private func SSKProtoDataMessageQuoteTypeUnwrap(_ value: SSKProtoDataMessageQuoteType) -> SignalServiceProtos_DataMessage.Quote.TypeEnum { + switch value { + case .normal: return .normal + case .giftBadge: return .giftBadge + } +} + // MARK: - SSKProtoDataMessageQuote @objc @@ -4056,6 +4078,26 @@ public class SSKProtoDataMessageQuote: NSObject, Codable, NSSecureCoding { return proto.hasText } + public var type: SSKProtoDataMessageQuoteType? { + guard hasType else { + return nil + } + return SSKProtoDataMessageQuoteTypeWrap(proto.type) + } + // This "unwrapped" accessor should only be used if the "has value" accessor has already been checked. + @objc + public var unwrappedType: SSKProtoDataMessageQuoteType { + if !hasType { + // TODO: We could make this a crashing assert. + owsFailDebug("Unsafe unwrap of missing optional: Quote.type.") + } + return SSKProtoDataMessageQuoteTypeWrap(proto.type) + } + @objc + public var hasType: Bool { + return proto.hasType + } + @objc public var hasValidAuthor: Bool { return authorAddress != nil @@ -4206,6 +4248,9 @@ extension SSKProtoDataMessageQuote { } builder.setAttachments(attachments) builder.setBodyRanges(bodyRanges) + if let _value = type { + builder.setType(_value) + } if let _value = unknownFields { builder.setUnknownFields(_value) } @@ -4294,6 +4339,11 @@ public class SSKProtoDataMessageQuoteBuilder: NSObject { proto.bodyRanges = wrappedItems.map { $0.proto } } + @objc + public func setType(_ valueParam: SSKProtoDataMessageQuoteType) { + proto.type = SSKProtoDataMessageQuoteTypeUnwrap(valueParam) + } + public func setUnknownFields(_ unknownFields: SwiftProtobuf.UnknownStorage) { proto.unknownFields = unknownFields } diff --git a/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift b/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift index 0704557db5..14344d6d49 100644 --- a/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift +++ b/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift @@ -1441,8 +1441,43 @@ struct SignalServiceProtos_DataMessage { var bodyRanges: [SignalServiceProtos_DataMessage.BodyRange] = [] + var type: SignalServiceProtos_DataMessage.Quote.TypeEnum { + get {return _type ?? .normal} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case normal // = 0 + case giftBadge // = 1 + + init() { + self = .normal + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .normal + case 1: self = .giftBadge + default: return nil + } + } + + var rawValue: Int { + switch self { + case .normal: return 0 + case .giftBadge: return 1 + } + } + + } + struct QuotedAttachment { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -1488,6 +1523,7 @@ struct SignalServiceProtos_DataMessage { fileprivate var _authorE164: String? = nil fileprivate var _authorUuid: String? = nil fileprivate var _text: String? = nil + fileprivate var _type: SignalServiceProtos_DataMessage.Quote.TypeEnum? = nil } struct Contact { @@ -2444,6 +2480,10 @@ extension SignalServiceProtos_DataMessage.ProtocolVersion: CaseIterable { // Support synthesized by the compiler. } +extension SignalServiceProtos_DataMessage.Quote.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + extension SignalServiceProtos_DataMessage.Contact.Phone.TypeEnum: CaseIterable { // Support synthesized by the compiler. } @@ -4592,6 +4632,7 @@ extension SignalServiceProtos_DataMessage: @unchecked Sendable {} extension SignalServiceProtos_DataMessage.Flags: @unchecked Sendable {} extension SignalServiceProtos_DataMessage.ProtocolVersion: @unchecked Sendable {} extension SignalServiceProtos_DataMessage.Quote: @unchecked Sendable {} +extension SignalServiceProtos_DataMessage.Quote.TypeEnum: @unchecked Sendable {} extension SignalServiceProtos_DataMessage.Quote.QuotedAttachment: @unchecked Sendable {} extension SignalServiceProtos_DataMessage.Contact: @unchecked Sendable {} extension SignalServiceProtos_DataMessage.Contact.Name: @unchecked Sendable {} @@ -5963,6 +6004,7 @@ extension SignalServiceProtos_DataMessage.Quote: SwiftProtobuf.Message, SwiftPro 3: .same(proto: "text"), 4: .same(proto: "attachments"), 6: .same(proto: "bodyRanges"), + 7: .same(proto: "type"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -5977,6 +6019,7 @@ extension SignalServiceProtos_DataMessage.Quote: SwiftProtobuf.Message, SwiftPro case 4: try { try decoder.decodeRepeatedMessageField(value: &self.attachments) }() case 5: try { try decoder.decodeSingularStringField(value: &self._authorUuid) }() case 6: try { try decoder.decodeRepeatedMessageField(value: &self.bodyRanges) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self._type) }() default: break } } @@ -6005,6 +6048,9 @@ extension SignalServiceProtos_DataMessage.Quote: SwiftProtobuf.Message, SwiftPro if !self.bodyRanges.isEmpty { try visitor.visitRepeatedMessageField(value: self.bodyRanges, fieldNumber: 6) } + try { if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 7) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -6015,11 +6061,19 @@ extension SignalServiceProtos_DataMessage.Quote: SwiftProtobuf.Message, SwiftPro if lhs._text != rhs._text {return false} if lhs.attachments != rhs.attachments {return false} if lhs.bodyRanges != rhs.bodyRanges {return false} + if lhs._type != rhs._type {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } +extension SignalServiceProtos_DataMessage.Quote.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "NORMAL"), + 1: .same(proto: "GIFT_BADGE"), + ] +} + extension SignalServiceProtos_DataMessage.Quote.QuotedAttachment: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = SignalServiceProtos_DataMessage.Quote.protoMessageName + ".QuotedAttachment" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/SignalUI/Categories/UIView+SignalUI.swift b/SignalUI/Categories/UIView+SignalUI.swift index ea762cb3e0..963659ceb2 100644 --- a/SignalUI/Categories/UIView+SignalUI.swift +++ b/SignalUI/Categories/UIView+SignalUI.swift @@ -3,6 +3,7 @@ // import Foundation +import UIKit @objc public extension UINavigationController { @@ -805,14 +806,62 @@ public extension UIView { @objc public extension UIBezierPath { - static func roundedRect(_ rect: CGRect, - sharpCorners: UIRectCorner, - sharpCornerRadius: CGFloat, - wideCornerRadius: CGFloat) -> UIBezierPath { + /// Create a roundedRect path with two different corner radii. + /// + /// - Parameters: + /// - rect: The outer bounds of the roundedRect. + /// - sharpCorners: The corners that should use `sharpCornerRadius`. The + /// other corners will use `wideCornerRadius`. + /// - sharpCornerRadius: The corner radius of `sharpCorners`. + /// - wideCornerRadius: The corner radius of non-`sharpCorners`. + /// + static func roundedRect( + _ rect: CGRect, + sharpCorners: UIRectCorner, + sharpCornerRadius: CGFloat, + wideCornerRadius: CGFloat + ) -> UIBezierPath { + + return roundedRect( + rect, + sharpCorners: sharpCorners, + sharpCornerRadius: sharpCornerRadius, + wideCorners: .allCorners.subtracting(sharpCorners), + wideCornerRadius: wideCornerRadius + ) + } + + /// Create a roundedRect path with two different corner radii. + /// + /// The behavior is undefined if `sharpCorners` and `wideCorners` overlap. + /// + /// - Parameters: + /// - rect: The outer bounds of the roundedRect. + /// - sharpCorners: The corners that should use `sharpCornerRadius`. + /// - sharpCornerRadius: The corner radius of `sharpCorners`. + /// - wideCorners: The corners that should use `wideCornerRadius`. + /// - wideCornerRadius: The corner radius of `wideCorners`. + /// + static func roundedRect( + _ rect: CGRect, + sharpCorners: UIRectCorner, + sharpCornerRadius: CGFloat, + wideCorners: UIRectCorner, + wideCornerRadius: CGFloat + ) -> UIBezierPath { + + assert(sharpCorners.isDisjoint(with: wideCorners)) + let bezierPath = UIBezierPath() func cornerRounding(forCorner corner: UIRectCorner) -> CGFloat { - sharpCorners.contains(corner) ? sharpCornerRadius : wideCornerRadius + if sharpCorners.contains(corner) { + return sharpCornerRadius + } + if wideCorners.contains(corner) { + return wideCornerRadius + } + return 0 } let topLeftRounding = cornerRounding(forCorner: .topLeft) let topRightRounding = cornerRounding(forCorner: .topRight) diff --git a/SignalUI/ViewModels/CVItemViewModel.h b/SignalUI/ViewModels/CVItemViewModel.h index 14bc82dc81..49500c0b6d 100644 --- a/SignalUI/ViewModels/CVItemViewModel.h +++ b/SignalUI/ViewModels/CVItemViewModel.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -21,6 +21,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, nullable) StickerInfo *stickerInfo; @property (nonatomic, readonly, nullable) TSAttachmentStream *stickerAttachment; @property (nonatomic, readonly, nullable) StickerMetadata *stickerMetadata; +@property (nonatomic, readonly) BOOL isGiftBadge; @end diff --git a/SignalUI/ViewModels/OWSQuotedReplyModel.h b/SignalUI/ViewModels/OWSQuotedReplyModel.h index 16eeb48aef..ee20807961 100644 --- a/SignalUI/ViewModels/OWSQuotedReplyModel.h +++ b/SignalUI/ViewModels/OWSQuotedReplyModel.h @@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nullable, nonatomic, readonly) NSString *reactionEmoji; @property (nonatomic, readonly) BOOL isRemotelySourced; @property (nonatomic, readonly) BOOL isStory; +@property (nonatomic, readonly) BOOL isGiftBadge; #pragma mark - Attachments diff --git a/SignalUI/ViewModels/OWSQuotedReplyModel.m b/SignalUI/ViewModels/OWSQuotedReplyModel.m index 4ee1772553..90412854ea 100644 --- a/SignalUI/ViewModels/OWSQuotedReplyModel.m +++ b/SignalUI/ViewModels/OWSQuotedReplyModel.m @@ -33,7 +33,9 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:(nullable NSString *)sourceFilename attachmentStream:(nullable TSAttachmentStream *)attachmentStream failedThumbnailAttachmentPointer:(nullable TSAttachmentPointer *)failedThumbnailAttachmentPointer - reactionEmoji:(nullable NSString *)reactionEmoji NS_DESIGNATED_INITIALIZER; + reactionEmoji:(nullable NSString *)reactionEmoji + isGiftBadge:(BOOL)isGiftBadge NS_DESIGNATED_INITIALIZER; + @end @@ -54,6 +56,7 @@ NS_ASSUME_NONNULL_BEGIN attachmentStream:(nullable TSAttachmentStream *)attachmentStream failedThumbnailAttachmentPointer:(nullable TSAttachmentPointer *)failedThumbnailAttachmentPointer reactionEmoji:(nullable NSString *)reactionEmoji + isGiftBadge:(BOOL)isGiftBadge { self = [super init]; if (!self) { @@ -72,6 +75,7 @@ NS_ASSUME_NONNULL_BEGIN _attachmentStream = attachmentStream; _failedThumbnailAttachmentPointer = failedThumbnailAttachmentPointer; _reactionEmoji = reactionEmoji; + _isGiftBadge = isGiftBadge; return self; } @@ -118,7 +122,8 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:quotedMessage.sourceFilename attachmentStream:nil failedThumbnailAttachmentPointer:failedAttachmentPointer - reactionEmoji:nil]; + reactionEmoji:nil + isGiftBadge:quotedMessage.isGiftBadge]; } + (nullable instancetype)quotedStoryReplyFromMessage:(TSMessage *)message @@ -146,7 +151,8 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:nil attachmentStream:nil failedThumbnailAttachmentPointer:nil - reactionEmoji:message.storyReactionEmoji]; + reactionEmoji:message.storyReactionEmoji + isGiftBadge:NO]; } return [self quotedReplyFromStoryMessage:storyMessage @@ -185,7 +191,8 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:nil attachmentStream:attachmentStream failedThumbnailAttachmentPointer:failedAttachmentPointer - reactionEmoji:reactionEmoji]; + reactionEmoji:reactionEmoji + isGiftBadge:NO]; } + (nullable instancetype)quotedReplyForSendingWithItem:(id)item @@ -233,7 +240,8 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:nil attachmentStream:nil failedThumbnailAttachmentPointer:nil - reactionEmoji:nil]; + reactionEmoji:nil + isGiftBadge:NO]; } if (item.contactShare) { @@ -254,7 +262,24 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:nil attachmentStream:nil failedThumbnailAttachmentPointer:nil - reactionEmoji:nil]; + reactionEmoji:nil + isGiftBadge:NO]; + } + + if (item.isGiftBadge) { + return [[self alloc] initWithTimestamp:timestamp + authorAddress:authorAddress + body:nil + bodyRanges:nil + bodySource:TSQuotedMessageContentSourceLocal + thumbnailImage:nil + thumbnailViewFactory:nil + contentType:nil + sourceFilename:nil + attachmentStream:nil + failedThumbnailAttachmentPointer:nil + reactionEmoji:nil + isGiftBadge:YES]; } if (item.stickerInfo || item.stickerAttachment || item.stickerMetadata) { @@ -344,7 +369,8 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:quotedAttachment.sourceFilename attachmentStream:quotedAttachment failedThumbnailAttachmentPointer:nil - reactionEmoji:nil]; + reactionEmoji:nil + isGiftBadge:NO]; } NSString *_Nullable quotedText; @@ -435,7 +461,8 @@ NS_ASSUME_NONNULL_BEGIN sourceFilename:quotedAttachment.sourceFilename attachmentStream:quotedAttachment failedThumbnailAttachmentPointer:nil - reactionEmoji:nil]; + reactionEmoji:nil + isGiftBadge:NO]; } #pragma mark - Instance Methods @@ -447,7 +474,8 @@ NS_ASSUME_NONNULL_BEGIN authorAddress:self.authorAddress body:self.body bodyRanges:self.bodyRanges - quotedAttachmentForSending:self.attachmentStream]; + quotedAttachmentForSending:self.attachmentStream + isGiftBadge:self.isGiftBadge]; } - (BOOL)isRemotelySourced