diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index a568443c1..fb8c89142 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4847,9 +4847,11 @@ typedef enum : NSUInteger { } }]; - // Update the "shouldShowDate" property of the view items. + // Update the "break" properties (shouldShowDate and unreadIndicator) of the view items. BOOL shouldShowDateOnNextViewItem = YES; uint64_t previousViewItemTimestamp = 0; + OWSUnreadIndicator *_Nullable unreadIndicator = self.dynamicInteractions.unreadIndicator; + BOOL hasPlacedUnreadIndicator = NO; for (ConversationViewItem *viewItem in viewItems) { BOOL canShowDate = NO; switch (viewItem.interaction.interactionType) { @@ -4884,12 +4886,47 @@ typedef enum : NSUInteger { viewItem.shouldShowDate = shouldShowDate; previousViewItemTimestamp = viewItemTimestamp; + + // When a conversation without unread messages receives an incoming message, + // we call ensureDynamicInteractions to ensure that the unread indicator (etc.) + // state is updated accordingly. However this is done in a separate transaction. + // We don't want to show the incoming message _without_ an unread indicator and + // then immediately re-render it _with_ an unread indicator. + // + // To avoid this, we use a temporary instance of OWSUnreadIndicator whenever + // we find an unread message that _should_ have an unread indicator, but no + // unread indicator exists yet on dynamicInteractions. + BOOL isItemUnread = ([viewItem.interaction conformsToProtocol:@protocol(OWSReadTracking)] + && !((id)viewItem.interaction).wasRead); + if (isItemUnread && !unreadIndicator && !hasPlacedUnreadIndicator) { + + unreadIndicator = + [[OWSUnreadIndicator alloc] initUnreadIndicatorWithTimestamp:viewItem.interaction.timestamp + hasMoreUnseenMessages:NO + missingUnseenSafetyNumberChangeCount:0 + unreadIndicatorPosition:0 + firstUnseenInteractionTimestamp:viewItem.interaction.timestamp]; + } + + // Place the unread indicator onto the first appropriate view item, + // if any. + if (unreadIndicator && viewItem.interaction.timestampForSorting >= unreadIndicator.timestamp) { + viewItem.unreadIndicator = unreadIndicator; + unreadIndicator = nil; + hasPlacedUnreadIndicator = YES; + } else { + viewItem.unreadIndicator = nil; + } + } + if (unreadIndicator) { + // This isn't necessarily a bug - all of the interactions after the + // unread indicator may have disappeared or been deleted. + DDLogWarn(@"%@ Couldn't find an interaction to hang the unread indicator on.", self.logTag); } // Update the properties of the view items. // - // NOTE: This logic uses shouldShowDate which is set in the previous pass. - OWSUnreadIndicator *_Nullable unreadIndicator = self.dynamicInteractions.unreadIndicator; + // NOTE: This logic uses the break properties which are set in the previous pass. for (NSUInteger i = 0; i < viewItems.count; i++) { ConversationViewItem *viewItem = viewItems[i]; ConversationViewItem *_Nullable previousViewItem = (i > 0 ? viewItems[i - 1] : nil); @@ -4900,15 +4937,6 @@ typedef enum : NSUInteger { BOOL isLastInCluster = YES; NSAttributedString *_Nullable senderName = nil; - // Place the unread indicator onto the first appropriate view item, - // if any. - if (unreadIndicator && viewItem.interaction.timestampForSorting >= unreadIndicator.timestamp) { - viewItem.unreadIndicator = unreadIndicator; - unreadIndicator = nil; - } else { - viewItem.unreadIndicator = nil; - } - OWSInteractionType interactionType = viewItem.interaction.interactionType; NSString *timestampText = [DateUtil formatTimestampShort:viewItem.interaction.timestamp]; @@ -4930,14 +4958,14 @@ typedef enum : NSUInteger { // ...and always show the "disappearing messages" animation. shouldHideFooter = ([timestampText isEqualToString:nextTimestampText] && receiptStatus == nextReceiptStatus - && outgoingMessage.messageState != TSOutgoingMessageStateFailed && !nextViewItem.shouldShowDate + && outgoingMessage.messageState != TSOutgoingMessageStateFailed && !nextViewItem.hasCellHeader && !isDisappearingMessage); } // clustering if (previousViewItem == nil) { isFirstInCluster = YES; - } else if (viewItem.shouldShowDate) { + } else if (viewItem.hasCellHeader) { isFirstInCluster = YES; } else { isFirstInCluster = previousViewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage; @@ -4945,7 +4973,7 @@ typedef enum : NSUInteger { if (nextViewItem == nil) { isLastInCluster = YES; - } else if (nextViewItem.shouldShowDate) { + } else if (nextViewItem.hasCellHeader) { isLastInCluster = YES; } else { isLastInCluster = nextViewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage; @@ -4969,7 +4997,7 @@ typedef enum : NSUInteger { // We can skip the "incoming message status" footer in a cluster if the next message // has the same footer and no "date break" separates us. // ...but always show the "disappearing messages" animation. - shouldHideFooter = ([timestampText isEqualToString:nextTimestampText] && !nextViewItem.shouldShowDate && + shouldHideFooter = ([timestampText isEqualToString:nextTimestampText] && !nextViewItem.hasCellHeader && [NSObject isNullableObject:nextIncomingSenderId equalTo:incomingSenderId] && !isDisappearingMessage); } @@ -4977,7 +5005,7 @@ typedef enum : NSUInteger { // clustering if (previousViewItem == nil) { isFirstInCluster = YES; - } else if (viewItem.shouldShowDate) { + } else if (viewItem.hasCellHeader) { isFirstInCluster = YES; } else if (previousViewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) { isFirstInCluster = YES; @@ -4990,7 +5018,7 @@ typedef enum : NSUInteger { isLastInCluster = YES; } else if (nextViewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) { isLastInCluster = YES; - } else if (nextViewItem.shouldShowDate) { + } else if (nextViewItem.hasCellHeader) { isLastInCluster = YES; } else { TSIncomingMessage *nextIncomingMessage = (TSIncomingMessage *)nextViewItem.interaction; @@ -5010,7 +5038,7 @@ typedef enum : NSUInteger { shouldShowSenderName = (![NSObject isNullableObject:previousIncomingSenderId equalTo:incomingSenderId] - || viewItem.shouldShowDate); + || viewItem.hasCellHeader); } if (shouldShowSenderName) { senderName = [self.contactsManager @@ -5027,7 +5055,7 @@ typedef enum : NSUInteger { shouldShowSenderAvatar = YES; if (nextViewItem && nextViewItem.interaction.interactionType == interactionType) { shouldShowSenderAvatar = (![NSObject isNullableObject:nextIncomingSenderId equalTo:incomingSenderId] - || nextViewItem.shouldShowDate); + || nextViewItem.hasCellHeader); } } } @@ -5038,11 +5066,6 @@ typedef enum : NSUInteger { viewItem.shouldHideFooter = shouldHideFooter; viewItem.senderName = senderName; } - if (unreadIndicator) { - // This isn't necessarily a bug - all of the interactions after the - // unread indicator may have disappeared or been deleted. - DDLogWarn(@"%@ Couldn't find an interaction to hang the unread indicator on.", self.logTag); - } self.viewItems = viewItems; self.viewItemCache = viewItemCache;