diff --git a/SignalServiceKit/Environment/AppSetup.swift b/SignalServiceKit/Environment/AppSetup.swift index 19c863f3071..24cc01be9b6 100644 --- a/SignalServiceKit/Environment/AppSetup.swift +++ b/SignalServiceKit/Environment/AppSetup.swift @@ -1136,6 +1136,7 @@ public class AppSetup { plaintextStreamProvider: MessageBackupPlaintextProtoStreamProviderImpl(), postFrameRestoreActionManager: MessageBackupPostFrameRestoreActionManager( interactionStore: backupInteractionStore, + lastVisibleInteractionStore: lastVisibleInteractionStore, recipientDatabaseTable: recipientDatabaseTable, sskPreferences: MessageBackupPostFrameRestoreActionManager.Wrappers.SSKPreferences(), threadStore: backupThreadStore diff --git a/SignalServiceKit/MessageBackup/Archivers/Chat/ChatContexts.swift b/SignalServiceKit/MessageBackup/Archivers/Chat/ChatContexts.swift index 360533577e6..d4ab16b112e 100644 --- a/SignalServiceKit/MessageBackup/Archivers/Chat/ChatContexts.swift +++ b/SignalServiceKit/MessageBackup/Archivers/Chat/ChatContexts.swift @@ -213,6 +213,7 @@ extension MessageBackup { public struct PostFrameRestoreActions { var isPinned: Bool var lastVisibleInteractionRowId: Int64? + var hadAnyUnreadMessages: Bool = false var shouldBeMarkedVisible: Bool { isPinned || lastVisibleInteractionRowId != nil @@ -234,6 +235,7 @@ extension MessageBackup { func updateLastVisibleInteractionRowId( interactionRowId: Int64, + wasRead: Bool, chatId: ChatId ) { var actions = postFrameRestoreActions[chatId] ?? .default @@ -245,6 +247,7 @@ extension MessageBackup { { actions.lastVisibleInteractionRowId = interactionRowId } + actions.hadAnyUnreadMessages = actions.hadAnyUnreadMessages || !wasRead postFrameRestoreActions[chatId] = actions } } diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupGroupCallArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupGroupCallArchiver.swift index 02e28534949..22bce9cfbaa 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupGroupCallArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupGroupCallArchiver.swift @@ -147,11 +147,20 @@ final class MessageBackupGroupCallArchiver { thread: groupThread, sentAtTimestamp: chatItem.dateSent ) + + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + do { try interactionStore.insert( groupCallInteraction, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupIndividualCallArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupIndividualCallArchiver.swift index 0eaf829d4c0..4dba92d32c9 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupIndividualCallArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/Calls/MessageBackupIndividualCallArchiver.swift @@ -188,11 +188,20 @@ final class MessageBackupIndividualCallArchiver { thread: contactThread, sentAtTimestamp: chatItem.dateSent ) + + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + do { try interactionStore.insert( individualCallInteraction, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift index 17ebabacdf7..401c4677032 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/GroupUpdates/MessageBackupGroupUpdateMessageArchiver.swift @@ -223,11 +223,20 @@ final class MessageBackupGroupUpdateMessageArchiver { groupThread: groupThread, updateItems: persistableUpdates ) + + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + do { try interactionStore.insert( infoMessage, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupExpirationTimerChatUpdateArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupExpirationTimerChatUpdateArchiver.swift index 2ab4be41ac2..78e8b868e05 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupExpirationTimerChatUpdateArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupExpirationTimerChatUpdateArchiver.swift @@ -144,11 +144,17 @@ final class MessageBackupExpirationTimerChatUpdateArchiver { configurationDurationSeconds: UInt32(clamping: expiresInSeconds), // Safe to clamp, we checked for overflow above createdByRemoteName: createdByRemoteName ) + + guard let directionalDetails = chatItem.directionalDetails else { + return invalidProtoData(.chatItemMissingDirectionalDetails) + } + do { try interactionStore.insert( dmUpdateInfoMessage, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupLearnedProfileChatUpdateArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupLearnedProfileChatUpdateArchiver.swift index 4db2d4e72d9..c63d8e81191 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupLearnedProfileChatUpdateArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupLearnedProfileChatUpdateArchiver.swift @@ -112,11 +112,20 @@ final class MessageBackupLearnedProfileChatUpdateArchiver { timestamp: chatItem.dateSent, displayNameBefore: displayNameBefore ) + + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + do { try interactionStore.insert( learnedProfileKeyInfoMessage, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupProfileChangeChatUpdateArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupProfileChangeChatUpdateArchiver.swift index 986c29fb1bd..46fe8b93b57 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupProfileChangeChatUpdateArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupProfileChangeChatUpdateArchiver.swift @@ -114,11 +114,19 @@ final class MessageBackupProfileChangeChatUpdateArchiver { ) ) + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + do { try interactionStore.insert( profileChangeInfoMessage, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSessionSwitchoverChatUpdateArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSessionSwitchoverChatUpdateArchiver.swift index e2c6d010e67..121dc7c3a8b 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSessionSwitchoverChatUpdateArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSessionSwitchoverChatUpdateArchiver.swift @@ -103,11 +103,19 @@ final class MessageBackupSessionSwitchoverChatUpdateArchiver { phoneNumber: e164.stringValue ) + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + do { try interactionStore.insert( sessionSwitchoverInfoMessage, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSimpleChatUpdateArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSimpleChatUpdateArchiver.swift index cddd7143311..8518ab91972 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSimpleChatUpdateArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupSimpleChatUpdateArchiver.swift @@ -564,28 +564,55 @@ final class MessageBackupSimpleChatUpdateArchiver { simpleChatUpdateInteraction = .simpleInfoMessage(.acceptedMessageRequest) } - let interactionToInsert: TSInteraction = switch simpleChatUpdateInteraction { + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + + switch simpleChatUpdateInteraction { case .simpleInfoMessage(let infoMessageType): - TSInfoMessage( + let infoMessage = TSInfoMessage( thread: thread, messageType: infoMessageType, timestamp: chatItem.dateSent ) + do { + try interactionStore.insert( + infoMessage, + in: chatThread, + chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, + context: context + ) + } catch let error { + return .messageFailure([.restoreFrameError(.databaseInsertionFailed(error), chatItem.id)]) + } case .prebuiltInfoMessage(let infoMessage): - infoMessage + do { + try interactionStore.insert( + infoMessage, + in: chatThread, + chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, + context: context + ) + } catch let error { + return .messageFailure([.restoreFrameError(.databaseInsertionFailed(error), chatItem.id)]) + } case .errorMessage(let errorMessage): - errorMessage - } - - do { - try interactionStore.insert( - interactionToInsert, - in: chatThread, - chatId: chatItem.typedChatId, - context: context - ) - } catch let error { - return .messageFailure([.restoreFrameError(.databaseInsertionFailed(error), chatItem.id)]) + do { + try interactionStore.insert( + errorMessage, + in: chatThread, + chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, + context: context + ) + } catch let error { + return .messageFailure([.restoreFrameError(.databaseInsertionFailed(error), chatItem.id)]) + } } return .success(()) diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupThreadMergeChatUpdateArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupThreadMergeChatUpdateArchiver.swift index 9eaec8f731c..380fd697c06 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupThreadMergeChatUpdateArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/ChatUpdateMessages/MessageBackupThreadMergeChatUpdateArchiver.swift @@ -103,11 +103,19 @@ final class MessageBackupThreadMergeChatUpdateArchiver { previousE164: previousE164.stringValue ) + guard let directionalDetails = chatItem.directionalDetails else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.chatItemMissingDirectionalDetails), + chatItem.id + )]) + } + do { try interactionStore.insert( threadMergeInfoMessage, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: directionalDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift index 5ae45f4c0dc..59063a11e68 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupInteractionStore.swift @@ -30,13 +30,127 @@ public final class MessageBackupInteractionStore { {} } + // MARK: Per type inserts + + func insert( + _ interaction: TSIncomingMessage, + in thread: MessageBackup.ChatThread, + chatId: MessageBackup.ChatId, + directionalDetails: BackupProto_ChatItem.IncomingMessageDetails, + context: MessageBackup.ChatItemRestoringContext + ) throws { + let wasRead = BackupProto_ChatItem.OneOf_DirectionalDetails + .incoming(directionalDetails).wasRead + interaction.wasRead = wasRead + try insert( + interaction: interaction, + in: thread, + chatId: chatId, + wasRead: wasRead, + context: context + ) + } + + func insert( + _ interaction: TSOutgoingMessage, + in thread: MessageBackup.ChatThread, + chatId: MessageBackup.ChatId, + directionalDetails: BackupProto_ChatItem.OutgoingMessageDetails, + context: MessageBackup.ChatItemRestoringContext + ) throws { + let wasRead = BackupProto_ChatItem.OneOf_DirectionalDetails + .outgoing(directionalDetails).wasRead + try insert( + interaction: interaction, + in: thread, + chatId: chatId, + wasRead: wasRead, + context: context + ) + } + + func insert( + _ interaction: TSInfoMessage, + in thread: MessageBackup.ChatThread, + chatId: MessageBackup.ChatId, + directionalDetails: BackupProto_ChatItem.OneOf_DirectionalDetails, + context: MessageBackup.ChatItemRestoringContext + ) throws { + let wasRead = directionalDetails.wasRead + interaction.wasRead = wasRead + try insert( + interaction: interaction, + in: thread, + chatId: chatId, + wasRead: wasRead, + context: context + ) + } + + func insert( + _ interaction: TSErrorMessage, + in thread: MessageBackup.ChatThread, + chatId: MessageBackup.ChatId, + directionalDetails: BackupProto_ChatItem.OneOf_DirectionalDetails, + context: MessageBackup.ChatItemRestoringContext + ) throws { + let wasRead = directionalDetails.wasRead + interaction.wasRead = wasRead + try insert( + interaction: interaction, + in: thread, + chatId: chatId, + wasRead: wasRead, + context: context + ) + } + + func insert( + _ interaction: TSCall, + in thread: MessageBackup.ChatThread, + chatId: MessageBackup.ChatId, + directionalDetails: BackupProto_ChatItem.OneOf_DirectionalDetails, + context: MessageBackup.ChatItemRestoringContext + ) throws { + let wasRead = directionalDetails.wasRead + interaction.wasRead = wasRead + try insert( + interaction: interaction, + in: thread, + chatId: chatId, + wasRead: wasRead, + context: context + ) + } + + func insert( + _ interaction: OWSGroupCallMessage, + in thread: MessageBackup.ChatThread, + chatId: MessageBackup.ChatId, + directionalDetails: BackupProto_ChatItem.OneOf_DirectionalDetails, + context: MessageBackup.ChatItemRestoringContext + ) throws { + let wasRead = directionalDetails.wasRead + interaction.wasRead = wasRead + try insert( + interaction: interaction, + in: thread, + chatId: chatId, + wasRead: wasRead, + context: context + ) + } + + // MARK: Insert + // Even generating the sql string itself is expensive when multiplied by 200k messages. // So we generate the string once and cache it (on top of caching the Statement) private var cachedSQL: String? - func insert( - _ interaction: TSInteraction, + private func insert( + interaction: TSInteraction, in thread: MessageBackup.ChatThread, chatId: MessageBackup.ChatId, + wasRead: Bool, context: MessageBackup.ChatItemRestoringContext ) throws { guard interaction.shouldBeSaved else { @@ -95,8 +209,26 @@ public final class MessageBackupInteractionStore { if shouldAppearInInbox { context.chatContext.updateLastVisibleInteractionRowId( interactionRowId: interactionRowId, + wasRead: wasRead, chatId: chatId ) } } } + +extension BackupProto_ChatItem.OneOf_DirectionalDetails { + + var wasRead: Bool { + switch self { + case .incoming(let incomingMessageDetails): + return incomingMessageDetails.read + case .outgoing: + // Outgoing messages are always implicitly read + return true + case .directionless: + // Since we don't track read state for directionless + // messages, just treat them as read. + return true + } + } +} diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSIncomingMessageArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSIncomingMessageArchiver.swift index 1ca6a85bc13..d2c13162d64 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSIncomingMessageArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSIncomingMessageArchiver.swift @@ -336,6 +336,7 @@ extension MessageBackupTSIncomingMessageArchiver: MessageBackupTSMessageEditHist message, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: incomingDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSOutgoingMessageArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSOutgoingMessageArchiver.swift index 5040275e7d3..d0ff58ad439 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSOutgoingMessageArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSOutgoingMessageArchiver.swift @@ -530,6 +530,7 @@ extension MessageBackupTSOutgoingMessageArchiver: MessageBackupTSMessageEditHist outgoingMessage, in: chatThread, chatId: chatItem.typedChatId, + directionalDetails: outgoingDetails, context: context ) } catch let error { diff --git a/SignalServiceKit/MessageBackup/Archivers/MessageBackupPostFrameRestoreActionManager.swift b/SignalServiceKit/MessageBackup/Archivers/MessageBackupPostFrameRestoreActionManager.swift index 6cd57d6c624..da1b52630a8 100644 --- a/SignalServiceKit/MessageBackup/Archivers/MessageBackupPostFrameRestoreActionManager.swift +++ b/SignalServiceKit/MessageBackup/Archivers/MessageBackupPostFrameRestoreActionManager.swift @@ -11,17 +11,20 @@ public class MessageBackupPostFrameRestoreActionManager { typealias ChatActions = MessageBackup.ChatRestoringContext.PostFrameRestoreActions private let interactionStore: MessageBackupInteractionStore + private let lastVisibleInteractionStore: LastVisibleInteractionStore private let recipientDatabaseTable: RecipientDatabaseTable private let sskPreferences: Shims.SSKPreferences private let threadStore: MessageBackupThreadStore init( interactionStore: MessageBackupInteractionStore, + lastVisibleInteractionStore: LastVisibleInteractionStore, recipientDatabaseTable: RecipientDatabaseTable, sskPreferences: Shims.SSKPreferences, threadStore: MessageBackupThreadStore ) { self.interactionStore = interactionStore + self.lastVisibleInteractionStore = lastVisibleInteractionStore self.recipientDatabaseTable = recipientDatabaseTable self.sskPreferences = sskPreferences self.threadStore = threadStore @@ -56,6 +59,23 @@ public class MessageBackupPostFrameRestoreActionManager { context: chatItemContext.chatContext ) } + if + let lastVisibleInteractionRowId = actions.lastVisibleInteractionRowId, + let lastVisibleInteractionRowId = UInt64(exactly: lastVisibleInteractionRowId), + !actions.hadAnyUnreadMessages + { + // If we had no unread messages but we have some message, + // set that as the last visible message so that thats what + // we scroll to. + lastVisibleInteractionStore.setLastVisibleInteraction( + TSThread.LastVisibleInteraction( + sortId: lastVisibleInteractionRowId, + onScreenPercentage: 1 + ), + for: thread.tsThread, + tx: chatItemContext.tx + ) + } } if wasAnyThreadVisible { sskPreferences.setHasSavedThread(true, tx: chatItemContext.tx) @@ -86,6 +106,8 @@ public class MessageBackupPostFrameRestoreActionManager { infoMessage, in: chatThread, chatId: chatId, + // This info message is directionless + directionalDetails: .directionless(BackupProto_ChatItem.DirectionlessMessageDetails()), context: chatItemContext ) }