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

Merge pull request #707 from deltachat/file_message_layout

new document view in chats
nayooti 5 жил өмнө
parent
commit
7ce46cab21

+ 21 - 6
DcCore/DcCore/Views/InitialsBadge.swift

@@ -16,12 +16,12 @@ public class InitialsBadge: UIView {
     }()
 
     private var verifiedView: UIImageView = {
-           let imgView = UIImageView()
-           let img = UIImage(named: "verified")
-           imgView.isHidden = true
-           imgView.image = img
-           imgView.translatesAutoresizingMaskIntoConstraints = false
-           return imgView
+        let imgView = UIImageView()
+        let img = UIImage(named: "verified")
+        imgView.isHidden = true
+        imgView.image = img
+        imgView.translatesAutoresizingMaskIntoConstraints = false
+        return imgView
     }()
 
     private var imageView: UIImageView = {
@@ -31,6 +31,15 @@ public class InitialsBadge: UIView {
         return imageViewContainer
     }()
 
+    public var cornerRadius: CGFloat {
+        set {
+            layer.cornerRadius = newValue
+            imageView.layer.cornerRadius = newValue
+        }
+        get {
+            return layer.cornerRadius
+        }
+    }
 
     public convenience init(name: String, color: UIColor, size: CGFloat, accessibilityLabel: String? = nil) {
         self.init(size: size, accessibilityLabel: accessibilityLabel)
@@ -110,4 +119,10 @@ public class InitialsBadge: UIView {
     public func setVerified(_ verified: Bool) {
         verifiedView.isHidden = !verified
     }
+
+    public func reset() {
+        verifiedView.isHidden = true
+        imageView.image = nil
+        label.text = nil
+    }
 }

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

@@ -93,6 +93,9 @@
 		306011B622E5E7FB00C1CE6F /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 306011B422E5E7FB00C1CE6F /* Localizable.stringsdict */; };
 		306C32322445CDE9001D89F3 /* DcLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306C32312445CDE9001D89F3 /* DcLogger.swift */; };
 		307D822E241669C7006D2490 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307D822D241669C7006D2490 /* LocationManager.swift */; };
+		308FEA4C2462F11300FCEAD6 /* FileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308FEA4B2462F11300FCEAD6 /* FileView.swift */; };
+		308FEA50246AB67100FCEAD6 /* FileMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308FEA4F246AB67100FCEAD6 /* FileMessageCell.swift */; };
+		308FEA52246ABA2700FCEAD6 /* FileMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308FEA51246ABA2700FCEAD6 /* FileMessageSizeCalculator.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 */; };
@@ -360,6 +363,9 @@
 		306011C922E5E83500C1CE6F /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
 		306C32312445CDE9001D89F3 /* DcLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DcLogger.swift; sourceTree = "<group>"; };
 		307D822D241669C7006D2490 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
+		308FEA4B2462F11300FCEAD6 /* FileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileView.swift; sourceTree = "<group>"; };
+		308FEA4F246AB67100FCEAD6 /* FileMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMessageCell.swift; sourceTree = "<group>"; };
+		308FEA51246ABA2700FCEAD6 /* FileMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMessageSizeCalculator.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>"; };
@@ -641,9 +647,9 @@
 		305961AC2346125100C80F33 /* Views */ = {
 			isa = PBXGroup;
 			children = (
+				305961B72346125100C80F33 /* HeadersFooters */,
 				305961AD2346125100C80F33 /* Cells */,
 				305961B62346125100C80F33 /* MessageLabel.swift */,
-				305961B72346125100C80F33 /* HeadersFooters */,
 				305961B92346125100C80F33 /* TypingIndicator.swift */,
 				305961BA2346125100C80F33 /* MessageContainerView.swift */,
 				305961BB2346125100C80F33 /* TypingBubble.swift */,
@@ -653,6 +659,7 @@
 				305961BF2346125100C80F33 /* PlayButtonView.swift */,
 				305961C02346125100C80F33 /* BubbleCircle.swift */,
 				3040F461234F550300FA34D5 /* AudioPlayerView.swift */,
+				308FEA4B2462F11300FCEAD6 /* FileView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -666,6 +673,7 @@
 				305961AF2346125100C80F33 /* LocationMessageCell.swift */,
 				305961B02346125100C80F33 /* MediaMessageCell.swift */,
 				305961B12346125100C80F33 /* TextMessageCell.swift */,
+				308FEA4F246AB67100FCEAD6 /* FileMessageCell.swift */,
 				305961B22346125100C80F33 /* TypingIndicatorCell.swift */,
 				305961B32346125100C80F33 /* MessageContentCell.swift */,
 				305961B42346125100C80F33 /* MessageCollectionViewCell.swift */,
@@ -692,6 +700,7 @@
 				305961C62346125100C80F33 /* MessagesCollectionViewFlowLayout.swift */,
 				305961C72346125100C80F33 /* MediaMessageSizeCalculator.swift */,
 				300C50A0234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift */,
+				308FEA51246ABA2700FCEAD6 /* FileMessageSizeCalculator.swift */,
 				305961C82346125100C80F33 /* AudioMessageSizeCalculator.swift */,
 				305961C92346125100C80F33 /* TextMessageSizeCalculator.swift */,
 				305961CA2346125100C80F33 /* LocationMessageSizeCalculator.swift */,
@@ -1343,6 +1352,7 @@
 				AE0D26FD1FB1FE88002FAFCE /* ChatListController.swift in Sources */,
 				30149D9322F21129003C12B5 /* QrViewController.swift in Sources */,
 				AEE56D80225504DB007DC082 /* Extensions.swift in Sources */,
+				308FEA50246AB67100FCEAD6 /* FileMessageCell.swift in Sources */,
 				7A0052C81FBE6CB40048C3BF /* NewContactController.swift in Sources */,
 				AEE56D762253431E007DC082 /* AccountSetupController.swift in Sources */,
 				305FE03623A81B4C0053BE90 /* EmptyStateLabel.swift in Sources */,
@@ -1354,6 +1364,7 @@
 				305961CC2346125100C80F33 /* UIView+Extensions.swift in Sources */,
 				7A9FB1441FB061E2001FEA36 /* AppDelegate.swift in Sources */,
 				AE76E5EE242BF2EA003CF461 /* WelcomeViewController.swift in Sources */,
+				308FEA52246ABA2700FCEAD6 /* FileMessageSizeCalculator.swift in Sources */,
 				305961F52346125100C80F33 /* TypingIndicatorCell.swift in Sources */,
 				305961FF2346125100C80F33 /* AvatarView.swift in Sources */,
 				3059620C2346125100C80F33 /* MessageSizeCalculator.swift in Sources */,
@@ -1375,6 +1386,7 @@
 				305962092346125100C80F33 /* AudioMessageSizeCalculator.swift in Sources */,
 				305961DB2346125100C80F33 /* AudioItem.swift in Sources */,
 				305962012346125100C80F33 /* PlayButtonView.swift in Sources */,
+				308FEA4C2462F11300FCEAD6 /* FileView.swift in Sources */,
 				B20462E42440A4A600367A57 /* SettingsAutodelOverviewController.swift in Sources */,
 				789E879D21D6DF86003ED1C5 /* ProgressHud.swift in Sources */,
 				305961F32346125100C80F33 /* MediaMessageCell.swift in Sources */,

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

@@ -527,10 +527,14 @@ class ChatViewController: MessagesViewController {
             let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
             cell.configure(with: message, at: indexPath, and: messagesCollectionView)
             return cell
-        case .photoText, .videoText, .fileText:
+        case .photoText, .videoText:
             let cell = messagesCollectionView.dequeueReusableCell(TextMediaMessageCell.self, for: indexPath)
             cell.configure(with: message, at: indexPath, and: messagesCollectionView)
             return cell
+        case .fileText:
+            let cell = messagesCollectionView.dequeueReusableCell(FileMessageCell.self, for: indexPath)
+            cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+            return cell
         case .location:
             let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
             cell.configure(with: message, at: indexPath, and: messagesCollectionView)

+ 15 - 12
deltachat-ios/DC/DcMsg+Extension.swift

@@ -52,7 +52,7 @@ extension DcMsg: MessageType {
         }
         let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0),
                                                                              NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor])
-        return MessageKind.videoText(Media(url: fileURL, image: thumbnail, text: attributedString))
+        return MessageKind.videoText(Media(url: fileURL, image: thumbnail, text: [attributedString]))
     }
 
     internal func createImageMessage(text: String) -> MessageKind {
@@ -61,7 +61,7 @@ extension DcMsg: MessageType {
         }
         let attributedString = NSAttributedString(string: text, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0),
                                                                              NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor])
-        return MessageKind.photoText(Media(image: image, text: attributedString))
+        return MessageKind.photoText(Media(image: image, text: [attributedString]))
     }
 
     internal func createAudioMessage(text: String) -> MessageKind {
@@ -76,18 +76,21 @@ extension DcMsg: MessageType {
     }
 
     internal func createFileMessage(text: String) -> MessageKind {
-        let fileString = "\(self.filename ?? "???") (\(self.filesize / 1024) kB)"
-        let attributedFileString = NSMutableAttributedString(string: fileString,
+        let fileString = "\(self.filename ?? "???")"
+        let fileSizeString = "(\(self.filesize / 1024) kB)"
+        let attributedMediaMessageString =
+                   NSAttributedString(string: text,
+                                             attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0),
+                                                          NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor])
+        let attributedFileString = NSAttributedString(string: fileString,
                                                              attributes: [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 13.0),
                                                                           NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor])
-        if !text.isEmpty {
-            attributedFileString.append(NSAttributedString(string: "\n\n",
-                                                           attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 7.0)]))
-            attributedFileString.append(NSAttributedString(string: text,
-                                                           attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0),
-                                                                        NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor]))
-        }
-        return MessageKind.fileText(Media(text: attributedFileString))
+        let attributedFileSizeString = NSAttributedString(string: fileSizeString,
+                                                                 attributes: [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 13.0),
+                                                                              NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor])
+
+        let mediaText = [attributedMediaMessageString, attributedFileString, attributedFileSizeString]
+        return MessageKind.fileText(Media(url: fileURL, placeholderImage: UIImage(named: "ic_attach_file_36pt"), text: mediaText))
     }
     
 }

+ 7 - 5
deltachat-ios/MessageKit/Controllers/MessagesViewController.swift

@@ -288,10 +288,14 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
             let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
             cell.configure(with: message, at: indexPath, and: messagesCollectionView)
             return cell
-        case .photoText, .videoText, .fileText:
+        case .photoText, .videoText:
             let cell = messagesCollectionView.dequeueReusableCell(TextMediaMessageCell.self, for: indexPath)
             cell.configure(with: message, at: indexPath, and: messagesCollectionView)
             return cell
+        case .fileText:
+            let cell = messagesCollectionView.dequeueReusableCell(FileMessageCell.self, for: indexPath)
+            cell.configure(with: message, at: indexPath, and: messagesCollectionView)
+            return cell
         case .location:
             let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
             cell.configure(with: message, at: indexPath, and: messagesCollectionView)
@@ -416,10 +420,8 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
             pasteBoard.string = text
         case .attributedText(let attributedText):
             pasteBoard.string = attributedText.string
-        case .photoText(let mediaItem), .videoText(let mediaItem):
-            pasteBoard.string = mediaItem.text?.string
-        case .fileText(let mediaItem):
-            pasteBoard.string = mediaItem.text?.string
+        case .photoText(let mediaItem), .videoText(let mediaItem), .fileText(let mediaItem):
+            pasteBoard.string = mediaItem.text?[MediaItemConstants.messageText].string
         default:
             break
         }

+ 73 - 0
deltachat-ios/MessageKit/Layout/FileMessageSizeCalculator.swift

@@ -0,0 +1,73 @@
+import Foundation
+import UIKit
+
+open class FileMessageSizeCalculator: MessageSizeCalculator {
+
+    var defaultFileMessageCellWidth: CGFloat {
+        switch UIApplication.shared.statusBarOrientation {
+        case .landscapeLeft, .landscapeRight:
+            return UIScreen.main.bounds.size.width * 0.66
+        default:
+            return UIScreen.main.bounds.size.width * 0.85
+        }
+    }
+
+    private var incomingMessageLabelInsets = UIEdgeInsets(top: 0,
+                                                         left: FileMessageCell.insetHorizontalBig,
+                                                         bottom: FileMessageCell.insetBottom,
+                                                         right: FileMessageCell.insetHorizontalSmall)
+    private var outgoingMessageLabelInsets = UIEdgeInsets(top: 0,
+                                                         left: FileMessageCell.insetHorizontalSmall,
+                                                         bottom: FileMessageCell.insetBottom,
+                                                         right: FileMessageCell.insetHorizontalBig)
+
+    private var messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
+
+    internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets {
+        let dataSource = messagesLayout.messagesDataSource
+        let isFromCurrentSender = dataSource.isFromCurrentSender(message: message)
+        return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets
+    }
+
+    open override func messageContainerSize(for message: MessageType) -> CGSize {
+        let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in
+            var messageContainerSize = CGSize(width: maxWidth, height: FileView.defaultHeight)
+            switch message.kind {
+            case .fileText(let mediaItem):
+                if let messageText = mediaItem.text?[MediaItemConstants.messageText], !messageText.string.isEmpty {
+                    let messageTextHeight = messageText.height(withConstrainedWidth: maxWidth - self.messageLabelInsets(for: message).horizontal)
+                    messageContainerSize.height += messageTextHeight + self.messageLabelInsets(for: message).bottom
+                }
+            default:
+                safe_fatalError("only fileText types can be calculated by FileMessageSizeCalculator")
+            }
+            return messageContainerSize
+        }
+
+        switch message.kind {
+        case .fileText(let item):
+            let maxImageWidth = item.image != nil ? messageContainerMaxWidth(for: message) : defaultFileMessageCellWidth
+            return sizeForMediaItem(maxImageWidth, item)
+        default:
+            safe_fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
+            return .zero
+        }
+    }
+
+    open override func configure(attributes: UICollectionViewLayoutAttributes) {
+        super.configure(attributes: attributes)
+        guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return }
+
+        let dataSource = messagesLayout.messagesDataSource
+        let indexPath = attributes.indexPath
+        let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
+
+        switch message.kind {
+        case .fileText:
+            attributes.messageLabelInsets = messageLabelInsets(for: message)
+            attributes.messageLabelFont = messageLabelFont
+        default:
+            break
+        }
+    }
+}

+ 5 - 1
deltachat-ios/MessageKit/Layout/MessagesCollectionViewFlowLayout.swift

@@ -167,6 +167,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
         return sizeCalculator
     }()
     lazy open var textMediaMessageSizeCalculator = TextMediaMessageSizeCalculator(layout: self)
+    lazy open var fileMessageSizeCalculator = FileMessageSizeCalculator(layout: self)
     lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
     lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self)
     lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self)
@@ -194,8 +195,10 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
             return emojiMessageSizeCalculator
         case .photo:
             return photoMessageSizeCalculator
-        case .photoText, .videoText, .fileText:
+        case .photoText, .videoText:
             return textMediaMessageSizeCalculator
+        case .fileText:
+            return fileMessageSizeCalculator
         case .video:
             return videoMessageSizeCalculator
         case .location:
@@ -326,6 +329,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
         return [textMessageSizeCalculator,
                 attributedTextMessageSizeCalculator,
                 emojiMessageSizeCalculator,
+                fileMessageSizeCalculator,
                 textMediaMessageSizeCalculator,
                 photoMessageSizeCalculator,
                 videoMessageSizeCalculator,

+ 4 - 4
deltachat-ios/MessageKit/Layout/TextMediaMessageSizeCalculator.swift

@@ -81,8 +81,8 @@ open class TextMediaMessageSizeCalculator: MessageSizeCalculator {
 
             var messageContainerSize = CGSize(width: imageWidth, height: imageHeight)
             switch message.kind {
-            case .photoText(let mediaItem), .videoText(let mediaItem), .fileText(let mediaItem):
-                if let text = mediaItem.text {
+            case .photoText(let mediaItem), .videoText(let mediaItem):
+                if let text = mediaItem.text?[MediaItemConstants.messageText] {
                     let textHeight = text.height(withConstrainedWidth: maxTextWidth)
                     messageContainerSize.height += textHeight
                     messageContainerSize.height +=  self.messageLabelInsets(for: message).vertical
@@ -94,7 +94,7 @@ open class TextMediaMessageSizeCalculator: MessageSizeCalculator {
         }
 
         switch message.kind {
-        case .photoText(let item), .videoText(let item), .fileText(let item):
+        case .photoText(let item), .videoText(let item):
             return sizeForMediaItem(maxImageWidth, item)
         default:
             fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)")
@@ -110,7 +110,7 @@ open class TextMediaMessageSizeCalculator: MessageSizeCalculator {
         let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView)
 
         switch message.kind {
-        case .photoText, .videoText, .fileText:
+        case .photoText, .videoText:
             attributes.messageLabelInsets = messageLabelInsets(for: message)
             attributes.messageLabelFont = messageLabelFont
         default:

+ 6 - 1
deltachat-ios/MessageKit/Protocols/MediaItem.swift

@@ -40,6 +40,11 @@ public protocol MediaItem {
     /// The size of the media item.
     var size: CGSize { get }
 
-    var text: NSAttributedString? { get }
+    var text: [NSAttributedString]? { get }
+}
 
+struct MediaItemConstants {
+    static let messageText = 0
+    static let mediaTitle = 1
+    static let mediaSubtitle = 2
 }

+ 3 - 3
deltachat-ios/MessageKit/Views/Cells/AudioMessageCell.swift

@@ -72,9 +72,9 @@ open class AudioMessageCell: MessageContentCell {
         if let text = messageLabel.attributedText {
             let height = (text.height(withConstrainedWidth:
                 messageContainerView.frame.width -
-                    TextMediaMessageCell.insetHorizontalSmall -
-                    TextMediaMessageCell.insetHorizontalBig))
-            return height + TextMediaMessageCell.insetBottom + TextMediaMessageCell.insetTop
+                    AudioMessageCell.insetHorizontalSmall -
+                    AudioMessageCell.insetHorizontalBig))
+            return height + AudioMessageCell.insetBottom + AudioMessageCell.insetTop
         }
         return 0
     }

+ 141 - 0
deltachat-ios/MessageKit/Views/Cells/FileMessageCell.swift

@@ -0,0 +1,141 @@
+import UIKit
+
+// A subclass of `MessageContentCell` used to display mixed media messages.
+open class FileMessageCell: MessageContentCell {
+
+    static let insetBottom: CGFloat = 12
+    static let insetHorizontalBig: CGFloat = 23
+    static let insetHorizontalSmall: CGFloat = 12
+
+    // MARK: - Properties
+    var fileViewLeadingPadding: CGFloat = 0 {
+        didSet {
+            fileViewLeadingAlignment.constant = fileViewLeadingPadding
+        }
+    }
+
+    private lazy var fileViewLeadingAlignment: NSLayoutConstraint = {
+        return fileView.constraintAlignLeadingTo(messageContainerView, paddingLeading: 0)
+    }()
+
+    /// The `MessageCellDelegate` for the cell.
+    open override weak var delegate: MessageCellDelegate? {
+        didSet {
+            messageLabel.delegate = delegate
+        }
+    }
+
+    /// The label used to display the message's text.
+    open var messageLabel = MessageLabel()
+
+    private lazy var fileView: FileView = {
+        let marginInsets = NSDirectionalEdgeInsets(top: FileMessageCell.insetHorizontalSmall,
+                                                   leading: FileMessageCell.insetHorizontalSmall,
+                                                   bottom: FileMessageCell.insetHorizontalSmall,
+                                                   trailing: FileMessageCell.insetHorizontalSmall)
+        let fileView = FileView(directionalLayoutMargins: marginInsets)
+        fileView.translatesAutoresizingMaskIntoConstraints = false
+        return fileView
+    }()
+
+    // MARK: - Methods
+
+    /// Responsible for setting up the constraints of the cell's subviews.
+    open func setupConstraints(for messageKind: MessageKind) {
+        messageContainerView.removeConstraints(messageContainerView.constraints)
+
+        let fileViewConstraints = [fileView.constraintHeightTo(FileView.defaultHeight),
+                                    fileViewLeadingAlignment,
+                                    fileView.constraintAlignTrailingTo(messageContainerView),
+                                    fileView.constraintAlignTopTo(messageContainerView),
+                                    ]
+        messageContainerView.addConstraints(fileViewConstraints)
+
+        messageLabel.frame = CGRect(x: 0,
+                                    y: FileView.defaultHeight,
+                                    width: messageContainerView.frame.width,
+                                    height: getMessageLabelHeight())
+    }
+
+    private func getMessageLabelHeight() -> CGFloat {
+        if let text = messageLabel.attributedText, !text.string.isEmpty {
+            let height = (text.height(withConstrainedWidth:
+                messageContainerView.frame.width -
+                    FileMessageCell.insetHorizontalSmall -
+                    FileMessageCell.insetHorizontalBig))
+            return height + FileMessageCell.insetBottom
+        }
+        return 0
+    }
+
+    open override func setupSubviews() {
+        super.setupSubviews()
+        messageContainerView.addSubview(fileView)
+        messageContainerView.addSubview(messageLabel)
+    }
+
+    open override func prepareForReuse() {
+        super.prepareForReuse()
+        self.messageLabel.attributedText = nil
+        self.fileView.prepareForReuse()
+    }
+
+    open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+        super.apply(layoutAttributes)
+        if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes {
+            messageLabel.textInsets = attributes.messageLabelInsets
+            messageLabel.messageLabelFont = attributes.messageLabelFont
+            fileViewLeadingPadding = attributes.messageLabelInsets.left
+        }
+    }
+
+    // MARK: - Configure Cell
+    open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
+        super.configure(with: message, at: indexPath, and: messagesCollectionView)
+
+        guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
+            fatalError(MessageKitError.nilMessagesDisplayDelegate)
+        }
+
+        switch message.kind {
+        case .fileText(let mediaItem):
+            configureFileView(for: mediaItem)
+            configureMessageLabel(for: mediaItem,
+                                             with: displayDelegate,
+                                             message: message,
+                                             at: indexPath,
+                                             in: messagesCollectionView)
+        default:
+            fatalError("Unexpected message kind in FileMessageCell")
+        }
+        setupConstraints(for: message.kind)
+    }
+
+    private func configureFileView(for mediaItem: MediaItem) {
+        fileView.configureFor(mediaItem: mediaItem)
+    }
+
+    private func configureMessageLabel(for mediaItem: MediaItem,
+                                       with displayDelegate: MessagesDisplayDelegate,
+                                       message: MessageType,
+                                       at indexPath: IndexPath,
+                                       in messagesCollectionView: MessagesCollectionView) {
+        let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView)
+        messageLabel.configure {
+            messageLabel.enabledDetectors = enabledDetectors
+            for detector in enabledDetectors {
+                let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath)
+                messageLabel.setAttributes(attributes, detector: detector)
+            }
+            messageLabel.attributedText = mediaItem.text?[MediaItemConstants.messageText]
+        }
+    }
+
+    /// Used to handle the cell's contentView's tap gesture.
+    /// Return false when the contentView does not need to handle the gesture.
+    open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
+        let touchPointWithoutImageHeight = CGPoint(x: touchPoint.x,
+                                                   y: touchPoint.y - fileView.frame.height)
+        return messageLabel.handleGesture(touchPointWithoutImageHeight)
+    }
+}

+ 19 - 40
deltachat-ios/MessageKit/Views/Cells/TextMediaMessageCell.swift

@@ -3,10 +3,10 @@ import UIKit
 // A subclass of `MessageContentCell` used to display mixed media messages.
 open class TextMediaMessageCell: MessageContentCell {
 
-    public static let insetTop: CGFloat = 12
-    public static let insetBottom: CGFloat = 12
-    public static let insetHorizontalBig: CGFloat = 23
-    public static let insetHorizontalSmall: CGFloat = 12
+    static let insetTop: CGFloat = 12
+    static let insetBottom: CGFloat = 12
+    static let insetHorizontalBig: CGFloat = 23
+    static let insetHorizontalSmall: CGFloat = 12
 
 
     // MARK: - Properties
@@ -34,12 +34,6 @@ open class TextMediaMessageCell: MessageContentCell {
         return playButtonView
     }()
 
-    open lazy var fileView: UIImageView = {
-        let fileView = UIImageView(image: UIImage(named: "ic_attach_file_36pt"))
-        fileView.translatesAutoresizingMaskIntoConstraints = false
-        return fileView
-    }()
-
     // MARK: - Methods
 
     open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
@@ -50,7 +44,7 @@ open class TextMediaMessageCell: MessageContentCell {
         }
     }
 
-    func getMessageLabelHeight() -> CGFloat {
+    private func getMessageLabelHeight() -> CGFloat {
         if let text = messageLabel.attributedText {
             let height = (text.height(withConstrainedWidth:
                 messageContainerView.frame.width -
@@ -85,11 +79,6 @@ open class TextMediaMessageCell: MessageContentCell {
             let playButtonViewConstraints = [ playButtonView.constraintCenterXTo(imageView),
                                               playButtonView.constraintCenterYTo(imageView)]
             messageContainerView.addConstraints(playButtonViewConstraints)
-        case .fileText:
-            fileView.constraint(equalTo: CGSize(width: 35, height: 35))
-            let fileViewConstraints = [ fileView.constraintCenterXTo(imageView),
-                                                         fileView.constraintCenterYTo(imageView)]
-            messageContainerView.addConstraints(fileViewConstraints)
         default:
             break
         }
@@ -106,7 +95,6 @@ open class TextMediaMessageCell: MessageContentCell {
         super.setupSubviews()
         messageContainerView.addSubview(imageView)
         messageContainerView.addSubview(playButtonView)
-        messageContainerView.addSubview(fileView)
         messageContainerView.addSubview(messageLabel)
     }
 
@@ -131,14 +119,13 @@ open class TextMediaMessageCell: MessageContentCell {
         }
 
         configurePlayButtonView(for: message.kind)
-        configureFileView(for: message.kind)
         setupConstraints(for: message.kind)
 
         displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView)
     }
 
 
-    func configurePlayButtonView(for messageKind: MessageKind) {
+    private func configurePlayButtonView(for messageKind: MessageKind) {
         switch messageKind {
         case .videoText:
             playButtonView.isHidden = false
@@ -147,31 +134,23 @@ open class TextMediaMessageCell: MessageContentCell {
         }
     }
 
-    func configureFileView(for messageKind: MessageKind) {
-        switch messageKind {
-        case .fileText:
-            fileView.isHidden = false
-        default:
-            fileView.isHidden = true
-        }
-    }
-
-    func configureImageView(for mediaItem: MediaItem) {
+    private func configureImageView(for mediaItem: MediaItem) {
         imageView.image = mediaItem.image ?? mediaItem.placeholderImage
     }
-    func configureMessageLabel(for mediaItem: MediaItem,
-                               with displayDelegate: MessagesDisplayDelegate,
-                               message: MessageType,
-                               at indexPath: IndexPath,
-                               in messagesCollectionView: MessagesCollectionView) {
+
+    private func configureMessageLabel(for mediaItem: MediaItem,
+                                       with displayDelegate: MessagesDisplayDelegate,
+                                       message: MessageType,
+                                       at indexPath: IndexPath,
+                                       in messagesCollectionView: MessagesCollectionView) {
         let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView)
         messageLabel.configure {
-           messageLabel.enabledDetectors = enabledDetectors
-           for detector in enabledDetectors {
-               let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath)
-               messageLabel.setAttributes(attributes, detector: detector)
-           }
-            messageLabel.attributedText = mediaItem.text
+            messageLabel.enabledDetectors = enabledDetectors
+            for detector in enabledDetectors {
+                let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath)
+                messageLabel.setAttributes(attributes, detector: detector)
+            }
+            messageLabel.attributedText = mediaItem.text?[MediaItemConstants.messageText]
         }
     }
 

+ 87 - 0
deltachat-ios/MessageKit/Views/FileView.swift

@@ -0,0 +1,87 @@
+import UIKit
+import DcCore
+
+class FileView: UIView {
+
+    static let badgeSize: CGFloat = 54
+    static let defaultHeight: CGFloat = 78
+    static let defaultWidth: CGFloat = 250
+
+    private lazy var titleView: MessageLabel = {
+        let label = MessageLabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        return label
+    }()
+
+    private lazy var subtitleView: MessageLabel = {
+        let label = MessageLabel()
+        label.numberOfLines = 1
+        label.adjustsFontSizeToFitWidth = false
+        label.lineBreakMode = .byTruncatingTail
+        label.translatesAutoresizingMaskIntoConstraints = false
+        return label
+    }()
+
+    private lazy var fileBadgeView: InitialsBadge = {
+        let badge: InitialsBadge = InitialsBadge(image: UIImage(), size: FileView.badgeSize)
+        badge.isAccessibilityElement = false
+        badge.isHidden = false
+        badge.cornerRadius = 6
+        return badge
+    }()
+
+    private lazy var verticalStackView: UIStackView = {
+        let stackView = UIStackView(arrangedSubviews: [titleView, subtitleView])
+        stackView.translatesAutoresizingMaskIntoConstraints = false
+        stackView.axis = .vertical
+        stackView.alignment = .leading
+        stackView.isLayoutMarginsRelativeArrangement = true
+        return stackView
+    }()
+
+    init(directionalLayoutMargins: NSDirectionalEdgeInsets) {
+        super.init(frame: .zero)
+        translatesAutoresizingMaskIntoConstraints = false
+        verticalStackView.directionalLayoutMargins = directionalLayoutMargins
+        addSubview(fileBadgeView)
+        addSubview(verticalStackView)
+        addConstraints([
+            fileBadgeView.constraintAlignLeadingTo(self),
+            fileBadgeView.constraintWidthTo(FileView.badgeSize),
+            fileBadgeView.constraintHeightTo(FileView.badgeSize),
+            fileBadgeView.constraintCenterYTo(self)
+        ])
+        addConstraints([
+            verticalStackView.constraintCenterYTo(self),
+            verticalStackView.constraintToTrailingOf(fileBadgeView),
+            verticalStackView.constraintAlignTrailingTo(self)
+        ])
+    }
+
+    func configureFor(mediaItem: MediaItem) {
+        if let url = mediaItem.url {
+            let controller = UIDocumentInteractionController(url: url)
+            fileBadgeView.setImage(controller.icons.first ?? mediaItem.placeholderImage)
+        } else {
+            fileBadgeView.setImage(mediaItem.placeholderImage)
+        }
+
+        if let title = mediaItem.text?[MediaItemConstants.mediaTitle] {
+            titleView.attributedText = title
+        }
+
+        if let subtitle = mediaItem.text?[MediaItemConstants.mediaSubtitle] {
+            subtitleView.attributedText = subtitle
+        }
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    func prepareForReuse() {
+        titleView.attributedText = nil
+        subtitleView.attributedText = nil
+        fileBadgeView.reset()
+    }
+}

+ 1 - 0
deltachat-ios/MessageKit/Views/MessagesCollectionView.swift

@@ -76,6 +76,7 @@ open class MessagesCollectionView: UICollectionView {
         register(TextMessageCell.self)
         register(MediaMessageCell.self)
         register(TextMediaMessageCell.self)
+        register(FileMessageCell.self)
         register(LocationMessageCell.self)
         register(AudioMessageCell.self)
         register(ContactMessageCell.self)

+ 5 - 2
deltachat-ios/Model/Media.swift

@@ -8,7 +8,7 @@ struct Media: MediaItem {
     var image: UIImage?
 
     var placeholderImage: UIImage = UIImage(color: .gray, size: CGSize(width: 250, height: 100))!
-    var text: NSAttributedString?
+    var text: [NSAttributedString]?
 
     var size: CGSize {
         if let image = image {
@@ -18,9 +18,12 @@ struct Media: MediaItem {
         }
     }
 
-    init(url: URL? = nil, image: UIImage? = nil, text: NSAttributedString? = nil) {
+    init(url: URL? = nil, image: UIImage? = nil, placeholderImage: UIImage? = nil, text: [NSAttributedString]? = nil) {
         self.url = url
         self.image = image
         self.text = text
+        if let placeholderImage = placeholderImage {
+            self.placeholderImage = placeholderImage
+        }
     }
 }