diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 09d4b7295..0f049a41b 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSUnreadIndicatorInteraction.h; sourceTree = ""; }; 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSUnreadIndicatorInteraction.m; sourceTree = ""; }; + 34CA1C231F706B5400E51C51 /* NSAttributedString+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+OWS.h"; sourceTree = ""; }; + 34CA1C241F706B5400E51C51 /* NSAttributedString+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+OWS.m"; sourceTree = ""; }; 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppUpdateNag.h; sourceTree = ""; }; 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = ""; }; 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToContactViewController.h; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 0b71dad4a..26c9d996d 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -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]; } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index fa1b46b08..ce447310d 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -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 #import #import +#import #import #import #import @@ -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> *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 possiblyRead = (id)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 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 diff --git a/Signal/src/ViewControllers/PrivacySettingsTableViewController.m b/Signal/src/ViewControllers/PrivacySettingsTableViewController.m index b275d8685..8f7e0d61d 100644 --- a/Signal/src/ViewControllers/PrivacySettingsTableViewController.m +++ b/Signal/src/ViewControllers/PrivacySettingsTableViewController.m @@ -3,10 +3,11 @@ // #import "PrivacySettingsTableViewController.h" -#import "OWSPreferences.h" #import "BlockListViewController.h" #import "Environment.h" +#import "OWSPreferences.h" #import "Signal-Swift.h" +#import 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 diff --git a/Signal/src/environment/OWSPreferences.h b/Signal/src/environment/OWSPreferences.h index 73387a701..d22783594 100644 --- a/Signal/src/environment/OWSPreferences.h +++ b/Signal/src/environment/OWSPreferences.h @@ -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 diff --git a/Signal/src/environment/OWSPreferences.m b/Signal/src/environment/OWSPreferences.m index b5b1c0c31..4ac6b2d31 100644 --- a/Signal/src/environment/OWSPreferences.m +++ b/Signal/src/environment/OWSPreferences.m @@ -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 diff --git a/Signal/src/util/NSAttributedString+OWS.h b/Signal/src/util/NSAttributedString+OWS.h new file mode 100644 index 000000000..a1b32e17e --- /dev/null +++ b/Signal/src/util/NSAttributedString+OWS.h @@ -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 diff --git a/Signal/src/util/NSAttributedString+OWS.m b/Signal/src/util/NSAttributedString+OWS.m new file mode 100644 index 000000000..6ffea8956 --- /dev/null +++ b/Signal/src/util/NSAttributedString+OWS.m @@ -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 diff --git a/SignalServiceKit/src/Devices/OWSReadReceiptsMessage.h b/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.h similarity index 50% rename from SignalServiceKit/src/Devices/OWSReadReceiptsMessage.h rename to SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.h index 5c980c7c3..acc306385 100644 --- a/SignalServiceKit/src/Devices/OWSReadReceiptsMessage.h +++ b/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.h @@ -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 *)readReceipts; @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Devices/OWSReadReceiptsMessage.m b/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.m similarity index 87% rename from SignalServiceKit/src/Devices/OWSReadReceiptsMessage.m rename to SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.m index 494b0e045..9c309b18b 100644 --- a/SignalServiceKit/src/Devices/OWSReadReceiptsMessage.m +++ b/SignalServiceKit/src/Devices/OWSReadReceiptsForLinkedDevicesMessage.m @@ -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 *readReceipts; @end -@implementation OWSReadReceiptsMessage +@implementation OWSReadReceiptsForLinkedDevicesMessage - (instancetype)initWithReadReceipts:(NSArray *)readReceipts { diff --git a/SignalServiceKit/src/Devices/OWSReadReceiptsForSenderMessage.h b/SignalServiceKit/src/Devices/OWSReadReceiptsForSenderMessage.h new file mode 100644 index 000000000..a3c2aaa5f --- /dev/null +++ b/SignalServiceKit/src/Devices/OWSReadReceiptsForSenderMessage.h @@ -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 *)messageTimestamps; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Devices/OWSReadReceiptsForSenderMessage.m b/SignalServiceKit/src/Devices/OWSReadReceiptsForSenderMessage.m new file mode 100644 index 000000000..85b1cc24b --- /dev/null +++ b/SignalServiceKit/src/Devices/OWSReadReceiptsForSenderMessage.m @@ -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 *messageTimestamps; + +@end + +@implementation OWSReadReceiptsForSenderMessage + +- (instancetype)initWithThread:(nullable TSThread *)thread messageTimestamps:(NSArray *)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 diff --git a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h index 35e780e9c..daa2dc462 100644 --- a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h +++ b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.h @@ -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; diff --git a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m index 821e9efe6..bdd5b0981 100644 --- a/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m +++ b/SignalServiceKit/src/Devices/OWSRecordTranscriptJob.m @@ -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 diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h index 0774d90cc..be3a48997 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h @@ -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 *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 diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index e1d4022b4..3af2f649f 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -39,6 +39,8 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec @property (atomic) TSGroupMetaMessage groupMetaMessage; +@property (atomic) NSSet *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 *readRecipientIds + = (message.readRecipientIds ? [message.readRecipientIds mutableCopy] + : [NSMutableSet new]); + [readRecipientIds addObject:recipientId]; + message.readRecipientIds = readRecipientIds; + }]; +} + #pragma mark - - (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder diff --git a/SignalServiceKit/src/Messages/OWSMessageHandler.m b/SignalServiceKit/src/Messages/OWSMessageHandler.m index 0bdf3bb16..59450d7ca 100644 --- a/SignalServiceKit/src/Messages/OWSMessageHandler.m +++ b/SignalServiceKit/src/Messages/OWSMessageHandler.m @@ -75,6 +75,8 @@ NSString *envelopeAddress(OWSSignalServiceProtosEnvelope *envelope) return [NSString stringWithFormat:@"", content.callMessage]; } else if (content.hasNullMessage) { return [NSString stringWithFormat:@"", content.nullMessage]; + } else if (content.hasReceiptMessage) { + return [NSString stringWithFormat:@"", 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. diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 9cba78d03..fc7738489 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -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); diff --git a/SignalServiceKit/src/Messages/OWSOutgoingCallMessage.m b/SignalServiceKit/src/Messages/OWSOutgoingCallMessage.m index f91a7d797..5ab30469c 100644 --- a/SignalServiceKit/src/Messages/OWSOutgoingCallMessage.m +++ b/SignalServiceKit/src/Messages/OWSOutgoingCallMessage.m @@ -32,7 +32,6 @@ NS_ASSUME_NONNULL_BEGIN return self; } - - (instancetype)initWithThread:(TSThread *)thread offerMessage:(OWSCallOfferMessage *)offerMessage { self = [self initWithThread:thread]; diff --git a/SignalServiceKit/src/Messages/OWSReadReceiptManager.h b/SignalServiceKit/src/Messages/OWSReadReceiptManager.h index 8a3f2729b..df6aade7e 100644 --- a/SignalServiceKit/src/Messages/OWSReadReceiptManager.h +++ b/SignalServiceKit/src/Messages/OWSReadReceiptManager.h @@ -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 diff --git a/SignalServiceKit/src/Messages/OWSReadReceiptManager.m b/SignalServiceKit/src/Messages/OWSReadReceiptManager.m index 94b61392b..0788dd9e3 100644 --- a/SignalServiceKit/src/Messages/OWSReadReceiptManager.m +++ b/SignalServiceKit/src/Messages/OWSReadReceiptManager.m @@ -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 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 *)outgoingMessagesWithTimestamp:(uint64_t)timestamp + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssert(transaction); + NSMutableArray *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 *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 *> *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 *readReceiptsToSend = [self.toLinkedDevicesReadReceiptMap allValues]; + NSArray *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 *readReceiptsToSend = [[self.toLinkedDevicesReadReceiptMap allValues] copy]; + [self.toLinkedDevicesReadReceiptMap removeAllObjects]; + if (self.toSenderReadReceiptMap.count > 0) { + for (NSString *recipientId in self.toSenderReadReceiptMap) { + NSArray *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> *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 possiblyRead = (id)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 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 *_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 *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 *recipientIds = + [transaction objectForKey:storageKey inCollection:OWSRecipientReadReceiptCollection]; + NSMutableSet *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 *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 diff --git a/SignalServiceKit/src/Storage/TSStorageManager.m b/SignalServiceKit/src/Storage/TSStorageManager.m index 898432694..f4046e262 100644 --- a/SignalServiceKit/src/Storage/TSStorageManager.m +++ b/SignalServiceKit/src/Storage/TSStorageManager.m @@ -20,6 +20,7 @@ #import #import #import +#import #import 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