Procházet zdrojové kódy

Merge pull request #1016 from deltachat/staging-area

Staging area
cyBerta před 4 roky
rodič
revize
2abeaf332f

+ 25 - 1
deltachat-ios.xcodeproj/project.pbxproj

@@ -21,6 +21,12 @@
 		302E1BB4252B5AB4008F4264 /* PlayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E1BB3252B5AB4008F4264 /* PlayButtonView.swift */; };
 		30349291256441E200A523D0 /* QuotePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30349290256441E200A523D0 /* QuotePreview.swift */; };
 		303492952565AABC00A523D0 /* DraftModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492942565AABC00A523D0 /* DraftModel.swift */; };
+		3034929F25752FC800A523D0 /* MediaPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3034929E25752FC800A523D0 /* MediaPreview.swift */; };
+		303492A5257546B400A523D0 /* DraftPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492A4257546B400A523D0 /* DraftPreview.swift */; };
+		303492AD2577CAC300A523D0 /* FileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492AC2577CAC300A523D0 /* FileView.swift */; };
+		303492B32577E40700A523D0 /* DocumentPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492B22577E40700A523D0 /* DocumentPreview.swift */; };
+		303492CB257A814200A523D0 /* DraftArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492CA257A814200A523D0 /* DraftArea.swift */; };
+		303492CF2587C2DC00A523D0 /* ChatInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492CE2587C2DC00A523D0 /* ChatInputBar.swift */; };
 		304219D3243F588500516852 /* DcCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 304219D1243F588500516852 /* DcCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304219D82440734A00516852 /* DcMsg+Extension.swift */; };
 		304F5E44244F571C00462538 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A9FB14A1FB061E2001FEA36 /* Assets.xcassets */; };
@@ -217,6 +223,12 @@
 		302E1BB3252B5AB4008F4264 /* PlayButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlayButtonView.swift; path = "deltachat-ios/Chat/Views/PlayButtonView.swift"; sourceTree = SOURCE_ROOT; };
 		30349290256441E200A523D0 /* QuotePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePreview.swift; sourceTree = "<group>"; };
 		303492942565AABC00A523D0 /* DraftModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftModel.swift; sourceTree = "<group>"; };
+		3034929E25752FC800A523D0 /* MediaPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreview.swift; sourceTree = "<group>"; };
+		303492A4257546B400A523D0 /* DraftPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftPreview.swift; sourceTree = "<group>"; };
+		303492AC2577CAC300A523D0 /* FileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileView.swift; sourceTree = "<group>"; };
+		303492B22577E40700A523D0 /* DocumentPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPreview.swift; sourceTree = "<group>"; };
+		303492CA257A814200A523D0 /* DraftArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftArea.swift; sourceTree = "<group>"; };
+		303492CE2587C2DC00A523D0 /* ChatInputBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputBar.swift; sourceTree = "<group>"; };
 		304219D1243F588500516852 /* DcCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DcCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		304219D82440734A00516852 /* DcMsg+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DcMsg+Extension.swift"; sourceTree = "<group>"; };
 		3052C609253F082E007D13EA /* MessageLabelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLabelDelegate.swift; sourceTree = "<group>"; };
@@ -546,6 +558,13 @@
 				3052C609253F082E007D13EA /* MessageLabelDelegate.swift */,
 				3052C60D253F088E007D13EA /* DetectorType.swift */,
 				30653080254358B10093E196 /* QuoteView.swift */,
+				3034929E25752FC800A523D0 /* MediaPreview.swift */,
+				30349290256441E200A523D0 /* QuotePreview.swift */,
+				303492A4257546B400A523D0 /* DraftPreview.swift */,
+				303492AC2577CAC300A523D0 /* FileView.swift */,
+				303492B22577E40700A523D0 /* DocumentPreview.swift */,
+				303492CA257A814200A523D0 /* DraftArea.swift */,
+				303492CE2587C2DC00A523D0 /* ChatInputBar.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -780,7 +799,6 @@
 				AEFBE22E23FEF23D0045327A /* ProviderInfoCell.swift */,
 				AED62BCD247687E6009E220D /* LocationStreamingIndicator.swift */,
 				30F4BFED252E3E020006B9B3 /* PaddingTextView.swift */,
-				30349290256441E200A523D0 /* QuotePreview.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1160,12 +1178,14 @@
 				78ED839421D5AF8A00243125 /* QrCodeView.swift in Sources */,
 				3059620E234614E700C80F33 /* DcContact+Extension.swift in Sources */,
 				AED423D7249F580700B6B2BB /* BlockedContactsViewController.swift in Sources */,
+				303492B32577E40700A523D0 /* DocumentPreview.swift in Sources */,
 				3008CB7424F9436C00E6A617 /* AudioPlayerView.swift in Sources */,
 				AED62BCE247687E6009E220D /* LocationStreamingIndicator.swift in Sources */,
 				AE9DAF0F22C278C6004C9591 /* ChatTitleView.swift in Sources */,
 				AE4AEE3522B1030D000AA495 /* PreviewController.swift in Sources */,
 				7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
 				304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
+				303492CF2587C2DC00A523D0 /* ChatInputBar.swift in Sources */,
 				AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
 				78E45E4421D3F14A00D4B15E /* UIImage+Extension.swift in Sources */,
 				30734326249A280B00BF9AD1 /* MediaQualityController.swift in Sources */,
@@ -1181,6 +1201,7 @@
 				AEC67A1C241CE9E4007DDBE1 /* AppStateRestorer.swift in Sources */,
 				3008CB7224F93EB900E6A617 /* AudioMessageCell.swift in Sources */,
 				302E1BB4252B5AB4008F4264 /* PlayButtonView.swift in Sources */,
+				303492AD2577CAC300A523D0 /* FileView.swift in Sources */,
 				7AE0A5491FC42F65005ECB4B /* NewChatViewController.swift in Sources */,
 				AE77838F23E4276D0093EABD /* ContactCellViewModel.swift in Sources */,
 				B20462E62440C99600367A57 /* SettingsAutodelSetController.swift in Sources */,
@@ -1189,6 +1210,7 @@
 				30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */,
 				302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
 				AE77838D23E32ED20093EABD /* ContactDetailViewModel.swift in Sources */,
+				303492CB257A814200A523D0 /* DraftArea.swift in Sources */,
 				AEE6EC3F2282C59C00EDC689 /* GroupMembersViewController.swift in Sources */,
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
 				AEE700252438E0E500D6992E /* ProgressAlertHandler.swift in Sources */,
@@ -1216,10 +1238,12 @@
 				3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */,
 				302B84CE2397F6CD001C261F /* URL+Extension.swift in Sources */,
 				7A9FB1441FB061E2001FEA36 /* AppDelegate.swift in Sources */,
+				3034929F25752FC800A523D0 /* MediaPreview.swift in Sources */,
 				AE76E5EE242BF2EA003CF461 /* WelcomeViewController.swift in Sources */,
 				3052C60A253F082E007D13EA /* MessageLabelDelegate.swift in Sources */,
 				AE0AA9562478191900D42A7F /* GridCollectionViewFlowLayout.swift in Sources */,
 				AEA0F6A124474146009F887B /* ProfileInfoViewController.swift in Sources */,
+				303492A5257546B400A523D0 /* DraftPreview.swift in Sources */,
 				305961D02346125100C80F33 /* NSAttributedString+Extensions.swift in Sources */,
 				302B84C72396770B001C261F /* RelayHelper.swift in Sources */,
 				305961CF2346125100C80F33 /* UIColor+Extensions.swift in Sources */,

+ 148 - 73
deltachat-ios/Chat/ChatViewController.swift

@@ -17,6 +17,7 @@ class ChatViewController: UITableViewController {
     var msgChangedObserver: Any?
     var incomingMsgObserver: Any?
     var ephemeralTimerModifiedObserver: Any?
+    var dismissCancelled = false
 
     lazy var isGroupChat: Bool = {
         return dcContext.getChat(chatId: chatId).isGroup
@@ -28,12 +29,13 @@ class ChatViewController: UITableViewController {
     }()
 
     /// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
-    open var messageInputBar = InputBarAccessoryView()
+    open var messageInputBar = ChatInputBar()
 
-    lazy var quotePreview: QuotePreview = {
-        let view = QuotePreview()
-        view.delegate = self
+    lazy var draftArea: DraftArea = {
+        let view = DraftArea()
         view.translatesAutoresizingMaskIntoConstraints = false
+        view.delegate = self
+        view.inputBarAccessoryView = messageInputBar
         return view
     }()
 
@@ -151,11 +153,10 @@ class ChatViewController: UITableViewController {
         if !disableWriting {
             configureMessageInputBar()
             draft.parse(draftMsg: dcContext.getDraft(chatId: chatId))
-            messageInputBar.inputTextView.text = draft.draftText
+            messageInputBar.inputTextView.text = draft.text
             configureDraftArea(draft: draft)
         }
 
-
         let notificationCenter = NotificationCenter.default
         notificationCenter.addObserver(self,
                                        selector: #selector(saveDraft),
@@ -163,17 +164,15 @@ class ChatViewController: UITableViewController {
                                        object: nil)
         notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
         prepareContextMenu()
-
-
     }
 
     @objc func keyboardWillShow(_ notification: Notification) {
-        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
-            if keyboardSize.height > tableView.inputAccessoryView?.frame.height ?? 0 {
-                if self.isLastRowVisible() {
-                    DispatchQueue.main.async { [weak self] in
-                        self?.scrollToBottom(animated: true)
-                    }
+        if self.isLastRowVisible() {
+            DispatchQueue.main.async { [weak self] in
+                if self?.messageInputBar.keyboardHeight ?? 0 > 0 {
+                    self?.scrollToBottom(animated: true)
+                } else { // inputbar height increased, probably because of draft area changes
+                    self?.scrollToBottom(animated: false)
                 }
             }
         }
@@ -206,7 +205,12 @@ class ChatViewController: UITableViewController {
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
-        self.tableView.becomeFirstResponder()
+        if dismissCancelled {
+            self.dismissCancelled = false
+        } else {
+            self.tableView.becomeFirstResponder()
+        }
+
         // this will be removed in viewWillDisappear
         navigationController?.navigationBar.addGestureRecognizer(navBarTap)
 
@@ -214,6 +218,18 @@ class ChatViewController: UITableViewController {
             updateTitle(chat: dcContext.getChat(chatId: chatId))
         }
 
+        loadMessages()
+
+        if RelayHelper.sharedInstance.isForwarding() {
+            askToForwardMessage()
+        }
+
+    }
+
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
+        AppStateRestorer.shared.storeLastActiveChat(chatId: chatId)
+
         let nc = NotificationCenter.default
         msgChangedObserver = nc.addObserver(
             forName: dcNotificationChanged,
@@ -263,17 +279,6 @@ class ChatViewController: UITableViewController {
             self.updateTitle(chat: self.dcContext.getChat(chatId: self.chatId))
         }
 
-        loadMessages()
-
-        if RelayHelper.sharedInstance.isForwarding() {
-            askToForwardMessage()
-        }
-    }
-
-    override func viewDidAppear(_ animated: Bool) {
-        super.viewDidAppear(animated)
-        AppStateRestorer.shared.storeLastActiveChat(chatId: chatId)
-
         // things that do not affect the chatview
         // and are delayed after the view is displayed
         DispatchQueue.global(qos: .background).async { [weak self] in
@@ -288,10 +293,12 @@ class ChatViewController: UITableViewController {
 
         // the navigationController will be used when chatDetail is pushed, so we have to remove that gestureRecognizer
         navigationController?.navigationBar.removeGestureRecognizer(navBarTap)
+        dismissCancelled = true
     }
 
     override func viewDidDisappear(_ animated: Bool) {
         super.viewDidDisappear(animated)
+        dismissCancelled = false
         AppStateRestorer.shared.resetLastActiveChat()
         saveDraft()
         let nc = NotificationCenter.default
@@ -394,9 +401,9 @@ class ChatViewController: UITableViewController {
     }
 
     private func configureDraftArea(draft: DraftModel) {
-        quotePreview.configure(draft: draft)
+        draftArea.configure(draft: draft)
         // setStackViewItems recalculates the proper messageInputBar height
-        messageInputBar.setStackViewItems([quotePreview], forStack: .top, animated: true)
+        messageInputBar.setStackViewItems([draftArea], forStack: .top, animated: true)
     }
 
     override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
@@ -619,6 +626,9 @@ class ChatViewController: UITableViewController {
         configureInputBarItems()
     }
 
+    func evaluateInputBar(draft: DraftModel) {
+        messageInputBar.sendButton.isEnabled = draft.canSend()
+    }
 
     private func configureInputBarItems() {
 
@@ -638,6 +648,7 @@ class ChatViewController: UITableViewController {
         messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
         messageInputBar.sendButton.setSize(CGSize(width: 40, height: 40), animated: false)
         messageInputBar.padding = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 12)
+        messageInputBar.shouldManageSendButtonEnabledState = false
 
         let leftItems = [
             InputBarButtonItem()
@@ -901,38 +912,72 @@ class ChatViewController: UITableViewController {
         }
     }
 
-    private func sendImage(_ image: UIImage, message: String? = nil) {
+    private func stageDocument(url: NSURL) {
+        self.draft.setAttachment(viewType: DC_MSG_FILE, path: url.relativePath)
+        self.configureDraftArea(draft: self.draft)
+        self.messageInputBar.inputTextView.becomeFirstResponder()
+    }
+
+    private func stageVideo(url: NSURL) {
+        DispatchQueue.main.async {
+            self.draft.setAttachment(viewType: DC_MSG_VIDEO, path: url.relativePath)
+            self.configureDraftArea(draft: self.draft)
+            self.messageInputBar.inputTextView.becomeFirstResponder()
+        }
+    }
+
+    private func stageImage(url: NSURL) {
+        if url.pathExtension == "gif" {
+            stageAnimatedImage(url: url)
+        } else if let data = try? Data(contentsOf: url as URL),
+                  let image = UIImage(data: data) {
+            stageImage(image)
+        }
+    }
+
+    private func stageAnimatedImage(url: NSURL) {
         DispatchQueue.global().async {
-            if let path = DcUtils.saveImage(image: image) {
-                self.sendImageMessage(viewType: DC_MSG_IMAGE, image: image, filePath: path)
+            if let path = url.path,
+               let result = SDAnimatedImage(contentsOfFile: path),
+               let animatedImageData = result.animatedImageData,
+               let pathInDocDir = DcUtils.saveImage(data: animatedImageData, suffix: "gif") {
+                DispatchQueue.main.async {
+                    self.draft.setAttachment(viewType: DC_MSG_GIF, path: pathInDocDir)
+                    self.configureDraftArea(draft: self.draft)
+                    self.messageInputBar.inputTextView.becomeFirstResponder()
+                }
+            }
+        }
+    }
+
+    private func stageImage(_ image: UIImage) {
+        DispatchQueue.global().async {
+            if let pathInDocDir = DcUtils.saveImage(image: image) {
+                DispatchQueue.main.async {
+                    self.draft.setAttachment(viewType: DC_MSG_IMAGE, path: pathInDocDir)
+                    self.configureDraftArea(draft: self.draft)
+                    self.messageInputBar.inputTextView.becomeFirstResponder()
+                }
             }
         }
     }
 
-    private func sendAnimatedImage(url: NSURL) {
-        if let path = url.path {
-            let result = SDAnimatedImage(contentsOfFile: path)
-            if let result = result,
-                let animatedImageData = result.animatedImageData,
-                let pathInDocDir = DcUtils.saveImage(data: animatedImageData, suffix: "gif") {
-                self.sendImageMessage(viewType: DC_MSG_GIF, image: result, filePath: pathInDocDir)
+    private func sendImage(_ image: UIImage, message: String? = nil) {
+        DispatchQueue.global().async {
+            if let path = DcUtils.saveImage(image: image) {
+                self.sendAttachmentMessage(viewType: DC_MSG_IMAGE, filePath: path, message: message)
             }
         }
     }
 
-    private func sendImageMessage(viewType: Int32, image: UIImage, filePath: String, message: String? = nil) {
+    private func sendAttachmentMessage(viewType: Int32, filePath: String, message: String? = nil, quoteMessage: DcMsg? = nil) {
         let msg = DcMsg(viewType: viewType)
         msg.setFile(filepath: filePath)
         msg.text = (message ?? "").isEmpty ? nil : message
-        msg.sendInChat(id: self.chatId)
-    }
-
-    private func sendDocumentMessage(url: NSURL) {
-        DispatchQueue.global().async {
-            let msg = DcMsg(viewType: DC_MSG_FILE)
-            msg.setFile(filepath: url.relativePath, mimeType: nil)
-            msg.sendInChat(id: self.chatId)
+        if quoteMessage != nil {
+            msg.quoteMessage = quoteMessage
         }
+        msg.sendInChat(id: self.chatId)
     }
 
     private func sendVoiceMessage(url: NSURL) {
@@ -943,23 +988,6 @@ class ChatViewController: UITableViewController {
         }
     }
 
-    private func sendVideo(url: NSURL) {
-        DispatchQueue.global().async {
-            let msg = DcMsg(viewType: DC_MSG_VIDEO)
-            msg.setFile(filepath: url.relativePath, mimeType: "video/mp4")
-            msg.sendInChat(id: self.chatId)
-        }
-    }
-
-    private func sendImage(url: NSURL) {
-        if url.pathExtension == "gif" {
-            sendAnimatedImage(url: url)
-        } else if let data = try? Data(contentsOf: url as URL),
-            let image = UIImage(data: data) {
-            sendImage(image)
-        }
-    }
-
     // MARK: - Context menu
     private func prepareContextMenu() {
         UIMenuController.shared.menuItems = [
@@ -1132,15 +1160,15 @@ extension ChatViewController: BaseMessageCellDelegate {
 // MARK: - MediaPickerDelegate
 extension ChatViewController: MediaPickerDelegate {
     func onVideoSelected(url: NSURL) {
-        sendVideo(url: url)
+        stageVideo(url: url)
     }
 
     func onImageSelected(url: NSURL) {
-        sendImage(url: url)
+        stageImage(url: url)
     }
 
     func onImageSelected(image: UIImage) {
-        sendImage(image)
+        stageImage(image)
     }
 
     func onVoiceMessageRecorded(url: NSURL) {
@@ -1148,7 +1176,7 @@ extension ChatViewController: MediaPickerDelegate {
     }
 
     func onDocumentSelected(url: NSURL) {
-        sendDocumentMessage(url: url)
+        stageDocument(url: url)
     }
 
 }
@@ -1158,25 +1186,72 @@ extension ChatViewController: InputBarAccessoryViewDelegate {
     func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
         let trimmedText = text.replacingOccurrences(of: "\u{FFFC}", with: "", options: .literal, range: nil)
             .trimmingCharacters(in: .whitespacesAndNewlines)
-        if inputBar.inputTextView.images.isEmpty {
+        if let filePath = draft.attachment, let viewType = draft.viewType {
+            switch viewType {
+            case DC_MSG_GIF, DC_MSG_IMAGE, DC_MSG_FILE, DC_MSG_VIDEO:
+                self.sendAttachmentMessage(viewType: viewType, filePath: filePath, message: trimmedText, quoteMessage: draft.quoteMessage)
+            default:
+                logger.warning("Unsupported viewType for drafted messages.")
+            }
+        } else if inputBar.inputTextView.images.isEmpty {
             self.sendTextMessage(text: trimmedText, quoteMessage: draft.quoteMessage)
-            self.quotePreview.cancel()
         } else {
             // only 1 attachment allowed for now, thus it takes the first one
             self.sendImage(inputBar.inputTextView.images[0], message: trimmedText)
         }
         inputBar.inputTextView.text = String()
         inputBar.inputTextView.attributedText = nil
+        draftArea.cancel()
     }
 
     func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String) {
-        draft.draftText = text
+        draft.text = text
+        evaluateInputBar(draft: draft)
     }
 }
 
-extension ChatViewController: QuotePreviewDelegate {
-    func onCancel() {
+// MARK: - DraftPreviewDelegate
+extension ChatViewController: DraftPreviewDelegate {
+    func onCancelQuote() {
         draft.setQuote(quotedMsg: nil)
         configureDraftArea(draft: draft)
     }
+
+    func onCancelAttachment() {
+        draft.setAttachment(viewType: nil, path: nil, mimetype: nil)
+        configureDraftArea(draft: draft)
+        evaluateInputBar(draft: draft)
+    }
+
+    func onAttachmentAdded() {
+        evaluateInputBar(draft: draft)
+    }
+
+    func onAttachmentTapped() {
+        if let attachmentPath = draft.attachment {
+            let attachmentURL = URL(fileURLWithPath: attachmentPath, isDirectory: false)
+            let previewController = PreviewController(type: .single(attachmentURL))
+            if #available(iOS 13.0, *), draft.viewType == DC_MSG_IMAGE || draft.viewType == DC_MSG_VIDEO {
+                previewController.setEditing(true, animated: true)
+                previewController.delegate = self
+            }
+            let nav = UINavigationController(rootViewController: previewController)
+            nav.modalPresentationStyle = .fullScreen
+            navigationController?.present(nav, animated: true)
+        }
+    }
+}
+
+extension ChatViewController: QLPreviewControllerDelegate {
+    @available(iOS 13.0, *)
+    func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
+        return .updateContents
+    }
+
+    func previewController(_ controller: QLPreviewController, didUpdateContentsOf previewItem: QLPreviewItem) {
+        DispatchQueue.main.async { [weak self] in
+            guard let self = self else { return }
+            self.draftArea.reload(draft: self.draft)
+        }
+    }
 }

+ 26 - 5
deltachat-ios/Chat/DraftModel.swift

@@ -5,7 +5,10 @@ import DcCore
 public class DraftModel {
     var quoteMessage: DcMsg?
     var quoteText: String?
-    var draftText: String?
+    var text: String?
+    var attachment: String?
+    var attachmentMimeType: String?
+    var viewType: Int32?
     let chatId: Int
 
     public init(chatId: Int) {
@@ -13,9 +16,14 @@ public class DraftModel {
     }
 
     public func parse(draftMsg: DcMsg?) {
-        draftText = draftMsg?.text
+        text = draftMsg?.text
         quoteText = draftMsg?.quoteText
         quoteMessage = draftMsg?.quoteMessage
+        attachment = draftMsg?.fileURL?.relativePath
+        if let viewType = draftMsg?.type {
+            self.viewType = Int32(viewType)
+        }
+        attachmentMimeType = draftMsg?.filemime
     }
 
     public func setQuote(quotedMsg: DcMsg?) {
@@ -31,17 +39,30 @@ public class DraftModel {
         }
     }
 
+    public func setAttachment(viewType: Int32?, path: String?, mimetype: String? = nil) {
+        attachment = path
+        self.viewType = viewType
+        attachmentMimeType = mimetype
+    }
+
     public func save(context: DcContext) {
-        if draftText == nil && quoteMessage == nil {
+        if text == nil && quoteMessage == nil {
             context.setDraft(chatId: chatId, message: nil)
             return
         }
 
-        let draftMessage = DcMsg(viewType: DC_MSG_TEXT)
-        draftMessage.text = draftText
+        let draftMessage = DcMsg(viewType: viewType ?? DC_MSG_TEXT)
+        draftMessage.text = text
         if quoteMessage != nil {
             draftMessage.quoteMessage = quoteMessage
         }
+        if attachment != nil {
+            draftMessage.setFile(filepath: attachment, mimeType: attachmentMimeType)
+        }
         context.setDraft(chatId: chatId, message: draftMessage)
     }
+
+    public func canSend() -> Bool {
+        return !(text?.isEmpty ?? true) || attachment != nil
+    }
 }

+ 9 - 90
deltachat-ios/Chat/Views/Cells/FileTextCell.swift

@@ -5,70 +5,11 @@ import SDWebImage
 
 class FileTextCell: BaseMessageCell {
 
-    private lazy var defaultImage: UIImage = {
-        let image = UIImage(named: "ic_attach_file_36pt")
-        return image!
-    }()
-
-    private var imageWidthConstraint: NSLayoutConstraint?
-    private var imageHeightConstraint: NSLayoutConstraint?
     private var spacer: NSLayoutConstraint?
 
-    private var horizontalLayout: Bool {
-        set {
-            if newValue {
-                fileStackView.axis = .horizontal
-                imageWidthConstraint?.isActive = true
-                imageHeightConstraint?.isActive = true
-                fileStackView.alignment = .center
-            } else {
-                fileStackView.axis = .vertical
-                imageWidthConstraint?.isActive = false
-                imageHeightConstraint?.isActive = false
-                fileStackView.alignment = .leading
-            }
-        }
-        get {
-            return fileStackView.axis == .horizontal
-        }
-    }
-
-    private lazy var fileStackView: UIStackView = {
-        let stackView = UIStackView(arrangedSubviews: [fileImageView, fileMetadataStackView])
-        stackView.axis = .horizontal
-        stackView.spacing = 6
-        return stackView
-    }()
-
-    private lazy var fileImageView: UIImageView = {
-        let imageView = UIImageView()
-        imageView.contentMode = .scaleAspectFit
-        return imageView
-    }()
-
-    private lazy var fileMetadataStackView: UIStackView = {
-        let stackView = UIStackView(arrangedSubviews: [fileTitle, fileSubtitle])
-        stackView.axis = .vertical
-        stackView.translatesAutoresizingMaskIntoConstraints = false
-        stackView.clipsToBounds = true
-        return stackView
-    }()
-
-    private lazy var fileTitle: UILabel = {
-        let title = UILabel()
-        title.font = UIFont.preferredItalicFont(for: .body)
-        title.translatesAutoresizingMaskIntoConstraints = false
-        title.numberOfLines = 3
-        title.lineBreakMode = .byCharWrapping
-        return title
-    }()
-
-    private lazy var fileSubtitle: UILabel = {
-        let subtitle = UILabel()
-        subtitle.font = UIFont.preferredItalicFont(for: .caption2)
-        subtitle.translatesAutoresizingMaskIntoConstraints = false
-        subtitle.numberOfLines = 1
-        return subtitle
+    private lazy var fileView: FileView = {
+        let view = FileView()
+        return view
     }()
 
     override func setupSubviews() {
@@ -76,17 +17,16 @@ class FileTextCell: BaseMessageCell {
         let spacerView = UIView()
         spacer = spacerView.constraintHeightTo(8, priority: .defaultHigh)
         spacer?.isActive = true
-        mainContentView.addArrangedSubview(fileStackView)
+        mainContentView.addArrangedSubview(fileView)
         mainContentView.addArrangedSubview(spacerView)
         mainContentView.addArrangedSubview(messageLabel)
-        imageWidthConstraint = fileImageView.constraintWidthTo(50)
-        imageHeightConstraint = fileImageView.constraintHeightTo(50 * 1.3, priority: .defaultLow)
-        horizontalLayout = true
+        fileView.horizontalLayout = true
         mainContentViewHorizontalPadding = 12
     }
 
     override func prepareForReuse() {
-        fileImageView.image = nil
+        super.prepareForReuse()
+        fileView.prepareForReuse()
     }
 
     override func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
@@ -96,30 +36,9 @@ class FileTextCell: BaseMessageCell {
         } else {
             spacer?.isActive = false
         }
-        if let url = msg.fileURL {
-            generateThumbnailFor(url: url, placeholder: defaultImage)
-        } else {
-            fileImageView.image = defaultImage
-            horizontalLayout = true
-        }
-        fileTitle.text = msg.filename
-        fileSubtitle.text = msg.getPrettyFileSize()
+        
+        fileView.configure(message: msg)
         super.update(msg: msg, messageStyle: messageStyle, isAvatarVisible: isAvatarVisible, isGroup: isGroup)
     }
-
-    private func generateThumbnailFor(url: URL, placeholder: UIImage?) {
-        if let thumbnail = ThumbnailCache.shared.restoreImage(key: url.absoluteString) {
-            fileImageView.image = thumbnail
-            horizontalLayout = false
-        } else if let pdfThumbnail = DcUtils.thumbnailFromPdf(withUrl: url) {
-            fileImageView.image = pdfThumbnail
-            horizontalLayout = false
-            ThumbnailCache.shared.storeImage(image: pdfThumbnail, key: url.absoluteString)
-        } else {
-            let controller = UIDocumentInteractionController(url: url)
-            fileImageView.image = controller.icons.first ?? placeholder
-            horizontalLayout = true
-        }
-    }
     
 }

+ 2 - 1
deltachat-ios/Chat/Views/Cells/ImageTextCell.swift

@@ -37,7 +37,6 @@ class ImageTextCell: BaseMessageCell {
         messageLabel.paddingTrailing = 12
         contentImageView.constraintAlignLeadingMaxTo(mainContentView, priority: .required).isActive = true
         contentImageView.constraintAlignTrailingMaxTo(mainContentView, priority: .required).isActive = true
-        topCompactView = true
         let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onImageTapped))
         gestureRecognizer.numberOfTapsRequired = 1
         contentImageView.addGestureRecognizer(gestureRecognizer)
@@ -47,6 +46,7 @@ class ImageTextCell: BaseMessageCell {
         messageLabel.text = msg.text
         bottomCompactView = msg.text?.isEmpty ?? true
         mainContentView.spacing = msg.text?.isEmpty ?? false ? 0 : 6
+        topCompactView = msg.quoteText == nil ? true : false
         tag = msg.id
         if msg.type == DC_MSG_IMAGE, let image = msg.image {
             contentImageView.image = image
@@ -164,6 +164,7 @@ class ImageTextCell: BaseMessageCell {
     }
 
     override func prepareForReuse() {
+        super.prepareForReuse()
         contentImageView.image = nil
         contentImageView.sd_cancelCurrentImageLoad()
         tag = -1

+ 6 - 0
deltachat-ios/Chat/Views/Cells/InfoMessageCell.swift

@@ -61,4 +61,10 @@ class InfoMessageCell: UITableViewCell {
         messageBackgroundContainer.update(rectCorners: corners, color: DcColors.systemMessageBackgroundColor)
     }
 
+    override func prepareForReuse() {
+        super.prepareForReuse()
+        messageLabel.text = nil
+        messageLabel.attributedText = nil
+    }
+
 }

+ 95 - 0
deltachat-ios/Chat/Views/ChatInputBar.swift

@@ -0,0 +1,95 @@
+import UIKit
+import InputBarAccessoryView
+
+
+public class ChatInputBar: InputBarAccessoryView {
+
+    var hasDraft: Bool = false
+    var hasQuote: Bool = false
+    var keyboardHeight: CGFloat = 0
+
+    public convenience init() {
+        self.init(frame: .zero)
+    }
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupKeyboardObserver()
+    }
+
+    required public init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        setupKeyboardObserver()
+    }
+
+    func setupKeyboardObserver() {
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(keyboardChanged),
+            name: UIResponder.keyboardWillShowNotification,
+            object: nil
+        )
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(keyboardChanged),
+            name: UIResponder.keyboardDidHideNotification,
+            object: nil
+        )
+    }
+    
+    override open func calculateMaxTextViewHeight() -> CGFloat {
+        if traitCollection.verticalSizeClass == .regular {
+            let divisor: CGFloat = 3
+            var subtract: CGFloat = 0
+            subtract += hasDraft ? 90 : 0
+            subtract += hasQuote ? 90 : 0
+            let height = (UIScreen.main.bounds.height / divisor).rounded(.down) - subtract
+            if height < 40 {
+                return 40
+            }
+            return height
+        } else {
+            // horizontal layout
+            let height = UIScreen.main.bounds.height - keyboardHeight - 12
+            return height
+        }
+    }
+
+    public func configure(draft: DraftModel) {
+        hasDraft = draft.attachment != nil
+        hasQuote = draft.quoteText != nil
+        maxTextViewHeight = calculateMaxTextViewHeight()
+    }
+
+    public func cancel() {
+        hasDraft = false
+        hasQuote = false
+        maxTextViewHeight = calculateMaxTextViewHeight()
+    }
+
+    @objc func keyboardChanged(_ notification: Notification) {
+        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
+            let keyboardRectangle = keyboardFrame.cgRectValue
+            keyboardHeight = keyboardRectangle.height - intrinsicContentSize.height
+            updateTextViewHeight()
+        }
+    }
+
+    public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+        if (self.traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass)
+                || (self.traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass) {
+            updateTextViewHeight()
+        }
+    }
+
+    private func updateTextViewHeight() {
+        maxTextViewHeight = calculateMaxTextViewHeight()
+        if keyboardHeight > 0, UIApplication.shared.statusBarOrientation.isLandscape {
+            setShouldForceMaxTextViewHeight(to: true, animated: false)
+        } else if shouldForceTextViewMaxHeight {
+            setShouldForceMaxTextViewHeight(to: false, animated: false)
+        }
+        invalidateIntrinsicContentSize()
+    }
+}

+ 52 - 0
deltachat-ios/Chat/Views/DocumentPreview.swift

@@ -0,0 +1,52 @@
+import UIKit
+import DcCore
+
+public class DocumentPreview: DraftPreview {
+
+    weak var delegate: DraftPreviewDelegate?
+
+    lazy var fileView: FileView = {
+        let view = FileView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.allowLayoutChange = false
+        view.fileTitle.numberOfLines = 2
+        view.isUserInteractionEnabled = true
+        return view
+    }()
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        mainContentView.addSubview(fileView)
+        addConstraints([
+            fileView.constraintAlignTopTo(mainContentView),
+            fileView.constraintAlignLeadingTo(mainContentView, paddingLeading: 8),
+            fileView.constraintAlignBottomTo(mainContentView),
+            fileView.constraintAlignTrailingTo(mainContentView),
+            mainContentView.constraintHeightTo(75, priority: .required)
+        ])
+        let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(fileViewTapped))
+        fileView.addGestureRecognizer(gestureRecognizer)
+    }
+    
+    override public func cancel() {
+        fileView.prepareForReuse()
+        delegate?.onCancelAttachment()
+    }
+
+    override public func configure(draft: DraftModel) {
+        if draft.viewType == DC_MSG_FILE, let path = draft.attachment {
+            let tmpMsg = DcMsg(viewType: DC_MSG_FILE)
+            tmpMsg.setFile(filepath: path)
+            tmpMsg.text = draft.text
+            fileView.configure(message: tmpMsg)
+            self.delegate?.onAttachmentAdded()
+            isHidden = false
+        } else {
+            isHidden = true
+        }
+    }
+
+    @objc func fileViewTapped() {
+        delegate?.onAttachmentTapped()
+    }
+}

+ 94 - 0
deltachat-ios/Chat/Views/DraftArea.swift

@@ -0,0 +1,94 @@
+import UIKit
+import DcCore
+import InputBarAccessoryView
+
+public class DraftArea: UIView, InputItem {
+    public var inputBarAccessoryView: InputBarAccessoryView?
+    public var parentStackViewPosition: InputStackView.Position?
+    public func textViewDidChangeAction(with textView: InputTextView) {}
+    public func keyboardSwipeGestureAction(with gesture: UISwipeGestureRecognizer) {}
+    public func keyboardEditingEndsAction() {}
+    public func keyboardEditingBeginsAction() {}
+
+    var delegate: DraftPreviewDelegate? {
+        set {
+            quotePreview.delegate = newValue
+            mediaPreview.delegate = newValue
+            documentPreview.delegate = newValue
+        }
+        get {
+            return quotePreview.delegate
+        }
+    }
+
+    lazy var mainContentView: UIStackView = {
+        let view = UIStackView(arrangedSubviews: [quotePreview, mediaPreview, documentPreview])
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.axis = .vertical
+        return view
+    }()
+
+    lazy var quotePreview: QuotePreview = {
+        let view = QuotePreview()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    lazy var mediaPreview: MediaPreview = {
+        let view = MediaPreview()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    lazy var documentPreview: DocumentPreview = {
+        let view = DocumentPreview()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    convenience init() {
+        self.init(frame: .zero)
+
+    }
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        self.setupSubviews()
+    }
+
+    required init(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    public func setupSubviews() {
+        addSubview(mainContentView)
+        mainContentView.fillSuperview()
+    }
+
+    public func configure(draft: DraftModel) {
+        guard let  chatInputBar = inputBarAccessoryView as? ChatInputBar else {
+            safe_fatalError("Expecting inputBarAccessoryView of type ChatInputBar")
+            return
+        }
+        quotePreview.configure(draft: draft)
+        mediaPreview.configure(draft: draft)
+        documentPreview.configure(draft: draft)
+        chatInputBar.configure(draft: draft)
+    }
+
+    /// reload cleans caches containing the drafted attachment so that the UI will update correctly
+    public func reload(draft: DraftModel) {
+        mediaPreview.reload(draft: draft)
+        ///TODO: add document reloading when document editing was added
+    }
+
+    public func cancel() {
+        quotePreview.cancel()
+        mediaPreview.cancel()
+        documentPreview.cancel()
+        if let chatInputBar = inputBarAccessoryView as? ChatInputBar {
+            chatInputBar.cancel()
+        }
+    }
+
+}

+ 20 - 41
deltachat-ios/View/QuotePreview.swift → deltachat-ios/Chat/Views/DraftPreview.swift

@@ -1,28 +1,14 @@
 import UIKit
-import InputBarAccessoryView
 import DcCore
 
-public protocol QuotePreviewDelegate: class {
-    func onCancel()
+public protocol DraftPreviewDelegate: class {
+    func onAttachmentAdded()
+    func onCancelAttachment()
+    func onCancelQuote()
+    func onAttachmentTapped()
 }
 
-public class QuotePreview: UIView, InputItem {
-    
-    public var inputBarAccessoryView: InputBarAccessoryView?
-    public var parentStackViewPosition: InputStackView.Position?
-    public func textViewDidChangeAction(with textView: InputTextView) {}
-    public func keyboardSwipeGestureAction(with gesture: UISwipeGestureRecognizer) {}
-    public func keyboardEditingEndsAction() {}
-    public func keyboardEditingBeginsAction() {}
-
-    public weak var delegate: QuotePreviewDelegate?
-
-    lazy var quoteView: QuoteView = {
-        let view = QuoteView()
-        view.translatesAutoresizingMaskIntoConstraints = false
-
-        return view
-    }()
+public class DraftPreview: UIView {
 
     lazy var cancelButton: UIView = {
         let view = UIView()
@@ -49,6 +35,12 @@ public class QuotePreview: UIView, InputItem {
         return view
     }()
 
+    lazy var mainContentView: UIView = {
+        let view = UIView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
     init() {
         super.init(frame: .zero)
         setupSubviews()
@@ -58,19 +50,19 @@ public class QuotePreview: UIView, InputItem {
         fatalError("init(coder:) has not been implemented")
     }
 
-    private func setupSubviews() {
+    func setupSubviews() {
         addSubview(upperBorder)
-        addSubview(quoteView)
+        addSubview(mainContentView)
         addSubview(cancelButton)
         addConstraints([
             upperBorder.constraintAlignLeadingTo(self),
             upperBorder.constraintAlignTrailingTo(self),
             upperBorder.constraintHeightTo(1),
             upperBorder.constraintAlignTopTo(self, paddingTop: 4),
-            quoteView.constraintAlignTopTo(upperBorder, paddingTop: 4),
-            quoteView.constraintAlignLeadingTo(self),
-            quoteView.constraintAlignBottomTo(self, paddingBottom: 4),
-            quoteView.constraintTrailingToLeadingOf(cancelButton, paddingTrailing: -2),
+            mainContentView.constraintAlignTopTo(upperBorder, paddingTop: 4),
+            mainContentView.constraintAlignLeadingTo(self),
+            mainContentView.constraintAlignBottomTo(self, paddingBottom: 4),
+            mainContentView.constraintTrailingToLeadingOf(cancelButton, paddingTrailing: -2),
             cancelButton.constraintAlignTrailingTo(self, paddingTrailing: 14),
             cancelButton.constraintWidthTo(36),
             cancelButton.constraintHeightTo(36),
@@ -86,23 +78,10 @@ public class QuotePreview: UIView, InputItem {
     }
 
     @objc public func cancel() {
-        quoteView.prepareForReuse()
-        delegate?.onCancel()
+        safe_fatalError("cancel needs to be implemented in inheriting class")
     }
 
     public func configure(draft: DraftModel) {
-        if let quoteText = draft.quoteText {
-            quoteView.quote.text = quoteText
-            if let quoteMessage = draft.quoteMessage {
-                let contact = quoteMessage.fromContact
-                quoteView.senderTitle.text = contact.displayName
-                quoteView.senderTitle.textColor = contact.color
-                quoteView.citeBar.backgroundColor = contact.color
-                quoteView.imagePreview.image = quoteMessage.image
-            }
-            isHidden = false
-        } else {
-            isHidden = true
-        }
+        safe_fatalError("configure needs to be implemented in inheriting class")
     }
 }

+ 130 - 0
deltachat-ios/Chat/Views/FileView.swift

@@ -0,0 +1,130 @@
+import UIKit
+import DcCore
+public class FileView: UIView {
+
+    private var imageWidthConstraint: NSLayoutConstraint?
+    private var imageHeightConstraint: NSLayoutConstraint?
+
+    public var horizontalLayout: Bool {
+        set {
+            if newValue {
+                fileStackView.axis = .horizontal
+                imageWidthConstraint?.isActive = true
+                imageHeightConstraint?.isActive = true
+                fileStackView.alignment = .center
+            } else {
+                fileStackView.axis = .vertical
+                imageWidthConstraint?.isActive = false
+                imageHeightConstraint?.isActive = false
+                fileStackView.alignment = .leading
+            }
+        }
+        get {
+            return fileStackView.axis == .horizontal
+        }
+    }
+
+    // allow to automatically switch between small and large preview of a file,
+    // depending on the file type, if false the view will be configured according to horizontalLayout Bool
+    public var allowLayoutChange: Bool = true
+
+    private lazy var defaultImage: UIImage = {
+        let image = UIImage(named: "ic_attach_file_36pt")
+        return image!
+    }()
+
+    private lazy var fileStackView: UIStackView = {
+        let stackView = UIStackView(arrangedSubviews: [fileImageView, fileMetadataStackView])
+        stackView.axis = .horizontal
+        stackView.translatesAutoresizingMaskIntoConstraints = false
+        stackView.spacing = 6
+        return stackView
+    }()
+
+    private lazy var fileImageView: UIImageView = {
+        let imageView = UIImageView()
+        imageView.contentMode = .scaleAspectFit
+        return imageView
+    }()
+
+    private lazy var fileMetadataStackView: UIStackView = {
+        let stackView = UIStackView(arrangedSubviews: [fileTitle, fileSubtitle])
+        stackView.axis = .vertical
+        stackView.translatesAutoresizingMaskIntoConstraints = false
+        stackView.clipsToBounds = true
+        return stackView
+    }()
+
+    lazy var fileTitle: UILabel = {
+        let title = UILabel()
+        title.font = UIFont.preferredItalicFont(for: .body)
+        title.translatesAutoresizingMaskIntoConstraints = false
+        title.numberOfLines = 3
+        title.lineBreakMode = .byCharWrapping
+        return title
+    }()
+
+    private lazy var fileSubtitle: UILabel = {
+        let subtitle = UILabel()
+        subtitle.font = UIFont.preferredItalicFont(for: .caption2)
+        subtitle.translatesAutoresizingMaskIntoConstraints = false
+        subtitle.numberOfLines = 1
+        return subtitle
+    }()
+
+    convenience init() {
+        self.init(frame: .zero)
+
+    }
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        self.setupSubviews()
+    }
+
+    required init(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+
+    func setupSubviews() {
+        addSubview(fileStackView)
+        fileStackView.fillSuperview()
+        imageWidthConstraint = fileImageView.constraintWidthTo(50)
+        imageHeightConstraint = fileImageView.constraintHeightTo(50 * 1.3, priority: .defaultLow)
+        horizontalLayout = true
+    }
+
+    public func configure(message: DcMsg) {
+        if let url = message.fileURL {
+            generateThumbnailFor(url: url, placeholder: defaultImage)
+        } else {
+            fileImageView.image = defaultImage
+            horizontalLayout = true
+        }
+        fileTitle.text = message.filename
+        fileSubtitle.text = message.getPrettyFileSize()
+    }
+
+
+    public func prepareForReuse() {
+        fileImageView.image = nil
+    }
+
+    private func generateThumbnailFor(url: URL, placeholder: UIImage?) {
+        if let thumbnail = ThumbnailCache.shared.restoreImage(key: url.absoluteString) {
+            fileImageView.image = thumbnail
+            horizontalLayout = allowLayoutChange ? false : horizontalLayout
+        } else if let pdfThumbnail = DcUtils.thumbnailFromPdf(withUrl: url) {
+            fileImageView.image = pdfThumbnail
+            horizontalLayout = allowLayoutChange ? false : horizontalLayout
+            ThumbnailCache.shared.storeImage(image: pdfThumbnail, key: url.absoluteString)
+        } else {
+            let controller = UIDocumentInteractionController(url: url)
+            fileImageView.image = controller.icons.first ?? placeholder
+            horizontalLayout = allowLayoutChange ? true : horizontalLayout
+        }
+    }
+
+
+}

+ 99 - 0
deltachat-ios/Chat/Views/MediaPreview.swift

@@ -0,0 +1,99 @@
+import UIKit
+import SDWebImage
+import DcCore
+
+class MediaPreview: DraftPreview {
+    var imageWidthConstraint: NSLayoutConstraint?
+    weak var delegate: DraftPreviewDelegate?
+
+    public lazy var contentImageView: SDAnimatedImageView = {
+        let imageView = SDAnimatedImageView()
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+        imageView.contentMode = .scaleAspectFit
+        imageView.clipsToBounds = true
+        imageView.layer.cornerRadius = 4
+        imageView.isUserInteractionEnabled = true
+        return imageView
+    }()
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        mainContentView.addSubview(contentImageView)
+        addConstraints([
+            contentImageView.constraintAlignTopTo(mainContentView),
+            contentImageView.constraintAlignLeadingTo(mainContentView, paddingLeading: 14),
+            contentImageView.constraintAlignTrailingMaxTo(mainContentView, paddingTrailing: 14),
+            contentImageView.constraintAlignBottomTo(mainContentView),
+            contentImageView.constraintHeightTo(90)
+        ])
+        let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
+        contentImageView.addGestureRecognizer(gestureRecognizer)
+    }
+
+    override func configure(draft: DraftModel) {
+        if (draft.viewType == DC_MSG_GIF || draft.viewType == DC_MSG_IMAGE), let path = draft.attachment {
+            contentImageView.sd_setImage(with: URL(fileURLWithPath: path, isDirectory: false), completed: { image, error, _, _ in
+                if let error = error {
+                    logger.error("could not load draft image: \(error)")
+                    self.cancel()
+                } else if let image = image {
+                    self.setAspectRatio(image: image)
+                    self.delegate?.onAttachmentAdded()
+                }
+            })
+            isHidden = false
+        } else if draft.viewType == DC_MSG_VIDEO, let path = draft.attachment {
+            if let image = ThumbnailCache.shared.restoreImage(key: path) {
+                self.contentImageView.image = image
+                self.setAspectRatio(image: image)
+            } else {
+                DispatchQueue.global(qos: .userInteractive).async {
+                    let thumbnailImage = DcUtils.generateThumbnailFromVideo(url: URL(fileURLWithPath: path, isDirectory: false))
+                    if let thumbnailImage = thumbnailImage {
+                        DispatchQueue.main.async { [weak self] in
+                            guard let self = self else { return }
+                            self.contentImageView.image = thumbnailImage
+                            self.setAspectRatio(image: thumbnailImage)
+                            ThumbnailCache.shared.storeImage(image: thumbnailImage, key: path)
+                        }
+                    }
+                }
+            }
+            self.isHidden = false
+        } else {
+            isHidden = true
+        }
+    }
+
+    override public func cancel() {
+        contentImageView.sd_cancelCurrentImageLoad()
+        contentImageView.image = nil
+        delegate?.onCancelAttachment()
+    }
+
+    func setAspectRatio(image: UIImage) {
+        let height = image.size.height
+        let width = image.size.width
+        imageWidthConstraint?.isActive = false
+        imageWidthConstraint = contentImageView.widthAnchor.constraint(lessThanOrEqualTo: contentImageView.heightAnchor, multiplier: width / height)
+        imageWidthConstraint?.isActive = true
+    }
+
+    @objc func imageTapped() {
+        delegate?.onAttachmentTapped()
+    }
+
+    func reload(draft: DraftModel) {
+        guard let attachment = draft.attachment else { return }
+        let url = URL(fileURLWithPath: attachment, isDirectory: false)
+        // there are editing options for DC_MSG_GIF, so that can be ignored
+        if draft.viewType == DC_MSG_IMAGE {
+            SDImageCache.shared.removeImage(forKey: url.absoluteString, withCompletion: { [weak self] in
+                self?.configure(draft: draft)
+            })
+        } else if draft.viewType == DC_MSG_VIDEO {
+            ThumbnailCache.shared.deleteImage(key: attachment)
+            self.configure(draft: draft)
+        }
+    }
+}

+ 63 - 0
deltachat-ios/Chat/Views/QuotePreview.swift

@@ -0,0 +1,63 @@
+import UIKit
+import InputBarAccessoryView
+import DcCore
+
+public class QuotePreview: DraftPreview {
+
+    public weak var delegate: DraftPreviewDelegate?
+    private var compactView = false
+
+    lazy var quoteView: QuoteView = {
+        let view = QuoteView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        mainContentView.addSubview(quoteView)
+        quoteView.fillSuperview()
+    }
+
+    override public func cancel() {
+        quoteView.prepareForReuse()
+        delegate?.onCancelQuote()
+        quoteView.quote.numberOfLines = 3
+    }
+
+    override public func configure(draft: DraftModel) {
+        if let quoteText = draft.quoteText {
+            quoteView.quote.text = quoteText
+            compactView = draft.attachment != nil
+            calculateQuoteHeight(compactView: compactView)
+            if let quoteMessage = draft.quoteMessage {
+                let contact = quoteMessage.fromContact
+                quoteView.senderTitle.text = contact.displayName
+                quoteView.senderTitle.textColor = contact.color
+                quoteView.citeBar.backgroundColor = contact.color
+                quoteView.imagePreview.image = quoteMessage.image
+            }
+
+            isHidden = false
+        } else {
+            isHidden = true
+        }
+    }
+
+    func calculateQuoteHeight(compactView: Bool) {
+        let vertical = traitCollection.verticalSizeClass == .regular
+        if vertical {
+            quoteView.quote.numberOfLines = compactView ? 1 : 3
+        } else {
+            quoteView.quote.numberOfLines = 1
+        }
+    }
+
+    public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+        if (self.traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass)
+                || (self.traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass) {
+            calculateQuoteHeight(compactView: compactView)
+        }
+    }
+}

+ 3 - 1
deltachat-ios/Chat/Views/QuoteView.swift

@@ -30,6 +30,8 @@ public class QuoteView: UIView {
     public lazy var imagePreview: UIImageView = {
         let view = UIImageView()
         view.translatesAutoresizingMaskIntoConstraints = false
+        view.contentMode = .scaleAspectFill
+        view.clipsToBounds = true
         return view
     }()
 
@@ -61,7 +63,7 @@ public class QuoteView: UIView {
             quote.constraintToBottomOf(senderTitle),
             quote.constraintTrailingToLeadingOf(imagePreview, paddingTrailing: 8),
             quote.constraintAlignBottomTo(self, paddingBottom: 4),
-            citeBar.constraintAlignLeadingTo(self, paddingLeading: 16),
+            citeBar.constraintAlignLeadingTo(self, paddingLeading: 14),
             citeBar.constraintAlignTopTo(senderTitle, paddingTop: 2),
             citeBar.constraintAlignBottomTo(quote, paddingBottom: 2),
             citeBar.constraintWidthTo(3),

+ 4 - 0
deltachat-ios/Helper/ThumbnailCache.swift

@@ -18,4 +18,8 @@ class ThumbnailCache {
     func restoreImage(key: String) -> UIImage? {
         return cache.object(forKey: NSString(string: key))
     }
+
+    func deleteImage(key: String) {
+        cache.removeObject(forKey: NSString(string: key))
+    }
 }