Browse Source

Merge pull request #892 from deltachat/messageKitReplacement

Message kit replacement
bjoern 5 years ago
parent
commit
7919bd6d63

+ 24 - 3
DcCore/DcCore/DC/Wrapper.swift

@@ -27,14 +27,19 @@ public class DcContext {
         return .dcContext
     }
 
-    public func getMessageIds(chatId: Int, count: Int, from: Int?) -> [Int] {
+    public func getMessageIds(chatId: Int, count: Int? = nil, from: Int? = nil) -> [Int] {
 		let cMessageIds = getChatMessages(chatId: chatId)
 
+
         let ids: [Int]
         if let from = from {
+            // skip last part
             ids = DcUtils.copyAndFreeArrayWithOffset(inputArray: cMessageIds, len: count, skipEnd: from)
-        } else {
+        } else if let count = count {
+            // skip first part
             ids = DcUtils.copyAndFreeArrayWithLen(inputArray: cMessageIds, len: count)
+        } else {
+            ids = DcUtils.copyAndFreeArray(inputArray: cMessageIds)
         }
         return ids
     }
@@ -832,6 +837,10 @@ public class DcMsg {
         DcContact(id: fromContactId)
     }()
 
+    public var isFromCurrentSender: Bool {
+        return fromContact.id == DcContact(id: Int(DC_CONTACT_ID_SELF)).id
+    }
+
     public var chatId: Int {
         return Int(dc_msg_get_chat_id(messagePointer))
     }
@@ -901,7 +910,19 @@ public class DcMsg {
         } else {
             return nil
         }
-        }()
+    }()
+
+    public var messageHeight: CGFloat {
+        return CGFloat(dc_msg_get_height(messagePointer))
+    }
+
+    public var messageWidth: CGFloat {
+        return CGFloat(dc_msg_get_width(messagePointer))
+    }
+
+    public func setLateFilingMediaSize(width: CGFloat, height: CGFloat, duration: Int) {
+        dc_msg_latefiling_mediasize(messagePointer, Int32(width), Int32(height), Int32(duration))
+    }
 
     public var file: String? {
         if let cString = dc_msg_get_file(messagePointer) {

+ 36 - 0
DcCore/DcCore/Extensions/UIView+Extensions.swift

@@ -95,6 +95,24 @@ public extension UIView {
         return constraint
     }
 
+    /**
+        allows to align leading to the leading of another view but allows left side shrinking
+     */
+    func constraintAlignLeadingMaxTo(_ view: UIView, paddingLeading: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
+        let constraint = NSLayoutConstraint(
+            item: self,
+            attribute: .leading,
+            relatedBy: .greaterThanOrEqual,
+            toItem: view,
+            attribute: .leading,
+            multiplier: 1.0,
+            constant: paddingLeading)
+        if let priority = priority {
+            constraint.priority = priority
+        }
+        return constraint
+    }
+
     func constraintAlignTrailingTo(_ view: UIView, paddingTrailing: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
         let constraint = NSLayoutConstraint(
             item: self,
@@ -110,6 +128,24 @@ public extension UIView {
         return constraint
     }
 
+    /**
+        allows to align trailing to the trailing of another view but allows right side shrinking
+     */
+    func constraintAlignTrailingMaxTo(_ view: UIView, paddingTrailing: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
+        let constraint = NSLayoutConstraint(
+            item: self,
+            attribute: .trailing,
+            relatedBy: .lessThanOrEqual,
+            toItem: view,
+            attribute: .trailing,
+            multiplier: 1.0,
+            constant: -paddingTrailing)
+        if let priority = priority {
+            constraint.priority = priority
+        }
+        return constraint
+    }
+
     func constraintToBottomOf(_ view: UIView, paddingTop: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
         let constraint = NSLayoutConstraint(
             item: self,

+ 5 - 4
DcCore/DcCore/Helper/DcUtils.swift

@@ -67,16 +67,17 @@ public struct DcUtils {
         return acc
     }
 
-    static func copyAndFreeArrayWithOffset(inputArray: OpaquePointer?, len: Int = 0, from: Int = 0, skipEnd: Int = 0) -> [Int] {
+    static func copyAndFreeArrayWithOffset(inputArray: OpaquePointer?, len: Int? = 0, from: Int = 0, skipEnd: Int = 0) -> [Int] {
         let lenArray = dc_array_get_cnt(inputArray)
+        let length = len ?? lenArray
         if lenArray <= skipEnd || lenArray == 0 {
             dc_array_unref(inputArray)
             return []
         }
 
         let start = lenArray - 1 - skipEnd
-        let end = max(0, start - len)
-        let finalLen = start - end + (len > 0 ? 0 : 1)
+        let end = max(0, start - length)
+        let finalLen = start - end + (length > 0 ? 0 : 1)
         var acc: [Int] = [Int](repeating: 0, count: finalLen)
 
         for i in stride(from: start, to: end, by: -1) {
@@ -85,7 +86,7 @@ public struct DcUtils {
         }
 
         dc_array_unref(inputArray)
-        DcContext.shared.logger?.info("got: \(from) \(len) \(lenArray) - \(acc)")
+        DcContext.shared.logger?.info("got: \(from) \(length) \(lenArray) - \(acc)")
 
         return acc
     }

+ 1 - 1
DcCore/DcCore/Views/InitialsBadge.swift

@@ -7,7 +7,6 @@ public class InitialsBadge: UIView {
 
     private var label: UILabel = {
         let label = UILabel()
-        label.font = UIFont.systemFont(ofSize: 26)
         label.textAlignment = NSTextAlignment.center
         label.textColor = UIColor.white
         label.translatesAutoresizingMaskIntoConstraints = false
@@ -62,6 +61,7 @@ public class InitialsBadge: UIView {
         translatesAutoresizingMaskIntoConstraints = false
         heightAnchor.constraint(equalToConstant: size).isActive = true
         widthAnchor.constraint(equalToConstant: size).isActive = true
+        label.font = UIFont.systemFont(ofSize: size * 3 / 5)
         setupSubviews(with: radius)
         isAccessibilityElement = true
     }

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

@@ -7,6 +7,9 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		3008CB7224F93EB900E6A617 /* NewAudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7124F93EB900E6A617 /* NewAudioMessageCell.swift */; };
+		3008CB7424F9436C00E6A617 /* NewAudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7324F9436C00E6A617 /* NewAudioPlayerView.swift */; };
+		3008CB7624F95B6D00E6A617 /* NewAudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7524F95B6D00E6A617 /* NewAudioController.swift */; };
 		300C509D234B551900F8AE22 /* TextMediaMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300C509C234B551900F8AE22 /* TextMediaMessageCell.swift */; };
 		300C50A1234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300C50A0234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift */; };
 		30149D9322F21129003C12B5 /* QrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30149D9222F21129003C12B5 /* QrViewController.swift */; };
@@ -17,6 +20,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 */; };
+		302E1BB4252B5AB4008F4264 /* NewPlayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E1BB3252B5AB4008F4264 /* NewPlayButtonView.swift */; };
 		3040F45E234DFBC000FA34D5 /* Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3040F45D234DFBC000FA34D5 /* Audio.swift */; };
 		3040F460234F419400FA34D5 /* BasicAudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3040F45F234F419300FA34D5 /* BasicAudioController.swift */; };
 		3040F462234F550300FA34D5 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3040F461234F550300FA34D5 /* AudioPlayerView.swift */; };
@@ -106,8 +110,12 @@
 		308FEA52246ABA2700FCEAD6 /* FileMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308FEA51246ABA2700FCEAD6 /* FileMessageSizeCalculator.swift */; };
 		3095A351237DD1F700AB07F7 /* MediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3095A350237DD1F700AB07F7 /* MediaPicker.swift */; };
 		30A2EC36247D72720024ADD8 /* AnimatedImageMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A2EC35247D72720024ADD8 /* AnimatedImageMessageCell.swift */; };
+		30A4149724F6EFBE00EC91EB /* NewInfoMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A4149624F6EFBE00EC91EB /* NewInfoMessageCell.swift */; };
 		30B0ACFA24AB5B99004D5E29 /* SettingsEphemeralMessageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0ACF924AB5B99004D5E29 /* SettingsEphemeralMessageController.swift */; };
 		30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */; };
+		30E348DF24F3F819005C93D1 /* ChatTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348DE24F3F819005C93D1 /* ChatTableView.swift */; };
+		30E348E124F53772005C93D1 /* NewImageTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348E024F53772005C93D1 /* NewImageTextCell.swift */; };
+		30E348E524F6647D005C93D1 /* NewFileTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E348E424F6647D005C93D1 /* NewFileTextCell.swift */; };
 		30E8F2132447285600CE2C90 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F2122447285600CE2C90 /* ShareViewController.swift */; };
 		30E8F2162447285600CE2C90 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 30E8F2142447285600CE2C90 /* MainInterface.storyboard */; };
 		30E8F21A2447285600CE2C90 /* Delta Chat.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 30E8F2102447285600CE2C90 /* Delta Chat.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -116,7 +124,13 @@
 		30E8F2442449C64100CE2C90 /* ChatListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F2432449C64100CE2C90 /* ChatListCell.swift */; };
 		30E8F2512449EA0E00CE2C90 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3060119E22DDE24000C1CE6F /* Localizable.strings */; };
 		30E8F253244DAD0E00CE2C90 /* SendingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E8F252244DAD0E00CE2C90 /* SendingController.swift */; };
+		30EF7308252F6A3300E2C54A /* PaddingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F4BFED252E3E020006B9B3 /* PaddingTextView.swift */; };
+		30EF7324252FF15F00E2C54A /* NewMessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EF7323252FF15F00E2C54A /* NewMessageLabel.swift */; };
+		30F8817624DA97DA0023780E /* BackgroundContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F8817524DA97DA0023780E /* BackgroundContainer.swift */; };
 		30F9B9EC235F2116006E7ACF /* MessageCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F9B9EB235F2116006E7ACF /* MessageCounter.swift */; };
+		30FDB70524D1C1000066C48D /* ChatViewControllerNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB6F824D1C1000066C48D /* ChatViewControllerNew.swift */; };
+		30FDB71F24D8170E0066C48D /* NewTextMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB71E24D8170E0066C48D /* NewTextMessageCell.swift */; };
+		30FDB72124D838240066C48D /* BaseMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FDB72024D838240066C48D /* BaseMessageCell.swift */; };
 		451CF971F08D38BCECADCB45 /* Pods_deltachat_ios_DcShare.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 546063D4BFB8FD920C4EAA22 /* Pods_deltachat_ios_DcShare.framework */; };
 		6795F63A82E94FF7CD2CEC0F /* Pods_deltachat_iosTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F7009234DB9408201A6CDCB /* Pods_deltachat_iosTests.framework */; };
 		7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7070FB9A2101ECBB000DC258 /* NewGroupController.swift */; };
@@ -238,6 +252,9 @@
 /* Begin PBXFileReference section */
 		21EE28844E7A690D73BF5285 /* Pods-deltachat-iosTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-deltachat-iosTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-deltachat-iosTests/Pods-deltachat-iosTests.debug.xcconfig"; sourceTree = "<group>"; };
 		2F7009234DB9408201A6CDCB /* Pods_deltachat_iosTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_deltachat_iosTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3008CB7124F93EB900E6A617 /* NewAudioMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAudioMessageCell.swift; sourceTree = "<group>"; };
+		3008CB7324F9436C00E6A617 /* NewAudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAudioPlayerView.swift; sourceTree = "<group>"; };
+		3008CB7524F95B6D00E6A617 /* NewAudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewAudioController.swift; sourceTree = "<group>"; };
 		300C509C234B551900F8AE22 /* TextMediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMediaMessageCell.swift; sourceTree = "<group>"; };
 		300C50A0234BDAB800F8AE22 /* TextMediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMediaMessageSizeCalculator.swift; sourceTree = "<group>"; };
 		30149D9222F21129003C12B5 /* QrViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrViewController.swift; sourceTree = "<group>"; };
@@ -266,6 +283,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>"; };
+		302E1BB3252B5AB4008F4264 /* NewPlayButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NewPlayButtonView.swift; path = "deltachat-ios/Chat/Views/NewPlayButtonView.swift"; sourceTree = SOURCE_ROOT; };
 		3040F45D234DFBC000FA34D5 /* Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audio.swift; sourceTree = "<group>"; };
 		3040F45F234F419300FA34D5 /* BasicAudioController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicAudioController.swift; sourceTree = "<group>"; };
 		3040F461234F550300FA34D5 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
@@ -389,9 +407,13 @@
 		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>"; };
 		30A2EC35247D72720024ADD8 /* AnimatedImageMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageMessageCell.swift; sourceTree = "<group>"; };
+		30A4149624F6EFBE00EC91EB /* NewInfoMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewInfoMessageCell.swift; sourceTree = "<group>"; };
 		30AC265E237F1807002A943F /* AvatarHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHelper.swift; sourceTree = "<group>"; };
 		30B0ACF924AB5B99004D5E29 /* SettingsEphemeralMessageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsEphemeralMessageController.swift; sourceTree = "<group>"; };
 		30C0D49C237C4908008E2A0E /* CertificateCheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateCheckController.swift; sourceTree = "<group>"; };
+		30E348DE24F3F819005C93D1 /* ChatTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableView.swift; sourceTree = "<group>"; };
+		30E348E024F53772005C93D1 /* NewImageTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewImageTextCell.swift; sourceTree = "<group>"; };
+		30E348E424F6647D005C93D1 /* NewFileTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewFileTextCell.swift; sourceTree = "<group>"; };
 		30E8F2102447285600CE2C90 /* Delta Chat.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Delta Chat.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
 		30E8F2122447285600CE2C90 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
 		30E8F2152447285600CE2C90 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
@@ -400,7 +422,13 @@
 		30E8F2412448B77600CE2C90 /* ChatListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = "<group>"; };
 		30E8F2432449C64100CE2C90 /* ChatListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListCell.swift; sourceTree = "<group>"; };
 		30E8F252244DAD0E00CE2C90 /* SendingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingController.swift; sourceTree = "<group>"; };
+		30EF7323252FF15F00E2C54A /* NewMessageLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMessageLabel.swift; sourceTree = "<group>"; };
+		30F4BFED252E3E020006B9B3 /* PaddingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingTextView.swift; sourceTree = "<group>"; };
+		30F8817524DA97DA0023780E /* BackgroundContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundContainer.swift; sourceTree = "<group>"; };
 		30F9B9EB235F2116006E7ACF /* MessageCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCounter.swift; sourceTree = "<group>"; };
+		30FDB6F824D1C1000066C48D /* ChatViewControllerNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewControllerNew.swift; sourceTree = "<group>"; };
+		30FDB71E24D8170E0066C48D /* NewTextMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTextMessageCell.swift; sourceTree = "<group>"; };
+		30FDB72024D838240066C48D /* BaseMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMessageCell.swift; sourceTree = "<group>"; };
 		546063D4BFB8FD920C4EAA22 /* Pods_deltachat_ios_DcShare.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_deltachat_ios_DcShare.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		6241BE1534A653E79AD5D01D /* Pods_deltachat_ios.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_deltachat_ios.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		7070FB9A2101ECBB000DC258 /* NewGroupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGroupController.swift; sourceTree = "<group>"; };
@@ -767,6 +795,42 @@
 			path = DcShare;
 			sourceTree = "<group>";
 		};
+		30FDB6B224D18E390066C48D /* Chat */ = {
+			isa = PBXGroup;
+			children = (
+				3008CB7524F95B6D00E6A617 /* NewAudioController.swift */,
+				30FDB6F824D1C1000066C48D /* ChatViewControllerNew.swift */,
+				30FDB6B524D193DD0066C48D /* Views */,
+			);
+			path = Chat;
+			sourceTree = "<group>";
+		};
+		30FDB6B524D193DD0066C48D /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				30FDB6B624D193DD0066C48D /* Cells */,
+				30E348DE24F3F819005C93D1 /* ChatTableView.swift */,
+				302E1BB3252B5AB4008F4264 /* NewPlayButtonView.swift */,
+				30F8817524DA97DA0023780E /* BackgroundContainer.swift */,
+				3008CB7324F9436C00E6A617 /* NewAudioPlayerView.swift */,
+				30EF7323252FF15F00E2C54A /* NewMessageLabel.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
+		30FDB6B624D193DD0066C48D /* Cells */ = {
+			isa = PBXGroup;
+			children = (
+				30FDB71E24D8170E0066C48D /* NewTextMessageCell.swift */,
+				30FDB72024D838240066C48D /* BaseMessageCell.swift */,
+				30E348E024F53772005C93D1 /* NewImageTextCell.swift */,
+				30E348E424F6647D005C93D1 /* NewFileTextCell.swift */,
+				30A4149624F6EFBE00EC91EB /* NewInfoMessageCell.swift */,
+				3008CB7124F93EB900E6A617 /* NewAudioMessageCell.swift */,
+			);
+			path = Cells;
+			sourceTree = "<group>";
+		};
 		7A9FB1371FB061E2001FEA36 = {
 			isa = PBXGroup;
 			children = (
@@ -808,6 +872,7 @@
 				7A9FB1431FB061E2001FEA36 /* AppDelegate.swift */,
 				AE851AC3227C695900ED86F0 /* View */,
 				AE851AC2227C695000ED86F0 /* Helper */,
+				30FDB6B224D18E390066C48D /* Chat */,
 				AE851AC1227C694300ED86F0 /* Coordinator */,
 				AE851AC0227C693B00ED86F0 /* Controller */,
 				AE851ABA227C692600ED86F0 /* Model */,
@@ -997,6 +1062,7 @@
 				AEFBE22E23FEF23D0045327A /* ProviderInfoCell.swift */,
 				AEB54C7E246DBA610004624C /* FlexLabel.swift */,
 				AED62BCD247687E6009E220D /* LocationStreamingIndicator.swift */,
+				30F4BFED252E3E020006B9B3 /* PaddingTextView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1378,6 +1444,7 @@
 				305961F02346125100C80F33 /* NSConstraintLayoutSet.swift in Sources */,
 				3059620E234614E700C80F33 /* DcContact+Extension.swift in Sources */,
 				AED423D7249F580700B6B2BB /* BlockedContactsViewController.swift in Sources */,
+				3008CB7424F9436C00E6A617 /* NewAudioPlayerView.swift in Sources */,
 				AED62BCE247687E6009E220D /* LocationStreamingIndicator.swift in Sources */,
 				305961F72346125100C80F33 /* MessageCollectionViewCell.swift in Sources */,
 				AE851AC9227C77CF00ED86F0 /* Media.swift in Sources */,
@@ -1397,6 +1464,7 @@
 				30A2EC36247D72720024ADD8 /* AnimatedImageMessageCell.swift in Sources */,
 				305962082346125100C80F33 /* MediaMessageSizeCalculator.swift in Sources */,
 				AE6EC5282497B9B200A400E4 /* ThumbnailCache.swift in Sources */,
+				30FDB70524D1C1000066C48D /* ChatViewControllerNew.swift in Sources */,
 				3057029D24C6442800D84EFC /* FlexLabel.swift in Sources */,
 				AE52EA20229EB9F000C586C9 /* EditGroupViewController.swift in Sources */,
 				AE18F294228C602A0007B1BE /* SecuritySettingsController.swift in Sources */,
@@ -1412,7 +1480,9 @@
 				305961E42346125100C80F33 /* MessageKitDateFormatter.swift in Sources */,
 				AEC67A1C241CE9E4007DDBE1 /* AppStateRestorer.swift in Sources */,
 				305961D32346125100C80F33 /* MessagesViewController+Keyboard.swift in Sources */,
+				3008CB7224F93EB900E6A617 /* NewAudioMessageCell.swift in Sources */,
 				305961EF2346125100C80F33 /* HorizontalEdgeInsets.swift in Sources */,
+				302E1BB4252B5AB4008F4264 /* NewPlayButtonView.swift in Sources */,
 				305961D62346125100C80F33 /* MessageInputBar.swift in Sources */,
 				305961ED2346125100C80F33 /* DetectorType.swift in Sources */,
 				305962062346125100C80F33 /* TypingIndicatorCellSizeCalculator.swift in Sources */,
@@ -1425,12 +1495,14 @@
 				305961EE2346125100C80F33 /* AvatarPosition.swift in Sources */,
 				3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */,
 				AE25F09022807AD800CDEA66 /* AvatarSelectionCell.swift in Sources */,
+				30A4149724F6EFBE00EC91EB /* NewInfoMessageCell.swift in Sources */,
 				302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
 				AE77838D23E32ED20093EABD /* ContactDetailViewModel.swift in Sources */,
 				305961E62346125100C80F33 /* LocationMessageSnapshotOptions.swift in Sources */,
 				AEE6EC3F2282C59C00EDC689 /* GroupMembersViewController.swift in Sources */,
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
 				AEE700252438E0E500D6992E /* ProgressAlertHandler.swift in Sources */,
+				30E348E524F6647D005C93D1 /* NewFileTextCell.swift in Sources */,
 				306C32322445CDE9001D89F3 /* DcLogger.swift in Sources */,
 				78E45E3A21D3CFBC00D4B15E /* SettingsController.swift in Sources */,
 				AE8519EA2272FDCA00ED86F0 /* DeviceContactsHandler.swift in Sources */,
@@ -1452,6 +1524,10 @@
 				AEE6EC412282DF5700EDC689 /* MailboxViewController.swift in Sources */,
 				AEF53BD5248904BF00D309C1 /* GalleryTimeLabel.swift in Sources */,
 				AEE6EC482283045D00EDC689 /* EditSettingsController.swift in Sources */,
+				30E348DF24F3F819005C93D1 /* ChatTableView.swift in Sources */,
+				30EF7308252F6A3300E2C54A /* PaddingTextView.swift in Sources */,
+				30E348E124F53772005C93D1 /* NewImageTextCell.swift in Sources */,
+				3008CB7624F95B6D00E6A617 /* NewAudioController.swift in Sources */,
 				305961DF2346125100C80F33 /* MessageCellDelegate.swift in Sources */,
 				302B84CE2397F6CD001C261F /* URL+Extension.swift in Sources */,
 				7A9FB1441FB061E2001FEA36 /* AppDelegate.swift in Sources */,
@@ -1471,6 +1547,7 @@
 				300C509D234B551900F8AE22 /* TextMediaMessageCell.swift in Sources */,
 				305961E92346125100C80F33 /* MessageKind.swift in Sources */,
 				305962022346125100C80F33 /* BubbleCircle.swift in Sources */,
+				30FDB71F24D8170E0066C48D /* NewTextMessageCell.swift in Sources */,
 				AE1988A523EB2FBA00B4CD5F /* Errors.swift in Sources */,
 				305961DD2346125100C80F33 /* SenderType.swift in Sources */,
 				305961E32346125100C80F33 /* MessagesDataSource.swift in Sources */,
@@ -1479,6 +1556,7 @@
 				305961E22346125100C80F33 /* MessagesDisplayDelegate.swift in Sources */,
 				305962092346125100C80F33 /* AudioMessageSizeCalculator.swift in Sources */,
 				305961DB2346125100C80F33 /* AudioItem.swift in Sources */,
+				30F8817624DA97DA0023780E /* BackgroundContainer.swift in Sources */,
 				30B0ACFA24AB5B99004D5E29 /* SettingsEphemeralMessageController.swift in Sources */,
 				305962012346125100C80F33 /* PlayButtonView.swift in Sources */,
 				308FEA4C2462F11300FCEAD6 /* FileView.swift in Sources */,
@@ -1499,6 +1577,7 @@
 				305961F82346125100C80F33 /* AudioMessageCell.swift in Sources */,
 				305961EC2346125100C80F33 /* Avatar.swift in Sources */,
 				305961CD2346125100C80F33 /* UIEdgeInsets+Extensions.swift in Sources */,
+				30EF7324252FF15F00E2C54A /* NewMessageLabel.swift in Sources */,
 				305962032346125100C80F33 /* CellSizeCalculator.swift in Sources */,
 				305961E02346125100C80F33 /* MessageLabelDelegate.swift in Sources */,
 				30C0D49D237C4908008E2A0E /* CertificateCheckController.swift in Sources */,
@@ -1512,6 +1591,7 @@
 				307D822E241669C7006D2490 /* LocationManager.swift in Sources */,
 				305961F12346125100C80F33 /* ContactMessageCell.swift in Sources */,
 				AE851AD0227DF50900ED86F0 /* GroupChatDetailViewController.swift in Sources */,
+				30FDB72124D838240066C48D /* BaseMessageCell.swift in Sources */,
 				305961D12346125100C80F33 /* Bundle+Extensions.swift in Sources */,
 				305962002346125100C80F33 /* MessagesCollectionView.swift in Sources */,
 				7A451DB01FB1F84900177250 /* AppCoordinator.swift in Sources */,

+ 1135 - 0
deltachat-ios/Chat/ChatViewControllerNew.swift

@@ -0,0 +1,1135 @@
+import MapKit
+import QuickLook
+import UIKit
+import InputBarAccessoryView
+import AVFoundation
+import DcCore
+import SDWebImage
+
+class ChatViewControllerNew: UITableViewController {
+    var dcContext: DcContext
+    let outgoingAvatarOverlap: CGFloat = 17.5
+    let loadCount = 30
+    let chatId: Int
+    var messageIds: [Int] = []
+
+    var msgChangedObserver: Any?
+    var incomingMsgObserver: Any?
+    var ephemeralTimerModifiedObserver: Any?
+
+    var lastContentOffset: CGFloat = -1
+    var isKeyboardShown: Bool = false
+    lazy var isGroupChat: Bool = {
+        return dcContext.getChat(chatId: chatId).isGroup
+    }()
+
+    /// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
+    open var messageInputBar = InputBarAccessoryView()
+
+    open override var shouldAutorotate: Bool {
+        return false
+    }
+
+    private weak var timer: Timer?
+
+    lazy var navBarTap: UITapGestureRecognizer = {
+        UITapGestureRecognizer(target: self, action: #selector(chatProfilePressed))
+    }()
+
+    private var locationStreamingItem: UIBarButtonItem = {
+        let indicator = LocationStreamingIndicator()
+        return UIBarButtonItem(customView: indicator)
+    }()
+
+    private lazy var muteItem: UIBarButtonItem = {
+        let imageView = UIImageView()
+        imageView.tintColor = DcColors.defaultTextColor
+        imageView.image =  #imageLiteral(resourceName: "volume_off").withRenderingMode(.alwaysTemplate)
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+        imageView.heightAnchor.constraint(equalToConstant: 20).isActive = true
+        imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true
+        return UIBarButtonItem(customView: imageView)
+    }()
+
+    private lazy var ephemeralMessageItem: UIBarButtonItem = {
+        let imageView = UIImageView()
+        imageView.tintColor = DcColors.defaultTextColor
+        imageView.image =  #imageLiteral(resourceName: "ephemeral_timer").withRenderingMode(.alwaysTemplate)
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+        imageView.heightAnchor.constraint(equalToConstant: 20).isActive = true
+        imageView.widthAnchor.constraint(equalToConstant: 20).isActive = true
+        return UIBarButtonItem(customView: imageView)
+    }()
+
+    private lazy var badgeItem: UIBarButtonItem = {
+        let badge: InitialsBadge
+        let chat = dcContext.getChat(chatId: chatId)
+        if let image = chat.profileImage {
+            badge = InitialsBadge(image: image, size: 28, accessibilityLabel: String.localized("menu_view_profile"))
+        } else {
+            badge = InitialsBadge(
+                name: chat.name,
+                color: chat.color,
+                size: 28,
+                accessibilityLabel: String.localized("menu_view_profile")
+            )
+            badge.setLabelFont(UIFont.systemFont(ofSize: 14))
+        }
+        badge.setVerified(chat.isVerified)
+        badge.accessibilityTraits = .button
+        return UIBarButtonItem(customView: badge)
+    }()
+
+    /// The `BasicAudioController` controll the AVAudioPlayer state (play, pause, stop) and update audio cell UI accordingly.
+    private lazy var audioController = NewAudioController(dcContext: dcContext, chatId: chatId)
+
+    private var disableWriting: Bool
+    private var showNamesAboveMessage: Bool
+    var showCustomNavBar = true
+
+    private lazy var mediaPicker: MediaPicker? = {
+        let mediaPicker = MediaPicker(navigationController: navigationController)
+        mediaPicker.delegate = self
+        return mediaPicker
+    }()
+
+    var emptyStateView: EmptyStateLabel = {
+        let view =  EmptyStateLabel()
+        return view
+    }()
+
+    init(dcContext: DcContext, chatId: Int) {
+        let dcChat = dcContext.getChat(chatId: chatId)
+        self.dcContext = dcContext
+        self.chatId = chatId
+        self.disableWriting = !dcChat.canSend
+        self.showNamesAboveMessage = dcChat.isGroup
+        super.init(nibName: nil, bundle: nil)
+        hidesBottomBarWhenPushed = true
+    }
+
+    required init?(coder _: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func loadView() {
+        self.tableView = ChatTableView(messageInputBar: self.disableWriting ? nil : messageInputBar)
+        self.tableView.delegate = self
+        self.tableView.dataSource = self
+        self.view = self.tableView
+    }
+
+    override func viewDidLoad() {
+        tableView.register(NewTextMessageCell.self, forCellReuseIdentifier: "text")
+        tableView.register(NewImageTextCell.self, forCellReuseIdentifier: "image")
+        tableView.register(NewFileTextCell.self, forCellReuseIdentifier: "file")
+        tableView.register(NewInfoMessageCell.self, forCellReuseIdentifier: "info")
+        tableView.register(NewAudioMessageCell.self, forCellReuseIdentifier: "audio")
+        tableView.rowHeight = UITableView.automaticDimension
+        tableView.separatorStyle = .none
+        super.viewDidLoad()
+        if !dcContext.isConfigured() {
+            // TODO: display message about nothing being configured
+            return
+        }
+        configureEmptyStateView()
+
+        if !disableWriting {
+            configureMessageInputBar()
+            messageInputBar.inputTextView.text = textDraft
+        }
+
+
+        let notificationCenter = NotificationCenter.default
+        notificationCenter.addObserver(self,
+                                       selector: #selector(setTextDraft),
+                                       name: UIApplication.willResignActiveNotification,
+                                       object: nil)
+        notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
+        notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
+        notificationCenter.addObserver(self, selector: #selector(keyboardDidShow(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
+        prepareContextMenu()
+    }
+
+    @objc func keyboardDidShow(_ notification: Notification) {
+        isKeyboardShown = true
+    }
+
+    @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)
+                    }
+                }
+            }
+        }
+    }
+
+    @objc func keyboardWillHide(_ notification: Notification) {
+        isKeyboardShown = false
+    }
+
+    private func startTimer() {
+        timer?.invalidate()
+        timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
+            //reload table
+            DispatchQueue.main.async {
+                guard let self = self else { return }
+                self.messageIds = self.getMessageIds()
+                self.tableView.reloadData()
+            }
+        }
+    }
+
+    private func stopTimer() {
+        timer?.invalidate()
+    }
+
+    private func configureEmptyStateView() {
+        view.addSubview(emptyStateView)
+        emptyStateView.translatesAutoresizingMaskIntoConstraints = false
+        emptyStateView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
+        emptyStateView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
+        emptyStateView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true
+        emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        self.tableView.becomeFirstResponder()
+        // this will be removed in viewWillDisappear
+        navigationController?.navigationBar.addGestureRecognizer(navBarTap)
+
+        if showCustomNavBar {
+            updateTitle(chat: dcContext.getChat(chatId: chatId))
+        }
+
+        let nc = NotificationCenter.default
+        msgChangedObserver = nc.addObserver(
+            forName: dcNotificationChanged,
+            object: nil,
+            queue: OperationQueue.main
+        ) { [weak self] notification in
+            guard let self = self else { return }
+            if let ui = notification.userInfo {
+                if self.disableWriting {
+                    // always refresh, as we can't check currently
+                    self.refreshMessages()
+                } else if let id = ui["message_id"] as? Int {
+                    if id > 0 {
+                        self.updateMessage(id)
+                    } else {
+                        // change might be a deletion
+                        self.refreshMessages()
+                    }
+                }
+                if self.showCustomNavBar {
+                    self.updateTitle(chat: self.dcContext.getChat(chatId: self.chatId))
+                }
+            }
+        }
+
+        incomingMsgObserver = nc.addObserver(
+            forName: dcNotificationIncoming,
+            object: nil, queue: OperationQueue.main
+        ) { [weak self] notification in
+            guard let self = self else { return }
+            if let ui = notification.userInfo {
+                if self.chatId == ui["chat_id"] as? Int {
+                    if let id = ui["message_id"] as? Int {
+                        if id > 0 {
+                            self.insertMessage(DcMsg(id: id))
+                        }
+                    }
+                }
+            }
+        }
+
+        ephemeralTimerModifiedObserver = nc.addObserver(
+            forName: dcEphemeralTimerModified,
+            object: nil, queue: OperationQueue.main
+        ) { [weak self] _ in
+            guard let self = self else { return }
+            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
+        dcContext.marknoticedChat(chatId: chatId)
+        let array = dcContext.getFreshMessages()
+        UIApplication.shared.applicationIconBadgeNumber = array.count
+        startTimer()
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+
+        // the navigationController will be used when chatDetail is pushed, so we have to remove that gestureRecognizer
+        navigationController?.navigationBar.removeGestureRecognizer(navBarTap)
+    }
+
+    override func viewDidDisappear(_ animated: Bool) {
+        super.viewDidDisappear(animated)
+        AppStateRestorer.shared.resetLastActiveChat()
+        setTextDraft()
+        let nc = NotificationCenter.default
+        if let msgChangedObserver = self.msgChangedObserver {
+            nc.removeObserver(msgChangedObserver)
+        }
+        if let incomingMsgObserver = self.incomingMsgObserver {
+            nc.removeObserver(incomingMsgObserver)
+        }
+        if let ephemeralTimerModifiedObserver = self.ephemeralTimerModifiedObserver {
+            nc.removeObserver(ephemeralTimerModifiedObserver)
+        }
+        audioController.stopAnyOngoingPlaying()
+        stopTimer()
+    }
+
+    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
+        let lastSectionVisibleBeforeTransition = self.isLastRowVisible()
+        coordinator.animate(
+            alongsideTransition: { [weak self] _ in
+                guard let self = self else { return }
+                if self.showCustomNavBar {
+                    self.navigationItem.setRightBarButton(self.badgeItem, animated: true)
+                }
+            },
+            completion: {[weak self] _ in
+                guard let self = self else { return }
+                self.updateTitle(chat: self.dcContext.getChat(chatId: self.chatId))
+                if lastSectionVisibleBeforeTransition {
+                    DispatchQueue.main.async { [weak self] in
+                        self?.scrollToBottom(animated: true)
+                    }
+                }
+            }
+        )
+        super.viewWillTransition(to: size, with: coordinator)
+    }
+
+    /// UITableView methods
+
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 1
+    }
+
+    override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return messageIds.count //viewModel.numberOfRowsIn(section: section)
+    }
+
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        UIMenuController.shared.setMenuVisible(false, animated: true)
+
+        let id = messageIds[indexPath.row]
+        let message = DcMsg(id: id)
+
+        if message.isInfo {
+            let cell = tableView.dequeueReusableCell(withIdentifier: "info", for: indexPath) as? NewInfoMessageCell ?? NewInfoMessageCell()
+            cell.update(msg: message)
+            return cell
+        }
+
+        let cell: BaseMessageCell
+        if message.type == DC_MSG_IMAGE || message.type == DC_MSG_GIF || message.type == DC_MSG_VIDEO {
+            cell = tableView.dequeueReusableCell(withIdentifier: "image", for: indexPath) as? NewImageTextCell ?? NewImageTextCell()
+        } else if message.type == DC_MSG_FILE {
+            if message.isSetupMessage {
+                cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? NewTextMessageCell ?? NewTextMessageCell()
+                message.text = String.localized("autocrypt_asm_click_body")
+            } else {
+                cell = tableView.dequeueReusableCell(withIdentifier: "file", for: indexPath) as? NewFileTextCell ?? NewFileTextCell()
+            }
+        } else if message.type == DC_MSG_AUDIO ||  message.type == DC_MSG_VOICE {
+            let audioMessageCell: NewAudioMessageCell = tableView.dequeueReusableCell(withIdentifier: "audio",
+                                                                                      for: indexPath) as? NewAudioMessageCell ?? NewAudioMessageCell()
+            audioController.update(audioMessageCell, with: message.id)
+            cell = audioMessageCell
+        } else {
+            cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? NewTextMessageCell ?? NewTextMessageCell()
+        }
+
+        cell.baseDelegate = self
+        cell.update(msg: message,
+                    messageStyle: configureMessageStyle(for: message, at: indexPath),
+                    isAvatarVisible: configureAvatarVisibility(for: message, at: indexPath),
+                    isGroup: isGroupChat)
+        return cell
+    }
+
+    public override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
+        lastContentOffset = scrollView.contentOffset.y
+    }
+
+    public override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
+        if !decelerate {
+            markSeenMessagesInVisibleArea()
+        }
+
+        if scrollView.contentOffset.y < lastContentOffset {
+            if isKeyboardShown {
+                tableView.endEditing(true)
+                tableView.becomeFirstResponder()
+            }
+        }
+        lastContentOffset = -1
+    }
+
+    public override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
+        markSeenMessagesInVisibleArea()
+    }
+
+    func markSeenMessagesInVisibleArea() {
+        if let indexPaths = tableView.indexPathsForVisibleRows {
+            let visibleMessagesIds = indexPaths.map { UInt32(messageIds[$0.row]) }
+            if !visibleMessagesIds.isEmpty {
+                dcContext.markSeenMessages(messageIds: visibleMessagesIds)
+            }
+        }
+    }
+
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        let messageId = messageIds[indexPath.row]
+        let message = DcMsg(id: messageId)
+        if message.isSetupMessage {
+            didTapAsm(msg: message, orgText: "")
+        } else if message.type == DC_MSG_FILE ||
+            message.type == DC_MSG_AUDIO ||
+            message.type == DC_MSG_VOICE {
+            showMediaGalleryFor(message: message)
+        }
+        UIMenuController.shared.setMenuVisible(false, animated: true)
+    }
+
+    func configureAvatarVisibility(for message: DcMsg, at indexPath: IndexPath) -> Bool {
+        return isGroupChat && !message.isFromCurrentSender && !isNextMessageSameSender(currentMessage: message, at: indexPath)
+    }
+
+    func configureMessageStyle(for message: DcMsg, at indexPath: IndexPath) -> UIRectCorner {
+
+        var corners: UIRectCorner = []
+
+        if message.isFromCurrentSender { //isFromCurrentSender(message: message) {
+            corners.formUnion(.topLeft)
+            corners.formUnion(.bottomLeft)
+            if !isPreviousMessageSameSender(currentMessage: message, at: indexPath) {
+                corners.formUnion(.topRight)
+            }
+            if !isNextMessageSameSender(currentMessage: message, at: indexPath) {
+                corners.formUnion(.bottomRight)
+            }
+        } else {
+            corners.formUnion(.topRight)
+            corners.formUnion(.bottomRight)
+            if !isPreviousMessageSameSender(currentMessage: message, at: indexPath) {
+                corners.formUnion(.topLeft)
+            }
+            if !isNextMessageSameSender(currentMessage: message, at: indexPath) {
+                corners.formUnion(.bottomLeft)
+            }
+        }
+
+        return corners
+    }
+
+    private func getBackgroundColor(for currentMessage: DcMsg) -> UIColor {
+        return currentMessage.isFromCurrentSender ? DcColors.messagePrimaryColor : DcColors.messageSecondaryColor
+    }
+
+    private func isPreviousMessageSameSender(currentMessage: DcMsg, at indexPath: IndexPath) -> Bool {
+        let previousRow = indexPath.row - 1
+        if previousRow < 0 {
+            return false
+        }
+
+        let messageId = messageIds[previousRow]
+        let previousMessage = DcMsg(id: messageId)
+
+        return previousMessage.fromContact.id == currentMessage.fromContact.id
+    }
+
+    private func isNextMessageSameSender(currentMessage: DcMsg, at indexPath: IndexPath) -> Bool {
+        let nextRow = indexPath.row + 1
+        if nextRow >= messageIds.count {
+            return false
+        }
+
+        let messageId = messageIds[nextRow]
+        let nextMessage = DcMsg(id: messageId)
+
+        return nextMessage.fromContact.id == currentMessage.fromContact.id
+    }
+
+
+    private func updateTitle(chat: DcChat) {
+        let titleView =  ChatTitleView()
+
+        var subtitle = "ErrSubtitle"
+        let chatContactIds = chat.contactIds
+        if chat.isGroup {
+            subtitle = String.localized(stringID: "n_members", count: chatContactIds.count)
+        } else if chatContactIds.count >= 1 {
+            if chat.isDeviceTalk {
+                subtitle = String.localized("device_talk_subtitle")
+            } else if chat.isSelfTalk {
+                subtitle = String.localized("chat_self_talk_subtitle")
+            } else {
+                subtitle = DcContact(id: chatContactIds[0]).email
+            }
+        }
+
+        titleView.updateTitleView(title: chat.name, subtitle: subtitle)
+        navigationItem.titleView = titleView
+
+        var rightBarButtonItems = [badgeItem]
+        if chat.isSendingLocations {
+            rightBarButtonItems.append(locationStreamingItem)
+        }
+        if chat.isMuted {
+            rightBarButtonItems.append(muteItem)
+        }
+
+        if dcContext.getChatEphemeralTimer(chatId: chat.id) > 0 {
+            rightBarButtonItems.append(ephemeralMessageItem)
+        }
+
+        navigationItem.rightBarButtonItems = rightBarButtonItems
+    }
+
+    // TODO: is the delay of one second needed?
+    @objc
+    private func refreshMessages() {
+        DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                self.messageIds = self.getMessageIds()
+                self.tableView.reloadData()
+                if self.isLastRowVisible() {
+                    self.scrollToBottom(animated: true)
+                }
+                self.showEmptyStateView(self.messageIds.isEmpty)
+            }
+        }
+    }
+
+    private func loadMessages() {
+        DispatchQueue.global(qos: .userInitiated).async {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                let wasLastRowVisible = self.isLastRowVisible()
+                let wasMessageIdsEmpty = self.messageIds.isEmpty
+                // update message ids
+                self.messageIds = self.getMessageIds()
+                self.tableView.reloadData()
+                if wasMessageIdsEmpty ||
+                    wasLastRowVisible {
+                    self.scrollToBottom(animated: false)
+                }
+                self.showEmptyStateView(self.messageIds.isEmpty)
+            }
+        }
+    }
+
+    func isLastRowVisible() -> Bool {
+        guard !messageIds.isEmpty else { return false }
+
+        let lastIndexPath = IndexPath(item: messageIds.count - 1, section: 0)
+        return tableView.indexPathsForVisibleRows?.contains(lastIndexPath) ?? false
+    }
+
+    func scrollToBottom(animated: Bool) {
+        if !messageIds.isEmpty {
+            self.tableView.scrollToRow(at: IndexPath(row: self.messageIds.count - 1, section: 0), at: .bottom, animated: animated)
+        }
+    }
+
+    private func showEmptyStateView(_ show: Bool) {
+        if show {
+            let dcChat = dcContext.getChat(chatId: chatId)
+            if chatId == DC_CHAT_ID_DEADDROP {
+                if dcContext.showEmails != DC_SHOW_EMAILS_ALL {
+                    emptyStateView.text = String.localized("chat_no_contact_requests")
+                } else {
+                    emptyStateView.text = String.localized("chat_no_messages")
+                }
+            } else if dcChat.isGroup {
+                if dcChat.isUnpromoted {
+                    emptyStateView.text = String.localized("chat_new_group_hint")
+                } else {
+                    emptyStateView.text = String.localized("chat_no_messages")
+                }
+            } else if dcChat.isSelfTalk {
+                emptyStateView.text = String.localized("saved_messages_explain")
+            } else if dcChat.isDeviceTalk {
+                emptyStateView.text = String.localized("device_talk_explain")
+            } else {
+                emptyStateView.text = String.localizedStringWithFormat(String.localized("chat_no_messages_hint"), dcChat.name, dcChat.name)
+            }
+            emptyStateView.isHidden = false
+        } else {
+            emptyStateView.isHidden = true
+        }
+    }
+
+    private var textDraft: String? {
+        return dcContext.getDraft(chatId: chatId)
+    }
+    
+    private func getMessageIds() -> [Int] {
+        return dcContext.getMessageIds(chatId: chatId)
+    }
+
+    @objc private func setTextDraft() {
+        if let text = self.messageInputBar.inputTextView.text {
+            dcContext.setDraft(chatId: chatId, draftText: text)
+        }
+    }
+
+    private func configureMessageInputBar() {
+        messageInputBar.delegate = self
+        messageInputBar.inputTextView.tintColor = DcColors.primary
+        messageInputBar.inputTextView.placeholder = String.localized("chat_input_placeholder")
+        messageInputBar.separatorLine.isHidden = true
+        messageInputBar.inputTextView.tintColor = DcColors.primary
+        messageInputBar.inputTextView.textColor = DcColors.defaultTextColor
+        messageInputBar.backgroundView.backgroundColor = DcColors.chatBackgroundColor
+
+        //scrollsToBottomOnKeyboardBeginsEditing = true
+
+        messageInputBar.inputTextView.backgroundColor = DcColors.inputFieldColor
+        messageInputBar.inputTextView.placeholderTextColor = DcColors.placeholderColor
+        messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 38)
+        messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 38)
+        messageInputBar.inputTextView.layer.borderColor = UIColor.themeColor(light: UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1),
+                                                                             dark: UIColor(red: 55 / 255, green: 55/255, blue: 55/255, alpha: 1)).cgColor
+        messageInputBar.inputTextView.layer.borderWidth = 1.0
+        messageInputBar.inputTextView.layer.cornerRadius = 13.0
+        messageInputBar.inputTextView.layer.masksToBounds = true
+        messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
+        configureInputBarItems()
+    }
+
+
+    private func configureInputBarItems() {
+
+        messageInputBar.setLeftStackViewWidthConstant(to: 40, animated: false)
+        messageInputBar.setRightStackViewWidthConstant(to: 40, animated: false)
+
+
+        let sendButtonImage = UIImage(named: "paper_plane")?.withRenderingMode(.alwaysTemplate)
+        messageInputBar.sendButton.image = sendButtonImage
+        messageInputBar.sendButton.accessibilityLabel = String.localized("menu_send")
+        messageInputBar.sendButton.accessibilityTraits = .button
+        messageInputBar.sendButton.title = nil
+        messageInputBar.sendButton.tintColor = UIColor(white: 1, alpha: 1)
+        messageInputBar.sendButton.layer.cornerRadius = 20
+        messageInputBar.middleContentViewPadding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)
+        // this adds a padding between textinputfield and send button
+        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)
+
+        let leftItems = [
+            InputBarButtonItem()
+                .configure {
+                    $0.spacing = .fixed(0)
+                    let clipperIcon = #imageLiteral(resourceName: "ic_attach_file_36pt").withRenderingMode(.alwaysTemplate)
+                    $0.image = clipperIcon
+                    $0.tintColor = DcColors.primary
+                    $0.setSize(CGSize(width: 40, height: 40), animated: false)
+                    $0.accessibilityLabel = String.localized("menu_add_attachment")
+                    $0.accessibilityTraits = .button
+            }.onSelected {
+                $0.tintColor = UIColor.themeColor(light: .lightGray, dark: .darkGray)
+            }.onDeselected {
+                $0.tintColor = DcColors.primary
+            }.onTouchUpInside { [weak self] _ in
+                self?.clipperButtonPressed()
+            }
+        ]
+
+        messageInputBar.setStackViewItems(leftItems, forStack: .left, animated: false)
+
+        // This just adds some more flare
+        messageInputBar.sendButton
+            .onEnabled { item in
+                UIView.animate(withDuration: 0.3, animations: {
+                    item.backgroundColor = DcColors.primary
+                })
+        }.onDisabled { item in
+            UIView.animate(withDuration: 0.3, animations: {
+                item.backgroundColor = DcColors.colorDisabled
+            })
+        }
+    }
+
+    @objc private func chatProfilePressed() {
+        if chatId != DC_CHAT_ID_DEADDROP {
+            showChatDetail(chatId: chatId)
+        }
+    }
+
+    @objc private func clipperButtonPressed() {
+        showClipperOptions()
+    }
+
+    private func showClipperOptions() {
+        let alert = UIAlertController(title: nil, message: nil, preferredStyle: .safeActionSheet)
+        let galleryAction = PhotoPickerAlertAction(title: String.localized("gallery"), style: .default, handler: galleryButtonPressed(_:))
+        let cameraAction = PhotoPickerAlertAction(title: String.localized("camera"), style: .default, handler: cameraButtonPressed(_:))
+        let documentAction = UIAlertAction(title: String.localized("files"), style: .default, handler: documentActionPressed(_:))
+        let voiceMessageAction = UIAlertAction(title: String.localized("voice_message"), style: .default, handler: voiceMessageButtonPressed(_:))
+        let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
+        let locationStreamingAction = UIAlertAction(title: isLocationStreaming ? String.localized("stop_sharing_location") : String.localized("location"),
+                                                    style: isLocationStreaming ? .destructive : .default,
+                                                    handler: locationStreamingButtonPressed(_:))
+
+        alert.addAction(cameraAction)
+        alert.addAction(galleryAction)
+        alert.addAction(documentAction)
+        alert.addAction(voiceMessageAction)
+        if UserDefaults.standard.bool(forKey: "location_streaming") {
+            alert.addAction(locationStreamingAction)
+        }
+        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+        self.present(alert, animated: true, completion: {
+            // unfortunately, voiceMessageAction.accessibilityHint does not work,
+            // but this hack does the trick
+            if UIAccessibility.isVoiceOverRunning {
+                if let view = voiceMessageAction.value(forKey: "__representer") as? UIView {
+                    view.accessibilityHint = String.localized("a11y_voice_message_hint_ios")
+                }
+            }
+        })
+    }
+
+    private func confirmationAlert(title: String, actionTitle: String, actionStyle: UIAlertAction.Style = .default, actionHandler: @escaping ((UIAlertAction) -> Void), cancelHandler: ((UIAlertAction) -> Void)? = nil) {
+        let alert = UIAlertController(title: title,
+                                      message: nil,
+                                      preferredStyle: .safeActionSheet)
+        alert.addAction(UIAlertAction(title: actionTitle, style: actionStyle, handler: actionHandler))
+
+        alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: cancelHandler ?? { _ in
+            self.dismiss(animated: true, completion: nil)
+            }))
+        present(alert, animated: true, completion: nil)
+    }
+
+    private func askToChatWith(email: String) {
+        let contactId = self.dcContext.createContact(name: "", email: email)
+        if dcContext.getChatIdByContactId(contactId: contactId) != 0 {
+            self.dismiss(animated: true, completion: nil)
+            let chatId = self.dcContext.createChatByContactId(contactId: contactId)
+            self.showChat(chatId: chatId)
+        } else {
+            confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_start_chat_with"), email),
+                              actionTitle: String.localized("start_chat"),
+                              actionHandler: { _ in
+                                self.dismiss(animated: true, completion: nil)
+                                let chatId = self.dcContext.createChatByContactId(contactId: contactId)
+                                self.showChat(chatId: chatId)})
+        }
+    }
+
+    private func askToDeleteMessage(id: Int) {
+        let title = String.localized(stringID: "ask_delete_messages", count: 1)
+        confirmationAlert(title: title, actionTitle: String.localized("delete"), actionStyle: .destructive,
+                          actionHandler: { _ in
+                            self.dcContext.deleteMessage(msgId: id)
+                            self.dismiss(animated: true, completion: nil)})
+    }
+
+    private func askToForwardMessage() {
+        let chat = dcContext.getChat(chatId: self.chatId)
+        if chat.isSelfTalk {
+            RelayHelper.sharedInstance.forward(to: self.chatId)
+        } else {
+            confirmationAlert(title: String.localizedStringWithFormat(String.localized("ask_forward"), chat.name),
+                              actionTitle: String.localized("menu_forward"),
+                              actionHandler: { _ in
+                                RelayHelper.sharedInstance.forward(to: self.chatId)
+                                self.dismiss(animated: true, completion: nil)},
+                              cancelHandler: { _ in
+                                self.dismiss(animated: false, completion: nil)
+                                self.navigationController?.popViewController(animated: true)})
+        }
+    }
+
+    // MARK: - coordinator
+    private func showChatDetail(chatId: Int) {
+        let chat = dcContext.getChat(chatId: chatId)
+        switch chat.chatType {
+        case .SINGLE:
+            if let contactId = chat.contactIds.first {
+                let contactDetailController = ContactDetailViewController(dcContext: dcContext, contactId: contactId)
+                navigationController?.pushViewController(contactDetailController, animated: true)
+            }
+        case .GROUP, .VERIFIEDGROUP:
+            let groupChatDetailViewController = GroupChatDetailViewController(chatId: chatId, dcContext: dcContext)
+            navigationController?.pushViewController(groupChatDetailViewController, animated: true)
+        }
+    }
+
+    func showChat(chatId: Int) {
+        if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
+            navigationController?.popToRootViewController(animated: false)
+            appDelegate.appCoordinator.showChat(chatId: chatId)
+        }
+    }
+
+    private func showDocumentLibrary() {
+        mediaPicker?.showDocumentLibrary()
+    }
+
+    private func showVoiceMessageRecorder() {
+        mediaPicker?.showVoiceRecorder()
+    }
+
+    private func showCameraViewController() {
+        mediaPicker?.showCamera()
+    }
+
+    private func showPhotoVideoLibrary(delegate: MediaPickerDelegate) {
+        mediaPicker?.showPhotoVideoLibrary()
+    }
+
+    private func showMediaGallery(currentIndex: Int, mediaUrls urls: [URL]) {
+        let betterPreviewController = PreviewController(currentIndex: currentIndex, urls: urls)
+        let nav = UINavigationController(rootViewController: betterPreviewController)
+        nav.modalPresentationStyle = .fullScreen
+        navigationController?.present(nav, animated: true)
+    }
+
+    private func documentActionPressed(_ action: UIAlertAction) {
+        showDocumentLibrary()
+    }
+
+    private func voiceMessageButtonPressed(_ action: UIAlertAction) {
+        showVoiceMessageRecorder()
+    }
+
+    private func cameraButtonPressed(_ action: UIAlertAction) {
+        showCameraViewController()
+    }
+
+    private func galleryButtonPressed(_ action: UIAlertAction) {
+        showPhotoVideoLibrary(delegate: self)
+    }
+
+    private func locationStreamingButtonPressed(_ action: UIAlertAction) {
+        let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
+        if isLocationStreaming {
+            locationStreamingFor(seconds: 0)
+        } else {
+            let alert = UIAlertController(title: String.localized("title_share_location"), message: nil, preferredStyle: .safeActionSheet)
+            addDurationSelectionAction(to: alert, key: "share_location_for_5_minutes", duration: Time.fiveMinutes)
+            addDurationSelectionAction(to: alert, key: "share_location_for_30_minutes", duration: Time.thirtyMinutes)
+            addDurationSelectionAction(to: alert, key: "share_location_for_one_hour", duration: Time.oneHour)
+            addDurationSelectionAction(to: alert, key: "share_location_for_two_hours", duration: Time.twoHours)
+            addDurationSelectionAction(to: alert, key: "share_location_for_six_hours", duration: Time.sixHours)
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+            self.present(alert, animated: true, completion: nil)
+        }
+    }
+
+    private func addDurationSelectionAction(to alert: UIAlertController, key: String, duration: Int) {
+        let action = UIAlertAction(title: String.localized(key), style: .default, handler: { _ in
+            self.locationStreamingFor(seconds: duration)
+        })
+        alert.addAction(action)
+    }
+
+    private func locationStreamingFor(seconds: Int) {
+        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
+            return
+        }
+        self.dcContext.sendLocationsToChat(chatId: self.chatId, seconds: seconds)
+        appDelegate.locationManager.shareLocation(chatId: self.chatId, duration: seconds)
+    }
+
+    func updateMessage(_ messageId: Int) {
+        if messageIds.firstIndex(where: { $0 == messageId }) != nil {
+            dcContext.markSeenMessages(messageIds: [UInt32(messageId)])
+            let wasLastSectionVisible = self.isLastRowVisible()
+            tableView.reloadData()
+            if wasLastSectionVisible {
+                self.scrollToBottom(animated: true)
+            }
+        } else {
+            let msg = DcMsg(id: messageId)
+            if msg.chatId == chatId {
+                insertMessage(msg)
+            }
+        }
+    }
+
+    func insertMessage(_ message: DcMsg) {
+        dcContext.markSeenMessages(messageIds: [UInt32(message.id)])
+        messageIds.append(message.id)
+        emptyStateView.isHidden = true
+
+        let wasLastSectionVisible = isLastRowVisible()
+        tableView.reloadData()
+        if wasLastSectionVisible || message.isFromCurrentSender {
+            scrollToBottom(animated: true)
+        }
+    }
+
+    private func sendTextMessage(message: String) {
+        DispatchQueue.global().async {
+            self.dcContext.sendTextInChat(id: self.chatId, message: message)
+        }
+    }
+
+    private func sendImage(_ image: UIImage, message: String? = nil) {
+        DispatchQueue.global().async {
+            if let path = DcUtils.saveImage(image: image) {
+                self.sendImageMessage(viewType: DC_MSG_IMAGE, image: image, filePath: path)
+            }
+        }
+    }
+
+    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 sendImageMessage(viewType: Int32, image: UIImage, filePath: String, message: String? = 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)
+        }
+    }
+
+    private func sendVoiceMessage(url: NSURL) {
+        DispatchQueue.global().async {
+            let msg = DcMsg(viewType: DC_MSG_VOICE)
+            msg.setFile(filepath: url.relativePath, mimeType: "audio/m4a")
+            msg.sendInChat(id: self.chatId)
+        }
+    }
+
+    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 = [
+            UIMenuItem(title: String.localized("info"), action: #selector(BaseMessageCell.messageInfo)),
+            UIMenuItem(title: String.localized("delete"), action: #selector(BaseMessageCell.messageDelete)),
+            UIMenuItem(title: String.localized("forward"), action: #selector(BaseMessageCell.messageForward))
+        ]
+        UIMenuController.shared.update()
+    }
+
+    override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
+        return !DcMsg(id: messageIds[indexPath.row]).isInfo 
+    }
+
+    override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
+        return action == #selector(UIResponderStandardEditActions.copy(_:))
+            || action == #selector(BaseMessageCell.messageInfo)
+            || action == #selector(BaseMessageCell.messageDelete)
+            || action == #selector(BaseMessageCell.messageForward)
+    }
+
+    override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
+        // handle standard actions here, but custom actions never trigger this. it still needs to be present for the menu to display, though.
+        switch action {
+        case #selector(copy(_:)):
+            let id = messageIds[indexPath.row]
+            let msg = DcMsg(id: id)
+
+            let pasteboard = UIPasteboard.general
+            if msg.type == DC_MSG_TEXT {
+                pasteboard.string = msg.text
+            } else {
+                pasteboard.string = msg.summary(chars: 10000000)
+            }
+        case #selector(BaseMessageCell.messageInfo(_:)):
+            let msg = DcMsg(id: messageIds[indexPath.row])
+            let msgViewController = MessageInfoViewController(dcContext: dcContext, message: msg)
+            if let ctrl = navigationController {
+                ctrl.pushViewController(msgViewController, animated: true)
+            }
+        case #selector(BaseMessageCell.messageDelete(_:)):
+            let msg = DcMsg(id: messageIds[indexPath.row])
+            askToDeleteMessage(id: msg.id)
+
+        case #selector(BaseMessageCell.messageForward(_:)):
+            let msg = DcMsg(id: messageIds[indexPath.row])
+            RelayHelper.sharedInstance.setForwardMessage(messageId: msg.id)
+            navigationController?.popViewController(animated: true)
+        default:
+            break
+        }
+    }
+
+    func showMediaGalleryFor(indexPath: IndexPath) {
+        let messageId = messageIds[indexPath.row]
+        let message = DcMsg(id: messageId)
+        showMediaGalleryFor(message: message)
+    }
+
+    func showMediaGalleryFor(message: DcMsg) {
+        if let url = message.fileURL {
+            // find all other messages with same message type
+            let previousUrls: [URL] = message.previousMediaURLs()
+            let nextUrls: [URL] = message.nextMediaURLs()
+            // these are the files user will be able to swipe trough
+            let mediaUrls: [URL] = previousUrls + [url] + nextUrls
+            showMediaGallery(currentIndex: previousUrls.count, mediaUrls: mediaUrls)
+        }
+    }
+
+    private func didTapAsm(msg: DcMsg, orgText: String) {
+        let inputDlg = UIAlertController(
+            title: String.localized("autocrypt_continue_transfer_title"),
+            message: String.localized("autocrypt_continue_transfer_please_enter_code"),
+            preferredStyle: .alert)
+        inputDlg.addTextField(configurationHandler: { (textField) in
+            textField.placeholder = msg.setupCodeBegin + ".."
+            textField.text = orgText
+            textField.keyboardType = UIKeyboardType.numbersAndPunctuation // allows entering spaces; decimalPad would require a mask to keep things readable
+        })
+        inputDlg.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+
+        let okAction = UIAlertAction(title: String.localized("ok"), style: .default, handler: { _ in
+            let textField = inputDlg.textFields![0]
+            let modText = textField.text ?? ""
+            let success = self.dcContext.continueKeyTransfer(msgId: msg.id, setupCode: modText)
+
+            let alert = UIAlertController(
+                title: String.localized("autocrypt_continue_transfer_title"),
+                message: String.localized(success ? "autocrypt_continue_transfer_succeeded" : "autocrypt_bad_setup_code"),
+                preferredStyle: .alert)
+            if success {
+                alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: nil))
+            } else {
+                alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .cancel, handler: nil))
+                let retryAction = UIAlertAction(title: String.localized("autocrypt_continue_transfer_retry"), style: .default, handler: { _ in
+                    self.didTapAsm(msg: msg, orgText: modText)
+                })
+                alert.addAction(retryAction)
+                alert.preferredAction = retryAction
+            }
+            self.navigationController?.present(alert, animated: true, completion: nil)
+        })
+
+        inputDlg.addAction(okAction)
+        inputDlg.preferredAction = okAction // without setting preferredAction, cancel become shown *bold* as the preferred action
+        navigationController?.present(inputDlg, animated: true, completion: nil)
+    }
+
+}
+
+// MARK: - BaseMessageCellDelegate
+extension ChatViewControllerNew: BaseMessageCellDelegate {
+    func phoneNumberTapped(number: String) {
+        logger.debug("phone number tapped \(number)")
+    }
+
+    func commandTapped(command: String) {
+        logger.debug("command tapped \(command)")
+    }
+
+    func urlTapped(url: URL) {
+        if Utils.isEmail(url: url) {
+            logger.debug("tapped on contact")
+            let email = Utils.getEmailFrom(url)
+            self.askToChatWith(email: email)
+            ///TODO: implement handling
+        } else {
+            UIApplication.shared.open(url)
+        }
+    }
+
+    func imageTapped(indexPath: IndexPath) {
+        showMediaGalleryFor(indexPath: indexPath)
+    }
+    func avatarTapped(indexPath: IndexPath) {
+        let message = DcMsg(id: messageIds[indexPath.row])
+        let contactDetailController = ContactDetailViewController(dcContext: dcContext, contactId: message.fromContactId)
+        navigationController?.pushViewController(contactDetailController, animated: true)
+    }
+}
+
+// MARK: - MediaPickerDelegate
+extension ChatViewControllerNew: MediaPickerDelegate {
+    func onVideoSelected(url: NSURL) {
+        sendVideo(url: url)
+    }
+
+    func onImageSelected(url: NSURL) {
+        sendImage(url: url)
+    }
+
+    func onImageSelected(image: UIImage) {
+        sendImage(image)
+    }
+
+    func onVoiceMessageRecorded(url: NSURL) {
+        sendVoiceMessage(url: url)
+    }
+
+    func onDocumentSelected(url: NSURL) {
+        sendDocumentMessage(url: url)
+    }
+
+}
+
+// MARK: - MessageInputBarDelegate
+extension ChatViewControllerNew: InputBarAccessoryViewDelegate {
+    func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
+        if inputBar.inputTextView.images.isEmpty {
+            self.sendTextMessage(message: text.trimmingCharacters(in: .whitespacesAndNewlines))
+        } else {
+            let trimmedText = text.replacingOccurrences(of: "\u{FFFC}", with: "", options: .literal, range: nil)
+                .trimmingCharacters(in: .whitespacesAndNewlines)
+            // 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
+    }
+}

+ 211 - 0
deltachat-ios/Chat/NewAudioController.swift

@@ -0,0 +1,211 @@
+import UIKit
+import AVFoundation
+import DcCore
+
+/// The `PlayerState` indicates the current audio controller state
+public enum PlayerState {
+
+    /// The audio controller is currently playing a sound
+    case playing
+
+    /// The audio controller is currently in pause state
+    case pause
+
+    /// The audio controller is not playing any sound and audioPlayer is nil
+    case stopped
+}
+
+/// The `NewAudioController` update UI for current audio cell that is playing a sound
+/// and also creates and manage an `AVAudioPlayer` states, play, pause and stop.
+open class NewAudioController: NSObject, AVAudioPlayerDelegate, NewAudioMessageCellDelegate {
+
+    lazy var audioSession: AVAudioSession = {
+        let audioSession = AVAudioSession.sharedInstance()
+        _ = try? audioSession.setCategory(AVAudioSession.Category.playback, options: [.defaultToSpeaker])
+        return audioSession
+    }()
+
+    /// The `AVAudioPlayer` that is playing the sound
+    open var audioPlayer: AVAudioPlayer?
+
+    /// The `AudioMessageCell` that is currently playing sound
+    open weak var playingCell: NewAudioMessageCell?
+
+    /// The `MessageType` that is currently playing sound
+    open var playingMessage: DcMsg?
+
+    /// Specify if current audio controller state: playing, in pause or none
+    open private(set) var state: PlayerState = .stopped
+
+    private let dcContext: DcContext
+    private let chatId: Int
+    private let chat: DcChat
+
+    /// The `Timer` that update playing progress
+    internal var progressTimer: Timer?
+
+    // MARK: - Init Methods
+
+    public init(dcContext: DcContext, chatId: Int) {
+        self.dcContext = dcContext
+        self.chatId = chatId
+        self.chat = dcContext.getChat(chatId: chatId)
+        super.init()
+        NotificationCenter.default.addObserver(self,
+                                               selector: #selector(audioRouteChanged),
+                                               name: AVAudioSession.routeChangeNotification,
+                                               object: AVAudioSession.sharedInstance())
+    }
+
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+    }
+
+    // MARK: - Methods
+
+    /// - Parameters:
+    ///   - cell: The `NewAudioMessageCell` that needs to be configure.
+    ///   - message: The `DcMsg` that configures the cell.
+    ///
+    /// - Note:
+    ///   This protocol method is called by MessageKit every time an audio cell needs to be configure
+    func update(_ cell: NewAudioMessageCell, with messageId: Int) {
+        cell.delegate = self
+        if playingMessage?.id == messageId, let player = audioPlayer {
+            playingCell = cell
+            cell.audioPlayerView.setProgress((player.duration == 0) ? 0 : Float(player.currentTime/player.duration))
+            cell.audioPlayerView.showPlayLayout((player.isPlaying == true) ? true : false)
+            cell.audioPlayerView.setDuration(duration: player.currentTime)
+        }
+    }
+
+    public func playButtonTapped(cell: NewAudioMessageCell, messageId: Int) {
+            let message = DcMsg(id: messageId)
+            guard state != .stopped else {
+                // There is no audio sound playing - prepare to start playing for given audio message
+                playSound(for: message, in: cell)
+                return
+            }
+            if playingMessage?.messageId == message.messageId {
+                // tap occur in the current cell that is playing audio sound
+                if state == .playing {
+                    pauseSound(in: cell)
+                } else {
+                    resumeSound()
+                }
+            } else {
+                // tap occur in a difference cell that the one is currently playing sound. First stop currently playing and start the sound for given message
+                stopAnyOngoingPlaying()
+                playSound(for: message, in: cell)
+            }
+    }
+
+    /// Used to start play audio sound
+    ///
+    /// - Parameters:
+    ///   - message: The `DcMsg` that contain the audio item to be played.
+    ///   - audioCell: The `NewAudioMessageCell` that needs to be updated while audio is playing.
+    open func playSound(for message: DcMsg, in audioCell: NewAudioMessageCell) {
+        if message.type == DC_MSG_AUDIO || message.type == DC_MSG_VOICE {
+            _ = try? audioSession.setActive(true)
+            playingCell = audioCell
+            playingMessage = message
+            if let fileUrl = message.fileURL, let player = try? AVAudioPlayer(contentsOf: fileUrl) {
+                audioPlayer = player
+                audioPlayer?.prepareToPlay()
+                audioPlayer?.delegate = self
+                audioPlayer?.play()
+                state = .playing
+                audioCell.audioPlayerView.showPlayLayout(true)  // show pause button on audio cell
+                startProgressTimer()
+            }
+
+            print("NewAudioController failed play sound becasue given message kind is not Audio")
+        }
+    }
+
+    /// Used to pause the audio sound
+    ///
+    /// - Parameters:
+    ///   - message: The `MessageType` that contain the audio item to be pause.
+    ///   - audioCell: The `AudioMessageCell` that needs to be updated by the pause action.
+    open func pauseSound(in audioCell: NewAudioMessageCell) {
+        audioPlayer?.pause()
+        state = .pause
+        audioCell.audioPlayerView.showPlayLayout(false) // show play button on audio cell
+        progressTimer?.invalidate()
+    }
+
+    /// Stops any ongoing audio playing if exists
+    open func stopAnyOngoingPlaying() {
+        // If the audio player is nil then we don't need to go through the stopping logic
+        guard let player = audioPlayer else { return }
+        player.stop()
+        state = .stopped
+        if let cell = playingCell {
+            cell.audioPlayerView.setProgress(0.0)
+            cell.audioPlayerView.showPlayLayout(false)
+            cell.audioPlayerView.setDuration(duration: player.duration)
+        }
+        progressTimer?.invalidate()
+        progressTimer = nil
+        audioPlayer = nil
+        playingMessage = nil
+        playingCell = nil
+        try? audioSession.setActive(false)
+    }
+
+    /// Resume a currently pause audio sound
+    open func resumeSound() {
+        guard let player = audioPlayer, let cell = playingCell else {
+            stopAnyOngoingPlaying()
+            return
+        }
+        player.prepareToPlay()
+        player.play()
+        state = .playing
+        startProgressTimer()
+        cell.audioPlayerView.showPlayLayout(true) // show pause button on audio cell
+    }
+
+    // MARK: - Fire Methods
+    @objc private func didFireProgressTimer(_ timer: Timer) {
+        guard let player = audioPlayer, let cell = playingCell else {
+            return
+        }
+        cell.audioPlayerView.setProgress((player.duration == 0) ? 0 : Float(player.currentTime/player.duration))
+        cell.audioPlayerView.setDuration(duration: player.currentTime)
+    }
+
+    // MARK: - Private Methods
+    private func startProgressTimer() {
+        progressTimer?.invalidate()
+        progressTimer = nil
+        progressTimer = Timer.scheduledTimer(timeInterval: 0.1,
+                                             target: self,
+                                             selector: #selector(NewAudioController.didFireProgressTimer(_:)),
+                                             userInfo: nil,
+                                             repeats: true)
+    }
+
+    // MARK: - AVAudioPlayerDelegate
+    open func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        stopAnyOngoingPlaying()
+    }
+
+    open func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
+        stopAnyOngoingPlaying()
+    }
+
+    // MARK: - AVAudioSession.routeChangeNotification handler
+    @objc func audioRouteChanged(note: Notification) {
+      if let userInfo = note.userInfo {
+        if let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? Int {
+            if reason == AVAudioSession.RouteChangeReason.oldDeviceUnavailable.rawValue {
+            // headphones plugged out
+            resumeSound()
+          }
+        }
+      }
+    }
+}

+ 38 - 0
deltachat-ios/Chat/Views/BackgroundContainer.swift

@@ -0,0 +1,38 @@
+
+
+import Foundation
+import UIKit
+import DcCore
+
+class BackgroundContainer: UIImageView {
+
+    var rectCorners: UIRectCorner?
+
+    func update(rectCorners: UIRectCorner, color: UIColor) {
+        self.rectCorners = rectCorners
+        image = UIImage(color: color)
+        setNeedsLayout()
+        layoutIfNeeded()
+    }
+
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        applyPath()
+    }
+
+    func applyPath() {
+        let radius: CGFloat = 16
+        let path = UIBezierPath(roundedRect: bounds,
+                                byRoundingCorners: rectCorners ?? UIRectCorner(),
+                                cornerRadii: CGSize(width: radius, height: radius))
+        let mask = CAShapeLayer()
+        mask.path = path.cgPath
+        layer.mask = mask
+    }
+
+    func prepareForReuse() {
+        layer.mask = nil
+        image = nil
+    }
+
+}

+ 413 - 0
deltachat-ios/Chat/Views/Cells/BaseMessageCell.swift

@@ -0,0 +1,413 @@
+import UIKit
+import DcCore
+
+public class BaseMessageCell: UITableViewCell {
+
+    private var leadingConstraint: NSLayoutConstraint?
+    private var trailingConstraint: NSLayoutConstraint?
+    private var leadingConstraintCurrentSender: NSLayoutConstraint?
+    private var leadingConstraintGroup: NSLayoutConstraint?
+    private var trailingConstraintCurrentSender: NSLayoutConstraint?
+    private var mainContentBelowTopLabelConstraint: NSLayoutConstraint?
+    private var mainContentUnderTopLabelConstraint: NSLayoutConstraint?
+    private var mainContentAboveBottomLabelConstraint: NSLayoutConstraint?
+    private var mainContentUnderBottomLabelConstraint: NSLayoutConstraint?
+    private var bottomLineLeftAlignedConstraint: [NSLayoutConstraint] = []
+    private var bottomLineRightAlignedConstraint: [NSLayoutConstraint] = []
+    private var mainContentViewLeadingConstraint: NSLayoutConstraint?
+    private var mainContentViewTrailingConstraint: NSLayoutConstraint?
+
+    public var mainContentViewHorizontalPadding: CGFloat {
+        set {
+            mainContentViewLeadingConstraint?.constant = newValue
+            mainContentViewTrailingConstraint?.constant = -newValue
+        }
+        get {
+            return mainContentViewLeadingConstraint?.constant ?? 0
+        }
+    }
+
+    //aligns the bottomLabel to the left / right
+    private var bottomLineLeftAlign: Bool {
+        set {
+            for constraint in bottomLineLeftAlignedConstraint {
+                constraint.isActive = newValue
+            }
+            for constraint in bottomLineRightAlignedConstraint {
+                constraint.isActive = !newValue
+            }
+        }
+        get {
+            return !bottomLineLeftAlignedConstraint.isEmpty && bottomLineLeftAlignedConstraint[0].isActive
+        }
+    }
+
+    // if set to true topLabel overlaps the main content
+    public var topCompactView: Bool {
+        set {
+            mainContentBelowTopLabelConstraint?.isActive = !newValue
+            mainContentUnderTopLabelConstraint?.isActive = newValue
+            topLabel.backgroundColor = newValue ?
+                UIColor(alpha: 200, red: 50, green: 50, blue: 50) :
+                UIColor(alpha: 0, red: 0, green: 0, blue: 0)
+        }
+        get {
+            return mainContentUnderTopLabelConstraint?.isActive ?? false
+        }
+    }
+
+    // if set to true bottomLabel overlaps the main content
+    public var bottomCompactView: Bool {
+        set {
+            mainContentAboveBottomLabelConstraint?.isActive = !newValue
+            mainContentUnderBottomLabelConstraint?.isActive = newValue
+            bottomLabel.backgroundColor = newValue ?
+                UIColor(alpha: 200, red: 50, green: 50, blue: 50) :
+                UIColor(alpha: 0, red: 0, green: 0, blue: 0)
+        }
+        get {
+            return mainContentUnderBottomLabelConstraint?.isActive ?? false
+        }
+    }
+
+    public weak var baseDelegate: BaseMessageCellDelegate?
+
+    lazy var messageLabel: PaddingTextView = {
+        let view = PaddingTextView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.setContentHuggingPriority(.defaultLow, for: .vertical)
+        view.font = UIFont.preferredFont(for: .body, weight: .regular)
+        view.delegate = self
+        view.enabledDetectors = [.url, .phoneNumber]
+        let attributes: [NSAttributedString.Key: Any] = [
+            NSAttributedString.Key.foregroundColor: DcColors.defaultTextColor,
+            NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
+            NSAttributedString.Key.underlineColor: DcColors.defaultTextColor ]
+        view.label.setAttributes(attributes, detector: .url)
+        view.label.setAttributes(attributes, detector: .phoneNumber)
+        view.isUserInteractionEnabled = true
+        return view
+    }()
+
+    lazy var avatarView: InitialsBadge = {
+        let view = InitialsBadge(size: 28)
+        view.setColor(UIColor.gray)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+        view.isHidden = true
+        view.isUserInteractionEnabled = true
+        return view
+    }()
+
+    lazy var topLabel: PaddingTextView = {
+        let view = PaddingTextView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.text = "title"
+        view.font = UIFont.preferredFont(for: .caption1, weight: .regular)
+        view.layer.cornerRadius = 4
+        view.clipsToBounds = true
+        view.paddingLeading = 4
+        view.paddingTrailing = 4
+        return view
+    }()
+
+    lazy var mainContentView: UIStackView = {
+        let view = UIStackView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.axis = .vertical
+        return view
+    }()
+
+    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
+        return label
+    }()
+
+    private lazy var messageBackgroundContainer: BackgroundContainer = {
+        let container = BackgroundContainer()
+        container.image = UIImage(color: UIColor.blue)
+        container.contentMode = .scaleToFill
+        container.clipsToBounds = true
+        container.translatesAutoresizingMaskIntoConstraints = false
+        container.isUserInteractionEnabled = true
+        return container
+    }()
+
+    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(messageBackgroundContainer)
+        messageBackgroundContainer.addSubview(mainContentView)
+        messageBackgroundContainer.addSubview(topLabel)
+        messageBackgroundContainer.addSubview(bottomLabel)
+        contentView.addSubview(avatarView)
+
+        contentView.addConstraints([
+            avatarView.constraintAlignLeadingTo(contentView),
+            avatarView.constraintAlignBottomTo(contentView),
+            avatarView.constraintWidthTo(28, priority: .defaultHigh),
+            avatarView.constraintHeightTo(28, priority: .defaultHigh),
+            topLabel.constraintAlignTopTo(messageBackgroundContainer, paddingTop: 6),
+            topLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 8),
+            topLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 8),
+            bottomLabel.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 6),
+            messageBackgroundContainer.constraintAlignTopTo(contentView, paddingTop: 6),
+            messageBackgroundContainer.constraintAlignBottomTo(contentView),
+        ])
+
+        leadingConstraint = messageBackgroundContainer.constraintAlignLeadingTo(contentView, paddingLeading: 6)
+        leadingConstraintGroup = messageBackgroundContainer.constraintToTrailingOf(avatarView, paddingLeading: 2)
+        trailingConstraint = messageBackgroundContainer.constraintAlignTrailingMaxTo(contentView, paddingTrailing: 36)
+        leadingConstraintCurrentSender = messageBackgroundContainer.constraintAlignLeadingMaxTo(contentView, paddingLeading: 36)
+        trailingConstraintCurrentSender = messageBackgroundContainer.constraintAlignTrailingTo(contentView, paddingTrailing: 6)
+
+        mainContentViewLeadingConstraint = mainContentView.constraintAlignLeadingTo(messageBackgroundContainer)
+        mainContentViewTrailingConstraint = mainContentView.constraintAlignTrailingTo(messageBackgroundContainer)
+        mainContentViewLeadingConstraint?.isActive = true
+        mainContentViewTrailingConstraint?.isActive = true
+
+        mainContentBelowTopLabelConstraint = mainContentView.constraintToBottomOf(topLabel, paddingTop: 6)
+        mainContentUnderTopLabelConstraint = mainContentView.constraintAlignTopTo(messageBackgroundContainer)
+        mainContentAboveBottomLabelConstraint = bottomLabel.constraintToBottomOf(mainContentView, paddingTop: 6, priority: .defaultHigh)
+        mainContentUnderBottomLabelConstraint = mainContentView.constraintAlignBottomTo(messageBackgroundContainer, paddingBottom: 0, priority: .defaultHigh)
+
+        bottomLineRightAlignedConstraint = [bottomLabel.constraintAlignLeadingMaxTo(messageBackgroundContainer, paddingLeading: 8),
+                                           bottomLabel.constraintAlignTrailingTo(messageBackgroundContainer, paddingTrailing: 8)]
+        bottomLineLeftAlignedConstraint = [bottomLabel.constraintAlignLeadingTo(messageBackgroundContainer, paddingLeading: 8),
+                                           bottomLabel.constraintAlignTrailingMaxTo(messageBackgroundContainer, paddingTrailing: 8)]
+
+        topCompactView = false
+        bottomCompactView = false
+        selectionStyle = .none
+
+        let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onAvatarTapped))
+        gestureRecognizer.numberOfTapsRequired = 1
+        avatarView.addGestureRecognizer(gestureRecognizer)
+
+        let messageLabelGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
+        gestureRecognizer.numberOfTapsRequired = 1
+        messageLabel.addGestureRecognizer(messageLabelGestureRecognizer)
+    }
+
+    @objc
+    open func handleTapGesture(_ gesture: UIGestureRecognizer) {
+        guard gesture.state == .ended else { return }
+
+        let touchLocation = gesture.location(in: messageLabel)
+        _ = messageLabel.label.handleGesture(touchLocation)
+    }
+
+    @objc func onAvatarTapped() {
+        if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
+            baseDelegate?.avatarTapped(indexPath: indexPath)
+        }
+    }
+
+    // update classes inheriting BaseMessageCell first before calling super.update(...)
+    func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
+        if msg.isFromCurrentSender {
+            topLabel.text = nil
+            leadingConstraint?.isActive = false
+            leadingConstraintGroup?.isActive = false
+            trailingConstraint?.isActive = false
+            bottomLineLeftAlign = false
+            leadingConstraintCurrentSender?.isActive = true
+            trailingConstraintCurrentSender?.isActive = true
+
+        } else {
+            topLabel.text = isGroup ? msg.fromContact.displayName : nil
+            leadingConstraintCurrentSender?.isActive = false
+            trailingConstraintCurrentSender?.isActive = false
+            if isGroup {
+                leadingConstraint?.isActive = false
+                leadingConstraintGroup?.isActive = true
+            } else {
+                leadingConstraintGroup?.isActive = false
+                leadingConstraint?.isActive = true
+            }
+            trailingConstraint?.isActive = true
+            bottomLineLeftAlign = true
+        }
+
+        if isAvatarVisible {
+            avatarView.isHidden = false
+            avatarView.setName(msg.fromContact.displayName)
+            avatarView.setColor(msg.fromContact.color)
+            if let profileImage = msg.fromContact.profileImage {
+                avatarView.setImage(profileImage)
+            }
+        } else {
+            avatarView.isHidden = true
+        }
+
+        messageBackgroundContainer.update(rectCorners: messageStyle,
+                                          color: msg.isFromCurrentSender ? DcColors.messagePrimaryColor : DcColors.messageSecondaryColor)
+
+        if !msg.isInfo {
+            bottomLabel.attributedText = getFormattedBottomLine(message: msg)
+        }
+        messageLabel.delegate = self
+    }
+
+    func getFormattedBottomLine(message: DcMsg) -> NSAttributedString {
+        var timestampAttributes: [NSAttributedString.Key: Any] = [
+            .font: UIFont.preferredFont(for: .caption1, weight: .regular),
+            .foregroundColor: DcColors.grayDateColor,
+            .paragraphStyle: NSParagraphStyle()
+        ]
+
+        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
+            }
+
+            text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
+
+            if message.showPadlock() {
+                attachPadlock(to: text)
+            }
+
+            attachSendingState(message.state, to: text)
+            return text
+        }
+
+        text.append(NSAttributedString(string: message.formattedSentDate(), attributes: timestampAttributes))
+        if message.showPadlock() {
+            attachPadlock(to: text)
+        }
+        return text
+    }
+
+    private func attachPadlock(to text: NSMutableAttributedString) {
+        let imageAttachment = NSTextAttachment()
+        imageAttachment.image = UIImage(named: "ic_lock")
+        imageAttachment.image?.accessibilityIdentifier = String.localized("encrypted_message")
+        let imageString = NSMutableAttributedString(attachment: imageAttachment)
+        imageString.addAttributes([NSAttributedString.Key.baselineOffset: -1], range: NSRange(location: 0, length: 1))
+        text.append(NSAttributedString(string: " "))
+        text.append(imageString)
+    }
+
+    private func attachSendingState(_ state: Int, to text: NSMutableAttributedString) {
+        let imageAttachment = NSTextAttachment()
+        var offset = -4
+
+
+        switch Int32(state) {
+        case DC_STATE_OUT_PENDING, DC_STATE_OUT_PREPARING:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_hourglass_empty_white_36pt").scaleDownImage(toMax: 16)?.maskWithColor(color: DcColors.grayDateColor)
+            imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_sending")
+            offset = -2
+        case DC_STATE_OUT_DELIVERED:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_done_36pt").scaleDownImage(toMax: 18)
+            imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_delivered")
+        case DC_STATE_OUT_MDN_RCVD:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_done_all_36pt").scaleDownImage(toMax: 18)
+            imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_read")
+            text.append(NSAttributedString(string: " "))
+        case DC_STATE_OUT_FAILED:
+            imageAttachment.image = #imageLiteral(resourceName: "ic_error_36pt").scaleDownImage(toMax: 16)
+            imageAttachment.image?.accessibilityIdentifier = String.localized("a11y_delivery_status_error")
+            offset = -2
+        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() {
+        textLabel?.text = nil
+        textLabel?.attributedText = nil
+        topLabel.text = nil
+        topLabel.attributedText = nil
+        avatarView.reset()
+        messageBackgroundContainer.prepareForReuse()
+        bottomLabel.text = nil
+        bottomLabel.attributedText = nil
+        baseDelegate = nil
+        messageLabel.text = nil
+        messageLabel.attributedText = nil
+        messageLabel.delegate = nil
+    }
+
+    // MARK: - Context menu
+    @objc func messageInfo(_ sender: Any?) {
+        self.performAction(#selector(BaseMessageCell.messageInfo(_:)), with: sender)
+    }
+
+    @objc func messageDelete(_ sender: Any?) {
+        self.performAction(#selector(BaseMessageCell.messageDelete(_:)), with: sender)
+    }
+
+    @objc func messageForward(_ sender: Any?) {
+        self.performAction(#selector(BaseMessageCell.messageForward(_:)), with: sender)
+    }
+
+    func performAction(_ action: Selector, with sender: Any?) {
+        if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
+            // Trigger action in tableView delegate (UITableViewController)
+            tableView.delegate?.tableView?(tableView,
+                                           performAction: action,
+                                           forRowAt: indexPath,
+                                           withSender: sender)
+        }
+    }
+}
+
+extension BaseMessageCell: MessageLabelDelegate {
+    public func didSelectAddress(_ addressComponents: [String: String]) {}
+
+    public func didSelectDate(_ date: Date) {}
+
+    public func didSelectPhoneNumber(_ phoneNumber: String) {
+        baseDelegate?.phoneNumberTapped(number: phoneNumber)
+    }
+
+    public func didSelectURL(_ url: URL) {
+        logger.debug("did select URL")
+        baseDelegate?.urlTapped(url: url)
+    }
+
+    public func didSelectTransitInformation(_ transitInformation: [String: String]) {}
+
+    public func didSelectMention(_ mention: String) {}
+
+    public func didSelectHashtag(_ hashtag: String) {}
+
+    public func didSelectCustom(_ pattern: String, match: String?) {}
+}
+
+// MARK: - BaseMessageCellDelegate
+// this delegate contains possible events from base cells or from derived cells
+public protocol BaseMessageCellDelegate: class {
+    func commandTapped(command: String) // `/command`
+    func phoneNumberTapped(number: String)
+    func urlTapped(url: URL) // url is eg. `https://foo.bar`
+    func imageTapped(indexPath: IndexPath)
+    func avatarTapped(indexPath: IndexPath)
+
+}

+ 60 - 0
deltachat-ios/Chat/Views/Cells/NewAudioMessageCell.swift

@@ -0,0 +1,60 @@
+import UIKit
+import DcCore
+
+// NewAudioMessageCellDelegate is for sending events to NewAudioController.
+// do not confuse with BaseMessageCellDelegate that is for sending events to ChatViewControllerNew.
+public protocol NewAudioMessageCellDelegate: AnyObject {
+    func playButtonTapped(cell: NewAudioMessageCell, messageId: Int)
+}
+
+public class NewAudioMessageCell: BaseMessageCell {
+
+    public weak var delegate: NewAudioMessageCellDelegate?
+
+    lazy var audioPlayerView: NewAudioPlayerView = {
+        let view = NewAudioPlayerView()
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    private var messageId: Int = 0
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        let spacerView = UIView()
+        spacerView.translatesAutoresizingMaskIntoConstraints = false
+        mainContentView.addArrangedSubview(audioPlayerView)
+        mainContentView.addArrangedSubview(messageLabel)
+        messageLabel.paddingLeading = 12
+        messageLabel.paddingTrailing = 12
+        audioPlayerView.constraintWidthTo(250).isActive = true
+        let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onPlayButtonTapped))
+        gestureRecognizer.numberOfTapsRequired = 1
+        audioPlayerView.playButton.addGestureRecognizer(gestureRecognizer)
+    }
+
+    @objc public func onPlayButtonTapped() {
+        delegate?.playButtonTapped(cell: self, messageId: messageId)
+    }
+
+    override func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
+        //audioPlayerView.reset()
+        messageId = msg.id
+        if let text = msg.text {
+            mainContentView.spacing = text.isEmpty ? 0 : 8
+            messageLabel.text = text
+        } else {
+            mainContentView.spacing = 0
+        }
+
+        super.update(msg: msg, messageStyle: messageStyle, isAvatarVisible: isAvatarVisible, isGroup: isGroup)
+    }
+
+    public override func prepareForReuse() {
+        super.prepareForReuse()
+        mainContentView.spacing = 0
+        messageId = 0
+        delegate = nil
+        audioPlayerView.reset()
+    }
+}

+ 125 - 0
deltachat-ios/Chat/Views/Cells/NewFileTextCell.swift

@@ -0,0 +1,125 @@
+import Foundation
+import UIKit
+import DcCore
+import SDWebImage
+
+class NewFileTextCell: 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
+    }()
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        let spacerView = UIView()
+        spacer = spacerView.constraintHeightTo(8, priority: .defaultHigh)
+        spacer?.isActive = true
+        mainContentView.addArrangedSubview(fileStackView)
+        mainContentView.addArrangedSubview(spacerView)
+        mainContentView.addArrangedSubview(messageLabel)
+        imageWidthConstraint = fileImageView.constraintWidthTo(50)
+        imageHeightConstraint = fileImageView.constraintHeightTo(50 * 1.3, priority: .defaultLow)
+        horizontalLayout = true
+        mainContentViewHorizontalPadding = 12
+    }
+
+    override func prepareForReuse() {
+        fileImageView.image = nil
+    }
+
+    override func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
+        if let text = msg.text, !text.isEmpty {
+            messageLabel.text = text
+            spacer?.isActive = true
+        } 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()
+        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
+        }
+    }
+    
+}

+ 145 - 0
deltachat-ios/Chat/Views/Cells/NewImageTextCell.swift

@@ -0,0 +1,145 @@
+import Foundation
+import UIKit
+import DcCore
+import SDWebImage
+
+class NewImageTextCell: BaseMessageCell {
+
+    var imageHeightConstraint: NSLayoutConstraint?
+    var imageWidthConstraint: NSLayoutConstraint?
+
+    lazy var contentImageView: SDAnimatedImageView = {
+        let imageView = SDAnimatedImageView()
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+        imageView.setContentHuggingPriority(.defaultHigh, for: .vertical)
+        imageView.isUserInteractionEnabled = true
+        imageView.contentMode = .scaleAspectFill
+        imageView.clipsToBounds = true
+        return imageView
+    }()
+
+    /// The play button view to display on video messages.
+    open lazy var playButtonView: PlayButtonView = {
+        let playButtonView = PlayButtonView()
+        playButtonView.isHidden = true
+        translatesAutoresizingMaskIntoConstraints = false
+        return playButtonView
+    }()
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        contentImageView.addSubview(playButtonView)
+        playButtonView.centerInSuperview()
+        playButtonView.constraint(equalTo: CGSize(width: 50, height: 50))
+        mainContentView.addArrangedSubview(contentImageView)
+        mainContentView.addArrangedSubview(messageLabel)
+        messageLabel.paddingLeading = 12
+        messageLabel.paddingTrailing = 12
+        contentImageView.constraintAlignLeadingMaxTo(mainContentView).isActive = true
+        contentImageView.constraintAlignTrailingMaxTo(mainContentView).isActive = true
+        topCompactView = true
+        let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onImageTapped))
+        gestureRecognizer.numberOfTapsRequired = 1
+        contentImageView.addGestureRecognizer(gestureRecognizer)
+    }
+
+    override func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
+        messageLabel.text = msg.text
+        bottomCompactView = msg.text?.isEmpty ?? true
+        mainContentView.spacing = msg.text?.isEmpty ?? false ? 0 : 6
+        tag = msg.id
+        if msg.type == DC_MSG_IMAGE, let image = msg.image {
+            contentImageView.image = image
+            playButtonView.isHidden = true
+            setAspectRatioFor(message: msg)
+        } else if msg.type == DC_MSG_GIF, let url = msg.fileURL {
+            contentImageView.sd_setImage(with: url,
+                                         placeholderImage: UIImage(color: UIColor.init(alpha: 0,
+                                                                                       red: 255,
+                                                                                       green: 255,
+                                                                                       blue: 255),
+                                                                   size: CGSize(width: 500, height: 500)))
+            playButtonView.isHidden = true
+            setAspectRatioFor(message: msg)
+        } else if msg.type == DC_MSG_VIDEO, let url = msg.fileURL {
+            playButtonView.isHidden = false
+            if let image = ThumbnailCache.shared.restoreImage(key: url.absoluteString) {
+                contentImageView.image = image
+                setAspectRatioFor(message: msg, with: image, isPlaceholder: false)
+            } else {
+                // no image in cache
+                let placeholderImage = UIImage(color: UIColor.init(alpha: 0,
+                                                                   red: 255,
+                                                                   green: 255,
+                                                                   blue: 255),
+                                               size: CGSize(width: 250, height: 250))
+                contentImageView.image = placeholderImage
+                DispatchQueue.global(qos: .userInteractive).async {
+                    let thumbnailImage = DcUtils.generateThumbnailFromVideo(url: url)
+                    if let thumbnailImage = thumbnailImage {
+                        DispatchQueue.main.async { [weak self] in
+                            if msg.id == self?.tag {
+                                self?.contentImageView.image = thumbnailImage
+                                ThumbnailCache.shared.storeImage(image: thumbnailImage, key: url.absoluteString)
+                            }
+                        }
+                    }
+                }
+                setAspectRatioFor(message: msg, with: placeholderImage, isPlaceholder: true)
+            }
+        }
+        super.update(msg: msg, messageStyle: messageStyle, isAvatarVisible: isAvatarVisible, isGroup: isGroup)
+    }
+
+    @objc func onImageTapped() {
+        if let tableView = self.superview as? UITableView, let indexPath = tableView.indexPath(for: self) {
+            baseDelegate?.imageTapped(indexPath: indexPath)
+        }
+    }
+
+    private func setAspectRatio(width: CGFloat, height: CGFloat) {
+        if height == 0 || width == 0 {
+            return
+        }
+        self.imageHeightConstraint?.isActive = false
+        self.imageWidthConstraint?.isActive = false
+        self.imageWidthConstraint = self.contentImageView.widthAnchor.constraint(lessThanOrEqualToConstant: width)
+        self.imageHeightConstraint = self.contentImageView.heightAnchor.constraint(
+            lessThanOrEqualTo: self.contentImageView.widthAnchor,
+            multiplier: height / width
+        )
+        self.imageHeightConstraint?.isActive = true
+        self.imageWidthConstraint?.isActive = true
+    }
+
+    private func setAspectRatioFor(message: DcMsg) {
+        var width = message.messageWidth
+        var height = message.messageHeight
+        if width == 0 || height == 0,
+           let image = message.image {
+            width = image.size.width
+            height = image.size.height
+            message.setLateFilingMediaSize(width: width, height: height, duration: 0)
+        }
+        setAspectRatio(width: width, height: height)
+    }
+
+    private func setAspectRatioFor(message: DcMsg, with image: UIImage?, isPlaceholder: Bool) {
+        var width = message.messageWidth
+        var height = message.messageHeight
+        if width == 0 || height == 0,
+           let image = image {
+            width = image.size.width
+            height = image.size.height
+            if !isPlaceholder {
+                message.setLateFilingMediaSize(width: width, height: height, duration: 0)
+            }
+        }
+        setAspectRatio(width: width, height: height)
+    }
+
+    override func prepareForReuse() {
+        contentImageView.image = nil
+        tag = -1
+    }
+}

+ 64 - 0
deltachat-ios/Chat/Views/Cells/NewInfoMessageCell.swift

@@ -0,0 +1,64 @@
+import UIKit
+import DcCore
+
+class NewInfoMessageCell: 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 messageLabel: UILabel = {
+        let label = UILabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.numberOfLines = 0
+        label.lineBreakMode = .byWordWrapping
+        label.textAlignment = .center
+        label.font = UIFont.preferredFont(for: .subheadline, weight: .medium)
+        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(messageBackgroundContainer)
+        contentView.addSubview(messageLabel)
+
+        contentView.addConstraints([
+            messageLabel.constraintAlignTopTo(contentView, paddingTop: 12),
+            messageLabel.constraintAlignBottomTo(contentView, paddingBottom: 12),
+            messageLabel.constraintAlignLeadingMaxTo(contentView, paddingLeading: 50),
+            messageLabel.constraintAlignTrailingMaxTo(contentView, paddingTrailing: 50),
+            messageLabel.constraintCenterXTo(contentView),
+            messageBackgroundContainer.constraintAlignLeadingTo(messageLabel, paddingLeading: -6),
+            messageBackgroundContainer.constraintAlignTopTo(messageLabel, paddingTop: -6),
+            messageBackgroundContainer.constraintAlignBottomTo(messageLabel, paddingBottom: -6),
+            messageBackgroundContainer.constraintAlignTrailingTo(messageLabel, paddingTrailing: -6)
+        ])
+        selectionStyle = .none
+    }
+
+    func update(msg: DcMsg) {
+        messageLabel.text = msg.text
+        var corners: UIRectCorner = []
+        corners.formUnion(.topLeft)
+        corners.formUnion(.bottomLeft)
+        corners.formUnion(.topRight)
+        corners.formUnion(.bottomRight)
+        messageBackgroundContainer.update(rectCorners: corners, color: DcColors.systemMessageBackgroundColor)
+    }
+
+}

+ 23 - 0
deltachat-ios/Chat/Views/Cells/NewTextMessageCell.swift

@@ -0,0 +1,23 @@
+import Foundation
+import DcCore
+import UIKit
+
+class NewTextMessageCell: BaseMessageCell {
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        mainContentView.addArrangedSubview(messageLabel)
+        messageLabel.paddingLeading = 12
+        messageLabel.paddingTrailing = 12
+    }
+
+    override func update(msg: DcMsg, messageStyle: UIRectCorner, isAvatarVisible: Bool, isGroup: Bool) {
+        messageLabel.text = msg.text
+        super.update(msg: msg, messageStyle: messageStyle, isAvatarVisible: isAvatarVisible, isGroup: isGroup)
+    }
+
+    override func prepareForReuse() {
+        super.prepareForReuse()
+    }
+    
+}

+ 26 - 0
deltachat-ios/Chat/Views/ChatTableView.swift

@@ -0,0 +1,26 @@
+import UIKit
+import InputBarAccessoryView
+
+class ChatTableView: UITableView {
+
+    var messageInputBar: InputBarAccessoryView?
+    override var inputAccessoryView: UIView? {
+        return messageInputBar
+    }
+
+
+    override var canBecomeFirstResponder: Bool {
+        return true
+    }
+
+
+    public init(messageInputBar: InputBarAccessoryView?) {
+        self.messageInputBar = messageInputBar
+        super.init(frame: .zero, style: .plain)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+}

+ 110 - 0
deltachat-ios/Chat/Views/NewAudioPlayerView.swift

@@ -0,0 +1,110 @@
+import Foundation
+import UIKit
+
+open class NewAudioPlayerView: UIView {
+
+    /// The play button view to display on audio messages.
+    lazy var playButton: UIButton = {
+        let playButton = UIButton(type: .custom)
+        let playImage = UIImage(named: "play")
+        let pauseImage = UIImage(named: "pause")
+        playButton.setImage(playImage?.withRenderingMode(.alwaysTemplate), for: .normal)
+        playButton.setImage(pauseImage?.withRenderingMode(.alwaysTemplate), for: .selected)
+        playButton.imageView?.contentMode = .scaleAspectFit
+        playButton.contentVerticalAlignment = .fill
+        playButton.contentHorizontalAlignment = .fill
+        playButton.translatesAutoresizingMaskIntoConstraints = false
+        playButton.isUserInteractionEnabled = true
+        return playButton
+    }()
+
+    /// The time duration lable to display on audio messages.
+    private lazy var durationLabel: UILabel = {
+        let durationLabel = UILabel(frame: CGRect.zero)
+        durationLabel.textAlignment = .right
+        durationLabel.font = UIFont.preferredFont(forTextStyle: .body)
+        durationLabel.adjustsFontForContentSizeCategory = true
+        durationLabel.text = "0:00"
+        durationLabel.translatesAutoresizingMaskIntoConstraints = false
+        return durationLabel
+    }()
+
+    private lazy var progressView: UIProgressView = {
+        let progressView = UIProgressView(progressViewStyle: .default)
+        progressView.progress = 0.0
+        progressView.translatesAutoresizingMaskIntoConstraints = false
+        return progressView
+    }()
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupSubviews()
+    }
+
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        self.translatesAutoresizingMaskIntoConstraints = false
+        setupSubviews()
+    }
+
+    /// Responsible for setting up the constraints of the cell's subviews.
+    open func setupConstraints() {
+        playButton.constraintHeightTo(45, priority: UILayoutPriority(rawValue: 999)).isActive = true
+        playButton.constraintWidthTo(45, priority: UILayoutPriority(rawValue: 999)).isActive = true
+
+        let playButtonConstraints = [playButton.constraintCenterYTo(self),
+                                     playButton.constraintAlignLeadingTo(self, paddingLeading: 12)]
+        let durationLabelConstraints = [durationLabel.constraintAlignTrailingTo(self, paddingTrailing: 12),
+                                        durationLabel.constraintCenterYTo(self)]
+        self.addConstraints(playButtonConstraints)
+        self.addConstraints(durationLabelConstraints)
+
+        progressView.addConstraints(left: playButton.rightAnchor,
+                                    right: durationLabel.leftAnchor,
+                                    centerY: self.centerYAnchor,
+                                    leftConstant: 8,
+                                    rightConstant: 8)
+        let height = self.heightAnchor.constraint(equalTo: playButton.heightAnchor)
+        height.priority = .required
+        height.isActive = true
+    }
+
+    open func setupSubviews() {
+        self.addSubview(playButton)
+        self.addSubview(durationLabel)
+        self.addSubview(progressView)
+        setupConstraints()
+    }
+
+    open func reset() {
+        progressView.progress = 0
+        playButton.isSelected = false
+        durationLabel.text = "0:00"
+    }
+
+    open func setProgress(_ progress: Float) {
+        progressView.progress = progress
+    }
+
+    open func setDuration(duration: Double) {
+        var formattedTime = "0:00"
+        // print the time as 0:ss if duration is up to 59 seconds
+        // print the time as m:ss if duration is up to 59:59 seconds
+        // print the time as h:mm:ss for anything longer
+        if duration < 60 {
+            formattedTime = String(format: "0:%.02d", Int(duration.rounded(.up)))
+        } else if duration < 3600 {
+            formattedTime = String(format: "%.02d:%.02d", Int(duration/60), Int(duration) % 60)
+        } else {
+            let hours = Int(duration/3600)
+            let remainingMinutsInSeconds = Int(duration) - hours*3600
+            formattedTime = String(format: "%.02d:%.02d:%.02d", hours, Int(remainingMinutsInSeconds/60), Int(remainingMinutsInSeconds) % 60)
+        }
+
+        durationLabel.text = formattedTime
+    }
+
+    open func showPlayLayout(_ play: Bool) {
+        playButton.isSelected = play
+    }
+}

+ 545 - 0
deltachat-ios/Chat/Views/NewMessageLabel.swift

@@ -0,0 +1,545 @@
+/*
+ MIT License
+
+ Copyright (c) 2017-2019 MessageKit
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+import UIKit
+
+open class NewMessageLabel: UILabel {
+
+    // MARK: - Private Properties
+
+    private lazy var layoutManager: NSLayoutManager = {
+        let layoutManager = NSLayoutManager()
+        layoutManager.addTextContainer(self.textContainer)
+        return layoutManager
+    }()
+
+    private lazy var textContainer: NSTextContainer = {
+        let textContainer = NSTextContainer()
+        textContainer.lineFragmentPadding = 0
+        textContainer.maximumNumberOfLines = self.numberOfLines
+        textContainer.lineBreakMode = self.lineBreakMode
+        textContainer.size = self.bounds.size
+        return textContainer
+    }()
+
+    private lazy var textStorage: NSTextStorage = {
+        let textStorage = NSTextStorage()
+        textStorage.addLayoutManager(self.layoutManager)
+        return textStorage
+    }()
+
+    internal lazy var rangesForDetectors: [DetectorType: [(NSRange, NewMessageTextCheckingType)]] = [:]
+
+    private var isConfiguring: Bool = false
+
+    // MARK: - Public Properties
+
+    open weak var delegate: MessageLabelDelegate?
+
+    open var enabledDetectors: [DetectorType] = [] {
+        didSet {
+            setTextStorage(attributedText, shouldParse: true)
+        }
+    }
+
+    open override var attributedText: NSAttributedString? {
+        didSet {
+            setTextStorage(attributedText, shouldParse: true)
+        }
+    }
+
+    open override var text: String? {
+        didSet {
+            setTextStorage(attributedText, shouldParse: true)
+        }
+    }
+
+    open override var font: UIFont! {
+        didSet {
+            setTextStorage(attributedText, shouldParse: false)
+        }
+    }
+
+    open override var textColor: UIColor! {
+        didSet {
+            setTextStorage(attributedText, shouldParse: false)
+        }
+    }
+
+    open override var lineBreakMode: NSLineBreakMode {
+        didSet {
+            textContainer.lineBreakMode = lineBreakMode
+            if !isConfiguring { setNeedsDisplay() }
+        }
+    }
+
+    open override var numberOfLines: Int {
+        didSet {
+            textContainer.maximumNumberOfLines = numberOfLines
+            if !isConfiguring { setNeedsDisplay() }
+        }
+    }
+
+    open override var textAlignment: NSTextAlignment {
+        didSet {
+            setTextStorage(attributedText, shouldParse: false)
+        }
+    }
+
+    open var textInsets: UIEdgeInsets = .zero {
+        didSet {
+            if !isConfiguring { setNeedsDisplay() }
+        }
+    }
+
+    open override var intrinsicContentSize: CGSize {
+        var size = super.intrinsicContentSize
+        size.width += textInsets.horizontal
+        size.height += textInsets.vertical
+        return size
+    }
+
+    internal var messageLabelFont: UIFont?
+
+    private var attributesNeedUpdate = false
+
+    public static var defaultAttributes: [NSAttributedString.Key: Any] = {
+        return [
+            NSAttributedString.Key.foregroundColor: UIColor.darkText,
+            NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
+            NSAttributedString.Key.underlineColor: UIColor.darkText
+        ]
+    }()
+
+    open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+    open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+    open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+    open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+    open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+    open internal(set) var hashtagAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+    open internal(set) var mentionAttributes: [NSAttributedString.Key: Any] = defaultAttributes
+
+    open internal(set) var customAttributes: [NSRegularExpression: [NSAttributedString.Key: Any]] = [:]
+
+    public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) {
+        switch detector {
+        case .phoneNumber:
+            phoneNumberAttributes = attributes
+        case .address:
+            addressAttributes = attributes
+        case .date:
+            dateAttributes = attributes
+        case .url:
+            urlAttributes = attributes
+        case .transitInformation:
+            transitInformationAttributes = attributes
+        case .mention:
+            mentionAttributes = attributes
+        case .hashtag:
+            hashtagAttributes = attributes
+        case .custom(let regex):
+            customAttributes[regex] = attributes
+        }
+        if isConfiguring {
+            attributesNeedUpdate = true
+        } else {
+            updateAttributes(for: [detector])
+        }
+    }
+
+    // MARK: - Initializers
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupView()
+    }
+
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        setupView()
+    }
+
+    // MARK: - Open Methods
+
+    open override func drawText(in rect: CGRect) {
+
+        let insetRect = rect.inset(by: textInsets)
+        textContainer.size = CGSize(width: insetRect.width, height: rect.height)
+
+        let origin = insetRect.origin
+        let range = layoutManager.glyphRange(for: textContainer)
+
+        layoutManager.drawBackground(forGlyphRange: range, at: origin)
+        layoutManager.drawGlyphs(forGlyphRange: range, at: origin)
+    }
+
+    // MARK: - Public Methods
+
+    public func configure(block: () -> Void) {
+        isConfiguring = true
+        block()
+        if attributesNeedUpdate {
+            updateAttributes(for: enabledDetectors)
+        }
+        attributesNeedUpdate = false
+        isConfiguring = false
+        setNeedsDisplay()
+    }
+
+    // MARK: - Private Methods
+
+    private func setTextStorage(_ newText: NSAttributedString?, shouldParse: Bool) {
+
+        guard let newText = newText, newText.length > 0 else {
+            textStorage.setAttributedString(NSAttributedString())
+            setNeedsDisplay()
+            return
+        }
+
+        let style = paragraphStyle(for: newText)
+        let range = NSRange(location: 0, length: newText.length)
+
+        let mutableText = NSMutableAttributedString(attributedString: newText)
+        mutableText.addAttribute(.paragraphStyle, value: style, range: range)
+
+        if shouldParse {
+            rangesForDetectors.removeAll()
+            let results = parse(text: mutableText)
+            setRangesForDetectors(in: results)
+        }
+
+        for (detector, rangeTuples) in rangesForDetectors {
+            if enabledDetectors.contains(detector) {
+                let attributes = detectorAttributes(for: detector)
+                rangeTuples.forEach { (range, _) in
+                    mutableText.addAttributes(attributes, range: range)
+                }
+            }
+        }
+
+        let modifiedText = NSAttributedString(attributedString: mutableText)
+        textStorage.setAttributedString(modifiedText)
+
+        if !isConfiguring { setNeedsDisplay() }
+
+    }
+
+    private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
+        guard text.length > 0 else { return NSParagraphStyle() }
+
+        var range = NSRange(location: 0, length: text.length)
+        let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
+        let style = existingStyle ?? NSMutableParagraphStyle()
+
+        style.lineBreakMode = lineBreakMode
+        style.alignment = textAlignment
+
+        return style
+    }
+
+    private func updateAttributes(for detectors: [DetectorType]) {
+
+        guard let attributedText = attributedText, attributedText.length > 0 else { return }
+        let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
+
+        for detector in detectors {
+            guard let rangeTuples = rangesForDetectors[detector] else { continue }
+
+            for (range, _)  in rangeTuples {
+                let attributes = detectorAttributes(for: detector)
+                mutableAttributedString.addAttributes(attributes, range: range)
+            }
+
+            let updatedString = NSAttributedString(attributedString: mutableAttributedString)
+            textStorage.setAttributedString(updatedString)
+        }
+    }
+
+    private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] {
+
+        switch detectorType {
+        case .address:
+            return addressAttributes
+        case .date:
+            return dateAttributes
+        case .phoneNumber:
+            return phoneNumberAttributes
+        case .url:
+            return urlAttributes
+        case .transitInformation:
+            return transitInformationAttributes
+        case .mention:
+            return mentionAttributes
+        case .hashtag:
+            return hashtagAttributes
+        case .custom(let regex):
+            return customAttributes[regex] ?? MessageLabel.defaultAttributes
+        }
+
+    }
+
+    private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] {
+        switch checkingResultType {
+        case .address:
+            return addressAttributes
+        case .date:
+            return dateAttributes
+        case .phoneNumber:
+            return phoneNumberAttributes
+        case .link:
+            return urlAttributes
+        case .transitInformation:
+            return transitInformationAttributes
+        default:
+            fatalError(MessageKitError.unrecognizedCheckingResult)
+        }
+    }
+
+    private func setupView() {
+        numberOfLines = 0
+        lineBreakMode = .byWordWrapping
+    }
+
+    // MARK: - Parsing Text
+
+    private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
+        guard enabledDetectors.isEmpty == false else { return [] }
+        let range = NSRange(location: 0, length: text.length)
+        var matches = [NSTextCheckingResult]()
+
+        // Get matches of all .custom DetectorType and add it to matches array
+        let regexs = enabledDetectors
+            .filter { $0.isCustom }
+            .map { parseForMatches(with: $0, in: text, for: range) }
+            .joined()
+        matches.append(contentsOf: regexs)
+
+        // Get all Checking Types of detectors, except for .custom because they contain their own regex
+        let detectorCheckingTypes = enabledDetectors
+            .filter { !$0.isCustom }
+            .reduce(0) { $0 | $1.textCheckingType.rawValue }
+        if detectorCheckingTypes > 0, let detector = try? NSDataDetector(types: detectorCheckingTypes) {
+            let detectorMatches = detector.matches(in: text.string, options: [], range: range)
+            matches.append(contentsOf: detectorMatches)
+        }
+
+        guard enabledDetectors.contains(.url) else {
+            return matches
+        }
+
+        // Enumerate NSAttributedString NSLinks and append ranges
+        var results: [NSTextCheckingResult] = matches
+
+        text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in
+            guard let url = value as? URL else { return }
+            let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url)
+            results.append(result)
+        }
+
+        return results
+    }
+
+    private func parseForMatches(with detector: DetectorType, in text: NSAttributedString, for range: NSRange) -> [NSTextCheckingResult] {
+        switch detector {
+        case .custom(let regex):
+            return regex.matches(in: text.string, options: [], range: range)
+        default:
+            fatalError("You must pass a .custom DetectorType")
+        }
+    }
+
+    private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
+
+        guard checkingResults.isEmpty == false else { return }
+
+        for result in checkingResults {
+
+            switch result.resultType {
+            case .address:
+                var ranges = rangesForDetectors[.address] ?? []
+                let tuple: (NSRange, NewMessageTextCheckingType) = (result.range, .addressComponents(result.addressComponents))
+                ranges.append(tuple)
+                rangesForDetectors.updateValue(ranges, forKey: .address)
+            case .date:
+                var ranges = rangesForDetectors[.date] ?? []
+                let tuple: (NSRange, NewMessageTextCheckingType) = (result.range, .date(result.date))
+                ranges.append(tuple)
+                rangesForDetectors.updateValue(ranges, forKey: .date)
+            case .phoneNumber:
+                var ranges = rangesForDetectors[.phoneNumber] ?? []
+                let tuple: (NSRange, NewMessageTextCheckingType) = (result.range, .phoneNumber(result.phoneNumber))
+                ranges.append(tuple)
+                rangesForDetectors.updateValue(ranges, forKey: .phoneNumber)
+            case .link:
+                var ranges = rangesForDetectors[.url] ?? []
+                let tuple: (NSRange, NewMessageTextCheckingType) = (result.range, .link(result.url)) // schl#gt fehl
+                ranges.append(tuple)
+                rangesForDetectors.updateValue(ranges, forKey: .url)
+            case .transitInformation:
+                var ranges = rangesForDetectors[.transitInformation] ?? []
+                let tuple: (NSRange, NewMessageTextCheckingType) = (result.range, .transitInfoComponents(result.components))
+                ranges.append(tuple)
+                rangesForDetectors.updateValue(ranges, forKey: .transitInformation)
+            case .regularExpression:
+                guard let text = text, let regex = result.regularExpression, let range = Range(result.range, in: text) else { return }
+                let detector = DetectorType.custom(regex)
+                var ranges = rangesForDetectors[detector] ?? []
+                let tuple: (NSRange, NewMessageTextCheckingType) = (result.range, .custom(pattern: regex.pattern, match: String(text[range])))
+                ranges.append(tuple)
+                rangesForDetectors.updateValue(ranges, forKey: detector)
+            default:
+                fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
+            }
+
+        }
+
+    }
+
+    // MARK: - Gesture Handling
+
+    private func stringIndex(at location: CGPoint) -> Int? {
+        guard textStorage.length > 0 else { return nil }
+
+        var location = location
+
+        location.x -= textInsets.left
+        location.y -= textInsets.top
+
+        let index = layoutManager.glyphIndex(for: location, in: textContainer)
+
+        let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)
+
+        var characterIndex: Int?
+
+        if lineRect.contains(location) {
+            characterIndex = layoutManager.characterIndexForGlyph(at: index)
+        }
+
+        return characterIndex
+
+    }
+
+  open func handleGesture(_ touchLocation: CGPoint) -> Bool {
+
+        guard let index = stringIndex(at: touchLocation) else { return false }
+
+        for (detectorType, ranges) in rangesForDetectors {
+            for (range, value) in ranges {
+                if range.contains(index) {
+                    handleGesture(for: detectorType, value: value)
+                    return true
+                }
+            }
+        }
+        return false
+    }
+
+    /// swiftlint:disable cyclomatic_complexity
+    private func handleGesture(for detectorType: DetectorType, value: NewMessageTextCheckingType) {
+        switch value {
+        case let .addressComponents(addressComponents):
+            var transformedAddressComponents = [String: String]()
+            guard let addressComponents = addressComponents else { return }
+            addressComponents.forEach { (key, value) in
+                transformedAddressComponents[key.rawValue] = value
+            }
+            handleAddress(transformedAddressComponents)
+        case let .phoneNumber(phoneNumber):
+            guard let phoneNumber = phoneNumber else { return }
+            handlePhoneNumber(phoneNumber)
+        case let .date(date):
+            guard let date = date else { return }
+            handleDate(date)
+        case let .link(url):
+            guard let url = url else { return }
+            handleURL(url)
+        case let .transitInfoComponents(transitInformation):
+            var transformedTransitInformation = [String: String]()
+            guard let transitInformation = transitInformation else { return }
+            transitInformation.forEach { (key, value) in
+                transformedTransitInformation[key.rawValue] = value
+            }
+            handleTransitInformation(transformedTransitInformation)
+        case let .custom(pattern, match):
+            guard let match = match else { return }
+            switch detectorType {
+            case .hashtag:
+                handleHashtag(match)
+            case .mention:
+                handleMention(match)
+            default:
+                handleCustom(pattern, match: match)
+            }
+        }
+    }
+    // swiftlint:enable cyclomatic_complexity
+
+    private func handleAddress(_ addressComponents: [String: String]) {
+        delegate?.didSelectAddress(addressComponents)
+    }
+
+    private func handleDate(_ date: Date) {
+        delegate?.didSelectDate(date)
+    }
+
+    private func handleURL(_ url: URL) {
+        delegate?.didSelectURL(url)
+    }
+
+    private func handlePhoneNumber(_ phoneNumber: String) {
+        delegate?.didSelectPhoneNumber(phoneNumber)
+    }
+
+    private func handleTransitInformation(_ components: [String: String]) {
+        delegate?.didSelectTransitInformation(components)
+    }
+
+    private func handleHashtag(_ hashtag: String) {
+        delegate?.didSelectHashtag(hashtag)
+    }
+
+    private func handleMention(_ mention: String) {
+        delegate?.didSelectMention(mention)
+    }
+
+    private func handleCustom(_ pattern: String, match: String) {
+        delegate?.didSelectCustom(pattern, match: match)
+    }
+
+}
+
+internal enum NewMessageTextCheckingType {
+    case addressComponents([NSTextCheckingKey: String]?)
+    case date(Date?)
+    case phoneNumber(String?)
+    case link(URL?)
+    case transitInfoComponents([NSTextCheckingKey: String]?)
+    case custom(pattern: String, match: String?)
+}

+ 122 - 0
deltachat-ios/Chat/Views/NewPlayButtonView.swift

@@ -0,0 +1,122 @@
+/*
+ MIT License
+
+ Copyright (c) 2017-2019 MessageKit
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+import UIKit
+
+open class NewPlayButtonView: UIView {
+
+    // MARK: - Properties
+
+    public let triangleView = UIView()
+
+    private var triangleCenterXConstraint: NSLayoutConstraint?
+    private var cacheFrame: CGRect = .zero
+
+    // MARK: - Initializers
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+
+        setupSubviews()
+        setupConstraints()
+        setupView()
+    }
+
+    required public init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+
+        setupSubviews()
+        setupConstraints()
+        setupView()
+    }
+
+    // MARK: - Methods
+
+    open override func layoutSubviews() {
+        super.layoutSubviews()
+
+        guard !cacheFrame.equalTo(frame) else { return }
+        cacheFrame = frame
+
+        updateTriangleConstraints()
+        applyCornerRadius()
+        applyTriangleMask()
+    }
+
+    private func setupSubviews() {
+        addSubview(triangleView)
+    }
+
+    private func setupView() {
+        triangleView.clipsToBounds = true
+        triangleView.backgroundColor = .black
+
+        backgroundColor = .playButtonLightGray
+    }
+
+    private func setupConstraints() {
+        triangleView.translatesAutoresizingMaskIntoConstraints = false
+
+        let centerX = triangleView.centerXAnchor.constraint(equalTo: centerXAnchor)
+        let centerY = triangleView.centerYAnchor.constraint(equalTo: centerYAnchor)
+        let width = triangleView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5)
+        let height = triangleView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5)
+
+        triangleCenterXConstraint = centerX
+
+        NSLayoutConstraint.activate([centerX, centerY, width, height])
+    }
+
+    private func triangleMask(for frame: CGRect) -> CAShapeLayer {
+        let shapeLayer = CAShapeLayer()
+        let trianglePath = UIBezierPath()
+
+        let point1 = CGPoint(x: frame.minX, y: frame.minY)
+        let point2 = CGPoint(x: (frame.maxX/5) * 4, y: frame.maxY/2)
+        let point3 = CGPoint(x: frame.minX, y: frame.maxY)
+
+        trianglePath .move(to: point1)
+        trianglePath .addLine(to: point2)
+        trianglePath .addLine(to: point3)
+        trianglePath .close()
+
+        shapeLayer.path = trianglePath.cgPath
+
+        return shapeLayer
+    }
+
+    private func updateTriangleConstraints() {
+        triangleCenterXConstraint?.constant = frame.width/8
+    }
+
+    private func applyTriangleMask() {
+        let rect = CGRect(origin: .zero, size: triangleView.bounds.size)
+        triangleView.layer.mask = triangleMask(for: rect)
+    }
+
+    private func applyCornerRadius() {
+        layer.cornerRadius = frame.width / 2
+    }
+
+}

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

@@ -405,7 +405,8 @@ class ChatListController: UITableViewController {
     }
 
     func showChat(chatId: Int, animated: Bool = true) {
-        let chatVC = ChatViewController(dcContext: dcContext, chatId: chatId)
+        //let chatVC = ChatViewController(dcContext: dcContext, chatId: chatId)
+        let chatVC = ChatViewControllerNew(dcContext: dcContext, chatId: chatId)
         navigationController?.pushViewController(chatVC, animated: animated)
     }
 

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

@@ -80,6 +80,14 @@ extension UIFont {
         let metrics = UIFontMetrics(forTextStyle: style)
         return metrics.scaledFont(for: font)
     }
+
+    static func preferredItalicFont(for style: TextStyle) -> UIFont {
+        let traits = UITraitCollection(preferredContentSizeCategory: .large)
+        let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traits)
+        let font = UIFont.italicSystemFont(ofSize: desc.pointSize)
+        let metrics = UIFontMetrics(forTextStyle: style)
+        return metrics.scaledFont(for: font)
+    }
 }
 
 extension UINavigationController {

+ 0 - 13
deltachat-ios/MessageKit/Controllers/BasicAudioController.swift

@@ -25,19 +25,6 @@
 import UIKit
 import AVFoundation
 
-/// The `PlayerState` indicates the current audio controller state
-public enum PlayerState {
-
-    /// The audio controller is currently playing a sound
-    case playing
-
-    /// The audio controller is currently in pause state
-    case pause
-
-    /// The audio controller is not playing any sound and audioPlayer is nil
-    case stopped
-}
-
 /// The `BasicAudioController` update UI for current audio cell that is playing a sound
 /// and also creates and manage an `AVAudioPlayer` states, play, pause and stop.
 open class BasicAudioController: NSObject, AVAudioPlayerDelegate {

+ 2 - 1
deltachat-ios/View/FlexLabel.swift

@@ -56,9 +56,10 @@ class FlexLabel: UIView {
         label.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, multiplier: 0.95).isActive = true
     }
 
+    ///FIXME: - replace this implementation, it's cutting off long texts, check PaddingTextView
     class PaddingLabel: UILabel {
-        let insets: UIEdgeInsets
 
+        let insets: UIEdgeInsets
         init(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) {
             self.insets = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
             super.init(frame: .zero)

+ 86 - 0
deltachat-ios/View/PaddingTextView.swift

@@ -0,0 +1,86 @@
+import UIKit
+public class PaddingTextView: UIView {
+
+    public lazy var label: NewMessageLabel = {
+        let label = NewMessageLabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.numberOfLines = 0
+        label.lineBreakMode = .byWordWrapping
+        label.isUserInteractionEnabled = true
+        return label
+    }()
+
+    public var paddingTop: CGFloat = 0 {
+        didSet { containerTopConstraint.constant = paddingTop }
+    }
+
+    public var paddingBottom: CGFloat = 0 {
+        didSet { containerBottomConstraint.constant = -paddingBottom }
+    }
+
+    public var paddingLeading: CGFloat = 0 {
+        didSet { containerLeadingConstraint.constant = paddingLeading }
+    }
+
+    public var paddingTrailing: CGFloat = 0 {
+        didSet { containerTailingConstraint.constant = -paddingTrailing }
+    }
+
+    private lazy var containerLeadingConstraint: NSLayoutConstraint = {
+        return label.constraintAlignLeadingTo(self)
+    }()
+    private lazy var containerTailingConstraint: NSLayoutConstraint = {
+        return label.constraintAlignTrailingTo(self)
+    }()
+    private lazy var containerTopConstraint: NSLayoutConstraint = {
+        return label.constraintAlignTopTo(self)
+    }()
+    private lazy var containerBottomConstraint: NSLayoutConstraint = {
+        return label.constraintAlignBottomTo(self)
+    }()
+
+    public var text: String? {
+        set { label.text = newValue }
+        get { return label.text }
+    }
+
+    public var attributedText: NSAttributedString? {
+        set { label.attributedText = newValue }
+        get { return label.attributedText }
+    }
+
+    public var numberOfLines: Int {
+        set { label.numberOfLines = newValue }
+        get { return label.numberOfLines }
+    }
+
+    public var font: UIFont {
+        set { label.font = newValue }
+        get { return label.font }
+    }
+
+    public var enabledDetectors: [DetectorType] {
+        set { label.enabledDetectors = newValue }
+        get { return label.enabledDetectors }
+    }
+
+    public var delegate: MessageLabelDelegate? {
+        set { label.delegate = newValue }
+        get { return label.delegate }
+    }
+
+    init() {
+        super.init(frame: .zero)
+        addSubview(label)
+        addConstraints([
+            containerTailingConstraint,
+            containerLeadingConstraint,
+            containerBottomConstraint,
+            containerTopConstraint
+        ])
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}