浏览代码

Merge pull request #867 from deltachat/share_contact_search

Share contact search
bjoern 4 年之前
父节点
当前提交
3d346ff191

+ 4 - 0
DcCore/DcCore.xcodeproj/project.pbxproj

@@ -22,6 +22,7 @@
 		30421986243F209E00516852 /* events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30421985243F209E00516852 /* events.swift */; };
 		30421988243F23E500516852 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30421987243F23E500516852 /* Constants.swift */; };
 		304F5E41244F2F3200462538 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304F5E40244F2F3200462538 /* UIImage+Extensions.swift */; };
+		3057028624C5C60000D84EFC /* UITableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057028524C5C60000D84EFC /* UITableView+Extensions.swift */; };
 		306C324824460CDE001D89F3 /* DateUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306C324724460CDE001D89F3 /* DateUtils.swift */; };
 		308198AB24866229003BE20D /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308198AA24866229003BE20D /* UserDefaults+Extensions.swift */; };
 		30E8F2212447357500CE2C90 /* DatabaseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F2202447357500CE2C90 /* DatabaseHelper.swift */; };
@@ -59,6 +60,7 @@
 		30421985243F209E00516852 /* events.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = events.swift; path = ../../DcCore/DcCore/DC/events.swift; sourceTree = "<group>"; };
 		30421987243F23E500516852 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
 		304F5E40244F2F3200462538 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
+		3057028524C5C60000D84EFC /* UITableView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extensions.swift"; sourceTree = "<group>"; };
 		306C324724460CDE001D89F3 /* DateUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateUtils.swift; sourceTree = "<group>"; };
 		308198AA24866229003BE20D /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = "<group>"; };
 		30E8F2202447357500CE2C90 /* DatabaseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseHelper.swift; sourceTree = "<group>"; };
@@ -158,6 +160,7 @@
 				30E8F2472449C98600CE2C90 /* UIView+Extensions.swift */,
 				304F5E40244F2F3200462538 /* UIImage+Extensions.swift */,
 				308198AA24866229003BE20D /* UserDefaults+Extensions.swift */,
+				3057028524C5C60000D84EFC /* UITableView+Extensions.swift */,
 			);
 			path = Extensions;
 			sourceTree = "<group>";
@@ -327,6 +330,7 @@
 				30421964243F0B8400516852 /* String+Extensions.swift in Sources */,
 				30421960243E257100516852 /* UIColor+Extensions.swift in Sources */,
 				30E8F2482449C98600CE2C90 /* UIView+Extensions.swift in Sources */,
+				3057028624C5C60000D84EFC /* UITableView+Extensions.swift in Sources */,
 				308198AB24866229003BE20D /* UserDefaults+Extensions.swift in Sources */,
 				304F5E41244F2F3200462538 /* UIImage+Extensions.swift in Sources */,
 			);

+ 37 - 0
DcCore/DcCore/Extensions/String+Extensions.swift

@@ -1,4 +1,6 @@
 import Foundation
+import UIKit
+
 public extension String {
     
 	static func localized(_ stringID: String) -> String {
@@ -20,4 +22,39 @@ public extension String {
         return resultString
     }
 
+    func containsCharacters() -> Bool {
+        return !trimmingCharacters(in: [" "]).isEmpty
+    }
+
+    func containsExact(subSequence: String?) -> [Int] {
+        guard let searchText = subSequence else {
+            return []
+        }
+        if searchText.count > count {
+            return []
+        }
+
+        if let range = range(of: searchText, options: .caseInsensitive) {
+            let index: Int = distance(from: startIndex, to: range.lowerBound)
+            var indexes: [Int] = []
+            for i in index..<(index + searchText.count) {
+                indexes.append(i)
+            }
+            return indexes
+        }
+        return []
+    }
+
+    func boldAt(indexes: [Int], fontSize: CGFloat) -> NSAttributedString {
+        let attributedText = NSMutableAttributedString(string: self)
+
+        for index in indexes {
+            if index < 0 || count <= index {
+                break
+            }
+            attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: NSRange(location: index, length: 1))
+        }
+        return attributedText
+    }
+
 }

+ 14 - 0
DcCore/DcCore/Extensions/UITableView+Extensions.swift

@@ -0,0 +1,14 @@
+import UIKit
+
+extension UITableView {
+    public func scrollToTop() {
+        let numberOfSections = self.numberOfSections
+        if numberOfSections > 0 {
+            let numberOfRows = self.numberOfRows(inSection: 0)
+            if numberOfRows > 0 {
+                let indexPath = IndexPath(row: 0, section: 0)
+                self.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.top, animated: false)
+            }
+        }
+    }
+}

+ 138 - 8
DcShare/Controller/ChatListController.swift

@@ -8,14 +8,37 @@ protocol ChatListDelegate: class {
 
 class ChatListController: UITableViewController {
     let dcContext: DcContext
-    var chatList: DcChatlist?
+    let viewModel: ChatListViewModel
     let contactCellReuseIdentifier = "contactCellReuseIdentifier"
     weak var chatListDelegate: ChatListDelegate?
+    var keyboardAppearedObserver: Any?
+    var keyboardDisappearedObserver: Any?
+
+    /// MARK - search
+
+    private lazy var searchController: UISearchController = {
+        let searchController = UISearchController(searchResultsController: nil)
+        searchController.searchResultsUpdater = viewModel
+        searchController.obscuresBackgroundDuringPresentation = false
+        searchController.searchBar.placeholder = String.localized("search")
+        searchController.dimsBackgroundDuringPresentation = false
+        searchController.hidesNavigationBarDuringPresentation = true
+        searchController.searchBar.delegate = self
+        return searchController
+    }()
+
+    private lazy var emptySearchStateLabel: EmptyStateLabel = {
+        let label = EmptyStateLabel()
+        label.isHidden = true
+        return label
+    }()
 
     init(dcContext: DcContext, chatListDelegate: ChatListDelegate) {
         self.dcContext = dcContext
         self.chatListDelegate = chatListDelegate
+        self.viewModel = ChatListViewModel(dcContext: dcContext)
         super.init(style: .grouped)
+        viewModel.onChatListUpdate = handleChatListUpdate
     }
 
     required init?(coder: NSCoder) {
@@ -24,20 +47,72 @@ class ChatListController: UITableViewController {
 
     override func viewWillAppear(_ animated: Bool) {
         preferredContentSize = UIScreen.main.bounds.size
+        navigationItem.hidesSearchBarWhenScrolling = false
+    }
+
+    override func viewDidAppear(_ animated: Bool) {
+        navigationItem.hidesSearchBarWhenScrolling = true
+        let nc = NotificationCenter.default
+        keyboardAppearedObserver = nc.addObserver(self,
+                                                  selector: #selector(keyboardWillShow(_:)),
+                                                  name: UIResponder.keyboardWillShowNotification,
+                                                  object: nil)
+        keyboardDisappearedObserver = nc.addObserver(self,
+                                                     selector: #selector(keyboardWillHide(_:)),
+                                                     name: UIResponder.keyboardWillHideNotification,
+                                                     object: nil)
     }
 
     override func viewDidLoad() {
         super.viewDidLoad()
-        chatList = dcContext.getChatlist(flags: DC_GCL_ADD_ALLDONE_HINT | DC_GCL_FOR_FORWARDING | DC_GCL_NO_SPECIALS, queryString: nil, queryId: 0)
+        navigationItem.searchController = searchController
+        configureTableView()
+        setupSubviews()
+    }
+
+    override func viewDidDisappear(_ animated: Bool) {
+        if let keyboardAppearedObserver = keyboardAppearedObserver {
+            NotificationCenter.default.removeObserver(keyboardAppearedObserver)
+        }
+        if let keyboardDisappearedObserver = keyboardDisappearedObserver {
+            NotificationCenter.default.removeObserver(keyboardDisappearedObserver)
+        }
+    }
+
+    @objc func keyboardWillShow(_ notification: Notification) {
+        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
+            tableView.tableFooterView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: keyboardSize.height))
+        }
+    }
+
+    @objc func keyboardWillHide(_ notification: Notification) {
+        tableView.tableFooterView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: Double.leastNormalMagnitude))
+    }
+
+    // MARK: - setup
+    private func setupSubviews() {
+        view.addSubview(emptySearchStateLabel)
+        emptySearchStateLabel.translatesAutoresizingMaskIntoConstraints = false
+        emptySearchStateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40).isActive = true
+        emptySearchStateLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
+        emptySearchStateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
+        emptySearchStateLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
+    }
+
+    // MARK: - configuration
+    private func configureTableView() {
         tableView.register(ChatListCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
         tableView.rowHeight = 64
         tableView.tableHeaderView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: Double.leastNormalMagnitude))
         tableView.tableFooterView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: Double.leastNormalMagnitude))
+    }
 
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return viewModel.numberOfSections
     }
 
     override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-        return chatList?.length ?? 0
+        return viewModel.numberOfRowsIn(section: section)
     }
 
     override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
@@ -45,17 +120,72 @@ class ChatListController: UITableViewController {
             fatalError("could not deque TableViewCell")
         }
 
-        if let chatList = chatList {
-            cell.updateCell(chatId: chatList.getChatId(index: indexPath.row))
-        }
+        let cellData = viewModel.cellDataFor(section: indexPath.section, row: indexPath.row)
+        cell.updateCell(cellViewModel: cellData)
 
         return cell
     }
 
     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-        if let chatList = chatList {
-            chatListDelegate?.onChatSelected(chatId: chatList.getChatId(index: indexPath.row))
+        if let chatId = viewModel.getChatId(section: indexPath.section, row: indexPath.row) {
+            chatListDelegate?.onChatSelected(chatId: chatId)
+        }
+
+        let cellData = viewModel.cellDataFor(section: indexPath.section, row: indexPath.row)
+        switch cellData.type {
+        case .chat(let data):
+            chatListDelegate?.onChatSelected(chatId: data.chatId)
+        case .contact(let data):
+             if let chatId = data.chatId {
+                chatListDelegate?.onChatSelected(chatId: chatId)
+            } else {
+                let chatId = dcContext.createChatByContactId(contactId: data.contactId)
+                chatListDelegate?.onChatSelected(chatId: chatId)
+            }
+        default:
+            fatalError("Other types are not allowed in Share contact search")
+        }
+    }
+
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+           return viewModel.titleForHeaderIn(section: section)
+       }
+
+    func handleChatListUpdate() {
+        tableView.reloadData()
+
+        if let emptySearchText = viewModel.emptySearchText {
+            let text = String.localizedStringWithFormat(
+                String.localized("search_no_result_for_x"),
+                emptySearchText
+            )
+            emptySearchStateLabel.text = text
+            emptySearchStateLabel.isHidden = false
+        } else {
+            emptySearchStateLabel.text = nil
+            emptySearchStateLabel.isHidden = true
         }
     }
 
 }
+
+// MARK: - uisearchbardelegate
+extension ChatListController: UISearchBarDelegate {
+    func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
+        viewModel.beginSearch()
+        return true
+    }
+
+    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
+        // searchBar will be set to "" by system
+        viewModel.endSearch()
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
+           self.tableView.scrollToTop()
+        }
+    }
+
+    func searchBar(_ searchBar: UISearchBar, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
+        tableView.scrollToTop()
+        return true
+    }
+}

+ 52 - 18
DcShare/View/ChatListCell.swift

@@ -13,13 +13,31 @@ class ChatListCell: UITableViewCell {
         return badge
     }()
 
-    let titleLabel: UILabel = {
+    lazy var stackView: UIStackView = {
+        let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
+        stackView.axis = .vertical
+        stackView.translatesAutoresizingMaskIntoConstraints = false
+        stackView.alignment = .leading
+        stackView.clipsToBounds = true
+        return stackView
+    }()
+
+    lazy var titleLabel: UILabel = {
         let label = UILabel()
         label.font = UIFont.preferredFont(forTextStyle: .headline)
         label.adjustsFontForContentSizeCategory = true
         label.lineBreakMode = .byTruncatingTail
         label.textColor = DcColors.defaultTextColor
-        label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1), for: NSLayoutConstraint.Axis.horizontal)
+        label.translatesAutoresizingMaskIntoConstraints = false
+        return label
+    }()
+
+    lazy var subtitleLabel: UILabel = {
+        let label = UILabel()
+        label.textColor = DcColors.middleGray
+        label.lineBreakMode = .byTruncatingTail
+        label.font = .preferredFont(forTextStyle: .subheadline)
+        label.adjustsFontForContentSizeCategory = true
         label.translatesAutoresizingMaskIntoConstraints = false
         return label
     }()
@@ -49,11 +67,11 @@ class ChatListCell: UITableViewCell {
             avatar.constraintCenterYTo(contentView),
         ])
 
-        contentView.addSubview(titleLabel)
+        contentView.addSubview(stackView)
         contentView.addConstraints([
-            titleLabel.constraintCenterYTo(contentView),
-            titleLabel.constraintToTrailingOf(avatar, paddingLeading: margin),
-            titleLabel.constraintAlignTrailingTo(contentView)
+            stackView.constraintCenterYTo(contentView),
+            stackView.constraintToTrailingOf(avatar, paddingLeading: margin),
+            stackView.constraintAlignTrailingTo(contentView),
         ])
     }
 
@@ -72,21 +90,37 @@ class ChatListCell: UITableViewCell {
     }
 
     private func setColor(_ color: UIColor) {
-          avatar.setColor(color)
-      }
+        avatar.setColor(color)
+    }
 
     // use this update-method to update cell in cellForRowAt whenever it is possible - other set-methods will be set private in progress
-    func updateCell(chatId: Int) {
-        let chat = DcContext.shared.getChat(chatId: chatId)
-        titleLabel.text = chat.name
-        backgroundColor = DcColors.contactCellBackgroundColor
-        contentView.backgroundColor = DcColors.contactCellBackgroundColor
+    func updateCell(cellViewModel: AvatarCellViewModel) {
+        // subtitle
+        switch cellViewModel.type {
+        case .chat(let chatData):
+            let chat = DcContext.shared.getChat(chatId: chatData.chatId)
+            titleLabel.attributedText = cellViewModel.title.boldAt(indexes: cellViewModel.titleHighlightIndexes, fontSize: titleLabel.font.pointSize)
+            if let img = chat.profileImage {
+                resetBackupImage()
+                setImage(img)
+            } else {
+                setBackupImage(name: chat.name, color: chat.color)
+            }
+            subtitleLabel.attributedText = nil
+
+        case .contact(let contactData):
+            let contact = DcContact(id: contactData.contactId)
+            titleLabel.attributedText = cellViewModel.title.boldAt(indexes: cellViewModel.titleHighlightIndexes, fontSize: titleLabel.font.pointSize)
+            if let profileImage = contact.profileImage {
+                avatar.setImage(profileImage)
+            } else {
+                setBackupImage(name: cellViewModel.title, color: contact.color)
+            }
+            subtitleLabel.attributedText = cellViewModel.subtitle.boldAt(indexes: cellViewModel.subtitleHighlightIndexes,
+                                                                         fontSize: subtitleLabel.font.pointSize)
+        default:
+            return
 
-        if let img = chat.profileImage {
-            resetBackupImage()
-            setImage(img)
-        } else {
-            setBackupImage(name: chat.name, color: chat.color)
         }
     }
 }

+ 205 - 0
DcShare/ViewModel/ChatListViewModel.swift

@@ -0,0 +1,205 @@
+import UIKit
+import DcCore
+
+
+// MARK: - ChatListViewModel
+class ChatListViewModel: NSObject {
+
+    var onChatListUpdate: VoidFunction?
+
+    enum ChatListSectionType {
+        case chats
+        case contacts
+    }
+
+    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] = []
+
+    // to manage sections dynamically
+    private var searchResultsChatsSection: ChatListSectionType = .chats
+    private var searchResultsContactsSection: ChatListSectionType = .contacts
+    private var searchResultSections: [ChatListSectionType] = []
+
+    init(dcContext: DcContext) {
+        self.dcContext = dcContext
+        super.init()
+        updateChatList(notifyListener: true)
+    }
+
+    private func updateChatList(notifyListener: Bool) {
+        self.chatList = dcContext.getChatlist(flags: DC_GCL_ADD_ALLDONE_HINT | DC_GCL_FOR_FORWARDING | DC_GCL_NO_SPECIALS, queryString: nil, queryId: 0)
+        if notifyListener {
+            onChatListUpdate?()
+        }
+    }
+
+    func getChatId(section: Int, row: Int) -> Int? {
+        if showSearchResults {
+            switch searchResultSections[section] {
+            case .chats:
+                let list: DcChatlist? = searchResultChatList
+                return list?.getChatId(index: row)
+            case .contacts:
+                return searchResultContactIds[row]
+            }
+        }
+        return chatList.getChatId(index: row)
+    }
+
+    func cellDataFor(section: Int, row: Int) -> AvatarCellViewModel {
+        if showSearchResults {
+            switch searchResultSections[section] {
+            case .chats:
+                return makeChatCellViewModel(index: row, searchText: searchText)
+            case .contacts:
+                return ContactCellViewModel.make(contactId: searchResultContactIds[row], searchText: searchText, dcContext: dcContext)
+            }
+        }
+        return makeChatCellViewModel(index: row, searchText: "")
+    }
+
+    func makeChatCellViewModel(index: Int, searchText: String) -> AvatarCellViewModel {
+        let list: DcChatlist = searchResultChatList ?? chatList
+        let chatId = list.getChatId(index: index)
+        let summary = list.getSummary(index: index)
+
+        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(
+            dcContext: dcContext,
+            chatData: ChatCellData(
+                chatId: chatId,
+                summary: summary,
+                unreadMessages: unreadMessages
+            ),
+            titleHighlightIndexes: chatTitleIndexes
+        )
+        return viewModel
+    }
+
+
+    var numberOfSections: Int {
+        if showSearchResults {
+            return searchResultSections.count
+        }
+        return 1
+    }
+
+    func numberOfRowsIn(section: Int) -> Int {
+        if showSearchResults {
+            switch searchResultSections[section] {
+            case .chats:
+                return searchResultChatList?.length ?? 0
+            case .contacts:
+                return searchResultContactIds.count
+            }
+        }
+        return chatList.length
+    }
+
+    func titleForHeaderIn(section: Int) -> String? {
+        if showSearchResults {
+            let title: String
+            switch searchResultSections[section] {
+            case .chats:
+                title = "n_chats"
+            case .contacts:
+                title = "n_contacts"
+            }
+            return String.localized(stringID: title, count: numberOfRowsIn(section: section))
+        }
+        return nil
+    }
+
+    func refreshData() {
+        updateChatList(notifyListener: true)
+    }
+
+    func beginSearch() {
+        searchActive = true
+    }
+
+    func endSearch() {
+        searchActive = false
+        searchText = ""
+        resetSearch()
+    }
+
+    var emptySearchText: String? {
+        if searchActive && numberOfSections == 0 {
+            return searchText
+        }
+        return nil
+    }
+
+    // MARK: - search
+    func updateSearchResultSections() {
+        var sections: [ChatListSectionType] = []
+        if let chatList = searchResultChatList, chatList.length > 0 {
+            sections.append(searchResultsChatsSection)
+        }
+        if !searchResultContactIds.isEmpty {
+            sections.append(searchResultsContactsSection)
+        }
+        searchResultSections = sections
+    }
+
+    func resetSearch() {
+        searchResultChatList = nil
+        searchResultContactIds = []
+        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
+        let flags = DC_GCL_ADD_ALLDONE_HINT | DC_GCL_FOR_FORWARDING | 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)
+
+        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
+        }
+    }
+}

+ 34 - 8
deltachat-ios.xcodeproj/project.pbxproj

@@ -23,6 +23,15 @@
 		304219D3243F588500516852 /* DcCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 304219D1243F588500516852 /* DcCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304219D82440734A00516852 /* DcMsg+Extension.swift */; };
 		304F5E44244F571C00462538 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A9FB14A1FB061E2001FEA36 /* Assets.xcassets */; };
+		3057027F24C5B2F800D84EFC /* ChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057027E24C5B2F800D84EFC /* ChatListViewModel.swift */; };
+		3057028724C5C88300D84EFC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 306011B422E5E7FB00C1CE6F /* Localizable.stringsdict */; };
+		3057028C24C5E7B600D84EFC /* ContactCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE77838E23E4276D0093EABD /* ContactCellViewModel.swift */; };
+		3057029B24C6441300D84EFC /* EmptyStateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057029A24C6441300D84EFC /* EmptyStateLabel.swift */; };
+		3057029D24C6442800D84EFC /* FlexLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057029C24C6442800D84EFC /* FlexLabel.swift */; };
+		3057029E24C6444D00D84EFC /* FlexLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057029C24C6442800D84EFC /* FlexLabel.swift */; };
+		3057029F24C6445000D84EFC /* EmptyStateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057029A24C6441300D84EFC /* EmptyStateLabel.swift */; };
+		305702A124C6453700D84EFC /* TypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305702A024C6453700D84EFC /* TypeAlias.swift */; };
+		305702A224C6455400D84EFC /* TypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305702A024C6453700D84EFC /* TypeAlias.swift */; };
 		305961CC2346125100C80F33 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961822346125000C80F33 /* UIView+Extensions.swift */; };
 		305961CD2346125100C80F33 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961832346125000C80F33 /* UIEdgeInsets+Extensions.swift */; };
 		305961CF2346125100C80F33 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961852346125000C80F33 /* UIColor+Extensions.swift */; };
@@ -88,7 +97,6 @@
 		3059620C2346125100C80F33 /* MessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961CB2346125100C80F33 /* MessageSizeCalculator.swift */; };
 		3059620E234614E700C80F33 /* DcContact+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3059620D234614E700C80F33 /* DcContact+Extension.swift */; };
 		305962102346154D00C80F33 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3059620F2346154D00C80F33 /* String+Extension.swift */; };
-		305FE03623A81B4C0053BE90 /* EmptyStateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305FE03523A81B4C0053BE90 /* EmptyStateLabel.swift */; };
 		3060119C22DDE24000C1CE6F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3060119E22DDE24000C1CE6F /* Localizable.strings */; };
 		306011B622E5E7FB00C1CE6F /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 306011B422E5E7FB00C1CE6F /* Localizable.stringsdict */; };
 		306C32322445CDE9001D89F3 /* DcLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306C32312445CDE9001D89F3 /* DcLogger.swift */; };
@@ -166,7 +174,6 @@
 		AEACE2DF1FB3246400DCDD78 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2DE1FB3246400DCDD78 /* Message.swift */; };
 		AEACE2E31FB32B5C00DCDD78 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2E21FB32B5C00DCDD78 /* Constants.swift */; };
 		AEACE2E51FB32E1900DCDD78 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2E41FB32E1900DCDD78 /* Utils.swift */; };
-		AEB54C7F246DBA610004624C /* FlexLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB54C7E246DBA610004624C /* FlexLabel.swift */; };
 		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 */; };
@@ -182,7 +189,6 @@
 		AEE700252438E0E500D6992E /* ProgressAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE700242438E0E500D6992E /* ProgressAlertHandler.swift */; };
 		AEF53BD5248904BF00D309C1 /* GalleryTimeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF53BD4248904BF00D309C1 /* GalleryTimeLabel.swift */; };
 		AEFBE22F23FEF23D0045327A /* ProviderInfoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEFBE22E23FEF23D0045327A /* ProviderInfoCell.swift */; };
-		AEFBE23123FF09B20045327A /* TypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEFBE23023FF09B20045327A /* TypeAlias.swift */; };
 		B20462E42440A4A600367A57 /* SettingsAutodelOverviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B20462E32440A4A600367A57 /* SettingsAutodelOverviewController.swift */; };
 		B20462E62440C99600367A57 /* SettingsAutodelSetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B20462E52440C99600367A57 /* SettingsAutodelSetController.swift */; };
 		B21005DB23383664004C70C5 /* SettingsClassicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21005DA23383664004C70C5 /* SettingsClassicViewController.swift */; };
@@ -267,6 +273,10 @@
 		3040F461234F550300FA34D5 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
 		304219D1243F588500516852 /* DcCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DcCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		304219D82440734A00516852 /* DcMsg+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DcMsg+Extension.swift"; sourceTree = "<group>"; };
+		3057027E24C5B2F800D84EFC /* ChatListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewModel.swift; sourceTree = "<group>"; };
+		3057029A24C6441300D84EFC /* EmptyStateLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyStateLabel.swift; sourceTree = "<group>"; };
+		3057029C24C6442800D84EFC /* FlexLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlexLabel.swift; sourceTree = "<group>"; };
+		305702A024C6453700D84EFC /* TypeAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeAlias.swift; sourceTree = "<group>"; };
 		305961822346125000C80F33 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
 		305961832346125000C80F33 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = "<group>"; };
 		305961852346125000C80F33 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
@@ -474,7 +484,6 @@
 		AEE700242438E0E500D6992E /* ProgressAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressAlertHandler.swift; sourceTree = "<group>"; };
 		AEF53BD4248904BF00D309C1 /* GalleryTimeLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryTimeLabel.swift; sourceTree = "<group>"; };
 		AEFBE22E23FEF23D0045327A /* ProviderInfoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderInfoCell.swift; sourceTree = "<group>"; };
-		AEFBE23023FF09B20045327A /* TypeAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeAlias.swift; sourceTree = "<group>"; };
 		B20462E02440805C00367A57 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		B20462E12440805C00367A57 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
 		B20462E22440805C00367A57 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -583,6 +592,14 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		3057027D24C5B2C700D84EFC /* ViewModel */ = {
+			isa = PBXGroup;
+			children = (
+				3057027E24C5B2F800D84EFC /* ChatListViewModel.swift */,
+			);
+			path = ViewModel;
+			sourceTree = "<group>";
+		};
 		3059617E234610A800C80F33 /* MessageKit */ = {
 			isa = PBXGroup;
 			children = (
@@ -744,6 +761,7 @@
 		30E8F2112447285600CE2C90 /* DcShare */ = {
 			isa = PBXGroup;
 			children = (
+				3057027D24C5B2C700D84EFC /* ViewModel */,
 				304F5E472451D2CA00462538 /* View */,
 				304F5E462451D2AA00462538 /* Controller */,
 				304F5E452451D27500462538 /* Helper */,
@@ -948,6 +966,7 @@
 		AE851AC2227C695000ED86F0 /* Helper */ = {
 			isa = PBXGroup;
 			children = (
+				305702A024C6453700D84EFC /* TypeAlias.swift */,
 				AEACE2E21FB32B5C00DCDD78 /* Constants.swift */,
 				AEACE2E41FB32E1900DCDD78 /* Utils.swift */,
 				AE851AC4227C755A00ED86F0 /* Protocols.swift */,
@@ -955,7 +974,6 @@
 				30AC265E237F1807002A943F /* AvatarHelper.swift */,
 				302B84C42396627F001C261F /* RelayHelper.swift */,
 				AE1988A423EB2FBA00B4CD5F /* Errors.swift */,
-				AEFBE23023FF09B20045327A /* TypeAlias.swift */,
 				307D822D241669C7006D2490 /* LocationManager.swift */,
 				AE0AA9552478191900D42A7F /* GridCollectionViewFlowLayout.swift */,
 				AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */,
@@ -966,6 +984,8 @@
 		AE851AC3227C695900ED86F0 /* View */ = {
 			isa = PBXGroup;
 			children = (
+				3057029C24C6442800D84EFC /* FlexLabel.swift */,
+				3057029A24C6441300D84EFC /* EmptyStateLabel.swift */,
 				AEF53BD4248904BF00D309C1 /* GalleryTimeLabel.swift */,
 				AE406EEE240FA454005F7022 /* Cell */,
 				B26B3BC6236DC3DC008ED35A /* SwitchCell.swift */,
@@ -1159,6 +1179,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				30E8F2162447285600CE2C90 /* MainInterface.storyboard in Resources */,
+				3057028724C5C88300D84EFC /* Localizable.stringsdict in Resources */,
 				304F5E44244F571C00462538 /* Assets.xcassets in Resources */,
 				30E8F2512449EA0E00CE2C90 /* Localizable.strings in Resources */,
 			);
@@ -1340,10 +1361,15 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				3057027F24C5B2F800D84EFC /* ChatListViewModel.swift in Sources */,
+				3057029E24C6444D00D84EFC /* FlexLabel.swift in Sources */,
 				302589FF2452FA280086C1CD /* ShareAttachment.swift in Sources */,
+				3057029F24C6445000D84EFC /* EmptyStateLabel.swift in Sources */,
+				305702A224C6455400D84EFC /* TypeAlias.swift in Sources */,
 				30E8F2442449C64100CE2C90 /* ChatListCell.swift in Sources */,
 				30E8F2132447285600CE2C90 /* ShareViewController.swift in Sources */,
 				30E8F253244DAD0E00CE2C90 /* SendingController.swift in Sources */,
+				3057028C24C5E7B600D84EFC /* ContactCellViewModel.swift in Sources */,
 				30E8F2422448B77600CE2C90 /* ChatListController.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -1377,6 +1403,7 @@
 				30A2EC36247D72720024ADD8 /* AnimatedImageMessageCell.swift in Sources */,
 				305962082346125100C80F33 /* MediaMessageSizeCalculator.swift in Sources */,
 				AE6EC5282497B9B200A400E4 /* ThumbnailCache.swift in Sources */,
+				3057029D24C6442800D84EFC /* FlexLabel.swift in Sources */,
 				AE52EA20229EB9F000C586C9 /* EditGroupViewController.swift in Sources */,
 				AE18F294228C602A0007B1BE /* SecuritySettingsController.swift in Sources */,
 				AE39D323249CFC1A007346A1 /* DocumentGalleryController.swift in Sources */,
@@ -1417,7 +1444,7 @@
 				3059620B2346125100C80F33 /* LocationMessageSizeCalculator.swift in Sources */,
 				305962072346125100C80F33 /* MessagesCollectionViewFlowLayout.swift in Sources */,
 				78ED838321D5379000243125 /* TextFieldCell.swift in Sources */,
-				AEFBE23123FF09B20045327A /* TypeAlias.swift in Sources */,
+				305702A124C6453700D84EFC /* TypeAlias.swift in Sources */,
 				AE19887523EB264000B4CD5F /* HelpViewController.swift in Sources */,
 				305961D52346125100C80F33 /* MessagesViewController+Menu.swift in Sources */,
 				305961F22346125100C80F33 /* LocationMessageCell.swift in Sources */,
@@ -1428,7 +1455,6 @@
 				7A0052C81FBE6CB40048C3BF /* NewContactController.swift in Sources */,
 				AEE56D762253431E007DC082 /* AccountSetupController.swift in Sources */,
 				AE8F503524753DFE007FEE0B /* GalleryViewController.swift in Sources */,
-				305FE03623A81B4C0053BE90 /* EmptyStateLabel.swift in Sources */,
 				AEACE2DD1FB323CA00DCDD78 /* ChatViewController.swift in Sources */,
 				AEE6EC412282DF5700EDC689 /* MailboxViewController.swift in Sources */,
 				AEF53BD5248904BF00D309C1 /* GalleryTimeLabel.swift in Sources */,
@@ -1472,6 +1498,7 @@
 				305961D22346125100C80F33 /* CGRect+Extensions.swift in Sources */,
 				305961E12346125100C80F33 /* LocationItem.swift in Sources */,
 				305961E72346125100C80F33 /* AccessoryPosition.swift in Sources */,
+				3057029B24C6441300D84EFC /* EmptyStateLabel.swift in Sources */,
 				30260CA7238F02F700D8D52C /* MultilineTextFieldCell.swift in Sources */,
 				305961DE2346125100C80F33 /* MessageType.swift in Sources */,
 				305961DA2346125100C80F33 /* MediaItem.swift in Sources */,
@@ -1494,7 +1521,6 @@
 				305961F12346125100C80F33 /* ContactMessageCell.swift in Sources */,
 				AE851AD0227DF50900ED86F0 /* GroupChatDetailViewController.swift in Sources */,
 				305961D12346125100C80F33 /* Bundle+Extensions.swift in Sources */,
-				AEB54C7F246DBA610004624C /* FlexLabel.swift in Sources */,
 				305962002346125100C80F33 /* MessagesCollectionView.swift in Sources */,
 				7A451DB01FB1F84900177250 /* AppCoordinator.swift in Sources */,
 				AE38B31822672DFC00EC37A1 /* ActionCell.swift in Sources */,

+ 0 - 13
deltachat-ios/Extensions/Extensions.swift

@@ -72,19 +72,6 @@ extension UIAlertController.Style {
     }
 }
 
-extension UITableView {
-    func scrollToTop() {
-        let numberOfSections = self.numberOfSections
-        if numberOfSections > 0 {
-            let numberOfRows = self.numberOfRows(inSection: 0)
-            if numberOfRows > 0 {
-                let indexPath = IndexPath(row: 0, section: 0)
-                self.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.top, animated: false)
-            }
-        }
-    }
-}
-
 extension UIFont {
     static func preferredFont(for style: TextStyle, weight: Weight) -> UIFont {
         let traits = UITraitCollection(preferredContentSizeCategory: .large)

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

@@ -9,29 +9,6 @@ extension String {
         return String(self[idx1..<idx2])
     }
 
-    func containsCharacters() -> Bool {
-        return !trimmingCharacters(in: [" "]).isEmpty
-    }
-
-    func containsExact(subSequence: String?) -> [Int] {
-        guard let searchText = subSequence else {
-            return []
-        }
-        if searchText.count > count {
-            return []
-        }
-
-        if let range = range(of: searchText, options: .caseInsensitive) {
-            let index: Int = distance(from: startIndex, to: range.lowerBound)
-            var indexes: [Int] = []
-            for i in index..<(index + searchText.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 {
@@ -62,18 +39,6 @@ extension String {
         return self[index(startIndex, offsetBy: i)]
     }
 
-    func boldAt(indexes: [Int], fontSize: CGFloat) -> NSAttributedString {
-        let attributedText = NSMutableAttributedString(string: self)
-
-        for index in indexes {
-            if index < 0 || count <= index {
-                break
-            }
-            attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: NSRange(location: index, length: 1))
-        }
-        return attributedText
-    }
-
     func bold(fontSize: CGFloat) -> NSAttributedString {
         let attributedText = NSMutableAttributedString(string: self)
         attributedText.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: NSRange(location: 0, length: count - 1))