Signal-iOS/Signal/src/ViewControllers/NewGroupView/NewGroupConfirmViewController.swift
2020-09-01 17:11:07 -03:00

519 lines
23 KiB
Swift

//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
import SafariServices
@objc
public class NewGroupConfirmViewController: OWSViewController {
// MARK: - Dependencies
private var databaseStorage: SDSDatabaseStorage {
return SDSDatabaseStorage.shared
}
private var contactsManager: OWSContactsManager {
return Environment.shared.contactsManager
}
private var messageSender: MessageSender {
return SSKEnvironment.shared.messageSender
}
private var tsAccountManager: TSAccountManager {
return .sharedInstance()
}
// MARK: -
private var newGroupState = NewGroupState()
private var groupSeed: NewGroupSeed {
return newGroupState.groupSeed
}
private var recipientSet: OrderedSet<PickedRecipient> {
return newGroupState.recipientSet
}
private let helper: GroupAttributesEditorHelper
private var nameTextField: UITextField {
return helper.nameTextField
}
private let recipientTableView = OWSTableViewController()
required init(newGroupState: NewGroupState) {
self.newGroupState = newGroupState
let groupId = newGroupState.groupSeed.possibleGroupId
let conversationColorName = newGroupState.groupSeed.possibleConversationColorName
self.helper = GroupAttributesEditorHelper(groupId: groupId,
conversationColorName: conversationColorName.rawValue,
groupNameOriginal: newGroupState.groupName,
avatarOriginalData: newGroupState.avatarData,
iconViewSize: 64)
super.init()
}
// MARK: - View Lifecycle
@objc
public override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("NEW_GROUP_NAME_GROUP_VIEW_TITLE",
comment: "The title for the 'name new group' view.")
view.backgroundColor = Theme.backgroundColor
navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("NEW_GROUP_CREATE_BUTTON",
comment: "The title for the 'create group' button."),
style: .plain,
target: self,
action: #selector(createNewGroup),
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "create"))
// First section.
helper.delegate = self
helper.buildContents(avatarViewHelperDelegate: self)
helper.avatarWrapper.setContentHuggingVerticalHigh()
helper.nameTextField.setContentHuggingHorizontalLow()
let firstSection = UIStackView(arrangedSubviews: [
helper.avatarWrapper,
helper.nameTextField
])
firstSection.axis = .horizontal
firstSection.alignment = .center
firstSection.spacing = 12
firstSection.isLayoutMarginsRelativeArrangement = true
firstSection.preservesSuperviewLayoutMargins = true
view.addSubview(firstSection)
firstSection.autoPinWidthToSuperview()
firstSection.autoPin(toTopLayoutGuideOf: self, withInset: 8)
var lastSection: UIView = firstSection
let membersDoNotSupportGroupsV2 = self.membersDoNotSupportGroupsV2
if RemoteConfig.groupsV2CreateGroups,
membersDoNotSupportGroupsV2.count > 0 {
let legacyGroupSection = UIView()
legacyGroupSection.backgroundColor = Theme.secondaryBackgroundColor
legacyGroupSection.preservesSuperviewLayoutMargins = true
view.addSubview(legacyGroupSection)
legacyGroupSection.autoPinWidthToSuperview()
legacyGroupSection.autoPinEdge(.top, to: .bottom, of: firstSection, withOffset: 16)
lastSection = legacyGroupSection
let legacyGroupText: String
let learnMoreText = NSLocalizedString("GROUPS_LEGACY_GROUP_LEARN_MORE_LINK",
comment: "A \"learn more\" link with more information about legacy groups.")
if membersDoNotSupportGroupsV2.count > 1 {
let memberCountText = OWSFormat.formatInt(membersDoNotSupportGroupsV2.count)
let legacyGroupFormat = NSLocalizedString("GROUPS_LEGACY_GROUP_CREATION_WARNING_FORMAT_N",
comment: "Indicates that a new group will be a legacy group because multiple members do not support v2 groups. Embeds {{ %1$@ the number of members who do not support v2 groups, %2$@ a \"learn more\" link. }}.")
legacyGroupText = String(format: legacyGroupFormat, memberCountText, learnMoreText)
} else {
let legacyGroupFormat = NSLocalizedString("GROUPS_LEGACY_GROUP_CREATION_WARNING_FORMAT_1",
comment: "Indicates that a new group will be a legacy group because a member does not support v2 groups. Embeds {{ a \"learn more\" link. }}.")
legacyGroupText = String(format: legacyGroupFormat, learnMoreText)
}
let attributedString = NSMutableAttributedString(string: legacyGroupText)
attributedString.setAttributes([
.foregroundColor: Theme.accentBlueColor
],
forSubstring: learnMoreText)
let legacyGroupLabel = UILabel()
legacyGroupLabel.textColor = Theme.secondaryTextAndIconColor
legacyGroupLabel.font = .ows_dynamicTypeFootnote
legacyGroupLabel.attributedText = attributedString
legacyGroupLabel.numberOfLines = 0
legacyGroupLabel.lineBreakMode = .byWordWrapping
legacyGroupSection.addSubview(legacyGroupLabel)
legacyGroupLabel.autoPinEdgesToSuperviewMargins()
legacyGroupSection.isUserInteractionEnabled = true
legacyGroupSection.addGestureRecognizer(UITapGestureRecognizer(target: self,
action: #selector(didTapLegacyGroupView)))
}
recipientTableView.customSectionHeaderFooterBackgroundColor = Theme.backgroundColor
addChild(recipientTableView)
view.addSubview(recipientTableView.view)
recipientTableView.view.autoPinEdge(toSuperviewSafeArea: .leading)
recipientTableView.view.autoPinEdge(toSuperviewSafeArea: .trailing)
recipientTableView.view.autoPinEdge(.top, to: .bottom, of: lastSection)
autoPinView(toBottomOfViewControllerOrKeyboard: recipientTableView.view, avoidNotch: false)
updateTableContents()
}
private var membersDoNotSupportGroupsV2: [PickedRecipient] {
return databaseStorage.read { transaction in
self.recipientSet.orderedMembers.filter {
guard let address = $0.address else {
return false
}
return !GroupManager.doesUserSupportGroupsV2(address: address, transaction: transaction)
}
}
}
@objc
func didTapLegacyGroupView(sender: UIGestureRecognizer) {
let membersDoNotSupportGroupsV2 = self.membersDoNotSupportGroupsV2
guard !membersDoNotSupportGroupsV2.isEmpty else {
return
}
NewLegacyGroupView(v1Members: membersDoNotSupportGroupsV2).present(fromViewController: self)
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
nameTextField.becomeFirstResponder()
}
// MARK: -
private func updateTableContents() {
let section = OWSTableSection()
section.headerTitle = NSLocalizedString("GROUP_MEMBERS_SECTION_TITLE_MEMBERS",
comment: "Title for the 'members' section of the 'group members' view.")
let members = databaseStorage.uiRead { transaction in
BaseGroupMemberViewController.orderedMembers(recipientSet: self.recipientSet,
shouldSort: true,
transaction: transaction)
}.compactMap { $0.address }
if members.count > 0 {
for address in members {
section.add(OWSTableItem(
customCellBlock: {
let cell = ContactTableViewCell()
cell.selectionStyle = .none
cell.configure(withRecipientAddress: address)
return cell
},
customRowHeight: UITableView.automaticDimension))
}
} else {
section.add(OWSTableItem.softCenterLabel(withText: NSLocalizedString("GROUP_MEMBERS_NO_OTHER_MEMBERS",
comment: "Label indicating that a group has no other members."),
customRowHeight: UITableView.automaticDimension))
}
let contents = OWSTableContents()
contents.addSection(section)
recipientTableView.contents = contents
}
// MARK: - Actions
@objc
func createNewGroup() {
AssertIsOnMainThread()
guard let localAddress = tsAccountManager.localAddress else {
owsFailDebug("missing local address")
return
}
guard let groupName = newGroupState.groupName,
!groupName.isEmpty else {
Self.showMissingGroupNameAlert()
return
}
let avatarData = newGroupState.avatarData
let memberSet = Set([localAddress] + recipientSet.orderedMembers.compactMap { $0.address })
let members = Array(memberSet)
let newGroupSeed = groupSeed
// GroupsV2 TODO: Should we allow cancel here?
ModalActivityIndicatorViewController.present(fromViewController: self,
canCancel: false) { modalActivityIndicator in
firstly {
GroupManager.localCreateNewGroup(members: members,
groupId: nil,
name: groupName,
avatarData: avatarData,
newGroupSeed: newGroupSeed,
shouldSendMessage: true)
}.done { groupThread in
self.groupWasCreated(groupThread: groupThread,
modalActivityIndicator: modalActivityIndicator)
}.catch { error in
owsFailDebug("Could not create group: \(error)")
modalActivityIndicator.dismiss {
// Partial success could create the group on the service.
// This would cause retries to fail with 409. Therefore
// we must rotate the seed after every failure.
self.newGroupState.deriveNewGroupSeedForRetry()
NewGroupConfirmViewController.showCreateErrorUI(error: error)
}
}
}
}
public class func showCreateErrorUI(error: Error) {
AssertIsOnMainThread()
if error.isNetworkFailureOrTimeout {
OWSActionSheets.showActionSheet(title: NSLocalizedString("ERROR_NETWORK_FAILURE",
comment: "Error indicating network connectivity problems."),
message: NSLocalizedString("NEW_GROUP_CREATION_FAILED_DUE_TO_NETWORK",
comment: "Error indicating that a new group could not be created due to network connectivity problems."))
return
}
OWSActionSheets.showActionSheet(title: NSLocalizedString("NEW_GROUP_CREATION_FAILED",
comment: "Error indicating that a new group could not be created."))
}
public class func showMissingGroupNameAlert() {
AssertIsOnMainThread()
OWSActionSheets.showActionSheet(title: NSLocalizedString("NEW_GROUP_CREATION_MISSING_NAME_ALERT_TITLE",
comment: "Title for error alert indicating that a group name is required."),
message: NSLocalizedString("NEW_GROUP_CREATION_MISSING_NAME_ALERT_MESSAGE",
comment: "Message for error alert indicating that a group name is required."))
}
func groupWasCreated(groupThread: TSGroupThread,
modalActivityIndicator: ModalActivityIndicatorViewController) {
AssertIsOnMainThread()
let navigateToNewGroup = { (completion: (() -> Void)?) in
SignalApp.shared().presentConversation(for: groupThread,
action: .compose,
animated: false)
self.presentingViewController?.dismiss(animated: true, completion: completion)
}
let pendingMembers = groupThread.groupModel.groupMembership.pendingProfileKeyMembers
guard let firstPendingMember = pendingMembers.first else {
// No pending members.
return navigateToNewGroup(nil)
}
let alertTitle: String
let alertMessage: String
if pendingMembers.count > 0 {
let alertTitleFormat = NSLocalizedString("GROUP_INVITES_SENT_ALERT_TITLE_N_FORMAT",
comment: "Format for the title for an alert indicating that some members were invited to a group. Embeds: {{ the number of invites sent. }}")
alertTitle = String(format: alertTitleFormat, OWSFormat.formatInt(pendingMembers.count))
alertMessage = NSLocalizedString("GROUP_INVITES_SENT_ALERT_TITLE_N_MESSAGE",
comment: "Message for an alert indicating that some members were invited to a group.")
} else {
alertTitle = NSLocalizedString("GROUP_INVITES_SENT_ALERT_TITLE_1",
comment: "Title for an alert indicating that a member was invited to a group.")
let inviteeName = contactsManager.displayName(for: firstPendingMember)
let alertMessageFormat = NSLocalizedString("GROUP_INVITES_SENT_ALERT_MESSAGE_1_FORMAT",
comment: "Format for the message for an alert indicating that a member was invited to a group. Embeds: {{ the number of invites sent. }}")
alertMessage = String(format: alertMessageFormat, inviteeName)
}
let actionSheet = ActionSheetController(title: alertTitle, message: alertMessage)
actionSheet.addAction(ActionSheetAction(title: CommonStrings.learnMore,
style: .default) { _ in
// We present the "learn more" view atop the
// new conversation view to avoid users getting
// stucks in the "create group" view.
navigateToNewGroup {
Self.showLearnMoreView()
}
})
actionSheet.addAction(ActionSheetAction(title: CommonStrings.okayButton,
style: .default) { _ in
navigateToNewGroup(nil)
})
modalActivityIndicator.dismiss {
self.presentActionSheet(actionSheet)
}
}
private class func showLearnMoreView() {
guard let url = URL(string: "https://support.signal.org/hc/articles/360007319331") else {
owsFailDebug("Invalid url.")
return
}
guard let fromViewController = CurrentAppContext().frontmostViewController() else {
owsFailDebug("Missing fromViewController.")
return
}
let vc = SFSafariViewController(url: url)
fromViewController.present(vc, animated: true, completion: nil)
}
}
// MARK: -
extension NewGroupConfirmViewController: AvatarViewHelperDelegate {
public func avatarActionSheetTitle() -> String? {
return NSLocalizedString("NEW_GROUP_ADD_PHOTO_ACTION", comment: "Action Sheet title prompting the user for a group avatar")
}
public func avatarDidChange(_ image: UIImage) {
helper.setAvatarImage(image)
}
public func fromViewController() -> UIViewController {
return self
}
public func hasClearAvatarAction() -> Bool {
return newGroupState.avatarData != nil
}
public func clearAvatar() {
helper.setAvatarImage(nil)
}
public func clearAvatarActionLabel() -> String {
return NSLocalizedString("EDIT_GROUP_CLEAR_AVATAR", comment: "The 'clear avatar' button in the 'edit group' view.")
}
}
// MARK: -
extension NewGroupConfirmViewController: GroupAttributesEditorHelperDelegate {
func groupAttributesEditorContentsDidChange() {
newGroupState.groupName = helper.groupNameCurrent
newGroupState.avatarData = helper.avatarCurrent?.imageData
}
}
// MARK: -
class NewLegacyGroupView: UIView {
// MARK: - Dependencies
private var databaseStorage: SDSDatabaseStorage {
return SDSDatabaseStorage.shared
}
// MARK: -
private let v1Members: [PickedRecipient]
private let tableViewController = OWSTableViewController()
weak var actionSheetController: ActionSheetController?
required init(v1Members: [PickedRecipient]) {
self.v1Members = v1Members
super.init(frame: .zero)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func present(fromViewController: UIViewController) {
let wrapViewWithHMargins = { (viewToWrap: UIView) -> UIView in
let stackView = UIStackView(arrangedSubviews: [viewToWrap])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 24)
stackView.isLayoutMarginsRelativeArrangement = true
return stackView
}
let headerLabel = UILabel()
headerLabel.textColor = Theme.primaryTextColor
headerLabel.numberOfLines = 0
headerLabel.lineBreakMode = .byWordWrapping
headerLabel.font = UIFont.ows_dynamicTypeBody
if v1Members.count > 1 {
let format = NSLocalizedString("GROUPS_LEGACY_GROUP_CREATION_WARNING_ALERT_TITLE_N_FORMAT",
comment: "Title for alert that explains that a new group will be a legacy group because multiple members do not support v2 groups. Embeds {{ the number of members which do not support v2 groups. }}")
let formattedCount = OWSFormat.formatInt(v1Members.count)
headerLabel.text = String(format: format, formattedCount)
} else {
headerLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_CREATION_WARNING_ALERT_TITLE_1",
comment: "Title for alert that explains that a new group will be a legacy group because 1 member does not support v2 groups.")
}
headerLabel.textAlignment = .center
let members = databaseStorage.uiRead { transaction in
BaseGroupMemberViewController.orderedMembers(recipientSet: OrderedSet(self.v1Members),
shouldSort: true,
transaction: transaction)
}.compactMap { $0.address }
let section = OWSTableSection()
for address in members {
section.add(OWSTableItem(
customCellBlock: {
let cell = ContactTableViewCell()
cell.selectionStyle = .none
cell.configure(withRecipientAddress: address)
return cell
},
customRowHeight: UITableView.automaticDimension))
}
let contents = OWSTableContents()
contents.addSection(section)
tableViewController.contents = contents
tableViewController.view.autoSetDimension(.height, toSize: 200)
let buttonFont = UIFont.ows_dynamicTypeBodyClamped.ows_semibold()
let buttonHeight = OWSFlatButton.heightForFont(buttonFont)
let okayButton = OWSFlatButton.button(title: CommonStrings.okayButton,
font: buttonFont,
titleColor: .white,
backgroundColor: .ows_accentBlue,
target: self,
selector: #selector(dismissAlert))
okayButton.autoSetDimension(.height, toSize: buttonHeight)
let stackView = UIStackView(arrangedSubviews: [
wrapViewWithHMargins(headerLabel),
UIView.spacer(withHeight: 20),
tableViewController.view,
UIView.spacer(withHeight: 20),
wrapViewWithHMargins(okayButton)
])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: 20, leading: 0, bottom: 38, trailing: 0)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.addBackgroundView(withBackgroundColor: Theme.backgroundColor)
layoutMargins = .zero
addSubview(stackView)
stackView.autoPinEdgesToSuperviewMargins()
let actionSheetController = ActionSheetController()
actionSheetController.customHeader = self
actionSheetController.isCancelable = true
fromViewController.presentActionSheet(actionSheetController)
self.actionSheetController = actionSheetController
}
@objc
func dismissAlert() {
actionSheetController?.dismiss(animated: true)
}
}