diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 813681aac2..8d8bd1006a 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -845,6 +845,7 @@ 88D6E93F254CF712003142D9 /* group_call_leave.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 88D6E93D254CF712003142D9 /* group_call_leave.aiff */; }; 88D6E940254CF712003142D9 /* group_call_join.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 88D6E93E254CF712003142D9 /* group_call_join.aiff */; }; 88D6E94325535D49003142D9 /* CVTextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D6E94125535482003142D9 /* CVTextTest.swift */; }; + 88DBDFB9263731C800C2101C /* DefaultDisappearingMessageTimerInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBDFB8263731C800C2101C /* DefaultDisappearingMessageTimerInteraction.swift */; }; 88E34F2722F269E900966CC2 /* StorageServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E34F2622F269E900966CC2 /* StorageServiceManager.swift */; }; 88E34F2922F26CC100966CC2 /* StorageServiceProto+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E34F2822F26CC100966CC2 /* StorageServiceProto+Sync.swift */; }; 88E728FF25F0241100A2E4A4 /* AccountSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E728FE25F0241100A2E4A4 /* AccountSettingsViewController.swift */; }; @@ -1980,6 +1981,7 @@ 88D6E93D254CF712003142D9 /* group_call_leave.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = group_call_leave.aiff; sourceTree = ""; }; 88D6E93E254CF712003142D9 /* group_call_join.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = group_call_join.aiff; sourceTree = ""; }; 88D6E94125535482003142D9 /* CVTextTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVTextTest.swift; sourceTree = ""; }; + 88DBDFB8263731C800C2101C /* DefaultDisappearingMessageTimerInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDisappearingMessageTimerInteraction.swift; sourceTree = ""; }; 88E34F2622F269E900966CC2 /* StorageServiceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceManager.swift; sourceTree = ""; }; 88E34F2822F26CC100966CC2 /* StorageServiceProto+Sync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageServiceProto+Sync.swift"; sourceTree = ""; }; 88E728FE25F0241100A2E4A4 /* AccountSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingsViewController.swift; sourceTree = ""; }; @@ -2508,6 +2510,7 @@ 88D1D40322EF8A9700F472C5 /* ThreadDetailsInteraction.swift */, 34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */, 346CD5C126306578004162D6 /* UnknownThreadWarningInteraction.swift */, + 88DBDFB8263731C800C2101C /* DefaultDisappearingMessageTimerInteraction.swift */, 4CFF115223A9C2130007F9D7 /* UnreadIndicatorInteraction.swift */, ); path = ConversationView; @@ -5465,6 +5468,7 @@ 346EAA14250199A400E8AB6F /* MemberRequestView.swift in Sources */, 3426A37A2563F0EA0036407F /* CVComponentBottomButtons.swift in Sources */, 32ECD08824BFBF8000EDDED0 /* HelpViewController.swift in Sources */, + 88DBDFB9263731C800C2101C /* DefaultDisappearingMessageTimerInteraction.swift in Sources */, 3497972125DAA86100E99FA4 /* SendPaymentViewController.swift in Sources */, 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, 45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */, diff --git a/Signal/src/ViewControllers/AppSettings/Privacy/PrivacySettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Privacy/PrivacySettingsViewController.swift index a309c8c024..d4cf47e5fa 100644 --- a/Signal/src/ViewControllers/AppSettings/Privacy/PrivacySettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Privacy/PrivacySettingsViewController.swift @@ -61,7 +61,7 @@ class PrivacySettingsViewController: OWSTableViewController2 { )) } - if FeatureFlags.phoneNumberDiscoverability { + if FeatureFlags.phoneNumberDiscoverability, tsAccountManager.isPrimaryDevice { whoCanSection.add(.disclosureItem( withText: NSLocalizedString( "SETTINGS_PHONE_NUMBER_DISCOVERABILITY", @@ -108,6 +108,49 @@ class PrivacySettingsViewController: OWSTableViewController2 { )) contents.addSection(messagingSection) + let disappearingMessagesSection = OWSTableSection() + disappearingMessagesSection.headerTitle = NSLocalizedString( + "SETTINGS_DISAPPEARING_MESSAGES", + comment: "Label for the 'disappearig messages' privacy settings." + ) + disappearingMessagesSection.footerTitle = NSLocalizedString( + "SETTINGS_DISAPPEARING_MESSAGES_FOOTER", + comment: "Explanation for the 'disappearig messages' privacy settings." + ) + let disappearingMessagesConfiguration = databaseStorage.read { transaction in + OWSDisappearingMessagesConfiguration.fetchOrBuildDefaultUniversalConfiguration(with: transaction) + } + disappearingMessagesSection.add(.init( + customCellBlock: { [weak self] in + guard let self = self else { return UITableViewCell() } + let cell = OWSTableItem.buildIconNameCell( + icon: disappearingMessagesConfiguration.isEnabled + ? .settingsTimer + : .settingsTimerDisabled, + itemName: NSLocalizedString( + "SETTINGS_DEFAULT_DISAPPEARING_MESSAGES", + comment: "table cell label in conversation settings" + ), + accessoryText: disappearingMessagesConfiguration.isEnabled + ? NSString.formatDurationSeconds(disappearingMessagesConfiguration.durationSeconds, useShortFormat: true) + : CommonStrings.switchOff, + accessoryType: .disclosureIndicator, + accessoryImage: nil, + accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "disappearing_messages") + ) + return cell + }, actionBlock: { [weak self] in + let vc = DisappearingMessagesTimerSettingsViewController(configuration: disappearingMessagesConfiguration) { configuration in + self?.databaseStorage.write { transaction in + configuration.anyUpsert(transaction: transaction) + } + self?.updateTableContents() + } + self?.presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true) + } + )) + contents.addSection(disappearingMessagesSection) + let appSecuritySection = OWSTableSection() appSecuritySection.headerTitle = NSLocalizedString("SETTINGS_SECURITY_TITLE", comment: "Section header") diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift index e61d1f5cc5..8d19fcaa43 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponent.swift @@ -302,6 +302,7 @@ public enum CVComponentKey: CustomStringConvertible, CaseIterable { case threadDetails case failedOrPendingDownloads case unknownThreadWarning + case defaultDisappearingMessageTimer public var description: String { switch self { @@ -349,6 +350,8 @@ public enum CVComponentKey: CustomStringConvertible, CaseIterable { return ".failedOrPendingDownloads" case .sendFailureBadge: return ".sendFailureBadge" + case .defaultDisappearingMessageTimer: + return ".defaultDisappearingMessageTimer" } } diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift index a11b03f869..8d69a89838 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponentState.swift @@ -210,6 +210,9 @@ public class CVComponentState: Equatable, Dependencies { typealias UnknownThreadWarning = CVComponentState.SystemMessage let unknownThreadWarning: UnknownThreadWarning? + typealias DefaultDisappearingMessageTimer = CVComponentState.SystemMessage + let defaultDisappearingMessageTimer: DefaultDisappearingMessageTimer? + struct BottomButtons: Equatable { let actions: [CVMessageAction] } @@ -243,6 +246,7 @@ public class CVComponentState: Equatable, Dependencies { typingIndicator: TypingIndicator?, threadDetails: ThreadDetails?, unknownThreadWarning: UnknownThreadWarning?, + defaultDisappearingMessageTimer: DefaultDisappearingMessageTimer?, bottomButtons: BottomButtons?, failedOrPendingDownloads: FailedOrPendingDownloads?, sendFailureBadge: SendFailureBadge?) { @@ -266,6 +270,7 @@ public class CVComponentState: Equatable, Dependencies { self.typingIndicator = typingIndicator self.threadDetails = threadDetails self.unknownThreadWarning = unknownThreadWarning + self.defaultDisappearingMessageTimer = defaultDisappearingMessageTimer self.bottomButtons = bottomButtons self.failedOrPendingDownloads = failedOrPendingDownloads self.sendFailureBadge = sendFailureBadge @@ -293,6 +298,7 @@ public class CVComponentState: Equatable, Dependencies { lhs.typingIndicator == rhs.typingIndicator && lhs.threadDetails == rhs.threadDetails && lhs.unknownThreadWarning == rhs.unknownThreadWarning && + lhs.defaultDisappearingMessageTimer == rhs.defaultDisappearingMessageTimer && lhs.bottomButtons == rhs.bottomButtons && lhs.failedOrPendingDownloads == rhs.failedOrPendingDownloads && lhs.sendFailureBadge == rhs.sendFailureBadge) @@ -318,6 +324,7 @@ public class CVComponentState: Equatable, Dependencies { typealias TypingIndicator = CVComponentState.TypingIndicator typealias ThreadDetails = CVComponentState.ThreadDetails typealias UnknownThreadWarning = CVComponentState.UnknownThreadWarning + typealias DefaultDisappearingMessageTimer = CVComponentState.DefaultDisappearingMessageTimer typealias FailedOrPendingDownloads = CVComponentState.FailedOrPendingDownloads typealias BottomButtons = CVComponentState.BottomButtons typealias SendFailureBadge = CVComponentState.SendFailureBadge @@ -342,6 +349,7 @@ public class CVComponentState: Equatable, Dependencies { var typingIndicator: TypingIndicator? var threadDetails: ThreadDetails? var unknownThreadWarning: UnknownThreadWarning? + var defaultDisappearingMessageTimer: DefaultDisappearingMessageTimer? var reactions: Reactions? var failedOrPendingDownloads: FailedOrPendingDownloads? var sendFailureBadge: SendFailureBadge? @@ -378,6 +386,7 @@ public class CVComponentState: Equatable, Dependencies { typingIndicator: typingIndicator, threadDetails: threadDetails, unknownThreadWarning: unknownThreadWarning, + defaultDisappearingMessageTimer: defaultDisappearingMessageTimer, bottomButtons: bottomButtons, failedOrPendingDownloads: failedOrPendingDownloads, sendFailureBadge: sendFailureBadge) @@ -409,6 +418,9 @@ public class CVComponentState: Equatable, Dependencies { if unknownThreadWarning != nil { return .unknownThreadWarning } + if defaultDisappearingMessageTimer != nil { + return .defaultDisappearingMessageTimer + } if systemMessage != nil { return .systemMessage } @@ -504,6 +516,9 @@ public class CVComponentState: Equatable, Dependencies { if unknownThreadWarning != nil { result.insert(.unknownThreadWarning) } + if defaultDisappearingMessageTimer != nil { + result.insert(.defaultDisappearingMessageTimer) + } if bottomButtons != nil { result.insert(.bottomButtons) } @@ -611,6 +626,13 @@ fileprivate extension CVComponentState.Builder { threadViewModel: threadViewModel, transaction: transaction) return build() + case .defaultDisappearingMessageTimer: + self.unknownThreadWarning = CVComponentSystemMessage.buildDefaultDisappearingMessageTimerState( + interaction: interaction, + threadViewModel: threadViewModel, + transaction: transaction + ) + return build() case .typingIndicator: guard let typingIndicatorInteraction = interaction as? TypingIndicatorInteraction else { owsFailDebug("Invalid typingIndicator.") diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift index 0bf0bb02f1..8f5021fa83 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentMessage.swift @@ -120,7 +120,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { // We don't render sender avatars with a subcomponent. case .senderAvatar: return nil - case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge, .unknownThreadWarning: + case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge, .unknownThreadWarning, .defaultDisappearingMessageTimer: return nil } } @@ -1239,7 +1239,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { case .senderAvatar: owsFailDebug("Invalid component key: \(key)") return nil - case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge, .unknownThreadWarning: + case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge, .unknownThreadWarning, .defaultDisappearingMessageTimer: owsFailDebug("Invalid component key: \(key)") return nil } @@ -1277,7 +1277,7 @@ public class CVComponentMessage: CVComponentBase, CVRootComponent { // We don't render sender avatars with a subcomponent. case .senderAvatar: owsAssertDebug(subcomponentView == nil) - case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge, .unknownThreadWarning: + case .systemMessage, .dateHeader, .unreadIndicator, .typingIndicator, .threadDetails, .failedOrPendingDownloads, .sendFailureBadge, .unknownThreadWarning, .defaultDisappearingMessageTimer: owsAssertDebug(subcomponentView == nil) } } diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift index 6a210197e6..a180fcd140 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentSystemMessage.swift @@ -782,6 +782,36 @@ extension CVComponentSystemMessage { } } + // MARK: - Default Disappearing Message Timer + + static func buildDefaultDisappearingMessageTimerState( + interaction: TSInteraction, + threadViewModel: ThreadViewModel, + transaction: SDSAnyReadTransaction + ) -> CVComponentState.SystemMessage { + let configuration = OWSDisappearingMessagesConfiguration.fetchOrBuildDefaultUniversalConfiguration(with: transaction) + + let labelText = NSMutableAttributedString() + labelText.appendImage( + Theme.iconImage(.timer16).withRenderingMode(.alwaysTemplate), + font: Self.titleLabelFont, + heightReference: ImageAttachmentHeightReference.lineHeight + ) + labelText.append(" ", attributes: [:]) + + let titleFormat = NSLocalizedString( + "SYSTEM_MESSAGE_DEFAULT_DISAPPEARING_MESSAGE_TIMER_FORMAT", + comment: "Indicator that the default disappearing message timer will be applied when you send a message. Embeds {default disappearing message time}" + ) + labelText.append(String(format: titleFormat, configuration.durationString)) + + return CVComponentState.SystemMessage( + title: labelText, + titleColor: Theme.secondaryTextAndIconColor, + action: nil + ) + } + // MARK: - Actions static func action(forInteraction interaction: TSInteraction, diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVItemViewModelImpl.swift b/Signal/src/ViewControllers/ConversationView/CV/CVItemViewModelImpl.swift index bde4957131..6b6447de00 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVItemViewModelImpl.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVItemViewModelImpl.swift @@ -214,7 +214,7 @@ extension CVItemViewModelImpl { } switch messageCellType { - case .unknown, .dateHeader, .typingIndicator, .unreadIndicator, .threadDetails, .systemMessage, .unknownThreadWarning: + case .unknown, .dateHeader, .typingIndicator, .unreadIndicator, .threadDetails, .systemMessage, .unknownThreadWarning, .defaultDisappearingMessageTimer: return false case .textOnlyMessage, .audio, .genericAttachment, .contactShare, .bodyMedia, .viewOnce, .stickerMessage: return !hasUnloadedAttachments diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift b/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift index 7535a04122..ec9dbbdc26 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVItemViewState.swift @@ -91,7 +91,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { // The thread details should have a stable timestamp. let threadDetailsTimestamp: UInt64 if let firstInteraction = messageMapping.loadedInteractions.first { - threadDetailsTimestamp = max(1, firstInteraction.timestamp) - 2 + threadDetailsTimestamp = max(1, firstInteraction.timestamp) - 3 } else { threadDetailsTimestamp = 1 } @@ -109,7 +109,7 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { // The "Unknown Thread Warning" should have a stable timestamp. let timestamp: UInt64 if let firstInteraction = messageMapping.loadedInteractions.first { - timestamp = max(1, firstInteraction.timestamp) - 1 + timestamp = max(1, firstInteraction.timestamp) - 2 } else { timestamp = 2 } @@ -119,6 +119,25 @@ struct CVItemModelBuilder: CVItemBuilding, Dependencies { owsAssertDebug(item != nil) } + if messageMapping.shouldShowDefaultTimerIndicator( + thread: thread, + transaction: transaction + ) { + // The default timer message should have a stable timestamp. + let timestamp: UInt64 + if let firstInteraction = messageMapping.loadedInteractions.first { + timestamp = max(1, firstInteraction.timestamp) - 1 + } else { + timestamp = 3 + } + let interaction = DefaultDisappearingMessageTimerInteraction( + thread: thread, + timestamp: timestamp + ) + let item = addItem(interaction: interaction) + owsAssertDebug(item != nil) + } + var interactionIds = Set() for interaction in messageMapping.loadedInteractions { guard !interactionIds.contains(interaction.uniqueId) else { @@ -623,6 +642,11 @@ fileprivate extension CVMessageMapping { !canLoadOlder && Self.contactsManagerImpl.shouldShowUnknownThreadWarning(thread: thread, transaction: transaction) } + func shouldShowDefaultTimerIndicator(thread: TSThread, transaction: SDSAnyReadTransaction) -> Bool { + guard !thread.shouldThreadBeVisible else { return false } + guard thread.lastInteractionRowId == 0 else { return false } + return OWSDisappearingMessagesConfiguration.fetchOrBuildDefaultUniversalConfiguration(with: transaction).isEnabled + } } // MARK: - @@ -655,7 +679,7 @@ private class ItemBuilder { var canShowDate: Bool { switch interaction.interactionType() { - case .unknown, .typingIndicator, .threadDetails, .dateHeader, .unknownThreadWarning: + case .unknown, .typingIndicator, .threadDetails, .dateHeader, .unknownThreadWarning, .defaultDisappearingMessageTimer: return false case .info: guard let infoMessage = interaction as? TSInfoMessage else { diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVNode.swift b/Signal/src/ViewControllers/ConversationView/CV/CVNode.swift index 3ee30a5b2c..ec72c6ea1d 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVNode.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVNode.swift @@ -24,6 +24,7 @@ public enum CVMessageCellType: Int, CustomStringConvertible, Equatable { case threadDetails case systemMessage case unknownThreadWarning + case defaultDisappearingMessageTimer // MARK: - CustomStringConvertible @@ -44,6 +45,7 @@ public enum CVMessageCellType: Int, CustomStringConvertible, Equatable { case .threadDetails: return "threadDetails" case .systemMessage: return "systemMessage" case .unknownThreadWarning: return "unknownThreadWarning" + case .defaultDisappearingMessageTimer: return "defaultDisappearingMessageTimer" } } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController+Scroll.swift b/Signal/src/ViewControllers/ConversationView/ConversationViewController+Scroll.swift index 43f7c4a2d2..5a1e1ae9b5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController+Scroll.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController+Scroll.swift @@ -631,7 +631,7 @@ extension ConversationViewController { switch interaction.interactionType() { case .unknown, .unreadIndicator, .dateHeader, .typingIndicator: return false - case .incomingMessage, .outgoingMessage, .error, .call, .info, .threadDetails, .unknownThreadWarning: + case .incomingMessage, .outgoingMessage, .error, .call, .info, .threadDetails, .unknownThreadWarning, .defaultDisappearingMessageTimer: return true } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 0c32357b1a..a6c27c99b0 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1370,7 +1370,7 @@ typedef enum : NSUInteger { [self removeGroupCallTooltip]; // We initiated a call, so if there was a pending message request we should accept it. - [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction:self.thread]; + [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction:self.thread]; [GroupCallViewController presentLobbyForThread:(TSGroupThread *)self.thread]; } @@ -1420,7 +1420,7 @@ typedef enum : NSUInteger { } // We initiated a call, so if there was a pending message request we should accept it. - [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction:self.thread]; + [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction:self.thread]; [self.outboundIndividualCallInitiator initiateCallWithAddress:contactThread.contactAddress isVideo:isVideo]; } @@ -2370,8 +2370,9 @@ typedef enum : NSUInteger { __block BOOL didAddToProfileWhitelist; TSThread *thread = self.thread; DatabaseStorageAsyncWrite(SDSDatabaseStorage.shared, ^(SDSAnyWriteTransaction *transaction) { - didAddToProfileWhitelist = [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequest:thread - transaction:transaction]; + didAddToProfileWhitelist = + [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer:thread + transaction:transaction]; // TODO - in line with QuotedReply and other message attachments, saving should happen as part of sending // preparation rather than duplicated here and in the SAE @@ -2777,7 +2778,8 @@ typedef enum : NSUInteger { DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) { // We updated the group, so if there was a pending message request we should accept it. - [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequest:self.thread transaction:transaction]; + [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer:self.thread + transaction:transaction]; }); } @@ -2915,8 +2917,8 @@ typedef enum : NSUInteger { } } - BOOL didAddToProfileWhitelist = - [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction:self.thread]; + BOOL didAddToProfileWhitelist = [ThreadUtil + addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction:self.thread]; __block TSOutgoingMessage *message; [self.databaseStorage uiReadWithBlock:^(SDSAnyReadTransaction *_Nonnull transaction) { @@ -3436,8 +3438,8 @@ typedef enum : NSUInteger { return; } - BOOL didAddToProfileWhitelist = - [ThreadUtil addThreadToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction:self.thread]; + BOOL didAddToProfileWhitelist = [ThreadUtil + addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction:self.thread]; __block TSOutgoingMessage *message; [self.databaseStorage uiReadWithBlock:^(SDSAnyReadTransaction *transaction) { diff --git a/Signal/src/ViewControllers/ConversationView/DefaultDisappearingMessageTimerInteraction.swift b/Signal/src/ViewControllers/ConversationView/DefaultDisappearingMessageTimerInteraction.swift new file mode 100644 index 0000000000..f73cfb8b6b --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/DefaultDisappearingMessageTimerInteraction.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc(OWSDefaultDisappearingMessageTimerInteraction) +public class DefaultDisappearingMessageTimerInteraction: TSInteraction { + + @objc + public override func isDynamicInteraction() -> Bool { + return true + } + + @objc + public override func interactionType() -> OWSInteractionType { + return .defaultDisappearingMessageTimer + } + + @available(*, unavailable, message: "use other constructor instead.") + @objc + public required init(coder aDecoder: NSCoder) { + notImplemented() + } + + @available(*, unavailable, message: "use other constructor instead.") + @objc + public required init(dictionary dictionaryValue: [String: Any]!) throws { + notImplemented() + } + + @objc + public init(thread: TSThread, timestamp: UInt64) { + // Include timestamp in uniqueId to ensure invariant that + // interactions don't move in the chat history ordering. + super.init( + uniqueId: "DefaultDisappearingMessageTimer_\(timestamp)", + timestamp: timestamp, + thread: thread + ) + } + + public override var shouldBeSaved: Bool { + return false + } + + @objc + public override func anyWillInsert(with transaction: SDSAnyWriteTransaction) { + owsFailDebug("The transient interaction should not be saved in the database.") + } +} diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIContacts.m b/Signal/src/ViewControllers/DebugUI/DebugUIContacts.m index 204a196c7a..c9000e1c1c 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIContacts.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIContacts.m @@ -103,6 +103,7 @@ NS_ASSUME_NONNULL_BEGIN groupId:nil name:groupName avatarData:nil + disappearingMessageToken:DisappearingMessageToken.disabledToken newGroupSeed:nil shouldSendMessage:YES success:^(TSGroupThread *thread) { [SignalApp.shared presentConversationForThread:thread animated:YES]; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift b/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift index 18d4e52d6f..b8daaead16 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIGroupsV2.swift @@ -589,6 +589,7 @@ class DebugUIGroupsV2: DebugUIPage { return firstly { GroupManager.localCreateNewGroup(members: [otherUserAddress], name: "Real group, both users are in the group", + disappearingMessageToken: .disabledToken, shouldSendMessage: false) }.map(on: .global()) { (groupThread: TSGroupThread) in guard let validGroupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { @@ -602,6 +603,7 @@ class DebugUIGroupsV2: DebugUIPage { return firstly { GroupManager.localCreateNewGroup(members: [], name: "Real group, recipient is not in the group", + disappearingMessageToken: .disabledToken, shouldSendMessage: false) }.map(on: .global()) { (groupThread: TSGroupThread) in guard let missingOtherUserGroupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { @@ -617,6 +619,7 @@ class DebugUIGroupsV2: DebugUIPage { return firstly { () -> Promise in GroupManager.localCreateNewGroup(members: [otherUserAddress], name: "Real group, sender is not in the group", + disappearingMessageToken: .disabledToken, shouldSendMessage: false) }.then(on: .global()) { (groupThread: TSGroupThread) -> Promise in guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages+OWS.swift b/Signal/src/ViewControllers/DebugUI/DebugUIMessages+OWS.swift index 6810daa973..7cfcd9d74e 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages+OWS.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages+OWS.swift @@ -64,7 +64,7 @@ public extension DebugUIMessages { let members = uuidMembers + [TSAccountManager.localAddress!] let groupName = "UUID Group" - _ = GroupManager.localCreateNewGroup(members: members, name: groupName, shouldSendMessage: true) + _ = GroupManager.localCreateNewGroup(members: members, name: groupName, disappearingMessageToken: .disabledToken, shouldSendMessage: true) } } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index a24c9c5694..518ba05a6b 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -4362,14 +4362,11 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction); groupId:nil name:groupName avatarData:nil + disappearingMessageToken:DisappearingMessageToken.disabledToken newGroupSeed:nil shouldSendMessage:YES - success:^(TSGroupThread *thread) { - success(thread); - } - failure:^(NSError *error) { - OWSFailDebug(@"Error: %@", error); - }]; + success:^(TSGroupThread *thread) { success(thread); } + failure:^(NSError *error) { OWSFailDebug(@"Error: %@", error); }]; } + (void)testIndicScriptsInThread:(TSThread *)thread diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIStress.m b/Signal/src/ViewControllers/DebugUI/DebugUIStress.m index 8a3f5dad3b..51725809db 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIStress.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIStress.m @@ -575,6 +575,7 @@ NS_ASSUME_NONNULL_BEGIN groupId:nil name:NSUUID.UUID.UUIDString avatarData:nil + disappearingMessageToken:DisappearingMessageToken.disabledToken newGroupSeed:nil shouldSendMessage:NO success:^(TSGroupThread *thread) { [SignalApp.shared presentConversationForThread:thread animated:YES]; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIStress.swift b/Signal/src/ViewControllers/DebugUI/DebugUIStress.swift index 774449b693..ef74986b16 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIStress.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIStress.swift @@ -26,6 +26,7 @@ public extension DebugUIStress { groupId: nil, name: groupName, avatarData: oldGroupThread.groupModel.groupAvatarData, + disappearingMessageToken: .disabledToken, newGroupSeed: nil, shouldSendMessage: false) }.done { newGroupThread in @@ -103,6 +104,7 @@ public extension DebugUIStress { groupId: nil, name: groupName, avatarData: oldGroupThread.groupModel.groupAvatarData, + disappearingMessageToken: .disabledToken, newGroupSeed: nil, shouldSendMessage: false) }.done { (newGroupThread) in diff --git a/Signal/src/ViewControllers/ForwardMessageNavigationController.swift b/Signal/src/ViewControllers/ForwardMessageNavigationController.swift index ce4db90b2b..d9690c7d45 100644 --- a/Signal/src/ViewControllers/ForwardMessageNavigationController.swift +++ b/Signal/src/ViewControllers/ForwardMessageNavigationController.swift @@ -142,7 +142,7 @@ extension ForwardMessageNavigationController { pushViewController(approvalViewController, animated: true) case .unknown, .viewOnce, .dateHeader, .unreadIndicator, .typingIndicator, - .threadDetails, .systemMessage, .unknownThreadWarning: + .threadDetails, .systemMessage, .unknownThreadWarning, .defaultDisappearingMessageTimer: throw OWSAssertionError("Invalid message type.") } } @@ -257,7 +257,7 @@ extension ForwardMessageNavigationController { } // } case .unknown, .viewOnce, .dateHeader, .unreadIndicator, .typingIndicator, - .threadDetails, .systemMessage, .unknownThreadWarning: + .threadDetails, .systemMessage, .unknownThreadWarning, .defaultDisappearingMessageTimer: throw OWSAssertionError("Invalid message type.") } } diff --git a/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift b/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift index e47cab119b..8ecb6cdefa 100644 --- a/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift +++ b/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift @@ -27,6 +27,10 @@ public class NewGroupConfirmViewController: OWSViewController { private let recipientTableView = OWSTableViewController2() + private lazy var disappearingMessagesConfiguration = databaseStorage.read { transaction in + OWSDisappearingMessagesConfiguration.fetchOrBuildDefaultUniversalConfiguration(with: transaction) + } + required init(newGroupState: NewGroupState) { self.newGroupState = newGroupState @@ -203,6 +207,39 @@ public class NewGroupConfirmViewController: OWSViewController { // MARK: - private func updateTableContents() { + let contents = OWSTableContents() + + let disappearingMessagesSection = OWSTableSection() + disappearingMessagesSection.add(.init( + customCellBlock: { [weak self] in + guard let self = self else { return UITableViewCell() } + let cell = OWSTableItem.buildIconNameCell( + icon: self.disappearingMessagesConfiguration.isEnabled + ? .settingsTimer + : .settingsTimerDisabled, + itemName: NSLocalizedString( + "DISAPPEARING_MESSAGES", + comment: "table cell label in conversation settings" + ), + accessoryText: self.disappearingMessagesConfiguration.isEnabled + ? NSString.formatDurationSeconds(self.disappearingMessagesConfiguration.durationSeconds, useShortFormat: true) + : CommonStrings.switchOff, + accessoryType: .disclosureIndicator, + accessoryImage: nil, + accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "disappearing_messages") + ) + return cell + }, actionBlock: { [weak self] in + guard let self = self else { return } + let vc = DisappearingMessagesTimerSettingsViewController(configuration: self.disappearingMessagesConfiguration) { configuration in + self.disappearingMessagesConfiguration = configuration + self.updateTableContents() + } + self.presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true) + } + )) + contents.addSection(disappearingMessagesSection) + let section = OWSTableSection() section.headerTitle = NSLocalizedString("GROUP_MEMBERS_SECTION_TITLE_MEMBERS", comment: "Title for the 'members' section of the 'group members' view.") @@ -251,9 +288,8 @@ public class NewGroupConfirmViewController: OWSViewController { return cell }, actionBlock: nil)) } - - let contents = OWSTableContents() contents.addSection(section) + recipientTableView.contents = contents } @@ -284,6 +320,7 @@ public class NewGroupConfirmViewController: OWSViewController { let memberSet = Set([localAddress] + recipientSet.orderedMembers.compactMap { $0.address }) let members = Array(memberSet) let newGroupSeed = groupSeed + let disappearingMessageToken = disappearingMessagesConfiguration.asToken // GroupsV2 TODO: Should we allow cancel here? ModalActivityIndicatorViewController.present(fromViewController: self, @@ -293,6 +330,7 @@ public class NewGroupConfirmViewController: OWSViewController { groupId: nil, name: groupName, avatarData: avatarData, + disappearingMessageToken: disappearingMessageToken, newGroupSeed: newGroupSeed, shouldSendMessage: true) }.done { groupThread in diff --git a/Signal/src/ViewControllers/ThreadSettings/GroupAttributesEditorHelper.swift b/Signal/src/ViewControllers/ThreadSettings/GroupAttributesEditorHelper.swift index bf33fcea9e..13e9e4241e 100644 --- a/Signal/src/ViewControllers/ThreadSettings/GroupAttributesEditorHelper.swift +++ b/Signal/src/ViewControllers/ThreadSettings/GroupAttributesEditorHelper.swift @@ -40,6 +40,7 @@ class GroupAttributesEditorHelper: NSObject { private let iconViewSize: UInt private let cameraButton = GroupAttributesEditorHelper.buildCameraButtonForCenter() + private let cameraCornerButton = GroupAttributesEditorHelper.buildCameraButtonForCorner() let nameTextField = UITextField() @@ -101,6 +102,10 @@ class GroupAttributesEditorHelper: NSObject { avatarWrapper.addSubview(cameraButton) cameraButton.autoCenterInSuperview() + avatarWrapper.addSubview(cameraCornerButton) + cameraCornerButton.autoPinEdge(toSuperviewEdge: .trailing) + cameraCornerButton.autoPinEdge(toSuperviewEdge: .bottom) + nameTextField.text = groupNameOriginal nameTextField.font = .ows_dynamicTypeBody nameTextField.backgroundColor = .clear @@ -143,7 +148,7 @@ class GroupAttributesEditorHelper: NSObject { public static func buildCameraButtonForCenter() -> UIView { let cameraImageView = UIImageView() - cameraImageView.setTemplateImageName("camera-outline-24", tintColor: Theme.accentBlueColor) + cameraImageView.setTemplateImageName("camera-outline-24", tintColor: Theme.primaryIconColor) let iconSize: CGFloat = 32 cameraImageView.autoSetDimensions(to: CGSize(square: iconSize)) return cameraImageView @@ -152,12 +157,16 @@ class GroupAttributesEditorHelper: NSObject { private func updateAvatarView(groupAvatar: GroupAvatar?) { if let groupAvatar = groupAvatar { avatarImageView.image = groupAvatar.image - avatarImageView.backgroundColor = nil + avatarImageView.layer.borderWidth = 0 + avatarImageView.layer.borderColor = nil cameraButton.isHidden = true + cameraCornerButton.isHidden = false } else { avatarImageView.image = nil - avatarImageView.backgroundColor = Theme.washColor + avatarImageView.layer.borderWidth = 2 + avatarImageView.layer.borderColor = Theme.outlineColor.cgColor cameraButton.isHidden = false + cameraCornerButton.isHidden = true } } diff --git a/Signal/src/ViewControllers/ThreadSettings/GroupAttributesViewController.swift b/Signal/src/ViewControllers/ThreadSettings/GroupAttributesViewController.swift index 38013b02f8..ab9651ebc8 100644 --- a/Signal/src/ViewControllers/ThreadSettings/GroupAttributesViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/GroupAttributesViewController.swift @@ -71,11 +71,6 @@ class GroupAttributesViewController: OWSTableViewController2 { helper.delegate = self helper.buildContents(avatarViewHelperDelegate: self) - let cameraButton = GroupAttributesEditorHelper.buildCameraButtonForCorner() - helper.avatarWrapper.addSubview(cameraButton) - cameraButton.autoPinEdge(toSuperviewEdge: .trailing) - cameraButton.autoPinEdge(toSuperviewEdge: .bottom) - updateTableContents() } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 2e7ee36b76..bc6643ee14 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -4708,6 +4708,9 @@ /* The title for the data settings. */ "SETTINGS_DATA_TITLE" = "Data Usage"; +/* table cell label in conversation settings */ +"SETTINGS_DEFAULT_DISAPPEARING_MESSAGES" = "Default Timer for New Chats"; + /* No comment provided by engineer. */ "SETTINGS_DELETE_ACCOUNT_BUTTON" = "Delete Account"; @@ -4732,6 +4735,12 @@ /* Confirmation text for button which deletes all message, calling, attachments, etc. */ "SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON" = "Delete Everything"; +/* Label for the 'disappearig messages' privacy settings. */ +"SETTINGS_DISAPPEARING_MESSAGES" = "Disappearing Messages"; + +/* Explanation for the 'disappearig messages' privacy settings. */ +"SETTINGS_DISAPPEARING_MESSAGES_FOOTER" = "Set a default disappearing message timer for all new chats started by you."; + /* Title for the 'donate to signal' link in settings. */ "SETTINGS_DONATE" = "Donate to Signal"; @@ -5539,6 +5548,9 @@ /* Label for button to verify a user's safety number. */ "SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER" = "Verify"; +/* Indicator that the default disappearing message timer will be applied when you send a message. Embeds {default disappearing message time} */ +"SYSTEM_MESSAGE_DEFAULT_DISAPPEARING_MESSAGE_TIMER_FORMAT" = "The disappearing message time will be set to %@ when you message them."; + /* Indicator warning about an unknown contact thread. */ "SYSTEM_MESSAGE_UNKNOWN_THREAD_WARNING_CONTACT" = "No groups in common. Review requests carefully."; diff --git a/SignalMessaging/attachments/AttachmentMultisend.swift b/SignalMessaging/attachments/AttachmentMultisend.swift index 6e271ca0bb..344c7d3483 100644 --- a/SignalMessaging/attachments/AttachmentMultisend.swift +++ b/SignalMessaging/attachments/AttachmentMultisend.swift @@ -40,7 +40,7 @@ public class AttachmentMultisend: Dependencies { } // If this thread has a pending message request, treat it as accepted. - ThreadUtil.addThread(toProfileWhitelistIfEmptyOrPendingRequest: thread, transaction: transaction) + ThreadUtil.addThread(toProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer: thread, transaction: transaction) let message = try! ThreadUtil.createUnsentMessage(with: approvalMessageBody, mediaAttachments: attachments, @@ -113,7 +113,7 @@ public class AttachmentMultisend: Dependencies { } // If this thread has a pending message request, treat it as accepted. - ThreadUtil.addThread(toProfileWhitelistIfEmptyOrPendingRequest: thread, transaction: transaction) + ThreadUtil.addThread(toProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer: thread, transaction: transaction) let message = try! ThreadUtil.createUnsentMessage(with: approvalMessageBody, mediaAttachments: attachments, diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index 490626d2e7..e6696810c5 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -84,9 +84,10 @@ NS_ASSUME_NONNULL_BEGIN // the local user took an action like initiating a call or updating the DM timer. // // Returns YES IFF the thread was just added to the profile whitelist. -+ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction:(TSThread *)thread NS_SWIFT_NAME(addToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction(thread:)); -+ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequest:(TSThread *)thread - transaction:(SDSAnyWriteTransaction *)transaction; ++ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction:(TSThread *)thread + NS_SWIFT_NAME(addToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction(thread:)); ++ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer:(TSThread *)thread + transaction:(SDSAnyWriteTransaction *)transaction; #pragma mark - Delete Content diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 6c55e12b4d..af85ee9b30 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -271,15 +271,39 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Profile Whitelist -+ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction:(TSThread *)thread ++ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction:(TSThread *)thread { OWSAssertDebug(thread); __block BOOL hasPendingMessageRequest; + __block BOOL needsDefaultTimerSet; + __block DisappearingMessageToken *defaultTimerToken; [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { hasPendingMessageRequest = [thread hasPendingMessageRequestWithTransaction:transaction.unwrapGrdbRead]; + OWSDisappearingMessagesConfiguration *configuration = + [thread disappearingMessagesConfigurationWithTransaction:transaction]; + needsDefaultTimerSet = thread.lastInteractionRowId == 0 + && ![OWSDisappearingMessagesConfiguration anyExistsWithUniqueId:configuration.uniqueId + transaction:transaction]; + defaultTimerToken = configuration.asToken; }]; + if (needsDefaultTimerSet) { + DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) { + OWSDisappearingMessagesConfiguration *configuration = + [OWSDisappearingMessagesConfiguration applyToken:defaultTimerToken + toThread:thread + transaction:transaction]; + + OWSDisappearingConfigurationUpdateInfoMessage *infoMessage = + [[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithThread:thread + configuration:configuration + createdByRemoteName:nil + createdInExistingGroup:NO]; + [infoMessage anyInsertWithTransaction:transaction]; + }); + } + // If we're creating this thread or we have a pending message request, // any action we trigger should share our profile. if (!thread.shouldThreadBeVisible || hasPendingMessageRequest) { @@ -290,11 +314,30 @@ NS_ASSUME_NONNULL_BEGIN return NO; } -+ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequest:(TSThread *)thread - transaction:(SDSAnyWriteTransaction *)transaction ++ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer:(TSThread *)thread + transaction:(SDSAnyWriteTransaction *)transaction { OWSAssertDebug(thread); + OWSDisappearingMessagesConfiguration *defaultConfiguration = + [thread disappearingMessagesConfigurationWithTransaction:transaction]; + BOOL needsDefaultTimerSet = thread.lastInteractionRowId == 0 + && ![OWSDisappearingMessagesConfiguration anyExistsWithUniqueId:defaultConfiguration.uniqueId + transaction:transaction]; + DisappearingMessageToken *defaultTimerToken = defaultConfiguration.asToken; + + if (needsDefaultTimerSet) { + OWSDisappearingMessagesConfiguration *configuration = + [OWSDisappearingMessagesConfiguration applyToken:defaultTimerToken toThread:thread transaction:transaction]; + + OWSDisappearingConfigurationUpdateInfoMessage *infoMessage = + [[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithThread:thread + configuration:configuration + createdByRemoteName:nil + createdInExistingGroup:NO]; + [infoMessage anyInsertWithTransaction:transaction]; + } + BOOL hasPendingMessageRequest = [thread hasPendingMessageRequestWithTransaction:transaction.unwrapGrdbRead]; // If we're creating this thread or we have a pending message request, // any action we trigger should share our profile. diff --git a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h index 11a310fec2..a0b2369e69 100644 --- a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h +++ b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.h @@ -1,12 +1,12 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN -#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval +#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration 0 @class SDSAnyReadTransaction; @class TSThread; @@ -49,6 +49,8 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:durationSeconds:ena // for a given thread; do not use anyFetchWithUniqueId. + (instancetype)fetchOrBuildDefaultWithThread:(TSThread *)thread transaction:(SDSAnyReadTransaction *)transaction; ++ (instancetype)fetchOrBuildDefaultUniversalConfigurationWithTransaction:(SDSAnyReadTransaction *)transaction; + + (NSArray *)validDurationsSeconds; + (uint32_t)maxDurationSeconds; diff --git a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m index 43919e50a7..f78bd09158 100644 --- a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m +++ b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // #import "OWSDisappearingMessagesConfiguration.h" @@ -9,6 +9,8 @@ NS_ASSUME_NONNULL_BEGIN +static NSString *const kUniversalTimerThreadId = @"kUniversalTimerThreadId"; + @interface OWSDisappearingMessagesConfiguration () @property (nonatomic, getter=isEnabled) BOOL enabled; @@ -96,10 +98,15 @@ NS_ASSUME_NONNULL_BEGIN } return [[self alloc] initWithThreadId:threadId - enabled:NO + enabled:OWSDisappearingMessagesConfigurationDefaultExpirationDuration > 0 durationSeconds:OWSDisappearingMessagesConfigurationDefaultExpirationDuration]; } ++ (instancetype)fetchOrBuildDefaultUniversalConfigurationWithTransaction:(SDSAnyReadTransaction *)transaction +{ + return [self fetchOrBuildDefaultWithThreadId:kUniversalTimerThreadId transaction:transaction]; +} + + (NSArray *)validDurationsSeconds { return @[ @@ -113,7 +120,8 @@ NS_ASSUME_NONNULL_BEGIN @(6 * kHourInterval), @(12 * kHourInterval), @(24 * kHourInterval), - @(1 * kWeekInterval) + @(1 * kWeekInterval), + @(4 * kWeekInterval) ]; } diff --git a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift index b52d18c42c..464952e4f1 100644 --- a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift +++ b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. // import Foundation @@ -66,10 +66,12 @@ public class DisappearingMessageToken: MTLModel { // MARK: - public extension OWSDisappearingMessagesConfiguration { + @objc var asToken: DisappearingMessageToken { return DisappearingMessageToken(isEnabled: isEnabled, durationSeconds: durationSeconds) } + @objc static func applyToken(_ token: DisappearingMessageToken, toThread thread: TSThread, transaction: SDSAnyWriteTransaction) -> OWSDisappearingMessagesConfiguration { @@ -78,6 +80,7 @@ public extension OWSDisappearingMessagesConfiguration { return oldConfiguration.applyToken(token, transaction: transaction) } + @objc func applyToken(_ token: DisappearingMessageToken, transaction: SDSAnyWriteTransaction) -> OWSDisappearingMessagesConfiguration { let newConfiguration: OWSDisappearingMessagesConfiguration diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+GroupUpdateCopy.swift b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+GroupUpdateCopy.swift index 5891493ec2..8419b8aff0 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+GroupUpdateCopy.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+GroupUpdateCopy.swift @@ -1469,8 +1469,18 @@ extension GroupUpdateCopy { guard let oldToken = oldToken else { if newToken.isEnabled { - let format = NSLocalizedString("DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT", - comment: "Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.") + let format: String + if updater == .localUser { + format = NSLocalizedString( + "YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when you update disappearing messages duration. Embeds a {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context." + ) + } else { + format = NSLocalizedString( + "DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT", + comment: "Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context." + ) + } addItem(.disappearingMessagesState_enabled, format: format, durationString) } return @@ -1831,7 +1841,7 @@ extension GroupUpdateCopy { // MARK: - Updater - enum Updater { + enum Updater: Equatable { case localUser case otherUser(updaterName: String, updaterAddress: SignalServiceAddress) case unknown diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.h b/SignalServiceKit/src/Messages/Interactions/TSInteraction.h index 01d1360386..d28576288e 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.h +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.h @@ -20,7 +20,8 @@ typedef NS_CLOSED_ENUM(NSInteger, OWSInteractionType) { OWSInteractionType_ThreadDetails, OWSInteractionType_UnreadIndicator, OWSInteractionType_DateHeader, - OWSInteractionType_UnknownThreadWarning + OWSInteractionType_UnknownThreadWarning, + OWSInteractionType_DefaultDisappearingMessageTimer }; NSString *NSStringFromOWSInteractionType(OWSInteractionType value); diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m index 71f956e411..ff33028646 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m @@ -34,6 +34,8 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) return @"OWSInteractionType_DateHeader"; case OWSInteractionType_UnknownThreadWarning: return @"OWSInteractionType_UnknownThreadWarning"; + case OWSInteractionType_DefaultDisappearingMessageTimer: + return @"OWSInteractionType_DefaultDisappearingMessageTimer"; } } diff --git a/SignalServiceKit/src/groups/GroupManager.swift b/SignalServiceKit/src/groups/GroupManager.swift index 0dce2b6f8d..0092f3cf08 100644 --- a/SignalServiceKit/src/groups/GroupManager.swift +++ b/SignalServiceKit/src/groups/GroupManager.swift @@ -225,6 +225,7 @@ public class GroupManager: NSObject { groupId: Data? = nil, name: String? = nil, avatarImage: UIImage?, + disappearingMessageToken: DisappearingMessageToken, newGroupSeed: NewGroupSeed? = nil, shouldSendMessage: Bool) -> Promise { @@ -235,6 +236,7 @@ public class GroupManager: NSObject { groupId: groupId, name: name, avatarData: avatarData, + disappearingMessageToken: disappearingMessageToken, newGroupSeed: newGroupSeed, shouldSendMessage: shouldSendMessage) } @@ -245,6 +247,7 @@ public class GroupManager: NSObject { groupId: Data? = nil, name: String? = nil, avatarData: Data? = nil, + disappearingMessageToken: DisappearingMessageToken, newGroupSeed: NewGroupSeed? = nil, shouldSendMessage: Bool) -> Promise { @@ -252,9 +255,6 @@ public class GroupManager: NSObject { return Promise(error: OWSAssertionError("Missing localAddress.")) } - // By default, DMs are disable for new groups. - let disappearingMessageToken = DisappearingMessageToken.disabledToken - return firstly { () -> Promise in return self.ensureLocalProfileHasCommitmentIfNecessary() }.then(on: .global()) { () -> Promise in @@ -496,6 +496,7 @@ public class GroupManager: NSObject { groupId: Data?, name: String, avatarImage: UIImage?, + disappearingMessageToken: DisappearingMessageToken, newGroupSeed: NewGroupSeed?, shouldSendMessage: Bool, success: @escaping (TSGroupThread) -> Void, @@ -505,6 +506,7 @@ public class GroupManager: NSObject { groupId: groupId, name: name, avatarImage: avatarImage, + disappearingMessageToken: disappearingMessageToken, newGroupSeed: newGroupSeed, shouldSendMessage: shouldSendMessage) }.done { thread in @@ -520,6 +522,7 @@ public class GroupManager: NSObject { groupId: Data?, name: String, avatarData: Data?, + disappearingMessageToken: DisappearingMessageToken, newGroupSeed: NewGroupSeed?, shouldSendMessage: Bool, success: @escaping (TSGroupThread) -> Void, @@ -529,6 +532,7 @@ public class GroupManager: NSObject { groupId: groupId, name: name, avatarData: avatarData, + disappearingMessageToken: disappearingMessageToken, newGroupSeed: newGroupSeed, shouldSendMessage: shouldSendMessage) }.done { thread in