// // Copyright (c) 2020 Open Whisper Systems. All rights reserved. // import Foundation import PromiseKit import UIKit import ContactsUI // TODO: We should describe which state updates & when it is committed. @objc class ConversationSettingsViewController: OWSTableViewController { // MARK: - Dependencies var databaseStorage: SDSDatabaseStorage { return SDSDatabaseStorage.shared } var contactsManager: OWSContactsManager { return Environment.shared.contactsManager } var messageSender: MessageSender { return SSKEnvironment.shared.messageSender } var tsAccountManager: TSAccountManager { return .sharedInstance() } var blockingManager: OWSBlockingManager { return .shared() } var profileManager: OWSProfileManager { return .shared() } var messageSenderJobQueue: MessageSenderJobQueue { return SSKEnvironment.shared.messageSenderJobQueue } var identityManager: OWSIdentityManager { return SSKEnvironment.shared.identityManager } // MARK: - @objc public weak var conversationSettingsViewDelegate: OWSConversationSettingsViewDelegate? private let threadViewModel: ThreadViewModel var thread: TSThread { return threadViewModel.threadRecord } // Group model reflecting the last known group state. // This is updated as we change group membership, etc. var currentGroupModel: TSGroupModel? public var showVerificationOnAppear = false var contactsViewHelper: ContactsViewHelper? var disappearingMessagesConfiguration: OWSDisappearingMessagesConfiguration! var avatarView: UIImageView? let disappearingMessagesDurationLabel = UILabel() // This is currently disabled behind a feature flag. private var colorPicker: ColorPicker? var isShowingAllGroupMembers = false private let groupViewHelper: GroupViewHelper @objc public required init(threadViewModel: ThreadViewModel) { self.threadViewModel = threadViewModel if let groupThread = threadViewModel.threadRecord as? TSGroupThread { self.currentGroupModel = groupThread.groupModel } groupViewHelper = GroupViewHelper(threadViewModel: threadViewModel) super.init() contactsViewHelper = ContactsViewHelper(delegate: self) groupViewHelper.delegate = self } private func observeNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(identityStateDidChange(notification:)), name: .identityStateDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(otherUsersProfileDidChange(notification:)), name: .otherUsersProfileDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(profileWhitelistDidChange(notification:)), name: .profileWhitelistDidChange, object: nil) } // MARK: - Accessors var canEditConversationAttributes: Bool { return groupViewHelper.canEditConversationAttributes } var canEditConversationMembership: Bool { return groupViewHelper.canEditConversationMembership } // Can local user edit group access. var canEditConversationAccess: Bool { return groupViewHelper.canEditConversationAccess } var isLocalUserInConversation: Bool { return groupViewHelper.isLocalUserInConversation } var isGroupThread: Bool { return thread.isGroupThread } var disappearingMessagesDurations: [NSNumber] { return OWSDisappearingMessagesConfiguration.validDurationsSeconds() } // A local feature flag. var shouldShowColorPicker: Bool { return false } class var headerBackgroundColor: UIColor { return (Theme.isDarkThemeEnabled ? Theme.tableViewBackgroundColor : Theme.tableCellBackgroundColor) } // MARK: - View Lifecycle @objc public override func viewDidLoad() { super.viewDidLoad() if isGroupThread { updateNavigationBar() } else { self.title = NSLocalizedString( "CONVERSATION_SETTINGS_CONTACT_INFO_TITLE", comment: "Navbar title when viewing settings for a 1-on-1 thread") } self.useThemeBackgroundColors = true tableView.estimatedRowHeight = 45 tableView.rowHeight = UITableView.automaticDimension // The header should "extend" offscreen so that we // don't see the root view's background color if we scroll down. let backgroundTopView = UIView() backgroundTopView.backgroundColor = Self.headerBackgroundColor tableView.addSubview(backgroundTopView) backgroundTopView.autoPinEdge(.leading, to: .leading, of: view, withOffset: 0) backgroundTopView.autoPinEdge(.trailing, to: .trailing, of: view, withOffset: 0) let backgroundTopSize: CGFloat = 300 backgroundTopView.autoSetDimension(.height, toSize: backgroundTopSize) backgroundTopView.autoPinEdge(.bottom, to: .top, of: tableView, withOffset: 0) disappearingMessagesDurationLabel.setAccessibilityIdentifier(in: self, name: "disappearingMessagesDurationLabel") databaseStorage.uiRead { transaction in self.disappearingMessagesConfiguration = OWSDisappearingMessagesConfiguration.fetchOrBuildDefault(with: self.thread, transaction: transaction) } if shouldShowColorPicker { let colorPicker = ColorPicker(thread: self.thread) colorPicker.delegate = self self.colorPicker = colorPicker } updateNavigationBar() updateTableContents() observeNotifications() } func updateNavigationBar() { navigationItem.leftBarButtonItem = createOWSBackButton() if isGroupThread, canEditConversationAttributes { navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("CONVERSATION_SETTINGS_EDIT_GROUP", comment: "Label for the 'edit group' button in conversation settings view."), style: .plain, target: self, action: #selector(editGroupButtonWasPressed)) } else { navigationItem.rightBarButtonItem = nil } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if showVerificationOnAppear { showVerificationOnAppear = false if isGroupThread { showAllGroupMembers() } else { showVerificationView() } } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let selectedPath = tableView.indexPathForSelectedRow { // HACK to unselect rows when swiping back // http://stackoverflow.com/questions/19379510/uitableviewcell-doesnt-get-deselected-when-swiping-back-quickly tableView.deselectRow(at: selectedPath, animated: animated) } updateTableContents() } // MARK: - func reloadGroupModelAndUpdateContent() { guard let oldGroupThread = self.thread as? TSGroupThread else { owsFailDebug("Invalid thread.") navigationController?.popViewController(animated: true) return } guard let newGroupThread = (databaseStorage.uiRead { transaction in TSGroupThread.fetch(groupId: oldGroupThread.groupModel.groupId, transaction: transaction) }) else { owsFailDebug("Missing thread.") navigationController?.popViewController(animated: true) return } currentGroupModel = newGroupThread.groupModel updateTableContents() } var lastContentWidth: CGFloat? public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // Reload the table content if this view's width changes. var hasContentWidthChanged = false if let lastContentWidth = lastContentWidth, lastContentWidth != view.width { hasContentWidthChanged = true } if hasContentWidthChanged { updateTableContents() } } // MARK: - func didSelectGroupMember(_ memberAddress: SignalServiceAddress) { guard memberAddress.isValid else { owsFailDebug("Invalid address.") return } guard let helper = self.contactsViewHelper else { owsFailDebug("Missing contactsViewHelper.") return } let memberActionSheet = MemberActionSheet(address: memberAddress, contactsViewHelper: helper, groupViewHelper: groupViewHelper) memberActionSheet.present(fromViewController: self) } func showAddToSystemContactsActionSheet(contactThread: TSContactThread) { let actionSheet = ActionSheetController() let createNewTitle = NSLocalizedString("CONVERSATION_SETTINGS_NEW_CONTACT", comment: "Label for 'new contact' button in conversation settings view.") actionSheet.addAction(ActionSheetAction(title: createNewTitle, style: .default, handler: { [weak self] _ in self?.presentContactViewController() })) let addToExistingTitle = NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT", comment: "Label for 'new contact' button in conversation settings view.") actionSheet.addAction(ActionSheetAction(title: addToExistingTitle, style: .default, handler: { [weak self] _ in self?.presentAddToContactViewController(address: contactThread.contactAddress) })) self.presentActionSheet(actionSheet) } // MARK: - private var hasUnsavedChangesToDisappearingMessagesConfiguration: Bool { return databaseStorage.uiRead { transaction in if let groupThread = self.thread as? TSGroupThread { guard let latestThread = TSGroupThread.fetch(groupId: groupThread.groupModel.groupId, transaction: transaction) else { // Thread no longer exists. return false } guard latestThread.isLocalUserInGroup else { // Local user is no longer in group, e.g. perhaps they just blocked it. return false } } return self.disappearingMessagesConfiguration.hasChanged(with: transaction) } } // MARK: - Actions @objc func conversationNameTouched(sender: UIGestureRecognizer) { if !canEditConversationAttributes { owsFailDebug("failure: !self.canEditConversationAttributes") return } guard let avatarView = avatarView else { owsFailDebug("Missing avatarView.") return } if sender.state == .recognized { if isGroupThread { let location = sender.location(in: avatarView) if avatarView.bounds.contains(location) { showGroupAttributesView(editAction: .avatar) } else { showGroupAttributesView(editAction: .name) } } else { if contactsManager.supportsContactEditing { presentContactViewController() } } } } func showShareProfileAlert() { profileManager.presentAddThread(toProfileWhitelist: thread, from: self) { self.updateTableContents() } } func showVerificationView() { guard let contactThread = thread as? TSContactThread else { owsFailDebug("Invalid thread.") return } let contactAddress = contactThread.contactAddress assert(contactAddress.isValid) FingerprintViewController.present(from: self, address: contactAddress) } func showSoundSettingsView() { let vc = OWSSoundSettingsViewController() vc.thread = thread navigationController?.pushViewController(vc, animated: true) } func showAllGroupMembers() { isShowingAllGroupMembers = true updateTableContents() } func showGroupAttributesView(editAction: GroupAttributesViewController.EditAction) { guard canEditConversationAttributes else { owsFailDebug("!canEditConversationAttributes") return } assert(conversationSettingsViewDelegate != nil) guard let groupThread = thread as? TSGroupThread else { owsFailDebug("Invalid thread.") return } let groupAttributesViewController = GroupAttributesViewController(groupThread: groupThread, editAction: editAction, delegate: self) navigationController?.pushViewController(groupAttributesViewController, animated: true) } func showGroupAttributesAccessView() { guard canEditConversationAccess else { owsFailDebug("!canEditConversationAccess") return } guard let groupThread = thread as? TSGroupThread, let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { owsFailDebug("Invalid thread.") return } let currentValue = groupModelV2.access.attributes let alert = ActionSheetController(title: NSLocalizedString("CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS", comment: "Label for 'edit attributes access' action in conversation settings view."), message: NSLocalizedString("CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS_ALERT_DESCRIPTION", comment: "Description for the 'edit group attributes access' alert.")) let memberAction = ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS_ALERT_MEMBERS_BUTTON", comment: "Label for button that sets 'group attributes access' to 'members-only'."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "group_attributes_access_members"), style: .default) { _ in self.setGroupAttributesAccess(groupModelV2: groupModelV2, access: .member) } if currentValue == .member { memberAction.trailingIcon = .checkCircle } alert.addAction(memberAction) let adminAction = ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_EDIT_ATTRIBUTES_ACCESS_ALERT_ADMINISTRATORS_BUTTON", comment: "Label for button that sets 'group attributes access' to 'administrators-only'."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "group_attributes_access_administrators"), style: .default) { _ in self.setGroupAttributesAccess(groupModelV2: groupModelV2, access: .administrator) } if currentValue == .administrator { adminAction.trailingIcon = .checkCircle } alert.addAction(adminAction) alert.addAction(OWSActionSheets.cancelAction) presentActionSheet(alert) } private func setGroupAttributesAccess(groupModelV2: TSGroupModelV2, access: GroupV2Access) { GroupViewUtils.updateGroupWithActivityIndicator(fromViewController: self, updatePromiseBlock: { self.setGroupAttributesAccessPromise(groupModelV2: groupModelV2, access: access) }, completion: { [weak self] in self?.reloadGroupModelAndUpdateContent() }) } private func setGroupAttributesAccessPromise(groupModelV2: TSGroupModelV2, access: GroupV2Access) -> Promise { let thread = self.thread return firstly { () -> Promise in return GroupManager.messageProcessingPromise(for: thread, description: "Update group attributes access") }.map(on: .global()) { // We're sending a message, so we're accepting any pending message request. ThreadUtil.addToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction(thread: thread) }.then(on: .global()) { GroupManager.changeGroupAttributesAccessV2(groupModel: groupModelV2, access: access) }.asVoid() } func showAddMembersView() { guard canEditConversationMembership else { owsFailDebug("Can't edit membership.") return } guard let groupThread = thread as? TSGroupThread else { owsFailDebug("Invalid thread.") return } let addGroupMembersViewController = AddGroupMembersViewController(groupThread: groupThread) addGroupMembersViewController.addGroupMembersViewControllerDelegate = self navigationController?.pushViewController(addGroupMembersViewController, animated: true) } func showPendingMembersView() { guard let groupThread = thread as? TSGroupThread else { owsFailDebug("Invalid thread.") return } let pendingGroupMembersViewController = PendingGroupMembersViewController(groupModel: groupThread.groupModel, groupViewHelper: groupViewHelper) pendingGroupMembersViewController.pendingGroupMembersViewControllerDelegate = self navigationController?.pushViewController(pendingGroupMembersViewController, animated: true) } func presentContactViewController() { if !contactsManager.supportsContactEditing { owsFailDebug("Contact editing not supported") return } guard let contactThread = thread as? TSContactThread else { owsFailDebug("Invalid thread.") return } guard let contactViewController = contactsViewHelper?.contactViewController(for: contactThread.contactAddress, editImmediately: true) else { owsFailDebug("Unexpectedly missing contact VC") return } contactViewController.delegate = self navigationController?.pushViewController(contactViewController, animated: true) } private func presentAddToContactViewController(address: SignalServiceAddress) { if !contactsManager.supportsContactEditing { // Should not expose UI that lets the user get here. owsFailDebug("Contact editing not supported.") return } if !contactsManager.isSystemContactsAuthorized { contactsViewHelper?.presentMissingContactAccessAlertController(from: self) return } let viewController = OWSAddToContactViewController() viewController.configure(with: address) navigationController?.pushViewController(viewController, animated: true) } func didTapLeaveGroup() { if isLastAdminInV2Group { showReplaceAdminAlert() } else { showLeaveGroupConfirmAlert() } } func showLeaveGroupConfirmAlert(replacementAdminUuid: UUID? = nil) { let alert = ActionSheetController(title: NSLocalizedString("CONFIRM_LEAVE_GROUP_TITLE", comment: "Alert title"), message: NSLocalizedString("CONFIRM_LEAVE_GROUP_DESCRIPTION", comment: "Alert body")) let leaveAction = ActionSheetAction(title: NSLocalizedString("LEAVE_BUTTON_TITLE", comment: "Confirmation button within contextual alert"), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "leave_group_confirm"), style: .destructive) { _ in self.leaveGroup(replacementAdminUuid: replacementAdminUuid) } alert.addAction(leaveAction) alert.addAction(OWSActionSheets.cancelAction) presentActionSheet(alert) } func showReplaceAdminAlert() { let candidates = self.replacementAdminCandidates guard !candidates.isEmpty else { // TODO: We could offer a "delete group locally" option here. OWSActionSheets.showErrorAlert(message: NSLocalizedString("GROUPS_CANT_REPLACE_ADMIN_ALERT_MESSAGE", comment: "Message for the 'can't replace group admin' alert.")) return } let alert = ActionSheetController(title: NSLocalizedString("GROUPS_REPLACE_ADMIN_ALERT_TITLE", comment: "Title for the 'replace group admin' alert."), message: NSLocalizedString("GROUPS_REPLACE_ADMIN_ALERT_MESSAGE", comment: "Message for the 'replace group admin' alert.")) alert.addAction(ActionSheetAction(title: NSLocalizedString("GROUPS_REPLACE_ADMIN_BUTTON", comment: "Label for the 'replace group admin' button."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "replace_admin_alert"), style: .default) { _ in self.showReplaceAdminView(candidates: candidates) }) alert.addAction(OWSActionSheets.cancelAction) presentActionSheet(alert) } func showReplaceAdminView(candidates: Set) { assert(!candidates.isEmpty) let replaceAdminViewController = ReplaceAdminViewController(candidates: candidates, replaceAdminViewControllerDelegate: self) navigationController?.pushViewController(replaceAdminViewController, animated: true) } private var isLastAdminInV2Group: Bool { guard let groupThread = thread as? TSGroupThread else { owsFailDebug("Invalid thread.") return false } guard let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { return false } guard let localAddress = tsAccountManager.localAddress else { owsFailDebug("missing local address") return false } let groupMembership = groupModelV2.groupMembership guard groupMembership.isNonPendingMember(localAddress), groupMembership.isAdministrator(localAddress), groupMembership.nonPendingAdministrators.count == 1 else { return false } return true } private var replacementAdminCandidates: Set { guard let groupThread = thread as? TSGroupThread else { owsFailDebug("Invalid thread.") return [] } guard let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { return [] } guard let localAddress = tsAccountManager.localAddress else { owsFailDebug("missing local address") return [] } var candidates = groupModelV2.groupMembership.nonPendingMembers candidates.remove(localAddress) return candidates } private func leaveGroup(replacementAdminUuid: UUID? = nil) { guard let groupThread = thread as? TSGroupThread else { owsFailDebug("Invalid thread.") return } guard let navigationController = self.navigationController else { owsFailDebug("Invalid navigationController.") return } // On success, we want to pop back to the conversation view controller. let viewControllers = navigationController.viewControllers guard let index = viewControllers.firstIndex(of: self), index > 0 else { owsFailDebug("Invalid navigation stack.") return } let conversationViewController = viewControllers[index - 1] GroupManager.leaveGroupOrDeclineInviteAsyncWithUI(groupThread: groupThread, fromViewController: self, replacementAdminUuid: replacementAdminUuid) { self.navigationController?.popToViewController(conversationViewController, animated: true) } } @objc func disappearingMessagesSwitchValueDidChange(_ sender: UISwitch) { assert(canEditConversationAttributes) toggleDisappearingMessages(sender.isOn) updateTableContents() } func didTapUnblockGroup() { let isCurrentlyBlocked = blockingManager.isThreadBlocked(thread) if !isCurrentlyBlocked { owsFailDebug("Not blocked.") return } BlockListUIUtils.showUnblockThreadActionSheet(thread, from: self) { [weak self] _ in self?.updateTableContents() } } func didTapBlockGroup() { let isCurrentlyBlocked = blockingManager.isThreadBlocked(thread) if isCurrentlyBlocked { owsFailDebug("Already blocked.") return } BlockListUIUtils.showBlockThreadActionSheet(thread, from: self) { [weak self] _ in self?.updateTableContents() } } private func toggleDisappearingMessages(_ flag: Bool) { assert(canEditConversationAttributes) self.disappearingMessagesConfiguration = self.disappearingMessagesConfiguration.copy(withIsEnabled: flag) updateTableContents() } @objc func durationSliderDidChange(_ slider: UISlider) { assert(canEditConversationAttributes) let values = self.disappearingMessagesDurations.map { $0.uint32Value } let maxValue = values.count - 1 let index = Int(slider.value + 0.5).clamp(0, maxValue) if !slider.isTracking { // Snap the slider to a valid value unless the user // is still interacting with the control. slider.setValue(Float(index), animated: true) } guard let durationSeconds = values[safe: index] else { owsFailDebug("Invalid index: \(index)") return } self.disappearingMessagesConfiguration = self.disappearingMessagesConfiguration.copyAsEnabled(withDurationSeconds: durationSeconds) updateDisappearingMessagesDurationLabel() } func updateDisappearingMessagesDurationLabel() { if disappearingMessagesConfiguration.isEnabled { let keepForFormat = NSLocalizedString("KEEP_MESSAGES_DURATION", comment: "Slider label embeds {{TIME_AMOUNT}}, e.g. '2 hours'. See *_TIME_AMOUNT strings for examples.") disappearingMessagesDurationLabel.text = String(format: keepForFormat, disappearingMessagesConfiguration.durationString) } else { disappearingMessagesDurationLabel.text = NSLocalizedString("KEEP_MESSAGES_FOREVER", comment: "Slider label when disappearing messages is off") } disappearingMessagesDurationLabel.setNeedsLayout() disappearingMessagesDurationLabel.superview?.setNeedsLayout() } func showMuteUnmuteActionSheet() { // The "unmute" action sheet has no title or message; the // action label speaks for itself. var title: String? var message: String? if !thread.isMuted { title = NSLocalizedString( "CONVERSATION_SETTINGS_MUTE_ACTION_SHEET_TITLE", comment: "Title of the 'mute this thread' action sheet.") message = NSLocalizedString( "MUTE_BEHAVIOR_EXPLANATION", comment: "An explanation of the consequences of muting a thread.") } let actionSheet = ActionSheetController(title: title, message: message) if thread.isMuted { let action = ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_UNMUTE_ACTION", comment: "Label for button to unmute a thread."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "unmute"), style: .destructive) { [weak self] _ in self?.setThreadMutedUntilDate(nil) } actionSheet.addAction(action) } else { #if DEBUG actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_MUTE_ONE_MINUTE_ACTION", comment: "Label for button to mute a thread for a minute."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "mute_1_minute"), style: .destructive) { [weak self] _ in self?.setThreadMuted { var dateComponents = DateComponents() dateComponents.minute = 1 return dateComponents } }) #endif actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_MUTE_ONE_HOUR_ACTION", comment: "Label for button to mute a thread for a hour."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "mute_1_hour"), style: .destructive) { [weak self] _ in self?.setThreadMuted { var dateComponents = DateComponents() dateComponents.hour = 1 return dateComponents } }) actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_MUTE_ONE_DAY_ACTION", comment: "Label for button to mute a thread for a day."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "mute_1_day"), style: .destructive) { [weak self] _ in self?.setThreadMuted { var dateComponents = DateComponents() dateComponents.day = 1 return dateComponents } }) actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_MUTE_ONE_WEEK_ACTION", comment: "Label for button to mute a thread for a week."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "mute_1_week"), style: .destructive) { [weak self] _ in self?.setThreadMuted { var dateComponents = DateComponents() dateComponents.day = 7 return dateComponents } }) actionSheet.addAction(ActionSheetAction(title: NSLocalizedString("CONVERSATION_SETTINGS_MUTE_ONE_YEAR_ACTION", comment: "Label for button to mute a thread for a year."), accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "mute_1_year"), style: .destructive) { [weak self] _ in self?.setThreadMuted { var dateComponents = DateComponents() dateComponents.year = 1 return dateComponents } }) } actionSheet.addAction(OWSActionSheets.cancelAction) presentActionSheet(actionSheet) } private func setThreadMuted(dateBlock: () -> DateComponents) { guard let timeZone = TimeZone(identifier: "UTC") else { owsFailDebug("Invalid timezone.") return } var calendar = Calendar.current calendar.timeZone = timeZone let dateComponents = dateBlock() guard let mutedUntilDate = calendar.date(byAdding: dateComponents, to: Date()) else { owsFailDebug("Couldn't modify date.") return } self.setThreadMutedUntilDate(mutedUntilDate) } private func setThreadMutedUntilDate(_ value: Date?) { databaseStorage.write { transaction in self.thread.updateWithMuted(until: value, transaction: transaction) } updateTableContents() } func showMediaGallery() { Logger.debug("") let tileVC = MediaTileViewController(thread: thread) navigationController?.pushViewController(tileVC, animated: true) } func tappedConversationSearch() { conversationSettingsViewDelegate?.conversationSettingsDidRequestConversationSearch() } @objc func editGroupButtonWasPressed(_ sender: Any) { showGroupAttributesView(editAction: .none) } // MARK: - Notifications @objc private func identityStateDidChange(notification: Notification) { AssertIsOnMainThread() updateTableContents() } @objc private func otherUsersProfileDidChange(notification: Notification) { AssertIsOnMainThread() guard let address = notification.userInfo?[kNSNotificationKey_ProfileAddress] as? SignalServiceAddress, address.isValid else { owsFailDebug("Missing or invalid address.") return } guard let contactThread = thread as? TSContactThread else { return } if contactThread.contactAddress == address { updateTableContents() } } @objc private func profileWhitelistDidChange(notification: Notification) { AssertIsOnMainThread() // If profile whitelist just changed, we may need to refresh the view. if let address = notification.userInfo?[kNSNotificationKey_ProfileAddress] as? SignalServiceAddress, let contactThread = thread as? TSContactThread, contactThread.contactAddress == address { updateTableContents() } if let groupId = notification.userInfo?[kNSNotificationKey_ProfileGroupId] as? Data, let groupThread = thread as? TSGroupThread, groupThread.groupModel.groupId == groupId { updateTableContents() } } } // MARK: - extension ConversationSettingsViewController: ContactsViewHelperDelegate { func contactsViewHelperDidUpdateContacts() { updateTableContents() } } // MARK: - extension ConversationSettingsViewController: CNContactViewControllerDelegate { public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) { updateTableContents() navigationController?.popToViewController(self, animated: true) } } // MARK: - extension ConversationSettingsViewController: ColorPickerDelegate { func showColorPicker() { guard let colorPicker = colorPicker else { owsFailDebug("Missing colorPicker.") return } let sheetViewController = colorPicker.sheetViewController sheetViewController.delegate = self self.present(sheetViewController, animated: true) { Logger.info("presented sheet view") } } public func colorPicker(_ colorPicker: ColorPicker, didPickConversationColor conversationColor: OWSConversationColor) { Logger.debug("picked color: \(conversationColor.name)") databaseStorage.write { transaction in self.thread.updateConversationColorName(conversationColor.name, transaction: transaction) } contactsManager.avatarCache.removeAllImages() contactsManager.clearColorNameCache() updateTableContents() conversationSettingsViewDelegate?.conversationColorWasUpdated() DispatchQueue.global().async { let operation = ConversationConfigurationSyncOperation(thread: self.thread) assert(operation.isReady) operation.start() } } } // MARK: - extension ConversationSettingsViewController: GroupAttributesViewControllerDelegate { func groupAttributesDidUpdate() { reloadGroupModelAndUpdateContent() } } // MARK: - extension ConversationSettingsViewController: AddGroupMembersViewControllerDelegate { func addGroupMembersViewDidUpdate() { reloadGroupModelAndUpdateContent() } } // MARK: - extension ConversationSettingsViewController: PendingGroupMembersViewControllerDelegate { func pendingGroupMembersViewDidUpdate() { reloadGroupModelAndUpdateContent() } } // MARK: - extension ConversationSettingsViewController: SheetViewControllerDelegate { public func sheetViewControllerRequestedDismiss(_ sheetViewController: SheetViewController) { dismiss(animated: true) } } // MARK: - extension ConversationSettingsViewController: OWSNavigationView { public func shouldCancelNavigationBack() -> Bool { let result = hasUnsavedChangesToDisappearingMessagesConfiguration if result { GroupAttributesViewController.showUnsavedGroupChangesActionSheet(from: self, saveBlock: { self.updateDisappearingMessagesConfigurationAndDismiss() }, discardBlock: { self.navigationController?.popViewController(animated: true) }) } return result } private func updateDisappearingMessagesConfigurationAndDismiss() { let dmConfiguration: OWSDisappearingMessagesConfiguration = disappearingMessagesConfiguration let thread = self.thread GroupViewUtils.updateGroupWithActivityIndicator(fromViewController: self, updatePromiseBlock: { self.updateDisappearingMessagesConfigurationPromise(dmConfiguration, thread: thread) }, completion: { [weak self] in self?.navigationController?.popViewController(animated: true) }) } private func updateDisappearingMessagesConfigurationPromise(_ dmConfiguration: OWSDisappearingMessagesConfiguration, thread: TSThread) -> Promise { return firstly { () -> Promise in return GroupManager.messageProcessingPromise(for: thread, description: "Update disappearing messages configuration") }.map(on: .global()) { // We're sending a message, so we're accepting any pending message request. ThreadUtil.addToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction(thread: thread) }.then(on: .global()) { GroupManager.localUpdateDisappearingMessages(thread: thread, disappearingMessageToken: dmConfiguration.asToken) } } } // MARK: - extension ConversationSettingsViewController: GroupViewHelperDelegate { func groupViewHelperDidUpdateGroup() { reloadGroupModelAndUpdateContent() } var fromViewController: UIViewController? { return self } } // MARK: - extension ConversationSettingsViewController: ReplaceAdminViewControllerDelegate { func replaceAdmin(uuid: UUID) { showLeaveGroupConfirmAlert(replacementAdminUuid: uuid) } }