Add support for replying to gift messages

This commit is contained in:
Max Radermacher 2022-07-01 09:51:27 -07:00
parent ea0986b676
commit 369118b045
16 changed files with 355 additions and 31 deletions

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "gift-thumbnail.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

View File

@ -137,6 +137,12 @@ public class CVItemViewModelImpl: NSObject, CVItemViewModel {
}
return !bodyMedia.items.compactMap { $0.attachment as? TSAttachmentPointer }.isEmpty
}
public var isGiftBadge: Bool {
AssertIsOnMainThread()
return componentState.giftBadge != nil
}
}
// MARK: - Actions

View File

@ -85,8 +85,14 @@ const CGFloat kRemotelySourcedContentRowSpacing = 3;
- (BOOL)hasQuotedAttachment
{
return (self.quotedMessage.contentType.length > 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?

View File

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

View File

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

View File

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

View File

@ -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.");

View File

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

View File

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

View File

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

View File

@ -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<D: SwiftProtobuf.Decoder>(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 = [

View File

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

View File

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

View File

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

View File

@ -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<CVItemViewModel>)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