Explorar el Código

Merge pull request #1282 from deltachat/video-invite

video invite
cyBerta hace 4 años
padre
commit
235e126ddf
Se han modificado 40 ficheros con 571 adiciones y 201 borrados
  1. 11 0
      DcCore/DcCore/DC/Wrapper.swift
  2. 30 10
      DcCore/DcCore/Extensions/UIView+Extensions.swift
  3. 2 1
      DcCore/DcCore/Helper/DcColors.swift
  4. 18 4
      DcCore/DcCore/Views/InitialsBadge.swift
  5. 12 0
      deltachat-ios.xcodeproj/project.pbxproj
  6. 23 0
      deltachat-ios/Assets.xcassets/ic_videochat.imageset/Contents.json
  7. BIN
      deltachat-ios/Assets.xcassets/ic_videochat.imageset/round_videocam_black_24pt_1x.png
  8. BIN
      deltachat-ios/Assets.xcassets/ic_videochat.imageset/round_videocam_black_24pt_2x.png
  9. BIN
      deltachat-ios/Assets.xcassets/ic_videochat.imageset/round_videocam_black_24pt_3x.png
  10. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-hdpi/ic_voice_chat_black_36dp.png
  11. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-mdpi/ic_voice_chat_black_36dp.png
  12. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-xhdpi/ic_voice_chat_black_36dp.png
  13. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-xxhdpi/ic_voice_chat_black_36dp.png
  14. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-xxxhdpi/ic_voice_chat_black_36dp.png
  15. 0 23
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/Contents.json
  16. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/ic_voice_chat_36pt.png
  17. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/ic_voice_chat_36pt_2x.png
  18. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/ic_voice_chat_36pt_3x.png
  19. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/web/ic_voice_chat_black_36dp_1x.png
  20. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/web/ic_voice_chat_black_36dp_2x.png
  21. 0 6
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/Contents.json
  22. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-hdpi/ic_voice_chat_white_36dp.png
  23. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-mdpi/ic_voice_chat_white_36dp.png
  24. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-xhdpi/ic_voice_chat_white_36dp.png
  25. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-xxhdpi/ic_voice_chat_white_36dp.png
  26. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-xxxhdpi/ic_voice_chat_white_36dp.png
  27. 0 23
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/Contents.json
  28. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/ic_voice_chat_white_36pt.png
  29. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/ic_voice_chat_white_36pt_2x.png
  30. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/ic_voice_chat_white_36pt_3x.png
  31. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/web/ic_voice_chat_white_36dp_1x.png
  32. BIN
      deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/web/ic_voice_chat_white_36dp_2x.png
  33. 54 7
      deltachat-ios/Chat/ChatViewController.swift
  34. 3 126
      deltachat-ios/Chat/Views/Cells/BaseMessageCell.swift
  35. 149 0
      deltachat-ios/Chat/Views/Cells/VideoInviteCell.swift
  36. 16 0
      deltachat-ios/Controller/QrPageController.swift
  37. 18 1
      deltachat-ios/Controller/SettingsController.swift
  38. 53 0
      deltachat-ios/Controller/SettingsVideoChatInstanceController.swift
  39. 53 0
      deltachat-ios/Controller/SettingsVideoChatViewController.swift
  40. 129 0
      deltachat-ios/Helper/MessageUtils.swift

+ 11 - 0
DcCore/DcCore/DC/Wrapper.swift

@@ -126,6 +126,10 @@ public class DcContext {
         dc_send_msg(contextPointer, UInt32(chatId), message.messagePointer)
     }
 
+    public func sendVideoChatInvitation(chatId: Int) -> Int {
+        return Int(dc_send_videochat_invitation(contextPointer,  UInt32(chatId)))
+    }
+
     // TODO: remove count and from parameters if we don't use it
     public func getMessageIds(chatId: Int, count: Int? = nil, from: Int? = nil) -> [Int] {
         let start = CFAbsoluteTimeGetCurrent()
@@ -1144,6 +1148,13 @@ public class DcMsg {
         return dc_msg_get_showpadlock(messagePointer) == 1
     }
 
+    public func getVideoChatUrl() -> String {
+        guard let cString = dc_msg_get_videochat_url(messagePointer) else { return "" }
+        let swiftString = String(cString: cString)
+        dc_str_unref(cString)
+        return swiftString
+    }
+
 }
 
 public class DcContact {

+ 30 - 10
DcCore/DcCore/Extensions/UIView+Extensions.swift

@@ -8,42 +8,62 @@ public extension UIView {
     }
 
     func alignLeadingToAnchor(_ anchor: NSLayoutXAxisAnchor, paddingLeading: CGFloat = 0.0, priority: UILayoutPriority? = .none) {
+        _ = constraintAlignLeadingToAnchor(anchor, paddingLeading: paddingLeading, priority: priority)
+    }
+
+    func alignTrailingToAnchor(_ anchor: NSLayoutXAxisAnchor, paddingTrailing: CGFloat = 0.0, priority: UILayoutPriority? = .none) {
+        _ = constraintAlignTrailingToAnchor(anchor, paddingTrailing: paddingTrailing, priority: priority)
+    }
+
+    func alignTopToAnchor(_ anchor: NSLayoutYAxisAnchor, paddingTop: CGFloat = 0.0, priority: UILayoutPriority? = .none) {
+        _ = constraintAlignTopToAnchor(anchor, paddingTop: paddingTop, priority: priority)
+    }
+
+    func alignBottomToAnchor(_ anchor: NSLayoutYAxisAnchor, paddingBottom: CGFloat = 0.0, priority: UILayoutPriority? = .none) {
+        _ = constraintAlignBottomToAnchor(anchor, paddingBottom: paddingBottom, priority: priority)
+    }
+
+    func fill(view: UIView, paddingLeading: CGFloat? = 0.0, paddingTrailing: CGFloat? = 0.0, paddingTop: CGFloat? = 0.0, paddingBottom: CGFloat? = 0.0) {
+        alignLeadingToAnchor(view.leadingAnchor, paddingLeading: paddingLeading ??  0.0)
+        alignTrailingToAnchor(view.trailingAnchor, paddingTrailing: paddingTrailing ?? 0.0)
+        alignTopToAnchor(view.topAnchor, paddingTop: paddingTop ?? 0.0)
+        alignBottomToAnchor(view.bottomAnchor, paddingBottom: paddingBottom ?? 0.0)
+    }
+
+    func constraintAlignLeadingToAnchor(_ anchor: NSLayoutXAxisAnchor, paddingLeading: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
         let constraint = self.leadingAnchor.constraint(equalTo: anchor, constant: paddingLeading)
         if let priority = priority {
             constraint.priority = priority
         }
         constraint.isActive = true
+        return constraint
     }
 
-    func alignTrailingToAnchor(_ anchor: NSLayoutXAxisAnchor, paddingTrailing: CGFloat = 0.0, priority: UILayoutPriority? = .none) {
+    func constraintAlignTrailingToAnchor(_ anchor: NSLayoutXAxisAnchor, paddingTrailing: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
         let constraint = self.trailingAnchor.constraint(equalTo: anchor, constant: -paddingTrailing)
         if let priority = priority {
             constraint.priority = priority
         }
         constraint.isActive = true
+        return constraint
     }
 
-    func alignTopToAnchor(_ anchor: NSLayoutYAxisAnchor, paddingTop: CGFloat = 0.0, priority: UILayoutPriority? = .none) {
+    func constraintAlignTopToAnchor(_ anchor: NSLayoutYAxisAnchor, paddingTop: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
         let constraint = self.topAnchor.constraint(equalTo: anchor, constant: paddingTop)
         if let priority = priority {
             constraint.priority = priority
         }
         constraint.isActive = true
+        return constraint
     }
 
-    func alignBottomToAnchor(_ anchor: NSLayoutYAxisAnchor, paddingBottom: CGFloat = 0.0, priority: UILayoutPriority? = .none) {
+    func constraintAlignBottomToAnchor(_ anchor: NSLayoutYAxisAnchor, paddingBottom: CGFloat = 0.0, priority: UILayoutPriority? = .none) -> NSLayoutConstraint {
         let constraint = self.bottomAnchor.constraint(equalTo: anchor, constant: -paddingBottom)
         if let priority = priority {
             constraint.priority = priority
         }
         constraint.isActive = true
-    }
-
-    func fill(view: UIView, paddingLeading: CGFloat? = 0.0, paddingTrailing: CGFloat? = 0.0, paddingTop: CGFloat? = 0.0, paddingBottom: CGFloat? = 0.0) {
-        alignLeadingToAnchor(view.leadingAnchor, paddingLeading: paddingLeading ??  0.0)
-        alignTrailingToAnchor(view.trailingAnchor, paddingTrailing: paddingTrailing ?? 0.0)
-        alignTopToAnchor(view.topAnchor, paddingTop: paddingTop ?? 0.0)
-        alignBottomToAnchor(view.bottomAnchor, paddingBottom: paddingBottom ?? 0.0)
+        return constraint
     }
 
     func constraintAlignTopTo(_ view: UIView) -> NSLayoutConstraint {

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

@@ -13,6 +13,7 @@ public struct DcColors {
                                                           dark: UIColor.init(hexString: "333333"))
     public static let contactCellBackgroundColor = UIColor.themeColor(light: .white, dark: .black)
     public static let defaultBackgroundColor = UIColor.themeColor(light: .white, dark: .black)
+    public static let defaultInverseColor = UIColor.themeColor(light: .black, dark: .white)
     public static let sharedChatCellBackgroundColor = UIColor.themeColor(light: white, dark: actionCellBackgroundDark)
     public static let chatBackgroundColor = UIColor.themeColor(light: .white, dark: .black)
     public static let checkmarkGreen = UIColor.themeColor(light: UIColor.rgb(red: 112, green: 177, blue: 92))
@@ -27,7 +28,7 @@ public struct DcColors {
                                                      dark: UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1))
     public static let providerPreparationBackground = UIColor.themeColor(lightHex: "#fffdf7b2", darkHex: "##fffdf7b2")
     public static let providerBrokenBackground = UIColor.themeColor(light: SystemColor.red.uiColor, dark: SystemColor.red.uiColor)
-    public static let systemMessageBackgroundColor = UIColor.themeColor(light: UIColor.rgb(red: 248, green: 248, blue: 248),
+    public static let systemMessageBackgroundColor = UIColor.themeColor(light: UIColor(white: 0.9, alpha: 0.5),
                                                                         dark: UIColor(white: 0.2, alpha: 0.5))
     public static let deaddropBackground = UIColor.themeColor(light: UIColor.init(hexString: "ebebec"), dark: UIColor.init(hexString: "1a1a1c"))
 }

+ 18 - 4
DcCore/DcCore/Views/InitialsBadge.swift

@@ -5,6 +5,20 @@ public class InitialsBadge: UIView {
     private let verificationViewPadding: CGFloat = 2
     private let size: CGFloat
 
+    var leadingImageAnchorConstraint: NSLayoutConstraint?
+    var trailingImageAnchorConstraint: NSLayoutConstraint?
+    var topImageAnchorConstraint: NSLayoutConstraint?
+    var bottomImageAnchorConstraint: NSLayoutConstraint?
+
+    public var imagePadding: CGFloat = 0 {
+        didSet {
+            leadingImageAnchorConstraint?.constant = imagePadding
+            topImageAnchorConstraint?.constant = imagePadding
+            trailingImageAnchorConstraint?.constant = -imagePadding
+            bottomImageAnchorConstraint?.constant = -imagePadding
+        }
+    }
+
     private var label: UILabel = {
         let label = UILabel()
         label.textAlignment = NSTextAlignment.center
@@ -69,10 +83,10 @@ public class InitialsBadge: UIView {
     private func setupSubviews(with radius: CGFloat) {
         addSubview(imageView)
         imageView.layer.cornerRadius = radius
-        imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
-        imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
-        imageView.alignTopToAnchor(topAnchor)
-        imageView.alignBottomToAnchor(bottomAnchor)
+        leadingImageAnchorConstraint = imageView.constraintAlignLeadingToAnchor(leadingAnchor)
+        trailingImageAnchorConstraint = imageView.constraintAlignTrailingToAnchor(trailingAnchor)
+        topImageAnchorConstraint = imageView.constraintAlignTopToAnchor(topAnchor)
+        bottomImageAnchorConstraint = imageView.constraintAlignBottomToAnchor(bottomAnchor)
 
         addSubview(label)
         label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true

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

@@ -11,6 +11,7 @@
 		3008CB7224F93EB900E6A617 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */; };
 		3008CB7424F9436C00E6A617 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */; };
 		3008CB7624F95B6D00E6A617 /* AudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7524F95B6D00E6A617 /* AudioController.swift */; };
+		3010968926838A050032CBA0 /* VideoInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3010968826838A040032CBA0 /* VideoInviteCell.swift */; };
 		30149D9322F21129003C12B5 /* QrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30149D9222F21129003C12B5 /* QrViewController.swift */; };
 		30152C9425A5D91400377714 /* PaddingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F4BFED252E3E020006B9B3 /* PaddingTextView.swift */; };
 		30152C9725A5D91900377714 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EF7323252FF15F00E2C54A /* MessageLabel.swift */; };
@@ -24,6 +25,8 @@
 		302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AC265E237F1807002A943F /* AvatarHelper.swift */; };
 		302B84C72396770B001C261F /* RelayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B84C42396627F001C261F /* RelayHelper.swift */; };
 		302B84CE2397F6CD001C261F /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302B84CD2397F6CD001C261F /* URL+Extension.swift */; };
+		302D5450268B6B2300A8B271 /* MessageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302D544F268B6B2300A8B271 /* MessageUtils.swift */; };
+		302D5454268B84CB00A8B271 /* SettingsVideoChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302D5453268B84CB00A8B271 /* SettingsVideoChatViewController.swift */; };
 		302E1BB4252B5AB4008F4264 /* PlayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E1BB3252B5AB4008F4264 /* PlayButtonView.swift */; };
 		30349291256441E200A523D0 /* QuotePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30349290256441E200A523D0 /* QuotePreview.swift */; };
 		303492952565AABC00A523D0 /* DraftModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492942565AABC00A523D0 /* DraftModel.swift */; };
@@ -210,6 +213,7 @@
 		3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessageCell.swift; sourceTree = "<group>"; };
 		3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
 		3008CB7524F95B6D00E6A617 /* AudioController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioController.swift; sourceTree = "<group>"; };
+		3010968826838A040032CBA0 /* VideoInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoInviteCell.swift; sourceTree = "<group>"; };
 		30149D9222F21129003C12B5 /* QrViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrViewController.swift; sourceTree = "<group>"; };
 		3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderController.swift; sourceTree = "<group>"; };
 		3022E6BF22E8768800763272 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -236,6 +240,8 @@
 		30260CA6238F02F700D8D52C /* MultilineTextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldCell.swift; sourceTree = "<group>"; };
 		302B84C42396627F001C261F /* RelayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayHelper.swift; sourceTree = "<group>"; };
 		302B84CD2397F6CD001C261F /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = "<group>"; };
+		302D544F268B6B2300A8B271 /* MessageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUtils.swift; sourceTree = "<group>"; };
+		302D5453268B84CB00A8B271 /* SettingsVideoChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsVideoChatViewController.swift; sourceTree = "<group>"; };
 		302E1BB3252B5AB4008F4264 /* PlayButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlayButtonView.swift; path = "deltachat-ios/Chat/Views/PlayButtonView.swift"; sourceTree = SOURCE_ROOT; };
 		30349290256441E200A523D0 /* QuotePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePreview.swift; sourceTree = "<group>"; };
 		303492942565AABC00A523D0 /* DraftModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftModel.swift; sourceTree = "<group>"; };
@@ -621,6 +627,7 @@
 				30E348E424F6647D005C93D1 /* FileTextCell.swift */,
 				30A4149624F6EFBE00EC91EB /* InfoMessageCell.swift */,
 				3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */,
+				3010968826838A040032CBA0 /* VideoInviteCell.swift */,
 			);
 			path = Cells;
 			sourceTree = "<group>";
@@ -795,6 +802,7 @@
 				78E45E3921D3CFBC00D4B15E /* SettingsController.swift */,
 				B20462E32440A4A600367A57 /* SettingsAutodelOverviewController.swift */,
 				B20462E52440C99600367A57 /* SettingsAutodelSetController.swift */,
+				302D5453268B84CB00A8B271 /* SettingsVideoChatViewController.swift */,
 				AE76E5ED242BF2EA003CF461 /* WelcomeViewController.swift */,
 				AE8F503424753DFE007FEE0B /* GalleryViewController.swift */,
 				30734325249A280B00BF9AD1 /* MediaQualityController.swift */,
@@ -830,6 +838,7 @@
 				AE0AA9552478191900D42A7F /* GridCollectionViewFlowLayout.swift */,
 				AE6EC5272497B9B200A400E4 /* ThumbnailCache.swift */,
 				21D6C9392606190600D0755A /* NotificationManager.swift */,
+				302D544F268B6B2300A8B271 /* MessageUtils.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -1282,6 +1291,7 @@
 				30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */,
 				302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
 				AE77838D23E32ED20093EABD /* ContactDetailViewModel.swift in Sources */,
+				3010968926838A050032CBA0 /* VideoInviteCell.swift in Sources */,
 				303492CB257A814200A523D0 /* DraftArea.swift in Sources */,
 				AEE6EC3F2282C59C00EDC689 /* GroupMembersViewController.swift in Sources */,
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
@@ -1296,6 +1306,7 @@
 				305702A124C6453700D84EFC /* TypeAlias.swift in Sources */,
 				AE19887523EB264000B4CD5F /* HelpViewController.swift in Sources */,
 				AE0D26FD1FB1FE88002FAFCE /* ChatListController.swift in Sources */,
+				302D5450268B6B2300A8B271 /* MessageUtils.swift in Sources */,
 				30149D9322F21129003C12B5 /* QrViewController.swift in Sources */,
 				AEE56D80225504DB007DC082 /* Extensions.swift in Sources */,
 				7A0052C81FBE6CB40048C3BF /* NewContactController.swift in Sources */,
@@ -1329,6 +1340,7 @@
 				AEC67A1E241FCFE0007DDBE1 /* ChatListViewModel.swift in Sources */,
 				30FDB71F24D8170E0066C48D /* TextMessageCell.swift in Sources */,
 				AE1988A523EB2FBA00B4CD5F /* Errors.swift in Sources */,
+				302D5454268B84CB00A8B271 /* SettingsVideoChatViewController.swift in Sources */,
 				AEFBE22F23FEF23D0045327A /* ProviderInfoCell.swift in Sources */,
 				AE6EC5242497663200A400E4 /* UIImageView+Extensions.swift in Sources */,
 				30F8817624DA97DA0023780E /* BackgroundContainer.swift in Sources */,

+ 23 - 0
deltachat-ios/Assets.xcassets/ic_videochat.imageset/Contents.json

@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "filename" : "round_videocam_black_24pt_1x.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "round_videocam_black_24pt_2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "round_videocam_black_24pt_3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
deltachat-ios/Assets.xcassets/ic_videochat.imageset/round_videocam_black_24pt_1x.png


BIN
deltachat-ios/Assets.xcassets/ic_videochat.imageset/round_videocam_black_24pt_2x.png


BIN
deltachat-ios/Assets.xcassets/ic_videochat.imageset/round_videocam_black_24pt_3x.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-hdpi/ic_voice_chat_black_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-mdpi/ic_voice_chat_black_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-xhdpi/ic_voice_chat_black_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-xxhdpi/ic_voice_chat_black_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/android/drawable-xxxhdpi/ic_voice_chat_black_36dp.png


+ 0 - 23
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/Contents.json

@@ -1,23 +0,0 @@
-{
-    "images": [
-        {
-            "filename": "ic_voice_chat_36pt.png",
-            "idiom": "universal",
-            "scale": "1x"
-        },
-        {
-            "filename": "ic_voice_chat_36pt_2x.png",
-            "idiom": "universal",
-            "scale": "2x"
-        },
-        {
-            "filename": "ic_voice_chat_36pt_3x.png",
-            "idiom": "universal",
-            "scale": "3x"
-        }
-    ],
-    "info": {
-        "author": "xcode",
-        "version": 1
-    }
-}

BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/ic_voice_chat_36pt.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/ic_voice_chat_36pt_2x.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/ios/ic_voice_chat_36pt.imageset/ic_voice_chat_36pt_3x.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/web/ic_voice_chat_black_36dp_1x.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_black_36dp/web/ic_voice_chat_black_36dp_2x.png


+ 0 - 6
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/Contents.json

@@ -1,6 +0,0 @@
-{
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}

BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-hdpi/ic_voice_chat_white_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-mdpi/ic_voice_chat_white_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-xhdpi/ic_voice_chat_white_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-xxhdpi/ic_voice_chat_white_36dp.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/android/drawable-xxxhdpi/ic_voice_chat_white_36dp.png


+ 0 - 23
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/Contents.json

@@ -1,23 +0,0 @@
-{
-    "images": [
-        {
-            "filename": "ic_voice_chat_white_36pt.png",
-            "idiom": "universal",
-            "scale": "1x"
-        },
-        {
-            "filename": "ic_voice_chat_white_36pt_2x.png",
-            "idiom": "universal",
-            "scale": "2x"
-        },
-        {
-            "filename": "ic_voice_chat_white_36pt_3x.png",
-            "idiom": "universal",
-            "scale": "3x"
-        }
-    ],
-    "info": {
-        "author": "xcode",
-        "version": 1
-    }
-}

BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/ic_voice_chat_white_36pt.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/ic_voice_chat_white_36pt_2x.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/ios/ic_voice_chat_white_36pt.imageset/ic_voice_chat_white_36pt_3x.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/web/ic_voice_chat_white_36dp_1x.png


BIN
deltachat-ios/Assets.xcassets/ic_voice_chat_white_36dp/web/ic_voice_chat_white_36dp_2x.png


+ 54 - 7
deltachat-ios/Chat/ChatViewController.swift

@@ -252,6 +252,7 @@ class ChatViewController: UITableViewController {
         tableView.register(FileTextCell.self, forCellReuseIdentifier: "file")
         tableView.register(InfoMessageCell.self, forCellReuseIdentifier: "info")
         tableView.register(AudioMessageCell.self, forCellReuseIdentifier: "audio")
+        tableView.register(VideoInviteCell.self, forCellReuseIdentifier: "video_invite")
         tableView.rowHeight = UITableView.automaticDimension
         tableView.separatorStyle = .none
         tableView.keyboardDismissMode = .interactive
@@ -557,21 +558,29 @@ class ChatViewController: UITableViewController {
         }
 
         let cell: BaseMessageCell
-        if message.type == DC_MSG_IMAGE || message.type == DC_MSG_GIF || message.type == DC_MSG_VIDEO || message.type == DC_MSG_STICKER {
+        switch Int32(message.type) {
+        case DC_MSG_VIDEOCHAT_INVITATION:
+            let videoInviteCell = tableView.dequeueReusableCell(withIdentifier: "video_invite", for: indexPath) as? VideoInviteCell ?? VideoInviteCell()
+            videoInviteCell.update(dcContext: dcContext, msg: message)
+            return videoInviteCell
+
+        case DC_MSG_IMAGE, DC_MSG_GIF, DC_MSG_VIDEO, DC_MSG_STICKER:
             cell = tableView.dequeueReusableCell(withIdentifier: "image", for: indexPath) as? ImageTextCell ?? ImageTextCell()
-        } else if message.type == DC_MSG_FILE {
+
+        case DC_MSG_FILE:
             if message.isSetupMessage {
                 cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextMessageCell ?? TextMessageCell()
                 message.text = String.localized("autocrypt_asm_click_body")
             } else {
                 cell = tableView.dequeueReusableCell(withIdentifier: "file", for: indexPath) as? FileTextCell ?? FileTextCell()
             }
-        } else if message.type == DC_MSG_AUDIO ||  message.type == DC_MSG_VOICE {
+
+        case DC_MSG_AUDIO, DC_MSG_VOICE:
             let audioMessageCell: AudioMessageCell = tableView.dequeueReusableCell(withIdentifier: "audio",
                                                                                       for: indexPath) as? AudioMessageCell ?? AudioMessageCell()
             audioController.update(audioMessageCell, with: message.id)
             cell = audioMessageCell
-        } else {
+        default:
             cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextMessageCell ?? TextMessageCell()
         }
 
@@ -625,7 +634,8 @@ class ChatViewController: UITableViewController {
 
 
     override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
-        if disableWriting || dcContext.getMessage(id: messageIds[indexPath.row]).isInfo {
+        let message = dcContext.getMessage(id: messageIds[indexPath.row])
+        if disableWriting || message.isInfo || message.type == DC_MSG_VIDEOCHAT_INVITATION {
             return nil
         }
 
@@ -695,6 +705,10 @@ class ChatViewController: UITableViewController {
             message.type == DC_MSG_AUDIO ||
             message.type == DC_MSG_VOICE {
             showMediaGalleryFor(message: message)
+        } else if message.type == DC_MSG_VIDEOCHAT_INVITATION {
+            if let url = NSURL(string: message.getVideoChatUrl()) {
+                UIApplication.shared.open(url as URL)
+            }
         }
         _ = handleUIMenu()
     }
@@ -982,6 +996,12 @@ class ChatViewController: UITableViewController {
         alert.addAction(galleryAction)
         alert.addAction(documentAction)
         alert.addAction(voiceMessageAction)
+
+        if let config = dcContext.getConfig("webrtc_instance"), !config.isEmpty {
+            let videoChatInvitation = UIAlertAction(title: String.localized("videochat"), style: .default, handler: videoChatButtonPressed(_:))
+            alert.addAction(videoChatInvitation)
+        }
+
         if UserDefaults.standard.bool(forKey: "location_streaming") {
             alert.addAction(locationStreamingAction)
         }
@@ -1157,6 +1177,31 @@ class ChatViewController: UITableViewController {
         }
     }
 
+    private func videoChatButtonPressed(_ action: UIAlertAction) {
+        let chat = dcContext.getChat(chatId: chatId)
+
+        let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("videochat_invite_user_to_videochat"), chat.name),
+                                      message: String.localized("videochat_invite_user_hint"),
+                                      preferredStyle: .alert)
+        let cancel = UIAlertAction(title: String.localized("cancel"), style: .default, handler: nil)
+        let ok = UIAlertAction(title: String.localized("ok"),
+                               style: .default,
+                               handler: { _ in
+                                DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+                                    guard let self = self else { return }
+                                    let messageId = self.dcContext.sendVideoChatInvitation(chatId: self.chatId)
+                                    let inviteMessage = self.dcContext.getMessage(id: messageId)
+                                    if let url = NSURL(string: inviteMessage.getVideoChatUrl()) {
+                                        DispatchQueue.main.async {
+                                            UIApplication.shared.open(url as URL)
+                                        }
+                                    }
+                                }})
+        alert.addAction(cancel)
+        alert.addAction(ok)
+        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)
@@ -1291,7 +1336,8 @@ class ChatViewController: UITableViewController {
     }
 
     override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
-        return !dcContext.getMessage(id: messageIds[indexPath.row]).isInfo
+        let messageId = messageIds[indexPath.row]
+        return !(dcContext.getMessage(id: messageId).isInfo || messageId == DC_MSG_ID_MARKER1 || messageId == DC_MSG_ID_DAYMARKER)
     }
 
     override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
@@ -1306,7 +1352,8 @@ class ChatViewController: UITableViewController {
     // context menu for iOS 13+
     @available(iOS 13, *)
     override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
-        if tableView.isEditing {
+        let messageId = messageIds[indexPath.row]
+        if tableView.isEditing || dcContext.getMessage(id: messageId).isInfo || messageId == DC_MSG_ID_MARKER1 || messageId == DC_MSG_ID_DAYMARKER {
             return nil
         }
         return UIContextMenuConfiguration(

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

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

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

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

+ 16 - 0
deltachat-ios/Controller/QrPageController.swift

@@ -215,6 +215,22 @@ extension QrPageController: QrCodeReaderDelegate {
             alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default))
             present(alert, animated: true)
 
+        case DC_QR_WEBRTC_INSTANCE:
+            guard let domain = qrParsed.text1 else { return }
+            let alert = UIAlertController(title: String.localizedStringWithFormat(String.localized("videochat_instance_from_qr"), domain),
+                                          message: nil,
+                                          preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: String.localized("cancel"), style: .default))
+            alert.addAction(UIAlertAction(title: String.localized("ok"), style: .default, handler: { [weak self] _ in
+                guard let self = self else { return }
+                let success = self.dcContext.setConfigFromQR(qrCode: code)
+                if !success {
+                    logger.warning("Could not set webrtc instance from QR code.")
+                    // TODO: alert?!
+                }
+            }))
+            present(alert, animated: true)
+
         default:
             var msg = String.localizedStringWithFormat(String.localized("qrscan_contains_text"), code)
             if state == DC_QR_ERROR {

+ 18 - 1
deltachat-ios/Controller/SettingsController.swift

@@ -25,6 +25,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         case autodel = 11
         case mediaQuality = 12
         case switchAccount = 13
+        case videoChat = 14
     }
 
     private var dcContext: DcContext
@@ -100,6 +101,15 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         return cell
     }()
 
+    private lazy var videoChatInstanceCell: UITableViewCell = {
+        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
+        cell.tag = CellTags.videoChat.rawValue
+        cell.textLabel?.text = String.localized("videochat_instance")
+        cell.accessoryType = .disclosureIndicator
+        cell.detailTextLabel?.text = dcContext.getConfig("webrtc_instance")
+        return cell
+    }()
+
     private lazy var notificationSwitch: UISwitch = {
         let switchControl = UISwitch()
         switchControl.isOn = !UserDefaults.standard.bool(forKey: "notifications_disabled")
@@ -197,7 +207,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         let preferencesSection = SectionConfigs(
             headerTitle: String.localized("pref_chats_and_media"),
             footerTitle: String.localized("pref_read_receipts_explain"),
-            cells: [contactRequestCell, showEmailsCell, blockedContactsCell, autodelCell, mediaQualityCell, notificationCell, receiptConfirmationCell]
+            cells: [contactRequestCell, showEmailsCell, blockedContactsCell, autodelCell, mediaQualityCell, videoChatInstanceCell, notificationCell, receiptConfirmationCell]
         )
         let autocryptSection = SectionConfigs(
             headerTitle: String.localized("autocrypt"),
@@ -294,6 +304,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         case .blockedContacts: showBlockedContacts()
         case .autodel: showAutodelOptions()
         case .mediaQuality: showMediaQuality()
+        case .videoChat: showVideoChatInstance()
         case .notifications: break
         case .receiptConfirmation: break
         case .autocryptPreferences: break
@@ -507,6 +518,7 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         profileCell.updateCell(cellViewModel: ProfileViewModel(context: dcContext))
         showEmailsCell.detailTextLabel?.text = SettingsClassicViewController.getValString(val: dcContext.showEmails)
         mediaQualityCell.detailTextLabel?.text = MediaQualityController.getValString(val: dcContext.getConfigInt("media_quality"))
+        videoChatInstanceCell.detailTextLabel?.text = dcContext.getConfig("webrtc_instance")
         autodelCell.detailTextLabel?.text = autodelSummary()
     }
 
@@ -526,6 +538,11 @@ internal final class SettingsViewController: UITableViewController, ProgressAler
         navigationController?.pushViewController(mediaQualityController, animated: true)
     }
 
+    private func showVideoChatInstance() {
+        let videoInstanceController = SettingsVideoChatViewController(dcContext: dcContext)
+        navigationController?.pushViewController(videoInstanceController, animated: true)
+    }
+
     private func showBlockedContacts() {
         let blockedContactsController = BlockedContactsViewController(dcContext: dcContext)
         navigationController?.pushViewController(blockedContactsController, animated: true)

+ 53 - 0
deltachat-ios/Controller/SettingsVideoChatInstanceController.swift

@@ -0,0 +1,53 @@
+import UIKit
+import DcCore
+class SettingsVideoChatViewController: UITableViewController {
+
+    private var dcContext: DcContext
+
+    private lazy var videoInstanceCell: TextFieldCell = {
+        let cell = TextFieldCell.makeConfigCell(labelID: String.localized("videochat_instance"),
+                                                placeholderID: String.localized("videochat_instance_placeholder"))
+        cell.textField.autocapitalizationType = .none
+        cell.textField.autocorrectionType = .no
+        cell.textField.textContentType = .URL
+        return cell
+    }()
+
+    init(dcContext: DcContext) {
+        self.dcContext = dcContext
+        super.init(style: .grouped)
+        self.title = String.localized("videochat_instance")
+        hidesBottomBarWhenPushed = true
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // MARK: - Table view data source
+
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 1
+    }
+
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return 1
+    }
+
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
+    }
+
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        videoInstanceCell.textField.text = dcContext.getConfig("webrtc_instance")
+        return videoInstanceCell
+    }
+
+    override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+        return String.localized("videochat_instance_explain")
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        dcContext.setConfig("webrtc_instance", videoInstanceCell.getText())
+    }
+}

+ 53 - 0
deltachat-ios/Controller/SettingsVideoChatViewController.swift

@@ -0,0 +1,53 @@
+import UIKit
+import DcCore
+class SettingsVideoChatViewController: UITableViewController {
+
+    private var dcContext: DcContext
+
+    private lazy var videoInstanceCell: TextFieldCell = {
+        let cell = TextFieldCell.makeConfigCell(labelID: String.localized("videochat_instance"),
+                                                placeholderID: String.localized("videochat_instance_placeholder"))
+        cell.textField.autocapitalizationType = .none
+        cell.textField.autocorrectionType = .no
+        cell.textField.textContentType = .URL
+        return cell
+    }()
+
+    init(dcContext: DcContext) {
+        self.dcContext = dcContext
+        super.init(style: .grouped)
+        self.title = String.localized("videochat_instance")
+        hidesBottomBarWhenPushed = true
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    // MARK: - Table view data source
+
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        return 1
+    }
+
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return 1
+    }
+
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
+    }
+
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        videoInstanceCell.textField.text = dcContext.getConfig("webrtc_instance")
+        return videoInstanceCell
+    }
+
+    override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+        return String.localized("videochat_instance_explain")
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        dcContext.setConfig("webrtc_instance", videoInstanceCell.getText())
+    }
+}

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

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