diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.m index 60be9b43c6..f7cc4ceb3c 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHiddenView.m @@ -265,7 +265,8 @@ NS_ASSUME_NONNULL_BEGIN return pendingColor; case PerMessageExpirationState_OutgoingSending: return self.conversationStyle.bubbleColorOutgoingSending; - case PerMessageExpirationState_OutgoingSent: + case PerMessageExpirationState_OutgoingSentAvailable: + case PerMessageExpirationState_OutgoingSentExpired: return self.conversationStyle.bubbleColorOutgoingSent; case PerMessageExpirationState_IncomingInvalidContent: return Theme.backgroundColor; @@ -285,7 +286,8 @@ NS_ASSUME_NONNULL_BEGIN case PerMessageExpirationState_IncomingAvailable: case PerMessageExpirationState_OutgoingFailed: case PerMessageExpirationState_OutgoingSending: - case PerMessageExpirationState_OutgoingSent: + case PerMessageExpirationState_OutgoingSentAvailable: + case PerMessageExpirationState_OutgoingSentExpired: return NO; case PerMessageExpirationState_IncomingInvalidContent: return YES; @@ -309,7 +311,8 @@ NS_ASSUME_NONNULL_BEGIN case PerMessageExpirationState_OutgoingFailed: return nil; case PerMessageExpirationState_OutgoingSending: - case PerMessageExpirationState_OutgoingSent: + case PerMessageExpirationState_OutgoingSentAvailable: + case PerMessageExpirationState_OutgoingSentExpired: return nil; case PerMessageExpirationState_IncomingInvalidContent: return UIColor.ows_destructiveRedColor; @@ -334,7 +337,8 @@ NS_ASSUME_NONNULL_BEGIN return ConversationStyle.bubbleTextColorIncoming; case PerMessageExpirationState_OutgoingFailed: case PerMessageExpirationState_OutgoingSending: - case PerMessageExpirationState_OutgoingSent: + case PerMessageExpirationState_OutgoingSentAvailable: + case PerMessageExpirationState_OutgoingSentExpired: return ConversationStyle.bubbleTextColorOutgoing; case PerMessageExpirationState_IncomingInvalidContent: return Theme.secondaryColor; @@ -359,7 +363,8 @@ NS_ASSUME_NONNULL_BEGIN case PerMessageExpirationState_OutgoingFailed: return pendingColor; case PerMessageExpirationState_OutgoingSending: - case PerMessageExpirationState_OutgoingSent: + case PerMessageExpirationState_OutgoingSentAvailable: + case PerMessageExpirationState_OutgoingSentExpired: return ConversationStyle.bubbleTextColorOutgoing; case PerMessageExpirationState_IncomingInvalidContent: return Theme.secondaryColor; @@ -443,10 +448,17 @@ NS_ASSUME_NONNULL_BEGIN self.label.text = CommonStrings.retryButton; break; case PerMessageExpirationState_OutgoingSending: - case PerMessageExpirationState_OutgoingSent: + self.label.text = NSLocalizedString(@"MESSAGE_STATUS_SENDING", @"message status while message is sending."); + break; + case PerMessageExpirationState_OutgoingSentExpired: self.label.text = NSLocalizedString(@"PER_MESSAGE_EXPIRATION_OUTGOING_MESSAGE", @"Label for outgoing messages with per-message expiration."); break; + case PerMessageExpirationState_OutgoingSentAvailable: + self.label.text = NSLocalizedString(@"PER_MESSAGE_EXPIRATION_TAP_TO_VIEW", + @"Label for messages with per-message expiration indicating that " + @"user can tap to view the message's contents."); + break; case PerMessageExpirationState_IncomingInvalidContent: self.label.text = NSLocalizedString(@"PER_MESSAGE_EXPIRATION_INVALID_CONTENT", @"Label for messages with per-message expiration that have invalid content."); @@ -487,8 +499,10 @@ NS_ASSUME_NONNULL_BEGIN case PerMessageExpirationState_OutgoingFailed: return @"arrow-down-circle-outline-24"; case PerMessageExpirationState_OutgoingSending: - case PerMessageExpirationState_OutgoingSent: + case PerMessageExpirationState_OutgoingSentExpired: return @"play-outline-24"; + case PerMessageExpirationState_OutgoingSentAvailable: + return @"play-filled-24"; case PerMessageExpirationState_IncomingInvalidContent: OWSFailDebug(@"Unexpected state."); return nil; @@ -549,9 +563,10 @@ NS_ASSUME_NONNULL_BEGIN return self.viewItem.perMessageExpirationState == PerMessageExpirationState_IncomingFailed; } -- (BOOL)isIncomingAvailable +- (BOOL)isAvailable { - return self.viewItem.perMessageExpirationState == PerMessageExpirationState_IncomingAvailable; + return (self.viewItem.perMessageExpirationState == PerMessageExpirationState_IncomingAvailable + || self.viewItem.perMessageExpirationState == PerMessageExpirationState_OutgoingSentAvailable); } #pragma mark - Measurement @@ -712,7 +727,7 @@ NS_ASSUME_NONNULL_BEGIN if (self.isIncomingFailed) { [self.delegate didTapFailedIncomingAttachment:self.viewItem]; - } else if (self.isIncomingAvailable) { + } else if (self.isAvailable) { [self.delegate didTapAttachmentWithPerMessageExpiration:self.viewItem attachmentStream:self.viewItem.attachmentStream]; } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 75e03eda68..f3bba45a45 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -32,9 +32,12 @@ typedef NS_ENUM(NSUInteger, PerMessageExpirationState) { PerMessageExpirationState_IncomingInvalidContent, PerMessageExpirationState_OutgoingSending, PerMessageExpirationState_OutgoingFailed, - PerMessageExpirationState_OutgoingSent, + PerMessageExpirationState_OutgoingSentAvailable, + PerMessageExpirationState_OutgoingSentExpired, }; +NSString *NSStringForPerMessageExpirationState(PerMessageExpirationState value); + @class ContactShareViewModel; @class ConversationViewCell; @class DisplayableText; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 102fbf2fd9..f86bd7844f 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -41,6 +41,32 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } +NSString *NSStringForPerMessageExpirationState(PerMessageExpirationState cellType) +{ + switch (cellType) { + case PerMessageExpirationState_Unknown: + return @"PerMessageExpirationState_Unknown"; + case PerMessageExpirationState_IncomingExpired: + return @"PerMessageExpirationState_IncomingExpired"; + case PerMessageExpirationState_IncomingDownloading: + return @"PerMessageExpirationState_IncomingDownloading"; + case PerMessageExpirationState_IncomingFailed: + return @"PerMessageExpirationState_IncomingFailed"; + case PerMessageExpirationState_IncomingAvailable: + return @"PerMessageExpirationState_IncomingAvailable"; + case PerMessageExpirationState_IncomingInvalidContent: + return @"PerMessageExpirationState_IncomingInvalidContent"; + case PerMessageExpirationState_OutgoingSending: + return @"PerMessageExpirationState_OutgoingSending"; + case PerMessageExpirationState_OutgoingFailed: + return @"PerMessageExpirationState_OutgoingFailed"; + case PerMessageExpirationState_OutgoingSentAvailable: + return @"PerMessageExpirationState_OutgoingSentAvailable"; + case PerMessageExpirationState_OutgoingSentExpired: + return @"PerMessageExpirationState_OutgoingSentExpired"; + } +} + #pragma mark - @implementation ConversationMediaAlbumItem @@ -856,10 +882,34 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) switch (outgoingMessage.messageState) { case TSOutgoingMessageStateSending: self.perMessageExpirationState = PerMessageExpirationState_OutgoingSending; + break; case TSOutgoingMessageStateFailed: self.perMessageExpirationState = PerMessageExpirationState_OutgoingFailed; + break; default: - self.perMessageExpirationState = PerMessageExpirationState_OutgoingSent; + if (message.perMessageExpirationHasExpired) { + self.perMessageExpirationState = PerMessageExpirationState_OutgoingSentExpired; + } else { + NSArray *mediaAttachments = [message mediaAttachmentsWithTransaction:transaction]; + // TODO: We currently only support single attachments for messages + // with per-message expiration. + TSAttachment *_Nullable mediaAttachment = mediaAttachments.firstObject; + if ([mediaAttachment isKindOfClass:[TSAttachmentPointer class]]) { + OWSFailDebug(@"Invalid outgoing attachment."); + self.perMessageExpirationState = PerMessageExpirationState_OutgoingSentExpired; + } else if ([mediaAttachment isKindOfClass:[TSAttachmentStream class]]) { + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)mediaAttachment; + if (attachmentStream.isValidVisualMedia + && (attachmentStream.isImage || attachmentStream.isAnimated)) { + self.perMessageExpirationState = PerMessageExpirationState_OutgoingSentAvailable; + self.attachmentStream = attachmentStream; + } else { + OWSFailDebug(@"Invalid outgoing attachment."); + self.perMessageExpirationState = PerMessageExpirationState_OutgoingSentExpired; + } + } + } + break; } self.messageCellType = OWSMessageCellType_PerMessageExpiration; return; diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 9b386e3230..b4db0351db 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -1429,12 +1429,18 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; }; [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:message + TSInteraction *_Nullable latestCopy = + [TSInteraction anyFetchWithUniqueId:message.uniqueId transaction:transaction.asAnyRead]; + if (![latestCopy isKindOfClass:[TSOutgoingMessage class]]) { + OWSLogWarn(@"Could not update expiration for deleted message."); + return; + } + TSOutgoingMessage *latestMessage = (TSOutgoingMessage *)latestCopy; + [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:latestMessage expirationStartedAt:[NSDate ows_millisecondTimeStamp] transaction:transaction]; - - [PerMessageExpiration expireIfNecessaryWithMessage:message - transaction:transaction.asAnyWrite]; + + [PerMessageExpiration expireIfNecessaryWithMessage:latestMessage transaction:transaction.asAnyWrite]; }]; if (!message.shouldSyncTranscript) { diff --git a/SignalServiceKit/src/Util/PerMessageExpiration.swift b/SignalServiceKit/src/Util/PerMessageExpiration.swift index 812860e3c2..60406fee0b 100644 --- a/SignalServiceKit/src/Util/PerMessageExpiration.swift +++ b/SignalServiceKit/src/Util/PerMessageExpiration.swift @@ -37,13 +37,6 @@ public class PerMessageExpiration: NSObject { @objc public class func startPerMessageExpiration(forMessage message: TSMessage, transaction: SDSAnyWriteTransaction) { - AssertIsOnMainThread() - - guard message.hasPerMessageExpiration else { - owsFailDebug("Message does not have per-message expiration.") - return - } - // Start expiration using "now" as the read time. startPerMessageExpiration(forMessage: message, readTimestamp: nowMs(), @@ -56,6 +49,15 @@ public class PerMessageExpiration: NSObject { sendSyncMessages: Bool, transaction: SDSAnyWriteTransaction) { + guard message.hasPerMessageExpiration else { + owsFailDebug("Message does not have per-message expiration.") + return + } + guard !isOutgoingNotYetSent(message: message) else { + Logger.warn("Not starting expiration for not-yet-sent outgoing message.") + return + } + // Make sure that timestamp is not later than now. let timestamp = min(readTimestamp, nowMs()) @@ -131,13 +133,14 @@ public class PerMessageExpiration: NSObject { for message in messages { if shouldMessageAutoExpire(message) { completeExpiration(forMessage: message, transaction: transaction) - } else if isOutgoingSent(message: message) { - completeExpiration(forMessage: message, transaction: transaction) } else if message.hasPerMessageExpirationStarted { if shouldResumeNormalExpiration { // If expiration is started, resume countdown. schedulePerMessageExpiration(forMessage: message, transaction: transaction) } + } else if isOutgoingSent(message: message), + !message.hasPerMessageExpirationStarted { + startPerMessageExpiration(forMessage: message, transaction: transaction) } } } @@ -162,15 +165,16 @@ public class PerMessageExpiration: NSObject { return } - // If outgoing message and is "sent", expire. - guard !isOutgoingSent(message: message) else { + // If countdown has completed, expire. + guard !hasExpirationCountdownCompleted(message: message) else { completeExpiration(forMessage: message, transaction: transaction) return } - // If countdown has completed, expire. - guard !hasExpirationCountdownCompleted(message: message) else { - completeExpiration(forMessage: message, transaction: transaction) + // If outgoing message and is "sent", start expiration if necessary. + if isOutgoingSent(message: message), + !message.hasPerMessageExpirationStarted { + startPerMessageExpiration(forMessage: message, transaction: transaction) return } } @@ -181,6 +185,10 @@ public class PerMessageExpiration: NSObject { } private class func isOutgoingSent(message: TSMessage) -> Bool { + guard message.hasPerMessageExpiration else { + owsFailDebug("Unexpected message.") + return false + } // If outgoing message and is "sent", expire. guard let outgoingMessage = message as? TSOutgoingMessage else { return false @@ -191,6 +199,17 @@ public class PerMessageExpiration: NSObject { return true } + private class func isOutgoingNotYetSent(message: TSMessage) -> Bool { + // If outgoing message and is "sent", expire. + guard let outgoingMessage = message as? TSOutgoingMessage else { + return false + } + guard outgoingMessage.messageState != .sent else { + return false + } + return true + } + // We auto-expire messages after 30 days, even if the user hasn't seen them. private class func shouldMessageAutoExpire(_ message: TSMessage) -> Bool { let autoExpireDeadlineMs = min(message.timestamp, message.receivedAtTimestamp) + 30 * kDayInMs