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:
parent
29dae9bb99
commit
11cadf4200
|
@ -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 */,
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,7 +8,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@class OWSReadReceipt;
|
||||
|
||||
@interface OWSReadReceiptsMessage : OWSOutgoingSyncMessage
|
||||
@interface OWSReadReceiptsForLinkedDevicesMessage : OWSOutgoingSyncMessage
|
||||
|
||||
- (instancetype)initWithReadReceipts:(NSArray<OWSReadReceipt *> *)readReceipts;
|
||||
|
|
@ -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
|
||||
{
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -32,7 +32,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithThread:(TSThread *)thread offerMessage:(OWSCallOfferMessage *)offerMessage
|
||||
{
|
||||
self = [self initWithThread:thread];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue