Переглянути джерело

Merge pull request #803 from deltachat/group_member_search

Group member search
cyBerta 5 роки тому
батько
коміт
1b9c381865

+ 12 - 0
deltachat-ios.xcodeproj/project.pbxproj

@@ -170,6 +170,9 @@
 		AEC67A1C241CE9E4007DDBE1 /* AppStateRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC67A1B241CE9E4007DDBE1 /* AppStateRestorer.swift */; };
 		AEC67A1E241FCFE0007DDBE1 /* ChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC67A1D241FCFE0007DDBE1 /* ChatListViewModel.swift */; };
 		AECEF03E244F2D55006C90DA /* QrPageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECEF03D244F2D55006C90DA /* QrPageController.swift */; };
+		AED423D3249F578B00B6B2BB /* NewGroupAddMembersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED423D2249F578B00B6B2BB /* NewGroupAddMembersViewController.swift */; };
+		AED423D5249F57C100B6B2BB /* AddGroupMembersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED423D4249F57C100B6B2BB /* AddGroupMembersViewController.swift */; };
+		AED423D7249F580700B6B2BB /* BlockedContactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED423D6249F580700B6B2BB /* BlockedContactsViewController.swift */; };
 		AED62BCE247687E6009E220D /* LocationStreamingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED62BCD247687E6009E220D /* LocationStreamingIndicator.swift */; };
 		AEE56D762253431E007DC082 /* AccountSetupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE56D752253431E007DC082 /* AccountSetupController.swift */; };
 		AEE56D80225504DB007DC082 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE56D7F225504DB007DC082 /* Extensions.swift */; };
@@ -459,6 +462,9 @@
 		AEC67A1B241CE9E4007DDBE1 /* AppStateRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateRestorer.swift; sourceTree = "<group>"; };
 		AEC67A1D241FCFE0007DDBE1 /* ChatListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewModel.swift; sourceTree = "<group>"; };
 		AECEF03D244F2D55006C90DA /* QrPageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrPageController.swift; sourceTree = "<group>"; };
+		AED423D2249F578B00B6B2BB /* NewGroupAddMembersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGroupAddMembersViewController.swift; sourceTree = "<group>"; };
+		AED423D4249F57C100B6B2BB /* AddGroupMembersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersViewController.swift; sourceTree = "<group>"; };
+		AED423D6249F580700B6B2BB /* BlockedContactsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewController.swift; sourceTree = "<group>"; };
 		AED62BCD247687E6009E220D /* LocationStreamingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationStreamingIndicator.swift; sourceTree = "<group>"; };
 		AEE56D752253431E007DC082 /* AccountSetupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupController.swift; sourceTree = "<group>"; tabWidth = 4; };
 		AEE56D7F225504DB007DC082 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
@@ -923,6 +929,9 @@
 				AE76E5ED242BF2EA003CF461 /* WelcomeViewController.swift */,
 				AE8F503424753DFE007FEE0B /* GalleryViewController.swift */,
 				30734325249A280B00BF9AD1 /* MediaQualityController.swift */,
+				AED423D2249F578B00B6B2BB /* NewGroupAddMembersViewController.swift */,
+				AED423D4249F57C100B6B2BB /* AddGroupMembersViewController.swift */,
+				AED423D6249F580700B6B2BB /* BlockedContactsViewController.swift */,
 			);
 			path = Controller;
 			sourceTree = "<group>";
@@ -1351,6 +1360,7 @@
 				AE406EF0240FF8FF005F7022 /* ProfileCell.swift in Sources */,
 				305961F02346125100C80F33 /* NSConstraintLayoutSet.swift in Sources */,
 				3059620E234614E700C80F33 /* DcContact+Extension.swift in Sources */,
+				AED423D7249F580700B6B2BB /* BlockedContactsViewController.swift in Sources */,
 				AED62BCE247687E6009E220D /* LocationStreamingIndicator.swift in Sources */,
 				305961F72346125100C80F33 /* MessageCollectionViewCell.swift in Sources */,
 				AE851AC9227C77CF00ED86F0 /* Media.swift in Sources */,
@@ -1374,6 +1384,7 @@
 				AE18F294228C602A0007B1BE /* SecuritySettingsController.swift in Sources */,
 				305961FD2346125100C80F33 /* TypingBubble.swift in Sources */,
 				305961D72346125100C80F33 /* MessageKit+Availability.swift in Sources */,
+				AED423D5249F57C100B6B2BB /* AddGroupMembersViewController.swift in Sources */,
 				3040F45E234DFBC000FA34D5 /* Audio.swift in Sources */,
 				305961FE2346125100C80F33 /* InsetLabel.swift in Sources */,
 				3095A351237DD1F700AB07F7 /* MediaPicker.swift in Sources */,
@@ -1493,6 +1504,7 @@
 				305961FC2346125100C80F33 /* MessageContainerView.swift in Sources */,
 				305961D42346125100C80F33 /* MessagesViewController.swift in Sources */,
 				785BE16821E247F1003BE98C /* MessageInfoViewController.swift in Sources */,
+				AED423D3249F578B00B6B2BB /* NewGroupAddMembersViewController.swift in Sources */,
 				AE851AC5227C755A00ED86F0 /* Protocols.swift in Sources */,
 				AE728F15229D5C390047565B /* PhotoPickerAlertAction.swift in Sources */,
 				305961F42346125100C80F33 /* TextMessageCell.swift in Sources */,

+ 176 - 0
deltachat-ios/Controller/AddGroupMembersViewController.swift

@@ -0,0 +1,176 @@
+import UIKit
+import DcCore
+
+class AddGroupMembersViewController: GroupMembersViewController {
+    private var chatId: Int?
+    private let sectionNewContact = 0
+    private let sectionMemberList = 1
+
+    private var contactAddedObserver: NSObjectProtocol?
+
+    private lazy var cancelButton: UIBarButtonItem = {
+        let button = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
+        return button
+    }()
+
+    lazy var doneButton: UIBarButtonItem = {
+        let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
+        return button
+    }()
+
+    private lazy var chat: DcChat? = {
+        if let chatId = chatId {
+            return dcContext.getChat(chatId: chatId)
+        }
+        return nil
+    }()
+
+    private lazy var chatMemberIds: [Int] = {
+        if let chat = chat {
+            return chat.contactIds
+        }
+        return []
+    }()
+
+    init(chatId: Int) {
+        super.init()
+        self.chatId = chatId
+        numberOfSections = 2
+
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK: - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        super.navigationItem.leftBarButtonItem = cancelButton
+        super.navigationItem.rightBarButtonItem = doneButton
+        title = String.localized("group_add_members")
+        super.contactIds = loadMemberCandidates()
+        // Do any additional setup after loading the view.
+        let nc = NotificationCenter.default
+        contactAddedObserver = nc.addObserver(
+            forName: dcNotificationContactChanged,
+            object: nil,
+            queue: nil
+        ) { [weak self] notification in
+            guard let self = self else { return }
+            if let ui = notification.userInfo {
+                if let contactId = ui["contact_id"] as? Int {
+                    if contactId == 0 {
+                        return
+                    }
+                    self.contactIds = self.loadMemberCandidates()
+                    if self.contactIds.contains(contactId) {
+                        self.selectedContactIds.insert(contactId)
+                        self.tableView.reloadData()
+                    }
+
+                }
+            }
+        }
+    }
+
+    override func viewWillDisappear(_: Bool) {
+        if !isMovingFromParent {
+            // a subview was added to the navigation stack, no action needed
+            return
+        }
+
+        let nc = NotificationCenter.default
+        if let observer = self.contactAddedObserver {
+            nc.removeObserver(observer)
+        }
+    }
+
+    override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
+        switch section {
+        case sectionNewContact:
+            return 1
+        case sectionMemberList:
+            return numberOfRowsForContactList
+        default:
+            return 0
+        }
+    }
+
+    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        switch indexPath.section {
+        case sectionNewContact:
+            return Constants.defaultCellHeight
+        case sectionMemberList:
+            return ContactCell.cellHeight
+        default:
+            return Constants.defaultCellHeight
+        }
+    }
+
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        switch indexPath.section {
+        case sectionNewContact:
+            return getNewContactCell()
+        case sectionMemberList:
+            return updateContactCell(for: indexPath)
+        default:
+            return UITableViewCell(style: .default, reuseIdentifier: nil)
+        }
+    }
+
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        switch indexPath.section {
+        case sectionNewContact:
+            tableView.deselectRow(at: indexPath, animated: true)
+            showNewContactController()
+        case sectionMemberList:
+            didSelectContactCell(at: indexPath)
+        default:
+            fatalError("unexpected section selected in GroupMembersViewController")
+        }
+    }
+
+    func loadMemberCandidates() -> [Int] {
+        var contactIds = dcContext.getContacts(flags: 0)
+        let memberSet = Set(chatMemberIds)
+        contactIds.removeAll(where: { memberSet.contains($0)})
+        return Array(contactIds)
+    }
+
+    @objc func cancelButtonPressed() {
+        navigationController?.popViewController(animated: true)
+    }
+
+    @objc func doneButtonPressed() {
+        guard let chatId = chatId else {
+            return
+        }
+        for contactId in selectedContactIds {
+           _ = dcContext.addContactToChat(chatId: chatId, contactId: contactId)
+        }
+        navigationController?.popViewController(animated: true)
+    }
+
+    func getNewContactCell() -> UITableViewCell {
+        let cell: UITableViewCell
+        if let c = tableView.dequeueReusableCell(withIdentifier: "actionCell") {
+            cell = c
+        } else {
+            cell = UITableViewCell(style: .default, reuseIdentifier: "actionCell")
+        }
+        cell.textLabel?.text = String.localized("menu_new_contact")
+        cell.textLabel?.textColor = view.tintColor
+        cell.textLabel?.textAlignment = .center
+
+        return cell
+    }
+
+    // MARK: - coordinator
+    private func showNewContactController() {
+        let newContactController = NewContactController(dcContext: dcContext)
+        newContactController.openChatOnSave = false
+        navigationController?.pushViewController(newContactController, animated: true)
+    }
+}

+ 72 - 0
deltachat-ios/Controller/BlockedContactsViewController.swift

@@ -0,0 +1,72 @@
+import UIKit
+import DcCore
+
+class BlockedContactsViewController: GroupMembersViewController, GroupMemberSelectionDelegate {
+
+    var emptyStateView: EmptyStateLabel = {
+        let view =  EmptyStateLabel()
+        view.text = String.localized("none_blocked_desktop")
+        return view
+    }()
+
+    override init() {
+        super.init()
+        enableCheckmarks = false
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK: - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        title = String.localized("pref_blocked_contacts")
+        contactIds = dcContext.getBlockedContacts()
+        selectedContactIds = Set(contactIds)
+        navigationItem.searchController = nil
+        groupMemberSelectionDelegate = self
+        setupSubviews()
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        updateEmtpyStateView()
+    }
+
+    // MARK: - setup
+    private func setupSubviews() {
+        view.addSubview(emptyStateView)
+        emptyStateView.translatesAutoresizingMaskIntoConstraints = false
+        emptyStateView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true
+        emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
+        emptyStateView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
+        emptyStateView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
+    }
+
+    // MARK: - actions + updates
+    func selected(contactId: Int, selected: Bool) {
+        if !selected {
+            let dcContact = DcContact(id: contactId)
+            let title = dcContact.displayName.isEmpty ? dcContact.email : dcContact.displayName
+            let alert = UIAlertController(title: title, message: String.localized("ask_unblock_contact"), preferredStyle: .safeActionSheet)
+            alert.addAction(UIAlertAction(title: String.localized("menu_unblock_contact"), style: .default, handler: { _ in
+                let contact = DcContact(id: contactId)
+                contact.unblock()
+                self.contactIds = self.dcContext.getBlockedContacts()
+                self.selectedContactIds = Set(self.contactIds)
+                self.tableView.reloadData()
+                self.updateEmtpyStateView()
+            }))
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
+                self.selectedContactIds = Set(self.contactIds)
+                self.tableView.reloadData()
+            }))
+           present(alert, animated: true, completion: nil)
+        }
+    }
+
+    private func updateEmtpyStateView() {
+        emptyStateView.isHidden = super.numberOfRowsForContactList > 0
+    }
+}

+ 46 - 371
deltachat-ios/Controller/GroupMembersViewController.swift

@@ -1,346 +1,56 @@
 import UIKit
 import DcCore
 
-class NewGroupAddMembersViewController: GroupMembersViewController {
-    var onMembersSelected: ((Set<Int>) -> Void)?
-    let isVerifiedGroup: Bool
-
-    private lazy var cancelButton: UIBarButtonItem = {
-        let button = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
-        return button
-    }()
-
-   lazy var doneButton: UIBarButtonItem = {
-       let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
-       return button
-   }()
-
-    init(preselected: Set<Int>, isVerified: Bool) {
-        isVerifiedGroup = isVerified
-        super.init()
-        selectedContactIds = preselected
-    }
-
-    required init?(coder _: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-
-    // MARK - lifecycle
-    override func viewDidLoad() {
-        super.viewDidLoad()
-        title = String.localized("group_add_members")
-        navigationItem.rightBarButtonItem = doneButton
-        navigationItem.leftBarButtonItem = cancelButton
-        contactIds = isVerifiedGroup ?
-            dcContext.getContacts(flags: DC_GCL_VERIFIED_ONLY) :
-            dcContext.getContacts(flags: 0)
-    }
-
-    override func viewWillAppear(_ animated: Bool) {
-        super.viewWillAppear(animated)
-    }
-
-    @objc func cancelButtonPressed() {
-        navigationController?.popViewController(animated: true)
-    }
-
-    @objc func doneButtonPressed() {
-        if let onMembersSelected = onMembersSelected {
-            selectedContactIds.insert(Int(DC_CONTACT_ID_SELF))
-            onMembersSelected(selectedContactIds)
-        } else {
-            navigationController?.popViewController(animated: true)
-        }
-    }
-
-}
-
-class AddGroupMembersViewController: GroupMembersViewController {
-    private var chatId: Int?
-    private let sectionNewContact = 0
-    private let sectionMemberList = 1
-
-    private var contactAddedObserver: NSObjectProtocol?
-
-    private lazy var cancelButton: UIBarButtonItem = {
-        let button = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
-        return button
-    }()
-
-    lazy var doneButton: UIBarButtonItem = {
-        let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
-        return button
-    }()
-
-    private lazy var chat: DcChat? = {
-        if let chatId = chatId {
-            return dcContext.getChat(chatId: chatId)
-        }
-        return nil
-    }()
-
-    private lazy var chatMemberIds: [Int] = {
-        if let chat = chat {
-            return chat.contactIds
-        }
-        return []
-    }()
-
-    init(chatId: Int) {
-        super.init()
-        self.chatId = chatId
-        numberOfSections = 2
-
-    }
-
-    required init?(coder _: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-
-    // MARK: - lifecycle
-    override func viewDidLoad() {
-        super.viewDidLoad()
-
-        super.navigationItem.leftBarButtonItem = cancelButton
-        super.navigationItem.rightBarButtonItem = doneButton
-        title = String.localized("group_add_members")
-        super.contactIds = loadMemberCandidates()
-        // Do any additional setup after loading the view.
-        let nc = NotificationCenter.default
-        contactAddedObserver = nc.addObserver(
-            forName: dcNotificationContactChanged,
-            object: nil,
-            queue: nil
-        ) { [weak self] notification in
-            guard let self = self else { return }
-            if let ui = notification.userInfo {
-                if let contactId = ui["contact_id"] as? Int {
-                    if contactId == 0 {
-                        return
-                    }
-                    self.contactIds = self.loadMemberCandidates()
-                    if self.contactIds.contains(contactId) {
-                        self.selectedContactIds.insert(contactId)
-                        self.tableView.reloadData()
-                    }
-
-                }
-            }
-        }
-    }
-
-    override func viewWillDisappear(_: Bool) {
-        if !isMovingFromParent {
-            // a subview was added to the navigation stack, no action needed
-            return
-        }
-
-        let nc = NotificationCenter.default
-        if let observer = self.contactAddedObserver {
-            nc.removeObserver(observer)
-        }
-    }
-
-    override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
-        switch section {
-        case sectionNewContact:
-            return 1
-        case sectionMemberList:
-            return getNumberOfRowsForContactList()
-        default:
-            return 0
-        }
-    }
-
-    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
-        switch indexPath.section {
-        case sectionNewContact:
-            return Constants.defaultCellHeight
-        case sectionMemberList:
-            return ContactCell.cellHeight
-        default:
-            return Constants.defaultCellHeight
-        }
-    }
-    
-    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        switch indexPath.section {
-        case sectionNewContact:
-            return getNewContactCell()
-        case sectionMemberList:
-            return getContactCell(cellForRowAt: indexPath)
-        default:
-            return UITableViewCell(style: .default, reuseIdentifier: nil)
-        }
-    }
-
-    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-        switch indexPath.section {
-        case sectionNewContact:
-            tableView.deselectRow(at: indexPath, animated: true)
-            showNewContactController()
-        case sectionMemberList:
-            didSelectContactCell(at: indexPath)
-        default:
-            fatalError("unexpected section selected in GroupMembersViewController")
-        }
-    }
-
-    func loadMemberCandidates() -> [Int] {
-        var contactIds = dcContext.getContacts(flags: 0)
-        let memberSet = Set(chatMemberIds)
-        contactIds.removeAll(where: { memberSet.contains($0)})
-        return Array(contactIds)
-    }
-
-    @objc func cancelButtonPressed() {
-        navigationController?.popViewController(animated: true)
-    }
-
-    @objc func doneButtonPressed() {
-        guard let chatId = chatId else {
-            return
-        }
-        for contactId in selectedContactIds {
-           _ = dcContext.addContactToChat(chatId: chatId, contactId: contactId)
-        }
-        navigationController?.popViewController(animated: true)
-    }
-
-    func getNewContactCell() -> UITableViewCell {
-        let cell: UITableViewCell
-        if let c = tableView.dequeueReusableCell(withIdentifier: "actionCell") {
-            cell = c
-        } else {
-            cell = UITableViewCell(style: .default, reuseIdentifier: "actionCell")
-        }
-        cell.textLabel?.text = String.localized("menu_new_contact")
-        cell.textLabel?.textColor = view.tintColor
-        cell.textLabel?.textAlignment = .center
-
-        return cell
-    }
-
-    // MARK: - coordinator
-    private func showNewContactController() {
-        let newContactController = NewContactController(dcContext: dcContext)
-        newContactController.openChatOnSave = false
-        navigationController?.pushViewController(newContactController, animated: true)
-    }
-}
-
-class BlockedContactsViewController: GroupMembersViewController, GroupMemberSelectionDelegate {
-
-    var emptyStateView: EmptyStateLabel = {
-        let view =  EmptyStateLabel()
-        view.text = String.localized("none_blocked_desktop")
-        return view
-    }()
-
-    override init() {
-        super.init()
-        enableCheckmarks = false
-    }
-
-    required init?(coder _: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-
-    // MARK: - lifecycle
-    override func viewDidLoad() {
-        super.viewDidLoad()
-        title = String.localized("pref_blocked_contacts")
-        contactIds = dcContext.getBlockedContacts()
-        selectedContactIds = Set(contactIds)
-        navigationItem.searchController = nil
-        groupMemberSelectionDelegate = self
-        setupSubviews()
-    }
-
-    override func viewWillAppear(_ animated: Bool) {
-        super.viewWillAppear(animated)
-        updateEmtpyStateView()
-    }
-
-    // MARK: - setup
-    private func setupSubviews() {
-        view.addSubview(emptyStateView)
-        emptyStateView.translatesAutoresizingMaskIntoConstraints = false
-        emptyStateView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true
-        emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
-        emptyStateView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
-        emptyStateView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
-    }
-
-    // MARK: - actions + updates
-    func selected(contactId: Int, selected: Bool) {
-        if !selected {
-            let dcContact = DcContact(id: contactId)
-            let title = dcContact.displayName.isEmpty ? dcContact.email : dcContact.displayName
-            let alert = UIAlertController(title: title, message: String.localized("ask_unblock_contact"), preferredStyle: .safeActionSheet)
-            alert.addAction(UIAlertAction(title: String.localized("menu_unblock_contact"), style: .default, handler: { _ in
-                let contact = DcContact(id: contactId)
-                contact.unblock()
-                self.contactIds = self.dcContext.getBlockedContacts()
-                self.selectedContactIds = Set(self.contactIds)
-                self.tableView.reloadData()
-                self.updateEmtpyStateView()
-            }))
-            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
-                self.selectedContactIds = Set(self.contactIds)
-                self.tableView.reloadData()
-            }))
-           present(alert, animated: true, completion: nil)
-        }
-    }
-
-    private func updateEmtpyStateView() {
-        emptyStateView.isHidden = super.getNumberOfRowsForContactList() > 0
-    }
-}
-
 protocol GroupMemberSelectionDelegate: class {
     func selected(contactId: Int, selected: Bool)
 }
 
-class GroupMembersViewController: UITableViewController, UISearchResultsUpdating {
-    let contactCellReuseIdentifier = "contactCell"
+// MARK: - GroupMembersViewController
+class GroupMembersViewController: UITableViewController {
     weak var groupMemberSelectionDelegate: GroupMemberSelectionDelegate?
     var enableCheckmarks = true
     var numberOfSections = 1
     let dcContext: DcContext
 
+    // MARK: - datasource
     var contactIds: [Int] = [] {
         didSet {
             tableView.reloadData()
         }
     }
+    // used when seachbar is active
+    private var filteredContactIds: [Int] = []
 
-    // contactWithSearchResults.indexesToHightLight empty by default
-    var contacts: [ContactWithSearchResults] {
-        return contactIds.map { ContactWithSearchResults(contact: DcContact(id: $0), indexesToHighlight: []) }
+    var numberOfRowsForContactList: Int {
+        return isFiltering ? filteredContactIds.count : contactIds.count
     }
 
-    // used when seachbar is active
-    var filteredContacts: [ContactWithSearchResults] = []
+    var selectedContactIds: Set<Int> = []
 
-    // searchBar active?
-    func isFiltering() -> Bool {
-        return searchController.isActive && !searchBarIsEmpty()
+    private func contactIdByRow(_ row: Int) -> Int {
+        return isFiltering ? filteredContactIds[row] : contactIds[row]
     }
 
-    private func searchBarIsEmpty() -> Bool {
-        return searchController.searchBar.text?.isEmpty ?? true
+    private func contactViewModelBy(row: Int) -> ContactCellViewModel {
+        let id = contactIdByRow(row)
+        return ContactCellViewModel.make(contactId: id, searchText: searchText, dcContext: dcContext)
     }
 
-    private func contactIdByRow(_ row: Int) -> Int {
-        return isFiltering() ? filteredContacts[row].contact.id : contactIds[row]
+    // MARK: - search
+    // searchBar active?
+    private var isFiltering: Bool {
+        return searchController.isActive && !searchBarIsEmpty
     }
 
-    private func contactSearchResultByRow(_ row: Int) -> ContactWithSearchResults {
-        return isFiltering() ? filteredContacts[row] : contacts[row]
+    private var searchBarIsEmpty: Bool {
+        return searchController.searchBar.text?.isEmpty ?? true
+    }
+
+    private var searchText: String? {
+        return searchController.searchBar.text
     }
 
+    // MARK: - subview configuration
     private lazy var searchController: UISearchController = {
         let searchController = UISearchController(searchResultsController: nil)
         searchController.searchResultsUpdater = self
@@ -356,8 +66,6 @@ class GroupMembersViewController: UITableViewController, UISearchResultsUpdating
         return label
     }()
 
-    var selectedContactIds: Set<Int> = []
-
     init() {
         self.dcContext = DcContext.shared
         super.init(style: .grouped)
@@ -370,15 +78,14 @@ class GroupMembersViewController: UITableViewController, UISearchResultsUpdating
 
     // MARK: - lifecycle
     override func viewDidLoad() {
-        tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
         navigationItem.searchController = searchController
-        if #available(iOS 11.0, *) {
-            navigationItem.hidesSearchBarWhenScrolling = false
-        }
+        navigationItem.hidesSearchBarWhenScrolling = false
+        configureTableView()
         definesPresentationContext = true
         setupSubviews()
     }
 
+    // MARK: - setup + configuration
     private func setupSubviews() {
         view.addSubview(emptySearchStateLabel)
         emptySearchStateLabel.translatesAutoresizingMaskIntoConstraints = false
@@ -388,13 +95,17 @@ class GroupMembersViewController: UITableViewController, UISearchResultsUpdating
         emptySearchStateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
     }
 
+    private func configureTableView() {
+        tableView.register(ContactCell.self, forCellReuseIdentifier: ContactCell.reuseIdentifier)
+    }
+
     // MARK: - UITableView datasource + delegate
     override func numberOfSections(in _: UITableView) -> Int {
         return numberOfSections
     }
 
     override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
-        return getNumberOfRowsForContactList()
+        return numberOfRowsForContactList
     }
 
     override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
@@ -402,30 +113,27 @@ class GroupMembersViewController: UITableViewController, UISearchResultsUpdating
     }
 
     override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        return getContactCell(cellForRowAt: indexPath)
+        return updateContactCell(for: indexPath)
     }
 
     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
         didSelectContactCell(at: indexPath)
     }
 
-    func getNumberOfRowsForContactList() -> Int {
-        return isFiltering() ? filteredContacts.count : contacts.count
-    }
-
-    func getContactCell(cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        guard let cell: ContactCell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell else {
-            fatalError("shouldn't happen")
+    func updateContactCell(for indexPath: IndexPath) -> UITableViewCell {
+        guard let cell: ContactCell = tableView.dequeueReusableCell(withIdentifier: ContactCell.reuseIdentifier, for: indexPath) as? ContactCell else {
+            safe_fatalError("unsupported cell type")
+            return UITableViewCell()
         }
 
         let row = indexPath.row
-        let contact: ContactWithSearchResults = contactSearchResultByRow(row)
-        updateContactCell(cell: cell, contactWithHighlight: contact)
+        let cellViewModel = contactViewModelBy(row: indexPath.row)
+        cell.updateCell(cellViewModel: cellViewModel)
         cell.accessoryType = selectedContactIds.contains(contactIdByRow(row)) && enableCheckmarks ? .checkmark : .none
-
         return cell
     }
 
+    // MARK: - actions
     func didSelectContactCell(at indexPath: IndexPath) {
         let row = indexPath.row
         if let cell = tableView.cellForRow(at: indexPath) {
@@ -446,25 +154,23 @@ class GroupMembersViewController: UITableViewController, UISearchResultsUpdating
             }
         }
     }
+}
 
+// MARK: - UISearchResultsUpdating
+extension GroupMembersViewController: UISearchResultsUpdating {
     func updateSearchResults(for searchController: UISearchController) {
         if let searchText = searchController.searchBar.text {
             filterContentForSearchText(searchText)
-        } 
+        }
     }
 
     private func filterContentForSearchText(_ searchText: String, scope _: String = String.localized("pref_show_emails_all")) {
-        let contactsWithHighlights: [ContactWithSearchResults] = contacts.map { contact in
-            let indexes = contact.contact.containsExact(searchText: searchText)
-            return ContactWithSearchResults(contact: contact.contact, indexesToHighlight: indexes)
-        }
-
-        filteredContacts = contactsWithHighlights.filter { !$0.indexesToHighlight.isEmpty }
+        filteredContactIds = dcContext.getContacts(flags: DC_GCL_ADD_SELF, queryString: searchText)
         tableView.reloadData()
         tableView.scrollToTop()
 
         // handle empty searchstate
-        if isFiltering() && getNumberOfRowsForContactList() == 0 {
+        if searchController.isActive && filteredContactIds.isEmpty {
             let text = String.localizedStringWithFormat(
                 String.localized("search_no_result_for_x"),
                 searchText
@@ -476,35 +182,4 @@ class GroupMembersViewController: UITableViewController, UISearchResultsUpdating
             emptySearchStateLabel.isHidden = true
         }
     }
-
-    private func updateContactCell(cell: ContactCell, contactWithHighlight: ContactWithSearchResults) {
-        let contact = contactWithHighlight.contact
-        let displayName = contact.displayName
-
-        let emailLabelFontSize = cell.subtitleLabel.font.pointSize
-        let nameLabelFontSize = cell.titleLabel.font.pointSize
-
-        cell.titleLabel.text = displayName
-        cell.subtitleLabel.text = contact.email
-        cell.avatar.setName(displayName)
-        cell.avatar.setColor(contact.color)
-        if let profileImage = contact.profileImage {
-            cell.avatar.setImage(profileImage)
-        }
-        cell.setVerified(isVerified: contact.isVerified)
-
-        if let emailHighlightedIndexes = contactWithHighlight.indexesToHighlight.filter({ $0.contactDetail == .EMAIL }).first {
-            // gets here when contact is a result of current search -> highlights relevant indexes
-            cell.subtitleLabel.attributedText = contact.email.boldAt(indexes: emailHighlightedIndexes.indexes, fontSize: emailLabelFontSize)
-        } else {
-            cell.subtitleLabel.attributedText = contact.email.boldAt(indexes: [], fontSize: emailLabelFontSize)
-        }
-
-        if let nameHighlightedIndexes = contactWithHighlight.indexesToHighlight.filter({ $0.contactDetail == .NAME }).first {
-            cell.titleLabel.attributedText = displayName.boldAt(indexes: nameHighlightedIndexes.indexes, fontSize: nameLabelFontSize)
-        } else {
-            cell.titleLabel.attributedText = displayName.boldAt(indexes: [], fontSize: nameLabelFontSize)
-        }
-    }
-
 }

+ 56 - 0
deltachat-ios/Controller/NewGroupAddMembersViewController.swift

@@ -0,0 +1,56 @@
+import UIKit
+import DcCore
+
+class NewGroupAddMembersViewController: GroupMembersViewController {
+    var onMembersSelected: ((Set<Int>) -> Void)?
+    let isVerifiedGroup: Bool
+
+    private lazy var cancelButton: UIBarButtonItem = {
+        let button = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
+        return button
+    }()
+
+   lazy var doneButton: UIBarButtonItem = {
+       let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
+       return button
+   }()
+
+    init(preselected: Set<Int>, isVerified: Bool) {
+        isVerifiedGroup = isVerified
+        super.init()
+        selectedContactIds = preselected
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        title = String.localized("group_add_members")
+        navigationItem.rightBarButtonItem = doneButton
+        navigationItem.leftBarButtonItem = cancelButton
+        contactIds = isVerifiedGroup ?
+            dcContext.getContacts(flags: DC_GCL_VERIFIED_ONLY) :
+            dcContext.getContacts(flags: 0)
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+    }
+
+    @objc func cancelButtonPressed() {
+        navigationController?.popViewController(animated: true)
+    }
+
+    @objc func doneButtonPressed() {
+        if let onMembersSelected = onMembersSelected {
+            selectedContactIds.insert(Int(DC_CONTACT_ID_SELF))
+            onMembersSelected(selectedContactIds)
+        } else {
+            navigationController?.popViewController(animated: true)
+        }
+    }
+
+}