Browse Source

Merge branch 'master' of https://github.com/deltachat/deltachat-ios

nayooti 5 năm trước cách đây
mục cha
commit
1f506bae3d
32 tập tin đã thay đổi với 1108 bổ sung356 xóa
  1. 8 0
      deltachat-ios.xcodeproj/project.pbxproj
  2. 3 1
      deltachat-ios/AppDelegate.swift
  3. 23 0
      deltachat-ios/Assets.xcassets/ic_location.imageset/Contents.json
  4. BIN
      deltachat-ios/Assets.xcassets/ic_location.imageset/ic_location_on_white_24pt_1x.png
  5. BIN
      deltachat-ios/Assets.xcassets/ic_location.imageset/ic_location_on_white_24pt_2x.png
  6. BIN
      deltachat-ios/Assets.xcassets/ic_location.imageset/ic_location_on_white_24pt_3x.png
  7. 226 220
      deltachat-ios/Controller/ChatListController.swift
  8. 70 34
      deltachat-ios/Controller/ChatViewController.swift
  9. 17 18
      deltachat-ios/Controller/ContactDetailViewController.swift
  10. 2 2
      deltachat-ios/Controller/EditContactController.swift
  11. 13 4
      deltachat-ios/Controller/GroupChatDetailViewController.swift
  12. 15 10
      deltachat-ios/Controller/NewChatViewController.swift
  13. 5 3
      deltachat-ios/Controller/NewContactController.swift
  14. 0 1
      deltachat-ios/Controller/NewGroupController.swift
  15. 2 2
      deltachat-ios/Controller/QrViewController.swift
  16. 18 9
      deltachat-ios/Coordinator/AppCoordinator.swift
  17. 76 3
      deltachat-ios/DC/Wrapper.swift
  18. 13 0
      deltachat-ios/Extensions/Extensions.swift
  19. 16 0
      deltachat-ios/Extensions/String+Extension.swift
  20. 1 0
      deltachat-ios/Helper/Colors.swift
  21. 11 0
      deltachat-ios/Helper/Constants.swift
  22. 132 0
      deltachat-ios/Helper/LocationManager.swift
  23. 2 2
      deltachat-ios/Helper/Utils.swift
  24. 5 0
      deltachat-ios/Info.plist
  25. 44 13
      deltachat-ios/View/ChatTitleView.swift
  26. 47 23
      deltachat-ios/View/ContactCell.swift
  27. 327 0
      deltachat-ios/ViewModel/ChatListViewModel.swift
  28. 22 9
      deltachat-ios/ViewModel/ContactCellViewModel.swift
  29. 6 2
      deltachat-ios/ViewModel/ContactDetailViewModel.swift
  30. 2 0
      deltachat-ios/en.lproj/InfoPlist.strings
  31. 1 0
      deltachat-ios/en.lproj/Localizable.strings
  32. 1 0
      tools/untranslated.xml

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

@@ -87,6 +87,7 @@
 		305FE03623A81B4C0053BE90 /* PaddingLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305FE03523A81B4C0053BE90 /* PaddingLabel.swift */; };
 		3060119C22DDE24000C1CE6F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3060119E22DDE24000C1CE6F /* Localizable.strings */; };
 		306011B622E5E7FB00C1CE6F /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 306011B422E5E7FB00C1CE6F /* Localizable.stringsdict */; };
+		307D822E241669C7006D2490 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307D822D241669C7006D2490 /* LocationManager.swift */; };
 		3095A351237DD1F700AB07F7 /* MediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3095A350237DD1F700AB07F7 /* MediaPicker.swift */; };
 		30A4D9AE2332672700544344 /* QrInviteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A4D9AD2332672600544344 /* QrInviteViewController.swift */; };
 		30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */; };
@@ -147,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 */; };
@@ -309,6 +311,7 @@
 		306011C722E5E82E00C1CE6F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lt; path = lt.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
 		306011C822E5E83100C1CE6F /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
 		306011C922E5E83500C1CE6F /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
+		307D822D241669C7006D2490 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
 		3095A350237DD1F700AB07F7 /* MediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPicker.swift; sourceTree = "<group>"; };
 		30A4D9AD2332672600544344 /* QrInviteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrInviteViewController.swift; sourceTree = "<group>"; };
 		30AC265E237F1807002A943F /* AvatarHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHelper.swift; sourceTree = "<group>"; };
@@ -379,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>"; };
@@ -710,6 +714,7 @@
 			children = (
 				AE77838C23E32ED20093EABD /* ContactDetailViewModel.swift */,
 				AE77838E23E4276D0093EABD /* ContactCellViewModel.swift */,
+				AEC67A1D241FCFE0007DDBE1 /* ChatListViewModel.swift */,
 			);
 			path = ViewModel;
 			sourceTree = "<group>";
@@ -784,6 +789,7 @@
 				302B84C42396627F001C261F /* RelayHelper.swift */,
 				AE1988A423EB2FBA00B4CD5F /* Errors.swift */,
 				AEFBE23023FF09B20045327A /* TypeAlias.swift */,
+				307D822D241669C7006D2490 /* LocationManager.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -1204,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 */,
@@ -1243,6 +1250,7 @@
 				7092474120B3869500AF8799 /* ContactDetailViewController.swift in Sources */,
 				300C50A1234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift in Sources */,
 				30F9B9EC235F2116006E7ACF /* MessageCounter.swift in Sources */,
+				307D822E241669C7006D2490 /* LocationManager.swift in Sources */,
 				305961F12346125100C80F33 /* ContactMessageCell.swift in Sources */,
 				AE851AD0227DF50900ED86F0 /* GroupChatDetailViewController.swift in Sources */,
 				305961D12346125100C80F33 /* Bundle+Extensions.swift in Sources */,

+ 3 - 1
deltachat-ios/AppDelegate.swift

@@ -20,6 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
     private let dcContext = DcContext()
     var appCoordinator: AppCoordinator!
     var relayHelper: RelayHelper!
+    var locationManager: LocationManager!
     // static let appCoordinatorDeprecated = AppCoordinatorDeprecated()
     static var progress: Float = 0 // TODO: delete
     static var lastErrorString: String?
@@ -81,9 +82,10 @@ 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()
         setStockTranslations()

+ 23 - 0
deltachat-ios/Assets.xcassets/ic_location.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "filename" : "ic_location_on_white_24pt_1x.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "ic_location_on_white_24pt_2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "filename" : "ic_location_on_white_24pt_3x.png",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

BIN
deltachat-ios/Assets.xcassets/ic_location.imageset/ic_location_on_white_24pt_1x.png


BIN
deltachat-ios/Assets.xcassets/ic_location.imageset/ic_location_on_white_24pt_2x.png


BIN
deltachat-ios/Assets.xcassets/ic_location.imageset/ic_location_on_white_24pt_3x.png


+ 226 - 220
deltachat-ios/Controller/ChatListController.swift

@@ -1,69 +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() {
+            tableView.scrollToTop()
+        }
 
         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) {
@@ -85,216 +126,150 @@ 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()
     }
 
     @objc func cancelButtonPressed() {
+        // cancel forwarding
         RelayHelper.sharedInstance.cancel()
+        viewModel.refreshData()
         updateTitle()
     }
 
-    private func getNumberOfArchivedChats() -> Int {
-        let chatList = dcContext.getChatlist(flags: DC_GCL_ARCHIVED_ONLY, queryString: nil, queryId: 0)
-        return chatList.length
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return viewModel.numberOfSections
     }
 
-    private func getChatList() {
-        var gclFlags: Int32 = 0
-        if showArchive {
-            gclFlags |= DC_GCL_ARCHIVED_ONLY
-        }
-        chatList = dcContext.getChatlist(flags: gclFlags, queryString: nil, queryId: 0)
-        chatTable.reloadData()
-    }
-
-    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 cellData = viewModel.cellDataFor(section: indexPath.section, row: indexPath.row)
 
-        let chatId = chatList.getChatId(index: row)
-        if chatId == DC_CHAT_ID_ARCHIVED_LINK {
-            return getArchiveCell(tableView)
+        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 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")
-        }
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+        return viewModel.titleForHeaderIn(section: section)
+    }
 
-        let chat = DcChat(id: chatId)
-        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) ])
-
-        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)
-        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 getArchiveCell(_ tableView: UITableView) -> UITableViewCell {
+    func handleChatListUpdate() {
+        tableView.reloadData()
+    }
+
+    func getArchiveCell(_ tableView: UITableView, title: String) -> UITableViewCell {
         let archiveCell: UITableViewCell
         if let cell = tableView.dequeueReusableCell(withIdentifier: "ArchiveCell") {
             archiveCell = cell
@@ -302,14 +277,12 @@ extension ChatListController: UITableViewDataSource, UITableViewDelegate {
             archiveCell = UITableViewCell(style: .default, reuseIdentifier: "ArchiveCell")
         }
         archiveCell.textLabel?.textAlignment = .center
-        var title = String.localized("chat_archived_chats_title")
-        let count = getNumberOfArchivedChats()
-        title.append(" (\(count))")
         archiveCell.textLabel?.text = title
         archiveCell.textLabel?.textColor = .systemBlue
         return archiveCell
     }
 
+    // MARK: - alerts
     private func showDeleteChatConfirmationAlert(chatId: Int) {
         let alert = UIAlertController(
             title: nil,
@@ -323,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()
     }
 }

+ 70 - 34
deltachat-ios/Controller/ChatViewController.swift

@@ -110,8 +110,6 @@ class ChatViewController: MessagesViewController {
             messageInputBar.inputTextView.becomeFirstResponder()
         }
 
-        loadFirstMessages()
-
         let notificationCenter = NotificationCenter.default
         notificationCenter.addObserver(self,
                                        selector: #selector(setTextDraft),
@@ -149,9 +147,8 @@ class ChatViewController: MessagesViewController {
         // this will be removed in viewWillDisappear
         navigationController?.navigationBar.addGestureRecognizer(navBarTap)
 
-        let chat = DcChat(id: chatId)
         if showCustomNavBar {
-            updateTitle(chat: chat)
+            updateTitle(chat: DcChat(id: chatId))
         }
 
         configureMessageMenu()
@@ -175,7 +172,7 @@ class ChatViewController: MessagesViewController {
                     }
                 }
                 if self.showCustomNavBar {
-                    self.updateTitle(chat: chat)
+                    self.updateTitle(chat: DcChat(id: self.chatId))
                 }
             }
         }
@@ -195,6 +192,8 @@ class ChatViewController: MessagesViewController {
             }
         }
 
+        loadFirstMessages()
+
         if RelayHelper.sharedInstance.isForwarding() {
             askToForwardMessage()
         }
@@ -233,6 +232,15 @@ class ChatViewController: MessagesViewController {
         stopTimer()
     }
 
+    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
+        coordinator.animate(alongsideTransition: { (_) -> Void in
+            if self.showCustomNavBar, let titleView = self.navigationItem.titleView as? ChatTitleView {
+                titleView.hideLocationStreamingIndicator() }},
+                            completion: { (_) -> Void in
+                                self.updateTitle(chat: DcChat(id: self.chatId)) })
+        super.viewWillTransition(to: size, with: coordinator)
+    }
+
     private func updateTitle(chat: DcChat) {
         let titleView =  ChatTitleView()
 
@@ -250,7 +258,7 @@ class ChatViewController: MessagesViewController {
             }
         }
         
-        titleView.updateTitleView(title: chat.name, subtitle: subtitle)
+        titleView.updateTitleView(title: chat.name, subtitle: subtitle, isLocationStreaming: chat.isSendingLocations)
         navigationItem.titleView = titleView
 
         let badge: InitialsBadge
@@ -595,13 +603,19 @@ class ChatViewController: MessagesViewController {
     }
 
     private func askToChatWith(email: String) {
-        confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), email),
-                          actionTitle: String.localized("start_chat"),
-                          actionHandler: { _ in
-                            self.dismiss(animated: true, completion: nil)
-                            let contactId = self.dcContext.createContact(name: "", email: email)
-                            let chatId = self.dcContext.createChat(contactId: contactId)
-                            self.coordinator?.showChat(chatId: chatId)})
+        let contactId = self.dcContext.createContact(name: "", email: email)
+        if dcContext.getChatIdByContactId(contactId: contactId) != 0 {
+            self.dismiss(animated: true, completion: nil)
+            let chatId = self.dcContext.createChatByContactId(contactId: contactId)
+            self.coordinator?.showChat(chatId: chatId)
+        } else {
+            confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), email),
+                              actionTitle: String.localized("start_chat"),
+                              actionHandler: { _ in
+                                self.dismiss(animated: true, completion: nil)
+                                let chatId = self.dcContext.createChatByContactId(contactId: contactId)
+                                self.coordinator?.showChat(chatId: chatId)})
+        }
     }
 
     private func askToDeleteMessage(id: Int) {
@@ -613,7 +627,10 @@ class ChatViewController: MessagesViewController {
 
     private func askToForwardMessage() {
         let chat = DcChat(id: self.chatId)
-        confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_forward"), chat.name),
+        if chat.isSelfTalk {
+            RelayHelper.sharedInstance.forward(to: self.chatId)
+        } else {
+            confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_forward"), chat.name),
                           actionTitle: String.localized("menu_forward"),
                           actionHandler: { _ in
                             RelayHelper.sharedInstance.forward(to: self.chatId)
@@ -621,6 +638,7 @@ class ChatViewController: MessagesViewController {
                           cancelHandler: { _ in
                             self.dismiss(animated: false, completion: nil)
                             self.coordinator?.navigateBack()})
+        }
     }
 }
 
@@ -1094,11 +1112,16 @@ extension ChatViewController: MessagesLayoutDelegate {
         let cameraAction = PhotoPickerAlertAction(title: String.localized("camera"), style: .default, handler: cameraButtonPressed(_:))
         let documentAction = UIAlertAction(title: String.localized("documents"), style: .default, handler: documentActionPressed(_:))
         let voiceMessageAction = UIAlertAction(title: String.localized("voice_message"), style: .default, handler: voiceMessageButtonPressed(_:))
+        let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
+        let locationStreamingAction = UIAlertAction(title: isLocationStreaming ? String.localized("stop_sharing_location") : String.localized("location"),
+                                                    style: isLocationStreaming ? .destructive : .default,
+                                                    handler: locationStreamingButtonPressed(_:))
 
         alert.addAction(cameraAction)
         alert.addAction(galleryAction)
         alert.addAction(documentAction)
         alert.addAction(voiceMessageAction)
+        alert.addAction(locationStreamingAction)
         alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
         self.present(alert, animated: true, completion: nil)
     }
@@ -1119,6 +1142,37 @@ extension ChatViewController: MessagesLayoutDelegate {
         coordinator?.showPhotoVideoLibrary(delegate: self)
     }
 
+    private func locationStreamingButtonPressed(_ action: UIAlertAction) {
+        let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
+        if isLocationStreaming {
+            locationStreamingFor(seconds: 0)
+        } else {
+            let alert = UIAlertController(title: String.localized("title_share_location"), message: nil, preferredStyle: .safeActionSheet)
+            addDurationSelectionAction(to: alert, key: "share_location_for_5_minutes", duration: Time.fiveMinutes)
+            addDurationSelectionAction(to: alert, key: "share_location_for_30_minutes", duration: Time.thirtyMinutes)
+            addDurationSelectionAction(to: alert, key: "share_location_for_one_hour", duration: Time.oneHour)
+            addDurationSelectionAction(to: alert, key: "share_location_for_two_hours", duration: Time.twoHours)
+            addDurationSelectionAction(to: alert, key: "share_location_for_six_hours", duration: Time.sixHours)
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+            self.present(alert, animated: true, completion: nil)
+        }
+    }
+
+    private func addDurationSelectionAction(to alert: UIAlertController, key: String, duration: Int) {
+        let action = UIAlertAction(title: String.localized(key), style: .default, handler: { _ in
+            self.locationStreamingFor(seconds: duration)
+        })
+        alert.addAction(action)
+    }
+
+    private func locationStreamingFor(seconds: Int) {
+            guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
+                return
+            }
+            self.dcContext.sendLocationsToChat(chatId: self.chatId, seconds: seconds)
+            appDelegate.locationManager.shareLocation(chatId: self.chatId, duration: seconds)
+    }
+
 }
 
 // MARK: - MessageCellDelegate
@@ -1130,26 +1184,8 @@ extension ChatViewController: MessageCellDelegate {
                 didTapAsm(msg: message, orgText: "")
             } else if let url = message.fileURL {
                 // find all other messages with same message type
-                var previousUrls: [URL] = []
-                var nextUrls: [URL] = []
-
-                var prev: Int = Int(dc_get_next_media(mailboxPointer, UInt32(message.id), -1, Int32(message.type), 0, 0))
-                while prev != 0 {
-                    let prevMessage = DcMsg(id: prev)
-                    if let url = prevMessage.fileURL {
-                        previousUrls.insert(url, at: 0)
-                    }
-                    prev = Int(dc_get_next_media(mailboxPointer, UInt32(prevMessage.id), -1, Int32(prevMessage.type), 0, 0))
-                }
-
-                var next: Int = Int(dc_get_next_media(mailboxPointer, UInt32(message.id), 1, Int32(message.type), 0, 0))
-                while next != 0 {
-                    let nextMessage = DcMsg(id: next)
-                    if let url = nextMessage.fileURL {
-                        nextUrls.insert(url, at: 0)
-                    }
-                    next = Int(dc_get_next_media(mailboxPointer, UInt32(nextMessage.id), 1, Int32(nextMessage.type), 0, 0))
-                }
+                let previousUrls: [URL] = message.previousMediaURLs()
+                let nextUrls: [URL] = message.nextMediaURLs()
 
                 // these are the files user will be able to swipe trough
                 let mediaUrls: [URL] = previousUrls + [url] + nextUrls

+ 17 - 18
deltachat-ios/Controller/ContactDetailViewController.swift

@@ -85,6 +85,7 @@ class ContactDetailViewController: UITableViewController {
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
+        updateHeader() // maybe contact name has been edited
         tableView.reloadData()
     }
 
@@ -94,10 +95,10 @@ class ContactDetailViewController: UITableViewController {
         tableView.register(ContactCell.self, forCellReuseIdentifier: ContactCell.reuseIdentifier)
         headerCell.frame = CGRect(0, 0, tableView.frame.width, ContactCell.cellHeight)
         tableView.tableHeaderView = headerCell
-
     }
 
     // MARK: - UITableViewDatasource, UITableViewDelegate
+
     override func numberOfSections(in tableView: UITableView) -> Int {
         return viewModel.numberOfSections
     }
@@ -146,7 +147,7 @@ class ContactDetailViewController: UITableViewController {
             handleCellAction(for: indexPath.row)
         case .startChat:
             let contactId = viewModel.contactId
-            askToChatWith(contactId: contactId)
+            chatWith(contactId: contactId)
         case .sharedChats:
             let chatId = viewModel.getSharedChatIdAt(indexPath: indexPath)
             coordinator?.showChat(chatId: chatId)
@@ -171,8 +172,18 @@ class ContactDetailViewController: UITableViewController {
         return Constants.defaultHeaderHeight
     }
 
-    // MARK: - actions
+    // MARK: - updates
+    private func updateHeader() {
+        headerCell.updateDetails(title: viewModel.contact.displayName, subtitle: viewModel.contact.email)
+        if let img = viewModel.contact.profileImage {
+            headerCell.setImage(img)
+        } else {
+            headerCell.setBackupImage(name: viewModel.contact.displayName, color: viewModel.contact.color)
+        }
+        headerCell.setVerified(isVerified: viewModel.contact.isVerified)
+    }
 
+    // MARK: - actions
     private func handleCellAction(for index: Int) {
         let action = viewModel.chatActionFor(row: index)
         switch action {
@@ -260,21 +271,9 @@ extension ContactDetailViewController {
         }
     }
 
-    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.dismiss(animated: true, completion: nil)
-            let chatId = Int(dc_create_chat_by_contact_id(mailboxPointer, UInt32(contactId)))
-            self.coordinator?.showChat(chatId: chatId)
-        }))
-        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
-            self.dismiss(animated: true, completion: nil)
-        }))
-        present(alert, animated: true, completion: nil)
+    private func chatWith(contactId: Int) {
+        let chatId = self.viewModel.context.createChatByContactId(contactId: contactId)
+        self.coordinator?.showChat(chatId: chatId)
     }
 
 }

+ 2 - 2
deltachat-ios/Controller/EditContactController.swift

@@ -5,8 +5,8 @@ class EditContactController: NewContactController {
     // for editing existing contacts (only
     // the name may be edited, therefore disable
     // the email field)
-    init(contactIdForUpdate: Int) {
-        super.init()
+    init(dcContext: DcContext, contactIdForUpdate: Int) {
+        super.init(dcContext: dcContext)
         title = String.localized("edit_contact")
 
         let contact = DcContact(id: contactIdForUpdate)

+ 13 - 4
deltachat-ios/Controller/GroupChatDetailViewController.swift

@@ -43,7 +43,6 @@ class GroupChatDetailViewController: UIViewController {
 
     lazy var tableView: UITableView = {
         let table = UITableView(frame: .zero, style: .grouped)
-        table.bounces = false
         table.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
         table.register(ActionCell.self, forCellReuseIdentifier: "actionCell")
         table.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
@@ -137,12 +136,12 @@ class GroupChatDetailViewController: UIViewController {
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
+        //update chat object, maybe chat name was edited
+        chat = DcChat(id: chat.id)
         updateGroupMembers()
         tableView.reloadData() // to display updates
         editBarButtonItem.isEnabled = currentUser != nil
         updateHeader()
-        //update chat object, maybe chat name was edited
-        chat = DcChat(id: chat.id)
     }
 
     // MARK: - update
@@ -156,6 +155,12 @@ class GroupChatDetailViewController: UIViewController {
             title: chat.name,
             subtitle: String.localizedStringWithFormat(String.localized("n_members"), chat.contactIds.count)
         )
+        if let img = chat.profileImage {
+            groupHeader.setImage(img)
+        } else {
+            groupHeader.setBackupImage(name: chat.name, color: chat.color)
+        }
+        groupHeader.setVerified(isVerified: chat.isVerified)
     }
 
     // MARK: - actions
@@ -248,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

+ 15 - 10
deltachat-ios/Controller/NewChatViewController.swift

@@ -291,18 +291,23 @@ class NewChatViewController: UITableViewController {
     }
 
     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
+        if dcContext.getChatIdByContactId(contactId: contactId) != 0 {
             self.dismiss(animated: true, completion: nil)
             self.coordinator?.showNewChat(contactId: contactId)
-        }))
-        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
-            self.reactivateSearchBarIfNeeded()
-        }))
-        present(alert, animated: true, completion: nil)
+        } else {
+            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.dismiss(animated: true, completion: nil)
+                self.coordinator?.showNewChat(contactId: contactId)
+            }))
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
+                self.reactivateSearchBarIfNeeded()
+            }))
+            present(alert, animated: true, completion: nil)
+        }
     }
 
     private func reactivateSearchBarIfNeeded() {

+ 5 - 3
deltachat-ios/Controller/NewContactController.swift

@@ -2,6 +2,7 @@ import UIKit
 
 class NewContactController: UITableViewController {
 
+    private let dcContext: DcContext
     weak var coordinator: EditContactCoordinatorProtocol?
     var openChatOnSave = true
 
@@ -27,7 +28,8 @@ class NewContactController: UITableViewController {
     let cells: [UITableViewCell]
 
     // for creating a new contact
-    init() {
+    init(dcContext: DcContext) {
+        self.dcContext = dcContext
         cells = [emailCell, nameCell]
         super.init(style: .grouped)
         emailCell.textField.delegate = self
@@ -74,8 +76,8 @@ class NewContactController: UITableViewController {
     }
 
     @objc func saveContactButtonPressed() {
-        let contactId = dc_create_contact(mailboxPointer, model.name, model.email)
-        let chatId = Int(dc_create_chat_by_contact_id(mailboxPointer, UInt32(contactId)))
+        let contactId = dcContext.createContact(name: model.name, email: model.email)
+        let chatId = dcContext.createChatByContactId(contactId: contactId)
         if openChatOnSave {
             coordinator?.showChat(chatId: chatId)
         } else {

+ 0 - 1
deltachat-ios/Controller/NewGroupController.swift

@@ -60,7 +60,6 @@ class NewGroupController: UITableViewController, MediaPickerDelegate {
         title = isVerifiedGroup ? String.localized("menu_new_verified_group") : String.localized("menu_new_group")
         doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
         navigationItem.rightBarButtonItem = doneButton
-        tableView.bounces = false
         doneButton.isEnabled = false
         tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
         tableView.register(ActionCell.self, forCellReuseIdentifier: "actionCell")

+ 2 - 2
deltachat-ios/Controller/QrViewController.swift

@@ -169,7 +169,7 @@ class QrViewController: UITableViewController, QrCodeReaderDelegate {
             let msg = String.localizedStringWithFormat(String.localized(state==DC_QR_ADDR ? "ask_start_chat_with" : "qrshow_x_verified"), nameAndAddress)
             let alert = UIAlertController(title: msg, message: nil, preferredStyle: .alert)
             alert.addAction(UIAlertAction(title: String.localized("start_chat"), style: .default, handler: { _ in
-                let chatId = self.dcContext.createChat(contactId: qrParsed.id)
+                let chatId = self.dcContext.createChatByContactId(contactId: qrParsed.id)
                 self.coordinator?.showChat(chatId: chatId)
             }))
             alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default, handler: nil))
@@ -311,7 +311,7 @@ class QrViewController: UITableViewController, QrCodeReaderDelegate {
     }
 
     func displayNewChat(contactId: Int) {
-        let chatId = dc_create_chat_by_contact_id(mailboxPointer, UInt32(contactId))
+        let chatId = dcContext.createChatByContactId(contactId: contactId)
         let chatVC = ChatViewController(dcContext: dcContext, chatId: Int(chatId))
 
         chatVC.hidesBottomBarWhenPushed = true

+ 18 - 9
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 {
@@ -318,7 +325,7 @@ class NewChatCoordinator: Coordinator {
     }
 
     func showNewContactController() {
-        let newContactController = NewContactController()
+        let newContactController = NewContactController(dcContext: dcContext)
         let coordinator = EditContactCoordinator(dcContext: dcContext, navigationController: navigationController)
         childCoordinators.append(coordinator)
         newContactController.coordinator = coordinator
@@ -326,7 +333,7 @@ class NewChatCoordinator: Coordinator {
     }
 
     func showNewChat(contactId: Int) {
-        let chatId = dc_create_chat_by_contact_id(mailboxPointer, UInt32(contactId))
+        let chatId = dcContext.createChatByContactId(contactId: contactId)
         showChat(chatId: Int(chatId))
     }
 
@@ -366,7 +373,7 @@ class GroupChatDetailCoordinator: Coordinator {
     }
 
     func showSingleChatEdit(contactId: Int) {
-        let editContactController = EditContactController(contactIdForUpdate: contactId)
+        let editContactController = EditContactController(dcContext: dcContext, contactIdForUpdate: contactId)
         let coordinator = EditContactCoordinator(dcContext: dcContext, navigationController: navigationController)
         childCoordinators.append(coordinator)
         editContactController.coordinator = 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
@@ -564,7 +572,7 @@ class AddGroupMembersCoordinator: Coordinator {
     }
 
     func showNewContactController() {
-        let newContactController = NewContactController()
+        let newContactController = NewContactController(dcContext: dcContext)
         newContactController.openChatOnSave = false
         let coordinator = EditContactCoordinator(dcContext: dcContext, navigationController: navigationController)
         childCoordinators.append(coordinator)
@@ -658,7 +666,7 @@ class ContactDetailCoordinator: Coordinator, ContactDetailCoordinatorProtocol {
     }
 
     func showEditContact(contactId: Int) {
-        let editContactController = EditContactController(contactIdForUpdate: contactId)
+        let editContactController = EditContactController(dcContext: dcContext, contactIdForUpdate: contactId)
         let coordinator = EditContactCoordinator(dcContext: dcContext, navigationController: navigationController)
         childCoordinators.append(coordinator)
         editContactController.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

+ 76 - 3
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)
@@ -55,10 +64,14 @@ class DcContext {
     }
 
     @discardableResult
-    func createChat(contactId: Int) -> Int {
+    func createChatByContactId(contactId: Int) -> Int {
         return Int(dc_create_chat_by_contact_id(contextPointer, UInt32(contactId)))
     }
 
+    func getChatIdByContactId(contactId: Int) -> Int {
+        return Int(dc_get_chat_id_by_contact_id(contextPointer, UInt32(contactId)))
+    }
+
     func createGroupChat(verified: Bool, name: String) -> Int {
         return Int(dc_create_group_chat(contextPointer, verified ? 1 : 0, name))
     }
@@ -221,6 +234,26 @@ class DcContext {
     func imex(what: Int32, directory: String) {
         dc_imex(contextPointer, what, directory, nil)
     }
+
+    func isSendingLocationsToChat(chatId: Int) -> Bool {
+        return dc_is_sending_locations_to_chat(contextPointer, UInt32(chatId)) == 1
+    }
+
+    func sendLocationsToChat(chatId: Int, seconds: Int) {
+        dc_send_locations_to_chat(contextPointer, UInt32(chatId), Int32(seconds))
+    }
+
+    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 {
@@ -552,6 +585,10 @@ class DcChat {
         }
         return nil
         }()
+
+    var isSendingLocations: Bool {
+        return dc_chat_is_sending_locations(chatPointer) == 1
+    }
 }
 
 class DcArray {
@@ -868,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
     }
@@ -880,6 +927,32 @@ class DcMsg: MessageType {
     func sendInChat(id: Int) {
         dc_send_msg(mailboxPointer, UInt32(id), messagePointer)
     }
+
+    func previousMediaURLs() -> [URL] {
+        var urls: [URL] = []
+        var prev: Int = Int(dc_get_next_media(mailboxPointer, UInt32(id), -1, Int32(type), 0, 0))
+        while prev != 0 {
+            let prevMessage = DcMsg(id: prev)
+            if let url = prevMessage.fileURL {
+                urls.insert(url, at: 0)
+            }
+            prev = Int(dc_get_next_media(mailboxPointer, UInt32(prevMessage.id), -1, Int32(prevMessage.type), 0, 0))
+        }
+        return urls
+    }
+
+    func nextMediaURLs() -> [URL] {
+        var urls: [URL] = []
+        var next: Int = Int(dc_get_next_media(mailboxPointer, UInt32(id), 1, Int32(type), 0, 0))
+        while next != 0 {
+            let nextMessage = DcMsg(id: next)
+            if let url = nextMessage.fileURL {
+                urls.append(url)
+            }
+            next = Int(dc_get_next_media(mailboxPointer, UInt32(nextMessage.id), 1, Int32(nextMessage.type), 0, 0))
+        }
+        return urls
+    }
 }
 
 class DcContact {

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

@@ -71,3 +71,16 @@ 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)
+            }
+        }
+    }
+}

+ 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 {

+ 1 - 0
deltachat-ios/Helper/Colors.swift

@@ -14,6 +14,7 @@ struct DcColors {
     static let defaultTextColor = UIColor.themeColor(light: .darkText, dark: .white)
     static let grayTextColor = UIColor.themeColor(light: .darkGray, dark: .lightGray)
     static let grayDateColor = UIColor.themeColor(lightHex: "999999", darkHex: "bbbbbb") // slight variations of lightGray (#aaaaaa)
+    static let middleGray = UIColor(hexString: "848ba7")
     static let secondaryTextColor = UIColor.themeColor(lightHex: "848ba7", darkHex: "a5abc0")
     static let inputFieldColor =  UIColor.themeColor(light: UIColor(red: 245 / 255, green: 245 / 255, blue: 245 / 255, alpha: 1),
                                                      dark: UIColor(red: 10 / 255, green: 10 / 255, blue: 10 / 255, alpha: 1))

+ 11 - 0
deltachat-ios/Helper/Constants.swift

@@ -11,6 +11,8 @@ struct Constants {
         static let deltachatImapPasswordKey = "__DELTACHAT_IMAP_PASSWORD_KEY__"
     }
 
+
+
     static let defaultShadow = UIImage(color: UIColor(hexString: "ff2b82"), size: CGSize(width: 1, height: 1))
     static let onlineShadow = UIImage(color: UIColor(hexString: "3ed67e"), size: CGSize(width: 1, height: 1))
 
@@ -19,3 +21,12 @@ struct Constants {
     static let defaultCellHeight: CGFloat = 48
     static let defaultHeaderHeight: CGFloat = 20
 }
+
+struct Time {
+    static let twoMinutes = 2 * 60
+    static let fiveMinutes = 5 * 60
+    static let thirtyMinutes = 30 * 6
+    static let oneHour = 60 * 60
+    static let twoHours = 2 * 60 * 60
+    static let sixHours = 6 * 60 * 60
+}

+ 132 - 0
deltachat-ios/Helper/LocationManager.swift

@@ -0,0 +1,132 @@
+import Foundation
+import CoreLocation
+
+class LocationManager: NSObject, CLLocationManagerDelegate {
+
+    let locationManager: CLLocationManager
+    let dcContext: DcContext
+    var lastLocation: CLLocation?
+
+    init(context: DcContext) {
+        dcContext = context
+        locationManager = CLLocationManager()
+        locationManager.distanceFilter = 25
+        locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
+        locationManager.allowsBackgroundLocationUpdates = true
+        locationManager.pausesLocationUpdatesAutomatically = false
+        locationManager.activityType = CLActivityType.fitness
+        super.init()
+        locationManager.delegate = self
+
+    }
+
+    func shareLocation(chatId: Int, duration: Int) {
+        dcContext.sendLocationsToChat(chatId: chatId, seconds: duration)
+        if duration > 0 {
+            locationManager.requestAlwaysAuthorization()
+        } else {
+            if !dcContext.isSendingLocationsToChat(chatId: 0) {
+                locationManager.stopUpdatingLocation()
+            }
+        }
+    }
+
+    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+        logger.debug("LOCATION: didUpdateLocations")
+
+        guard let newLocation = locations.last else {
+            logger.debug("LOCATION: new location is emtpy")
+            return
+        }
+
+        let isBetter = isBetterLocation(newLocation: newLocation, lastLocation: lastLocation)
+        logger.debug("LOCATION: isBetterLocation: \(isBetter)")
+        if isBetter {
+            if dcContext.isSendingLocationsToChat(chatId: 0) {
+                dcContext.setLocation(latitude: newLocation.coordinate.latitude,
+                                      longitude: newLocation.coordinate.longitude,
+                                      accuracy: newLocation.horizontalAccuracy)
+                lastLocation = newLocation
+            } else {
+                locationManager.stopUpdatingLocation()
+            }
+        }
+    }
+
+    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
+        if let error = error as? CLError, error.code == .denied {
+            logger.warning("LOCATION MANAGER: didFailWithError: \(error.localizedDescription)")
+           // Location updates are not authorized.
+           disableLocationStreamingInAllChats()
+           return
+        }
+    }
+
+    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
+        logger.debug("LOCATION MANAGER: didChangeAuthorization: \(status)")
+        switch status {
+        case .denied, .restricted:
+            disableLocationStreamingInAllChats()
+        case .authorizedWhenInUse:
+            logger.warning("Location streaming will only work as long as the app is in foreground.")
+            locationManager.startUpdatingLocation()
+        case .authorizedAlways:
+            locationManager.startUpdatingLocation()
+        default:
+            break
+        }
+    }
+
+    func disableLocationStreamingInAllChats() {
+        if dcContext.isSendingLocationsToChat(chatId: 0) {
+            let dcChatlist = dcContext.getChatlist(flags: 0, queryString: nil, queryId: 0)
+            for i in 0...dcChatlist.length {
+                let chatId = dcChatlist.getChatId(index: i)
+                if dcContext.isSendingLocationsToChat(chatId: chatId) {
+                    dcContext.sendLocationsToChat(chatId: chatId, seconds: 0)
+                }
+            }
+            locationManager.stopUpdatingLocation()
+        }
+    }
+
+    func isBetterLocation(newLocation: CLLocation, lastLocation: CLLocation?) -> Bool {
+        guard let lastLocation = lastLocation else {
+            return !isNewLocationOutdated(newLocation: newLocation) && hasValidAccuracy(newLocation: newLocation)
+        }
+
+        return !isNewLocationOutdated(newLocation: newLocation) &&
+            hasValidAccuracy(newLocation: newLocation) &&
+            (isMoreAccurate(newLocation: newLocation, lastLocation: lastLocation) && hasLocationChanged(newLocation: newLocation, lastLocation: lastLocation) ||
+            hasLocationSignificantlyChanged(newLocation: newLocation, lastLocation: lastLocation))
+    }
+
+    func hasValidAccuracy(newLocation: CLLocation) -> Bool {
+        return newLocation.horizontalAccuracy >= 0
+    }
+
+    func isMoreAccurate(newLocation: CLLocation, lastLocation: CLLocation) -> Bool {
+//        logger.debug("LOCATION: isMoreAccurate \(lastLocation.horizontalAccuracy - newLocation.horizontalAccuracy > 0)")
+        return lastLocation.horizontalAccuracy - newLocation.horizontalAccuracy > 0
+    }
+
+    func hasLocationChanged(newLocation: CLLocation, lastLocation: CLLocation) -> Bool {
+//        logger.debug("LOCATION: hasLocationChanged \(newLocation.distance(from: lastLocation) > 10)")
+        return newLocation.distance(from: lastLocation) > 10
+    }
+
+    func hasLocationSignificantlyChanged(newLocation: CLLocation, lastLocation: CLLocation) -> Bool {
+//        logger.debug("LOCATION: hasLocationSignificantlyChanged \(newLocation.distance(from: lastLocation) > 30)")
+        return newLocation.distance(from: lastLocation) > 30
+    }
+
+    /**
+        Locations can be cached by iOS, timestamp comparison checks if the location has been tracked within the last 5 minutes
+     */
+    func isNewLocationOutdated(newLocation: CLLocation) -> Bool {
+        let timeDelta = DateUtils.getRelativeTimeInSeconds(timeStamp: Double(newLocation.timestamp.timeIntervalSince1970))
+ //       logger.debug("LOCATION: isLocationOutdated timeDelta: \(timeDelta) -> \(Double(Time.fiveMinutes)) -> \(timeDelta < Double(Time.fiveMinutes))")
+        return timeDelta > Double(Time.fiveMinutes)
+    }
+    
+}

+ 2 - 2
deltachat-ios/Helper/Utils.swift

@@ -131,7 +131,7 @@ struct Utils {
         }
 
         do {
-            let timestamp = Int(Date().timeIntervalSince1970)
+            let timestamp = Double(Date().timeIntervalSince1970)
             let path = directory.appendingPathComponent("\(timestamp).jpg")
             try data.write(to: path!)
             return path?.relativePath
@@ -178,7 +178,7 @@ class DateUtils {
     static let day: Double = 86400
     static let year: Double = 365 * day
 
-    private static func getRelativeTimeInSeconds(timeStamp: Double) -> Double {
+    static func getRelativeTimeInSeconds(timeStamp: Double) -> Double {
         let unixTime = Double(Date().timeIntervalSince1970)
         return unixTime - timeStamp
     }

+ 5 - 0
deltachat-ios/Info.plist

@@ -42,6 +42,10 @@
 	<string>Delta Chat uses your camera to take and send photos and videos and to scan QR codes.</string>
 	<key>NSContactsUsageDescription</key>
 	<string>Delta Chat uses your contacts to show a list of email addresses you can write to. Delta Chat has no server, your contacts are not sent anywhere.</string>
+	<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
+	<string>Delta Chat needs the location permission in order to share your location for the timespan you have enabled location sharing.</string>
+	<key>NSLocationWhenInUseUsageDescription</key>
+	<string>Delta Chat needs the location permission in order to share your location for the timespan you have enabled location sharing.</string>
 	<key>NSMicrophoneUsageDescription</key>
 	<string>Delta Chat uses your microphone to record and send voice messages and videos with sound.</string>
 	<key>NSPhotoLibraryUsageDescription</key>
@@ -49,6 +53,7 @@
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>fetch</string>
+		<string>location</string>
 	</array>
 	<key>UIFileSharingEnabled</key>
 	<true/>

+ 44 - 13
deltachat-ios/View/ChatTitleView.swift

@@ -4,6 +4,7 @@ class ChatTitleView: UIView {
 
     private var titleLabel: UILabel = {
         let titleLabel = UILabel()
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
         titleLabel.backgroundColor = UIColor.clear
         titleLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
         titleLabel.textAlignment = .center
@@ -13,11 +14,26 @@ class ChatTitleView: UIView {
 
     private var subtitleLabel: UILabel = {
         let subtitleLabel = UILabel()
+        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
         subtitleLabel.font = UIFont.systemFont(ofSize: 12)
         subtitleLabel.textAlignment = .center
         return subtitleLabel
     }()
 
+    private let locationStreamingIndicator: UIImageView = {
+        let view = UIImageView()
+        view.tintColor = DcColors.checkmarkGreen
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.constraintHeightTo(28).isActive = true
+        view.constraintWidthTo(28).isActive = true
+        view.image = #imageLiteral(resourceName: "ic_location").withRenderingMode(.alwaysTemplate)
+        view.isHidden = true
+        return view
+    }()
+
+    private let paddingNaviationButtons = 120
+    private let sizeStreamingIndicator = 28
+
     init() {
         super.init(frame: .zero)
         setupSubviews()
@@ -29,25 +45,40 @@ class ChatTitleView: UIView {
     }
 
     private func setupSubviews() {
-        addSubview(titleLabel)
-        titleLabel.translatesAutoresizingMaskIntoConstraints = false
-        titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
-        titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
-        titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
-        titleLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true
+        let containerView = UIView()
+        containerView.translatesAutoresizingMaskIntoConstraints = false
+        addSubview(containerView)
+        addConstraints([ containerView.constraintAlignTopTo(self),
+                         containerView.constraintAlignBottomTo(self),
+                         containerView.constraintCenterXTo(self),
+                         containerView.constraintWidthTo(UIScreen.main.bounds.width - CGFloat(paddingNaviationButtons + sizeStreamingIndicator)) ])
 
-        addSubview(subtitleLabel)
-        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
-        subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0).isActive = true
-        subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 0).isActive = true
-        subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0).isActive = true
-        subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true
+        containerView.addSubview(titleLabel)
+        containerView.addConstraints([ titleLabel.constraintAlignLeadingTo(containerView),
+                                       titleLabel.constraintAlignTrailingTo(containerView),
+                                       titleLabel.constraintAlignTopTo(containerView) ])
+
+        containerView.addSubview(subtitleLabel)
+        containerView.addConstraints([ subtitleLabel.constraintToBottomOf(titleLabel),
+                                       subtitleLabel.constraintAlignLeadingTo(containerView),
+                                       subtitleLabel.constraintAlignTrailingTo(containerView),
+                                       subtitleLabel.constraintAlignBottomTo(containerView)])
+        addSubview(locationStreamingIndicator)
+        addConstraints([
+                         locationStreamingIndicator.constraintCenterYTo(self),
+                         locationStreamingIndicator.constraintAlignTrailingTo(self),
+                         locationStreamingIndicator.constraintToTrailingOf(containerView)])
     }
 
-    func updateTitleView(title: String, subtitle: String?, baseColor: UIColor = DcColors.defaultTextColor) {
+    func updateTitleView(title: String, subtitle: String?, baseColor: UIColor = DcColors.defaultTextColor, isLocationStreaming: Bool) {
         subtitleLabel.textColor = baseColor.withAlphaComponent(0.95)
         titleLabel.textColor = baseColor
         titleLabel.text = title
         subtitleLabel.text = subtitle
+        locationStreamingIndicator.isHidden = !isLocationStreaming
+    }
+
+    func hideLocationStreamingIndicator() {
+        locationStreamingIndicator.isHidden = true
     }
 }

+ 47 - 23
deltachat-ios/View/ContactCell.swift

@@ -7,15 +7,14 @@ 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
 
     lazy var toplineStackView: UIStackView = {
-        let stackView = UIStackView(arrangedSubviews: [titleLabel, pinnedIndicator, timeLabel])
+        let stackView = UIStackView(arrangedSubviews: [titleLabel, pinnedIndicator, timeLabel, locationStreamingIndicator])
         stackView.axis = .horizontal
         stackView.spacing = 4
         return stackView
@@ -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
     }()
 
@@ -51,7 +48,7 @@ class ContactCell: UITableViewCell {
         view.translatesAutoresizingMaskIntoConstraints = false
         view.heightAnchor.constraint(equalToConstant: 16).isActive = true
         view.widthAnchor.constraint(equalToConstant: 16).isActive = true
-        view.tintColor = UIColor(hexString: "848ba7")
+        view.tintColor = DcColors.middleGray
         view.image = #imageLiteral(resourceName: "pinned_chatlist").withRenderingMode(.alwaysTemplate)
         view.isHidden = true
         return view
@@ -60,16 +57,27 @@ class ContactCell: UITableViewCell {
     private let timeLabel: UILabel = {
         let label = UILabel()
         label.font = UIFont.systemFont(ofSize: 14)
-        label.textColor = UIColor(hexString: "848ba7")
+        label.textColor = DcColors.middleGray
         label.textAlignment = .right
         label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 2), for: NSLayoutConstraint.Axis.horizontal)
         return label
     }()
 
+    private let locationStreamingIndicator: UIImageView = {
+        let view = UIImageView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.heightAnchor.constraint(equalToConstant: 16).isActive = true
+        view.widthAnchor.constraint(equalToConstant: 16).isActive = true
+        view.tintColor = DcColors.checkmarkGreen
+        view.image = #imageLiteral(resourceName: "ic_location").withRenderingMode(.alwaysTemplate)
+        view.isHidden = true
+        return view
+    }()
+
     let subtitleLabel: UILabel = {
         let label = UILabel()
         label.font = UIFont.systemFont(ofSize: 14)
-        label.textColor = UIColor(hexString: "848ba7")
+        label.textColor = DcColors.middleGray
         label.lineBreakMode = .byTruncatingTail
         label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1), for: NSLayoutConstraint.Axis.horizontal)
         return label
@@ -77,7 +85,6 @@ class ContactCell: UITableViewCell {
 
     private let deliveryStatusIndicator: UIImageView = {
         let view = UIImageView()
-        view.tintColor = UIColor.green
         view.isHidden = true
         return view
     }()
@@ -171,7 +178,7 @@ class ContactCell: UITableViewCell {
         avatar.setName(name)
     }
 
-    func setStatusIndicators(unreadCount: Int, status: Int, visibility: Int32) {
+    func setStatusIndicators(unreadCount: Int, status: Int, visibility: Int32, isLocationStreaming: Bool) {
         if visibility==DC_CHAT_VISIBILITY_ARCHIVED {
             pinnedIndicator.isHidden = true
             unreadMessageCounter.isHidden = true
@@ -203,6 +210,8 @@ class ContactCell: UITableViewCell {
             deliveryStatusIndicator.isHidden = deliveryStatusIndicator.image == nil ? true : false
             archivedIndicator.isHidden = true
         }
+
+        locationStreamingIndicator.isHidden = !isLocationStreaming
     }
 
     func setTimeLabel(_ timestamp: Int64?) {
@@ -220,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
@@ -241,23 +258,30 @@ 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)
+            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)
             avatar.setColor(contact.color)
-            setStatusIndicators(unreadCount: 0, status: 0, visibility: 0)
+            setStatusIndicators(unreadCount: 0, status: 0, visibility: 0, isLocationStreaming: false)
         }
     }
 }

+ 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
+    }
 }

+ 6 - 2
deltachat-ios/ViewModel/ContactDetailViewModel.swift

@@ -1,6 +1,7 @@
 import UIKit
 
 protocol ContactDetailViewModelProtocol {
+    var context: DcContext { get }
     var contactId: Int { get }
     var contact: DcContact { get }
     var numberOfSections: Int { get }
@@ -18,6 +19,7 @@ protocol ContactDetailViewModelProtocol {
 class ContactDetailViewModel: ContactDetailViewModelProtocol {
 
     let context: DcContext
+
     enum ProfileSections {
         case startChat
         case attachments
@@ -38,7 +40,10 @@ class ContactDetailViewModel: ContactDetailViewModelProtocol {
 
     var contactId: Int
 
-    var contact: DcContact
+    var contact: DcContact {
+        return DcContact(id: contactId)
+    }
+
     private let chatId: Int?
     private let sharedChats: DcChatlist
     private var sections: [ProfileSections] = []
@@ -50,7 +55,6 @@ class ContactDetailViewModel: ContactDetailViewModelProtocol {
         self.context = context
         self.contactId = contactId
         self.chatId = chatId
-        self.contact = DcContact(id: contactId)
         self.sharedChats = context.getChatlist(flags: 0, queryString: nil, queryId: contactId)
 
         sections.append(.attachments)

+ 2 - 0
deltachat-ios/en.lproj/InfoPlist.strings

@@ -2,3 +2,5 @@ NSCameraUsageDescription = "Delta Chat uses your camera to take and send photos
 NSContactsUsageDescription = "Delta Chat uses your contacts to show a list of email addresses you can write to. Delta Chat has no server, your contacts are not sent anywhere.";
 NSMicrophoneUsageDescription = "Delta Chat uses your microphone to record and send voice messages and videos with sound.";
 NSPhotoLibraryUsageDescription = "Delta Chat will let you choose which photos from your library to send.";
+NSLocationAlwaysAndWhenInUseUsageDescription = "Delta Chat needs the location permission in order to share your location for the timespan you have enabled location sharing.";
+NSLocationWhenInUseUsageDescription = "Delta Chat needs the location permission in order to share your location for the timespan you have enabled location sharing.";

+ 1 - 0
deltachat-ios/en.lproj/Localizable.strings

@@ -621,3 +621,4 @@
 
 "import_contacts" = "Import device contacts";
 "import_contacts_message" = "To chat with contacts from your device open Settings and enable Contacts.";
+"stop_sharing_location" = "Stop sharing location";

+ 1 - 0
tools/untranslated.xml

@@ -4,4 +4,5 @@
     <!-- iOS specific untranslated strings -->
     <string name="import_contacts">Import device contacts</string>
     <string name="import_contacts_message">To chat with contacts from your device open Settings and enable Contacts.</string>
+    <string name="stop_sharing_location">Stop sharing location</string>
 </resources>