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.
		
		
		
		
		
			
		
			
				
	
	
		
			516 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			516 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import UIKit
 | 
						|
import GRDB
 | 
						|
import DifferenceKit
 | 
						|
import SessionUIKit
 | 
						|
import SessionMessagingKit
 | 
						|
import SignalUtilitiesKit
 | 
						|
 | 
						|
class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
 | 
						|
    private static let loadingHeaderHeight: CGFloat = 40
 | 
						|
    
 | 
						|
    private let viewModel: BlockedContactsViewModel = BlockedContactsViewModel()
 | 
						|
    private var dataChangeObservable: DatabaseCancellable?
 | 
						|
    private var hasLoadedInitialContactData: Bool = false
 | 
						|
    private var isLoadingMore: Bool = false
 | 
						|
    private var isAutoLoadingNextPage: Bool = false
 | 
						|
    private var viewHasAppeared: Bool = false
 | 
						|
    
 | 
						|
    // MARK: - Intialization
 | 
						|
    
 | 
						|
    init() {
 | 
						|
        Storage.shared.addObserver(viewModel.pagedDataObserver)
 | 
						|
        
 | 
						|
        super.init(nibName: nil, bundle: nil)
 | 
						|
    }
 | 
						|
 | 
						|
    required init?(coder: NSCoder) {
 | 
						|
        preconditionFailure("Use init() instead.")
 | 
						|
    }
 | 
						|
    
 | 
						|
    deinit {
 | 
						|
        NotificationCenter.default.removeObserver(self)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - UI
 | 
						|
 | 
						|
    private lazy var tableView: UITableView = {
 | 
						|
        let result: UITableView = UITableView()
 | 
						|
        result.translatesAutoresizingMaskIntoConstraints = false
 | 
						|
        result.clipsToBounds = true
 | 
						|
        result.separatorStyle = .none
 | 
						|
        result.themeBackgroundColor = .clear
 | 
						|
        result.showsVerticalScrollIndicator = false
 | 
						|
        result.register(view: SessionCell.self)
 | 
						|
        result.dataSource = self
 | 
						|
        result.delegate = self
 | 
						|
        result.layer.cornerRadius = SessionCell.cornerRadius
 | 
						|
        
 | 
						|
        if #available(iOS 15.0, *) {
 | 
						|
            result.sectionHeaderTopPadding = 0
 | 
						|
        }
 | 
						|
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
 | 
						|
    private lazy var emptyStateLabel: UILabel = {
 | 
						|
        let result: UILabel = UILabel()
 | 
						|
        result.translatesAutoresizingMaskIntoConstraints = false
 | 
						|
        result.isUserInteractionEnabled = false
 | 
						|
        result.font = .systemFont(ofSize: Values.smallFontSize)
 | 
						|
        result.text = "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE".localized()
 | 
						|
        result.themeTextColor = .textSecondary
 | 
						|
        result.textAlignment = .center
 | 
						|
        result.numberOfLines = 0
 | 
						|
        result.isHidden = true
 | 
						|
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
    
 | 
						|
    private lazy var fadeView: GradientView = {
 | 
						|
        let result: GradientView = GradientView()
 | 
						|
        result.themeBackgroundGradient = [
 | 
						|
            .value(.backgroundPrimary, alpha: 0), // Want this to take up 20% (~25pt)
 | 
						|
            .backgroundPrimary,
 | 
						|
            .backgroundPrimary,
 | 
						|
            .backgroundPrimary,
 | 
						|
            .backgroundPrimary
 | 
						|
        ]
 | 
						|
        result.set(.height, to: Values.footerGradientHeight(window: UIApplication.shared.keyWindow))
 | 
						|
        
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
    
 | 
						|
    private lazy var unblockButton: SessionButton = {
 | 
						|
        let result: SessionButton = SessionButton(style: .destructive, size: .large)
 | 
						|
        result.translatesAutoresizingMaskIntoConstraints = false
 | 
						|
        result.setTitle("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK".localized(), for: .normal)
 | 
						|
        result.addTarget(self, action: #selector(unblockTapped), for: .touchUpInside)
 | 
						|
 | 
						|
        return result
 | 
						|
    }()
 | 
						|
 | 
						|
    // MARK: - Lifecycle
 | 
						|
 | 
						|
    override func viewDidLoad() {
 | 
						|
        super.viewDidLoad()
 | 
						|
        
 | 
						|
        view.themeBackgroundColor = .backgroundPrimary
 | 
						|
 | 
						|
        ViewControllerUtilities.setUpDefaultSessionStyle(
 | 
						|
            for: self,
 | 
						|
               title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized(),
 | 
						|
               hasCustomBackButton: false
 | 
						|
        )
 | 
						|
 | 
						|
        view.addSubview(tableView)
 | 
						|
        view.addSubview(emptyStateLabel)
 | 
						|
        view.addSubview(fadeView)
 | 
						|
        view.addSubview(unblockButton)
 | 
						|
        setupLayout()
 | 
						|
 | 
						|
        // Notifications
 | 
						|
        NotificationCenter.default.addObserver(
 | 
						|
            self,
 | 
						|
            selector: #selector(applicationDidBecomeActive(_:)),
 | 
						|
            name: UIApplication.didBecomeActiveNotification,
 | 
						|
            object: nil
 | 
						|
        )
 | 
						|
        NotificationCenter.default.addObserver(
 | 
						|
            self,
 | 
						|
            selector: #selector(applicationDidResignActive(_:)),
 | 
						|
            name: UIApplication.didEnterBackgroundNotification, object: nil
 | 
						|
        )
 | 
						|
    }
 | 
						|
    
 | 
						|
    override func viewWillAppear(_ animated: Bool) {
 | 
						|
        super.viewWillAppear(animated)
 | 
						|
        
 | 
						|
        startObservingChanges()
 | 
						|
    }
 | 
						|
    
 | 
						|
    override func viewDidAppear(_ animated: Bool) {
 | 
						|
        super.viewDidAppear(animated)
 | 
						|
        
 | 
						|
        self.viewHasAppeared = true
 | 
						|
        self.autoLoadNextPageIfNeeded()
 | 
						|
    }
 | 
						|
    
 | 
						|
    override func viewWillDisappear(_ animated: Bool) {
 | 
						|
        super.viewWillDisappear(animated)
 | 
						|
        
 | 
						|
        // Stop observing database changes
 | 
						|
        dataChangeObservable?.cancel()
 | 
						|
    }
 | 
						|
    
 | 
						|
    @objc func applicationDidBecomeActive(_ notification: Notification) {
 | 
						|
        startObservingChanges(didReturnFromBackground: true)
 | 
						|
    }
 | 
						|
    
 | 
						|
    @objc func applicationDidResignActive(_ notification: Notification) {
 | 
						|
        // Stop observing database changes
 | 
						|
        dataChangeObservable?.cancel()
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Layout
 | 
						|
 | 
						|
    private func setupLayout() {
 | 
						|
        NSLayoutConstraint.activate([
 | 
						|
            tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing),
 | 
						|
            tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.largeSpacing),
 | 
						|
            tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.largeSpacing),
 | 
						|
            tableView.bottomAnchor.constraint(
 | 
						|
                equalTo: unblockButton.topAnchor,
 | 
						|
                constant: -Values.largeSpacing
 | 
						|
            ),
 | 
						|
 | 
						|
            emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing),
 | 
						|
            emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing),
 | 
						|
            emptyStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.mediumSpacing),
 | 
						|
            emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
 | 
						|
            
 | 
						|
            fadeView.leftAnchor.constraint(equalTo: view.leftAnchor),
 | 
						|
            fadeView.rightAnchor.constraint(equalTo: view.rightAnchor),
 | 
						|
            fadeView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
 | 
						|
            
 | 
						|
            unblockButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
 | 
						|
            unblockButton.bottomAnchor.constraint(
 | 
						|
                equalTo: view.safeAreaLayoutGuide.bottomAnchor,
 | 
						|
                constant: -Values.smallSpacing
 | 
						|
            ),
 | 
						|
            unblockButton.widthAnchor.constraint(equalToConstant: Values.iPadButtonWidth)
 | 
						|
        ])
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - Updating
 | 
						|
    
 | 
						|
    private func startObservingChanges(didReturnFromBackground: Bool = false) {
 | 
						|
        self.viewModel.onContactChange = { [weak self] updatedContactData, changeset in
 | 
						|
            self?.handleContactUpdates(updatedContactData, changeset: changeset)
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Note: When returning from the background we could have received notifications but the
 | 
						|
        // PagedDatabaseObserver won't have them so we need to force a re-fetch of the current
 | 
						|
        // data to ensure everything is up to date
 | 
						|
        if didReturnFromBackground {
 | 
						|
            self.viewModel.pagedDataObserver?.reload()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func handleContactUpdates(
 | 
						|
        _ updatedData: [BlockedContactsViewModel.SectionModel],
 | 
						|
        changeset: StagedChangeset<[BlockedContactsViewModel.SectionModel]>,
 | 
						|
        initialLoad: Bool = false
 | 
						|
    ) {
 | 
						|
        // Ensure the first load runs without animations (if we don't do this the cells will animate
 | 
						|
        // in from a frame of CGRect.zero)
 | 
						|
        guard hasLoadedInitialContactData else {
 | 
						|
            hasLoadedInitialContactData = true
 | 
						|
            UIView.performWithoutAnimation {
 | 
						|
                handleContactUpdates(updatedData, changeset: changeset, initialLoad: true)
 | 
						|
            }
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Show the empty state if there is no data
 | 
						|
        let hasContactsData: Bool = (updatedData
 | 
						|
            .first(where: { $0.model == .contacts })?
 | 
						|
            .elements
 | 
						|
            .isEmpty == false)
 | 
						|
        unblockButton.isEnabled = !viewModel.selectedContactIds.isEmpty
 | 
						|
        unblockButton.isHidden = !hasContactsData
 | 
						|
        emptyStateLabel.isHidden = hasContactsData
 | 
						|
        
 | 
						|
        CATransaction.begin()
 | 
						|
        CATransaction.setCompletionBlock { [weak self] in
 | 
						|
            // Complete page loading
 | 
						|
            self?.isLoadingMore = false
 | 
						|
            self?.autoLoadNextPageIfNeeded()
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Reload the table content (animate changes after the first load)
 | 
						|
        tableView.reload(
 | 
						|
            using: changeset,
 | 
						|
            deleteSectionsAnimation: .none,
 | 
						|
            insertSectionsAnimation: .none,
 | 
						|
            reloadSectionsAnimation: .none,
 | 
						|
            deleteRowsAnimation: .bottom,
 | 
						|
            insertRowsAnimation: .top,
 | 
						|
            reloadRowsAnimation: .none,
 | 
						|
            interrupt: { $0.changeCount > 100 }    // Prevent too many changes from causing performance issues
 | 
						|
        ) { [weak self] updatedData in
 | 
						|
            self?.viewModel.updateContactData(updatedData)
 | 
						|
        }
 | 
						|
        
 | 
						|
        CATransaction.commit()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func autoLoadNextPageIfNeeded() {
 | 
						|
        guard !self.isAutoLoadingNextPage && !self.isLoadingMore else { return }
 | 
						|
        
 | 
						|
        self.isAutoLoadingNextPage = true
 | 
						|
        
 | 
						|
        DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in
 | 
						|
            self?.isAutoLoadingNextPage = false
 | 
						|
            
 | 
						|
            // Note: We sort the headers as we want to prioritise loading newer pages over older ones
 | 
						|
            let sections: [(BlockedContactsViewModel.Section, CGRect)] = (self?.viewModel.contactData
 | 
						|
                .enumerated()
 | 
						|
                .map { index, section in
 | 
						|
                    (section.model, (self?.tableView.rectForHeader(inSection: index) ?? .zero))
 | 
						|
                })
 | 
						|
                .defaulting(to: [])
 | 
						|
            let shouldLoadMore: Bool = sections
 | 
						|
                .contains { section, headerRect in
 | 
						|
                    section == .loadMore &&
 | 
						|
                    headerRect != .zero &&
 | 
						|
                    (self?.tableView.bounds.contains(headerRect) == true)
 | 
						|
                }
 | 
						|
            
 | 
						|
            guard shouldLoadMore else { return }
 | 
						|
            
 | 
						|
            self?.isLoadingMore = true
 | 
						|
            
 | 
						|
            DispatchQueue.global(qos: .userInitiated).async { [weak self] in
 | 
						|
                self?.viewModel.pagedDataObserver?.load(.pageAfter)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - UITableViewDataSource
 | 
						|
 | 
						|
    func numberOfSections(in tableView: UITableView) -> Int {
 | 
						|
        return viewModel.contactData.count
 | 
						|
    }
 | 
						|
    
 | 
						|
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 | 
						|
        let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section]
 | 
						|
        
 | 
						|
        return section.elements.count
 | 
						|
    }
 | 
						|
 | 
						|
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 | 
						|
        let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[indexPath.section]
 | 
						|
        
 | 
						|
        switch section.model {
 | 
						|
            case .contacts:
 | 
						|
                let info: SessionCell.Info<Profile> = section.elements[indexPath.row]
 | 
						|
                let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
 | 
						|
                cell.update(
 | 
						|
                    with: info,
 | 
						|
                    style: .roundedEdgeToEdge,
 | 
						|
                    position: Position.with(indexPath.row, count: section.elements.count)
 | 
						|
                )
 | 
						|
                
 | 
						|
                return cell
 | 
						|
 | 
						|
            default: preconditionFailure("Other sections should have no content")
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
 | 
						|
        let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section]
 | 
						|
        
 | 
						|
        switch section.model {
 | 
						|
            case .loadMore:
 | 
						|
                let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium)
 | 
						|
                loadingIndicator.themeTintColor = .textPrimary
 | 
						|
                loadingIndicator.alpha = 0.5
 | 
						|
                loadingIndicator.startAnimating()
 | 
						|
                
 | 
						|
                let view: UIView = UIView()
 | 
						|
                view.addSubview(loadingIndicator)
 | 
						|
                loadingIndicator.center(in: view)
 | 
						|
                
 | 
						|
                return view
 | 
						|
            
 | 
						|
            default: return nil
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - UITableViewDelegate
 | 
						|
    
 | 
						|
    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
 | 
						|
        return UITableView.automaticDimension
 | 
						|
    }
 | 
						|
    
 | 
						|
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
 | 
						|
        return UITableView.automaticDimension
 | 
						|
    }
 | 
						|
    
 | 
						|
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
 | 
						|
        let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section]
 | 
						|
        
 | 
						|
        switch section.model {
 | 
						|
            case .loadMore: return BlockedContactsViewController.loadingHeaderHeight
 | 
						|
            default: return 0
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
 | 
						|
        guard self.hasLoadedInitialContactData && self.viewHasAppeared && !self.isLoadingMore else { return }
 | 
						|
        
 | 
						|
        let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[section]
 | 
						|
        
 | 
						|
        switch section.model {
 | 
						|
            case .loadMore:
 | 
						|
                self.isLoadingMore = true
 | 
						|
                
 | 
						|
                DispatchQueue.global(qos: .userInitiated).async { [weak self] in
 | 
						|
                    self?.viewModel.pagedDataObserver?.load(.pageAfter)
 | 
						|
                }
 | 
						|
                
 | 
						|
            default: break
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 | 
						|
        tableView.deselectRow(at: indexPath, animated: true)
 | 
						|
        
 | 
						|
        let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[indexPath.section]
 | 
						|
        
 | 
						|
        switch section.model {
 | 
						|
            case .contacts:
 | 
						|
                let info: SessionCell.Info<Profile> = section.elements[indexPath.row]
 | 
						|
                
 | 
						|
                // Do nothing if the item is disabled
 | 
						|
                guard info.isEnabled else { return }
 | 
						|
                
 | 
						|
                // Get the view that was tapped (for presenting on iPad)
 | 
						|
                let tappedView: UIView? = tableView.cellForRow(at: indexPath)
 | 
						|
                let maybeOldSelection: (Int, SessionCell.Info<Profile>)? = section.elements
 | 
						|
                    .enumerated()
 | 
						|
                    .first(where: { index, info in
 | 
						|
                        switch (info.leftAccessory, info.rightAccessory) {
 | 
						|
                            case (_, .radio(_, let isSelected, _)): return isSelected()
 | 
						|
                            case (.radio(_, let isSelected, _), _): return isSelected()
 | 
						|
                            default: return false
 | 
						|
                        }
 | 
						|
                    })
 | 
						|
                
 | 
						|
                info.onTap?(tappedView)
 | 
						|
                self.manuallyReload(indexPath: indexPath, section: section, info: info)
 | 
						|
                self.unblockButton.isEnabled = !self.viewModel.selectedContactIds.isEmpty
 | 
						|
                
 | 
						|
                // Update the old selection as well
 | 
						|
                if let oldSelection: (index: Int, info: SessionCell.Info<Profile>) = maybeOldSelection {
 | 
						|
                    self.manuallyReload(
 | 
						|
                        indexPath: IndexPath(
 | 
						|
                            row: oldSelection.index,
 | 
						|
                            section: indexPath.section
 | 
						|
                        ),
 | 
						|
                        section: section,
 | 
						|
                        info: oldSelection.info
 | 
						|
                    )
 | 
						|
                }
 | 
						|
                
 | 
						|
            default: break
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func manuallyReload(
 | 
						|
        indexPath: IndexPath,
 | 
						|
        section: BlockedContactsViewModel.SectionModel,
 | 
						|
        info: SessionCell.Info<Profile>
 | 
						|
    ) {
 | 
						|
        // Try update the existing cell to have a nice animation instead of reloading the cell
 | 
						|
        if let existingCell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell {
 | 
						|
            existingCell.update(
 | 
						|
                with: info,
 | 
						|
                style: .roundedEdgeToEdge,
 | 
						|
                position: Position.with(indexPath.row, count: section.elements.count)
 | 
						|
            )
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            tableView.reloadRows(at: [indexPath], with: .none)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: - Interaction
 | 
						|
    
 | 
						|
    @objc private func unblockTapped() {
 | 
						|
        guard !viewModel.selectedContactIds.isEmpty else { return }
 | 
						|
        
 | 
						|
        let contactIds: Set<String> = viewModel.selectedContactIds
 | 
						|
        let contactNames: [String] = contactIds
 | 
						|
            .map { contactId in
 | 
						|
                guard
 | 
						|
                    let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData
 | 
						|
                        .first(where: { section in section.model == .contacts }),
 | 
						|
                    let info: SessionCell.Info<Profile> = section.elements
 | 
						|
                        .first(where: { info in info.id.id == contactId })
 | 
						|
                else { return contactId }
 | 
						|
                
 | 
						|
                return info.title
 | 
						|
            }
 | 
						|
        let confirmationTitle: String = {
 | 
						|
            guard contactNames.count > 1 else {
 | 
						|
                // Show a single users name
 | 
						|
                return String(
 | 
						|
                    format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_SINGLE".localized(),
 | 
						|
                    (
 | 
						|
                        contactNames.first ??
 | 
						|
                        "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_FALLBACK".localized()
 | 
						|
                    )
 | 
						|
                )
 | 
						|
            }
 | 
						|
            guard contactNames.count > 3 else {
 | 
						|
                // Show up to three users names
 | 
						|
                let initialNames: [String] = Array(contactNames.prefix(upTo: (contactNames.count - 1)))
 | 
						|
                let lastName: String = contactNames[contactNames.count - 1]
 | 
						|
                
 | 
						|
                return [
 | 
						|
                    String(
 | 
						|
                        format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_1".localized(),
 | 
						|
                        initialNames.joined(separator: ", ")
 | 
						|
                    ),
 | 
						|
                    String(
 | 
						|
                        format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_2_SINGLE".localized(),
 | 
						|
                        lastName
 | 
						|
                    )
 | 
						|
                ]
 | 
						|
                .reversed(if: CurrentAppContext().isRTL)
 | 
						|
                .joined(separator: " ")
 | 
						|
            }
 | 
						|
            
 | 
						|
            // If we have exactly 4 users, show the first two names followed by 'and X others', for
 | 
						|
            // more than 4 users, show the first 3 names followed by 'and X others'
 | 
						|
            let numNamesToShow: Int = (contactNames.count == 4 ? 2 : 3)
 | 
						|
            let initialNames: [String] = Array(contactNames.prefix(upTo: numNamesToShow))
 | 
						|
            
 | 
						|
            return [
 | 
						|
                String(
 | 
						|
                    format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_1".localized(),
 | 
						|
                    initialNames.joined(separator: ", ")
 | 
						|
                ),
 | 
						|
                String(
 | 
						|
                    format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_3".localized(),
 | 
						|
                    (contactNames.count - numNamesToShow)
 | 
						|
                )
 | 
						|
            ]
 | 
						|
            .reversed(if: CurrentAppContext().isRTL)
 | 
						|
            .joined(separator: " ")
 | 
						|
        }()
 | 
						|
        let confirmationModal: ConfirmationModal = ConfirmationModal(
 | 
						|
            info: ConfirmationModal.Info(
 | 
						|
                title: confirmationTitle,
 | 
						|
                confirmTitle: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON".localized(),
 | 
						|
                confirmStyle: .danger,
 | 
						|
                cancelStyle: .alert_text
 | 
						|
            ) { _ in
 | 
						|
                // Unblock the contacts
 | 
						|
                Storage.shared.write { db in
 | 
						|
                    _ = try Contact
 | 
						|
                        .filter(ids: contactIds)
 | 
						|
                        .updateAll(db, Contact.Columns.isBlocked.set(to: false))
 | 
						|
                    
 | 
						|
                    // Force a config sync
 | 
						|
                    try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
 | 
						|
                }
 | 
						|
            }
 | 
						|
        )
 | 
						|
        self.present(confirmationModal, animated: true, completion: nil)
 | 
						|
    }
 | 
						|
}
 |