Signal-iOS/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift

1047 lines
43 KiB
Swift

//
// 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<Void> {
let thread = self.thread
return firstly { () -> Promise<Void> 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<SignalServiceAddress>) {
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<SignalServiceAddress> {
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<Void> {
return firstly { () -> Promise<Void> 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)
}
}