diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 0cd4deb4c..0c17a06c4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -811,6 +811,8 @@ FD848B9628422A2A000E298B /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B86283B844B000E298B /* MessageViewModel.swift */; }; FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9728422F1A000E298B /* Date+Utilities.swift */; }; FD848B9A28442CE6000E298B /* StorageError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9928442CE6000E298B /* StorageError.swift */; }; + FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */; }; + FD860CBE2D6E7DAA00BBE29C /* DeveloperSettingsViewModel+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */; }; FD86FDA32BC5020600EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; FD86FDA42BC51C5400EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; FD86FDA52BC51C5500EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; @@ -1997,6 +1999,8 @@ FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = ""; }; FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = ""; }; FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; + FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _024_FixBustedInteractionVariant.swift; sourceTree = ""; }; + FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeveloperSettingsViewModel+Testing.swift"; sourceTree = ""; }; FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = ""; }; FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = ""; }; @@ -3207,9 +3211,9 @@ C360969125AD1765008B62B2 /* Settings */ = { isa = PBXGroup; children = ( + FD37E9CD28A1E682003AE748 /* Views */, 9422569A2C23F8F000C0FDBF /* QRCodeScreen.swift */, 9422569B2C23F8F000C0FDBF /* RecoveryPasswordScreen.swift */, - FD37E9CD28A1E682003AE748 /* Views */, FD71162D28E168C700B47552 /* SettingsViewModel.swift */, FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */, FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */, @@ -3221,6 +3225,7 @@ FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, FDC1BD672CFE6EEA002CDC71 /* DeveloperSettingsViewModel.swift */, + FD860CBD2D6E7DA000BBE29C /* DeveloperSettingsViewModel+Testing.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); @@ -3801,6 +3806,7 @@ FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */, FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */, FDE33BBD2D5C3AE800E56F42 /* _023_GroupsExpiredFlag.swift */, + FD860CBB2D6E7A9400BBE29C /* _024_FixBustedInteractionVariant.swift */, ); path = Migrations; sourceTree = ""; @@ -6234,6 +6240,7 @@ 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, FD2286692C37DA5500BC06F7 /* PollerType.swift in Sources */, + FD860CBC2D6E7A9F00BBE29C /* _024_FixBustedInteractionVariant.swift in Sources */, FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, @@ -6357,6 +6364,7 @@ 3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */, 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */, FDE754FA2C9BB0B0002A2623 /* NotificationPresenter.swift in Sources */, + FD860CBE2D6E7DAA00BBE29C /* DeveloperSettingsViewModel+Testing.swift in Sources */, FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */, 7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */, B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, @@ -7907,7 +7915,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7983,7 +7991,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 546; + CURRENT_PROJECT_VERSION = 547; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 4518d3591..b18d036d2 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -176,12 +176,13 @@ extension ContextMenuVC { using dependencies: Dependencies ) -> [Action]? { switch cellViewModel.variant { - case .standardIncomingDeleted, .standardIncomingDeletedLocally, .standardOutgoingDeleted, - .standardOutgoingDeletedLocally, .infoCall, .infoScreenshotNotification, .infoMediaSavedNotification, - .infoLegacyGroupCreated, .infoLegacyGroupUpdated, .infoLegacyGroupCurrentUserLeft, - .infoGroupCurrentUserLeaving, .infoGroupCurrentUserErrorLeaving, - .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate, .infoGroupInfoInvited, - .infoGroupInfoUpdated, .infoGroupMembersUpdated: + case ._legacyStandardIncomingDeleted, .standardIncomingDeleted, .standardIncomingDeletedLocally, + .standardOutgoingDeleted, .standardOutgoingDeletedLocally, .infoCall, + .infoScreenshotNotification, .infoMediaSavedNotification, .infoLegacyGroupCreated, + .infoLegacyGroupUpdated, .infoLegacyGroupCurrentUserLeft, .infoGroupCurrentUserLeaving, + .infoGroupCurrentUserErrorLeaving, .infoMessageRequestAccepted, + .infoDisappearingMessagesUpdate, .infoGroupInfoInvited, .infoGroupInfoUpdated, + .infoGroupMembersUpdated: // Let the user delete info messages and unsent messages return [ Action.delete(cellViewModel, delegate) ] diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 7746b7ae9..14a6f0785 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -78,8 +78,9 @@ public class MessageCell: UITableViewCell { guard viewModel.cellType != .unreadMarker else { return UnreadMarkerCell.self } switch viewModel.variant { - case .standardOutgoing, .standardIncoming, .standardIncomingDeleted, .standardOutgoingDeleted, - .standardIncomingDeletedLocally, .standardOutgoingDeletedLocally: + case .standardOutgoing, .standardIncoming, ._legacyStandardIncomingDeleted, + .standardIncomingDeleted, .standardOutgoingDeleted, .standardIncomingDeletedLocally, + .standardOutgoingDeletedLocally: return VisibleMessageCell.self case .infoLegacyGroupCreated, .infoLegacyGroupUpdated, .infoLegacyGroupCurrentUserLeft, diff --git a/Session/Settings/DeveloperSettingsViewModel+Testing.swift b/Session/Settings/DeveloperSettingsViewModel+Testing.swift new file mode 100644 index 000000000..099708b6c --- /dev/null +++ b/Session/Settings/DeveloperSettingsViewModel+Testing.swift @@ -0,0 +1,90 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import UIKit +import SessionUtilitiesKit + +// MARK: - Automated Test Convenience + +extension DeveloperSettingsViewModel { + /// Processes and sets feature flags based on environment variables when running in the iOS simulator to allow extenrally + /// triggered automated tests to start in a specific state or with specific features enabled + /// + /// In order to use these with Appium (a UI testing framework used internally) these settings can be added to the device + /// configuration as below, where the name of the value should match exactly to the `EnvironmentVariable` value + /// below and the value should match one of the options documented below + /// ``` + /// const iOSCapabilities: AppiumXCUITestCapabilities = { + /// 'appium:processArguments': { + /// env: { + /// 'serviceNetwork': "testnet", + /// 'debugDisappearingMessageDurations': true + /// } + /// } + /// } + /// ``` + static func processUnitTestEnvVariablesIfNeeded(using dependencies: Dependencies) { +#if targetEnvironment(simulator) + enum EnvironmentVariable: String { + /// Disables animations for the app (where possible) + /// + /// **Value:** `true`/`false` (default: `true`) + case animationsEnabled + + /// Controls whether the "keys" for strings should be displayed instead of their localized values + /// + /// **Value:** `true`/`false` (default: `false`) + case showStringKeys + + /// Controls whether the app communicates with mainnet or testnet by default + /// + /// **Value:** `"mainnet"`/`"testnet"` (default: `"mainnet"`) + case serviceNetwork + + /// Controls whether the app should trigger it's "Force Offline" behaviour (the network doesn't connect and all requests + /// fail after a 1 second delay with a serviceUnavailable error) + /// + /// **Value:** `true`/`false` (default: `false`) + case forceOffline + + /// Controls whether the app should offer the debug durations for disappearing messages (eg. `10s`, `30s`, etc.) + /// + /// **Value:** `true`/`false` (default: `false`) + case debugDisappearingMessageDurations + } + + ProcessInfo.processInfo.environment.forEach { key, value in + guard let variable: EnvironmentVariable = EnvironmentVariable(rawValue: key) else { return } + + switch variable { + case .animationsEnabled: + dependencies.set(feature: .animationsEnabled, to: (value == "true")) + + guard value == "false" else { return } + + UIView.setAnimationsEnabled(false) + + case .showStringKeys: + dependencies.set(feature: .showStringKeys, to: (value == "true")) + + case .serviceNetwork: + let network: ServiceNetwork + + switch value { + case "testnet": network = .testnet + default: network = .mainnet + } + + DeveloperSettingsViewModel.updateServiceNetwork(to: network, using: dependencies) + + case .forceOffline: + dependencies.set(feature: .forceOffline, to: (value == "true")) + + case .debugDisappearingMessageDurations: + dependencies.set(feature: .debugDisappearingMessageDurations, to: (value == "true")) + } + } +#endif + } +} diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index de84ef3d3..7fcf4ef3d 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -889,7 +889,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, .sinkUntilComplete() } - private static func updateServiceNetwork( + internal static func updateServiceNetwork( to updatedNetwork: ServiceNetwork?, using dependencies: Dependencies ) { @@ -1434,51 +1434,6 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, } } -// MARK: - Automated Test Convenience - -extension DeveloperSettingsViewModel { - static func processUnitTestEnvVariablesIfNeeded(using dependencies: Dependencies) { -#if targetEnvironment(simulator) - enum EnvironmentVariable: String { - case animationsEnabled - case showStringKeys - - case serviceNetwork - case forceOffline - } - - ProcessInfo.processInfo.environment.forEach { key, value in - guard let variable: EnvironmentVariable = EnvironmentVariable(rawValue: key) else { return } - - switch variable { - case .animationsEnabled: - dependencies.set(feature: .animationsEnabled, to: (value == "true")) - - guard value == "false" else { return } - - UIView.setAnimationsEnabled(false) - - case .showStringKeys: - dependencies.set(feature: .showStringKeys, to: (value == "true")) - - case .serviceNetwork: - let network: ServiceNetwork - - switch value { - case "testnet": network = .testnet - default: network = .mainnet - } - - DeveloperSettingsViewModel.updateServiceNetwork(to: network, using: dependencies) - - case .forceOffline: - dependencies.set(feature: .forceOffline, to: (value == "true")) - } - } -#endif - } -} - // MARK: - DocumentPickerResult private class DocumentPickerResult: NSObject, UIDocumentPickerDelegate { diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 1fb422469..fb8b60794 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -40,7 +40,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API _020_AddMissingWhisperFlag.self, _021_ReworkRecipientState.self, _022_GroupsRebuildChanges.self, - _023_GroupsExpiredFlag.self + _023_GroupsExpiredFlag.self, + _024_FixBustedInteractionVariant.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift b/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift index 59615cdb2..4dee77739 100644 --- a/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift +++ b/SessionMessagingKit/Database/Migrations/_023_GroupsExpiredFlag.swift @@ -1,9 +1,7 @@ // Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. import Foundation -import UIKit.UIImage import GRDB -import SessionSnodeKit import SessionUtilitiesKit enum _023_GroupsExpiredFlag: Migration { diff --git a/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift b/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift new file mode 100644 index 000000000..c58a0f4ee --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_024_FixBustedInteractionVariant.swift @@ -0,0 +1,26 @@ +// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// There was a bug with internal releases of the Groups Rebuild feature where we incorrectly assigned an `Interaction.Variant` +/// value of `3` to deleted message artifacts when it should have been `2`, this migration updates any interactions with a value of `2` +/// to be `3` +enum _024_FixBustedInteractionVariant: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "FixBustedInteractionVariant" + static let minExpectedRunDuration: TimeInterval = 0.1 + static var createdTables: [(FetchableRecord & TableRecord).Type] = [] + + static func migrate(_ db: Database, using dependencies: Dependencies) throws { + try db.execute(sql: """ + UPDATE interaction + SET variant = \(Interaction.Variant.standardIncomingDeleted.rawValue) + WHERE variant = \(Interaction.Variant._legacyStandardIncomingDeleted.rawValue) + """) + + Storage.update(progress: 1, for: self, in: target, using: dependencies) + } +} + diff --git a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift index 617a785d7..5fac26366 100644 --- a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift +++ b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift @@ -158,8 +158,9 @@ internal extension ControlMessageProcessRecord { using dependencies: Dependencies ) { switch variant { - case .standardOutgoing, .standardIncoming, .standardIncomingDeleted, .standardIncomingDeletedLocally, - .standardOutgoingDeleted, .standardOutgoingDeletedLocally, .infoLegacyGroupCreated: + case .standardOutgoing, .standardIncoming, ._legacyStandardIncomingDeleted, + .standardIncomingDeleted, .standardIncomingDeletedLocally, .standardOutgoingDeleted, + .standardOutgoingDeletedLocally, .infoLegacyGroupCreated: return nil case .infoLegacyGroupUpdated, .infoLegacyGroupCurrentUserLeft: self.variant = .legacyGroupControlMessage diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index b6e2eb827..c1d097449 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -73,7 +73,9 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu } public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible, CaseIterable { - case standardIncoming + case _legacyStandardIncomingDeleted = 2 // Had an incorrect index so broke this... + + case standardIncoming = 0 case standardOutgoing // Deleted message variants @@ -988,7 +990,7 @@ public extension Interaction { using dependencies: Dependencies ) -> String { switch variant { - case .standardIncomingDeleted, .standardIncomingDeletedLocally, + case ._legacyStandardIncomingDeleted, .standardIncomingDeleted, .standardIncomingDeletedLocally, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: return "" @@ -1105,7 +1107,8 @@ public extension Interaction.Variant { .infoGroupMembersUpdated: return true - case .standardIncoming, .standardOutgoing, .standardIncomingDeleted, .standardIncomingDeletedLocally, + case .standardIncoming, .standardOutgoing, ._legacyStandardIncomingDeleted, + .standardIncomingDeleted, .standardIncomingDeletedLocally, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: return false } @@ -1151,8 +1154,9 @@ public extension Interaction.Variant { .infoGroupMembersUpdated: return true - case .standardIncomingDeleted, .standardIncomingDeletedLocally, - .standardOutgoingDeleted, .standardOutgoingDeletedLocally: + case ._legacyStandardIncomingDeleted, .standardIncomingDeleted, + .standardIncomingDeletedLocally, .standardOutgoingDeleted, + .standardOutgoingDeletedLocally: return false } } @@ -1162,7 +1166,8 @@ public extension Interaction.Variant { case .standardIncoming: return .sent case .standardOutgoing: return .sending - case .standardIncomingDeleted, .standardIncomingDeletedLocally, .standardOutgoingDeleted, + case ._legacyStandardIncomingDeleted, .standardIncomingDeleted, + .standardIncomingDeletedLocally, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: return .deleted @@ -1189,8 +1194,8 @@ public extension Interaction.Variant { /// after being read (if we don't do this their expiration timer will start immediately when received) return true - case .standardOutgoing, .standardIncomingDeleted, .standardIncomingDeletedLocally, - .standardOutgoingDeleted, .standardOutgoingDeletedLocally: + case .standardOutgoing, ._legacyStandardIncomingDeleted, .standardIncomingDeleted, + .standardIncomingDeletedLocally, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: return false case .infoLegacyGroupCreated, .infoLegacyGroupUpdated, .infoLegacyGroupCurrentUserLeft,