Signal-iOS/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift
2021-01-25 15:13:03 -08:00

370 lines
15 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import ContactsUI
@objc
class MemberActionSheet: NSObject {
// MARK: - Dependencies
private var contactsManager: OWSContactsManager {
return Environment.shared.contactsManager
}
private var contactsViewHelper: ContactsViewHelper {
return Environment.shared.contactsViewHelper
}
// MARK: -
@objc
let address: SignalServiceAddress
public var canMakeGroupAdmin = false
public var groupViewHelper: GroupViewHelper?
@objc
init(address: SignalServiceAddress, groupViewHelper: GroupViewHelper?) {
assert(address.isValid)
self.address = address
self.groupViewHelper = groupViewHelper
}
// When presenting the contact view, we must retain ourselves
// as we are the delegate. This will get released when contact
// editing has concluded.
private var strongSelf: MemberActionSheet?
@objc
func present(fromViewController: UIViewController) {
guard let navController = fromViewController.navigationController else {
return owsFailDebug("Must be presented within a nav controller")
}
let contactsViewHelper = self.contactsViewHelper
let actionSheet = ActionSheetController()
actionSheet.customHeader = MemberHeader(address: address) { [weak actionSheet] in
actionSheet?.dismiss(animated: true)
}
actionSheet.contentAlignment = .leading
actionSheet.addAction(OWSActionSheets.cancelAction)
// If the local user, show no options.
guard !address.isLocalAddress else {
fromViewController.presentActionSheet(actionSheet)
return
}
// If blocked, only show unblock as an option
guard !contactsViewHelper.isSignalServiceAddressBlocked(address) else {
let unblockAction = ActionSheetAction(
title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON",
comment: "Button label for the 'unblock' button"),
accessibilityIdentifier: "MemberActionSheet.unblock"
) { _ in
BlockListUIUtils.showUnblockAddressActionSheet(
self.address,
from: fromViewController,
completionBlock: nil
)
}
unblockAction.leadingIcon = .settingsBlock
actionSheet.addAction(unblockAction)
fromViewController.presentActionSheet(actionSheet)
return
}
let blockAction = ActionSheetAction(
title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON",
comment: "Button label for the 'block' button"),
accessibilityIdentifier: "MemberActionSheet.block"
) { _ in
BlockListUIUtils.showBlockAddressActionSheet(
self.address,
from: fromViewController,
completionBlock: nil
)
}
blockAction.leadingIcon = .settingsBlock
actionSheet.addAction(blockAction)
if contactsManager.supportsContactEditing && !contactsManager.isSystemContact(address: address) {
let addToContactsAction = ActionSheetAction(
title: NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_SYSTEM_CONTACTS",
comment: "button in conversation settings view."),
accessibilityIdentifier: "MemberActionSheet.block"
) { _ in
guard let contactVC = contactsViewHelper.contactViewController(for: self.address, editImmediately: true) else {
return owsFailDebug("unexpectedly failed to present contact view")
}
self.strongSelf = self
contactVC.delegate = self
navController.pushViewController(contactVC, animated: true)
}
addToContactsAction.leadingIcon = .settingsAddToContacts
actionSheet.addAction(addToContactsAction)
}
let addToGroupAction = ActionSheetAction(
title: NSLocalizedString("ADD_TO_GROUP",
comment: "Label for button or row which allows users to add to another group."),
accessibilityIdentifier: "MemberActionSheet.addToGroup"
) { _ in
AddToGroupViewController.presentForUser(self.address, from: fromViewController)
}
addToGroupAction.leadingIcon = .settingsAddToGroup
actionSheet.addAction(addToGroupAction)
let safetyNumberAction = ActionSheetAction(
title: NSLocalizedString("VERIFY_PRIVACY",
comment: "Label for button or row which allows users to verify the safety number of another user."),
accessibilityIdentifier: "MemberActionSheet.block"
) { _ in
FingerprintViewController.present(from: fromViewController, address: self.address)
}
safetyNumberAction.leadingIcon = .settingsViewSafetyNumber
actionSheet.addAction(safetyNumberAction)
let address = self.address
if let groupViewHelper = self.groupViewHelper,
groupViewHelper.isFullOrInvitedMember(address) {
if groupViewHelper.memberActionSheetCanMakeGroupAdmin(address: address) {
let action = ActionSheetAction(
title: NSLocalizedString("CONVERSATION_SETTINGS_MAKE_GROUP_ADMIN_BUTTON",
comment: "Label for 'make group admin' button in conversation settings view."),
accessibilityIdentifier: "MemberActionSheet.makeGroupAdmin"
) { _ in
groupViewHelper.memberActionSheetMakeGroupAdminWasSelected(address: address)
}
action.leadingIcon = .settingsViewMakeGroupAdmin
actionSheet.addAction(action)
}
if groupViewHelper.memberActionSheetCanRevokeGroupAdmin(address: address) {
let action = ActionSheetAction(
title: NSLocalizedString("CONVERSATION_SETTINGS_REVOKE_GROUP_ADMIN_BUTTON",
comment: "Label for 'revoke group admin' button in conversation settings view."),
accessibilityIdentifier: "MemberActionSheet.revokeGroupAdmin"
) { _ in
groupViewHelper.memberActionSheetRevokeGroupAdminWasSelected(address: address)
}
action.leadingIcon = .settingsViewRevokeGroupAdmin
actionSheet.addAction(action)
}
if groupViewHelper.memberActionSheetCanRemoveFromGroup(address: address) {
let action = ActionSheetAction(
title: NSLocalizedString("CONVERSATION_SETTINGS_REMOVE_FROM_GROUP_BUTTON",
comment: "Label for 'remove from group' button in conversation settings view."),
accessibilityIdentifier: "MemberActionSheet.removeFromGroup"
) { _ in
groupViewHelper.memberActionSheetRemoveFromGroupWasSelected(address: address)
}
action.leadingIcon = .settingsViewRemoveFromGroup
actionSheet.addAction(action)
}
}
fromViewController.presentActionSheet(actionSheet)
}
}
extension MemberActionSheet: CNContactViewControllerDelegate {
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
viewController.navigationController?.popViewController(animated: true)
strongSelf = nil
}
}
private class MemberHeader: UIStackView {
private var dismiss: () -> Void
init(address: SignalServiceAddress, dismiss: @escaping () -> Void) {
self.dismiss = dismiss
super.init(frame: .zero)
addBackgroundView(withBackgroundColor: Theme.actionSheetBackgroundColor)
axis = .vertical
spacing = 2
isLayoutMarginsRelativeArrangement = true
layoutMargins = UIEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
createViews(address: address)
}
func createViews(address: SignalServiceAddress) {
var fetchedThread: TSContactThread?
var fetchedDisplayName: String?
var username: String?
var bioForDisplay: String?
databaseStorage.read { transaction in
fetchedThread = TSContactThread.getWithContactAddress(address, transaction: transaction)
fetchedDisplayName = self.contactsManager.displayName(for: address, transaction: transaction)
username = self.profileManager.username(for: address, transaction: transaction)
bioForDisplay = self.profileManager.profileBioForDisplay(for: address, transaction: transaction)
}
// Only open a write transaction if we need to create a new thread record.
if fetchedThread == nil {
databaseStorage.write { transaction in
fetchedThread = TSContactThread(contactAddress: address)
fetchedThread?.anyInsert(transaction: transaction)
}
}
guard let thread = fetchedThread, let displayName = fetchedDisplayName else {
return owsFailDebug("Unexpectedly missing name and thread")
}
let avatarContainer = UIView()
addArrangedSubview(avatarContainer)
let avatarDiameter: CGFloat = 80
let avatarView = AvatarImageView()
avatarContainer.addSubview(avatarView)
avatarView.autoHCenterInSuperview()
avatarView.autoPinEdge(toSuperviewEdge: .top)
avatarView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 8)
avatarView.autoSetDimension(.height, toSize: avatarDiameter)
let avatarBuilder = OWSContactAvatarBuilder(
address: address,
colorName: thread.conversationColorName,
diameter: UInt(avatarDiameter)
)
if address.isLocalAddress {
avatarView.image = OWSProfileManager.shared().localProfileAvatarImage() ?? avatarBuilder.buildDefaultImage()
} else {
avatarView.image = avatarBuilder.build()
}
let titleLabel = UILabel()
titleLabel.font = UIFont.ows_dynamicTypeBodyClamped.ows_semibold
titleLabel.textColor = Theme.primaryTextColor
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
titleLabel.setContentHuggingVerticalHigh()
titleLabel.setCompressionResistanceVerticalHigh()
titleLabel.text = displayName
addArrangedSubview(titleLabel)
var detailText: String?
if let phoneNumber = address.phoneNumber {
let formattedNumber = PhoneNumber.bestEffortFormatPartialUserSpecifiedText(toLookLikeAPhoneNumber: phoneNumber)
if displayName != formattedNumber {
detailText = formattedNumber
}
}
if let username = username {
if let formattedUsername = CommonFormats.formatUsername(username), displayName != formattedUsername {
if let existingDetails = detailText {
detailText = existingDetails + "\n" + formattedUsername
} else {
detailText = formattedUsername
}
}
}
func buildSubtitleLabel(text: String) -> UILabel {
let label = UILabel()
label.font = .ows_dynamicTypeSubheadline
label.textColor = Theme.secondaryTextAndIconColor
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.textAlignment = .center
label.setContentHuggingVerticalHigh()
label.setCompressionResistanceVerticalHigh()
label.text = text
return label
}
if let bioForDisplay = bioForDisplay {
let label = buildSubtitleLabel(text: bioForDisplay)
label.numberOfLines = 0
addArrangedSubview(label)
}
if let detailText = detailText {
addArrangedSubview(buildSubtitleLabel(text: detailText))
}
let actionsStackView = UIStackView()
actionsStackView.axis = .horizontal
actionsStackView.spacing = 36
actionsStackView.isLayoutMarginsRelativeArrangement = true
actionsStackView.layoutMargins = UIEdgeInsets(top: 14, leading: 0, bottom: 18, trailing: 0)
addArrangedSubview(actionsStackView)
let messageButton = createActionButton(
icon: .message,
accessibilityLabel: NSLocalizedString("GROUP_MEMBERS_SEND_MESSAGE",
comment: "Accessibility label for the 'send message to group member' button"),
accessibilityIdentifier: "MemberActionSheet.send_message"
) { SignalApp.shared().presentConversation(for: address, action: .compose, animated: true) }
actionsStackView.addArrangedSubview(messageButton)
let videoCallButton = createActionButton(
icon: .videoCall,
accessibilityLabel: NSLocalizedString("GROUP_MEMBERS_VIDEO_CALL",
comment: "Accessibility label for the 'call group member' button"),
accessibilityIdentifier: "MemberActionSheet.video_call"
) { SignalApp.shared().presentConversation(for: address, action: .videoCall, animated: true) }
videoCallButton.isEnabled = !address.isLocalAddress
actionsStackView.addArrangedSubview(videoCallButton)
let audioCallButton = createActionButton(
icon: .audioCall,
accessibilityLabel: NSLocalizedString("GROUP_MEMBERS_CALL",
comment: "Accessibility label for the 'call group member' button"),
accessibilityIdentifier: "MemberActionSheet.audio_call"
) { SignalApp.shared().presentConversation(for: address, action: .audioCall, animated: true) }
audioCallButton.isEnabled = !address.isLocalAddress
actionsStackView.addArrangedSubview(audioCallButton)
let leftSpacer = UIView.hStretchingSpacer()
let rightSpacer = UIView.hStretchingSpacer()
actionsStackView.insertArrangedSubview(leftSpacer, at: 0)
actionsStackView.addArrangedSubview(rightSpacer)
leftSpacer.autoMatch(.width, to: .width, of: rightSpacer)
}
private func createActionButton(
icon: ThemeIcon,
accessibilityLabel: String?,
accessibilityIdentifier: String?,
action: @escaping () -> Void
) -> UIButton {
let button = OWSButton { [weak self] in
guard let self = self else { return }
action()
self.dismiss()
}
button.accessibilityLabel = accessibilityLabel
button.accessibilityIdentifier = accessibilityIdentifier
button.autoSetDimensions(to: CGSize(square: 48))
button.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray02
button.layer.cornerRadius = 24
button.clipsToBounds = true
button.imageEdgeInsets = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)
button.setTemplateImageName(Theme.iconName(icon), tintColor: Theme.accentBlueColor)
return button
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}