723 lines
27 KiB
Objective-C
723 lines
27 KiB
Objective-C
//
|
|
// Copyright 2017 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
#import "TSMessage.h"
|
|
#import "TSQuotedMessage.h"
|
|
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
static const NSUInteger OWSMessageSchemaVersion = 4;
|
|
|
|
#pragma mark -
|
|
|
|
@interface TSMessage ()
|
|
|
|
@property (nonatomic, nullable) NSString *body;
|
|
@property (nonatomic, nullable) MessageBodyRanges *bodyRanges;
|
|
|
|
@property (nonatomic) uint32_t expiresInSeconds;
|
|
@property (nonatomic) uint64_t expireStartedAt;
|
|
@property (nonatomic, nullable) NSNumber *expireTimerVersion;
|
|
|
|
/**
|
|
* The version of the model class's schema last used to serialize this model. Use this to manage data migrations during
|
|
* object de/serialization.
|
|
*
|
|
* e.g.
|
|
*
|
|
* - (id)initWithCoder:(NSCoder *)coder
|
|
* {
|
|
* self = [super initWithCoder:coder];
|
|
* if (!self) { return self; }
|
|
* if (_schemaVersion < 2) {
|
|
* _newName = [coder decodeObjectForKey:@"oldName"]
|
|
* }
|
|
* ...
|
|
* _schemaVersion = 2;
|
|
* }
|
|
*/
|
|
@property (nonatomic, readonly) NSUInteger schemaVersion;
|
|
|
|
@property (nonatomic, nullable) TSQuotedMessage *quotedMessage;
|
|
@property (nonatomic, nullable) OWSContact *contactShare;
|
|
@property (nonatomic, nullable) OWSLinkPreview *linkPreview;
|
|
@property (nonatomic, nullable) MessageSticker *messageSticker;
|
|
|
|
@property (nonatomic) BOOL isViewOnceMessage;
|
|
@property (nonatomic) BOOL isViewOnceComplete;
|
|
@property (nonatomic) BOOL wasRemotelyDeleted;
|
|
|
|
@property (nonatomic, nullable) NSString *storyReactionEmoji;
|
|
|
|
@property (nonatomic) BOOL isPoll;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation TSMessage
|
|
|
|
- (instancetype)initMessageWithBuilder:(TSMessageBuilder *)messageBuilder
|
|
{
|
|
self = [super initWithTimestamp:messageBuilder.timestamp
|
|
receivedAtTimestamp:messageBuilder.receivedAtTimestamp
|
|
thread:messageBuilder.thread];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
_schemaVersion = OWSMessageSchemaVersion;
|
|
|
|
if (messageBuilder.messageBody.length > 0) {
|
|
_body = messageBuilder.messageBody;
|
|
_bodyRanges = messageBuilder.bodyRanges;
|
|
}
|
|
_deprecated_attachmentIds = nil;
|
|
_editState = messageBuilder.editState;
|
|
_expiresInSeconds = messageBuilder.expiresInSeconds;
|
|
_expireStartedAt = messageBuilder.expireStartedAt;
|
|
_expireTimerVersion = messageBuilder.expireTimerVersion;
|
|
[self updateExpiresAt];
|
|
_isSmsMessageRestoredFromBackup = messageBuilder.isSmsMessageRestoredFromBackup;
|
|
_isViewOnceMessage = messageBuilder.isViewOnceMessage;
|
|
_isViewOnceComplete = messageBuilder.isViewOnceComplete;
|
|
_wasRemotelyDeleted = messageBuilder.wasRemotelyDeleted;
|
|
|
|
_storyTimestamp = messageBuilder.storyTimestamp;
|
|
_storyAuthorUuidString = messageBuilder.storyAuthorAci.serviceIdUppercaseString;
|
|
_storyReactionEmoji = messageBuilder.storyReactionEmoji;
|
|
_isGroupStoryReply = messageBuilder.isGroupStoryReply;
|
|
|
|
_quotedMessage = messageBuilder.quotedMessage;
|
|
_contactShare = messageBuilder.contactShare;
|
|
_linkPreview = messageBuilder.linkPreview;
|
|
_messageSticker = messageBuilder.messageSticker;
|
|
_giftBadge = messageBuilder.giftBadge;
|
|
_isPoll = messageBuilder.isPoll;
|
|
|
|
return self;
|
|
}
|
|
|
|
// --- CODE GENERATION MARKER
|
|
|
|
// This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run
|
|
// `sds_codegen.sh`.
|
|
|
|
// clang-format off
|
|
|
|
- (instancetype)initWithGrdbId:(int64_t)grdbId
|
|
uniqueId:(NSString *)uniqueId
|
|
receivedAtTimestamp:(uint64_t)receivedAtTimestamp
|
|
sortId:(uint64_t)sortId
|
|
timestamp:(uint64_t)timestamp
|
|
uniqueThreadId:(NSString *)uniqueThreadId
|
|
body:(nullable NSString *)body
|
|
bodyRanges:(nullable MessageBodyRanges *)bodyRanges
|
|
contactShare:(nullable OWSContact *)contactShare
|
|
deprecated_attachmentIds:(nullable NSArray<NSString *> *)deprecated_attachmentIds
|
|
editState:(TSEditState)editState
|
|
expireStartedAt:(uint64_t)expireStartedAt
|
|
expireTimerVersion:(nullable NSNumber *)expireTimerVersion
|
|
expiresAt:(uint64_t)expiresAt
|
|
expiresInSeconds:(unsigned int)expiresInSeconds
|
|
giftBadge:(nullable OWSGiftBadge *)giftBadge
|
|
isGroupStoryReply:(BOOL)isGroupStoryReply
|
|
isPoll:(BOOL)isPoll
|
|
isSmsMessageRestoredFromBackup:(BOOL)isSmsMessageRestoredFromBackup
|
|
isViewOnceComplete:(BOOL)isViewOnceComplete
|
|
isViewOnceMessage:(BOOL)isViewOnceMessage
|
|
linkPreview:(nullable OWSLinkPreview *)linkPreview
|
|
messageSticker:(nullable MessageSticker *)messageSticker
|
|
quotedMessage:(nullable TSQuotedMessage *)quotedMessage
|
|
storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer
|
|
storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString
|
|
storyReactionEmoji:(nullable NSString *)storyReactionEmoji
|
|
storyTimestamp:(nullable NSNumber *)storyTimestamp
|
|
wasRemotelyDeleted:(BOOL)wasRemotelyDeleted
|
|
{
|
|
self = [super initWithGrdbId:grdbId
|
|
uniqueId:uniqueId
|
|
receivedAtTimestamp:receivedAtTimestamp
|
|
sortId:sortId
|
|
timestamp:timestamp
|
|
uniqueThreadId:uniqueThreadId];
|
|
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
_body = body;
|
|
_bodyRanges = bodyRanges;
|
|
_contactShare = contactShare;
|
|
_deprecated_attachmentIds = deprecated_attachmentIds;
|
|
_editState = editState;
|
|
_expireStartedAt = expireStartedAt;
|
|
_expireTimerVersion = expireTimerVersion;
|
|
_expiresAt = expiresAt;
|
|
_expiresInSeconds = expiresInSeconds;
|
|
_giftBadge = giftBadge;
|
|
_isGroupStoryReply = isGroupStoryReply;
|
|
_isPoll = isPoll;
|
|
_isSmsMessageRestoredFromBackup = isSmsMessageRestoredFromBackup;
|
|
_isViewOnceComplete = isViewOnceComplete;
|
|
_isViewOnceMessage = isViewOnceMessage;
|
|
_linkPreview = linkPreview;
|
|
_messageSticker = messageSticker;
|
|
_quotedMessage = quotedMessage;
|
|
_storedShouldStartExpireTimer = storedShouldStartExpireTimer;
|
|
_storyAuthorUuidString = storyAuthorUuidString;
|
|
_storyReactionEmoji = storyReactionEmoji;
|
|
_storyTimestamp = storyTimestamp;
|
|
_wasRemotelyDeleted = wasRemotelyDeleted;
|
|
|
|
[self sdsFinalizeMessage];
|
|
|
|
return self;
|
|
}
|
|
|
|
// clang-format on
|
|
|
|
// --- CODE GENERATION MARKER
|
|
|
|
- (void)sdsFinalizeMessage
|
|
{
|
|
[self updateExpiresAt];
|
|
}
|
|
|
|
- (void)encodeWithCoder:(NSCoder *)coder
|
|
{
|
|
[super encodeWithCoder:coder];
|
|
NSString *body = self.body;
|
|
if (body != nil) {
|
|
[coder encodeObject:body forKey:@"body"];
|
|
}
|
|
MessageBodyRanges *bodyRanges = self.bodyRanges;
|
|
if (bodyRanges != nil) {
|
|
[coder encodeObject:bodyRanges forKey:@"bodyRanges"];
|
|
}
|
|
OWSContact *contactShare = self.contactShare;
|
|
if (contactShare != nil) {
|
|
[coder encodeObject:contactShare forKey:@"contactShare"];
|
|
}
|
|
NSArray *deprecated_attachmentIds = self.deprecated_attachmentIds;
|
|
if (deprecated_attachmentIds != nil) {
|
|
[coder encodeObject:deprecated_attachmentIds forKey:@"deprecated_attachmentIds"];
|
|
}
|
|
[coder encodeObject:[self valueForKey:@"editState"] forKey:@"editState"];
|
|
[coder encodeObject:[self valueForKey:@"expireStartedAt"] forKey:@"expireStartedAt"];
|
|
NSNumber *expireTimerVersion = self.expireTimerVersion;
|
|
if (expireTimerVersion != nil) {
|
|
[coder encodeObject:expireTimerVersion forKey:@"expireTimerVersion"];
|
|
}
|
|
[coder encodeObject:[self valueForKey:@"expiresAt"] forKey:@"expiresAt"];
|
|
[coder encodeObject:[self valueForKey:@"expiresInSeconds"] forKey:@"expiresInSeconds"];
|
|
OWSGiftBadge *giftBadge = self.giftBadge;
|
|
if (giftBadge != nil) {
|
|
[coder encodeObject:giftBadge forKey:@"giftBadge"];
|
|
}
|
|
[coder encodeObject:[self valueForKey:@"isGroupStoryReply"] forKey:@"isGroupStoryReply"];
|
|
[coder encodeObject:[self valueForKey:@"isPoll"] forKey:@"isPoll"];
|
|
[coder encodeObject:[self valueForKey:@"isSmsMessageRestoredFromBackup"] forKey:@"isSmsMessageRestoredFromBackup"];
|
|
[coder encodeObject:[self valueForKey:@"isViewOnceComplete"] forKey:@"isViewOnceComplete"];
|
|
[coder encodeObject:[self valueForKey:@"isViewOnceMessage"] forKey:@"isViewOnceMessage"];
|
|
OWSLinkPreview *linkPreview = self.linkPreview;
|
|
if (linkPreview != nil) {
|
|
[coder encodeObject:linkPreview forKey:@"linkPreview"];
|
|
}
|
|
MessageSticker *messageSticker = self.messageSticker;
|
|
if (messageSticker != nil) {
|
|
[coder encodeObject:messageSticker forKey:@"messageSticker"];
|
|
}
|
|
TSQuotedMessage *quotedMessage = self.quotedMessage;
|
|
if (quotedMessage != nil) {
|
|
[coder encodeObject:quotedMessage forKey:@"quotedMessage"];
|
|
}
|
|
[coder encodeObject:[self valueForKey:@"schemaVersion"] forKey:@"schemaVersion"];
|
|
[coder encodeObject:[self valueForKey:@"storedShouldStartExpireTimer"] forKey:@"storedShouldStartExpireTimer"];
|
|
NSString *storyAuthorUuidString = self.storyAuthorUuidString;
|
|
if (storyAuthorUuidString != nil) {
|
|
[coder encodeObject:storyAuthorUuidString forKey:@"storyAuthorUuidString"];
|
|
}
|
|
NSString *storyReactionEmoji = self.storyReactionEmoji;
|
|
if (storyReactionEmoji != nil) {
|
|
[coder encodeObject:storyReactionEmoji forKey:@"storyReactionEmoji"];
|
|
}
|
|
NSNumber *storyTimestamp = self.storyTimestamp;
|
|
if (storyTimestamp != nil) {
|
|
[coder encodeObject:storyTimestamp forKey:@"storyTimestamp"];
|
|
}
|
|
[coder encodeObject:[self valueForKey:@"wasRemotelyDeleted"] forKey:@"wasRemotelyDeleted"];
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(NSCoder *)coder
|
|
{
|
|
self = [super initWithCoder:coder];
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
self->_body = [coder decodeObjectOfClass:[NSString class] forKey:@"body"];
|
|
self->_bodyRanges = [coder decodeObjectOfClass:[MessageBodyRanges class] forKey:@"bodyRanges"];
|
|
self->_contactShare = [coder decodeObjectOfClass:[OWSContact class] forKey:@"contactShare"];
|
|
self->_deprecated_attachmentIds =
|
|
[coder decodeObjectOfClasses:[NSSet setWithArray:@[ [NSArray class], [NSString class] ]]
|
|
forKey:@"deprecated_attachmentIds"];
|
|
self->_editState = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"editState"] integerValue];
|
|
self->_expireStartedAt = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"expireStartedAt"] unsignedLongLongValue];
|
|
self->_expireTimerVersion = [coder decodeObjectOfClass:[NSNumber class] forKey:@"expireTimerVersion"];
|
|
self->_expiresAt = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"expiresAt"] unsignedLongLongValue];
|
|
self->_expiresInSeconds = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"expiresInSeconds"] unsignedIntValue];
|
|
self->_giftBadge = [coder decodeObjectOfClass:[OWSGiftBadge class] forKey:@"giftBadge"];
|
|
self->_isGroupStoryReply = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"isGroupStoryReply"] boolValue];
|
|
self->_isPoll = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"isPoll"] boolValue];
|
|
self->_isSmsMessageRestoredFromBackup =
|
|
[(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"isSmsMessageRestoredFromBackup"] boolValue];
|
|
self->_isViewOnceComplete = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"isViewOnceComplete"] boolValue];
|
|
self->_isViewOnceMessage = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"isViewOnceMessage"] boolValue];
|
|
self->_linkPreview = [coder decodeObjectOfClass:[OWSLinkPreview class] forKey:@"linkPreview"];
|
|
self->_messageSticker = [coder decodeObjectOfClass:[MessageSticker class] forKey:@"messageSticker"];
|
|
self->_quotedMessage = [coder decodeObjectOfClass:[TSQuotedMessage class] forKey:@"quotedMessage"];
|
|
self->_schemaVersion = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"schemaVersion"] unsignedIntegerValue];
|
|
self->_storedShouldStartExpireTimer =
|
|
[(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"storedShouldStartExpireTimer"] boolValue];
|
|
self->_storyAuthorUuidString = [coder decodeObjectOfClass:[NSString class] forKey:@"storyAuthorUuidString"];
|
|
self->_storyReactionEmoji = [coder decodeObjectOfClass:[NSString class] forKey:@"storyReactionEmoji"];
|
|
self->_storyTimestamp = [coder decodeObjectOfClass:[NSNumber class] forKey:@"storyTimestamp"];
|
|
self->_wasRemotelyDeleted = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"wasRemotelyDeleted"] boolValue];
|
|
|
|
if (_schemaVersion < 2) {
|
|
// renamed _attachments to _attachmentIds
|
|
if (!_deprecated_attachmentIds) {
|
|
_deprecated_attachmentIds =
|
|
[coder decodeObjectOfClasses:[NSSet setWithArray:@[ [NSArray class], [NSString class] ]]
|
|
forKey:@"attachments"];
|
|
}
|
|
}
|
|
|
|
if (_schemaVersion < 3) {
|
|
_expiresInSeconds = 0;
|
|
_expireStartedAt = 0;
|
|
_expiresAt = 0;
|
|
}
|
|
|
|
if (_schemaVersion < 4) {
|
|
// Wipe out the body field on these legacy attachment messages.
|
|
//
|
|
// Explanation: Historically, a message sent from iOS could be an attachment XOR a text message,
|
|
// but now we support sending an attachment+caption as a single message.
|
|
//
|
|
// Other clients have supported sending attachment+caption in a single message for a long time.
|
|
// So the way we used to handle receiving them was to make it look like they'd sent two messages:
|
|
// first the attachment+caption (we'd ignore this caption when rendering), followed by a separate
|
|
// message with just the caption (which we'd render as a simple independent text message), for
|
|
// which we'd offset the timestamp by a little bit to get the desired ordering.
|
|
//
|
|
// Now that we can properly render an attachment+caption message together, these legacy "dummy" text
|
|
// messages are not only unnecessary, but worse, would be rendered redundantly. For safety, rather
|
|
// than building the logic to try to find and delete the redundant "dummy" text messages which users
|
|
// have been seeing and interacting with, we delete the body field from the attachment message,
|
|
// which iOS users have never seen directly.
|
|
if (_deprecated_attachmentIds.count > 0) {
|
|
_body = nil;
|
|
}
|
|
}
|
|
|
|
_schemaVersion = OWSMessageSchemaVersion;
|
|
|
|
// Upgrades legacy messages.
|
|
//
|
|
// TODO: We can eventually remove this migration since
|
|
// per-message expiration was never released to
|
|
// production.
|
|
NSNumber *_Nullable perMessageExpirationDurationSeconds =
|
|
[coder decodeObjectOfClass:[NSNumber class] forKey:@"perMessageExpirationDurationSeconds"];
|
|
if (perMessageExpirationDurationSeconds.unsignedIntegerValue > 0) {
|
|
_isViewOnceMessage = YES;
|
|
}
|
|
NSNumber *_Nullable perMessageExpirationHasExpired = [coder decodeObjectOfClass:[NSNumber class]
|
|
forKey:@"perMessageExpirationHasExpired"];
|
|
if (perMessageExpirationHasExpired.boolValue > 0) {
|
|
_isViewOnceComplete = YES;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (NSUInteger)hash
|
|
{
|
|
NSUInteger result = [super hash];
|
|
result ^= self.body.hash;
|
|
result ^= self.bodyRanges.hash;
|
|
result ^= self.contactShare.hash;
|
|
result ^= self.deprecated_attachmentIds.hash;
|
|
result ^= (NSUInteger)self.editState;
|
|
result ^= self.expireStartedAt;
|
|
result ^= self.expireTimerVersion.hash;
|
|
result ^= self.expiresAt;
|
|
result ^= self.expiresInSeconds;
|
|
result ^= self.giftBadge.hash;
|
|
result ^= self.isGroupStoryReply;
|
|
result ^= self.isPoll;
|
|
result ^= self.isSmsMessageRestoredFromBackup;
|
|
result ^= self.isViewOnceComplete;
|
|
result ^= self.isViewOnceMessage;
|
|
result ^= self.linkPreview.hash;
|
|
result ^= self.messageSticker.hash;
|
|
result ^= self.quotedMessage.hash;
|
|
result ^= self.schemaVersion;
|
|
result ^= self.storedShouldStartExpireTimer;
|
|
result ^= self.storyAuthorUuidString.hash;
|
|
result ^= self.storyReactionEmoji.hash;
|
|
result ^= self.storyTimestamp.hash;
|
|
result ^= self.wasRemotelyDeleted;
|
|
return result;
|
|
}
|
|
|
|
- (BOOL)isEqual:(id)other
|
|
{
|
|
if (![super isEqual:other]) {
|
|
return NO;
|
|
}
|
|
TSMessage *typedOther = (TSMessage *)other;
|
|
if (![NSObject isObject:self.body equalToObject:typedOther.body]) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.bodyRanges equalToObject:typedOther.bodyRanges]) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.contactShare equalToObject:typedOther.contactShare]) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.deprecated_attachmentIds equalToObject:typedOther.deprecated_attachmentIds]) {
|
|
return NO;
|
|
}
|
|
if (self.editState != typedOther.editState) {
|
|
return NO;
|
|
}
|
|
if (self.expireStartedAt != typedOther.expireStartedAt) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.expireTimerVersion equalToObject:typedOther.expireTimerVersion]) {
|
|
return NO;
|
|
}
|
|
if (self.expiresAt != typedOther.expiresAt) {
|
|
return NO;
|
|
}
|
|
if (self.expiresInSeconds != typedOther.expiresInSeconds) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.giftBadge equalToObject:typedOther.giftBadge]) {
|
|
return NO;
|
|
}
|
|
if (self.isGroupStoryReply != typedOther.isGroupStoryReply) {
|
|
return NO;
|
|
}
|
|
if (self.isPoll != typedOther.isPoll) {
|
|
return NO;
|
|
}
|
|
if (self.isSmsMessageRestoredFromBackup != typedOther.isSmsMessageRestoredFromBackup) {
|
|
return NO;
|
|
}
|
|
if (self.isViewOnceComplete != typedOther.isViewOnceComplete) {
|
|
return NO;
|
|
}
|
|
if (self.isViewOnceMessage != typedOther.isViewOnceMessage) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.linkPreview equalToObject:typedOther.linkPreview]) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.messageSticker equalToObject:typedOther.messageSticker]) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.quotedMessage equalToObject:typedOther.quotedMessage]) {
|
|
return NO;
|
|
}
|
|
if (self.schemaVersion != typedOther.schemaVersion) {
|
|
return NO;
|
|
}
|
|
if (self.storedShouldStartExpireTimer != typedOther.storedShouldStartExpireTimer) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.storyAuthorUuidString equalToObject:typedOther.storyAuthorUuidString]) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.storyReactionEmoji equalToObject:typedOther.storyReactionEmoji]) {
|
|
return NO;
|
|
}
|
|
if (![NSObject isObject:self.storyTimestamp equalToObject:typedOther.storyTimestamp]) {
|
|
return NO;
|
|
}
|
|
if (self.wasRemotelyDeleted != typedOther.wasRemotelyDeleted) {
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (void)setExpireStartedAt:(uint64_t)expireStartedAt
|
|
{
|
|
if (_expireStartedAt != 0 && _expireStartedAt < expireStartedAt) {
|
|
return;
|
|
}
|
|
|
|
uint64_t now = [NSDate ows_millisecondTimeStamp];
|
|
if (expireStartedAt > now) {
|
|
OWSLogWarn(@"using `now` instead of future time");
|
|
}
|
|
|
|
_expireStartedAt = MIN(now, expireStartedAt);
|
|
|
|
[self updateExpiresAt];
|
|
}
|
|
|
|
// This method will be called after every insert and update, so it needs
|
|
// to be cheap.
|
|
- (BOOL)shouldStartExpireTimer
|
|
{
|
|
if (self.hasPerConversationExpirationStarted) {
|
|
// Expiration already started.
|
|
return YES;
|
|
}
|
|
|
|
return self.hasPerConversationExpiration;
|
|
}
|
|
|
|
- (void)updateExpiresAt
|
|
{
|
|
if (self.hasPerConversationExpirationStarted) {
|
|
_expiresAt = _expireStartedAt + (uint64_t)_expiresInSeconds * 1000;
|
|
} else {
|
|
_expiresAt = 0;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Story Context
|
|
|
|
- (nullable AciObjC *)storyAuthorAci
|
|
{
|
|
return [[AciObjC alloc] initWithAciString:self.storyAuthorUuidString];
|
|
}
|
|
|
|
- (BOOL)isStoryReply
|
|
{
|
|
return self.storyAuthorUuidString != nil;
|
|
}
|
|
|
|
- (NSString *)debugDescription
|
|
{
|
|
return [NSString stringWithFormat:@"%@ with body: %@ has mentions: %@",
|
|
[self class],
|
|
self.body,
|
|
self.bodyRanges.hasMentions ? @"YES" : @"NO"];
|
|
}
|
|
|
|
|
|
- (void)anyWillInsertWithTransaction:(DBWriteTransaction *)transaction
|
|
{
|
|
[super anyWillInsertWithTransaction:transaction];
|
|
|
|
[self insertMentionsInDatabaseWithTx:transaction];
|
|
}
|
|
|
|
- (void)anyDidInsertWithTransaction:(DBWriteTransaction *)transaction
|
|
{
|
|
[super anyDidInsertWithTransaction:transaction];
|
|
|
|
[self _anyDidInsertWithTx:transaction];
|
|
|
|
[self ensurePerConversationExpirationWithTransaction:transaction];
|
|
|
|
[self touchStoryMessageIfNecessaryWithReplyCountIncrement:ReplyCountIncrementNewReplyAdded transaction:transaction];
|
|
}
|
|
|
|
- (void)anyWillUpdateWithTransaction:(DBWriteTransaction *)transaction
|
|
{
|
|
[super anyWillUpdateWithTransaction:transaction];
|
|
}
|
|
|
|
- (void)anyDidUpdateWithTransaction:(DBWriteTransaction *)transaction
|
|
{
|
|
[super anyDidUpdateWithTransaction:transaction];
|
|
|
|
[self _anyDidUpdateWithTx:transaction];
|
|
|
|
[self ensurePerConversationExpirationWithTransaction:transaction];
|
|
|
|
[self touchStoryMessageIfNecessaryWithReplyCountIncrement:ReplyCountIncrementNoIncrement transaction:transaction];
|
|
}
|
|
|
|
- (void)ensurePerConversationExpirationWithTransaction:(DBWriteTransaction *)transaction
|
|
{
|
|
if (self.hasPerConversationExpirationStarted) {
|
|
// Expiration already started.
|
|
return;
|
|
}
|
|
if (![self shouldStartExpireTimer]) {
|
|
return;
|
|
}
|
|
uint64_t nowMs = [NSDate ows_millisecondTimeStamp];
|
|
[DisappearingMessagesExpirationJobObjcBridge startExpirationForMessage:self
|
|
expirationStartedAt:nowMs
|
|
tx:transaction];
|
|
}
|
|
|
|
- (BOOL)hasPerConversationExpiration
|
|
{
|
|
return self.expiresInSeconds > 0;
|
|
}
|
|
|
|
- (BOOL)hasPerConversationExpirationStarted
|
|
{
|
|
return _expireStartedAt > 0 && _expiresInSeconds > 0;
|
|
}
|
|
|
|
- (BOOL)shouldUseReceiptDateForSorting
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (nullable NSString *)body
|
|
{
|
|
if (self.isPoll) {
|
|
return _body;
|
|
}
|
|
|
|
return _body.filterStringForDisplay;
|
|
}
|
|
|
|
#pragma mark - Update With... Methods
|
|
|
|
- (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(expireStartedAt > 0);
|
|
OWSAssertDebug(self.expiresInSeconds > 0);
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction
|
|
block:^(TSMessage *message) { [message setExpireStartedAt:expireStartedAt]; }];
|
|
}
|
|
|
|
- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(linkPreview);
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction
|
|
block:^(TSMessage *message) { [message setLinkPreview:linkPreview]; }];
|
|
}
|
|
|
|
- (void)updateWithQuotedMessage:(TSQuotedMessage *)quotedMessage transaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(quotedMessage);
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction
|
|
block:^(TSMessage *message) { [message setQuotedMessage:quotedMessage]; }];
|
|
}
|
|
|
|
- (void)updateWithMessageSticker:(MessageSticker *)messageSticker transaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(messageSticker);
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction
|
|
block:^(TSMessage *message) { message.messageSticker = messageSticker; }];
|
|
}
|
|
|
|
- (void)updateWithContactShare:(OWSContact *)contactShare transaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(contactShare);
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction
|
|
block:^(TSMessage *message) { message.contactShare = contactShare; }];
|
|
}
|
|
|
|
- (void)updateWithIsPoll:(BOOL)isPoll transaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction block:^(TSMessage *message) { message.isPoll = isPoll; }];
|
|
}
|
|
|
|
#ifdef TESTABLE_BUILD
|
|
|
|
// This method is for testing purposes only.
|
|
- (void)updateWithMessageBody:(nullable NSString *)messageBody transaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction block:^(TSMessage *message) { message.body = messageBody; }];
|
|
}
|
|
|
|
#endif
|
|
|
|
#pragma mark - View Once
|
|
|
|
- (void)updateWithViewOnceCompleteAndRemoveRenderableContentWithTransaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
OWSAssertDebug(self.isViewOnceMessage);
|
|
OWSAssertDebug(!self.isViewOnceComplete);
|
|
|
|
[self removeAllRenderableContentWithTransaction:transaction
|
|
messageUpdateBlock:^(TSMessage *message) { message.isViewOnceComplete = YES; }];
|
|
}
|
|
|
|
#pragma mark - Remote Delete
|
|
|
|
- (void)updateWithRemotelyDeletedAndRemoveRenderableContentWithTransaction:(DBWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(transaction);
|
|
OWSAssertDebug(!self.wasRemotelyDeleted);
|
|
|
|
[self removeAllReactionsWithTransaction:transaction];
|
|
|
|
[self unpinMessageIfNeededWithTx:transaction];
|
|
|
|
[self removeAllRenderableContentWithTransaction:transaction
|
|
messageUpdateBlock:^(TSMessage *message) { message.wasRemotelyDeleted = YES; }];
|
|
}
|
|
|
|
#pragma mark - Remove Renderable Content
|
|
|
|
- (void)removeAllRenderableContentWithTransaction:(DBWriteTransaction *)transaction
|
|
messageUpdateBlock:(void (^)(TSMessage *message))messageUpdateBlock
|
|
{
|
|
// We call removeAllAttachmentsWithTransaction() before
|
|
// anyUpdateWithTransaction, because anyUpdateWithTransaction's
|
|
// block can be called twice, once on this instance and once
|
|
// on the copy from the database. We only want to remove
|
|
// attachments once.
|
|
[self removeAllAttachmentsWithTx:transaction];
|
|
[self removeAllMentionsWithTransaction:transaction];
|
|
[MessageSendLogObjC deleteAllPayloadsForInteraction:self tx:transaction];
|
|
|
|
[self anyUpdateMessageWithTransaction:transaction
|
|
block:^(TSMessage *message) {
|
|
// Remove renderable content.
|
|
message.body = nil;
|
|
message.bodyRanges = nil;
|
|
message.contactShare = nil;
|
|
message.quotedMessage = nil;
|
|
message.linkPreview = nil;
|
|
message.messageSticker = nil;
|
|
message.storyReactionEmoji = nil;
|
|
|
|
messageUpdateBlock(message);
|
|
}];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|