Forráskód Böngészése

implement basic VideoInviteCell

cyberta 4 éve
szülő
commit
d4287d5fb2

+ 1 - 0
DcCore/DcCore/Helper/DcColors.swift

@@ -13,6 +13,7 @@ public struct DcColors {
                                                           dark: UIColor.init(hexString: "333333"))
     public static let contactCellBackgroundColor = UIColor.themeColor(light: .white, dark: .black)
     public static let defaultBackgroundColor = UIColor.themeColor(light: .white, dark: .black)
+    public static let defaultInverseColor = UIColor.themeColor(light: .black, dark: .white)
     public static let sharedChatCellBackgroundColor = UIColor.themeColor(light: white, dark: actionCellBackgroundDark)
     public static let chatBackgroundColor = UIColor.themeColor(light: .white, dark: .black)
     public static let checkmarkGreen = UIColor.themeColor(light: UIColor.rgb(red: 112, green: 177, blue: 92))

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

@@ -11,6 +11,7 @@
 		3008CB7224F93EB900E6A617 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */; };
 		3008CB7424F9436C00E6A617 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */; };
 		3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7524F95B6D00E6A617 /* AudioController.swift */; };
+		3010968926838A050032CBA0 /* VideoInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3010968826838A040032CBA0 /* VideoInviteCell.swift */; };
 		30149D9322F21129003C12B5 /* QrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30149D9222F21129003C12B5 /* QrViewController.swift */; };
 		30152C9425A5D91400377714 /* PaddingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F4BFED252E3E020006B9B3 /* PaddingTextView.swift */; };
 		30152C9725A5D91900377714 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EF7323252FF15F00E2C54A /* MessageLabel.swift */; };
@@ -24,6 +25,7 @@
 		302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AC265E237F1807002A943F /* AvatarHelper.swift */; };
 		302B84C72396770B001C261F /* RelayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B84C42396627F001C261F /* RelayHelper.swift */; };
 		302B84CE2397F6CD001C261F /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B84CD2397F6CD001C261F /* URL+Extension.swift */; };
+		302D5450268B6B2300A8B271 /* MessageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302D544F268B6B2300A8B271 /* MessageUtils.swift */; };
 		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 */; };
@@ -210,6 +212,7 @@
 		3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = "<group>"; };
 		3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
 		3008CB7524F95B6D00E6A617 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
+		3010968826838A040032CBA0 /* VideoInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoInviteCell.swift; sourceTree = "<group>"; };
 		30149D9222F21129003C12B5 /* QrViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrViewController.swift; sourceTree = "<group>"; };
 		3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderController.swift; sourceTree = "<group>"; };
 		3022E6BF22E8768800763272 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -236,6 +239,7 @@
 		30260CA6238F02F700D8D52C /* MultilineTextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldCell.swift; sourceTree = "<group>"; };
 		302B84C42396627F001C261F /* RelayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayHelper.swift; sourceTree = "<group>"; };
 		302B84CD2397F6CD001C261F /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = "<group>"; };
+		302D544F268B6B2300A8B271 /* MessageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUtils.swift; sourceTree = "<group>"; };
 		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>"; };
@@ -621,6 +625,7 @@
 				30E348E424F6647D005C93D1 /* FileTextCell.swift */,
 				30A4149624F6EFBE00EC91EB /* InfoMessageCell.swift */,
 				3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */,
+				3010968826838A040032CBA0 /* VideoInviteCell.swift */,
 			);
 			path = Cells;
 			sourceTree = "<group>";
@@ -830,6 +835,7 @@
 				AE0AA9552478191900D42A7F /* GridCollectionViewFlowLayout.swift */,
 				AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */,
 				21D6C9392606190600D0755A /* NotificationManager.swift */,
+				302D544F268B6B2300A8B271 /* MessageUtils.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -1282,6 +1288,7 @@
 				30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */,
 				302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
 				AE77838D23E32ED20093EABD /* ContactDetailViewModel.swift in Sources */,
+				3010968926838A050032CBA0 /* VideoInviteCell.swift in Sources */,
 				303492CB257A814200A523D0 /* DraftArea.swift in Sources */,
 				AEE6EC3F2282C59C00EDC689 /* GroupMembersViewController.swift in Sources */,
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
@@ -1296,6 +1303,7 @@
 				305702A124C6453700D84EFC /* TypeAlias.swift in Sources */,
 				AE19887523EB264000B4CD5F /* HelpViewController.swift in Sources */,
 				AE0D26FD1FB1FE88002FAFCE /* ChatListController.swift in Sources */,
+				302D5450268B6B2300A8B271 /* MessageUtils.swift in Sources */,
 				30149D9322F21129003C12B5 /* QrViewController.swift in Sources */,
 				AEE56D80225504DB007DC082 /* Extensions.swift in Sources */,
 				7A0052C81FBE6CB40048C3BF /* NewContactController.swift in Sources */,

+ 13 - 4
deltachat-ios/Chat/ChatViewController.swift

@@ -252,6 +252,7 @@ class ChatViewController: UITableViewController {
         tableView.register(FileTextCell.self, forCellReuseIdentifier: "file")
         tableView.register(InfoMessageCell.self, forCellReuseIdentifier: "info")
         tableView.register(AudioMessageCell.self, forCellReuseIdentifier: "audio")
+        tableView.register(VideoInviteCell.self, forCellReuseIdentifier: "video_invite")
         tableView.rowHeight = UITableView.automaticDimension
         tableView.separatorStyle = .none
         tableView.keyboardDismissMode = .interactive
@@ -557,21 +558,29 @@ class ChatViewController: UITableViewController {
         }
 
         let cell: BaseMessageCell
-        if message.type == DC_MSG_IMAGE || message.type == DC_MSG_GIF || message.type == DC_MSG_VIDEO || message.type == DC_MSG_STICKER {
+        switch Int32(message.type) {
+        case DC_MSG_VIDEOCHAT_INVITATION:
+            let videoInviteCell = tableView.dequeueReusableCell(withIdentifier: "video_invite", for: indexPath) as? VideoInviteCell ?? VideoInviteCell()
+            videoInviteCell.update(dcContext: dcContext, msg: message)
+            return videoInviteCell
+
+        case DC_MSG_IMAGE, DC_MSG_GIF, DC_MSG_VIDEO, DC_MSG_STICKER:
             cell = tableView.dequeueReusableCell(withIdentifier: "image", for: indexPath) as? ImageTextCell ?? ImageTextCell()
-        } else if message.type == DC_MSG_FILE {
+
+        case DC_MSG_FILE:
             if message.isSetupMessage {
                 cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextMessageCell ?? TextMessageCell()
                 message.text = String.localized("autocrypt_asm_click_body")
             } else {
                 cell = tableView.dequeueReusableCell(withIdentifier: "file", for: indexPath) as? FileTextCell ?? FileTextCell()
             }
-        } else if message.type == DC_MSG_AUDIO ||  message.type == DC_MSG_VOICE {
+
+        case DC_MSG_AUDIO, DC_MSG_VOICE:
             let audioMessageCell: AudioMessageCell = tableView.dequeueReusableCell(withIdentifier: "audio",
                                                                                       for: indexPath) as? AudioMessageCell ?? AudioMessageCell()
             audioController.update(audioMessageCell, with: message.id)
             cell = audioMessageCell
-        } else {
+        default:
             cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextMessageCell ?? TextMessageCell()
         }
 

+ 3 - 126
deltachat-ios/Chat/Views/Cells/BaseMessageCell.swift

@@ -316,7 +316,8 @@ public class BaseMessageCell: UITableViewCell {
                                           color: getBackgroundColor(dcContext: dcContext, message: msg))
 
         if !msg.isInfo {
-            bottomLabel.attributedText = getFormattedBottomLine(message: msg)
+            bottomLabel.attributedText = MessageUtils.getFormattedBottomLine(message: msg,
+                                                                             tintColor: !(isTransparent || bottomCompactView) ? DcColors.checkmarkGreen : nil)
         }
 
         if let quoteText = msg.quoteText {
@@ -368,7 +369,7 @@ public class BaseMessageCell: UITableViewCell {
             "\(quoteAccessibilityString) " +
             "\(additionalAccessibilityString) " +
             "\(messageLabelAccessibilityString) " +
-            "\(getFormattedBottomLineAccessibilityString(message: message))"
+            "\(MessageUtils.getFormattedBottomLineAccessibilityString(message: message))"
     }
 
     func getBackgroundColor(dcContext: DcContext, message: DcMsg) -> UIColor {
@@ -383,130 +384,6 @@ public class BaseMessageCell: UITableViewCell {
         return backgroundColor
     }
 
-    func getFormattedBottomLineAccessibilityString(message: DcMsg) -> String {
-        let padlock =  message.showPadlock() ? "\(String.localized("encrypted_message")), " : ""
-        let date = "\(message.formattedSentDate()), "
-        let sendingState = "\(getSendingStateString(message.state))"
-        return "\(date) \(padlock) \(sendingState)"
-    }
-
-    func getFormattedBottomLine(message: DcMsg) -> NSAttributedString {
-
-        var paragraphStyle = NSParagraphStyle()
-        if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
-            paragraphStyle = style
-        }
-
-        var timestampAttributes: [NSAttributedString.Key: Any] = [
-            .font: UIFont.preferredFont(for: .caption1, weight: .regular),
-            .foregroundColor: DcColors.grayDateColor,
-            .paragraphStyle: paragraphStyle,
-        ]
-
-        let text = NSMutableAttributedString()
-        if message.fromContactId == Int(DC_CONTACT_ID_SELF) {
-            let tintColor: UIColor? = !(bottomCompactView || isTransparent) ? DcColors.checkmarkGreen : nil
-            if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
-                style.alignment = .right
-                timestampAttributes[.paragraphStyle] = style
-                if let tintColor = tintColor {
-                    timestampAttributes[.foregroundColor] = tintColor
-                }
-            }
-
-            text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
-            if message.showPadlock() {
-                attachPadlock(to: text, color: tintColor)
-            }
-            
-            if message.hasLocation {
-                attachLocation(to: text, color: tintColor)
-            }
-
-            attachSendingState(message.state, to: text)
-            return text
-        }
-
-        text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
-        if message.showPadlock() {
-            attachPadlock(to: text)
-        }
-        
-        if message.hasLocation {
-            attachLocation(to: text)
-        }
-        
-        return text
-    }
-
-    private func attachLocation(to text: NSMutableAttributedString, color: UIColor? = nil) {
-        let imageAttachment = NSTextAttachment()
-        
-        if let color = color {
-            imageAttachment.image = UIImage(named: "ic_location")?.maskWithColor(color: color)?.scaleDownImage(toMax: 12)
-        } else {
-            imageAttachment.image = UIImage(named: "ic_location")?.maskWithColor(color: DcColors.grayDateColor)?.scaleDownImage(toMax: 12)
-        }
-        
-        let imageString = NSMutableAttributedString(attachment: imageAttachment)
-        imageString.addAttributes([NSAttributedString.Key.baselineOffset: -0.5], range: NSRange(location: 0, length: 1))
-        text.append(NSAttributedString(string: "\u{202F}"))
-        text.append(imageString)
-    }
-    
-    private func attachPadlock(to text: NSMutableAttributedString, color: UIColor? = nil) {
-        let imageAttachment = NSTextAttachment()
-        if let color = color {
-            imageAttachment.image = UIImage(named: "ic_lock")?.maskWithColor(color: color)?.scaleDownImage(toMax: 15)
-        } else {
-            imageAttachment.image = UIImage(named: "ic_lock")?.scaleDownImage(toMax: 15)
-        }
-        let imageString = NSMutableAttributedString(attachment: imageAttachment)
-        imageString.addAttributes([NSAttributedString.Key.baselineOffset: -0.5], range: NSRange(location: 0, length: 1))
-        text.append(NSAttributedString(string: " "))
-        text.append(imageString)
-    }
-
-    private func getSendingStateString(_ state: Int) -> String {
-        switch Int32(state) {
-        case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
-            return String.localized("a11y_delivery_status_sending")
-        case DC_STATE_OUT_DELIVERED:
-            return String.localized("a11y_delivery_status_delivered")
-        case DC_STATE_OUT_MDN_RCVD:
-            return String.localized("a11y_delivery_status_read")
-        case DC_STATE_OUT_FAILED:
-            return String.localized("a11y_delivery_status_error")
-        default:
-            return ""
-        }
-    }
-
-    private func attachSendingState(_ state: Int, to text: NSMutableAttributedString) {
-        let imageAttachment = NSTextAttachment()
-        var offset: CGFloat = -2
-
-        switch Int32(state) {
-        case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
-            imageAttachment.image = #imageLiteral(resourceName: "ic_hourglass_empty_white_36pt").scaleDownImage(toMax: 14)?.maskWithColor(color: DcColors.grayDateColor)
-        case DC_STATE_OUT_DELIVERED:
-            imageAttachment.image = #imageLiteral(resourceName: "ic_done_36pt").scaleDownImage(toMax: 16)?.sd_croppedImage(with: CGRect(x: 0, y: 4, width: 16, height: 14))
-            offset = -3.5
-        case DC_STATE_OUT_MDN_RCVD:
-            imageAttachment.image = #imageLiteral(resourceName: "ic_done_all_36pt").scaleDownImage(toMax: 16)?.sd_croppedImage(with: CGRect(x: 0, y: 4, width: 16, height: 14))
-            text.append(NSAttributedString(string: "\u{202F}"))
-            offset = -3.5
-        case DC_STATE_OUT_FAILED:
-            imageAttachment.image = #imageLiteral(resourceName: "ic_error_36pt").scaleDownImage(toMax: 14)
-        default:
-            imageAttachment.image = nil
-        }
-        let imageString = NSMutableAttributedString(attachment: imageAttachment)
-        imageString.addAttributes([.baselineOffset: offset],
-                                  range: NSRange(location: 0, length: 1))
-        text.append(imageString)
-    }
-
     override public func prepareForReuse() {
         accessibilityLabel = nil
         textLabel?.text = nil

+ 142 - 0
deltachat-ios/Chat/Views/Cells/VideoInviteCell.swift

@@ -0,0 +1,142 @@
+import Foundation
+import UIKit
+import DcCore
+
+public class VideoInviteCell: UITableViewCell {
+
+    private lazy var messageBackgroundContainer: BackgroundContainer = {
+        let container = BackgroundContainer()
+        container.image = UIImage(color: DcColors.systemMessageBackgroundColor)
+        container.contentMode = .scaleToFill
+        container.clipsToBounds = true
+        container.translatesAutoresizingMaskIntoConstraints = false
+        return container
+    }()
+
+    lazy var avatarView: InitialsBadge = {
+        let view = InitialsBadge(size: 28)
+        view.setColor(DcColors.systemMessageBackgroundColor)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        return view
+    }()
+
+    lazy var videoIcon: InitialsBadge = {
+        let view = InitialsBadge(size: 28)
+        view.setColor(DcColors.systemMessageBackgroundColor)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        view.setImage(#imageLiteral(resourceName: "ic_videochat").withRenderingMode(.alwaysTemplate))
+        view.tintColor = DcColors.defaultInverseColor
+        return view
+    }()
+
+    lazy var messageLabel: UILabel = {
+        let label = UILabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.numberOfLines = 0
+        label.lineBreakMode = .byWordWrapping
+        label.textAlignment = .center
+        label.font = UIFont.preferredFont(for: .body, weight: .regular)
+        return label
+    }()
+
+    lazy var openLabel: UILabel = {
+        let label = UILabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.numberOfLines = 0
+        label.lineBreakMode = .byWordWrapping
+        label.textAlignment = .center
+        label.font = UIFont.preferredFont(for: .body, weight: .bold)
+        label.text = String.localized("open")
+        return label
+    }()
+
+    lazy var bottomLabel: PaddingTextView = {
+        let label = PaddingTextView()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.font = UIFont.preferredFont(for: .caption1, weight: .regular)
+        label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
+        label.layer.cornerRadius = 4
+        label.paddingLeading = 4
+        label.paddingTrailing = 4
+        label.clipsToBounds = true
+        label.isAccessibilityElement = false
+        label.backgroundColor = UIColor(alpha: 200, red: 50, green: 50, blue: 50)
+        return label
+    }()
+
+
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
+        clipsToBounds = false
+        backgroundColor = .none
+        setupSubviews()
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    func setupSubviews() {
+        contentView.addSubview(videoIcon)
+        contentView.addSubview(avatarView)
+        contentView.addSubview(messageBackgroundContainer)
+        contentView.addSubview(messageLabel)
+        contentView.addSubview(openLabel)
+        contentView.addSubview(bottomLabel)
+        contentView.addConstraints([
+            videoIcon.constraintAlignTopTo(contentView, paddingTop: 12),
+            videoIcon.constraintCenterXTo(contentView, paddingX: -36),
+            avatarView.constraintAlignTopTo(contentView, paddingTop: 12),
+            avatarView.constraintCenterXTo(contentView, paddingX: 36),
+            messageLabel.constraintToBottomOf(videoIcon, paddingTop: 16),
+            messageLabel.constraintAlignLeadingMaxTo(contentView, paddingLeading: UIDevice.current.userInterfaceIdiom == .pad ? 150 : 50),
+            messageLabel.constraintAlignTrailingMaxTo(contentView, paddingTrailing: UIDevice.current.userInterfaceIdiom == .pad ? 150 : 50),
+            messageLabel.constraintCenterXTo(contentView),
+            openLabel.constraintToBottomOf(messageLabel),
+            openLabel.constraintCenterXTo(contentView),
+            openLabel.constraintAlignLeadingMaxTo(contentView, paddingLeading: UIDevice.current.userInterfaceIdiom == .pad ? 150 : 50),
+            openLabel.constraintAlignTrailingMaxTo(contentView, paddingTrailing: UIDevice.current.userInterfaceIdiom == .pad ? 150 : 50),
+            messageBackgroundContainer.constraintAlignLeadingTo(messageLabel, paddingLeading: -6),
+            messageBackgroundContainer.constraintAlignTopTo(messageLabel, paddingTop: -6),
+            messageBackgroundContainer.constraintAlignBottomTo(openLabel, paddingBottom: -6),
+            messageBackgroundContainer.constraintAlignTrailingTo(messageLabel, paddingTrailing: -6),
+            bottomLabel.constraintAlignTrailingTo(messageBackgroundContainer),
+            bottomLabel.constraintToBottomOf(messageBackgroundContainer, paddingTop: 8),
+            bottomLabel.constraintAlignLeadingMaxTo(contentView, paddingLeading: UIDevice.current.userInterfaceIdiom == .pad ? 150 : 50),
+            bottomLabel.constraintAlignBottomTo(contentView, paddingBottom: 12)
+        ])
+        selectionStyle = .none
+    }
+
+    func update(dcContext: DcContext, msg: DcMsg) {
+        messageLabel.text = msg.text
+        let fromContact = dcContext.getContact(id: msg.fromContactId)
+        avatarView.setName(msg.getSenderName(fromContact))
+        avatarView.setColor(fromContact.color)
+        if let profileImage = fromContact.profileImage {
+            avatarView.setImage(profileImage)
+        }
+
+        bottomLabel.attributedText = MessageUtils.getFormattedBottomLine(message: msg, tintColor: DcColors.checkmarkGreen)
+        
+        var corners: UIRectCorner = []
+        corners.formUnion(.topLeft)
+        corners.formUnion(.bottomLeft)
+        corners.formUnion(.topRight)
+        corners.formUnion(.bottomRight)
+        messageBackgroundContainer.update(rectCorners: corners, color: DcColors.systemMessageBackgroundColor)
+    }
+
+    public override func prepareForReuse() {
+        super.prepareForReuse()
+        messageLabel.text = nil
+        messageLabel.attributedText = nil
+        bottomLabel.text = nil
+        bottomLabel.attributedText = nil
+        avatarView.reset()
+    }
+
+}

+ 129 - 0
deltachat-ios/Helper/MessageUtils.swift

@@ -0,0 +1,129 @@
+import Foundation
+import UIKit
+import DcCore
+
+
+public class MessageUtils {
+    static func getFormattedBottomLine(message: DcMsg, tintColor: UIColor?) -> NSAttributedString {
+
+        var paragraphStyle = NSParagraphStyle()
+        if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
+            paragraphStyle = style
+        }
+
+        var timestampAttributes: [NSAttributedString.Key: Any] = [
+            .font: UIFont.preferredFont(for: .caption1, weight: .regular),
+            .foregroundColor: DcColors.grayDateColor,
+            .paragraphStyle: paragraphStyle,
+        ]
+
+        let text = NSMutableAttributedString()
+        if message.fromContactId == Int(DC_CONTACT_ID_SELF) {
+            if let style = NSMutableParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle {
+                style.alignment = .right
+                timestampAttributes[.paragraphStyle] = style
+                if let tintColor = tintColor {
+                    timestampAttributes[.foregroundColor] = tintColor
+                }
+            }
+
+            text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
+            if message.showPadlock() {
+                attachPadlock(to: text, color: tintColor)
+            }
+
+            if message.hasLocation {
+                attachLocation(to: text, color: tintColor)
+            }
+
+            attachSendingState(message.state, to: text)
+            return text
+        }
+
+        text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
+        if message.showPadlock() {
+            attachPadlock(to: text)
+        }
+
+        if message.hasLocation {
+            attachLocation(to: text)
+        }
+
+        return text
+    }
+
+    private static func attachLocation(to text: NSMutableAttributedString, color: UIColor? = nil) {
+        let imageAttachment = NSTextAttachment()
+
+        if let color = color {
+            imageAttachment.image = UIImage(named: "ic_location")?.maskWithColor(color: color)?.scaleDownImage(toMax: 12)
+        } else {
+            imageAttachment.image = UIImage(named: "ic_location")?.maskWithColor(color: DcColors.grayDateColor)?.scaleDownImage(toMax: 12)
+        }
+
+        let imageString = NSMutableAttributedString(attachment: imageAttachment)
+        imageString.addAttributes([NSAttributedString.Key.baselineOffset: -0.5], range: NSRange(location: 0, length: 1))
+        text.append(NSAttributedString(string: "\u{202F}"))
+        text.append(imageString)
+    }
+
+    private static func attachPadlock(to text: NSMutableAttributedString, color: UIColor? = nil) {
+        let imageAttachment = NSTextAttachment()
+        if let color = color {
+            imageAttachment.image = UIImage(named: "ic_lock")?.maskWithColor(color: color)?.scaleDownImage(toMax: 15)
+        } else {
+            imageAttachment.image = UIImage(named: "ic_lock")?.scaleDownImage(toMax: 15)
+        }
+        let imageString = NSMutableAttributedString(attachment: imageAttachment)
+        imageString.addAttributes([NSAttributedString.Key.baselineOffset: -0.5], range: NSRange(location: 0, length: 1))
+        text.append(NSAttributedString(string: " "))
+        text.append(imageString)
+    }
+
+    private static func getSendingStateString(_ state: Int) -> String {
+        switch Int32(state) {
+        case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
+            return String.localized("a11y_delivery_status_sending")
+        case DC_STATE_OUT_DELIVERED:
+            return String.localized("a11y_delivery_status_delivered")
+        case DC_STATE_OUT_MDN_RCVD:
+            return String.localized("a11y_delivery_status_read")
+        case DC_STATE_OUT_FAILED:
+            return String.localized("a11y_delivery_status_error")
+        default:
+            return ""
+        }
+    }
+
+    private static func attachSendingState(_ state: Int, to text: NSMutableAttributedString) {
+        let imageAttachment = NSTextAttachment()
+        var offset: CGFloat = -2
+
+        switch Int32(state) {
+        case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_hourglass_empty_white_36pt").scaleDownImage(toMax: 14)?.maskWithColor(color: DcColors.grayDateColor)
+        case DC_STATE_OUT_DELIVERED:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_done_36pt").scaleDownImage(toMax: 16)?.sd_croppedImage(with: CGRect(x: 0, y: 4, width: 16, height: 14))
+            offset = -3.5
+        case DC_STATE_OUT_MDN_RCVD:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_done_all_36pt").scaleDownImage(toMax: 16)?.sd_croppedImage(with: CGRect(x: 0, y: 4, width: 16, height: 14))
+            text.append(NSAttributedString(string: "\u{202F}"))
+            offset = -3.5
+        case DC_STATE_OUT_FAILED:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_error_36pt").scaleDownImage(toMax: 14)
+        default:
+            imageAttachment.image = nil
+        }
+        let imageString = NSMutableAttributedString(attachment: imageAttachment)
+        imageString.addAttributes([.baselineOffset: offset],
+                                  range: NSRange(location: 0, length: 1))
+        text.append(imageString)
+    }
+
+    public static func getFormattedBottomLineAccessibilityString(message: DcMsg) -> String {
+        let padlock =  message.showPadlock() ? "\(String.localized("encrypted_message")), " : ""
+        let date = "\(message.formattedSentDate()), "
+        let sendingState = "\(MessageUtils.getSendingStateString(message.state))"
+        return "\(date) \(padlock) \(sendingState)"
+    }
+}