mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			313 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			313 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
import PromiseKit
 | 
						|
 | 
						|
@objc(SNEditClosedGroupVC)
 | 
						|
final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
 | 
						|
    private let thread: TSGroupThread
 | 
						|
    private var name = ""
 | 
						|
    private var zombies: Set<String> = []
 | 
						|
    private var membersAndZombies: [String] = [] { didSet { handleMembersChanged() } }
 | 
						|
    private var isEditingGroupName = false { didSet { handleIsEditingGroupNameChanged() } }
 | 
						|
    private var tableViewHeightConstraint: NSLayoutConstraint!
 | 
						|
 | 
						|
    private lazy var groupPublicKey: String = {
 | 
						|
        let groupID = thread.groupModel.groupId
 | 
						|
        return LKGroupUtilities.getDecodedGroupID(groupID)
 | 
						|
    }()
 | 
						|
    
 | 
						|
    // MARK: Components
 | 
						|
    private lazy var groupNameLabel: UILabel = {
 | 
						|
        let result = UILabel()
 | 
						|
        result.textColor = Colors.text
 | 
						|
        result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
 | 
						|
        result.lineBreakMode = .byTruncatingTail
 | 
						|
        result.textAlignment = .center
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
 | 
						|
    private lazy var groupNameTextField: TextField = {
 | 
						|
        let result = TextField(placeholder: "Enter a group name", usesDefaultHeight: false)
 | 
						|
        result.textAlignment = .center
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
 | 
						|
    private lazy var addMembersButton: Button = {
 | 
						|
        let result = Button(style: .prominentOutline, size: .large)
 | 
						|
        result.setTitle("Add Members", for: UIControl.State.normal)
 | 
						|
        result.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside)
 | 
						|
        result.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing)
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
 | 
						|
    @objc private lazy var tableView: UITableView = {
 | 
						|
        let result = UITableView()
 | 
						|
        result.dataSource = self
 | 
						|
        result.delegate = self
 | 
						|
        result.register(UserCell.self, forCellReuseIdentifier: "UserCell")
 | 
						|
        result.separatorStyle = .none
 | 
						|
        result.backgroundColor = .clear
 | 
						|
        result.isScrollEnabled = false
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
 | 
						|
    // MARK: Lifecycle
 | 
						|
    @objc(initWithThreadID:)
 | 
						|
    init(with threadID: String) {
 | 
						|
        var thread: TSGroupThread!
 | 
						|
        Storage.read { transaction in
 | 
						|
            thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction)!
 | 
						|
        }
 | 
						|
        self.thread = thread
 | 
						|
        super.init(nibName: nil, bundle: nil)
 | 
						|
    }
 | 
						|
 | 
						|
    required init?(coder: NSCoder) {
 | 
						|
        preconditionFailure("Use init(with:) instead.")
 | 
						|
    }
 | 
						|
 | 
						|
    override func viewDidLoad() {
 | 
						|
        super.viewDidLoad()
 | 
						|
        setUpGradientBackground()
 | 
						|
        setUpNavBarStyle()
 | 
						|
        setNavBarTitle("Edit Group")
 | 
						|
        let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
 | 
						|
        backButton.tintColor = Colors.text
 | 
						|
        navigationItem.backBarButtonItem = backButton
 | 
						|
        func getDisplayName(for publicKey: String) -> String {
 | 
						|
            return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
 | 
						|
        }
 | 
						|
        setUpViewHierarchy()
 | 
						|
        // Always show zombies at the bottom
 | 
						|
        zombies = Storage.shared.getZombieMembers(for: groupPublicKey)
 | 
						|
        membersAndZombies = GroupUtilities.getClosedGroupMembers(thread).sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
 | 
						|
            + zombies.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
 | 
						|
        updateNavigationBarButtons()
 | 
						|
        name = thread.groupModel.groupName!
 | 
						|
    }
 | 
						|
 | 
						|
    private func setUpViewHierarchy() {
 | 
						|
        // Group name container
 | 
						|
        groupNameLabel.text = thread.groupModel.groupName
 | 
						|
        let groupNameContainer = UIView()
 | 
						|
        groupNameContainer.addSubview(groupNameLabel)
 | 
						|
        groupNameLabel.pin(to: groupNameContainer)
 | 
						|
        groupNameContainer.addSubview(groupNameTextField)
 | 
						|
        groupNameTextField.pin(to: groupNameContainer)
 | 
						|
        groupNameContainer.set(.height, to: 40)
 | 
						|
        groupNameTextField.alpha = 0
 | 
						|
        // Top container
 | 
						|
        let topContainer = UIView()
 | 
						|
        topContainer.addSubview(groupNameContainer)
 | 
						|
        groupNameContainer.center(in: topContainer)
 | 
						|
        topContainer.set(.height, to: 40)
 | 
						|
        let topContainerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditGroupNameUI))
 | 
						|
        topContainer.addGestureRecognizer(topContainerTapGestureRecognizer)
 | 
						|
        // Members label
 | 
						|
        let membersLabel = UILabel()
 | 
						|
        membersLabel.textColor = Colors.text
 | 
						|
        membersLabel.font = .systemFont(ofSize: Values.mediumFontSize)
 | 
						|
        membersLabel.text = "Members"
 | 
						|
        // Add members button
 | 
						|
        let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.membersAndZombies).isEmpty
 | 
						|
        if (!hasContactsToAdd) {
 | 
						|
            addMembersButton.isUserInteractionEnabled = false
 | 
						|
            let disabledColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
 | 
						|
            addMembersButton.layer.borderColor = disabledColor.cgColor
 | 
						|
            addMembersButton.setTitleColor(disabledColor, for: UIControl.State.normal)
 | 
						|
        }
 | 
						|
        // Middle stack view
 | 
						|
        let middleStackView = UIStackView(arrangedSubviews: [ membersLabel, addMembersButton ])
 | 
						|
        middleStackView.axis = .horizontal
 | 
						|
        middleStackView.alignment = .center
 | 
						|
        middleStackView.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, leading: Values.mediumSpacing, bottom: Values.smallSpacing, trailing: Values.mediumSpacing)
 | 
						|
        middleStackView.isLayoutMarginsRelativeArrangement = true
 | 
						|
        middleStackView.set(.height, to: Values.largeButtonHeight + Values.smallSpacing * 2)
 | 
						|
        // Table view
 | 
						|
        tableViewHeightConstraint = tableView.set(.height, to: 0)
 | 
						|
        // Main stack view
 | 
						|
        let mainStackView = UIStackView(arrangedSubviews: [
 | 
						|
            UIView.vSpacer(Values.veryLargeSpacing),
 | 
						|
            topContainer,
 | 
						|
            UIView.vSpacer(Values.veryLargeSpacing),
 | 
						|
            UIView.separator(),
 | 
						|
            middleStackView,
 | 
						|
            UIView.separator(),
 | 
						|
            tableView
 | 
						|
        ])
 | 
						|
        mainStackView.axis = .vertical
 | 
						|
        mainStackView.alignment = .fill
 | 
						|
        mainStackView.set(.width, to: UIScreen.main.bounds.width)
 | 
						|
        // Scroll view
 | 
						|
        let scrollView = UIScrollView()
 | 
						|
        scrollView.showsVerticalScrollIndicator = false
 | 
						|
        scrollView.addSubview(mainStackView)
 | 
						|
        mainStackView.pin(to: scrollView)
 | 
						|
        view.addSubview(scrollView)
 | 
						|
        scrollView.pin(to: view)
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: Table View Data Source / Delegate
 | 
						|
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 | 
						|
        return membersAndZombies.count
 | 
						|
    }
 | 
						|
 | 
						|
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 | 
						|
        let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell") as! UserCell
 | 
						|
        let publicKey = membersAndZombies[indexPath.row]
 | 
						|
        cell.publicKey = publicKey
 | 
						|
        cell.isZombie = zombies.contains(publicKey)
 | 
						|
        let userPublicKey = getUserHexEncodedPublicKey()
 | 
						|
        let isCurrentUserAdmin = thread.groupModel.groupAdminIds.contains(userPublicKey)
 | 
						|
        cell.accessory = !isCurrentUserAdmin ? .lock : .none
 | 
						|
        cell.update()
 | 
						|
        return cell
 | 
						|
    }
 | 
						|
 | 
						|
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
 | 
						|
        let userPublicKey = getUserHexEncodedPublicKey()
 | 
						|
        return thread.groupModel.groupAdminIds.contains(userPublicKey)
 | 
						|
    }
 | 
						|
 | 
						|
    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
 | 
						|
        let publicKey = membersAndZombies[indexPath.row]
 | 
						|
        let removeAction = UITableViewRowAction(style: .destructive, title: "Remove") { [weak self] _, _ in
 | 
						|
            guard let self = self, let index = self.membersAndZombies.firstIndex(of: publicKey) else { return }
 | 
						|
            self.membersAndZombies.remove(at: index)
 | 
						|
        }
 | 
						|
        removeAction.backgroundColor = Colors.destructive
 | 
						|
        return [ removeAction ]
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: Updating
 | 
						|
    private func updateNavigationBarButtons() {
 | 
						|
        if isEditingGroupName {
 | 
						|
            let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCancelGroupNameEditingButtonTapped))
 | 
						|
            cancelButton.tintColor = Colors.text
 | 
						|
            navigationItem.leftBarButtonItem = cancelButton
 | 
						|
        } else {
 | 
						|
            navigationItem.leftBarButtonItem = nil
 | 
						|
        }
 | 
						|
        let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneButtonTapped))
 | 
						|
        doneButton.tintColor = Colors.text
 | 
						|
        navigationItem.rightBarButtonItem = doneButton
 | 
						|
    }
 | 
						|
 | 
						|
    private func handleMembersChanged() {
 | 
						|
        tableViewHeightConstraint.constant = CGFloat(membersAndZombies.count) * 67
 | 
						|
        tableView.reloadData()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func handleIsEditingGroupNameChanged() {
 | 
						|
        updateNavigationBarButtons()
 | 
						|
        UIView.animate(withDuration: 0.25) {
 | 
						|
            self.groupNameLabel.alpha = self.isEditingGroupName ? 0 : 1
 | 
						|
            self.groupNameTextField.alpha = self.isEditingGroupName ? 1 : 0
 | 
						|
        }
 | 
						|
        if isEditingGroupName {
 | 
						|
            groupNameTextField.becomeFirstResponder()
 | 
						|
        } else {
 | 
						|
            groupNameTextField.resignFirstResponder()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: Interaction
 | 
						|
    @objc private func showEditGroupNameUI() {
 | 
						|
        isEditingGroupName = true
 | 
						|
    }
 | 
						|
 | 
						|
    @objc private func handleCancelGroupNameEditingButtonTapped() {
 | 
						|
        isEditingGroupName = false
 | 
						|
    }
 | 
						|
 | 
						|
    @objc private func handleDoneButtonTapped() {
 | 
						|
        if isEditingGroupName {
 | 
						|
            updateGroupName()
 | 
						|
        } else {
 | 
						|
            commitChanges()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private func updateGroupName() {
 | 
						|
        let name = groupNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
 | 
						|
        guard !name.isEmpty else {
 | 
						|
            return showError(title: NSLocalizedString("vc_create_closed_group_group_name_missing_error", comment: ""))
 | 
						|
        }
 | 
						|
        guard name.count < 64 else {
 | 
						|
            return showError(title: NSLocalizedString("vc_create_closed_group_group_name_too_long_error", comment: ""))
 | 
						|
        }
 | 
						|
        isEditingGroupName = false
 | 
						|
        self.name = name
 | 
						|
        groupNameLabel.text = name
 | 
						|
    }
 | 
						|
 | 
						|
    @objc private func addMembers() {
 | 
						|
        let title = "Add Members"
 | 
						|
        let userSelectionVC = UserSelectionVC(with: title, excluding: Set(membersAndZombies)) { [weak self] selectedUsers in
 | 
						|
            guard let self = self else { return }
 | 
						|
            var members = self.membersAndZombies
 | 
						|
            members.append(contentsOf: selectedUsers)
 | 
						|
            func getDisplayName(for publicKey: String) -> String {
 | 
						|
                return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
 | 
						|
            }
 | 
						|
            self.membersAndZombies = members.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
 | 
						|
            let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.membersAndZombies).isEmpty
 | 
						|
            self.addMembersButton.isUserInteractionEnabled = hasContactsToAdd
 | 
						|
            let color = hasContactsToAdd ? Colors.accent : Colors.text.withAlphaComponent(Values.mediumOpacity)
 | 
						|
            self.addMembersButton.layer.borderColor = color.cgColor
 | 
						|
            self.addMembersButton.setTitleColor(color, for: UIControl.State.normal)
 | 
						|
        }
 | 
						|
        navigationController!.pushViewController(userSelectionVC, animated: true, completion: nil)
 | 
						|
    }
 | 
						|
 | 
						|
    private func commitChanges() {
 | 
						|
        let popToConversationVC: (EditClosedGroupVC) -> Void = { editVC in
 | 
						|
            if let conversationVC = editVC.navigationController!.viewControllers.first(where: { $0 is ConversationVC }) {
 | 
						|
                editVC.navigationController!.popToViewController(conversationVC, animated: true)
 | 
						|
            } else {
 | 
						|
                editVC.navigationController!.popViewController(animated: true)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        let storage = SNMessagingKitConfiguration.shared.storage
 | 
						|
        let members = Set(self.membersAndZombies)
 | 
						|
        let name = self.name
 | 
						|
        let zombies = storage.getZombieMembers(for: groupPublicKey)
 | 
						|
        guard members != Set(thread.groupModel.groupMemberIds + zombies) || name != thread.groupModel.groupName else {
 | 
						|
            return popToConversationVC(self)
 | 
						|
        }
 | 
						|
        if !members.contains(getUserHexEncodedPublicKey()) {
 | 
						|
            guard Set(thread.groupModel.groupMemberIds).subtracting([ getUserHexEncodedPublicKey() ]) == members else {
 | 
						|
                return showError(title: "Couldn't Update Group", message: "Can't leave while adding or removing other members.")
 | 
						|
            }
 | 
						|
        }
 | 
						|
        guard members.count <= 100 else {
 | 
						|
            return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: ""))
 | 
						|
        }
 | 
						|
        var promise: Promise<Void>!
 | 
						|
        ModalActivityIndicatorViewController.present(fromViewController: navigationController!) { [groupPublicKey, weak self] _ in
 | 
						|
            Storage.write(with: { transaction in
 | 
						|
                if !members.contains(getUserHexEncodedPublicKey()) {
 | 
						|
                    promise = MessageSender.leave(groupPublicKey, using: transaction)
 | 
						|
                } else {
 | 
						|
                    promise = MessageSender.update(groupPublicKey, with: members, name: name, transaction: transaction)
 | 
						|
                }
 | 
						|
            }, completion: {
 | 
						|
                let _ = promise.done(on: DispatchQueue.main) {
 | 
						|
                    guard let self = self else { return }
 | 
						|
                    self.dismiss(animated: true, completion: nil) // Dismiss the loader
 | 
						|
                    popToConversationVC(self)
 | 
						|
                }
 | 
						|
                promise.catch(on: DispatchQueue.main) { error in
 | 
						|
                    self?.dismiss(animated: true, completion: nil) // Dismiss the loader
 | 
						|
                    self?.showError(title: "Couldn't Update Group", message: error.localizedDescription)
 | 
						|
                }
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: Convenience
 | 
						|
    private func showError(title: String, message: String = "") {
 | 
						|
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
 | 
						|
        alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
 | 
						|
        presentAlert(alert)
 | 
						|
    }
 | 
						|
}
 |