Эх сурвалжийг харах

Merge pull request #577 from deltachat/searchBarChatList#3

Search bar chat list#3
bjoern 5 жил өмнө
parent
commit
0c4af5ddc3

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

@@ -148,6 +148,7 @@
 		AEACE2E31FB32B5C00DCDD78 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2E21FB32B5C00DCDD78 /* Constants.swift */; };
 		AEACE2E51FB32E1900DCDD78 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2E41FB32E1900DCDD78 /* Utils.swift */; };
 		AEC67A1C241CE9E4007DDBE1 /* TabBarRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC67A1B241CE9E4007DDBE1 /* TabBarRestorer.swift */; };
+		AEC67A1E241FCFE0007DDBE1 /* ChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC67A1D241FCFE0007DDBE1 /* ChatListViewModel.swift */; };
 		AEE56D762253431E007DC082 /* AccountSetupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE56D752253431E007DC082 /* AccountSetupController.swift */; };
 		AEE56D7D2253ADB4007DC082 /* HudHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE56D7C2253ADB4007DC082 /* HudHandler.swift */; };
 		AEE56D80225504DB007DC082 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE56D7F225504DB007DC082 /* Extensions.swift */; };
@@ -381,6 +382,7 @@
 		AEACE2E21FB32B5C00DCDD78 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
 		AEACE2E41FB32E1900DCDD78 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
 		AEC67A1B241CE9E4007DDBE1 /* TabBarRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRestorer.swift; sourceTree = "<group>"; };
+		AEC67A1D241FCFE0007DDBE1 /* ChatListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewModel.swift; sourceTree = "<group>"; };
 		AEE56D752253431E007DC082 /* AccountSetupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupController.swift; sourceTree = "<group>"; tabWidth = 4; };
 		AEE56D7C2253ADB4007DC082 /* HudHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HudHandler.swift; sourceTree = "<group>"; };
 		AEE56D7F225504DB007DC082 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
@@ -712,6 +714,7 @@
 			children = (
 				AE77838C23E32ED20093EABD /* ContactDetailViewModel.swift */,
 				AE77838E23E4276D0093EABD /* ContactCellViewModel.swift */,
+				AEC67A1D241FCFE0007DDBE1 /* ChatListViewModel.swift */,
 			);
 			path = ViewModel;
 			sourceTree = "<group>";
@@ -1207,6 +1210,7 @@
 				302B84C72396770B001C261F /* RelayHelper.swift in Sources */,
 				305961CF2346125100C80F33 /* UIColor+Extensions.swift in Sources */,
 				AEACE2E51FB32E1900DCDD78 /* Utils.swift in Sources */,
+				AEC67A1E241FCFE0007DDBE1 /* ChatListViewModel.swift in Sources */,
 				300C509D234B551900F8AE22 /* TextMediaMessageCell.swift in Sources */,
 				305961E92346125100C80F33 /* MessageKind.swift in Sources */,
 				305962022346125100C80F33 /* BubbleCircle.swift in Sources */,

+ 1 - 1
deltachat-ios/AppDelegate.swift

@@ -82,9 +82,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         // setup deltachat core context
         //       - second param remains nil (user data for more than one mailbox)
         open()
+        RelayHelper.setup(dcContext)
         appCoordinator = AppCoordinator(window: window, dcContext: dcContext)
         appCoordinator.start()
-        RelayHelper.setup(dcContext)
         locationManager = LocationManager(context: dcContext)
         UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
         start()

+ 222 - 217
deltachat-ios/Controller/ChatListController.swift

@@ -1,74 +1,110 @@
 import UIKit
 
-class ChatListController: UIViewController {
+class ChatListController: UITableViewController {
     weak var coordinator: ChatListCoordinator?
+    let viewModel: ChatListViewModelProtocol
 
-    private var dcContext: DcContext
-    private var chatList: DcChatlist?
-    private let showArchive: Bool
-
-    private lazy var chatTable: UITableView = {
-        let chatTable = UITableView()
-        chatTable.dataSource = self
-        chatTable.delegate = self
-        chatTable.rowHeight = 80
-        return chatTable
-    }()
+    private let chatCellReuseIdentifier = "chat_cell"
+    private let deadDropCellReuseIdentifier = "deaddrop_cell"
+    private let contactCellReuseIdentifier = "contact_cell"
 
     private var msgChangedObserver: Any?
     private var incomingMsgObserver: Any?
     private var viewChatObserver: Any?
     private var deleteChatObserver: Any?
 
-    private var newButton: UIBarButtonItem!
+    private lazy var searchController: UISearchController = {
+        let searchController = UISearchController(searchResultsController: nil)
+        searchController.searchResultsUpdater = viewModel
+        searchController.obscuresBackgroundDuringPresentation = false
+        searchController.searchBar.placeholder = String.localized("search")
+        searchController.searchBar.delegate = self
+        return searchController
+    }()
 
-    lazy var cancelButton: UIBarButtonItem = {
+    private lazy var newButton: UIBarButtonItem = {
+        let button = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.compose, target: self, action: #selector(didPressNewChat))
+        button.tintColor = DcColors.primary
+        return button
+    }()
+
+    private lazy var cancelButton: UIBarButtonItem = {
         let button = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonPressed))
         return button
     }()
 
-    init(dcContext: DcContext, showArchive: Bool) {
-        self.dcContext = dcContext
-        self.showArchive = showArchive
-        dcContext.updateDeviceChats()
-        super.init(nibName: nil, bundle: nil)
+    func getArchiveCell(title: String) -> UITableViewCell {
+        let cell = UITableViewCell()
+        cell.textLabel?.textColor = .systemBlue
+        cell.textLabel?.text = title
+        cell.textLabel?.textAlignment = .center
+        return cell
+    }
+
+    init(viewModel: ChatListViewModelProtocol) {
+        self.viewModel = viewModel
+        if viewModel.isArchive {
+            super.init(nibName: nil, bundle: nil)
+        } else {
+            super.init(style: .grouped)
+        }
+        viewModel.onChatListUpdate = handleChatListUpdate // register listener
     }
 
     required init?(coder _: NSCoder) {
         fatalError("init(coder:) has not been implemented")
     }
 
+    // MARK: - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        navigationItem.rightBarButtonItem = newButton
+        if !viewModel.isArchive {
+            navigationItem.searchController = searchController
+        }
+        configureTableView()
+    }
+
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
-        getChatList()
+
+        updateTitle()
+        viewModel.refreshData()
 
         if RelayHelper.sharedInstance.isForwarding() {
-            chatTable.scrollToTop()
+            tableView.scrollToTop()
         }
 
-        updateTitle()
-
         let nc = NotificationCenter.default
-          msgChangedObserver = nc.addObserver(forName: dcNotificationChanged,
-                                              object: nil, queue: nil) { _ in
-              self.getChatList()
-          }
-          incomingMsgObserver = nc.addObserver(forName: dcNotificationIncoming,
-                                               object: nil, queue: nil) { _ in
-              self.getChatList()
-          }
-
-          viewChatObserver = nc.addObserver(forName: dcNotificationViewChat, object: nil, queue: nil) { notification in
-              if let chatId = notification.userInfo?["chat_id"] as? Int {
-                  self.coordinator?.showChat(chatId: chatId)
-              }
-          }
-
-          deleteChatObserver = nc.addObserver(forName: dcNotificationChatDeletedInChatDetail, object: nil, queue: nil) { notification in
-              if let chatId = notification.userInfo?["chat_id"] as? Int {
-                  self.deleteChat(chatId: chatId, animated: true)
-              }
-          }
+        msgChangedObserver = nc.addObserver(
+            forName: dcNotificationChanged,
+            object: nil,
+            queue: nil) { _ in
+                self.viewModel.refreshData()
+
+        }
+        incomingMsgObserver = nc.addObserver(
+            forName: dcNotificationIncoming,
+            object: nil,
+            queue: nil) { _ in
+                self.viewModel.refreshData()
+        }
+        viewChatObserver = nc.addObserver(
+            forName: dcNotificationViewChat,
+            object: nil,
+            queue: nil) { notification in
+                if let chatId = notification.userInfo?["chat_id"] as? Int {
+                    self.coordinator?.showChat(chatId: chatId)
+                }
+        }
+        deleteChatObserver = nc.addObserver(
+            forName: dcNotificationChatDeletedInChatDetail,
+            object: nil,
+            queue: nil) { notification in
+                if let chatId = notification.userInfo?["chat_id"] as? Int {
+                    self.deleteChat(chatId: chatId, animated: true)
+                }
+        }
     }
 
     override func viewDidDisappear(_ animated: Bool) {
@@ -90,25 +126,15 @@ class ChatListController: UIViewController {
         }
     }
 
-    override func viewDidLoad() {
-        super.viewDidLoad()
-
-        newButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.compose, target: self, action: #selector(didPressNewChat))
-        newButton.tintColor = DcColors.primary
-        navigationItem.rightBarButtonItem = newButton
-
-        setupChatTable()
-    }
-
-    private func setupChatTable() {
-        view.addSubview(chatTable)
-        chatTable.translatesAutoresizingMaskIntoConstraints = false
-        chatTable.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
-        chatTable.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
-        chatTable.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
-        chatTable.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
+    // MARK: - configuration
+    private func configureTableView() {
+        tableView.register(ContactCell.self, forCellReuseIdentifier: chatCellReuseIdentifier)
+        tableView.register(ContactCell.self, forCellReuseIdentifier: deadDropCellReuseIdentifier)
+        tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
+        tableView.rowHeight = 80
     }
 
+    // MARK: - actions
     @objc func didPressNewChat() {
         coordinator?.showNewChatController()
     }
@@ -116,186 +142,131 @@ class ChatListController: UIViewController {
     @objc func cancelButtonPressed() {
         // cancel forwarding
         RelayHelper.sharedInstance.cancel()
-        getChatList()
+        viewModel.refreshData()
         updateTitle()
     }
 
-    private func getChatList() {
-        var gclFlags: Int32 = 0
-        if showArchive {
-            gclFlags |= DC_GCL_ARCHIVED_ONLY
-        } else if RelayHelper.sharedInstance.isForwarding() {
-            gclFlags |= DC_GCL_FOR_FORWARDING
-        }
-        chatList = dcContext.getChatlist(flags: gclFlags, queryString: nil, queryId: 0)
-        chatTable.reloadData()
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return viewModel.numberOfSections
     }
 
-    private func updateTitle() {
-        if RelayHelper.sharedInstance.isForwarding() {
-            title = String.localized("forward_to")
-            if !showArchive {
-                navigationItem.setLeftBarButton(cancelButton, animated: true)
-            }
-        } else {
-            title = showArchive ? String.localized("chat_archived_chats_title") :
-                String.localized("pref_chats")
-            navigationItem.setLeftBarButton(nil, animated: true)
-        }
+    override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return viewModel.numberOfRowsIn(section: section)
     }
-}
 
-extension ChatListController: UITableViewDataSource, UITableViewDelegate {
-    func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
-        guard let chatList = self.chatList else {
-            fatalError("chatList was nil in data source")
-        }
 
-        return chatList.length
-    }
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 
-    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-        let row = indexPath.row
-        guard let chatList = self.chatList else {
-            fatalError("chatList was nil in data source")
-        }
-
-        let chatId = chatList.getChatId(index: row)
-        let chat = DcChat(id: chatId)
-        if chatId == DC_CHAT_ID_ARCHIVED_LINK {
-            return getArchiveCell(tableView, title: chat.name)
-        }
+        let cellData = viewModel.cellDataFor(section: indexPath.section, row: indexPath.row)
 
-        let cell: ContactCell
-        if chatId == DC_CHAT_ID_DEADDROP {
-            cell = getDeaddropCell(tableView)
-        } else if let c = tableView.dequeueReusableCell(withIdentifier: "ChatCell") as? ContactCell {
-            cell = c
-        } else {
-            cell = ContactCell(style: .default, reuseIdentifier: "ChatCell")
+        switch cellData.type {
+        case .deaddrop(let deaddropData):
+            guard let deaddropCell = tableView.dequeueReusableCell(withIdentifier: deadDropCellReuseIdentifier, for: indexPath) as? ContactCell else {
+                break
+            }
+            deaddropCell.updateCell(cellViewModel: cellData)
+            return deaddropCell
+        case .chat(let chatData):
+            let chatId = chatData.chatId
+            if chatId == DC_CHAT_ID_ARCHIVED_LINK {
+                return getArchiveCell(title: DcChat(id: chatId).name)
+            } else if let chatCell = tableView.dequeueReusableCell(withIdentifier: chatCellReuseIdentifier, for: indexPath) as? ContactCell {
+                // default chatCell
+                chatCell.updateCell(cellViewModel: cellData)
+                return chatCell
+            }
+        case .contact:
+            safe_assert(viewModel.searchActive)
+            if let contactCell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell {
+                contactCell.updateCell(cellViewModel: cellData)
+                return contactCell
+            }
         }
+        safe_fatalError("Could not find/dequeue or recycle UITableViewCell.")
+        return UITableViewCell()
+    }
 
-        let summary = chatList.getSummary(index: row)
-        let unreadMessages = dcContext.getUnreadMessages(chatId: chatId)
-
-        cell.titleLabel.attributedText = (unreadMessages > 0) ?
-            NSAttributedString(string: chat.name, attributes: [ .font: UIFont.systemFont(ofSize: 16, weight: .bold) ]) :
-            NSAttributedString(string: chat.name, attributes: [ .font: UIFont.systemFont(ofSize: 16, weight: .medium) ])
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        return viewModel.titleForHeaderIn(section: section)
+    }
 
-        if chatId == DC_CHAT_ID_DEADDROP {
-            let contact = DcContact(id: DcMsg(id: chatList.getMsgId(index: row)).fromContactId)
-            if let img = contact.profileImage {
-                cell.resetBackupImage()
-                cell.setImage(img)
+    override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
+        let cellData = viewModel.cellDataFor(section: indexPath.section, row: indexPath.row)
+        switch cellData.type {
+        case .deaddrop(let deaddropData):
+            safe_assert(deaddropData.chatId == DC_CHAT_ID_DEADDROP)
+            showDeaddropRequestAlert(msgId: deaddropData.msgId)
+        case .chat(let chatData):
+            let chatId = chatData.chatId
+            if chatId == DC_CHAT_ID_ARCHIVED_LINK {
+                coordinator?.showArchive()
             } else {
-                cell.setBackupImage(name: contact.name, color: contact.color)
+                coordinator?.showChat(chatId: chatId)
             }
-        } else {
-            if let img = chat.profileImage {
-                cell.resetBackupImage()
-                cell.setImage(img)
+        case .contact(let contactData):
+            let contactId = contactData.contactId
+            if let chatId = contactData.chatId {
+                coordinator?.showChat(chatId: chatId)
             } else {
-                cell.setBackupImage(name: chat.name, color: chat.color)
+                self.askToChatWith(contactId: contactId)
             }
-
-            if chat.visibility == DC_CHAT_VISIBILITY_PINNED {
-                cell.backgroundColor = DcColors.deaddropBackground
-                cell.contentView.backgroundColor = DcColors.deaddropBackground
-            } else {
-                cell.backgroundColor = DcColors.contactCellBackgroundColor
-                cell.contentView.backgroundColor = DcColors.contactCellBackgroundColor
-            }
-        }
-
-        cell.setVerified(isVerified: chat.isVerified)
-
-        let result1 = summary.text1 ?? ""
-        let result2 = summary.text2 ?? ""
-        let result: String
-        if !result1.isEmpty, !result2.isEmpty {
-            result = "\(result1): \(result2)"
-        } else {
-            result = "\(result1)\(result2)"
         }
-
-        cell.subtitleLabel.text = result
-        cell.setTimeLabel(summary.timestamp)
-        cell.setStatusIndicators(unreadCount: unreadMessages, status: summary.state, visibility: chat.visibility, isLocationStreaming: chat.isSendingLocations)
-        return cell
     }
 
-    func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
-        let row = indexPath.row
-        guard let chatList = chatList else { return }
-        let chatId = chatList.getChatId(index: row)
-        if chatId == DC_CHAT_ID_DEADDROP {
-            let msgId = chatList.getMsgId(index: row)
-            let dcMsg = DcMsg(id: msgId)
-            let dcContact = DcContact(id: dcMsg.fromContactId)
-            let title = String.localizedStringWithFormat(String.localized("ask_start_chat_with"), dcContact.nameNAddr)
-            let alert = UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
-            alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
-                let chat = dcMsg.createChat()
-                self.coordinator?.showChat(chatId: chat.id)
-            }))
-            alert.addAction(UIAlertAction(title: String.localized("not_now"), style: .default, handler: { _ in
-                dcContact.marknoticed()
-            }))
-            alert.addAction(UIAlertAction(title: String.localized("menu_block_contact"), style: .destructive, handler: { _ in
-                dcContact.block()
-            }))
-            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel))
-            present(alert, animated: true, completion: nil)
-        } else if chatId == DC_CHAT_ID_ARCHIVED_LINK {
-            coordinator?.showArchive()
-        } else {
-            coordinator?.showChat(chatId: chatId)
+    override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
+
+        if viewModel.searchActive {
+            // no swipe actions during search
+            return []
         }
-    }
 
-    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
-        let row = indexPath.row
-        guard let chatList = chatList else {
+        guard let chatId = viewModel.chatIdFor(section: indexPath.section, row: indexPath.row) else {
             return []
         }
 
-        let chatId = chatList.getChatId(index: row)
         if chatId==DC_CHAT_ID_ARCHIVED_LINK || chatId==DC_CHAT_ID_DEADDROP {
             return []
             // returning nil may result in a default delete action,
             // see https://forums.developer.apple.com/thread/115030
         }
+        let archiveActionTitle: String = String.localized(viewModel.isArchive ? "unarchive" : "archive")
 
-        let archive = UITableViewRowAction(style: .destructive, title: String.localized(showArchive ? "unarchive" : "archive")) { [unowned self] _, _ in
-            self.dcContext.archiveChat(chatId: chatId, archive: !self.showArchive)
+        let archiveAction = UITableViewRowAction(style: .destructive, title: archiveActionTitle) { [unowned self] _, _ in
+            self.viewModel.archiveChatToggle(chatId: chatId)
         }
-        archive.backgroundColor = UIColor.lightGray
+        archiveAction.backgroundColor = UIColor.lightGray
 
-        let chat = dcContext.getChat(chatId: chatId)
+        let chat = DcChat(id: chatId)
         let pinned = chat.visibility==DC_CHAT_VISIBILITY_PINNED
-        let pin = UITableViewRowAction(style: .destructive, title: String.localized(pinned ? "unpin" : "pin")) { [unowned self] _, _ in
-            self.dcContext.setChatVisibility(chatId: chatId, visibility: pinned ? DC_CHAT_VISIBILITY_NORMAL : DC_CHAT_VISIBILITY_PINNED)
+        let pinAction = UITableViewRowAction(style: .destructive, title: String.localized(pinned ? "unpin" : "pin")) { [unowned self] _, _ in
+            self.viewModel.pinChatToggle(chatId: chat.id)
         }
-        pin.backgroundColor = UIColor.systemGreen
+        pinAction.backgroundColor = UIColor.systemGreen
 
-        let delete = UITableViewRowAction(style: .normal, title: String.localized("delete")) { [unowned self] _, _ in
+        let deleteAction = UITableViewRowAction(style: .normal, title: String.localized("delete")) { [unowned self] _, _ in
             self.showDeleteChatConfirmationAlert(chatId: chatId)
         }
-        delete.backgroundColor = UIColor.systemRed
+        deleteAction.backgroundColor = UIColor.systemRed
 
-        return [archive, pin, delete]
+        return [archiveAction, pinAction, deleteAction]
     }
 
-    func getDeaddropCell(_ tableView: UITableView) -> ContactCell {
-        let deaddropCell: ContactCell
-        if let cell = tableView.dequeueReusableCell(withIdentifier: "DeaddropCell") as? ContactCell {
-            deaddropCell = cell
+    // MARK: updates
+    private func updateTitle() {
+        if RelayHelper.sharedInstance.isForwarding() {
+            title = String.localized("forward_to")
+            if !viewModel.isArchive {
+                navigationItem.setLeftBarButton(cancelButton, animated: true)
+            }
         } else {
-            deaddropCell = ContactCell(style: .default, reuseIdentifier: "DeaddropCell")
+            title = viewModel.isArchive ? String.localized("chat_archived_chats_title") :
+                String.localized("pref_chats")
+            navigationItem.setLeftBarButton(nil, animated: true)
         }
-        deaddropCell.backgroundColor = DcColors.deaddropBackground
-        deaddropCell.contentView.backgroundColor = DcColors.deaddropBackground
-        return deaddropCell
+    }
+
+    func handleChatListUpdate() {
+        tableView.reloadData()
     }
 
     func getArchiveCell(_ tableView: UITableView, title: String) -> UITableViewCell {
@@ -311,6 +282,7 @@ extension ChatListController: UITableViewDataSource, UITableViewDelegate {
         return archiveCell
     }
 
+    // MARK: - alerts
     private func showDeleteChatConfirmationAlert(chatId: Int) {
         let alert = UIAlertController(
             title: nil,
@@ -324,32 +296,65 @@ extension ChatListController: UITableViewDataSource, UITableViewDelegate {
         self.present(alert, animated: true, completion: nil)
     }
 
-    private func deleteChat(chatId: Int, animated: Bool) {
+    private func showDeaddropRequestAlert(msgId: Int) {
+        let dcMsg = DcMsg(id: msgId)
+        let dcContact = DcContact(id: dcMsg.fromContactId)
+        let title = String.localizedStringWithFormat(String.localized("ask_start_chat_with"), dcContact.nameNAddr)
+        let alert = UIAlertController(title: title, message: nil, preferredStyle: .safeActionSheet)
+        alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
+            let chat = dcMsg.createChat()
+            self.coordinator?.showChat(chatId: chat.id)
+        }))
+        alert.addAction(UIAlertAction(title: String.localized("not_now"), style: .default, handler: { _ in
+            dcContact.marknoticed()
+        }))
+        alert.addAction(UIAlertAction(title: String.localized("menu_block_contact"), style: .destructive, handler: { _ in
+            dcContact.block()
+        }))
+        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel))
+        present(alert, animated: true, completion: nil)
+    }
 
-        if !animated {
-            dcContext.deleteChat(chatId: chatId)
-            self.getChatList()
-            return
-        }
+    private func askToChatWith(contactId: Int) {
+        let dcContact = DcContact(id: contactId)
+        let alert = UIAlertController(
+            title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), dcContact.nameNAddr),
+            message: nil,
+            preferredStyle: .safeActionSheet)
+        alert.addAction(UIAlertAction(
+            title: String.localized("start_chat"),
+            style: .default,
+            handler: { _ in
+                self.coordinator?.showNewChat(contactId: contactId)
+        }))
+        alert.addAction(UIAlertAction(
+            title: String.localized("cancel"),
+            style: .cancel,
+            handler: { _ in
+        }))
+        self.present(alert, animated: true, completion: nil)
+    }
 
-        guard let chatList = chatList else {
+    private func deleteChat(chatId: Int, animated: Bool) {
+        if !animated {
+            _ = viewModel.deleteChat(chatId: chatId)
+            viewModel.refreshData()
             return
         }
 
-        // find index of chatId
-        let indexToDelete = Array(0..<chatList.length).filter { chatList.getChatId(index: $0) == chatId }.first
-
-        guard let row = indexToDelete else {
-            return
-        }
+        let row = viewModel.deleteChat(chatId: chatId)
+        tableView.deleteRows(at: [IndexPath(row: row, section: 0)], with: .fade)
+    }
+}
 
-        var gclFlags: Int32 = 0
-        if showArchive {
-            gclFlags |= DC_GCL_ARCHIVED_ONLY
-        }
+// MARK: - uisearchbardelegate
+extension ChatListController: UISearchBarDelegate {
+    func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
+        viewModel.beginSearch()
+        return true
+    }
 
-        dcContext.deleteChat(chatId: chatId)
-        self.chatList = dcContext.getChatlist(flags: gclFlags, queryString: nil, queryId: 0)
-        chatTable.deleteRows(at: [IndexPath(row: row, section: 0)], with: .fade)
+    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
+        viewModel.endSearch()
     }
 }

+ 5 - 1
deltachat-ios/Controller/GroupChatDetailViewController.swift

@@ -253,7 +253,11 @@ extension GroupChatDetailViewController: UITableViewDelegate, UITableViewDataSou
                 safe_fatalError("could not dequeue contactCell cell")
                 break
             }
-            let cellData = ContactCellData(contactId: getGroupMemberIdFor(row))
+            let contactId: Int = getGroupMemberIdFor(row)
+            let cellData = ContactCellData(
+                contactId: contactId,
+                chatId: context.getChatIdByContactId(contactId)
+            )
             let cellViewModel = ContactCellViewModel(contactData: cellData)
             contactCell.updateCell(cellViewModel: cellViewModel)
             return contactCell

+ 13 - 4
deltachat-ios/Coordinator/AppCoordinator.swift

@@ -46,7 +46,8 @@ class AppCoordinator: NSObject, Coordinator {
     }()
 
     private lazy var chatListController: UIViewController = {
-        let controller = ChatListController(dcContext: dcContext, showArchive: false)
+        let viewModel = ChatListViewModel(dcContext: dcContext, isArchive: false)
+        let controller = ChatListController(viewModel: viewModel)
         let nav = UINavigationController(rootViewController: controller)
         let settingsImage = UIImage(named: "ic_chat")
         nav.tabBarItem = UITabBarItem(title: String.localized("pref_chats"), image: settingsImage, tag: chatsTab)
@@ -190,12 +191,18 @@ class ChatListCoordinator: Coordinator {
     }
 
     func showArchive() {
-        let controller = ChatListController(dcContext: dcContext, showArchive: true)
+        let viewModel = ChatListViewModel(dcContext: dcContext, isArchive: true)
+        let controller = ChatListController(viewModel: viewModel)
         let coordinator = ChatListCoordinator(dcContext: dcContext, navigationController: navigationController)
         childCoordinators.append(coordinator)
         controller.coordinator = coordinator
         navigationController.pushViewController(controller, animated: true)
     }
+
+    func showNewChat(contactId: Int) {
+        let chatId = dc_create_chat_by_contact_id(mailboxPointer, UInt32(contactId))
+        showChat(chatId: Int(chatId))
+    }
 }
 
 class SettingsCoordinator: Coordinator {
@@ -437,7 +444,8 @@ class GroupChatDetailCoordinator: Coordinator {
 
         func showArchive() {
             self.navigationController.popToRootViewController(animated: false) // in main ChatList now
-            let controller = ChatListController(dcContext: dcContext, showArchive: true)
+            let viewModel = ChatListViewModel(dcContext: dcContext, isArchive: true)
+            let controller = ChatListController(viewModel: viewModel)
             let coordinator = ChatListCoordinator(dcContext: dcContext, navigationController: navigationController)
             childCoordinators.append(coordinator)
             controller.coordinator = coordinator
@@ -705,7 +713,8 @@ class ContactDetailCoordinator: Coordinator, ContactDetailCoordinatorProtocol {
 
         func showArchive() {
             self.navigationController.popToRootViewController(animated: false) // in main ChatList now
-            let controller = ChatListController(dcContext: dcContext, showArchive: true)
+            let viewModel = ChatListViewModel(dcContext: dcContext, isArchive: true)
+            let controller = ChatListController(viewModel: viewModel)
             let coordinator = ChatListCoordinator(dcContext: dcContext, navigationController: navigationController)
             childCoordinators.append(coordinator)
             controller.coordinator = coordinator

+ 29 - 2
deltachat-ios/DC/Wrapper.swift

@@ -26,8 +26,8 @@ class DcContext {
         return dc_delete_contact(self.contextPointer, UInt32(contactId)) == 1
     }
 
-    func getContacts(flags: Int32) -> [Int] {
-        let cContacts = dc_get_contacts(self.contextPointer, UInt32(flags), nil)
+    func getContacts(flags: Int32, queryString: String? = nil) -> [Int] {
+        let cContacts = dc_get_contacts(self.contextPointer, UInt32(flags), queryString)
         return Utils.copyAndFreeArray(inputArray: cContacts)
     }
 
@@ -39,6 +39,15 @@ class DcContext {
         return DcChat(id: chatId)
     }
 
+    func getChatIdByContactId(_ contactId: Int) -> Int? {
+        let chatId = dc_get_chat_id_by_contact_id(self.contextPointer, UInt32(contactId))
+        if chatId == 0 {
+            return nil
+        } else {
+            return Int(chatId)
+        }
+    }
+
     func getChatlist(flags: Int32, queryString: String?, queryId: Int) -> DcChatlist {
         let chatlistPointer = dc_get_chatlist(contextPointer, flags, queryString, UInt32(queryId))
         let chatlist = DcChatlist(chatListPointer: chatlistPointer)
@@ -237,6 +246,14 @@ class DcContext {
     func setLocation(latitude: Double, longitude: Double, accuracy: Double) {
         dc_set_location(contextPointer, latitude, longitude, accuracy)
     }
+
+    func searchMessages(chatId: Int = 0, searchText: String) -> [Int] {
+        guard let arrayPointer = dc_search_msgs(contextPointer, UInt32(chatId), searchText) else {
+            return []
+        }
+        let messageIds = Utils.copyAndFreeArray(inputArray: arrayPointer)
+        return messageIds
+    }
 }
 
 class DcConfig {
@@ -888,6 +905,16 @@ class DcMsg: MessageType {
         return swiftString
     }
 
+    func summary(chat: DcChat) -> DcLot {
+        guard let chatPointer = chat.chatPointer else {
+            fatalError()
+        }
+        guard let dcLotPointer = dc_msg_get_summary(messagePointer, chatPointer) else {
+            fatalError()
+        }
+        return DcLot(dcLotPointer)
+    }
+
     func showPadlock() -> Bool {
         return dc_msg_get_showpadlock(messagePointer) == 1
     }

+ 16 - 0
deltachat-ios/Extensions/String+Extension.swift

@@ -13,6 +13,22 @@ extension String {
         return !trimmingCharacters(in: [" "]).isEmpty
     }
 
+    func containsExact(subSequence: String) -> [Int] {
+        if subSequence.count > count {
+            return []
+        }
+
+        if let range = range(of: subSequence, options: .caseInsensitive) {
+            let index: Int = distance(from: startIndex, to: range.lowerBound)
+            var indexes: [Int] = []
+            for i in index..<(index + subSequence.count) {
+                indexes.append(i)
+            }
+            return indexes
+        }
+        return []
+    }
+
     // O(n) - returns indexes of subsequences -> can be used to highlight subsequence within string
     func contains(subSequence: String) -> [Int] {
         if subSequence.count > count {

+ 28 - 16
deltachat-ios/View/ContactCell.swift

@@ -7,10 +7,9 @@ protocol ContactCellDelegate: class {
 class ContactCell: UITableViewCell {
 
     static let reuseIdentifier = "contact_cell_reuse_identifier"
+    static let cellHeight: CGFloat = 74.5
 
-    public static let cellHeight: CGFloat = 74.5
     weak var delegate: ContactCellDelegate?
-    var rowIndex = -1 // TODO: is this still needed?
     private let badgeSize: CGFloat = 54
     private let imgSize: CGFloat = 20
 
@@ -32,8 +31,6 @@ class ContactCell: UITableViewCell {
         let badge = InitialsBadge(size: badgeSize)
         badge.setColor(UIColor.lightGray)
         badge.isAccessibilityElement = false
-        let tap = UITapGestureRecognizer(target: self, action: #selector(onAvatarTapped))
-        badge.addGestureRecognizer(tap)
         return badge
     }()
 
@@ -232,19 +229,27 @@ class ContactCell: UITableViewCell {
         avatar.setColor(color)
     }
 
-    @objc func onAvatarTapped() {
-        if rowIndex == -1 {
-            return
-        }
-        delegate?.onAvatarTapped(at: rowIndex)
-    }
-
+    // use this update-method to update cell in cellForRowAt whenever it is possible - other set-methods will be set private in progress
     func updateCell(cellViewModel: AvatarCellViewModel) {
+
         // subtitle
         subtitleLabel.attributedText = cellViewModel.subtitle.boldAt(indexes: cellViewModel.subtitleHighlightIndexes, fontSize: subtitleLabel.font.pointSize)
 
         switch cellViewModel.type {
-        case .CHAT(let chatData):
+        case .deaddrop(let deaddropData):
+            safe_assert(deaddropData.chatId == DC_CHAT_ID_DEADDROP)
+            backgroundColor = DcColors.deaddropBackground
+            contentView.backgroundColor = DcColors.deaddropBackground
+            let contact = DcContact(id: DcMsg(id: deaddropData.msgId).fromContactId)
+            if let img = contact.profileImage {
+                resetBackupImage()
+                setImage(img)
+            } else {
+                setBackupImage(name: contact.nameNAddr, color: contact.color)
+            }
+            titleLabel.attributedText = cellViewModel.title.boldAt(indexes: cellViewModel.titleHighlightIndexes, fontSize: titleLabel.font.pointSize)
+
+        case .chat(let chatData):
             let chat = DcChat(id: chatData.chatId)
 
             // text bold if chat contains unread messages - otherwise hightlight search results if needed
@@ -253,18 +258,25 @@ class ContactCell: UITableViewCell {
             } else {
                 titleLabel.attributedText = cellViewModel.title.boldAt(indexes: cellViewModel.titleHighlightIndexes, fontSize: titleLabel.font.pointSize)
             }
-
+            if chat.visibility == DC_CHAT_VISIBILITY_PINNED {
+                backgroundColor = DcColors.deaddropBackground
+                contentView.backgroundColor = DcColors.deaddropBackground
+            } else {
+                backgroundColor = DcColors.contactCellBackgroundColor
+                contentView.backgroundColor = DcColors.contactCellBackgroundColor
+            }
             if let img = chat.profileImage {
                 resetBackupImage()
                 setImage(img)
             } else {
-              setBackupImage(name: chat.name, color: chat.color)
+                setBackupImage(name: chat.name, color: chat.color)
             }
             setVerified(isVerified: chat.isVerified)
             setTimeLabel(chatData.summary.timestamp)
-            setStatusIndicators(unreadCount: chatData.unreadMessages, status: chatData.summary.state, visibility: chat.visibility, isLocationStreaming: chat.isSendingLocations)
+            setStatusIndicators(unreadCount: chatData.unreadMessages, status: chatData.summary.state,
+                                visibility: chat.visibility, isLocationStreaming: chat.isSendingLocations)
 
-        case .CONTACT(let contactData):
+        case .contact(let contactData):
             let contact = DcContact(id: contactData.contactId)
             titleLabel.attributedText = cellViewModel.title.boldAt(indexes: cellViewModel.titleHighlightIndexes, fontSize: titleLabel.font.pointSize)
             avatar.setName(cellViewModel.title)

+ 327 - 0
deltachat-ios/ViewModel/ChatListViewModel.swift

@@ -0,0 +1,327 @@
+import UIKit
+
+// MARK: - ChatListViewModelProtocol
+protocol ChatListViewModelProtocol: class, UISearchResultsUpdating {
+
+    var onChatListUpdate: VoidFunction? { get set }
+
+    var isArchive: Bool { get }
+
+    var numberOfSections: Int { get }
+    func numberOfRowsIn(section: Int) -> Int
+    func cellDataFor(section: Int, row: Int) -> AvatarCellViewModel
+
+    func msgIdFor(row: Int) -> Int?
+    func chatIdFor(section: Int, row: Int) -> Int? // needed to differentiate betweeen deaddrop / archive / default
+
+    // search related
+    var searchActive: Bool { get }
+    func beginSearch()
+    func endSearch()
+    func titleForHeaderIn(section: Int) -> String? // only visible on search results
+    
+    /// returns ROW of table
+    func deleteChat(chatId: Int) -> Int
+    func archiveChatToggle(chatId: Int)
+    func pinChatToggle(chatId: Int)
+    func refreshData()
+}
+
+// MARK: - ChatListViewModel
+class ChatListViewModel: NSObject, ChatListViewModelProtocol {
+
+    var onChatListUpdate: VoidFunction?
+
+    enum ChatListSectionType {
+        case chats
+        case contacts
+        case messages
+    }
+
+    class ChatListSection {
+        let type: ChatListSectionType
+        var headerTitle: String {
+            switch type {
+            case .chats:
+                return String.localized("pref_chats")
+            case .contacts:
+                return String.localized("contacts_headline")
+            case .messages:
+                return String.localized("pref_messages")
+            }
+        }
+        init(type: ChatListSectionType) {
+            self.type = type
+        }
+    }
+
+    var isArchive: Bool
+    private let dcContext: DcContext
+
+    var searchActive: Bool = false
+
+    // if searchfield is empty we show default chat list
+    private var showSearchResults: Bool {
+        return searchActive && searchText.containsCharacters()
+    }
+
+    private var chatList: DcChatlist!
+
+    // for search filtering
+    private var searchText: String = ""
+    private var searchResultChatList: DcChatlist?
+    private var searchResultContactIds: [Int] = []
+    private var searchResultMessageIds: [Int] = []
+
+    // to manage sections dynamically
+    private var searchResultsChatsSection: ChatListSection = ChatListSection(type: .chats)
+    private var searchResultsContactsSection: ChatListSection = ChatListSection(type: .contacts)
+    private var searchResultsMessagesSection: ChatListSection = ChatListSection(type: .messages)
+    private var searchResultSections: [ChatListSection] = []
+
+    init(dcContext: DcContext, isArchive: Bool) {
+        dcContext.updateDeviceChats()
+        self.isArchive = isArchive
+        self.dcContext = dcContext
+        super.init()
+        updateChatList(notifyListener: true)
+    }
+
+    private func updateChatList(notifyListener: Bool) {
+        var gclFlags: Int32 = 0
+        if isArchive {
+            gclFlags |= DC_GCL_ARCHIVED_ONLY
+        } else if RelayHelper.sharedInstance.isForwarding() {
+            gclFlags |= DC_GCL_FOR_FORWARDING
+        }
+        self.chatList = dcContext.getChatlist(flags: gclFlags, queryString: nil, queryId: 0)
+        if notifyListener {
+            onChatListUpdate?()
+        }
+    }
+
+    var numberOfSections: Int {
+        if showSearchResults {
+            return searchResultSections.count
+        }
+        return 1
+    }
+
+    func numberOfRowsIn(section: Int) -> Int {
+        if showSearchResults {
+            switch searchResultSections[section].type {
+            case .chats:
+                return searchResultChatList?.length ?? 0
+            case .contacts:
+                return searchResultContactIds.count
+            case .messages:
+                return searchResultMessageIds.count
+            }
+        }
+        return chatList.length
+    }
+
+    func cellDataFor(section: Int, row: Int) -> AvatarCellViewModel {
+        if showSearchResults {
+            switch searchResultSections[section].type {
+            case .chats:
+                break
+            case .contacts:
+                return makeContactCellViewModel(contactId: searchResultContactIds[row])
+            case .messages:
+                return makeMessageCellViewModel(msgId: searchResultMessageIds[row])
+            }
+        }
+        return makeChatCellViewModel(index: row, searchText: searchText)
+    }
+
+    func titleForHeaderIn(section: Int) -> String? {
+        if showSearchResults {
+            return searchResultSections[section].headerTitle
+        }
+        return nil
+    }
+
+    func chatIdFor(section: Int, row: Int) -> Int? {
+        let cellData = cellDataFor(section: section, row: row)
+        switch cellData.type {
+        case .deaddrop(let data):
+            return data.chatId
+        case .chat(let data):
+            return data.chatId
+        case .contact:
+            return nil
+        }
+    }
+
+    func msgIdFor(row: Int) -> Int? {
+        if showSearchResults {
+            return nil
+        }
+        return chatList.getMsgId(index: row)
+    }
+
+    func refreshData() {
+        updateChatList(notifyListener: true)
+    }
+
+    func beginSearch() {
+        searchActive = true
+    }
+
+    func endSearch() {
+        searchText = ""
+        searchActive = false
+        resetSearch()
+    }
+
+    func deleteChat(chatId: Int) -> Int {
+        // find index of chatId
+        let indexToDelete = Array(0..<chatList.length).filter { chatList.getChatId(index: $0) == chatId }.first
+        dcContext.deleteChat(chatId: chatId)
+        updateChatList(notifyListener: false)
+        safe_assert(indexToDelete != nil)
+        return indexToDelete ?? -1
+    }
+
+    func archiveChatToggle(chatId: Int) {
+        dcContext.archiveChat(chatId: chatId, archive: !self.isArchive)
+        updateChatList(notifyListener: false)
+    }
+
+    func pinChatToggle(chatId: Int) {
+        let chat: DcChat = dcContext.getChat(chatId: chatId)
+        let pinned = chat.visibility==DC_CHAT_VISIBILITY_PINNED
+        self.dcContext.setChatVisibility(chatId: chatId, visibility: pinned ? DC_CHAT_VISIBILITY_NORMAL : DC_CHAT_VISIBILITY_PINNED)
+        updateChatList(notifyListener: false)
+    }
+}
+
+private extension ChatListViewModel {
+
+    /// MARK: - avatarCellViewModel factory
+    func makeChatCellViewModel(index: Int, searchText: String) -> AvatarCellViewModel {
+
+        let list: DcChatlist = searchResultChatList ?? chatList
+        let chatId = list.getChatId(index: index)
+        let summary = list.getSummary(index: index)
+
+
+        if let msgId = msgIdFor(row: index), chatId == DC_CHAT_ID_DEADDROP {
+            return ChatCellViewModel(dearddropCellData: DeaddropCellData(chatId: chatId, msgId: msgId, summary: summary))
+        }
+
+        let chat = dcContext.getChat(chatId: chatId)
+        let unreadMessages = dcContext.getUnreadMessages(chatId: chatId)
+
+        var chatTitleIndexes: [Int] = []
+        if searchText.containsCharacters() {
+            let chatName = chat.name
+            chatTitleIndexes = chatName.containsExact(subSequence: searchText)
+        }
+
+        let viewModel = ChatCellViewModel(
+            chatData: ChatCellData(
+                chatId: chatId,
+                summary: summary,
+                unreadMessages: unreadMessages
+            ),
+            titleHighlightIndexes: chatTitleIndexes
+        )
+        return viewModel
+    }
+
+    func makeContactCellViewModel(contactId: Int) -> AvatarCellViewModel {
+        let contact = DcContact(id: contactId)
+        let nameIndexes = contact.displayName.containsExact(subSequence: searchText)
+        let emailIndexes = contact.email.containsExact(subSequence: searchText)
+        let chatId: Int? = dcContext.getChatIdByContactId(contactId)
+        // contact contains searchText
+        let viewModel = ContactCellViewModel(
+            contactData: ContactCellData(
+                contactId: contact.id,
+                chatId: chatId
+            ),
+            titleHighlightIndexes: nameIndexes,
+            subtitleHighlightIndexes: emailIndexes
+        )
+        return viewModel
+    }
+
+    func makeMessageCellViewModel(msgId: Int) -> AvatarCellViewModel {
+        let msg: DcMsg = DcMsg(id: msgId)
+        let chatId: Int = msg.chatId
+        let chat: DcChat = DcChat(id: chatId)
+        let summary: DcLot = msg.summary(chat: chat)
+        let unreadMessages = dcContext.getUnreadMessages(chatId: chatId)
+
+        let viewModel = ChatCellViewModel(
+            chatData: ChatCellData(
+                chatId: chatId,
+                summary: summary,
+                unreadMessages: unreadMessages
+            )
+        )
+        let subtitle = viewModel.subtitle
+        viewModel.subtitleHighlightIndexes = subtitle.containsExact(subSequence: searchText)
+        return viewModel
+    }
+
+    // MARK: - search
+    func updateSearchResultSections() {
+        var sections: [ChatListSection] = []
+        if let chatList = searchResultChatList, chatList.length > 0 {
+            sections.append(searchResultsChatsSection)
+        }
+        if !searchResultContactIds.isEmpty {
+            sections.append(searchResultsContactsSection)
+        }
+        if !searchResultMessageIds.isEmpty {
+            sections.append(searchResultsMessagesSection)
+        }
+        searchResultSections = sections
+    }
+
+    func resetSearch() {
+        searchResultChatList = nil
+        searchResultContactIds = []
+        searchResultMessageIds = []
+        updateSearchResultSections()
+    }
+
+    func filterContentForSearchText(_ searchText: String) {
+           if !searchText.isEmpty {
+               filterAndUpdateList(searchText: searchText)
+           } else {
+               // when search input field empty we show default chatList
+               resetSearch()
+           }
+           onChatListUpdate?()
+       }
+
+    func filterAndUpdateList(searchText: String) {
+
+           // #1 chats with searchPattern in title bar
+           var flags: Int32 = 0
+           flags |= DC_GCL_NO_SPECIALS
+           searchResultChatList = dcContext.getChatlist(flags: flags, queryString: searchText, queryId: 0)
+
+           // #2 contacts with searchPattern in name or in email
+           searchResultContactIds = dcContext.getContacts(flags: DC_GCL_ADD_SELF, queryString: searchText)
+
+           // #3 messages with searchPattern (filtered by dc_core)
+           searchResultMessageIds = dcContext.searchMessages(searchText: searchText)
+           updateSearchResultSections()
+       }
+}
+
+// MARK: UISearchResultUpdating
+extension ChatListViewModel: UISearchResultsUpdating {
+    func updateSearchResults(for searchController: UISearchController) {
+        self.searchText = searchController.searchBar.text ?? ""
+        if let searchText = searchController.searchBar.text {
+            filterContentForSearchText(searchText)
+            return
+        }
+    }
+}

+ 22 - 9
deltachat-ios/ViewModel/ContactCellViewModel.swift

@@ -1,7 +1,3 @@
-/*
- this file and the containing classes are manually imported from searchBarContactList-branch which has not been merged into master at this time. Once it has been merged, this file can be deleted.
- */
-
 import Foundation
 
 protocol AvatarCellViewModel {
@@ -13,12 +9,14 @@ protocol AvatarCellViewModel {
 }
 
 enum CellModel {
-    case CONTACT(ContactCellData)
-    case CHAT(ChatCellData)
+    case contact(ContactCellData)
+    case chat(ChatCellData)
+    case deaddrop(DeaddropCellData)
 }
 
 struct ContactCellData {
     let contactId: Int
+    let chatId: Int?
 }
 
 struct ChatCellData {
@@ -27,6 +25,12 @@ struct ChatCellData {
     let unreadMessages: Int
 }
 
+struct DeaddropCellData {
+    let chatId: Int
+    let msgId: Int
+    let summary: DcLot
+}
+
 class ContactCellViewModel: AvatarCellViewModel {
 
     private let contact: DcContact
@@ -47,7 +51,7 @@ class ContactCellViewModel: AvatarCellViewModel {
     var subtitleHighlightIndexes: [Int]
 
     init(contactData: ContactCellData, titleHighlightIndexes: [Int] = [], subtitleHighlightIndexes: [Int] = []) {
-        type = CellModel.CONTACT(contactData)
+        type = CellModel.contact(contactData)
         self.titleHighlightIndexes = titleHighlightIndexes
         self.subtitleHighlightIndexes = subtitleHighlightIndexes
         self.contact = DcContact(id: contactData.contactId)
@@ -57,7 +61,8 @@ class ContactCellViewModel: AvatarCellViewModel {
 class ChatCellViewModel: AvatarCellViewModel {
 
     private let chat: DcChat
-    private let summary: DcLot
+
+    private var summary: DcLot
 
     var type: CellModel
     var title: String {
@@ -80,10 +85,18 @@ class ChatCellViewModel: AvatarCellViewModel {
     var subtitleHighlightIndexes: [Int]
 
     init(chatData: ChatCellData, titleHighlightIndexes: [Int] = [], subtitleHighlightIndexes: [Int] = []) {
-        self.type = CellModel.CHAT(chatData)
+        self.type = CellModel.chat(chatData)
         self.titleHighlightIndexes = titleHighlightIndexes
         self.subtitleHighlightIndexes = subtitleHighlightIndexes
         self.summary = chatData.summary
         self.chat = DcChat(id: chatData.chatId)
     }
+
+    init(dearddropCellData cellData: DeaddropCellData) {
+        self.type = CellModel.deaddrop(cellData)
+        self.titleHighlightIndexes = []
+        self.subtitleHighlightIndexes = []
+        self.chat = DcChat(id: cellData.chatId)
+        self.summary = cellData.summary
+    }
 }