Signal-iOS/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift
2021-05-25 17:52:15 -03:00

390 lines
16 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import ContactsUI
@objc
class MemberActionSheet: InteractiveSheetViewController {
let tableViewController = OWSTableViewController2()
private var groupViewHelper: GroupViewHelper?
private let handleContainer = UIView()
var avatarView: UIImageView?
var thread: TSThread { threadViewModel.threadRecord }
let threadViewModel: ThreadViewModel
let address: SignalServiceAddress
override var interactiveScrollViews: [UIScrollView] { [tableViewController.tableView] }
override var renderExternalHandle: Bool { false }
var contentSizeHeight: CGFloat {
tableViewController.tableView.contentSize.height + tableViewController.tableView.adjustedContentInset.totalHeight
}
override var minimizedHeight: CGFloat {
return min(contentSizeHeight, maximizedHeight)
}
override var maximizedHeight: CGFloat {
min(contentSizeHeight, CurrentAppContext().frame.height - (view.safeAreaInsets.top + 32))
}
@objc
init(address: SignalServiceAddress, groupViewHelper: GroupViewHelper?) {
self.threadViewModel = {
// Avoid opening a write transaction if we can
guard let threadViewModel: ThreadViewModel = Self.databaseStorage.read(block: { transaction in
guard let thread = TSContactThread.getWithContactAddress(
address,
transaction: transaction
) else { return nil }
return ThreadViewModel(
thread: thread,
forConversationList: false,
transaction: transaction
)
}) else {
return Self.databaseStorage.write { transaction in
let thread = TSContactThread.getOrCreateThread(
withContactAddress: address,
transaction: transaction
)
return ThreadViewModel(
thread: thread,
forConversationList: false,
transaction: transaction
)
}
}
return threadViewModel
}()
self.groupViewHelper = groupViewHelper
self.address = address
super.init()
tableViewController.shouldDeferInitialLoad = false
}
public required init() {
fatalError("init() has not been implemented")
}
private weak var fromViewController: UIViewController?
@objc(presentFromViewController:)
func present(from viewController: UIViewController) {
fromViewController = viewController
viewController.present(self, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
tableViewController.defaultSeparatorInsetLeading = OWSTableViewController2.cellHInnerMargin + 24 + OWSTableItem.iconSpacing
addChild(tableViewController)
contentView.addSubview(tableViewController.view)
tableViewController.view.autoPinEdgesToSuperviewEdges()
// We add the handle directly to the content view,
// so that it doesn't scroll with the table.
handleContainer.backgroundColor = tableViewController.tableBackgroundColor
contentView.addSubview(handleContainer)
handleContainer.autoPinWidthToSuperview()
handleContainer.autoPinEdge(toSuperviewEdge: .top)
let handle = UIView()
handle.backgroundColor = tableViewController.separatorColor
handle.autoSetDimensions(to: CGSize(width: 36, height: 5))
handle.layer.cornerRadius = 5 / 2
handleContainer.addSubview(handle)
handle.autoPinHeightToSuperview(withMargin: 12)
handle.autoHCenterInSuperview()
updateViewState()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateViewState()
}
private var previousMinimizedHeight: CGFloat?
private var previousSafeAreaInsets: UIEdgeInsets?
private func updateViewState() {
if previousSafeAreaInsets != tableViewController.view.safeAreaInsets {
updateTableContents()
previousSafeAreaInsets = tableViewController.view.safeAreaInsets
}
if minimizedHeight != previousMinimizedHeight {
heightConstraint?.constant = minimizedHeight
previousMinimizedHeight = minimizedHeight
}
}
override func themeDidChange() {
super.themeDidChange()
handleContainer.backgroundColor = tableViewController.tableBackgroundColor
updateTableContents()
}
// 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?
func updateTableContents(shouldReload: Bool = true) {
let contents = OWSTableContents()
defer { tableViewController.setContents(contents, shouldReload: shouldReload) }
// Leave space at the top for the handle
let handleSection = OWSTableSection()
handleSection.customHeaderHeight = 50
contents.addSection(handleSection)
let section = OWSTableSection()
contents.addSection(section)
section.customHeaderView = ConversationHeaderBuilder.buildHeader(
for: thread,
avatarSize: 80,
options: [.message, .videoCall, .audioCall],
delegate: self
)
// If the local user, show no options.
guard !address.isLocalAddress else { return }
// If blocked, only show unblock as an option
guard !contactsViewHelper.isSignalServiceAddressBlocked(address) else {
section.add(.actionItem(
icon: .settingsBlock,
name: NSLocalizedString(
"BLOCK_LIST_UNBLOCK_BUTTON",
comment: "Button label for the 'unblock' button"
),
accessibilityIdentifier: "MemberActionSheet.unblock",
actionBlock: { [weak self] in
self?.didTapUnblockThread {}
}
))
return
}
section.add(.actionItem(
icon: .settingsBlock,
name: NSLocalizedString(
"BLOCK_LIST_BLOCK_BUTTON",
comment: "Button label for the 'block' button"
),
accessibilityIdentifier: "MemberActionSheet.block",
actionBlock: { [weak self] in
guard let self = self, let fromViewController = self.fromViewController else { return }
self.dismiss(animated: true) {
BlockListUIUtils.showBlockAddressActionSheet(
self.address,
from: fromViewController,
completionBlock: nil
)
}
}
))
if let groupViewHelper = self.groupViewHelper, groupViewHelper.isFullOrInvitedMember(address) {
if groupViewHelper.canRemoveFromGroup(address: address) {
section.add(.actionItem(
icon: .settingsViewRemoveFromGroup,
name: NSLocalizedString(
"CONVERSATION_SETTINGS_REMOVE_FROM_GROUP_BUTTON",
comment: "Label for 'remove from group' button in conversation settings view."
),
accessibilityIdentifier: "MemberActionSheet.removeFromGroup",
actionBlock: { [weak self] in
guard let self = self else { return }
self.dismiss(animated: true) {
self.groupViewHelper?.presentRemoveFromGroupActionSheet(address: self.address)
}
}
))
}
if groupViewHelper.memberActionSheetCanMakeGroupAdmin(address: address) {
section.add(.actionItem(
icon: .settingsViewMakeGroupAdmin,
name: NSLocalizedString(
"CONVERSATION_SETTINGS_MAKE_GROUP_ADMIN_BUTTON",
comment: "Label for 'make group admin' button in conversation settings view."
),
accessibilityIdentifier: "MemberActionSheet.makeGroupAdmin",
actionBlock: { [weak self] in
guard let self = self else { return }
self.dismiss(animated: true) {
self.groupViewHelper?.memberActionSheetMakeGroupAdminWasSelected(address: self.address)
}
}
))
}
if groupViewHelper.memberActionSheetCanRevokeGroupAdmin(address: address) {
section.add(.actionItem(
icon: .settingsViewRevokeGroupAdmin,
name: NSLocalizedString(
"CONVERSATION_SETTINGS_REVOKE_GROUP_ADMIN_BUTTON",
comment: "Label for 'revoke group admin' button in conversation settings view."
),
accessibilityIdentifier: "MemberActionSheet.revokeGroupAdmin",
actionBlock: { [weak self] in
guard let self = self else { return }
self.dismiss(animated: true) {
self.groupViewHelper?.memberActionSheetRevokeGroupAdminWasSelected(address: self.address)
}
}
))
}
}
section.add(.actionItem(
icon: .settingsAddToGroup,
name: NSLocalizedString(
"ADD_TO_GROUP",
comment: "Label for button or row which allows users to add to another group."
),
accessibilityIdentifier: "MemberActionSheet.add_to_group",
actionBlock: { [weak self] in
guard let self = self, let fromViewController = self.fromViewController else { return }
self.dismiss(animated: true) {
AddToGroupViewController.presentForUser(self.address, from: fromViewController)
}
}
))
if contactsManagerImpl.supportsContactEditing {
if contactsManager.isSystemContact(address: address) {
section.add(.actionItem(
icon: .settingsUserInContacts,
name: NSLocalizedString(
"CONVERSATION_SETTINGS_VIEW_IS_SYSTEM_CONTACT",
comment: "Indicates that user is in the system contacts list."
),
accessibilityIdentifier: "MemberActionSheet.contact",
actionBlock: { [weak self] in
guard let self = self,
let fromViewController = self.fromViewController,
let navController = fromViewController.navigationController else { return }
self.dismiss(animated: true) {
guard let contactVC = self.contactsViewHelper.contactViewController(
for: self.address,
editImmediately: false
) else {
return owsFailDebug("unexpectedly failed to present contact view")
}
self.strongSelf = self
contactVC.delegate = self
navController.pushViewController(contactVC, animated: true)
}
}
))
} else {
section.add(.actionItem(
icon: .settingsAddToContacts,
name: NSLocalizedString(
"CONVERSATION_SETTINGS_ADD_TO_SYSTEM_CONTACTS",
comment: "button in conversation settings view."
),
accessibilityIdentifier: "MemberActionSheet.add_to_contacts",
actionBlock: { [weak self] in
guard let self = self,
let fromViewController = self.fromViewController,
let navController = fromViewController.navigationController else { return }
self.dismiss(animated: true) {
guard let contactVC = self.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)
}
}
))
}
}
section.add(.actionItem(
icon: .settingsViewSafetyNumber,
name: NSLocalizedString(
"VERIFY_PRIVACY",
comment: "Label for button or row which allows users to verify the safety number of another user."
),
accessibilityIdentifier: "MemberActionSheet.safety_number",
actionBlock: { [weak self] in
guard let self = self, let fromViewController = self.fromViewController else { return }
self.dismiss(animated: true) {
FingerprintViewController.present(from: fromViewController, address: self.address)
}
}
))
}
}
extension MemberActionSheet: ConversationHeaderDelegate {
var isBlockedByMigration: Bool { groupViewHelper?.isBlockedByMigration == true }
func tappedAvatar() {
guard let avatarView = avatarView, avatarView.image != nil else { return }
guard let vc = databaseStorage.read(block: { readTx in
AvatarViewController(address: self.address, renderLocalUserAsNoteToSelf: false, readTx: readTx)
}) else { return }
present(vc, animated: true)
}
func tappedConversationSearch() {}
func didTapUnblockThread(completion: @escaping () -> Void) {
guard let fromViewController = fromViewController else { return }
dismiss(animated: true) {
BlockListUIUtils.showUnblockAddressActionSheet(
self.address,
from: fromViewController
) { _ in
completion()
}
}
}
func tappedButton() {
dismiss(animated: true)
}
func didTapAddGroupDescription() {}
var canEditConversationAttributes: Bool { false }
}
extension MemberActionSheet: CNContactViewControllerDelegate {
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
viewController.navigationController?.popViewController(animated: true)
strongSelf = nil
}
}
extension MemberActionSheet: MediaPresentationContextProvider {
func mediaPresentationContext(item: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? {
let mediaView: UIView
switch item {
case .gallery:
owsFailDebug("Unexpected item")
return nil
case .image:
guard let avatarView = self.avatarView else { return nil }
mediaView = avatarView
}
guard let mediaSuperview = mediaView.superview else {
owsFailDebug("mediaSuperview was unexpectedly nil")
return nil
}
let presentationFrame = coordinateSpace.convert(mediaView.frame, from: mediaSuperview)
return MediaPresentationContext(mediaView: mediaView, presentationFrame: presentationFrame, cornerRadius: 0)
}
func snapshotOverlayView(in coordinateSpace: UICoordinateSpace) -> (UIView, CGRect)? {
return nil
}
}