You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionMessagingKit/Database/Migrations/_021_ReworkRecipientState.s...

197 lines
8.9 KiB
Swift

// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
enum _021_ReworkRecipientState: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "ReworkRecipientState"
static let minExpectedRunDuration: TimeInterval = 0.1
static let createdTables: [(TableRecord & FetchableRecord).Type] = []
static func migrate(_ db: Database, using dependencies: Dependencies) throws {
/// First we need to add the new columns to the `Interaction` table
try db.alter(table: "interaction") { t in
t.add(column: "state", .integer)
.notNull()
.indexed() // Quicker querying
.defaults(to: Interaction.State.sending.rawValue)
t.add(column: "recipientReadTimestampMs", .integer)
t.add(column: "mostRecentFailureText", .text)
}
/// As part of this change we have added two new `State` types: `deleted` and `localOnly` which
/// will simplify some querying and logic for behaviours which have multiple `Interaction.Variant` cases
try db.execute(sql: """
UPDATE interaction
SET state = \(Interaction.State.deleted.rawValue)
WHERE variant = \(Interaction.Variant.standardIncomingDeleted.rawValue)
""")
try db.execute(sql: """
UPDATE interaction
SET state = \(Interaction.State.localOnly.rawValue)
WHERE variant IN (\(Interaction.Variant.variantsWhichAreLocalOnly.map { "\($0.rawValue)" }.joined(separator: ", ")))
""")
/// Part of the logic in the `FailedMessageSendsJob` is to update all pending sends to be in the "failed" state but
/// this migration will run before that gets the chance to so we need to trigger the same transition here to ensure that
/// when we move the data from the old `recipientState` table across to the `Interaction` it's already in the
/// correct state
try db.execute(sql: """
UPDATE recipientState
SET state = 1 -- failed
WHERE state = 0 -- sending
""")
try db.execute(sql: """
UPDATE recipientState
SET state = 4 -- failedToSync
WHERE state = 5 -- syncing
""")
/// In the old logic there would be a `recipientState` for every participant in a `ClosedGroup` conversation and
/// there were special rules around how to merge the states for display purposes so we want to replicate that
/// behaviour here (the default `sending` state will be handled on the column itself so we only need to deal with
/// other behaviours here)
let recipientStateInfo: [Row] = try Row
.fetchAll(db, sql: """
SELECT
interactionId,
recipientId,
state,
readTimestampMs,
mostRecentFailureText
FROM recipientState
""")
.grouped(by: { info -> Int64 in info["interactionId"] })
.reduce(into: []) { result, next in
guard next.value.count > 1 else {
result.append(contentsOf: next.value)
return
}
// If there is a single "failed" state, consider this message "failed"
if let legacyInfo: Row = next.value.first(where: { $0["state"] == LegacyState.failed.rawValue }) {
result.append(legacyInfo)
return
}
// If there is a single "failedToSync" state, consider this message "failedToSync"
if let legacyInfo: Row = next.value.first(where: { $0["state"] == LegacyState.failedToSync.rawValue }) {
result.append(legacyInfo)
return
}
// There isn't really a simple way to combine other combinations (the query would
// pick the smallest, and there was UI logic which would just default to "sent") so
// just go with the smallest
result.append(next.value.sorted(by: { lhs, rhs in
let lhsState: Int = lhs["state"]
let rhsState: Int = rhs["state"]
return (lhsState < rhsState)
})[0])
}
/// Group the `recipientStates` by each of their properties so we can bulk update their associated
/// interactions
let recipientStatesByState: [Int: [Row]] = recipientStateInfo
.grouped(by: { info -> Int in info["state"] })
let recipientStatesByMostRecentFailureText: [String?: [Row]] = recipientStateInfo
.filter { legacyState in legacyState["mostRecentFailureText"] != nil } // Filter out nulls
.filter { legacyState in legacyState["state"] != LegacyState.sent.rawValue } // No need to keep failure text after send
.grouped(by: { info -> String in info["mostRecentFailureText"] })
/// Add the `state` and `mostRecentFailureText` values directly to their interactions
try recipientStatesByState.forEach { rawLegacyState, states in
guard let legacyState: LegacyState = LegacyState(rawValue: rawLegacyState) else { return }
try db.execute(sql: """
UPDATE interaction
SET state = \(legacyState.interactionState.rawValue)
WHERE id IN (\(states
.compactMap { $0["interactionId"].map { "\($0)" } }
.joined(separator: ", ")))
""")
}
try recipientStatesByMostRecentFailureText.forEach { failureText, states in
guard let failureText: String = failureText else { return }
try db.execute(sql: """
UPDATE interaction
SET mostRecentFailureText = '\(failureText)'
WHERE id IN (\(states
.compactMap { $0["interactionId"].map { "\($0)" } }
.joined(separator: ", ")))
""")
}
/// Any interactions which didn't have a `recipientState` or a `MessageSendJob` should be considered `sent` (as
/// the old UI behaviour was to render any messages without a `recipientState` as `sent`)
let interactionIdsWithMessageSendJobs: Set<Int64> = try Int64.fetchSet(db, sql: """
SELECT interactionId
FROM job
WHERE (
variant = \(Job.Variant.messageSend.rawValue) AND
interactionId IS NOT NULL
)
""")
let interactionIdsToExclude: Set<Int64> = Set(recipientStateInfo
.map { info -> Int64 in info["interactionId"] })
.union(interactionIdsWithMessageSendJobs)
if !interactionIdsToExclude.isEmpty {
try db.execute(sql: """
UPDATE interaction
SET state = \(Interaction.State.sent.rawValue)
WHERE id NOT IN (\(interactionIdsToExclude.map { "\($0)" }.joined(separator: ", ")))
""")
}
else {
try db.execute(sql: "UPDATE interaction SET state = \(Interaction.State.sent.rawValue)")
}
/// The timestamps are unlikely to have duplicates so we just need to add those individually
try recipientStateInfo
.filter { $0["readTimestampMs"] != nil }
.forEach { state in
guard
let interactionId: Int64 = state["interactionId"],
let readTimestampMs: Int64 = state["readTimestampMs"]
else { return }
try db.execute(sql: """
UPDATE interaction
SET recipientReadTimestampMs = \(readTimestampMs)
WHERE id = \(interactionId)
""")
}
/// Finally we can drop the old recipient states table
try db.drop(table: "recipientState")
Merge remote-tracking branch 'upstream/dev' into feature/groups-rebuild # Conflicts: # Scripts/LintLocalizableStrings.swift # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Calls/Call Management/SessionCallManager.swift # Session/Calls/WebRTC/WebRTCSession+DataChannel.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/Message Cells/Content Views/OpenGroupInvitationView.swift # Session/Conversations/Message Cells/Content Views/QuoteView.swift # Session/Conversations/Message Cells/Content Views/SwiftUI/OpenGroupInvitationView_SwiftUI.swift # Session/Conversations/Message Cells/Content Views/SwiftUI/VoiceMessageView_SwiftUI.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Database/Migrations/_001_ThemePreferences.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeViewModel.swift # Session/Home/Message Requests/MessageRequestsViewModel.swift # Session/Home/New Conversation/NewMessageScreen.swift # Session/Media Viewing & Editing/GIFs/GiphyAPI.swift # Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift # Session/Media Viewing & Editing/PhotoLibrary.swift # Session/Meta/MainAppContext.swift # Session/Meta/SessionApp.swift # Session/Meta/Translations/remove_unused_strings.swift # Session/Notifications/PushRegistrationManager.swift # Session/Onboarding/Onboarding.swift # Session/Settings/HelpViewModel.swift # Session/Settings/SettingsViewModel.swift # Session/Settings/Views/VersionFooterView.swift # Session/Shared/FullConversationCell.swift # Session/Shared/SessionHostingViewController.swift # Session/Utilities/BackgroundPoller.swift # Session/Utilities/UIContextualAction+Utilities.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/ClosedGroup.swift # SessionMessagingKit/Database/Models/Interaction.swift # SessionMessagingKit/Database/Models/RecipientState.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/Jobs/FailedMessageSendsJob.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift # SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift # SessionMessagingKit/Messages/Message.swift # SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift # SessionMessagingKit/Sending & Receiving/Attachments/ThumbnailService.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift # SessionMessagingKit/Utilities/OWSAudioPlayer.m # SessionMessagingKit/Utilities/Preferences.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionShareExtension/ShareNavController.swift # SessionSnodeKit/LibSession/LibSession+Networking.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionSnodeKit/Types/ProxiedContentDownloader.swift # SessionUIKit/Components/ConfirmationModal.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/General/AppContext.swift # SessionUtilitiesKit/General/FileSystem.swift # SessionUtilitiesKit/General/NSTimer+Proxying.m # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/UIDevice+featureSupport.swift # SessionUtilitiesKit/Media/MediaUtils.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift # SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift # SignalUtilitiesKit/Screen Lock/ScreenLockViewController.swift
5 months ago
Storage.update(progress: 1, for: self, in: target, using: dependencies)
}
}
private extension _021_ReworkRecipientState {
enum LegacyState: Int {
case sending
case failed
case skipped
case sent
case failedToSync
case syncing
var interactionState: Interaction.State {
switch self {
case .sending: return .sending
case .failed: return .failed
case .skipped: return .failed // Have removed the 'skipped' status
case .sent: return .sent
case .failedToSync: return .failedToSync
case .syncing: return .syncing
}
}
}
}