Parcourir la source

Merge branch 'master' into less-clicks

bjoern il y a 5 ans
Parent
commit
776d9cf580
28 fichiers modifiés avec 482 ajouts et 97 suppressions
  1. 8 4
      deltachat-ios.xcodeproj/project.pbxproj
  2. 7 0
      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. 1 1
      deltachat-ios/Controller/ChatListController.swift
  8. 51 27
      deltachat-ios/Controller/ChatViewController.swift
  9. 13 2
      deltachat-ios/Controller/ContactDetailViewController.swift
  10. 0 17
      deltachat-ios/Controller/DcNavigationController.swift
  11. 8 3
      deltachat-ios/Controller/GroupChatDetailViewController.swift
  12. 8 2
      deltachat-ios/Controller/NewChatViewController.swift
  13. 0 1
      deltachat-ios/Controller/NewGroupController.swift
  14. 31 5
      deltachat-ios/Controller/PreviewController.swift
  15. 25 10
      deltachat-ios/Coordinator/AppCoordinator.swift
  16. 42 0
      deltachat-ios/DC/Wrapper.swift
  17. 41 0
      deltachat-ios/Handler/TabBarRestorer.swift
  18. 1 0
      deltachat-ios/Helper/Colors.swift
  19. 11 0
      deltachat-ios/Helper/Constants.swift
  20. 132 0
      deltachat-ios/Helper/LocationManager.swift
  21. 2 2
      deltachat-ios/Helper/Utils.swift
  22. 5 0
      deltachat-ios/Info.plist
  23. 44 13
      deltachat-ios/View/ChatTitleView.swift
  24. 20 8
      deltachat-ios/View/ContactCell.swift
  25. 5 2
      deltachat-ios/ViewModel/ContactDetailViewModel.swift
  26. 2 0
      deltachat-ios/en.lproj/InfoPlist.strings
  27. 1 0
      deltachat-ios/en.lproj/Localizable.strings
  28. 1 0
      tools/untranslated.xml

+ 8 - 4
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 */; };
@@ -103,7 +104,6 @@
 		789E879D21D6DF86003ED1C5 /* ProgressHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789E879C21D6DF86003ED1C5 /* ProgressHud.swift */; };
 		78E45E3A21D3CFBC00D4B15E /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E45E3921D3CFBC00D4B15E /* SettingsController.swift */; };
 		78E45E3C21D3D03700D4B15E /* TextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E45E3B21D3D03700D4B15E /* TextFieldTableViewCell.swift */; };
-		78E45E3E21D3D28C00D4B15E /* DcNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E45E3D21D3D28C00D4B15E /* DcNavigationController.swift */; };
 		78E45E4421D3F14A00D4B15E /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E45E4321D3F14A00D4B15E /* UIImage+Extension.swift */; };
 		78E45E4C21D404AE00D4B15E /* CustomMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E45E4B21D404AE00D4B15E /* CustomMessageCell.swift */; };
 		78ED838321D5379000243125 /* TextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78ED838221D5379000243125 /* TextFieldCell.swift */; };
@@ -147,6 +147,7 @@
 		AEACE2DF1FB3246400DCDD78 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2DE1FB3246400DCDD78 /* Message.swift */; };
 		AEACE2E31FB32B5C00DCDD78 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2E21FB32B5C00DCDD78 /* Constants.swift */; };
 		AEACE2E51FB32E1900DCDD78 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACE2E41FB32E1900DCDD78 /* Utils.swift */; };
+		AEC67A1C241CE9E4007DDBE1 /* TabBarRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC67A1B241CE9E4007DDBE1 /* TabBarRestorer.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 +310,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>"; };
@@ -328,7 +330,6 @@
 		78E45E2121D1768900D4B15E /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = "deltachat-ios/libraries/deltachat-core/src"; sourceTree = "<group>"; };
 		78E45E3921D3CFBC00D4B15E /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
 		78E45E3B21D3D03700D4B15E /* TextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewCell.swift; sourceTree = "<group>"; };
-		78E45E3D21D3D28C00D4B15E /* DcNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DcNavigationController.swift; sourceTree = "<group>"; };
 		78E45E4321D3F14A00D4B15E /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; };
 		78E45E4B21D404AE00D4B15E /* CustomMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMessageCell.swift; sourceTree = "<group>"; };
 		78ED838221D5379000243125 /* TextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldCell.swift; sourceTree = "<group>"; };
@@ -379,6 +380,7 @@
 		AEACE2DE1FB3246400DCDD78 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
@@ -743,7 +745,6 @@
 				AE0D26FC1FB1FE88002FAFCE /* ChatListController.swift */,
 				AEACE2DC1FB323CA00DCDD78 /* ChatViewController.swift */,
 				7092474020B3869500AF8799 /* ContactDetailViewController.swift */,
-				78E45E3D21D3D28C00D4B15E /* DcNavigationController.swift */,
 				AE9DAF0C22C1215D004C9591 /* EditContactController.swift */,
 				AE52EA1F229EB9F000C586C9 /* EditGroupViewController.swift */,
 				AEE6EC472283045D00EDC689 /* EditSettingsController.swift */,
@@ -785,6 +786,7 @@
 				302B84C42396627F001C261F /* RelayHelper.swift */,
 				AE1988A423EB2FBA00B4CD5F /* Errors.swift */,
 				AEFBE23023FF09B20045327A /* TypeAlias.swift */,
+				307D822D241669C7006D2490 /* LocationManager.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -828,6 +830,7 @@
 		AE851ACB227C7A5000ED86F0 /* Handler */ = {
 			isa = PBXGroup;
 			children = (
+				AEC67A1B241CE9E4007DDBE1 /* TabBarRestorer.swift */,
 				AEE56D7C2253ADB4007DC082 /* HudHandler.swift */,
 				AE8519E92272FDCA00ED86F0 /* DeviceContactsHandler.swift */,
 			);
@@ -1142,7 +1145,6 @@
 				305962082346125100C80F33 /* MediaMessageSizeCalculator.swift in Sources */,
 				AE52EA20229EB9F000C586C9 /* EditGroupViewController.swift in Sources */,
 				70B08FCD21073B910097D3EA /* NewGroupMemberChoiceController.swift in Sources */,
-				78E45E3E21D3D28C00D4B15E /* DcNavigationController.swift in Sources */,
 				AE18F294228C602A0007B1BE /* SecuritySettingsController.swift in Sources */,
 				78ED838D21D577D000243125 /* events.swift in Sources */,
 				305961FD2346125100C80F33 /* TypingBubble.swift in Sources */,
@@ -1153,6 +1155,7 @@
 				B21005DB23383664004C70C5 /* SettingsClassicViewController.swift in Sources */,
 				305961F62346125100C80F33 /* MessageContentCell.swift in Sources */,
 				305961E42346125100C80F33 /* MessageKitDateFormatter.swift in Sources */,
+				AEC67A1C241CE9E4007DDBE1 /* TabBarRestorer.swift in Sources */,
 				305961D32346125100C80F33 /* MessagesViewController+Keyboard.swift in Sources */,
 				305961EF2346125100C80F33 /* HorizontalEdgeInsets.swift in Sources */,
 				30A4D9AE2332672700544344 /* QrInviteViewController.swift in Sources */,
@@ -1243,6 +1246,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 */,

+ 7 - 0
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?
@@ -73,12 +74,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         guard let window = window else {
             fatalError("window was nil in app delegate")
         }
+        if #available(iOS 13.0, *) {
+            window.backgroundColor = UIColor.systemBackground
+        } else {
+            window.backgroundColor = UIColor.white
+        }
         // setup deltachat core context
         //       - second param remains nil (user data for more than one mailbox)
         open()
         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


+ 1 - 1
deltachat-ios/Controller/ChatListController.swift

@@ -220,7 +220,7 @@ extension ChatListController: UITableViewDataSource, UITableViewDelegate {
 
         cell.subtitleLabel.text = result
         cell.setTimeLabel(summary.timestamp)
-        cell.setStatusIndicators(unreadCount: unreadMessages, status: summary.state, visibility: chat.visibility)
+        cell.setStatusIndicators(unreadCount: unreadMessages, status: summary.state, visibility: chat.visibility, isLocationStreaming: chat.isSendingLocations)
         return cell
     }
 

+ 51 - 27
deltachat-ios/Controller/ChatViewController.swift

@@ -65,7 +65,6 @@ class ChatViewController: MessagesViewController {
     private var showNamesAboveMessage: Bool
     var showCustomNavBar = true
     var previewView: UIView?
-    var previewController: PreviewController?
 
     var emptyStateView: PaddingLabel = {
         let view =  PaddingLabel()
@@ -150,9 +149,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()
@@ -176,7 +174,7 @@ class ChatViewController: MessagesViewController {
                     }
                 }
                 if self.showCustomNavBar {
-                    self.updateTitle(chat: chat)
+                    self.updateTitle(chat: DcChat(id: self.chatId))
                 }
             }
         }
@@ -234,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()
 
@@ -251,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
@@ -1105,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)
     }
@@ -1130,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
@@ -1141,31 +1184,12 @@ 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
-                previewController = PreviewController(currentIndex: previousUrls.count, urls: mediaUrls)
-                present(previewController!.qlController, animated: true)
+                coordinator?.showMediaGallery(currentIndex: previousUrls.count, mediaUrls: mediaUrls)
             }
         }
     }

+ 13 - 2
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
     }
@@ -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 {

+ 0 - 17
deltachat-ios/Controller/DcNavigationController.swift

@@ -1,17 +0,0 @@
-import UIKit
-import Reachability
-
-final class DcNavigationController: UINavigationController {
-
-    override func viewDidLoad() {
-        super.viewDidLoad()
-
-        if #available(iOS 11.0, *) {
-            // preferred height of navigation bar title is configured in ViewControllers
-        } else {
-            //navigationBar.setBackgroundImage(UIImage(), for: .default)
-        }
-        //navigationBar.backgroundColor = .white
-    }
-
-}

+ 8 - 3
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

+ 8 - 2
deltachat-ios/Controller/NewChatViewController.swift

@@ -39,7 +39,7 @@ class NewChatViewController: UITableViewController {
 
     // searchBar active?
     func isFiltering() -> Bool {
-        return searchController.isActive && !searchBarIsEmpty()
+        return !searchBarIsEmpty()
     }
 
     // weak var chatDisplayer: ChatDisplayer?
@@ -304,12 +304,18 @@ class NewChatViewController: UITableViewController {
                 self.coordinator?.showNewChat(contactId: contactId)
             }))
             alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: { _ in
-                self.dismiss(animated: true, completion: nil)
+                self.reactivateSearchBarIfNeeded()
             }))
             present(alert, animated: true, completion: nil)
         }
     }
 
+    private func reactivateSearchBarIfNeeded() {
+        if !searchBarIsEmpty() {
+            searchController.isActive = true
+        }
+    }
+
     private func showChatAt(row: Int) {
         if searchController.isActive {
             // edge case: when searchController is active but searchBar is empty -> filteredContacts is empty, so we fallback to contactIds

+ 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")

+ 31 - 5
deltachat-ios/Controller/PreviewController.swift

@@ -1,16 +1,42 @@
 import QuickLook
 import UIKit
 
-class PreviewController: QLPreviewControllerDataSource {
+class PreviewController: QLPreviewController {
+
     var urls: [URL]
-    var qlController: QLPreviewController
+
+    private lazy var doneButtonItem: UIBarButtonItem = {
+        let button = UIBarButtonItem(title: String.localized("done"), style: .done, target: self, action: #selector(doneButtonPressed(_:)))
+        return button
+    }()
 
     init(currentIndex: Int, urls: [URL]) {
         self.urls = urls
-        qlController = QLPreviewController()
-        qlController.dataSource = self
-        qlController.currentPreviewItemIndex = currentIndex
+        super.init(nibName: nil, bundle: nil)
+        dataSource = self
+        currentPreviewItemIndex = currentIndex
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        if navigationController != nil && isBeingPresented {
+            /* QLPreviewController comes with a done-button by default. But if is embedded in UINavigationContrller we need to set a done-button manually.
+            */
+            navigationItem.leftBarButtonItem = doneButtonItem
+        }
+    }
+
+    // MARK: - actions
+    @objc private func doneButtonPressed(_ sender: UIBarButtonItem) {
+        self.dismiss(animated: true, completion: nil)
     }
+}
+
+extension PreviewController: QLPreviewControllerDataSource {
 
     func numberOfPreviewItems(in _: QLPreviewController) -> Int {
         return urls.count

+ 25 - 10
deltachat-ios/Coordinator/AppCoordinator.swift

@@ -10,10 +10,13 @@ class AppCoordinator: NSObject, Coordinator {
     private let chatsTab = 1
     private let settingsTab = 2
 
+    private let tabBarRestorer = TabBarRestorer()
+
     private var childCoordinators: [Coordinator] = []
 
     private lazy var tabBarController: UITabBarController = {
         let tabBarController = UITabBarController()
+        tabBarController.delegate = tabBarRestorer
         tabBarController.viewControllers = [qrController, chatListController, settingsController]
         tabBarController.tabBar.tintColor = DcColors.primary
         return tabBarController
@@ -21,19 +24,19 @@ class AppCoordinator: NSObject, Coordinator {
 
     private lazy var loginController: UIViewController = {
         let accountSetupController = AccountSetupController(dcContext: dcContext, editView: false)
-        let accountSetupNav = DcNavigationController(rootViewController: accountSetupController)
-        let coordinator = AccountSetupCoordinator(dcContext: dcContext, navigationController: accountSetupNav)
+        let nav = UINavigationController(rootViewController: accountSetupController)
+        let coordinator = AccountSetupCoordinator(dcContext: dcContext, navigationController: nav)
         coordinator.onLoginSuccess = presentTabBarController
         childCoordinators.append(coordinator)
         accountSetupController.coordinator = coordinator
-        return accountSetupNav
+        return nav
     }()
 
     // MARK: viewControllers
 
     private lazy var qrController: UIViewController = {
         let controller = QrViewController(dcContext: dcContext)
-        let nav = DcNavigationController(rootViewController: controller)
+        let nav = UINavigationController(rootViewController: controller)
         let settingsImage = UIImage(named: "qr_code")
         nav.tabBarItem = UITabBarItem(title: String.localized("qr_code"), image: settingsImage, tag: qrTab)
         let coordinator = QrViewCoordinator(navigationController: nav)
@@ -44,7 +47,7 @@ class AppCoordinator: NSObject, Coordinator {
 
     private lazy var chatListController: UIViewController = {
         let controller = ChatListController(dcContext: dcContext, showArchive: false)
-        let nav = DcNavigationController(rootViewController: controller)
+        let nav = UINavigationController(rootViewController: controller)
         let settingsImage = UIImage(named: "ic_chat")
         nav.tabBarItem = UITabBarItem(title: String.localized("pref_chats"), image: settingsImage, tag: chatsTab)
         let coordinator = ChatListCoordinator(dcContext: dcContext, navigationController: nav)
@@ -55,7 +58,7 @@ class AppCoordinator: NSObject, Coordinator {
 
     private lazy var settingsController: UIViewController = {
         let controller = SettingsViewController(dcContext: dcContext)
-        let nav = DcNavigationController(rootViewController: controller)
+        let nav = UINavigationController(rootViewController: controller)
         let settingsImage = UIImage(named: "settings")
         nav.tabBarItem = UITabBarItem(title: String.localized("menu_settings"), image: settingsImage, tag: settingsTab)
         let coordinator = SettingsCoordinator(dcContext: dcContext, navigationController: nav)
@@ -77,8 +80,13 @@ class AppCoordinator: NSObject, Coordinator {
     }
 
     public func start() {
-        print(tabBarController.selectedIndex)
-        showTab(index: chatsTab)
+        let lastActiveTab = tabBarRestorer.restoreLastActiveTab()
+        if lastActiveTab == -1 {
+            // no stored tab
+            showTab(index: chatsTab)
+        } else {
+            showTab(index: lastActiveTab)
+        }
     }
 
     func showTab(index: Int) {
@@ -414,7 +422,7 @@ class GroupChatDetailCoordinator: Coordinator {
         }
         previewController = PreviewController(currentIndex: 0, urls: mediaUrls)
         if let previewController = previewController {
-            navigationController.pushViewController(previewController.qlController, animated: true)
+            navigationController.pushViewController(previewController, animated: true)
         }
     }
 
@@ -523,6 +531,13 @@ class ChatViewCoordinator: NSObject, Coordinator {
     func showPhotoVideoLibrary(delegate: MediaPickerDelegate) {
         mediaPicker.showPhotoVideoLibrary(delegate: delegate)
     }
+
+    func showMediaGallery(currentIndex: Int, mediaUrls urls: [URL]) {
+        let betterPreviewController = PreviewController(currentIndex: currentIndex, urls: urls)
+        let nav = UINavigationController(rootViewController: betterPreviewController)
+
+        navigationController.present(nav, animated: true)
+    }
 }
 
 class NewGroupAddMembersCoordinator: Coordinator {
@@ -670,7 +685,7 @@ class ContactDetailCoordinator: Coordinator, ContactDetailCoordinatorProtocol {
         }
         previewController = PreviewController(currentIndex: 0, urls: mediaUrls)
         if let previewController = previewController {
-            navigationController.pushViewController(previewController.qlController, animated: true)
+            navigationController.pushViewController(previewController, animated: true)
         }
     }
 

+ 42 - 0
deltachat-ios/DC/Wrapper.swift

@@ -225,6 +225,18 @@ 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)
+    }
 }
 
 class DcConfig {
@@ -556,6 +568,10 @@ class DcChat {
         }
         return nil
         }()
+
+    var isSendingLocations: Bool {
+        return dc_chat_is_sending_locations(chatPointer) == 1
+    }
 }
 
 class DcArray {
@@ -884,6 +900,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 {

+ 41 - 0
deltachat-ios/Handler/TabBarRestorer.swift

@@ -0,0 +1,41 @@
+import UIKit
+
+class TabBarRestorer: NSObject, UITabBarControllerDelegate {
+
+    private let lastActiveTabKey = "last_active_tab"
+    private let offsetKey = 10
+
+    // UserDefaults returns 0 by default which conflicts with tab 0 -> therefore we map our tab indexes by adding an offsetKey
+
+    private enum Tab: Int {
+        case qrTab = 10
+        case chatTab = 11
+        case settingsTab = 12
+        case firstLaunch = 0
+    }
+
+    func restoreLastActiveTab() -> Int {
+
+        let restoredTab = UserDefaults.standard.integer(forKey: lastActiveTabKey)
+
+        guard let lastTab = Tab(rawValue: restoredTab) else {
+            safe_fatalError("invalid restored tab")
+            return -1
+        }
+
+        switch lastTab {
+        case .qrTab, .chatTab, .settingsTab:
+            return lastTab.rawValue - offsetKey
+        case .firstLaunch:
+            return -1
+        }
+    }
+
+    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
+        let activeTab = tabBarController.selectedIndex + offsetKey
+        UserDefaults.standard.set(activeTab, forKey: lastActiveTabKey)
+        UserDefaults.standard.synchronize()
+    }
+
+
+}

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

+ 20 - 8
deltachat-ios/View/ContactCell.swift

@@ -15,7 +15,7 @@ class ContactCell: UITableViewCell {
     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
@@ -51,7 +51,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 +60,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 +88,6 @@ class ContactCell: UITableViewCell {
 
     private let deliveryStatusIndicator: UIImageView = {
         let view = UIImageView()
-        view.tintColor = UIColor.green
         view.isHidden = true
         return view
     }()
@@ -171,7 +181,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 +213,8 @@ class ContactCell: UITableViewCell {
             deliveryStatusIndicator.isHidden = deliveryStatusIndicator.image == nil ? true : false
             archivedIndicator.isHidden = true
         }
+
+        locationStreamingIndicator.isHidden = !isLocationStreaming
     }
 
     func setTimeLabel(_ timestamp: Int64?) {
@@ -250,14 +262,14 @@ class ContactCell: UITableViewCell {
             }
             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):
             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)
         }
     }
 }

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

@@ -19,6 +19,7 @@ protocol ContactDetailViewModelProtocol {
 class ContactDetailViewModel: ContactDetailViewModelProtocol {
 
     let context: DcContext
+
     enum ProfileSections {
         case startChat
         case attachments
@@ -39,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] = []
@@ -51,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>