Send, receive & show read receipts to senders/from receivers.

* Send read receipts to senders.
* Honor "send read receipts" preference.
* Process read receipts from recipients.
* Refactor "mark as read" logic.
* Serialize and apply recipient read receipts received before sync transcript.
* Show recipient read receipts in conversation view.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-09-15 15:28:44 -04:00
parent 29dae9bb99
commit 11cadf4200
22 changed files with 693 additions and 109 deletions

View File

@ -80,6 +80,7 @@
34C42D611F4734CA0072EC04 /* OWSContactOffersCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D601F4734CA0072EC04 /* OWSContactOffersCell.m */; };
34C42D661F4734ED0072EC04 /* OWSContactOffersInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D631F4734ED0072EC04 /* OWSContactOffersInteraction.m */; };
34C42D671F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */; };
34CA1C251F706B5400E51C51 /* NSAttributedString+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CA1C241F706B5400E51C51 /* NSAttributedString+OWS.m */; };
34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */; };
34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */; };
34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; };
@ -526,6 +527,8 @@
34C42D631F4734ED0072EC04 /* OWSContactOffersInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactOffersInteraction.m; sourceTree = "<group>"; };
34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSUnreadIndicatorInteraction.h; sourceTree = "<group>"; };
34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSUnreadIndicatorInteraction.m; sourceTree = "<group>"; };
34CA1C231F706B5400E51C51 /* NSAttributedString+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+OWS.h"; sourceTree = "<group>"; };
34CA1C241F706B5400E51C51 /* NSAttributedString+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+OWS.m"; sourceTree = "<group>"; };
34CCAF361F0C0599004084F4 /* AppUpdateNag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppUpdateNag.h; sourceTree = "<group>"; };
34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = "<group>"; };
34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToContactViewController.h; sourceTree = "<group>"; };
@ -1415,6 +1418,8 @@
76EB04EA18170B33006006FC /* FunctionalUtil.h */,
76EB04EB18170B33006006FC /* FunctionalUtil.m */,
455AC69A1F4F79E500134004 /* ImageCache.swift */,
34CA1C231F706B5400E51C51 /* NSAttributedString+OWS.h */,
34CA1C241F706B5400E51C51 /* NSAttributedString+OWS.m */,
B62F5E0E1C2980B4000D370C /* NSData+ows_StripToken.h */,
B62F5E0F1C2980B4000D370C /* NSData+ows_StripToken.m */,
76EB04EC18170B33006006FC /* NumberUtil.h */,
@ -1436,6 +1441,7 @@
4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */,
76EB04F518170B33006006FC /* StringUtil.h */,
76EB04F618170B33006006FC /* StringUtil.m */,
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
345670FF1E89A5F1006EE662 /* ThreadUtil.h */,
345671001E89A5F1006EE662 /* ThreadUtil.m */,
FCFA64B11A24F29E0007FB87 /* UI Categories */,
@ -1446,7 +1452,6 @@
76EB04FB18170B33006006FC /* Util.h */,
45F170D51E315310003FC1F2 /* Weak.swift */,
45F170CB1E310E22003FC1F2 /* WeakTimer.swift */,
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
);
path = util;
sourceTree = "<group>";
@ -2210,6 +2215,7 @@
34CE88ED1F3237260098030F /* ProfileFetcherJob.swift in Sources */,
34B3F8791E8DF1700035BE1A /* CountryCodeViewController.m in Sources */,
4CE0E3771B954546007210CF /* TSAnimatedAdapter.m in Sources */,
34CA1C251F706B5400E51C51 /* NSAttributedString+OWS.m in Sources */,
4531C9C41DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m in Sources */,
4542F0961EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */,
4516E3FF1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */,

View File

@ -196,11 +196,11 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
NSString *lastLaunchedAppVersion = AppVersion.instance.lastAppVersion;
NSString *lastCompletedLaunchAppVersion = AppVersion.instance.lastCompletedLaunchAppVersion;
// Every time we change a database view in such a way that might cause a delay on launch,
// we need to bump this constant.
// Every time we change or add a database view in such a way that
// might cause a delay on launch, we need to bump this constant.
//
// We added a number of database views in v2.13.0.
NSString *kLastVersionWithDatabaseViewChange = @"2.13.0";
// We added a new database view in v2.17.0.
NSString *kLastVersionWithDatabaseViewChange = @"2.17.0";
BOOL mayNeedUpgrade = ([TSAccountManager isRegistered] && lastLaunchedAppVersion
&& (!lastCompletedLaunchAppVersion ||
[VersionMigrations isVersion:lastCompletedLaunchAppVersion
@ -833,7 +833,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
[[OWSProfileManager sharedManager] ensureLocalProfileCached];
// For non-legacy users, read receipts are on by default.
[[Environment preferences] setAreReadReceiptsEnabled:YES];
[OWSReadReceiptManager.sharedManager setAreReadReceiptsEnabled:YES];
}
}

View File

@ -12,6 +12,7 @@
#import "Environment.h"
#import "FingerprintViewController.h"
#import "FullImageViewController.h"
#import "NSAttributedString+OWS.h"
#import "NewGroupViewController.h"
#import "OWSAudioAttachmentPlayer.h"
#import "OWSCall.h"
@ -77,6 +78,7 @@
#import <SignalServiceKit/OWSIdentityManager.h>
#import <SignalServiceKit/OWSMessageManager.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/OWSReadReceiptManager.h>
#import <SignalServiceKit/OWSVerificationStateChangeMessage.h>
#import <SignalServiceKit/SignalRecipient.h>
#import <SignalServiceKit/TSAccountManager.h>
@ -2132,6 +2134,17 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
? NSLocalizedString(@"MESSAGE_STATUS_DELIVERED", @"message footer for delivered messages")
: NSLocalizedString(@"MESSAGE_STATUS_SENT", @"message footer for sent messages"));
NSAttributedString *result = [[NSAttributedString alloc] initWithString:text];
if (outgoingMessage.wasDelivered && outgoingMessage.readRecipientIds.count > 0) {
NSAttributedString *checkmark = [[NSAttributedString alloc]
initWithString:@"\uf00c "
attributes:@{
NSFontAttributeName : [UIFont ows_fontAwesomeFont:10.f],
NSForegroundColorAttributeName : [UIColor ows_materialBlueColor],
}];
NSAttributedString *spacing = [[NSAttributedString alloc] initWithString:@" "];
result = [[checkmark rtlSafeAppend:spacing referenceView:self.view] rtlSafeAppend:result
referenceView:self.view];
}
// Show when it's the last message in the thread
if (indexPath.item == [self.collectionView numberOfItemsInSection:indexPath.section] - 1) {
@ -4037,46 +4050,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
{
[self updateLastVisibleTimestamp];
TSThread *thread = self.thread;
uint64_t lastVisibleTimestamp = self.lastVisibleTimestamp;
[self.editingDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
NSMutableArray<id<OWSReadTracking>> *interactions = [NSMutableArray new];
[[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:thread.uniqueId
usingBlock:^(
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
OWSFail(@"Expected to conform to OWSReadTracking: object with class: %@ collection: %@ "
@"key: %@",
[object class],
collection,
key);
return;
}
id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
if (possiblyRead.timestampForSorting > lastVisibleTimestamp) {
*stop = YES;
return;
}
OWSAssert(!possiblyRead.read);
if (!possiblyRead.read) {
[interactions addObject:possiblyRead];
}
}];
if (interactions.count < 1) {
return;
}
DDLogError(@"Marking %zd messages as read.", interactions.count);
for (id<OWSReadTracking> possiblyRead in interactions) {
[possiblyRead markAsReadWithTransaction:transaction sendReadReceipt:YES updateExpiration:YES];
}
}];
[OWSReadReceiptManager.sharedManager markAsReadLocallyBeforeTimestamp:lastVisibleTimestamp thread:self.thread];
}
- (void)updateGroupModelTo:(TSGroupModel *)newGroupModel successCompletion:(void (^_Nullable)())successCompletion

View File

@ -3,10 +3,11 @@
//
#import "PrivacySettingsTableViewController.h"
#import "OWSPreferences.h"
#import "BlockListViewController.h"
#import "Environment.h"
#import "OWSPreferences.h"
#import "Signal-Swift.h"
#import <SignalServiceKit/OWSReadReceiptManager.h>
NS_ASSUME_NONNULL_BEGIN
@ -59,11 +60,12 @@ NS_ASSUME_NONNULL_BEGIN
= NSLocalizedString(@"SETTINGS_READ_RECEIPTS_SECTION_TITLE", @"Title of the 'read receipts' settings section.");
readReceiptsSection.footerTitle = NSLocalizedString(
@"SETTINGS_READ_RECEIPTS_SECTION_FOOTER", @"An explanation of the 'read receipts' setting.");
[readReceiptsSection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_READ_RECEIPT",
@"Label for the 'read receipts' setting.")
isOn:[Environment.preferences areReadReceiptsEnabled]
target:weakSelf
selector:@selector(didToggleReadReceiptsSwitch:)]];
[readReceiptsSection
addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_READ_RECEIPT",
@"Label for the 'read receipts' setting.")
isOn:[OWSReadReceiptManager.sharedManager areReadReceiptsEnabled]
target:weakSelf
selector:@selector(didToggleReadReceiptsSwitch:)]];
[contents addSection:readReceiptsSection];
// Allow calls to connect directly vs. using TURN exclusively
@ -152,7 +154,7 @@ NS_ASSUME_NONNULL_BEGIN
{
BOOL enabled = sender.isOn;
DDLogInfo(@"%@ toggled areReadReceiptsEnabled: %@", self.tag, enabled ? @"ON" : @"OFF");
[Environment.preferences setAreReadReceiptsEnabled:enabled];
[OWSReadReceiptManager.sharedManager setAreReadReceiptsEnabled:enabled];
}
- (void)didToggleCallsHideIPAddressSwitch:(UISwitch *)sender

View File

@ -52,9 +52,6 @@ extern NSString *const OWSPreferencesKeyEnableDebugLog;
- (void)setIOSUpgradeNagVersion:(NSString *)value;
- (nullable NSString *)iOSUpgradeNagVersion;
- (BOOL)areReadReceiptsEnabled;
- (void)setAreReadReceiptsEnabled:(BOOL)value;
#pragma mark - Calling
#pragma mark Callkit

View File

@ -22,7 +22,6 @@ NSString *const OWSPreferencesKeyCallKitPrivacyEnabled = @"CallKitPrivacyEnabled
NSString *const OWSPreferencesKeyCallsHideIPAddress = @"CallsHideIPAddress";
NSString *const OWSPreferencesKeyHasDeclinedNoContactsView = @"hasDeclinedNoContactsView";
NSString *const OWSPreferencesKeyIOSUpgradeNagVersion = @"iOSUpgradeNagVersion";
NSString *const OWSPreferencesKeyAreReadReceiptsEnabled = @"areReadReceiptsEnabled";
@implementation OWSPreferences
@ -145,18 +144,6 @@ NSString *const OWSPreferencesKeyAreReadReceiptsEnabled = @"areReadReceiptsEnabl
return [self tryGetValueForKey:OWSPreferencesKeyIOSUpgradeNagVersion];
}
- (BOOL)areReadReceiptsEnabled
{
NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyAreReadReceiptsEnabled];
// Default to NO.
return preference ? [preference boolValue] : NO;
}
- (void)setAreReadReceiptsEnabled:(BOOL)value
{
[self setValueForKey:OWSPreferencesKeyAreReadReceiptsEnabled toValue:@(value)];
}
#pragma mark - Calling
#pragma mark CallKit

View File

@ -0,0 +1,13 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface NSAttributedString (OWS)
- (NSAttributedString *)rtlSafeAppend:(NSAttributedString *)string referenceView:(UIView *)referenceView;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "NSAttributedString+OWS.h"
#import "UIView+OWS.h"
NS_ASSUME_NONNULL_BEGIN
@implementation NSAttributedString (OWS)
- (NSAttributedString *)rtlSafeAppend:(NSAttributedString *)string referenceView:(UIView *)referenceView
{
OWSAssert(string);
OWSAssert(referenceView);
NSMutableAttributedString *result = [NSMutableAttributedString new];
if ([referenceView isRTL]) {
[result appendAttributedString:string];
[result appendAttributedString:self];
} else {
[result appendAttributedString:self];
[result appendAttributedString:string];
}
return [result copy];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,4 +1,6 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSOutgoingSyncMessage.h"
@ -6,10 +8,10 @@ NS_ASSUME_NONNULL_BEGIN
@class OWSReadReceipt;
@interface OWSReadReceiptsMessage : OWSOutgoingSyncMessage
@interface OWSReadReceiptsForLinkedDevicesMessage : OWSOutgoingSyncMessage
- (instancetype)initWithReadReceipts:(NSArray<OWSReadReceipt *> *)readReceipts;
@end
NS_ASSUME_NONNULL_END
NS_ASSUME_NONNULL_END

View File

@ -2,19 +2,19 @@
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSReadReceiptsMessage.h"
#import "OWSReadReceiptsForLinkedDevicesMessage.h"
#import "OWSReadReceipt.h"
#import "OWSSignalServiceProtos.pb.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSReadReceiptsMessage ()
@interface OWSReadReceiptsForLinkedDevicesMessage ()
@property (nonatomic, readonly) NSArray<OWSReadReceipt *> *readReceipts;
@end
@implementation OWSReadReceiptsMessage
@implementation OWSReadReceiptsForLinkedDevicesMessage
- (instancetype)initWithReadReceipts:(NSArray<OWSReadReceipt *> *)readReceipts
{

View File

@ -0,0 +1,17 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSOutgoingSyncMessage.h"
NS_ASSUME_NONNULL_BEGIN
@class OWSReadReceipt;
@interface OWSReadReceiptsForSenderMessage : TSOutgoingMessage
- (instancetype)initWithThread:(nullable TSThread *)thread messageTimestamps:(NSArray<NSNumber *> *)messageTimestamps;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,94 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSReadReceiptsForSenderMessage.h"
#import "NSDate+millisecondTimeStamp.h"
#import "OWSReadReceipt.h"
#import "OWSSignalServiceProtos.pb.h"
#import "SignalRecipient.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSReadReceiptsForSenderMessage ()
@property (nonatomic, readonly) NSArray<NSNumber *> *messageTimestamps;
@end
@implementation OWSReadReceiptsForSenderMessage
- (instancetype)initWithThread:(nullable TSThread *)thread messageTimestamps:(NSArray<NSNumber *> *)messageTimestamps;
{
self = [super initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:thread];
if (!self) {
return self;
}
_messageTimestamps = [messageTimestamps copy];
return self;
}
#pragma mark - TSOutgoingMessage overrides
- (BOOL)shouldSyncTranscript
{
return NO;
}
- (NSData *)buildPlainTextData:(SignalRecipient *)recipient
{
OWSAssert(recipient);
OWSSignalServiceProtosContentBuilder *contentBuilder = [OWSSignalServiceProtosContentBuilder new];
[contentBuilder setReceiptMessage:[self buildReceiptMessage:recipient.recipientId]];
return [[contentBuilder build] data];
}
- (OWSSignalServiceProtosReceiptMessage *)buildReceiptMessage:(NSString *)recipientId
{
OWSSignalServiceProtosReceiptMessageBuilder *builder = [OWSSignalServiceProtosReceiptMessageBuilder new];
[builder setType:OWSSignalServiceProtosReceiptMessageTypeRead];
OWSAssert(self.messageTimestamps.count > 0);
for (NSNumber *messageTimestamp in self.messageTimestamps) {
[builder addTimestamp:[messageTimestamp unsignedLongLongValue]];
}
// TODO: addLocalProfileKeyIfNecessary.
return [builder build];
}
#pragma mark - TSYapDatabaseObject overrides
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
// override superclass with no-op.
//
// There's no need to save this message, since it's not displayed to the user.
//
// Should we find a need to save this in the future, we need to exclude any non-serializable properties.
}
- (NSString *)debugDescription
{
return [NSString stringWithFormat:@"%@ with message timestamps: %zd", self.tag, self.messageTimestamps.count];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -6,16 +6,23 @@ NS_ASSUME_NONNULL_BEGIN
@class OWSIncomingSentMessageTranscript;
@class OWSMessageSender;
@class TSNetworkManager;
@class OWSReadReceiptManager;
@class TSAttachmentStream;
@class TSNetworkManager;
@class TSStorageManager;
@class YapDatabaseReadWriteTransaction;
// This job is used to process "outgoing message" notifications from linked devices.
@interface OWSRecordTranscriptJob : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript;
- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript
messageSender:(OWSMessageSender *)messageSender
networkManager:(TSNetworkManager *)networkManager NS_DESIGNATED_INITIALIZER;
networkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
readReceiptManager:(OWSReadReceiptManager *)readReceiptManager
NS_DESIGNATED_INITIALIZER;
- (void)runWithAttachmentHandler:(void (^)(TSAttachmentStream *attachmentStream))attachmentHandler
transaction:(YapDatabaseReadWriteTransaction *)transaction;

View File

@ -6,26 +6,42 @@
#import "OWSAttachmentsProcessor.h"
#import "OWSIncomingSentMessageTranscript.h"
#import "OWSMessageSender.h"
#import "OWSReadReceiptManager.h"
#import "TSInfoMessage.h"
#import "TSNetworkManager.h"
#import "TSOutgoingMessage.h"
#import "TSStorageManager+SessionStore.h"
#import "TextSecureKitEnv.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSRecordTranscriptJob ()
@property (nonatomic, readonly) OWSIncomingSentMessageTranscript *incomingSentMessageTranscript;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) TSNetworkManager *networkManager;
@property (nonatomic, readonly) TSStorageManager *storageManager;
@property (nonatomic, readonly) OWSReadReceiptManager *readReceiptManager;
@property (nonatomic, readonly) OWSIncomingSentMessageTranscript *incomingSentMessageTranscript;
@end
@implementation OWSRecordTranscriptJob
- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript
{
return [self initWithIncomingSentMessageTranscript:incomingSentMessageTranscript
messageSender:[TextSecureKitEnv sharedEnv].messageSender
networkManager:TSNetworkManager.sharedManager
storageManager:TSStorageManager.sharedManager
readReceiptManager:OWSReadReceiptManager.sharedManager];
}
- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript
messageSender:(OWSMessageSender *)messageSender
networkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
readReceiptManager:(OWSReadReceiptManager *)readReceiptManager
{
self = [super init];
if (!self) {
@ -35,7 +51,8 @@ NS_ASSUME_NONNULL_BEGIN
_incomingSentMessageTranscript = incomingSentMessageTranscript;
_messageSender = messageSender;
_networkManager = networkManager;
_storageManager = [TSStorageManager sharedManager];
_storageManager = storageManager;
_readReceiptManager = readReceiptManager;
return self;
}
@ -86,10 +103,19 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
if (outgoingMessage.body.length < 1 && outgoingMessage.attachmentIds.count < 1) {
// TODO: Is this safe?
DDLogInfo(@"Ignoring message transcript for empty message.");
return;
}
// TODO: Refactor this logic.
[self.messageSender handleMessageSentRemotely:outgoingMessage
sentAt:transcript.expirationStartedAt
transaction:transaction];
[self.readReceiptManager outgoingMessageFromLinkedDevice:outgoingMessage transaction:transaction];
[attachmentsProcessor
fetchAttachmentsForMessage:outgoingMessage
transaction:transaction

View File

@ -31,8 +31,8 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
};
@class OWSSignalServiceProtosAttachmentPointer;
@class OWSSignalServiceProtosDataMessageBuilder;
@class OWSSignalServiceProtosContentBuilder;
@class OWSSignalServiceProtosDataMessageBuilder;
@class SignalRecipient;
@interface TSOutgoingMessage : TSMessage
@ -104,6 +104,9 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
// This property won't be accurate for legacy messages.
@property (atomic, readonly) BOOL isFromLinkedDevice;
// The recipient ids of the recipients who have read the message.
@property (atomic, readonly) NSSet<NSString *> *readRecipientIds;
/**
* Signal Identifier (e.g. e164 number) or nil if in a group thread.
*/
@ -173,6 +176,7 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
- (void)updateWithWasSentFromLinkedDeviceWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithSingleGroupRecipient:(NSString *)singleGroupRecipient
transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)updateWithReadRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark - Sent Recipients

View File

@ -39,6 +39,8 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
@property (atomic) TSGroupMetaMessage groupMetaMessage;
@property (atomic) NSSet<NSString *> *readRecipientIds;
@end
#pragma mark -
@ -409,6 +411,21 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
}];
}
- (void)updateWithReadRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(recipientId.length > 0);
OWSAssert(transaction);
[self applyChangeToSelfAndLatestOutgoingMessage:transaction
changeBlock:^(TSOutgoingMessage *message) {
NSMutableSet<NSString *> *readRecipientIds
= (message.readRecipientIds ? [message.readRecipientIds mutableCopy]
: [NSMutableSet new]);
[readRecipientIds addObject:recipientId];
message.readRecipientIds = readRecipientIds;
}];
}
#pragma mark -
- (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder

View File

@ -75,6 +75,8 @@ NSString *envelopeAddress(OWSSignalServiceProtosEnvelope *envelope)
return [NSString stringWithFormat:@"<CallMessage: %@ />", content.callMessage];
} else if (content.hasNullMessage) {
return [NSString stringWithFormat:@"<NullMessage: %@ />", content.nullMessage];
} else if (content.hasReceiptMessage) {
return [NSString stringWithFormat:@"<ReceiptMessage: %@ />", content.receiptMessage];
} else {
// Don't fire an analytics event; if we ever add a new content type, we'd generate a ton of
// analytics traffic.

View File

@ -18,6 +18,7 @@
#import "OWSIncomingMessageFinder.h"
#import "OWSIncomingSentMessageTranscript.h"
#import "OWSMessageSender.h"
#import "OWSReadReceiptManager.h"
#import "OWSReadReceiptsProcessor.h"
#import "OWSRecordTranscriptJob.h"
#import "OWSSyncContactsMessage.h"
@ -232,6 +233,8 @@ NS_ASSUME_NONNULL_BEGIN
[self handleIncomingEnvelope:envelope withCallMessage:content.callMessage];
} else if (content.hasNullMessage) {
DDLogInfo(@"%@ Received null message.", self.tag);
} else if (content.hasReceiptMessage) {
[self handleIncomingEnvelope:envelope withReceiptMessage:content.receiptMessage];
} else {
DDLogWarn(@"%@ Ignoring envelope. Content with no known payload", self.tag);
}
@ -326,6 +329,26 @@ NS_ASSUME_NONNULL_BEGIN
return [TextSecureKitEnv sharedEnv].profileManager;
}
- (void)handleIncomingEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
withReceiptMessage:(OWSSignalServiceProtosReceiptMessage *)receiptMessage
{
OWSAssert(envelope);
OWSAssert(receiptMessage);
switch (receiptMessage.type) {
case OWSSignalServiceProtosReceiptMessageTypeDelivery:
DDLogInfo(@"%@ Ignoring receipt message with delivery receipt.", self.tag);
return;
case OWSSignalServiceProtosReceiptMessageTypeRead:
DDLogVerbose(@"%@ Processing receipt message with read receipts.", self.tag);
[OWSReadReceiptManager.sharedManager processReadReceiptsFromRecipient:receiptMessage envelope:envelope];
break;
default:
DDLogInfo(@"%@ Ignoring receipt message of unknown type: %d.", self.tag, (int)receiptMessage.type);
return;
}
}
- (void)handleIncomingEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
withCallMessage:(OWSSignalServiceProtosCallMessage *)callMessage
{
@ -466,9 +489,7 @@ NS_ASSUME_NONNULL_BEGIN
[[OWSIncomingSentMessageTranscript alloc] initWithProto:syncMessage.sent relay:envelope.relay];
OWSRecordTranscriptJob *recordJob =
[[OWSRecordTranscriptJob alloc] initWithIncomingSentMessageTranscript:transcript
messageSender:self.messageSender
networkManager:self.networkManager];
[[OWSRecordTranscriptJob alloc] initWithIncomingSentMessageTranscript:transcript];
OWSSignalServiceProtosDataMessage *dataMessage = syncMessage.sent.message;
OWSAssert(dataMessage);

View File

@ -32,7 +32,6 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
- (instancetype)initWithThread:(TSThread *)thread offerMessage:(OWSCallOfferMessage *)offerMessage
{
self = [self initWithThread:thread];

View File

@ -4,7 +4,13 @@
NS_ASSUME_NONNULL_BEGIN
@class OWSSignalServiceProtosEnvelope;
@class OWSSignalServiceProtosReceiptMessage;
@class TSIncomingMessage;
@class TSOutgoingMessage;
@class TSThread;
@class YapDatabase;
@class YapDatabaseReadWriteTransaction;
// There are four kinds of read receipts:
//
@ -27,17 +33,38 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)sharedManager;
// This method can be called from any thread.
// This method should be called when we receive a read receipt
// from a user to whom we have sent a message.
//
// It cues this manager:
// This method can be called from any thread.
- (void)processReadReceiptsFromRecipient:(OWSSignalServiceProtosReceiptMessage *)receiptMessage
envelope:(OWSSignalServiceProtosEnvelope *)envelope;
- (void)outgoingMessageFromLinkedDevice:(TSOutgoingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction;
// This method cues this manager:
//
// * ...to inform the sender that this message was read (if read receipts
// are enabled).
// * ...to inform the local user's other devices that this message was read.
//
// Both types of messages are deduplicated.
//
// This method can be called from any thread.
- (void)messageWasReadLocally:(TSIncomingMessage *)message;
- (void)markAsReadLocallyBeforeTimestamp:(uint64_t)timestamp thread:(TSThread *)thread;
#pragma mark - Settings
- (BOOL)areReadReceiptsEnabled;
- (void)setAreReadReceiptsEnabled:(BOOL)value;
#pragma mark - Database Extension
+ (void)asyncRegisterDatabaseExtension:(YapDatabase *)database;
@end
NS_ASSUME_NONNULL_END

View File

@ -5,29 +5,176 @@
#import "OWSReadReceiptManager.h"
#import "OWSMessageSender.h"
#import "OWSReadReceipt.h"
#import "OWSReadReceiptsMessage.h"
#import "OWSReadReceiptsForLinkedDevicesMessage.h"
#import "OWSReadReceiptsForSenderMessage.h"
#import "OWSSignalServiceProtos.pb.h"
#import "TSContactThread.h"
#import "TSDatabaseView.h"
#import "TSIncomingMessage.h"
#import "TSStorageManager.h"
#import "TextSecureKitEnv.h"
#import "Threading.h"
#import <YapDatabase/YapDatabaseView.h>
NS_ASSUME_NONNULL_BEGIN
//#pragma mark - Finder
NSString *const OWSOutgoingMessageFinderExtensionName = @"OWSOutgoingMessageFinderExtensionName";
@interface OWSOutgoingMessageFinder : NSObject
@end
#pragma mark -
@interface OWSOutgoingMessageFinder ()
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@end
#pragma mark -
@implementation OWSOutgoingMessageFinder
- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection
{
OWSSingletonAssert();
self = [super init];
if (!self) {
return self;
}
_dbConnection = dbConnection;
return self;
}
- (NSArray<TSOutgoingMessage *> *)outgoingMessagesWithTimestamp:(uint64_t)timestamp
transaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(transaction);
NSMutableArray<TSOutgoingMessage *> *result = [NSMutableArray new];
YapDatabaseViewTransaction *viewTransaction = [transaction ext:OWSOutgoingMessageFinderExtensionName];
OWSAssert(viewTransaction);
[viewTransaction
enumerateKeysAndObjectsInGroup:[OWSOutgoingMessageFinder groupForTimestamp:timestamp]
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
OWSAssert([object isKindOfClass:[TSOutgoingMessage class]]);
TSOutgoingMessage *message = (TSOutgoingMessage *)object;
OWSAssert(message.timestamp == timestamp);
[result addObject:object];
}];
return [result copy];
}
+ (NSString *)groupForTimestamp:(uint64_t)timestamp
{
return [NSString stringWithFormat:@"%llu", timestamp];
}
+ (YapDatabaseView *)databaseExtension
{
YapDatabaseViewSorting *sorting =
[YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *transaction,
NSString *group,
NSString *collection1,
NSString *key1,
id object1,
NSString *collection2,
NSString *key2,
id object2) {
// The ordering doesn't matter as long as its consistent.
return [key1 compare:key2];
}];
YapDatabaseViewGrouping *grouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable(
YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) {
if (![object isKindOfClass:[TSOutgoingMessage class]]) {
return nil;
}
TSOutgoingMessage *message = (TSOutgoingMessage *)object;
// Arbitrary string - all in the same group. We're only using the view for sorting.
return [OWSOutgoingMessageFinder groupForTimestamp:message.timestamp];
}];
YapDatabaseViewOptions *options = [YapDatabaseViewOptions new];
options.allowedCollections =
[[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSOutgoingMessage collection]]];
return [[YapDatabaseView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"1" options:options];
}
+ (void)asyncRegisterDatabaseExtension:(YapDatabase *)database
{
YapDatabaseView *existingView = [database registeredExtension:OWSOutgoingMessageFinderExtensionName];
if (existingView) {
OWSFail(@"%@ was already initialized.", OWSOutgoingMessageFinderExtensionName);
return;
}
[database
asyncRegisterExtension:[self databaseExtension]
withName:OWSOutgoingMessageFinderExtensionName
completionBlock:^(BOOL ready) {
OWSCAssert(ready);
DDLogInfo(
@"%@ asyncRegisterExtension: %@ -> %d", self.tag, OWSOutgoingMessageFinderExtensionName, ready);
}];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
#pragma mark -
NSString *const OWSReadReceiptManagerCollection = @"OWSReadReceiptManagerCollection";
NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsEnabled";
NSString *const OWSRecipientReadReceiptCollection = @"OWSRecipientReadReceiptCollection";
@interface OWSReadReceiptManager ()
@property (nonatomic, readonly) TSStorageManager *storageManager;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) OWSOutgoingMessageFinder *outgoingMessageFinder;
// A map of "thread unique id"-to-"read receipt" for read receipts that
// we will send to our linked devices.
//
// Should only be accessed while synchronized on the OWSReadReceiptManager.
@property (nonatomic, readonly) NSMutableDictionary<NSString *, OWSReadReceipt *> *toLinkedDevicesReadReceiptMap;
// A map of "recipient id"-to-"timestamp list" for read receipts that
// we will send to senders.
//
// Should only be accessed while synchronized on the OWSReadReceiptManager.
@property (nonatomic, readonly) NSMutableDictionary<NSString *, NSMutableArray<NSNumber *> *> *toSenderReadReceiptMap;
// Should only be accessed while synchronized on the OWSReadReceiptManager.
@property (nonatomic) BOOL isProcessing;
// Should only be accessed while synchronized on the OWSReadReceiptManager.
@property (nonatomic) NSNumber *areReadReceiptsEnabledCached;
@end
#pragma mark -
@ -47,11 +194,13 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initDefault
{
OWSMessageSender *messageSender = [TextSecureKitEnv sharedEnv].messageSender;
TSStorageManager *storageManager = [TSStorageManager sharedManager];
return [self initWithMessageSender:messageSender];
return [self initWithMessageSender:messageSender storageManager:storageManager];
}
- (instancetype)initWithMessageSender:(OWSMessageSender *)messageSender
storageManager:(TSStorageManager *)storageManager
{
self = [super init];
@ -60,8 +209,12 @@ NS_ASSUME_NONNULL_BEGIN
}
_messageSender = messageSender;
_dbConnection = storageManager.newDatabaseConnection;
_outgoingMessageFinder = [[OWSOutgoingMessageFinder alloc] initWithDBConnection:self.dbConnection];
_toLinkedDevicesReadReceiptMap = [NSMutableDictionary new];
_toSenderReadReceiptMap = [NSMutableDictionary new];
OWSSingletonAssert();
@ -95,6 +248,8 @@ NS_ASSUME_NONNULL_BEGIN
@synchronized(self)
{
if ([TSDatabaseView hasPendingViewRegistrations]) {
DDLogInfo(
@"%@ Deferring read receipt processing due to pending database view registrations.", self.tag);
return;
}
if (self.isProcessing) {
@ -122,54 +277,254 @@ NS_ASSUME_NONNULL_BEGIN
{
@synchronized(self)
{
DDLogVerbose(@"%@ Processing read receipts.", self.tag);
self.isProcessing = NO;
NSArray<OWSReadReceipt *> *readReceiptsToSend = [self.toLinkedDevicesReadReceiptMap allValues];
NSArray<OWSReadReceipt *> *readReceiptsForLinkedDevices = [self.toLinkedDevicesReadReceiptMap allValues];
[self.toLinkedDevicesReadReceiptMap removeAllObjects];
if (readReceiptsToSend.count > 0) {
OWSReadReceiptsMessage *message = [[OWSReadReceiptsMessage alloc] initWithReadReceipts:readReceiptsToSend];
if (readReceiptsForLinkedDevices.count > 0) {
OWSReadReceiptsForLinkedDevicesMessage *message =
[[OWSReadReceiptsForLinkedDevicesMessage alloc] initWithReadReceipts:readReceiptsForLinkedDevices];
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageSender sendMessage:message
success:^{
DDLogInfo(@"%@ Successfully sent %zd read receipt to linked devices.",
self.tag,
readReceiptsToSend.count);
readReceiptsForLinkedDevices.count);
}
failure:^(NSError *error) {
DDLogError(@"%@ Failed to send read receipt to linked devices with error: %@", self.tag, error);
}];
});
}
NSArray<OWSReadReceipt *> *readReceiptsToSend = [[self.toLinkedDevicesReadReceiptMap allValues] copy];
[self.toLinkedDevicesReadReceiptMap removeAllObjects];
if (self.toSenderReadReceiptMap.count > 0) {
for (NSString *recipientId in self.toSenderReadReceiptMap) {
NSArray<NSNumber *> *timestamps = self.toSenderReadReceiptMap[recipientId];
OWSAssert(timestamps.count > 0);
TSThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId];
OWSReadReceiptsForSenderMessage *message =
[[OWSReadReceiptsForSenderMessage alloc] initWithThread:thread messageTimestamps:timestamps];
dispatch_async(dispatch_get_main_queue(), ^{
[self.messageSender sendMessage:message
success:^{
DDLogInfo(@"%@ Successfully sent %zd read receipts to sender.",
self.tag,
readReceiptsToSend.count);
}
failure:^(NSError *error) {
DDLogError(@"%@ Failed to send read receipts to sender with error: %@", self.tag, error);
}];
});
}
[self.toSenderReadReceiptMap removeAllObjects];
}
}
}
- (void)messageWasReadLocally:(TSIncomingMessage *)message;
#pragma mark - Mark as Read Locally
- (void)markAsReadLocallyBeforeTimestamp:(uint64_t)timestamp thread:(TSThread *)thread
{
OWSAssert(thread);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray<id<OWSReadTracking>> *interactions = [NSMutableArray new];
[[TSDatabaseView unseenDatabaseViewExtension:transaction]
enumerateRowsInGroup:thread.uniqueId
usingBlock:^(NSString *collection,
NSString *key,
id object,
id metadata,
NSUInteger index,
BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
OWSFail(
@"Expected to conform to OWSReadTracking: object with class: %@ collection: %@ "
@"key: %@",
[object class],
collection,
key);
return;
}
id<OWSReadTracking> possiblyRead = (id<OWSReadTracking>)object;
if (possiblyRead.timestampForSorting > timestamp) {
*stop = YES;
return;
}
OWSAssert(!possiblyRead.read);
if (!possiblyRead.read) {
[interactions addObject:possiblyRead];
}
}];
if (interactions.count < 1) {
return;
}
DDLogError(@"Marking %zd messages as read.", interactions.count);
for (id<OWSReadTracking> possiblyRead in interactions) {
[possiblyRead markAsReadWithTransaction:transaction sendReadReceipt:YES updateExpiration:YES];
}
}];
});
}
- (void)messageWasReadLocally:(TSIncomingMessage *)message
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self)
{
NSString *threadUniqueId = message.uniqueThreadId;
OWSAssert(threadUniqueId.length > 0);
NSString *messageAuthorId = message.messageAuthorId;
OWSAssert(messageAuthorId.length > 0);
OWSReadReceipt *newReadReceipt =
[[OWSReadReceipt alloc] initWithSenderId:messageAuthorId timestamp:message.timestamp];
OWSReadReceipt *_Nullable oldReadReceipt = self.toLinkedDevicesReadReceiptMap[threadUniqueId];
if (oldReadReceipt && oldReadReceipt.timestamp > newReadReceipt.timestamp) {
// If there's an existing read receipt for the same thread with
// a newer timestamp, discard the new read receipt.
DDLogVerbose(@"%@ Ignoring redundant read receipt for linked devices.", self.tag);
} else {
DDLogVerbose(@"%@ Enqueuing read receipt for linked devices.", self.tag);
self.toLinkedDevicesReadReceiptMap[threadUniqueId] = newReadReceipt;
}
if ([self areReadReceiptsEnabled]) {
DDLogVerbose(@"%@ Enqueuing read receipt for sender.", self.tag);
NSMutableArray<NSNumber *> *_Nullable timestamps = self.toSenderReadReceiptMap[messageAuthorId];
if (!timestamps) {
timestamps = [NSMutableArray new];
self.toSenderReadReceiptMap[messageAuthorId] = timestamps;
}
[timestamps addObject:@(message.timestamp)];
}
[self scheduleProcessing];
}
});
}
#pragma mark - Read Receipts From Recipient
- (void)processReadReceiptsFromRecipient:(OWSSignalServiceProtosReceiptMessage *)receiptMessage
envelope:(OWSSignalServiceProtosEnvelope *)envelope
{
OWSAssert(receiptMessage);
OWSAssert(envelope);
OWSAssert(receiptMessage.type == OWSSignalServiceProtosReceiptMessageTypeRead);
if (![self areReadReceiptsEnabled]) {
DDLogInfo(@"%@ Ignoring incoming receipt message as read receipts are disabled.", self.tag);
return;
}
NSString *recipientId = envelope.source;
OWSAssert(recipientId.length > 0);
PBArray *timestamps = receiptMessage.timestamp;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (int i = 0; i < timestamps.count; i++) {
UInt64 timestamp = [timestamps uint64AtIndex:i];
NSArray<TSOutgoingMessage *> *messages =
[self.outgoingMessageFinder outgoingMessagesWithTimestamp:timestamp transaction:transaction];
OWSAssert(messages.count <= 1);
if (messages.count > 0) {
// TODO: We might also need to "mark as read by recipient" any older messages
// from us in that thread. Or maybe this state should hang on the thread?
for (TSOutgoingMessage *message in messages) {
[message updateWithReadRecipient:recipientId transaction:transaction];
}
} else {
// Persist the read receipts so that we can apply them to outgoing messages
// that we learn about later through sync messages.
NSString *storageKey = [NSString stringWithFormat:@"%llu", timestamp];
NSSet<NSString *> *recipientIds =
[transaction objectForKey:storageKey inCollection:OWSRecipientReadReceiptCollection];
NSMutableSet<NSString *> *recipientIdsCopy
= (recipientIds ? [recipientIds mutableCopy] : [NSMutableSet new]);
[recipientIdsCopy addObject:recipientId];
[transaction setObject:recipientIdsCopy
forKey:storageKey
inCollection:OWSRecipientReadReceiptCollection];
}
}
}];
});
}
- (void)outgoingMessageFromLinkedDevice:(TSOutgoingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(message);
OWSAssert(transaction);
NSString *storageKey = [NSString stringWithFormat:@"%llu", message.timestamp];
NSSet<NSString *> *recipientIds =
[transaction objectForKey:storageKey inCollection:OWSRecipientReadReceiptCollection];
if (recipientIds) {
OWSAssert(recipientIds.count > 0);
for (NSString *recipientId in recipientIds) {
[message updateWithReadRecipient:recipientId transaction:transaction];
}
}
[transaction removeObjectForKey:storageKey inCollection:OWSRecipientReadReceiptCollection];
}
#pragma mark - Settings
- (BOOL)areReadReceiptsEnabled
{
@synchronized(self)
{
NSString *threadUniqueId = message.uniqueThreadId;
OWSAssert(threadUniqueId.length > 0);
NSString *messageAuthorId = message.messageAuthorId;
OWSAssert(messageAuthorId.length > 0);
OWSReadReceipt *newReadReceipt =
[[OWSReadReceipt alloc] initWithSenderId:messageAuthorId timestamp:message.timestamp];
OWSReadReceipt *_Nullable oldReadReceipt = self.toLinkedDevicesReadReceiptMap[threadUniqueId];
if (oldReadReceipt && oldReadReceipt.timestamp > newReadReceipt.timestamp) {
// If there's an existing read receipt for the same thread with
// a newer timestamp, discard the new read receipt.
return;
if (!self.areReadReceiptsEnabledCached) {
// Default to NO.
self.areReadReceiptsEnabledCached =
@([self.dbConnection boolForKey:OWSReadReceiptManagerAreReadReceiptsEnabled
inCollection:OWSReadReceiptManagerCollection]);
}
self.toLinkedDevicesReadReceiptMap[threadUniqueId] = newReadReceipt;
[self scheduleProcessing];
return [self.areReadReceiptsEnabledCached boolValue];
}
}
- (void)setAreReadReceiptsEnabled:(BOOL)value
{
DDLogInfo(@"%@ areReadReceiptsEnabled: %d.", self.tag, value);
@synchronized(self)
{
[self.dbConnection setBool:value
forKey:OWSReadReceiptManagerAreReadReceiptsEnabled
inCollection:OWSReadReceiptManagerCollection];
self.areReadReceiptsEnabledCached = @(value);
}
}
#pragma mark - Database Extension
+ (void)asyncRegisterDatabaseExtension:(YapDatabase *)database
{
[OWSOutgoingMessageFinder asyncRegisterDatabaseExtension:database];
}
#pragma mark - Logging
+ (NSString *)tag

View File

@ -20,6 +20,7 @@
#import <SAMKeychain/SAMKeychain.h>
#import <SignalServiceKit/OWSBatchMessageProcessor.h>
#import <SignalServiceKit/OWSMessageReceiver.h>
#import <SignalServiceKit/OWSReadReceiptManager.h>
#import <YapDatabase/YapDatabaseRelationship.h>
NS_ASSUME_NONNULL_BEGIN
@ -342,6 +343,7 @@ void setDatabaseInitialized()
OWSFailedAttachmentDownloadsJob *failedAttachmentDownloadsMessagesJob =
[[OWSFailedAttachmentDownloadsJob alloc] initWithStorageManager:self];
[failedAttachmentDownloadsMessagesJob asyncRegisterDatabaseExtensions];
[OWSReadReceiptManager asyncRegisterDatabaseExtension:self.database];
// NOTE: [TSDatabaseView asyncRegistrationCompletion] ensures that
// kNSNotificationName_DatabaseViewRegistrationComplete is not fired until all