diff --git a/LibSession-Util b/LibSession-Util index d8f07fa92..e3ccf29db 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit d8f07fa92c12c5c2409774e03e03395d7847d1c2 +Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index cac7103f7..e0f3f7efe 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -531,6 +531,8 @@ FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; + FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */; }; + FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; }; FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* NetworkType.swift */; }; FD23CE1F2A65269C0000B97C /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1E2A65269C0000B97C /* Crypto.swift */; }; FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */; }; @@ -1689,6 +1691,8 @@ FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistableRecordUtilitiesSpec.swift; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; + FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Utilities.swift"; sourceTree = ""; }; + FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = ""; }; FD23CE1A2A651E6D0000B97C /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = ""; }; FD23CE1E2A65269C0000B97C /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+Crypto.swift"; sourceTree = ""; }; @@ -3618,6 +3622,7 @@ FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */, FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, + FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */, ); path = Migrations; sourceTree = ""; @@ -3763,6 +3768,7 @@ FD2B4B022949886900AB4848 /* Database */ = { isa = PBXGroup; children = ( + FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */, FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */, ); path = Database; @@ -5914,12 +5920,14 @@ C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */, FD2959922A4417A900888A17 /* PreparedSendData.swift in Sources */, FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */, + FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */, FD432437299DEA38008A0213 /* TypeConversion+Utilities.swift in Sources */, FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */, FD09797027FA6FF300936362 /* Profile.swift in Sources */, FD245C56285065EA00B966DD /* SNProto.swift in Sources */, FD09798B27FD1CFE00936362 /* Capability.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, + FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */, FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */, FD09798127FCFEE800936362 /* SessionThread.swift in Sources */, FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */, diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index fa5656fa1..523cdd884 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -208,17 +208,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers }() private lazy var emptyStateLabel: UILabel = { - let text: String = String( - format: { - switch (viewModel.threadData.threadIsNoteToSelf, viewModel.threadData.canWrite) { - case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() - case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() - default: return "CONVERSATION_EMPTY_STATE".localized() - } - }(), - viewModel.threadData.displayName - ) - + let text: String = emptyStateText(for: viewModel.threadData) let result: UILabel = UILabel() result.accessibilityLabel = "Empty state label" result.translatesAutoresizingMaskIntoConstraints = false @@ -698,6 +688,24 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers self.viewModel.onInteractionChange = nil } + private func emptyStateText(for threadData: SessionThreadViewModel) -> String { + return String( + format: { + switch (threadData.threadIsNoteToSelf, threadData.canWrite) { + case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() + case (_, false): + return (threadData.profile?.blocksCommunityMessageRequests == true ? + "COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE".localized() : + "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() + ) + + default: return "CONVERSATION_EMPTY_STATE".localized() + } + }(), + threadData.displayName + ) + } + private func handleThreadUpdates(_ updatedThreadData: SessionThreadViewModel, initialLoad: Bool = false) { // Ensure the first load or a load when returning from a child screen runs without animations (if // we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition) @@ -738,17 +746,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers ) // Update the empty state - let text: String = String( - format: { - switch (updatedThreadData.threadIsNoteToSelf, updatedThreadData.canWrite) { - case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized() - case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized() - default: return "CONVERSATION_EMPTY_STATE".localized() - } - }(), - updatedThreadData.displayName - ) - + let text: String = emptyStateText(for: updatedThreadData) emptyStateLabel.attributedText = NSAttributedString(string: text) .adding( attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)], @@ -791,8 +789,10 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers updatedThreadData.threadRequiresApproval == true ) self?.messageRequestStackView.isHidden = ( - updatedThreadData.threadIsMessageRequest == false && - updatedThreadData.threadRequiresApproval == false + !updatedThreadData.canWrite || ( + updatedThreadData.threadIsMessageRequest == false && + updatedThreadData.threadRequiresApproval == false + ) ) self?.messageRequestBackgroundView.isHidden = (self?.messageRequestStackView.isHidden == true) self?.messageRequestDescriptionLabelBottomConstraint?.constant = (updatedThreadData.threadRequiresApproval == true ? -4 : -20) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 40792b3fc..07251bcc5 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -178,6 +178,7 @@ class ThreadSettingsViewModel: SessionTableViewModel [SectionModel] in + .trackingConstantRegion { [weak self] db -> State in + State( + trimOpenGroupMessagesOlderThanSixMonths: db[.trimOpenGroupMessagesOlderThanSixMonths], + shouldAutoPlayConsecutiveAudioMessages: db[.shouldAutoPlayConsecutiveAudioMessages] + ) + } + .removeDuplicates() + .handleEvents(didFail: { SNLog("[ConversationSettingsViewModel] Observation failed with error: \($0)") }) + .publisher(in: Storage.shared) + .withPrevious() + .map { (previous: State?, current: State) -> [SectionModel] in return [ SectionModel( model: .messageTrimming, @@ -55,7 +70,11 @@ class ConversationSettingsViewModel: SessionTableViewModel { +class NotificationSettingsViewModel: SessionTableViewModel { // MARK: - Config public enum Section: SessionTableSection { @@ -31,7 +31,7 @@ class NotificationSettingsViewModel: SessionTableViewModel [SectionModel] in - let notificationSound: Preferences.Sound = db[.defaultNotificationSound] - .defaulting(to: Preferences.Sound.defaultNotificationSound) - let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] - .defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType) - + .trackingConstantRegion { db -> State in + State( + isUsingFullAPNs: false, // Set later the the data flow + notificationSound: db[.defaultNotificationSound] + .defaulting(to: Preferences.Sound.defaultNotificationSound), + playNotificationSoundInForeground: db[.playNotificationSoundInForeground], + previewType: db[.preferencesNotificationPreviewType] + .defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType) + ) + } + .removeDuplicates() + .handleEvents(didFail: { SNLog("[NotificationSettingsViewModel] Observation failed with error: \($0)") }) + .publisher(in: Storage.shared) + .manualRefreshFrom(forcedRefresh) + .map { dbState -> State in + State( + isUsingFullAPNs: UserDefaults.standard[.isUsingFullAPNs], + notificationSound: dbState.notificationSound, + playNotificationSoundInForeground: dbState.playNotificationSoundInForeground, + previewType: dbState.previewType + ) + } + .withPrevious() + .map { (previous: State?, current: State) -> [SectionModel] in return [ SectionModel( model: .strategy, @@ -68,20 +93,24 @@ class NotificationSettingsViewModel: SessionTableViewModel [SectionModel] in + .trackingConstantRegion { [weak self] db -> State in + State( + isScreenLockEnabled: db[.isScreenLockEnabled], + checkForCommunityMessageRequests: db[.checkForCommunityMessageRequests], + areReadReceiptsEnabled: db[.areReadReceiptsEnabled], + typingIndicatorsEnabled: db[.typingIndicatorsEnabled], + areLinkPreviewsEnabled: db[.areLinkPreviewsEnabled], + areCallsEnabled: db[.areCallsEnabled] + ) + } + .removeDuplicates() + .handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") }) + .publisher(in: Storage.shared) + .withPrevious() + .map { (previous: State?, current: State) -> [SectionModel] in return [ SectionModel( model: .screenSecurity, @@ -96,7 +122,13 @@ class PrivacySettingsViewModel: SessionTableViewModel 100 } // Prevent too many changes from causing performance issues ) { [weak self] updatedData in self?.viewModel.updateTableData(updatedData) @@ -339,6 +339,7 @@ class SessionTableViewController { Just(nil).eraseToAnyPublisher() } open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() } + private let _forcedRefresh: PassthroughSubject = PassthroughSubject() + lazy var forcedRefresh: AnyPublisher = _forcedRefresh + .shareReplay(0) private let _showToast: PassthroughSubject<(String, ThemeValue), Never> = PassthroughSubject() lazy var showToast: AnyPublisher<(String, ThemeValue), Never> = _showToast .shareReplay(0) @@ -62,6 +65,10 @@ class SessionTableViewModel( for viewModel: SessionTableViewModel ) -> AnyPublisher<(Output, StagedChangeset), Failure> where Output == [ArraySection>] { diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index af9d617eb..4bacade56 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -394,19 +394,30 @@ extension SessionCell.Accessory { extension SessionCell.Accessory { public enum DataSource: Hashable, Equatable { - case boolValue(Bool) + case boolValue(key: String, value: Bool, oldValue: Bool) case dynamicString(() -> String?) - case userDefaults(UserDefaults, key: String) - case settingBool(key: Setting.BoolKey) + + static func boolValue(_ value: Bool, oldValue: Bool) -> DataSource { + return .boolValue(key: "", value: value, oldValue: oldValue) + } + + static func boolValue(key: Setting.BoolKey, value: Bool, oldValue: Bool) -> DataSource { + return .boolValue(key: key.rawValue, value: value, oldValue: oldValue) + } // MARK: - Convenience public var currentBoolValue: Bool { switch self { - case .boolValue(let value): return value + case .boolValue(_, let value, _): return value case .dynamicString: return false - case .userDefaults(let defaults, let key): return defaults.bool(forKey: key) - case .settingBool(let key): return Storage.shared[key] + } + } + + public var oldBoolValue: Bool { + switch self { + case .boolValue(_, _, let oldValue): return oldValue + default: return false } } @@ -421,27 +432,27 @@ extension SessionCell.Accessory { public func hash(into hasher: inout Hasher) { switch self { - case .boolValue(let value): value.hash(into: &hasher) + case .boolValue(let key, let value, let oldValue): + key.hash(into: &hasher) + value.hash(into: &hasher) + oldValue.hash(into: &hasher) + case .dynamicString(let generator): generator().hash(into: &hasher) - case .userDefaults(_, let key): key.hash(into: &hasher) - case .settingBool(let key): key.hash(into: &hasher) } } public static func == (lhs: DataSource, rhs: DataSource) -> Bool { switch (lhs, rhs) { - case (.boolValue(let lhsValue), .boolValue(let rhsValue)): - return (lhsValue == rhsValue) + case (.boolValue(let lhsKey, let lhsValue, let lhsOldValue), .boolValue(let rhsKey, let rhsValue, let rhsOldValue)): + return ( + lhsKey == rhsKey && + lhsValue == rhsValue && + lhsOldValue == rhsOldValue + ) case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)): return (lhsGenerator() == rhsGenerator()) - case (.userDefaults(_, let lhsKey), .userDefaults(_, let rhsKey)): - return (lhsKey == rhsKey) - - case (.settingBool(let lhsKey), .settingBool(let rhsKey)): - return (lhsKey == rhsKey) - default: return false } } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 44c81b9eb..39ca4344a 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -277,7 +277,8 @@ extension SessionCell { public func update( with accessory: Accessory?, tintColor: ThemeValue, - isEnabled: Bool + isEnabled: Bool, + isManualReload: Bool ) { guard let accessory: Accessory = accessory else { return } @@ -356,10 +357,15 @@ extension SessionCell { fixedWidthConstraint.isActive = true toggleSwitchConstraints.forEach { $0.isActive = true } - let newValue: Bool = dataSource.currentBoolValue - - if newValue != toggleSwitch.isOn { - toggleSwitch.setOn(newValue, animated: true) + if !isManualReload { + toggleSwitch.setOn(dataSource.oldBoolValue, animated: false) + + // Dispatch so the cell reload doesn't conflict with the setting change animation + if dataSource.oldBoolValue != dataSource.currentBoolValue { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak toggleSwitch] in + toggleSwitch?.setOn(dataSource.currentBoolValue, animated: true) + } + } } case .dropDown(let dataSource, let accessibility): diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index 912fb37a9..1b2b8630e 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -313,7 +313,7 @@ public class SessionCell: UITableViewCell { botSeparator.isHidden = true } - public func update(with info: Info) { + public func update(with info: Info, isManualReload: Bool = false) { interactionMode = (info.title?.interaction ?? .none) shouldHighlightTitle = (info.title?.interaction != .copy) titleExtraView = info.title?.extraViewGenerator?() @@ -332,7 +332,8 @@ public class SessionCell: UITableViewCell { leftAccessoryView.update( with: info.leftAccessory, tintColor: info.styling.tintColor, - isEnabled: info.isEnabled + isEnabled: info.isEnabled, + isManualReload: isManualReload ) titleStackView.isHidden = (info.title == nil && info.subtitle == nil) titleLabel.isUserInteractionEnabled = (info.title?.interaction == .copy) @@ -356,7 +357,8 @@ public class SessionCell: UITableViewCell { rightAccessoryView.update( with: info.rightAccessory, tintColor: info.styling.tintColor, - isEnabled: info.isEnabled + isEnabled: info.isEnabled, + isManualReload: isManualReload ) contentStackViewLeadingConstraint.isActive = (info.styling.alignment == .leading) diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index 1cbf94fbe..d80ea168b 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -99,7 +99,8 @@ enum MockDataGenerator { .compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) } .joined(), lastNameUpdate: Date().timeIntervalSince1970, - lastProfilePictureUpdate: Date().timeIntervalSince1970 + lastProfilePictureUpdate: Date().timeIntervalSince1970, + lastBlocksCommunityMessageRequests: 0 ) .saved(db) @@ -180,7 +181,8 @@ enum MockDataGenerator { .compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) } .joined(), lastNameUpdate: Date().timeIntervalSince1970, - lastProfilePictureUpdate: Date().timeIntervalSince1970 + lastProfilePictureUpdate: Date().timeIntervalSince1970, + lastBlocksCommunityMessageRequests: 0 ) .saved(db) @@ -310,7 +312,8 @@ enum MockDataGenerator { .compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) } .joined(), lastNameUpdate: Date().timeIntervalSince1970, - lastProfilePictureUpdate: Date().timeIntervalSince1970 + lastProfilePictureUpdate: Date().timeIntervalSince1970, + lastBlocksCommunityMessageRequests: 0 ) .saved(db) diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 0aa4f5d37..8522a4276 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -37,7 +37,8 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API (Features.useSharedUtilForUserConfig(db) ? _014_GenerateInitialUserConfigDumps.self : (nil as Migration.Type?) - ) + ), + _015_BlockCommunityMessageRequests.self ].compactMap { $0 } ] ) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 8918c1c9b..9f0af5346 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -422,7 +422,8 @@ enum _003_YDBToGRDBMigration: Migration { profilePictureUrl: legacyContact.profilePictureURL, profilePictureFileName: legacyContact.profilePictureFileName, profileEncryptionKey: legacyContact.profileEncryptionKey?.keyData, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + lastBlocksCommunityMessageRequests: 0 ).migrationSafeInsert(db) /// **Note:** The blow "shouldForce" flags are here to allow us to avoid having to run legacy migrations they @@ -645,7 +646,8 @@ enum _003_YDBToGRDBMigration: Migration { id: profileId, name: profileId, lastNameUpdate: 0, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + lastBlocksCommunityMessageRequests: 0 ).migrationSafeSave(db) } @@ -1059,7 +1061,8 @@ enum _003_YDBToGRDBMigration: Migration { id: quotedMessage.authorId, name: quotedMessage.authorId, lastNameUpdate: 0, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + lastBlocksCommunityMessageRequests: 0 ).migrationSafeSave(db) } diff --git a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift b/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift index 65e68507c..235a217d3 100644 --- a/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift +++ b/SessionMessagingKit/Database/Migrations/_005_FixDeletedMessageReadState.swift @@ -9,7 +9,7 @@ enum _005_FixDeletedMessageReadState: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "FixDeletedMessageReadState" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { _ = try Interaction diff --git a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift index c1097eb94..b746c6362 100644 --- a/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift +++ b/SessionMessagingKit/Database/Migrations/_006_FixHiddenModAdminSupport.swift @@ -10,7 +10,7 @@ enum _006_FixHiddenModAdminSupport: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "FixHiddenModAdminSupport" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.alter(table: GroupMember.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift b/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift index b468098f7..5e53bb6ee 100644 --- a/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift +++ b/SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift @@ -9,7 +9,7 @@ enum _007_HomeQueryOptimisationIndexes: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "HomeQueryOptimisationIndexes" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.create( diff --git a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift b/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift index b06687dca..399dba483 100644 --- a/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift +++ b/SessionMessagingKit/Database/Migrations/_008_EmojiReacts.swift @@ -9,7 +9,7 @@ enum _008_EmojiReacts: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "EmojiReacts" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.create(table: Reaction.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift b/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift index 4f6036a2d..f4c7e8617 100644 --- a/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift +++ b/SessionMessagingKit/Database/Migrations/_009_OpenGroupPermission.swift @@ -8,7 +8,7 @@ enum _009_OpenGroupPermission: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "OpenGroupPermission" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: GRDB.Database) throws { try db.alter(table: OpenGroup.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift b/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift index 9c2e228d5..2fb57b2cf 100644 --- a/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift +++ b/SessionMessagingKit/Database/Migrations/_011_AddPendingReadReceipts.swift @@ -10,7 +10,7 @@ enum _011_AddPendingReadReceipts: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "AddPendingReadReceipts" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { try db.create(table: PendingReadReceipt.self) { t in diff --git a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift b/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift index d994b6a90..57cb66e7d 100644 --- a/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift +++ b/SessionMessagingKit/Database/Migrations/_012_AddFTSIfNeeded.swift @@ -9,7 +9,7 @@ enum _012_AddFTSIfNeeded: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "AddFTSIfNeeded" static let needsConfigSync: Bool = false - static let minExpectedRunDuration: TimeInterval = 0.1 + static let minExpectedRunDuration: TimeInterval = 0.01 static func migrate(_ db: Database) throws { // Fix an issue that the fullTextSearchTable was dropped unintentionally and global search won't work. diff --git a/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift b/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift new file mode 100644 index 000000000..f887a6ce3 --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift @@ -0,0 +1,33 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +/// This migration adds a flag indicating whether a profile has indicated it is blocking community message requests +enum _015_BlockCommunityMessageRequests: Migration { + static let target: TargetMigrations.Identifier = .messagingKit + static let identifier: String = "BlockCommunityMessageRequests" + static let needsConfigSync: Bool = false + static let minExpectedRunDuration: TimeInterval = 0.01 + + static func migrate(_ db: Database) throws { + // Add the new 'Profile' properties + try db.alter(table: Profile.self) { t in + t.add(.blocksCommunityMessageRequests, .boolean) + t.add(.lastBlocksCommunityMessageRequests, .integer) + .notNull() + .defaults(to: 0) + } + + // If the user exists and the 'checkForCommunityMessageRequests' hasn't already been set then default it to "false" + if + Identity.userExists(db), + (try Setting.exists(db, id: Setting.BoolKey.checkForCommunityMessageRequests.rawValue)) == false + { + db[.checkForCommunityMessageRequests] = true + } + + Storage.update(progress: 1, for: self, in: target) // In case this is the last migration + } +} diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index cc4e4bef6..a9a6bd8af 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -27,6 +27,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco case profilePictureFileName case profileEncryptionKey case lastProfilePictureUpdate + + case blocksCommunityMessageRequests + case lastBlocksCommunityMessageRequests } /// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant) @@ -53,6 +56,12 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco /// The timestamp (in seconds since epoch) that the profile picture was last updated public let lastProfilePictureUpdate: TimeInterval + /// A flag indicating whether this profile has reported that it blocks community message requests + public let blocksCommunityMessageRequests: Bool? + + /// The timestamp (in seconds since epoch) that the `blocksCommunityMessageRequests` setting was last updated + public let lastBlocksCommunityMessageRequests: TimeInterval + // MARK: - Initialization public init( @@ -63,7 +72,9 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco profilePictureUrl: String? = nil, profilePictureFileName: String? = nil, profileEncryptionKey: Data? = nil, - lastProfilePictureUpdate: TimeInterval + lastProfilePictureUpdate: TimeInterval, + blocksCommunityMessageRequests: Bool? = nil, + lastBlocksCommunityMessageRequests: TimeInterval ) { self.id = id self.name = name @@ -73,6 +84,8 @@ public struct Profile: Codable, Identifiable, Equatable, Hashable, FetchableReco self.profilePictureFileName = profilePictureFileName self.profileEncryptionKey = profileEncryptionKey self.lastProfilePictureUpdate = lastProfilePictureUpdate + self.blocksCommunityMessageRequests = blocksCommunityMessageRequests + self.lastBlocksCommunityMessageRequests = lastBlocksCommunityMessageRequests } // MARK: - Description @@ -114,7 +127,9 @@ public extension Profile { profilePictureUrl: profilePictureUrl, profilePictureFileName: try? container.decode(String.self, forKey: .profilePictureFileName), profileEncryptionKey: profileKey, - lastProfilePictureUpdate: try container.decode(TimeInterval.self, forKey: .lastProfilePictureUpdate) + lastProfilePictureUpdate: try container.decode(TimeInterval.self, forKey: .lastProfilePictureUpdate), + blocksCommunityMessageRequests: try? container.decode(Bool.self, forKey: .blocksCommunityMessageRequests), + lastBlocksCommunityMessageRequests: try container.decode(TimeInterval.self, forKey: .lastBlocksCommunityMessageRequests) ) } @@ -129,6 +144,8 @@ public extension Profile { try container.encodeIfPresent(profilePictureFileName, forKey: .profilePictureFileName) try container.encodeIfPresent(profileEncryptionKey, forKey: .profileEncryptionKey) try container.encode(lastProfilePictureUpdate, forKey: .lastProfilePictureUpdate) + try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests) + try container.encode(lastBlocksCommunityMessageRequests, forKey: .lastBlocksCommunityMessageRequests) } } @@ -156,7 +173,9 @@ public extension Profile { profilePictureUrl: profilePictureUrl, profilePictureFileName: nil, profileEncryptionKey: profileKey, - lastProfilePictureUpdate: sentTimestamp + lastProfilePictureUpdate: sentTimestamp, + blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil), + lastBlocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? sentTimestamp : 0) ) } @@ -242,7 +261,9 @@ public extension Profile { profilePictureUrl: nil, profilePictureFileName: nil, profileEncryptionKey: nil, - lastProfilePictureUpdate: 0 + lastProfilePictureUpdate: 0, + blocksCommunityMessageRequests: nil, + lastBlocksCommunityMessageRequests: 0 ) } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift index 8f63ed5a9..4ad3649ac 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Profile.swift @@ -10,15 +10,22 @@ public extension VisibleMessage { public let displayName: String? public let profileKey: Data? public let profilePictureUrl: String? + public let blocksCommunityMessageRequests: Bool? // MARK: - Initialization - internal init(displayName: String, profileKey: Data? = nil, profilePictureUrl: String? = nil) { + internal init( + displayName: String, + profileKey: Data? = nil, + profilePictureUrl: String? = nil, + blocksCommunityMessageRequests: Bool? = nil + ) { let hasUrlAndKey: Bool = (profileKey != nil && profilePictureUrl != nil) self.displayName = displayName self.profileKey = (hasUrlAndKey ? profileKey : nil) self.profilePictureUrl = (hasUrlAndKey ? profilePictureUrl : nil) + self.blocksCommunityMessageRequests = blocksCommunityMessageRequests } // MARK: - Proto Conversion @@ -32,7 +39,8 @@ public extension VisibleMessage { return VMProfile( displayName: displayName, profileKey: proto.profileKey, - profilePictureUrl: profileProto.profilePicture + profilePictureUrl: profileProto.profilePicture, + blocksCommunityMessageRequests: (proto.hasBlocksCommunityMessageRequests ? proto.blocksCommunityMessageRequests : nil) ) } @@ -45,6 +53,10 @@ public extension VisibleMessage { let profileProto = SNProtoLokiProfile.builder() profileProto.setDisplayName(displayName) + if let blocksCommunityMessageRequests: Bool = self.blocksCommunityMessageRequests { + dataMessageProto.setBlocksCommunityMessageRequests(blocksCommunityMessageRequests) + } + if let profileKey = profileKey, let profilePictureUrl = profilePictureUrl { dataMessageProto.setProfileKey(profileKey) profileProto.setProfilePicture(profilePictureUrl) @@ -112,10 +124,14 @@ public extension VisibleMessage { // MARK: - Conversion extension VisibleMessage.VMProfile { - init(profile: Profile) { + init( + profile: Profile, + blocksCommunityMessageRequests: Bool? + ) { self.displayName = profile.name self.profileKey = profile.profileEncryptionKey self.profilePictureUrl = profile.profilePictureUrl + self.blocksCommunityMessageRequests = blocksCommunityMessageRequests } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 4cd74f821..5c5d622c0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -109,10 +109,12 @@ public enum OpenGroupAPI { // The 'inbox' and 'outbox' only work with blinded keys so don't bother polling them if not blinded !capabilities.contains(.blind) ? [] : [ - // Inbox - (lastInboxMessageId == 0 ? - try preparedInbox(db, on: server, using: dependencies) : - try preparedInboxSince(db, id: lastInboxMessageId, on: server, using: dependencies) + // Inbox (only check the inbox if the user want's community message requests) + (!db[.checkForCommunityMessageRequests] ? nil : + (lastInboxMessageId == 0 ? + try preparedInbox(db, on: server, using: dependencies) : + try preparedInboxSince(db, id: lastInboxMessageId, on: server, using: dependencies) + ) ), // Outbox @@ -120,7 +122,7 @@ public enum OpenGroupAPI { try preparedOutbox(db, on: server, using: dependencies) : try preparedOutboxSince(db, id: lastOutboxMessageId, on: server, using: dependencies) ), - ] + ].compactMap { $0 } ) ) diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index ed766ca8d..72e4ca517 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -2497,6 +2497,9 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr if let _value = syncTarget { builder.setSyncTarget(_value) } + if hasBlocksCommunityMessageRequests { + builder.setBlocksCommunityMessageRequests(blocksCommunityMessageRequests) + } return builder } @@ -2570,6 +2573,10 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr proto.syncTarget = valueParam } + @objc public func setBlocksCommunityMessageRequests(_ valueParam: Bool) { + proto.blocksCommunityMessageRequests = valueParam + } + @objc public func build() throws -> SNProtoDataMessage { return try SNProtoDataMessage.parseProto(proto) } @@ -2646,6 +2653,13 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr return proto.hasSyncTarget } + @objc public var blocksCommunityMessageRequests: Bool { + return proto.blocksCommunityMessageRequests + } + @objc public var hasBlocksCommunityMessageRequests: Bool { + return proto.hasBlocksCommunityMessageRequests + } + private init(proto: SessionProtos_DataMessage, attachments: [SNProtoAttachmentPointer], quote: SNProtoDataMessageQuote?, diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index 6f209cb67..3fb72c9ad 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -600,7 +600,7 @@ struct SessionProtos_DataMessage { set {_uniqueStorage()._attachments = newValue} } - /// optional GroupContext group = 3; // No longer used + /// optional GroupContext group = 3; // No longer used var flags: UInt32 { get {return _storage._flags ?? 0} set {_uniqueStorage()._flags = newValue} @@ -696,6 +696,15 @@ struct SessionProtos_DataMessage { /// Clears the value of `syncTarget`. Subsequent reads from it will return its default value. mutating func clearSyncTarget() {_uniqueStorage()._syncTarget = nil} + var blocksCommunityMessageRequests: Bool { + get {return _storage._blocksCommunityMessageRequests ?? false} + set {_uniqueStorage()._blocksCommunityMessageRequests = newValue} + } + /// Returns true if `blocksCommunityMessageRequests` has been explicitly set. + var hasBlocksCommunityMessageRequests: Bool {return _storage._blocksCommunityMessageRequests != nil} + /// Clears the value of `blocksCommunityMessageRequests`. Subsequent reads from it will return its default value. + mutating func clearBlocksCommunityMessageRequests() {_uniqueStorage()._blocksCommunityMessageRequests = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() enum Flags: SwiftProtobuf.Enum { @@ -1665,6 +1674,43 @@ extension SessionProtos_SharedConfigMessage.Kind: CaseIterable { #endif // swift(>=4.2) +#if swift(>=5.5) && canImport(_Concurrency) +extension SessionProtos_Envelope: @unchecked Sendable {} +extension SessionProtos_Envelope.TypeEnum: @unchecked Sendable {} +extension SessionProtos_TypingMessage: @unchecked Sendable {} +extension SessionProtos_TypingMessage.Action: @unchecked Sendable {} +extension SessionProtos_UnsendRequest: @unchecked Sendable {} +extension SessionProtos_MessageRequestResponse: @unchecked Sendable {} +extension SessionProtos_Content: @unchecked Sendable {} +extension SessionProtos_CallMessage: @unchecked Sendable {} +extension SessionProtos_CallMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_KeyPair: @unchecked Sendable {} +extension SessionProtos_DataExtractionNotification: @unchecked Sendable {} +extension SessionProtos_DataExtractionNotification.TypeEnum: @unchecked Sendable {} +extension SessionProtos_LokiProfile: @unchecked Sendable {} +extension SessionProtos_DataMessage: @unchecked Sendable {} +extension SessionProtos_DataMessage.Flags: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote.QuotedAttachment: @unchecked Sendable {} +extension SessionProtos_DataMessage.Quote.QuotedAttachment.Flags: @unchecked Sendable {} +extension SessionProtos_DataMessage.Preview: @unchecked Sendable {} +extension SessionProtos_DataMessage.Reaction: @unchecked Sendable {} +extension SessionProtos_DataMessage.Reaction.Action: @unchecked Sendable {} +extension SessionProtos_DataMessage.OpenGroupInvitation: @unchecked Sendable {} +extension SessionProtos_DataMessage.ClosedGroupControlMessage: @unchecked Sendable {} +extension SessionProtos_DataMessage.ClosedGroupControlMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_DataMessage.ClosedGroupControlMessage.KeyPairWrapper: @unchecked Sendable {} +extension SessionProtos_ConfigurationMessage: @unchecked Sendable {} +extension SessionProtos_ConfigurationMessage.ClosedGroup: @unchecked Sendable {} +extension SessionProtos_ConfigurationMessage.Contact: @unchecked Sendable {} +extension SessionProtos_ReceiptMessage: @unchecked Sendable {} +extension SessionProtos_ReceiptMessage.TypeEnum: @unchecked Sendable {} +extension SessionProtos_AttachmentPointer: @unchecked Sendable {} +extension SessionProtos_AttachmentPointer.Flags: @unchecked Sendable {} +extension SessionProtos_SharedConfigMessage: @unchecked Sendable {} +extension SessionProtos_SharedConfigMessage.Kind: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "SessionProtos" @@ -2288,6 +2334,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 102: .same(proto: "openGroupInvitation"), 104: .same(proto: "closedGroupControlMessage"), 105: .same(proto: "syncTarget"), + 106: .same(proto: "blocksCommunityMessageRequests"), ] fileprivate class _StorageClass { @@ -2304,6 +2351,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa var _openGroupInvitation: SessionProtos_DataMessage.OpenGroupInvitation? = nil var _closedGroupControlMessage: SessionProtos_DataMessage.ClosedGroupControlMessage? = nil var _syncTarget: String? = nil + var _blocksCommunityMessageRequests: Bool? = nil static let defaultInstance = _StorageClass() @@ -2323,6 +2371,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa _openGroupInvitation = source._openGroupInvitation _closedGroupControlMessage = source._closedGroupControlMessage _syncTarget = source._syncTarget + _blocksCommunityMessageRequests = source._blocksCommunityMessageRequests } } @@ -2366,6 +2415,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa case 102: try { try decoder.decodeSingularMessageField(value: &_storage._openGroupInvitation) }() case 104: try { try decoder.decodeSingularMessageField(value: &_storage._closedGroupControlMessage) }() case 105: try { try decoder.decodeSingularStringField(value: &_storage._syncTarget) }() + case 106: try { try decoder.decodeSingularBoolField(value: &_storage._blocksCommunityMessageRequests) }() default: break } } @@ -2417,6 +2467,9 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa try { if let v = _storage._syncTarget { try visitor.visitSingularStringField(value: v, fieldNumber: 105) } }() + try { if let v = _storage._blocksCommunityMessageRequests { + try visitor.visitSingularBoolField(value: v, fieldNumber: 106) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -2439,6 +2492,7 @@ extension SessionProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if _storage._openGroupInvitation != rhs_storage._openGroupInvitation {return false} if _storage._closedGroupControlMessage != rhs_storage._closedGroupControlMessage {return false} if _storage._syncTarget != rhs_storage._syncTarget {return false} + if _storage._blocksCommunityMessageRequests != rhs_storage._blocksCommunityMessageRequests {return false} return true } if !storagesAreEqual {return false} diff --git a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift index 737e40ce6..2fe165044 100644 --- a/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift +++ b/SessionMessagingKit/Protos/Generated/WebSocketResources.pb.swift @@ -218,6 +218,13 @@ extension WebSocketProtos_WebSocketMessage.TypeEnum: CaseIterable { #endif // swift(>=4.2) +#if swift(>=5.5) && canImport(_Concurrency) +extension WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketMessage: @unchecked Sendable {} +extension WebSocketProtos_WebSocketMessage.TypeEnum: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "WebSocketProtos" diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 429c10b14..55d77b18f 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -192,20 +192,21 @@ message DataMessage { optional uint32 expirationTimer = 8; } - optional string body = 1; - repeated AttachmentPointer attachments = 2; - // optional GroupContext group = 3; // No longer used - optional uint32 flags = 4; - optional uint32 expireTimer = 5; - optional bytes profileKey = 6; - optional uint64 timestamp = 7; - optional Quote quote = 8; - repeated Preview preview = 10; - optional Reaction reaction = 11; - optional LokiProfile profile = 101; - optional OpenGroupInvitation openGroupInvitation = 102; - optional ClosedGroupControlMessage closedGroupControlMessage = 104; - optional string syncTarget = 105; + optional string body = 1; + repeated AttachmentPointer attachments = 2; + // optional GroupContext group = 3; // No longer used + optional uint32 flags = 4; + optional uint32 expireTimer = 5; + optional bytes profileKey = 6; + optional uint64 timestamp = 7; + optional Quote quote = 8; + repeated Preview preview = 10; + optional Reaction reaction = 11; + optional LokiProfile profile = 101; + optional OpenGroupInvitation openGroupInvitation = 102; + optional ClosedGroupControlMessage closedGroupControlMessage = 104; + optional string syncTarget = 105; + optional bool blocksCommunityMessageRequests = 106; } message ConfigurationMessage { diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index a1a959306..4b2573c5e 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -31,6 +31,7 @@ extension MessageReceiver { db, publicKey: sender, name: profile.displayName, + blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, avatarUpdate: { guard let profilePictureUrl: String = profile.profilePictureUrl, diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 9b968bdab..747841f4e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -436,7 +436,8 @@ public final class MessageSender { // Attach the user's profile message.profile = VisibleMessage.VMProfile( - profile: Profile.fetchOrCreateCurrentUser() + profile: Profile.fetchOrCreateCurrentUser(db), + blocksCommunityMessageRequests: !db[.checkForCommunityMessageRequests] ) if (message.profile?.displayName ?? "").isEmpty { diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift index 019b19829..b6a8b86f0 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift @@ -573,7 +573,8 @@ private extension SessionUtil { count: ProfileManager.avatarAES256KeyByteLength ) ), - lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000) + lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000), + lastBlocksCommunityMessageRequests: 0 ) result[contactId] = ContactData( diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift index 909ea9ce7..c05bc17c3 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift @@ -213,6 +213,30 @@ internal extension SessionUtil { return updated } + static func updatingSetting(_ db: Database, _ updated: Setting?) throws { + // Don't current support any nullable settings + guard let updatedSetting: Setting = updated else { return } + + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + // Currently the only synced setting is 'checkForCommunityMessageRequests' + switch updatedSetting.id { + case Setting.BoolKey.checkForCommunityMessageRequests.rawValue: + try SessionUtil.performAndPushChange( + db, + for: .userProfile, + publicKey: userPublicKey + ) { conf in + try SessionUtil.updateSettings( + checkForCommunityMessageRequests: updatedSetting.unsafeValue(as: Bool.self), + in: conf + ) + } + + default: break + } + } + static func kickFromConversationUIIfNeeded(removedThreadIds: [String]) { guard !removedThreadIds.isEmpty else { return } diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift index ed522930e..44ed7b2b2 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift @@ -12,6 +12,10 @@ internal extension SessionUtil { Profile.Columns.profileEncryptionKey ] + static let syncedSettings: [String] = [ + Setting.BoolKey.checkForCommunityMessageRequests.rawValue + ] + // MARK: - Incoming Changes static func handleUserProfileUpdate( @@ -115,6 +119,17 @@ internal extension SessionUtil { } } + // Update settings if needed + let updatedAllowBlindedMessageRequests: Int32 = user_profile_get_blinded_msgreqs(conf) + let updatedAllowBlindedMessageRequestsBoolValue: Bool = (updatedAllowBlindedMessageRequests >= 1) + + if + updatedAllowBlindedMessageRequests >= 0 && + updatedAllowBlindedMessageRequestsBoolValue != db[.checkForCommunityMessageRequests] + { + db[.checkForCommunityMessageRequests] = updatedAllowBlindedMessageRequestsBoolValue + } + // Create a contact for the current user if needed (also force-approve the current user // in case the account got into a weird state or restored directly from a migration) let userContact: Contact = Contact.fetchOrCreate(db, id: userPublicKey) @@ -159,4 +174,15 @@ internal extension SessionUtil { user_profile_set_nts_priority(conf, priority) } + + static func updateSettings( + checkForCommunityMessageRequests: Bool? = nil, + in conf: UnsafeMutablePointer? + ) throws { + guard conf != nil else { throw SessionUtilError.nilConfigObject } + + if let blindedMessageRequests: Bool = checkForCommunityMessageRequests { + user_profile_set_blinded_msgreqs(conf, (blindedMessageRequests ? 1 : 0)) + } + } } diff --git a/SessionMessagingKit/SessionUtil/Database/Setting+Utilities.swift b/SessionMessagingKit/SessionUtil/Database/Setting+Utilities.swift new file mode 100644 index 000000000..2e178c5b1 --- /dev/null +++ b/SessionMessagingKit/SessionUtil/Database/Setting+Utilities.swift @@ -0,0 +1,58 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +public extension Database { + func setAndUpdateConfig(_ key: Setting.BoolKey, to newValue: Bool) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.DoubleKey, to newValue: Double?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.IntKey, to newValue: Int?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.StringKey, to newValue: String?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.EnumKey, to newValue: T?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + func setAndUpdateConfig(_ key: Setting.EnumKey, to newValue: T?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + /// Value will be stored as a timestamp in seconds since 1970 + func setAndUpdateConfig(_ key: Setting.DateKey, to newValue: Date?) throws { + try updateConfigIfNeeded(self, key: key.rawValue, updatedSetting: self.setting(key: key, to: newValue)) + } + + private func updateConfigIfNeeded( + _ db: Database, + key: String, + updatedSetting: Setting? + ) throws { + // Before we do anything custom make sure the setting should trigger a change + guard SessionUtil.syncedSettings.contains(key) else { return } + + defer { + // If we changed a column that requires a config update then we may as well automatically + // enqueue a new config sync job once the transaction completes (but only enqueue it once + // per transaction - doing it more than once is pointless) + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in + ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey) + } + } + + try SessionUtil.updatingSetting(db, updatedSetting) + } +} diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 0ee0f5f5a..c9d0c43eb 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -104,7 +104,11 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat public var canWrite: Bool { switch threadVariant { - case .contact: return true + case .contact: + guard threadIsMessageRequest == true else { return true } + + return (profile?.blocksCommunityMessageRequests != true) + case .legacyGroup, .group: return ( currentUserIsClosedGroupMember == true && diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index 34a00860e..4713e8ce1 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -65,6 +65,9 @@ public extension Setting.BoolKey { /// Controls whether concurrent audio messages should automatically be played after the one the user starts /// playing finishes static let shouldAutoPlayConsecutiveAudioMessages: Setting.BoolKey = "shouldAutoPlayConsecutiveAudioMessages" + + /// Controls whether the device will poll for community message requests (SOGS `/inbox` endpoint) + static let checkForCommunityMessageRequests: Setting.BoolKey = "checkForCommunityMessageRequests" } public extension Setting.StringKey { diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index 7266b30f6..7597c683a 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -498,6 +498,7 @@ public struct ProfileManager { _ db: Database, publicKey: String, name: String?, + blocksCommunityMessageRequests: Bool? = nil, avatarUpdate: AvatarUpdate, sentTimestamp: TimeInterval, calledFromConfigHandling: Bool = false, @@ -516,6 +517,12 @@ public struct ProfileManager { } } + // Blocks community message requets flag + if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > profile.lastBlocksCommunityMessageRequests { + profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) + profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) + } + // Profile picture & profile key var avatarNeedsDownload: Bool = false var targetAvatarUrl: String? = nil diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift index ce0ba7a6e..d7f08b3a1 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift @@ -285,6 +285,17 @@ class ConfigUserProfileSpec { ) user_profile_set_pic(conf2, p2) + user_profile_set_nts_expiry(conf2, 86200) + expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) + + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) + user_profile_set_blinded_msgreqs(conf2, 0) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(0)) + user_profile_set_blinded_msgreqs(conf2, -1) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1)) + user_profile_set_blinded_msgreqs(conf2, 1) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) + // Both have changes, so push need a push expect(config_needs_push(conf)).to(beTrue()) expect(config_needs_push(conf2)).to(beTrue()) @@ -364,6 +375,10 @@ class ConfigUserProfileSpec { .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) expect(user_profile_get_nts_priority(conf)).to(equal(9)) expect(user_profile_get_nts_priority(conf2)).to(equal(9)) + expect(user_profile_get_nts_expiry(conf)).to(equal(86400)) + expect(user_profile_get_nts_expiry(conf2)).to(equal(86400)) + expect(user_profile_get_blinded_msgreqs(conf)).to(equal(1)) + expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) let fakeHash4: String = "fakehash4" var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 457d5e277..c2d6c1ad6 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -211,11 +211,11 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { } func shareViewWasCompleted() { - extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } func shareViewWasCancelled() { - extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } func shareViewFailed(error: Error) { diff --git a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift index a0985631e..c4a2dc0cb 100644 --- a/SessionUtilitiesKit/Combine/Publisher+Utilities.swift +++ b/SessionUtilitiesKit/Combine/Publisher+Utilities.swift @@ -65,6 +65,23 @@ public extension Publisher { return self.receive(on: scheduler, options: options) .eraseToAnyPublisher() } + + func manualRefreshFrom(_ refreshTrigger: some Publisher) -> AnyPublisher { + return Publishers + .CombineLatest(refreshTrigger.prepend(()).setFailureType(to: Failure.self), self) + .map { _, value in value } + .eraseToAnyPublisher() + } + + func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> { + scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) } + .compactMap { $0 } + .eraseToAnyPublisher() + } + + func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> { + scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher() + } } // MARK: - Convenience diff --git a/SessionUtilitiesKit/Database/Models/Setting.swift b/SessionUtilitiesKit/Database/Models/Setting.swift index 1f634dac5..163fd01b8 100644 --- a/SessionUtilitiesKit/Database/Models/Setting.swift +++ b/SessionUtilitiesKit/Database/Models/Setting.swift @@ -15,6 +15,7 @@ public struct Setting: Codable, Identifiable, FetchableRecord, PersistableRecord } public var id: String { key } + public var rawValue: Data { value } let key: String let value: Data @@ -53,7 +54,7 @@ extension Setting { self.value = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue)) } - fileprivate func value(as type: Bool.Type) -> Bool? { + public func unsafeValue(as type: Bool.Type) -> Bool? { // Note: The 'assumingMemoryBound' is essentially going to try to convert // the memory into the provided type so can result in invalid data being // returned if the type is incorrect. But it does seem safer than the 'load' @@ -189,7 +190,7 @@ public extension Database { subscript(key: Setting.BoolKey) -> Bool { get { // Default to false if it doesn't exist - (self[key.rawValue]?.value(as: Bool.self) ?? false) + (self[key.rawValue]?.unsafeValue(as: Bool.self) ?? false) } set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) } } @@ -245,4 +246,47 @@ public extension Database { ) } } + + func setting(key: Setting.BoolKey, to newValue: Bool) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.DoubleKey, to newValue: Double?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.IntKey, to newValue: Int?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.StringKey, to newValue: String?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.EnumKey, to newValue: T?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue?.rawValue) + self[key.rawValue] = result + return result + } + + func setting(key: Setting.EnumKey, to newValue: T?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue?.rawValue) + self[key.rawValue] = result + return result + } + + /// Value will be stored as a timestamp in seconds since 1970 + func setting(key: Setting.DateKey, to newValue: Date?) -> Setting? { + let result: Setting? = Setting(key: key.rawValue, value: newValue.map { $0.timeIntervalSince1970 }) + self[key.rawValue] = result + return result + } }