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

This commit is contained in:
Michael Kirk 2018-08-10 10:36:07 -06:00
parent 9ab447a3db
commit 8829cdfb4b
20 changed files with 477 additions and 125 deletions

View File

@ -436,6 +436,7 @@
4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; };
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C63CBFF210A620B003AE45C /* SignalTSan.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 */; };
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.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>"; };
4C63CBFF210A620B003AE45C /* SignalTSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalTSan.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>"; };
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; };
@ -2229,6 +2231,7 @@
450D19111F85236600970622 /* RemoteVideoView.h */,
450D19121F85236600970622 /* RemoteVideoView.m */,
4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */,
4CA5F792211E1F06008C2708 /* Toast.swift */,
);
name = Views;
path = views;
@ -3390,6 +3393,7 @@
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */,
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
45DDA6242090CEB500DE97F8 /* ConversationHeaderView.swift in Sources */,
4CA5F793211E1F06008C2708 /* Toast.swift in Sources */,
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,

View File

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

View File

@ -13,10 +13,13 @@
#import <SignalMessaging/UIView+OWS.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSMessage.h>
#import <SignalServiceKit/TSQuotedMessage.h>
NS_ASSUME_NONNULL_BEGIN
const CGFloat kRemotelySourcedContentGlyphLength = 16;
const CGFloat kRemotelySourcedContentRowMargin = 4;
const CGFloat kRemotelySourcedContentRowSpacing = 3;
@interface OWSQuotedMessageView ()
@property (nonatomic, readonly) OWSQuotedReplyModel *quotedMessage;
@ -29,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) UILabel *quotedAuthorLabel;
@property (nonatomic, readonly) UILabel *quotedTextLabel;
@property (nonatomic, readonly) UILabel *quoteContentSourceLabel;
@end
@ -97,6 +101,7 @@ NS_ASSUME_NONNULL_BEGIN
_quotedAuthorLabel = [UILabel new];
_quotedTextLabel = [UILabel new];
_quoteContentSourceLabel = [UILabel new];
return self;
}
@ -143,6 +148,11 @@ NS_ASSUME_NONNULL_BEGIN
return 4.f;
}
- (UIColor *)quoteBubbleBackgroundColor
{
return [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:!self.isOutgoing];
}
- (void)createContents
{
// Ensure only called once.
@ -179,7 +189,7 @@ NS_ASSUME_NONNULL_BEGIN
maskLayer.path = bezierPath.CGPath;
}];
innerBubbleView.layer.mask = maskLayer;
innerBubbleView.backgroundColor = [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:!self.isOutgoing];
innerBubbleView.backgroundColor = self.quoteBubbleBackgroundColor;
[self addSubview:innerBubbleView];
[innerBubbleView autoPinLeadingToSuperviewMarginWithInset:self.bubbleHMargin];
[innerBubbleView autoPinTrailingToSuperviewMarginWithInset:self.bubbleHMargin];
@ -189,8 +199,6 @@ NS_ASSUME_NONNULL_BEGIN
UIStackView *hStackView = [UIStackView new];
hStackView.axis = UILayoutConstraintAxisHorizontal;
hStackView.spacing = self.hSpacing;
[innerBubbleView addSubview:hStackView];
[hStackView ows_autoPinToSuperviewEdges];
UIView *stripeView = [UIView new];
stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:!self.isOutgoing];
@ -278,6 +286,48 @@ NS_ASSUME_NONNULL_BEGIN
[emptyView setContentHuggingHigh];
[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
@ -367,6 +417,20 @@ NS_ASSUME_NONNULL_BEGIN
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
{
// TODO: Are we going to use the filename? For all mimetypes?
@ -487,6 +551,16 @@ NS_ASSUME_NONNULL_BEGIN
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);
result.width += textWidth;
result.height += MAX(textHeight, thumbnailHeight);

View File

@ -2433,7 +2433,7 @@ typedef enum : NSUInteger {
}];
if (!quotedInteraction || !groupIndex) {
DDLogError(@"%@ Couldn't find message quoted in quoted reply.", self.logTag);
[self presentMissingQuotedReplyToast];
return;
}
@ -2534,7 +2534,8 @@ typedef enum : NSUInteger {
__block OWSQuotedReplyModel *quotedReply;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
quotedReply = [OWSQuotedReplyModel quotedReplyForConversationViewItem:conversationItem transaction:transaction];
quotedReply = [OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:conversationItem
transaction:transaction];
}];
if (![quotedReply isKindOfClass:[OWSQuotedReplyModel class]]) {
@ -5292,6 +5293,23 @@ typedef enum : NSUInteger {
[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 -
- (void)presentViewController:(UIViewController *)viewController

View File

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

View File

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

View File

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

View File

@ -1607,6 +1607,12 @@
/* message header label when quoting 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. */
"QUOTED_REPLY_TYPE_ATTACHMENT" = "Attachment";

View File

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

View File

@ -16,43 +16,64 @@
#import <SignalServiceKit/TSQuotedMessage.h>
#import <SignalServiceKit/TSThread.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSQuotedReplyModel ()
@property (nonatomic, readonly) TSQuotedMessageContentSource bodySource;
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId
body:(nullable NSString *)body
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 NS_DESIGNATED_INITIALIZER;
@end
// View Model which has already fetched any thumbnail attachment.
@implementation OWSQuotedReplyModel
#pragma mark - Initializers
- (instancetype)initWithTimestamp:(uint64_t)timestamp
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
thumbnailAttachmentPointer:(nullable TSAttachmentPointer *)thumbnailAttachmentPointer
thumbnailDownloadFailed:(BOOL)thumbnailDownloadFailed
{
return [self initWithTimestamp:timestamp
authorId:authorId
body:body
thumbnailImage:attachmentStream.thumbnailImage
contentType:attachmentStream.contentType
sourceFilename:attachmentStream.sourceFilename
attachmentStream:attachmentStream
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
self = [super init];
if (!self) {
return self;
}
_timestamp = timestamp;
_authorId = authorId;
_body = body;
_bodySource = bodySource;
_thumbnailImage = thumbnailImage;
_contentType = contentType;
_sourceFilename = sourceFilename;
_attachmentStream = attachmentStream;
_thumbnailAttachmentPointer = thumbnailAttachmentPointer;
_thumbnailDownloadFailed = thumbnailDownloadFailed;
return self;
}
- (instancetype)initWithTimestamp:(uint64_t)timestamp
authorId:(NSString *)authorId
body:(NSString *_Nullable)body
thumbnailImage:(nullable UIImage *)thumbnailImage;
{
return [self initWithTimestamp:timestamp
authorId:authorId
body:body
thumbnailImage:thumbnailImage
contentType:nil
sourceFilename:nil
attachmentStream:nil
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
}
#pragma mark - Factory Methods
- (instancetype)initWithQuotedMessage:(TSQuotedMessage *)quotedMessage
transaction:(YapDatabaseReadTransaction *)transaction
+ (instancetype)quotedReplyWithQuotedMessage:(TSQuotedMessage *)quotedMessage
transaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(quotedMessage.quotedAttachments.count <= 1);
OWSAttachmentInfo *attachmentInfo = quotedMessage.quotedAttachments.firstObject;
@ -82,57 +103,20 @@
}
}
return [self initWithTimestamp:quotedMessage.timestamp
authorId:quotedMessage.authorId
body:quotedMessage.body
thumbnailImage:thumbnailImage
contentType:attachmentInfo.contentType
sourceFilename:attachmentInfo.sourceFilename
attachmentStream:nil
thumbnailAttachmentPointer:attachmentPointer
thumbnailDownloadFailed:thumbnailDownloadFailed];
return [[self alloc] initWithTimestamp:quotedMessage.timestamp
authorId:quotedMessage.authorId
body:quotedMessage.body
bodySource:quotedMessage.bodySource
thumbnailImage:thumbnailImage
contentType:attachmentInfo.contentType
sourceFilename:attachmentInfo.sourceFilename
attachmentStream:nil
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
transaction:(YapDatabaseReadTransaction *)transaction;
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(ConversationViewItem *)conversationItem
transaction:(YapDatabaseReadTransaction *)transaction;
{
OWSAssert(conversationItem);
OWSAssert(transaction);
@ -167,11 +151,16 @@
// 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
// contactShare avatar in the quoted reply.
return [[OWSQuotedReplyModel alloc] initWithTimestamp:timestamp
authorId:authorId
body:[@"👤 " stringByAppendingString:contactShare.displayName]
thumbnailImage:nil];
return [[self alloc] initWithTimestamp:timestamp
authorId:authorId
body:[@"👤 " stringByAppendingString:contactShare.displayName]
bodySource:TSQuotedMessageContentSourceLocal
thumbnailImage:nil
contentType:nil
sourceFilename:nil
attachmentStream:nil
thumbnailAttachmentPointer:nil
thumbnailDownloadFailed:NO];
}
NSString *_Nullable quotedText = message.body;
@ -234,11 +223,35 @@
hasText = YES;
}
return [[OWSQuotedReplyModel alloc] initWithTimestamp:timestamp
authorId:authorId
body:quotedText
attachmentStream:quotedAttachment];
return [[self alloc] initWithTimestamp:timestamp
authorId:authorId
body:quotedText
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
NS_ASSUME_NONNULL_END

View File

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

View File

@ -154,6 +154,18 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
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
NS_ASSUME_NONNULL_END

View File

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

View File

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

View File

@ -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.
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{

View File

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

View File

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