Add support for replying to gift messages
This commit is contained in:
parent
ea0986b676
commit
369118b045
12
Signal/Images.xcassets/gift-thumbnail.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/gift-thumbnail.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gift-thumbnail.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/gift-thumbnail.imageset/gift-thumbnail.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/gift-thumbnail.imageset/gift-thumbnail.pdf
vendored
Normal file
Binary file not shown.
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 %@";
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user