Toast view when tapped message doesn't exist, mark remotely sourced.

pull/1/head
Michael Kirk 7 years ago
parent 9ab447a3db
commit 8829cdfb4b

@ -436,6 +436,7 @@
4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; }; 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; };
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C63CBFF210A620B003AE45C /* SignalTSan.supp */; }; 4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C63CBFF210A620B003AE45C /* SignalTSan.supp */; };
4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */; }; 4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */; };
4CA5F793211E1F06008C2708 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5F792211E1F06008C2708 /* Toast.swift */; };
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; }; 4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; };
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; }; 4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */; }; 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */; };
@ -1118,6 +1119,7 @@
4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = "<group>"; }; 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = "<group>"; };
4C63CBFF210A620B003AE45C /* SignalTSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalTSan.supp; sourceTree = "<group>"; }; 4C63CBFF210A620B003AE45C /* SignalTSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalTSan.supp; sourceTree = "<group>"; };
4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalUBSan.supp; sourceTree = "<group>"; }; 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalUBSan.supp; sourceTree = "<group>"; };
4CA5F792211E1F06008C2708 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; }; 4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = "<group>"; }; 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = "<group>"; };
4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
@ -2229,6 +2231,7 @@
450D19111F85236600970622 /* RemoteVideoView.h */, 450D19111F85236600970622 /* RemoteVideoView.h */,
450D19121F85236600970622 /* RemoteVideoView.m */, 450D19121F85236600970622 /* RemoteVideoView.m */,
4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */, 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */,
4CA5F792211E1F06008C2708 /* Toast.swift */,
); );
name = Views; name = Views;
path = views; path = views;
@ -3390,6 +3393,7 @@
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */, 34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */,
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */, 458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
45DDA6242090CEB500DE97F8 /* ConversationHeaderView.swift in Sources */, 45DDA6242090CEB500DE97F8 /* ConversationHeaderView.swift in Sources */,
4CA5F793211E1F06008C2708 /* Toast.swift in Sources */,
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */, 45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */, 34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */, 457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

@ -13,10 +13,13 @@
#import <SignalMessaging/UIView+OWS.h> #import <SignalMessaging/UIView+OWS.h>
#import <SignalServiceKit/TSAttachmentStream.h> #import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSMessage.h> #import <SignalServiceKit/TSMessage.h>
#import <SignalServiceKit/TSQuotedMessage.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
const CGFloat kRemotelySourcedContentGlyphLength = 16;
const CGFloat kRemotelySourcedContentRowMargin = 4;
const CGFloat kRemotelySourcedContentRowSpacing = 3;
@interface OWSQuotedMessageView () @interface OWSQuotedMessageView ()
@property (nonatomic, readonly) OWSQuotedReplyModel *quotedMessage; @property (nonatomic, readonly) OWSQuotedReplyModel *quotedMessage;
@ -29,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) UILabel *quotedAuthorLabel; @property (nonatomic, readonly) UILabel *quotedAuthorLabel;
@property (nonatomic, readonly) UILabel *quotedTextLabel; @property (nonatomic, readonly) UILabel *quotedTextLabel;
@property (nonatomic, readonly) UILabel *quoteContentSourceLabel;
@end @end
@ -97,6 +101,7 @@ NS_ASSUME_NONNULL_BEGIN
_quotedAuthorLabel = [UILabel new]; _quotedAuthorLabel = [UILabel new];
_quotedTextLabel = [UILabel new]; _quotedTextLabel = [UILabel new];
_quoteContentSourceLabel = [UILabel new];
return self; return self;
} }
@ -143,6 +148,11 @@ NS_ASSUME_NONNULL_BEGIN
return 4.f; return 4.f;
} }
- (UIColor *)quoteBubbleBackgroundColor
{
return [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:!self.isOutgoing];
}
- (void)createContents - (void)createContents
{ {
// Ensure only called once. // Ensure only called once.
@ -179,7 +189,7 @@ NS_ASSUME_NONNULL_BEGIN
maskLayer.path = bezierPath.CGPath; maskLayer.path = bezierPath.CGPath;
}]; }];
innerBubbleView.layer.mask = maskLayer; innerBubbleView.layer.mask = maskLayer;
innerBubbleView.backgroundColor = [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:!self.isOutgoing]; innerBubbleView.backgroundColor = self.quoteBubbleBackgroundColor;
[self addSubview:innerBubbleView]; [self addSubview:innerBubbleView];
[innerBubbleView autoPinLeadingToSuperviewMarginWithInset:self.bubbleHMargin]; [innerBubbleView autoPinLeadingToSuperviewMarginWithInset:self.bubbleHMargin];
[innerBubbleView autoPinTrailingToSuperviewMarginWithInset:self.bubbleHMargin]; [innerBubbleView autoPinTrailingToSuperviewMarginWithInset:self.bubbleHMargin];
@ -189,8 +199,6 @@ NS_ASSUME_NONNULL_BEGIN
UIStackView *hStackView = [UIStackView new]; UIStackView *hStackView = [UIStackView new];
hStackView.axis = UILayoutConstraintAxisHorizontal; hStackView.axis = UILayoutConstraintAxisHorizontal;
hStackView.spacing = self.hSpacing; hStackView.spacing = self.hSpacing;
[innerBubbleView addSubview:hStackView];
[hStackView ows_autoPinToSuperviewEdges];
UIView *stripeView = [UIView new]; UIView *stripeView = [UIView new];
stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:!self.isOutgoing]; stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:!self.isOutgoing];
@ -278,6 +286,48 @@ NS_ASSUME_NONNULL_BEGIN
[emptyView setContentHuggingHigh]; [emptyView setContentHuggingHigh];
[emptyView autoSetDimension:ALDimensionWidth toSize:0.f]; [emptyView autoSetDimension:ALDimensionWidth toSize:0.f];
} }
UIStackView *quoteSourceWrapper = [[UIStackView alloc] initWithArrangedSubviews:@[ hStackView ]];
quoteSourceWrapper.axis = UILayoutConstraintAxisVertical;
if (self.quotedMessage.isRemotelySourced) {
[quoteSourceWrapper addArrangedSubview:[self buildRemoteContentSourceView]];
}
[innerBubbleView addSubview:quoteSourceWrapper];
[quoteSourceWrapper ows_autoPinToSuperviewEdges];
}
- (UIView *)buildRemoteContentSourceView
{
UIImage *glyphImage =
[[UIImage imageNamed:@"ic_broken_link"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
OWSAssert(glyphImage);
OWSAssert(CGSizeEqualToSize(
CGSizeMake(kRemotelySourcedContentGlyphLength, kRemotelySourcedContentGlyphLength), glyphImage.size));
UIImageView *glyphView = [[UIImageView alloc] initWithImage:glyphImage];
glyphView.tintColor = Theme.secondaryColor;
[glyphView
autoSetDimensionsToSize:CGSizeMake(kRemotelySourcedContentGlyphLength, kRemotelySourcedContentGlyphLength)];
UILabel *label = [self configureQuoteContentSourceLabel];
UIStackView *sourceRow = [[UIStackView alloc] initWithArrangedSubviews:@[ glyphView, label ]];
sourceRow.axis = UILayoutConstraintAxisHorizontal;
sourceRow.alignment = UIStackViewAlignmentCenter;
// TODO verify spacing w/ design
sourceRow.spacing = kRemotelySourcedContentRowSpacing;
sourceRow.layoutMarginsRelativeArrangement = YES;
const CGFloat leftMargin = 8;
sourceRow.layoutMargins = UIEdgeInsetsMake(kRemotelySourcedContentRowMargin,
leftMargin,
kRemotelySourcedContentRowMargin,
kRemotelySourcedContentRowMargin);
UIColor *backgroundColor = [UIColor.whiteColor colorWithAlphaComponent:0.4];
[sourceRow addBackgroundViewWithBackgroundColor:backgroundColor];
return sourceRow;
} }
- (void)didTapFailedThumbnailDownload:(UITapGestureRecognizer *)gestureRecognizer - (void)didTapFailedThumbnailDownload:(UITapGestureRecognizer *)gestureRecognizer
@ -367,6 +417,20 @@ NS_ASSUME_NONNULL_BEGIN
return self.quotedTextLabel; return self.quotedTextLabel;
} }
- (UILabel *)configureQuoteContentSourceLabel
{
OWSAssert(self.quoteContentSourceLabel);
self.quoteContentSourceLabel.font = UIFont.ows_dynamicTypeFootnoteFont;
self.quoteContentSourceLabel.textColor = Theme.primaryColor;
self.quoteContentSourceLabel.text = NSLocalizedString(@"QUOTED_REPLY_CONTENT_FROM_REMOTE_SOURCE",
@"Footer label that appears below quoted messages when the quoted content was note derived locally. When the "
@"local user doesn't have a copy of the message being quoted, e.g. if it had since been deleted, we instead "
@"show the content specified by the sender.");
return self.quoteContentSourceLabel;
}
- (nullable NSString *)fileTypeForSnippet - (nullable NSString *)fileTypeForSnippet
{ {
// TODO: Are we going to use the filename? For all mimetypes? // TODO: Are we going to use the filename? For all mimetypes?
@ -487,6 +551,16 @@ NS_ASSUME_NONNULL_BEGIN
textHeight += textSize.height; textHeight += textSize.height;
} }
if (self.quotedMessage.isRemotelySourced) {
UILabel *quoteContentSourceLabel = [self configureQuoteContentSourceLabel];
CGSize textSize = CGSizeCeil([quoteContentSourceLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
CGFloat sourceStackViewHeight = MAX(kRemotelySourcedContentGlyphLength, textSize.height);
textWidth
= MAX(textWidth, textSize.width + kRemotelySourcedContentGlyphLength + kRemotelySourcedContentRowSpacing);
result.height += kRemotelySourcedContentRowMargin * 2 + sourceStackViewHeight;
}
textWidth = MIN(textWidth, maxTextWidth); textWidth = MIN(textWidth, maxTextWidth);
result.width += textWidth; result.width += textWidth;
result.height += MAX(textHeight, thumbnailHeight); result.height += MAX(textHeight, thumbnailHeight);

@ -2433,7 +2433,7 @@ typedef enum : NSUInteger {
}]; }];
if (!quotedInteraction || !groupIndex) { if (!quotedInteraction || !groupIndex) {
DDLogError(@"%@ Couldn't find message quoted in quoted reply.", self.logTag); [self presentMissingQuotedReplyToast];
return; return;
} }
@ -2534,7 +2534,8 @@ typedef enum : NSUInteger {
__block OWSQuotedReplyModel *quotedReply; __block OWSQuotedReplyModel *quotedReply;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
quotedReply = [OWSQuotedReplyModel quotedReplyForConversationViewItem:conversationItem transaction:transaction]; quotedReply = [OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:conversationItem
transaction:transaction];
}]; }];
if (![quotedReply isKindOfClass:[OWSQuotedReplyModel class]]) { if (![quotedReply isKindOfClass:[OWSQuotedReplyModel class]]) {
@ -5292,6 +5293,23 @@ typedef enum : NSUInteger {
[self dismissViewControllerAnimated:YES completion:nil]; [self dismissViewControllerAnimated:YES completion:nil];
} }
#pragma mark - Toast
- (void)presentMissingQuotedReplyToast
{
DDLogInfo(@"%@ in %s", self.logTag, __PRETTY_FUNCTION__);
NSString *toastText = NSLocalizedString(@"QUOTED_REPLY_MISSING_ORIGINAL_MESSAGE",
@"Toast alert text shown when tapping on a quoted message which we cannot scroll to, because the local copy of "
@"the message doesn't exist.");
ToastController *toastController = [[ToastController alloc] initWithText:toastText];
CGFloat bottomInset = 10 + self.collectionView.contentInset.bottom + self.view.layoutMargins.bottom;
[toastController presentToastViewFromBottomOfView:self.view inset:bottomInset];
}
#pragma mark - #pragma mark -
- (void)presentViewController:(UIViewController *)viewController - (void)presentViewController:(UIViewController *)viewController

@ -539,7 +539,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (message.quotedMessage) { if (message.quotedMessage) {
self.quotedReply = self.quotedReply =
[[OWSQuotedReplyModel alloc] initWithQuotedMessage:message.quotedMessage transaction:transaction]; [OWSQuotedReplyModel quotedReplyWithQuotedMessage:message.quotedMessage transaction:transaction];
if (self.quotedReply.body.length > 0) { if (self.quotedReply.body.length > 0) {
self.displayableQuotedText = self.displayableQuotedText =

@ -1995,7 +1995,9 @@ NS_ASSUME_NONNULL_BEGIN
isGroupThread:thread.isGroupThread isGroupThread:thread.isGroupThread
transaction:transaction transaction:transaction
conversationStyle:conversationStyle]; conversationStyle:conversationStyle];
quotedMessage = [[OWSQuotedReplyModel quotedReplyForConversationViewItem:viewItem transaction:transaction] buildQuotedMessage]; quotedMessage = [
[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem transaction:transaction]
buildQuotedMessageForSending];
} else { } else {
TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread TSOutgoingMessage *_Nullable messageToQuote = [self createFakeOutgoingMessage:thread
messageBody:quotedMessageBodyWIndex messageBody:quotedMessageBodyWIndex
@ -2012,7 +2014,9 @@ NS_ASSUME_NONNULL_BEGIN
isGroupThread:thread.isGroupThread isGroupThread:thread.isGroupThread
transaction:transaction transaction:transaction
conversationStyle:conversationStyle]; conversationStyle:conversationStyle];
quotedMessage = [[OWSQuotedReplyModel quotedReplyForConversationViewItem:viewItem transaction:transaction] buildQuotedMessage]; quotedMessage = [
[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem transaction:transaction]
buildQuotedMessageForSending];
} }
OWSAssert(quotedMessage); OWSAssert(quotedMessage);

@ -0,0 +1,142 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
protocol ToastViewDelegate: class {
func didTapToastView(_ toastView: ToastView)
func didSwipeToastView(_ toastView: ToastView)
}
class ToastView: UIView {
var text: String? {
get {
return label.text
}
set {
label.text = newValue
}
}
weak var delegate: ToastViewDelegate?
private let label: UILabel
// MARK: Initializers
override init(frame: CGRect) {
label = UILabel()
super.init(frame: frame)
self.layer.cornerRadius = 4
self.backgroundColor = Theme.toastBackgroundColor
self.layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
label.textAlignment = .center
label.textColor = Theme.toastForegroundColor
label.font = UIFont.ows_dynamicTypeBody
label.numberOfLines = 0
self.addSubview(label)
label.autoPinEdgesToSuperviewMargins()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(gesture:)))
self.addGestureRecognizer(tapGesture)
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didSwipe(gesture:)))
self.addGestureRecognizer(swipeGesture)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Gestures
@objc
func didTap(gesture: UITapGestureRecognizer) {
self.delegate?.didTapToastView(self)
}
@objc
func didSwipe(gesture: UISwipeGestureRecognizer) {
self.delegate?.didSwipeToastView(self)
}
}
@objc
class ToastController: NSObject, ToastViewDelegate {
private let toastView: ToastView
private var isDismissing: Bool
// MARK: Initializers
@objc
required init(text: String) {
toastView = ToastView()
toastView.text = text
isDismissing = false
super.init()
toastView.delegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Public
@objc
func presentToastView(fromBottomOfView view: UIView, inset: CGFloat) {
Logger.debug("\(logTag) in \(#function)")
toastView.alpha = 0
view.addSubview(toastView)
toastView.setCompressionResistanceHigh()
toastView.autoPinEdge(.bottom, to: .bottom, of: view, withOffset: -inset)
toastView.autoPinWidthToSuperview(withMargin: 24)
UIView.animate(withDuration: 0.1) {
self.toastView.alpha = 1
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5) {
// intentional strong reference to self.
// As with an AlertController, the caller likely expects toast to
// be presented and dismissed without maintaining a strong reference to ToastController
self.dismissToastView()
}
}
// MARK: ToastViewDelegate
func didTapToastView(_ toastView: ToastView) {
Logger.debug("\(logTag) in \(#function)")
self.dismissToastView()
}
func didSwipeToastView(_ toastView: ToastView) {
Logger.debug("\(logTag) in \(#function)")
self.dismissToastView()
}
// MARK: Internal
func dismissToastView() {
Logger.debug("\(logTag) in \(#function)")
guard !isDismissing else {
return
}
isDismissing = true
UIView.animate(withDuration: 0.1,
animations: {
self.toastView.alpha = 0
},
completion: { (_) in
self.toastView.removeFromSuperview()
})
}
}

@ -1607,6 +1607,12 @@
/* message header label when quoting yourself */ /* message header label when quoting yourself */
"QUOTED_REPLY_AUTHOR_INDICATOR_YOURSELF" = "Replying to Yourself"; "QUOTED_REPLY_AUTHOR_INDICATOR_YOURSELF" = "Replying to Yourself";
/* Footer label that appears below quoted messages when the quoted content was note derived locally. When the local user doesn't have a copy of the message being quoted, e.g. if it had since been deleted, we instead show the content specified by the sender. */
"QUOTED_REPLY_CONTENT_FROM_REMOTE_SOURCE" = "Original message not found.";
/* Toast alert text shown when tapping on a quoted message which we cannot scroll to, because the local copy of the message doesn't exist. */
"QUOTED_REPLY_MISSING_ORIGINAL_MESSAGE" = "Original message not found.";
/* Indicates this message is a quoted reply to an attachment of unknown type. */ /* Indicates this message is a quoted reply to an attachment of unknown type. */
"QUOTED_REPLY_TYPE_ATTACHMENT" = "Attachment"; "QUOTED_REPLY_TYPE_ATTACHMENT" = "Attachment";

@ -2,15 +2,16 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
#import <SignalServiceKit/TSQuotedMessage.h>
NS_ASSUME_NONNULL_BEGIN
@class ConversationViewItem; @class ConversationViewItem;
@class TSAttachmentPointer; @class TSAttachmentPointer;
@class TSAttachmentStream; @class TSAttachmentStream;
@class TSMessage; @class TSMessage;
@class TSQuotedMessage;
@class YapDatabaseReadTransaction; @class YapDatabaseReadTransaction;
NS_ASSUME_NONNULL_BEGIN
// View model which has already fetched any attachments. // View model which has already fetched any attachments.
@interface OWSQuotedReplyModel : NSObject @interface OWSQuotedReplyModel : NSObject
@ -23,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
// This property should be set IFF we are quoting a text message // This property should be set IFF we are quoting a text message
// or attachment with caption. // or attachment with caption.
@property (nullable, nonatomic, readonly) NSString *body; @property (nullable, nonatomic, readonly) NSString *body;
@property (nonatomic, readonly) BOOL isRemotelySourced;
#pragma mark - Attachments #pragma mark - Attachments
@ -33,27 +35,17 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly, nullable) NSString *sourceFilename; @property (nonatomic, readonly, nullable) NSString *sourceFilename;
@property (nonatomic, readonly, nullable) UIImage *thumbnailImage; @property (nonatomic, readonly, nullable) UIImage *thumbnailImage;
// Convenience initializer for building an outgoing quoted reply preview, before it's sent - (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId
body:(NSString *_Nullable)body
attachmentStream:(nullable TSAttachmentStream *)attachment; //TODO quotedAttachmentStream?
// Convenience initializer for building an outgoing quoted reply preview, before it's sent
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId
body:(NSString *_Nullable)body
thumbnailImage:(nullable UIImage *)thumbnailImage;
// Used for persisted quoted replies, both incoming and outgoing. // Used for persisted quoted replies, both incoming and outgoing.
- (instancetype)initWithQuotedMessage:(TSQuotedMessage *)quotedMessage + (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
transaction:(YapDatabaseReadTransaction *)transaction; transaction:(YapDatabaseReadTransaction *)transaction;
// Builds a not-yet-sent QuotedReplyModel // Builds a not-yet-sent QuotedReplyModel
+ (nullable instancetype)quotedReplyForConversationViewItem:(ConversationViewItem *)conversationItem + (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(ConversationViewItem *)conversationItem
transaction:(YapDatabaseReadTransaction *)transaction; transaction:(YapDatabaseReadTransaction *)transaction;
- (TSQuotedMessage *)buildQuotedMessage; - (TSQuotedMessage *)buildQuotedMessageForSending;
@end @end

@ -16,43 +16,64 @@
#import <SignalServiceKit/TSQuotedMessage.h> #import <SignalServiceKit/TSQuotedMessage.h>
#import <SignalServiceKit/TSThread.h> #import <SignalServiceKit/TSThread.h>
// View Model which has already fetched any thumbnail attachment. NS_ASSUME_NONNULL_BEGIN
@implementation OWSQuotedReplyModel
@interface OWSQuotedReplyModel ()
@property (nonatomic, readonly) TSQuotedMessageContentSource bodySource;
- (instancetype)initWithTimestamp:(uint64_t)timestamp - (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId authorId:(NSString *)authorId
body:(NSString *_Nullable)body body:(nullable NSString *)body
bodySource:(TSQuotedMessageContentSource)bodySource
thumbnailImage:(nullable UIImage *)thumbnailImage
contentType:(nullable NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream attachmentStream:(nullable TSAttachmentStream *)attachmentStream
{ thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
return [self initWithTimestamp:timestamp thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed NS_DESIGNATED_INITIALIZER;
authorId:authorId
body:body @end
thumbnailImage:attachmentStream.thumbnailImage
contentType:attachmentStream.contentType // View Model which has already fetched any thumbnail attachment.
sourceFilename:attachmentStream.sourceFilename @implementation OWSQuotedReplyModel
attachmentStream:attachmentStream
thumbnailAttachmentPointer:nil #pragma mark - Initializers
thumbnailDownloadFailed:NO];
}
- (instancetype)initWithTimestamp:(uint64_t)timestamp - (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId authorId:(NSString *)authorId
body:(NSString *_Nullable)body body:(nullable NSString *)body
thumbnailImage:(nullable UIImage *)thumbnailImage; bodySource:(TSQuotedMessageContentSource)bodySource
thumbnailImage:(nullable UIImage *)thumbnailImage
contentType:(nullable NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed
{ {
return [self initWithTimestamp:timestamp self = [super init];
authorId:authorId if (!self) {
body:body return self;
thumbnailImage:thumbnailImage }
contentType:nil
sourceFilename:nil _timestamp = timestamp;
attachmentStream:nil _authorId = authorId;
thumbnailAttachmentPointer:nil _body = body;
thumbnailDownloadFailed:NO]; _bodySource = bodySource;
_thumbnailImage = thumbnailImage;
_contentType = contentType;
_sourceFilename = sourceFilename;
_attachmentStream = attachmentStream;
_thumbnailAttachmentPointer = thumbnailAttachmentPointer;
_thumbnailDownloadFailed = thumbnailDownloadFailed;
return self;
} }
- (instancetype)initWithQuotedMessage:(TSQuotedMessage *)quotedMessage #pragma mark - Factory Methods
transaction:(YapDatabaseReadTransaction *)transaction
+ (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
transaction:(YapDatabaseReadTransaction *)transaction
{ {
OWSAssert(quotedMessage.quotedAttachments.count <= 1); OWSAssert(quotedMessage.quotedAttachments.count <= 1);
OWSAttachmentInfo *attachmentInfo = quotedMessage.quotedAttachments.firstObject; OWSAttachmentInfo *attachmentInfo = quotedMessage.quotedAttachments.firstObject;
@ -82,57 +103,20 @@
} }
} }
return [self initWithTimestamp:quotedMessage.timestamp return [[self alloc] initWithTimestamp:quotedMessage.timestamp
authorId:quotedMessage.authorId authorId:quotedMessage.authorId
body:quotedMessage.body body:quotedMessage.body
thumbnailImage:thumbnailImage bodySource:quotedMessage.bodySource
contentType:attachmentInfo.contentType thumbnailImage:thumbnailImage
sourceFilename:attachmentInfo.sourceFilename contentType:attachmentInfo.contentType
attachmentStream:nil sourceFilename:attachmentInfo.sourceFilename
thumbnailAttachmentPointer:attachmentPointer attachmentStream:nil
thumbnailDownloadFailed:thumbnailDownloadFailed]; thumbnailAttachmentPointer:attachmentPointer
} thumbnailDownloadFailed:thumbnailDownloadFailed];
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId
body:(nullable NSString *)body
thumbnailImage:(nullable UIImage *)thumbnailImage
contentType:(nullable NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
attachmentStream:(nullable TSAttachmentStream *)attachmentStream
thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed
{
self = [super init];
if (!self) {
return self;
}
_timestamp = timestamp;
_authorId = authorId;
_body = body;
_thumbnailImage = thumbnailImage;
_contentType = contentType;
_sourceFilename = sourceFilename;
_attachmentStream = attachmentStream;
_thumbnailAttachmentPointer = thumbnailAttachmentPointer;
_thumbnailDownloadFailed = thumbnailDownloadFailed;
return self;
}
- (TSQuotedMessage *)buildQuotedMessage
{
NSArray *attachments = self.attachmentStream ? @[ self.attachmentStream ] : @[];
return [[TSQuotedMessage alloc] initWithTimestamp:self.timestamp
authorId:self.authorId
body:self.body
quotedAttachmentsForSending:attachments];
} }
+ (nullable instancetype)quotedReplyForConversationViewItem:(ConversationViewItem *)conversationItem + (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(ConversationViewItem *)conversationItem
transaction:(YapDatabaseReadTransaction *)transaction; transaction:(YapDatabaseReadTransaction *)transaction;
{ {
OWSAssert(conversationItem); OWSAssert(conversationItem);
OWSAssert(transaction); OWSAssert(transaction);
@ -167,11 +151,16 @@
// because the QuotedReplyViewModel has some hardcoded assumptions that only quoted attachments have // because the QuotedReplyViewModel has some hardcoded assumptions that only quoted attachments have
// thumbnails. Until we address that we want to be consistent about neither showing nor sending the // thumbnails. Until we address that we want to be consistent about neither showing nor sending the
// contactShare avatar in the quoted reply. // contactShare avatar in the quoted reply.
return [[OWSQuotedReplyModel alloc] initWithTimestamp:timestamp return [[self alloc] initWithTimestamp:timestamp
authorId:authorId authorId:authorId
body:[@"👤 " stringByAppendingString:contactShare.displayName] body:[@"👤 " stringByAppendingString:contactShare.displayName]
thumbnailImage:nil]; bodySource:TSQuotedMessageContentSourceLocal
thumbnailImage:nil
contentType:nil
sourceFilename:nil
attachmentStream:nil
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
} }
NSString *_Nullable quotedText = message.body; NSString *_Nullable quotedText = message.body;
@ -234,11 +223,35 @@
hasText = YES; hasText = YES;
} }
return [[OWSQuotedReplyModel alloc] initWithTimestamp:timestamp return [[self alloc] initWithTimestamp:timestamp
authorId:authorId authorId:authorId
body:quotedText body:quotedText
attachmentStream:quotedAttachment]; bodySource:TSQuotedMessageContentSourceLocal
thumbnailImage:quotedAttachment.thumbnailImage
contentType:quotedAttachment.contentType
sourceFilename:quotedAttachment.sourceFilename
attachmentStream:quotedAttachment
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
} }
#pragma mark - Instance Methods
- (TSQuotedMessage *)buildQuotedMessageForSending
{
NSArray *attachments = self.attachmentStream ? @[ self.attachmentStream ] : @[];
return [[TSQuotedMessage alloc] initWithTimestamp:self.timestamp
authorId:self.authorId
body:self.body
quotedAttachmentsForSending:attachments];
}
- (BOOL)isRemotelySourced
{
return self.bodySource == TSQuotedMessageContentSourceRemote;
}
@end @end
NS_ASSUME_NONNULL_END

@ -49,6 +49,11 @@ extern NSString *const ThemeDidChangeNotification;
@property (class, readonly, nonatomic) UIColor *searchBarBackgroundColor; @property (class, readonly, nonatomic) UIColor *searchBarBackgroundColor;
@property (class, readonly, nonatomic) UIBlurEffect *barBlurEffect; @property (class, readonly, nonatomic) UIBlurEffect *barBlurEffect;
#pragma mark -
@property (class, readonly, nonatomic) UIColor *toastForegroundColor;
@property (class, readonly, nonatomic) UIColor *toastBackgroundColor;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -154,6 +154,18 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
return Theme.backgroundColor; return Theme.backgroundColor;
} }
#pragma mark -
+ (UIColor *)toastForegroundColor
{
return (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_whiteColor);
}
+ (UIColor *)toastBackgroundColor
{
return (Theme.isDarkThemeEnabled ? UIColor.ows_dark60Color : UIColor.ows_light60Color);
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

@ -84,11 +84,12 @@ NS_ASSUME_NONNULL_BEGIN
OWSDisappearingMessagesConfiguration *configuration = OWSDisappearingMessagesConfiguration *configuration =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId]; [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId];
uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0); uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0);
TSOutgoingMessage *message = [TSOutgoingMessage outgoingMessageInThread:thread TSOutgoingMessage *message =
messageBody:text [TSOutgoingMessage outgoingMessageInThread:thread
attachmentId:nil messageBody:text
expiresInSeconds:expiresInSeconds attachmentId:nil
quotedMessage:[quotedReplyModel buildQuotedMessage]]; expiresInSeconds:expiresInSeconds
quotedMessage:[quotedReplyModel buildQuotedMessageForSending]];
[messageSender enqueueMessage:message success:successHandler failure:failureHandler]; [messageSender enqueueMessage:message success:successHandler failure:failureHandler];
@ -136,7 +137,7 @@ NS_ASSUME_NONNULL_BEGIN
expireStartedAt:0 expireStartedAt:0
isVoiceMessage:[attachment isVoiceMessage] isVoiceMessage:[attachment isVoiceMessage]
groupMetaMessage:TSGroupMessageUnspecified groupMetaMessage:TSGroupMessageUnspecified
quotedMessage:[quotedReplyModel buildQuotedMessage] quotedMessage:[quotedReplyModel buildQuotedMessageForSending]
contactShare:nil]; contactShare:nil];
[messageSender enqueueAttachment:attachment.dataSource [messageSender enqueueAttachment:attachment.dataSource

@ -44,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable TSAttachment *)attachmentWithTransaction:(YapDatabaseReadTransaction *)transaction; - (nullable TSAttachment *)attachmentWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream; - (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream;
- (nullable NSString *)bodyTextWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction; - (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction;

@ -226,6 +226,32 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
} }
} }
- (nullable NSString *)bodyTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{
if (self.hasAttachments) {
TSAttachment *_Nullable attachment = [self attachmentWithTransaction:transaction];
if ([OWSMimeTypeOversizeTextMessage isEqualToString:attachment.contentType] &&
[attachment isKindOfClass:TSAttachmentStream.class]) {
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.filePath];
if (data) {
NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text) {
return text.filterStringForDisplay;
}
}
}
}
if (self.body.length > 0) {
return self.body.filterStringForDisplay;
}
return nil;
}
// TODO: This method contains view-specific logic and probably belongs in NotificationsManager, not in SSK. // TODO: This method contains view-specific logic and probably belongs in NotificationsManager, not in SSK.
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction - (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{ {

@ -41,10 +41,17 @@ NS_ASSUME_NONNULL_BEGIN
@end @end
typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) {
TSQuotedMessageContentSourceUnknown,
TSQuotedMessageContentSourceLocal,
TSQuotedMessageContentSourceRemote
};
@interface TSQuotedMessage : MTLModel @interface TSQuotedMessage : MTLModel
@property (nonatomic, readonly) uint64_t timestamp; @property (nonatomic, readonly) uint64_t timestamp;
@property (nonatomic, readonly) NSString *authorId; @property (nonatomic, readonly) NSString *authorId;
@property (nonatomic, readonly) TSQuotedMessageContentSource bodySource;
// This property should be set IFF we are quoting a text message // This property should be set IFF we are quoting a text message
// or attachment with caption. // or attachment with caption.
@ -80,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithTimestamp:(uint64_t)timestamp - (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId authorId:(NSString *)authorId
body:(NSString *_Nullable)body body:(NSString *_Nullable)body
bodySource:(TSQuotedMessageContentSource)bodySource
receivedQuotedAttachmentInfos:(NSArray<OWSAttachmentInfo *> *)attachmentInfos; receivedQuotedAttachmentInfos:(NSArray<OWSAttachmentInfo *> *)attachmentInfos;
// used when sending quoted messages // used when sending quoted messages

@ -59,6 +59,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithTimestamp:(uint64_t)timestamp - (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId authorId:(NSString *)authorId
body:(NSString *_Nullable)body body:(NSString *_Nullable)body
bodySource:(TSQuotedMessageContentSource)bodySource
receivedQuotedAttachmentInfos:(NSArray<OWSAttachmentInfo *> *)attachmentInfos receivedQuotedAttachmentInfos:(NSArray<OWSAttachmentInfo *> *)attachmentInfos
{ {
OWSAssert(timestamp > 0); OWSAssert(timestamp > 0);
@ -72,6 +73,7 @@ NS_ASSUME_NONNULL_BEGIN
_timestamp = timestamp; _timestamp = timestamp;
_authorId = authorId; _authorId = authorId;
_body = body; _body = body;
_bodySource = bodySource;
_quotedAttachments = attachmentInfos; _quotedAttachments = attachmentInfos;
return self; return self;
@ -93,7 +95,8 @@ NS_ASSUME_NONNULL_BEGIN
_timestamp = timestamp; _timestamp = timestamp;
_authorId = authorId; _authorId = authorId;
_body = body; _body = body;
_bodySource = TSQuotedMessageContentSourceLocal;
NSMutableArray *attachmentInfos = [NSMutableArray new]; NSMutableArray *attachmentInfos = [NSMutableArray new];
for (TSAttachmentStream *attachmentStream in attachments) { for (TSAttachmentStream *attachmentStream in attachments) {
[attachmentInfos addObject:[[OWSAttachmentInfo alloc] initWithAttachmentStream:attachmentStream]]; [attachmentInfos addObject:[[OWSAttachmentInfo alloc] initWithAttachmentStream:attachmentStream]];
@ -129,13 +132,32 @@ NS_ASSUME_NONNULL_BEGIN
NSString *authorId = [quoteProto author]; NSString *authorId = [quoteProto author];
NSString *_Nullable body = nil; NSString *_Nullable body = nil;
BOOL hasText = NO;
BOOL hasAttachment = NO; BOOL hasAttachment = NO;
if ([quoteProto hasText] && [quoteProto text].length > 0) { TSQuotedMessageContentSource bodySource = TSQuotedMessageContentSourceUnknown;
body = [quoteProto text];
hasText = YES; // Prefer to generate the text snippet locally if available.
TSMessage *_Nullable localRecord = (TSMessage *)[
[TSInteraction interactionsWithTimestamp:quoteProto.id ofClass:TSMessage.class withTransaction:transaction]
firstObject];
if (localRecord) {
bodySource = TSQuotedMessageContentSourceLocal;
NSString *localText = [localRecord bodyTextWithTransaction:transaction];
if (localText.length > 0) {
body = localText;
}
} }
if (body.length == 0) {
if (quoteProto.text.length > 0) {
bodySource = TSQuotedMessageContentSourceRemote;
body = quoteProto.text;
}
}
OWSAssert(bodySource != TSQuotedMessageContentSourceUnknown);
NSMutableArray<OWSAttachmentInfo *> *attachmentInfos = [NSMutableArray new]; NSMutableArray<OWSAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
for (SSKProtoDataMessageQuoteQuotedAttachment *quotedAttachment in quoteProto.attachments) { for (SSKProtoDataMessageQuoteQuotedAttachment *quotedAttachment in quoteProto.attachments) {
hasAttachment = YES; hasAttachment = YES;
@ -180,7 +202,7 @@ NS_ASSUME_NONNULL_BEGIN
[attachmentInfos addObject:attachmentInfo]; [attachmentInfos addObject:attachmentInfo];
} }
if (!hasText && !hasAttachment) { if (body.length == 0 && !hasAttachment) {
OWSFail(@"%@ quoted message has neither text nor attachment", self.logTag); OWSFail(@"%@ quoted message has neither text nor attachment", self.logTag);
return nil; return nil;
} }
@ -188,6 +210,7 @@ NS_ASSUME_NONNULL_BEGIN
return [[TSQuotedMessage alloc] initWithTimestamp:timestamp return [[TSQuotedMessage alloc] initWithTimestamp:timestamp
authorId:authorId authorId:authorId
body:body body:body
bodySource:bodySource
receivedQuotedAttachmentInfos:attachmentInfos]; receivedQuotedAttachmentInfos:attachmentInfos];
} }

Loading…
Cancel
Save