Răsfoiți Sursa

Merge pull request #1440 from deltachat/webxdc

webxdc
cyBerta 3 ani în urmă
părinte
comite
eaf5f3e0a4

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

@@ -151,6 +151,17 @@ public class DcContext {
         dc_download_full_msg(contextPointer, Int32(id))
     }
 
+    public func sendWebxdcStatusUpdate(msgId: Int, payload: String, description: String) -> Bool {
+        return dc_send_webxdc_status_update(contextPointer, UInt32(msgId), payload, description) == 1
+    }
+
+    public func getWebxdcStatusUpdates(msgId: Int, statusUpdateId: Int) -> String {
+        guard let cString = dc_get_webxdc_status_updates(contextPointer, UInt32(msgId), UInt32(statusUpdateId)) else { return "" }
+        let swiftString = String(cString: cString)
+        dc_str_unref(cString)
+        return swiftString
+    }
+
     public func sendVideoChatInvitation(chatId: Int) -> Int {
         return Int(dc_send_videochat_invitation(contextPointer, UInt32(chatId)))
     }
@@ -1052,6 +1063,8 @@ public class DcMsg {
             return .video
         case DC_MSG_VOICE:
             return .voice
+        case DC_MSG_WEBXDC:
+            return .webxdc
         default:
             return nil
         }
@@ -1083,6 +1096,28 @@ public class DcMsg {
         }
     }()
 
+    public func getWebxdcBlob(filename: String) -> Data {
+        let ptrSize = UnsafeMutablePointer<Int>.allocate(capacity: 1)
+        defer {
+            ptrSize.deallocate()
+        }
+        guard let ccharPtr = dc_msg_get_webxdc_blob(messagePointer, filename, ptrSize) else {
+            return Data()
+        }
+
+        let count = ptrSize.pointee
+        let buffer = UnsafeBufferPointer<Int8>(start: ccharPtr, count: count)
+        dc_str_unref(ccharPtr)
+        return Data(buffer: buffer)
+    }
+
+    public func getWebxdcInfoJson() -> String {
+        guard let cString = dc_msg_get_webxdc_info(messagePointer) else { return "" }
+        let swiftString = String(cString: cString)
+        dc_str_unref(cString)
+        return swiftString
+    }
+
     public var messageHeight: CGFloat {
         return CGFloat(dc_msg_get_height(messagePointer))
     }
@@ -1388,6 +1423,7 @@ public enum MessageViewType: CustomStringConvertible {
     case text
     case video
     case voice
+    case webxdc
 
     public var description: String {
         switch self {
@@ -1399,6 +1435,7 @@ public enum MessageViewType: CustomStringConvertible {
         case .text: return "Text"
         case .video: return "Video"
         case .voice: return "Voice"
+        case .webxdc: return "Webxdc"
         }
     }
 }

+ 18 - 0
DcCore/DcCore/DC/events.swift

@@ -11,6 +11,7 @@ public let dcNotificationChatModified = Notification.Name(rawValue: "dcNotificat
 public let dcEphemeralTimerModified =  Notification.Name(rawValue: "dcEphemeralTimerModified")
 public let dcMsgsNoticed = Notification.Name(rawValue: "dcMsgsNoticed")
 public let dcNotificationConnectivityChanged = Notification.Name(rawValue: "dcNotificationConnectivityChanged")
+public let dcNotificationWebxdcUpdate = Notification.Name(rawValue: "dcNotificationWebxdcUpdate")
 
 public class DcEventHandler {
     let dcAccounts: DcAccounts
@@ -222,6 +223,23 @@ public class DcEventHandler {
                 )
             }
 
+        case DC_EVENT_WEBXDC_STATUS_UPDATE:
+            if dcContext.id != dcAccounts.getSelected().id {
+                return
+            }
+            dcContext.logger?.info("webxdc: update!")
+            DispatchQueue.main.async {
+                let nc = NotificationCenter.default
+                nc.post(
+                    name: dcNotificationWebxdcUpdate,
+                    object: nil,
+                    userInfo: [
+                        "message_id": Int(data1),
+                        "status_id": Int(data2),
+                    ]
+                )
+            }
+
         default:
             dcContext.logger?.warning("unknown event: \(id)")
         }

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

@@ -40,11 +40,13 @@
 		303492CB257A814200A523D0 /* DraftArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303492CA257A814200A523D0 /* DraftArea.swift */; };
 		304219D3243F588500516852 /* DcCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 304219D1243F588500516852 /* DcCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304219D82440734A00516852 /* DcMsg+Extension.swift */; };
+		304A92C127AAE10B00588A15 /* WebxdcPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304A92C027AAE10B00588A15 /* WebxdcPreview.swift */; };
 		304F5E44244F571C00462538 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A9FB14A1FB061E2001FEA36 /* Assets.xcassets */; };
 		304F769525DD237B0094B5E2 /* WebViewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304F769425DD237B0094B5E2 /* WebViewViewController.swift */; };
 		304F769925DD23D70094B5E2 /* FullMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 304F769825DD23D70094B5E2 /* FullMessageViewController.swift */; };
 		3052C60A253F082E007D13EA /* MessageLabelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3052C609253F082E007D13EA /* MessageLabelDelegate.swift */; };
 		3052C60E253F088E007D13EA /* DetectorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3052C60D253F088E007D13EA /* DetectorType.swift */; };
+		305501742798CDE1008FD5CA /* WebxdcViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305501732798CDE1008FD5CA /* WebxdcViewController.swift */; };
 		3057027F24C5B2F800D84EFC /* ChatListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3057027E24C5B2F800D84EFC /* ChatListViewModel.swift */; };
 		3057028724C5C88300D84EFC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 306011B422E5E7FB00C1CE6F /* Localizable.stringsdict */; };
 		3057028C24C5E7B600D84EFC /* ContactCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE77838E23E4276D0093EABD /* ContactCellViewModel.swift */; };
@@ -66,6 +68,7 @@
 		3067AAC62667F3FE00525036 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3067AAC52667F3FE00525036 /* ImageFormat.swift */; };
 		3067AAC72667F3FE00525036 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3067AAC52667F3FE00525036 /* ImageFormat.swift */; };
 		306C32322445CDE9001D89F3 /* DcLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306C32312445CDE9001D89F3 /* DcLogger.swift */; };
+		30703B6D27AA80FF00BDADE6 /* WebxdcCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30703B6C27AA80FF00BDADE6 /* WebxdcCell.swift */; };
 		30734326249A280B00BF9AD1 /* MediaQualityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30734325249A280B00BF9AD1 /* MediaQualityController.swift */; };
 		307A82CC25B8D26700748B57 /* ChatEditingBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307A82CB25B8D26700748B57 /* ChatEditingBar.swift */; };
 		307D822E241669C7006D2490 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307D822D241669C7006D2490 /* LocationManager.swift */; };
@@ -280,10 +283,12 @@
 		303492CA257A814200A523D0 /* DraftArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftArea.swift; sourceTree = "<group>"; };
 		304219D1243F588500516852 /* DcCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DcCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		304219D82440734A00516852 /* DcMsg+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DcMsg+Extension.swift"; sourceTree = "<group>"; };
+		304A92C027AAE10B00588A15 /* WebxdcPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcPreview.swift; sourceTree = "<group>"; };
 		304F769425DD237B0094B5E2 /* WebViewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewViewController.swift; sourceTree = "<group>"; };
 		304F769825DD23D70094B5E2 /* FullMessageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullMessageViewController.swift; sourceTree = "<group>"; };
 		3052C609253F082E007D13EA /* MessageLabelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLabelDelegate.swift; sourceTree = "<group>"; };
 		3052C60D253F088E007D13EA /* DetectorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectorType.swift; sourceTree = "<group>"; };
+		305501732798CDE1008FD5CA /* WebxdcViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcViewController.swift; sourceTree = "<group>"; };
 		3057027E24C5B2F800D84EFC /* ChatListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewModel.swift; sourceTree = "<group>"; };
 		3057029A24C6441300D84EFC /* EmptyStateLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyStateLabel.swift; sourceTree = "<group>"; };
 		305702A024C6453700D84EFC /* TypeAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeAlias.swift; sourceTree = "<group>"; };
@@ -339,6 +344,7 @@
 		3067AA4B2666310E00525036 /* ChatInputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputTextView.swift; sourceTree = "<group>"; };
 		3067AAC52667F3FE00525036 /* ImageFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = "<group>"; };
 		306C32312445CDE9001D89F3 /* DcLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DcLogger.swift; sourceTree = "<group>"; };
+		30703B6C27AA80FF00BDADE6 /* WebxdcCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcCell.swift; sourceTree = "<group>"; };
 		30734325249A280B00BF9AD1 /* MediaQualityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaQualityController.swift; sourceTree = "<group>"; };
 		307A82CB25B8D26700748B57 /* ChatEditingBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatEditingBar.swift; sourceTree = "<group>"; };
 		307D822D241669C7006D2490 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
@@ -734,6 +740,7 @@
 				303492AC2577CAC300A523D0 /* FileView.swift */,
 				303492B22577E40700A523D0 /* DocumentPreview.swift */,
 				303492CA257A814200A523D0 /* DraftArea.swift */,
+				304A92C027AAE10B00588A15 /* WebxdcPreview.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -749,6 +756,7 @@
 				3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */,
 				3010968826838A040032CBA0 /* VideoInviteCell.swift */,
 				30AAD71A2762869600DE3DC1 /* SelectableCell.swift */,
+				30703B6C27AA80FF00BDADE6 /* WebxdcCell.swift */,
 			);
 			path = Cells;
 			sourceTree = "<group>";
@@ -909,6 +917,7 @@
 				AE19887423EB264000B4CD5F /* HelpViewController.swift */,
 				304F769425DD237B0094B5E2 /* WebViewViewController.swift */,
 				304F769825DD23D70094B5E2 /* FullMessageViewController.swift */,
+				305501732798CDE1008FD5CA /* WebxdcViewController.swift */,
 				785BE16721E247F1003BE98C /* MessageInfoViewController.swift */,
 				7AE0A5481FC42F65005ECB4B /* NewChatViewController.swift */,
 				7A0052C71FBE6CB40048C3BF /* NewContactController.swift */,
@@ -1402,6 +1411,7 @@
 				304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
 				AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
 				78E45E4421D3F14A00D4B15E /* UIImage+Extension.swift in Sources */,
+				304A92C127AAE10B00588A15 /* WebxdcPreview.swift in Sources */,
 				3080A022277DE09900E74565 /* InputTextView.swift in Sources */,
 				30734326249A280B00BF9AD1 /* MediaQualityController.swift in Sources */,
 				3080A01C277DDB8A00E74565 /* InputBarAccessoryViewDelegate.swift in Sources */,
@@ -1439,6 +1449,7 @@
 				306C32322445CDE9001D89F3 /* DcLogger.swift in Sources */,
 				3080A036277DE30100E74565 /* NSMutableAttributedString+Extensions.swift in Sources */,
 				307A82CC25B8D26700748B57 /* ChatEditingBar.swift in Sources */,
+				30703B6D27AA80FF00BDADE6 /* WebxdcCell.swift in Sources */,
 				303492952565AABC00A523D0 /* DraftModel.swift in Sources */,
 				78E45E3A21D3CFBC00D4B15E /* SettingsController.swift in Sources */,
 				3080A021277DE09900E74565 /* InputStackView.swift in Sources */,
@@ -1468,6 +1479,7 @@
 				3080A035277DE30100E74565 /* String+Extensions.swift in Sources */,
 				302B84CE2397F6CD001C261F /* URL+Extension.swift in Sources */,
 				7A9FB1441FB061E2001FEA36 /* AppDelegate.swift in Sources */,
+				305501742798CDE1008FD5CA /* WebxdcViewController.swift in Sources */,
 				3034929F25752FC800A523D0 /* MediaPreview.swift in Sources */,
 				AE76E5EE242BF2EA003CF461 /* WelcomeViewController.swift in Sources */,
 				3052C60A253F082E007D13EA /* MessageLabelDelegate.swift in Sources */,

+ 10 - 1
deltachat-ios/Chat/ChatViewController.swift

@@ -297,6 +297,7 @@ class ChatViewController: UITableViewController {
         tableView.register(InfoMessageCell.self, forCellReuseIdentifier: "info")
         tableView.register(AudioMessageCell.self, forCellReuseIdentifier: "audio")
         tableView.register(VideoInviteCell.self, forCellReuseIdentifier: "video_invite")
+        tableView.register(WebxdcCell.self, forCellReuseIdentifier: "webxdc")
         tableView.rowHeight = UITableView.automaticDimension
         tableView.separatorStyle = .none
         tableView.keyboardDismissMode = .interactive
@@ -709,7 +710,8 @@ class ChatViewController: UITableViewController {
             } else {
                 cell = tableView.dequeueReusableCell(withIdentifier: "file", for: indexPath) as? FileTextCell ?? FileTextCell()
             }
-
+        case DC_MSG_WEBXDC:
+                cell = tableView.dequeueReusableCell(withIdentifier: "webxdc", for: indexPath) as? WebxdcCell ?? WebxdcCell()
         case DC_MSG_AUDIO, DC_MSG_VOICE:
             let audioMessageCell: AudioMessageCell = tableView.dequeueReusableCell(withIdentifier: "audio",
                                                                                       for: indexPath) as? AudioMessageCell ?? AudioMessageCell()
@@ -1601,6 +1603,11 @@ class ChatViewController: UITableViewController {
         )
     }
 
+    func showWebxdcViewFor(message: DcMsg) {
+        let webxdcViewController = WebxdcViewController(dcContext: dcContext, messageId: message.id)
+        navigationController?.pushViewController(webxdcViewController, animated: true)
+    }
+
     func showMediaGalleryFor(indexPath: IndexPath) {
         let messageId = messageIds[indexPath.row]
         let message = dcContext.getMessage(id: messageId)
@@ -1708,6 +1715,8 @@ extension ChatViewController: BaseMessageCellDelegate {
         let msg = dcContext.getMessage(id: messageIds[indexPath.row])
         if msg.downloadState != DC_DOWNLOAD_DONE {
             dcContext.downloadFullMessage(id: msg.id)
+        } else if msg.type == DC_MSG_WEBXDC {
+            showWebxdcViewFor(message: msg)
         } else {
             let fullMessageViewController = FullMessageViewController(dcContext: dcContext, messageId: msg.id)
             navigationController?.pushViewController(fullMessageViewController, animated: true)

+ 7 - 6
deltachat-ios/Chat/Views/Cells/BaseMessageCell.swift

@@ -354,7 +354,8 @@ public class BaseMessageCell: UITableViewCell {
 
         let downloadState = msg.downloadState
         let hasHtml = msg.hasHtml
-        isActionButtonHidden = !hasHtml && downloadState == DC_DOWNLOAD_DONE
+        let hasWebxdc =  msg.type == DC_MSG_WEBXDC
+        isActionButtonHidden = !hasWebxdc && !hasHtml && downloadState == DC_DOWNLOAD_DONE
         
         switch downloadState {
         case DC_DOWNLOAD_FAILURE, DC_DOWNLOAD_AVAILABLE:
@@ -363,11 +364,11 @@ public class BaseMessageCell: UITableViewCell {
             actionButton.isEnabled = false
             actionButton.setTitle(String.localized("downloading"), for: .normal)
         default:
-            break
-        }
-        
-        if hasHtml {
-            actionButton.setTitle(String.localized("show_full_message"), for: .normal)
+            if hasHtml {
+                actionButton.setTitle(String.localized("show_full_message"), for: .normal)
+            } else if hasWebxdc {
+                actionButton.setTitle(String.localized("start_app"), for: .normal)
+            }
         }
 
         messageBackgroundContainer.update(rectCorners: messageStyle,

+ 49 - 0
deltachat-ios/Chat/Views/Cells/WebxdcCell.swift

@@ -0,0 +1,49 @@
+import UIKit
+import DcCore
+
+public class WebxdcCell: BaseMessageCell {
+    
+    private var spacer: NSLayoutConstraint?
+    
+    private lazy var webxdcView: WebxdcPreview = {
+        let view = WebxdcPreview()
+        return view
+    }()
+    
+
+    override func setupSubviews() {
+        super.setupSubviews()
+        let spacerView = UIView()
+        spacer = spacerView.constraintHeightTo(8, priority: .defaultHigh)
+        spacer?.isActive = true
+        spacerView.constraintWidthTo(300, priority: UILayoutPriority(rawValue: 400)).isActive = true
+        mainContentView.addArrangedSubview(webxdcView)
+        mainContentView.addArrangedSubview(spacerView)
+        mainContentView.addArrangedSubview(messageLabel)
+        mainContentViewHorizontalPadding = 12
+    }
+
+    public override func prepareForReuse() {
+        super.prepareForReuse()
+        webxdcView.prepareForReuse()
+    }
+
+    override func update(dcContext: DcContext, msg: DcMsg, messageStyle: UIRectCorner, showAvatar: Bool, showName: Bool, searchText: String? = nil, highlight: Bool) {
+        if let text = msg.text, !text.isEmpty {
+            messageLabel.text = text
+            spacer?.isActive = true
+        } else {
+            spacer?.isActive = false
+        }
+        
+        webxdcView.configure(message: msg)
+        accessibilityLabel = "\(webxdcView.configureAccessibilityLabel())"
+        super.update(dcContext: dcContext,
+                     msg: msg,
+                     messageStyle: messageStyle,
+                     showAvatar: showAvatar,
+                     showName: showName,
+                     searchText: searchText,
+                     highlight: highlight)
+    }
+}

+ 91 - 0
deltachat-ios/Chat/Views/WebxdcPreview.swift

@@ -0,0 +1,91 @@
+import UIKit
+import DcCore
+
+public class WebxdcPreview: UIView {
+    
+    lazy var imagePreview: UIImageView = {
+        let view = UIImageView()
+        view.contentMode = .left
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.layer.cornerRadius = 8
+        view.clipsToBounds = true
+        return view
+    }()
+    
+    lazy var titleView: UILabel = {
+        let view = UILabel()
+        view.font = UIFont.preferredBoldFont(for: .body)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.numberOfLines = 1
+        view.lineBreakMode = .byTruncatingTail
+        isAccessibilityElement = false
+        return view
+    }()
+    
+    lazy var subtitleView: UILabel = {
+        let view = UILabel()
+        view.font = UIFont.preferredFont(forTextStyle: .body)
+        view.translatesAutoresizingMaskIntoConstraints = false
+        view.numberOfLines = 3
+        isAccessibilityElement = false
+        return view
+    }()
+    
+    lazy var containerStackView: UIStackView = {
+        let view = UIStackView(arrangedSubviews: [imagePreview, titleView, subtitleView])
+        view.axis = .vertical
+        view.alignment = .leading
+        view.spacing = 6
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+    
+    convenience init() {
+        self.init(frame: .zero)
+
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        self.setupSubviews()
+    }
+    
+    private func setupSubviews() {
+        addSubview(containerStackView)
+        containerStackView.fillSuperview()
+    }
+    
+    public func configure(message: DcMsg) {
+        let dict = message.getWebxdcInfoDict()
+        if let iconfilePath = dict["icon"] as? String {
+            let blob = message.getWebxdcBlob(filename: iconfilePath)
+            if !blob.isEmpty {
+                imagePreview.image = UIImage(data: blob)?.sd_resizedImage(with: CGSize(width: 175, height: 175), scaleMode: .aspectFill)
+            }
+        }
+        titleView.text = dict["name"] as? String
+        subtitleView.text = dict["summary"] as? String ?? "Webxdc"
+    }
+
+    public func configureAccessibilityLabel() -> String {
+        var accessibilityTitle = ""
+        var accessiblitySubtitle = ""
+        if let titleText = titleView.text {
+            accessibilityTitle = titleText
+        }
+        if let subtitleText = subtitleView.text {
+            accessiblitySubtitle = subtitleText
+        }
+        
+        return "\(accessibilityTitle), \(accessiblitySubtitle)"
+    }
+
+    public func prepareForReuse() {
+        imagePreview.image = nil
+    }
+    
+}

+ 8 - 5
deltachat-ios/Controller/WebViewViewController.swift

@@ -4,16 +4,19 @@ import WebKit
 class WebViewViewController: UIViewController, WKNavigationDelegate {
 
     public lazy var webView: WKWebView = {
+        let view = WKWebView(frame: .zero, configuration: configuration)
+        view.navigationDelegate = self
+        return view
+    }()
+
+    open var configuration: WKWebViewConfiguration {
         let preferences = WKPreferences()
         preferences.javaScriptEnabled = false
 
         let configuration = WKWebViewConfiguration()
         configuration.preferences = preferences
-
-        let view = WKWebView(frame: .zero, configuration: configuration)
-        view.navigationDelegate = self
-        return view
-    }()
+        return configuration
+    }
 
     init() {
         super.init(nibName: nil, bundle: nil)

+ 281 - 0
deltachat-ios/Controller/WebxdcViewController.swift

@@ -0,0 +1,281 @@
+import UIKit
+import WebKit
+import DcCore
+
+class WebxdcViewController: WebViewViewController {
+    
+    enum WebxdcHandler: String {
+        case log  = "log"
+        case getStatusUpdates = "getStatusUpdatesHandler"
+        case sendStatusUpdate = "sendStatusUpdateHandler"
+    }
+    let INTERNALSCHEMA = "webxdc"
+    let INTERNALDOMAIN = "local.app"
+    
+    var messageId: Int
+    var dcContext: DcContext
+    var webxdcUpdateObserver: NSObjectProtocol?
+    
+    
+    // Block just everything :)
+    let blockRules = """
+    [
+        {
+            "trigger": {
+                "url-filter": ".*"
+            },
+            "action": {
+                "type": "block"
+            }
+        }
+    ]
+    """
+    
+    lazy var webxdcbridge: String = {
+        let script = """
+        window.webxdc = (() => {
+          var log = (s)=>webkit.messageHandlers.log.postMessage(s);
+        
+          var update_listener = () => {};
+        
+          // instead of calling .getStatusUpdatesHandler (-> async),
+          // we're passing the updates directly to this js function
+          window.__webxdcUpdate = (updateString) => {
+            try {
+                var updates = JSON.parse(updateString);
+                if (updates.length === 1) {
+                  update_listener(updates[0]);
+                }
+            } catch (e) {
+                log("json error: "+ e.message)
+            }
+          };
+        
+          
+          var async_calls = {};
+          var async_call_id = 0;
+          window.__resolve_async_call = (id, rawPayload) => {
+            try {
+                const payload = JSON.parse(rawPayload);
+                if (async_calls[id]) {
+                  async_calls[id](payload);
+                  delete async_calls[id];
+                }
+            } catch (e) {
+                log("json error: "+ e.message)
+            }
+          };
+        
+          return {
+            selfAddr: atob("\((dcContext.addr ?? "unknown").toBase64())"),
+        
+            selfName: atob("\((dcContext.displayname ?? dcContext.addr ?? "unknown").toBase64())"),
+        
+            setUpdateListener: (cb) => (update_listener = cb),
+        
+            getAllUpdates: () => {
+              const invocation_id = async_call_id++;
+              webkit.messageHandlers.getStatusUpdatesHandler.postMessage(invocation_id);
+              return new Promise((resolve, reject) => {async_calls[invocation_id] = resolve;});
+            },
+        
+            sendUpdate: (payload, descr) => {
+                // only one parameter is allowed, we we create a new parameter object here
+                var parameter = {
+                    payload: payload,
+                    descr: descr
+                };
+                webkit.messageHandlers.sendStatusUpdateHandler.postMessage(parameter);
+            },
+          };
+        })();
+        """
+        return script
+    }()
+    
+    override var configuration: WKWebViewConfiguration {
+        let config = WKWebViewConfiguration()
+        let preferences = WKPreferences()
+        let contentController = WKUserContentController()
+        
+        contentController.add(self, name: WebxdcHandler.sendStatusUpdate.rawValue)
+        contentController.add(self, name: WebxdcHandler.getStatusUpdates.rawValue)
+        contentController.add(self, name: WebxdcHandler.log.rawValue)
+        
+        config.userContentController = contentController
+        config.setURLSchemeHandler(self, forURLScheme: INTERNALSCHEMA)
+        
+        if #available(iOS 13.0, *) {
+            preferences.isFraudulentWebsiteWarningEnabled = true
+        }
+        
+        if #available(iOS 14.0, *) {
+            config.defaultWebpagePreferences.allowsContentJavaScript = true
+        } else {
+            preferences.javaScriptEnabled = true
+        }
+        preferences.javaScriptCanOpenWindowsAutomatically = false
+        config.preferences = preferences
+        return config
+    }
+    
+    
+    init(dcContext: DcContext, messageId: Int) {
+        self.dcContext = dcContext
+        self.messageId = messageId
+        super.init()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        self.title = dcContext.getMessage(id: messageId).getWebxdcInfoDict()["name"] as? String
+    }
+    
+    override func willMove(toParent parent: UIViewController?) {
+        super.willMove(toParent: parent)
+        if parent == nil {
+            // remove observer
+            let nc = NotificationCenter.default
+            if let webxdcUpdateObserver = webxdcUpdateObserver {
+                nc.removeObserver(webxdcUpdateObserver)
+            }
+        } else {
+            addObserver()
+        }
+    }
+    
+    private func addObserver() {
+        let nc = NotificationCenter.default
+        webxdcUpdateObserver = nc.addObserver(
+            forName: dcNotificationWebxdcUpdate,
+            object: nil,
+            queue: OperationQueue.main
+        ) { [weak self] notification in
+            guard let self = self else { return }
+            guard let ui = notification.userInfo,
+                  let messageId = ui["message_id"] as? Int,
+                  let statusId = ui["status_id"] as? Int,
+                  messageId == self.messageId else {
+                      logger.error("failed to handle dcNotificationWebxdcUpdate")
+                      return
+                  }
+            self.updateWebxdc(statusId: statusId)
+        }
+    }
+    
+    override func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
+        // TODO: what about tel:// and mailto://
+        if let url = navigationAction.request.url,
+           url.scheme != INTERNALSCHEMA {
+            logger.debug("cancel loading: \(url)")
+            decisionHandler(.cancel)
+            return
+        }
+        logger.debug("loading: \(String(describing: navigationAction.request.url))")
+        decisionHandler(.allow)
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        loadHtml()
+    }
+    
+    
+    private func loadRestrictedHtml() {
+        // TODO: compile only once
+        WKContentRuleListStore.default().compileContentRuleList(
+            forIdentifier: "WebxdcContentBlockingRules",
+            encodedContentRuleList: blockRules) { (contentRuleList, error) in
+                
+                guard let contentRuleList = contentRuleList, error == nil else {
+                    return
+                }
+                
+                let configuration = self.webView.configuration
+                configuration.userContentController.add(contentRuleList)
+                self.loadHtml()
+            }
+    }
+    
+    private func loadHtml() {
+        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+            guard let self = self else { return }
+            DispatchQueue.main.async {
+                self.webView.load(URLRequest(url: URL(string: "\(self.INTERNALSCHEMA)://msg\(self.messageId).\(self.INTERNALDOMAIN)/index.html")!))
+            }
+        }
+    }
+    
+    private func updateWebxdc(statusId: Int) {
+        let statusUpdates = self.dcContext.getWebxdcStatusUpdates(msgId: messageId, statusUpdateId: statusId)
+        logger.debug("status updates: \(statusUpdates)")
+        webView.evaluateJavaScript("window.__webxdcUpdate(atob(\"\(statusUpdates.toBase64())\"))", completionHandler: nil)
+    }
+}
+
+extension WebxdcViewController: WKScriptMessageHandler {
+    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        let handler = WebxdcHandler(rawValue: message.name)
+        switch handler {
+        case .getStatusUpdates:
+            guard let invocationId = message.body as? Int else {
+                logger.error("could not convert param \(message.body) to int")
+                return
+            }
+            let statusUpdates = dcContext.getWebxdcStatusUpdates(msgId: messageId, statusUpdateId: 0)
+            logger.debug("status updates for message \(messageId): \(statusUpdates)")
+            webView.evaluateJavaScript("window.__resolve_async_call(\(invocationId), (atob(\"\(statusUpdates.toBase64())\")))", completionHandler: nil)
+            
+        case .log:
+            guard let msg = message.body as? String else {
+                logger.error("could not convert param \(message.body) to string")
+                return
+            }
+            logger.debug("webxdc log msg: "+msg)
+            
+        case .sendStatusUpdate:
+            guard let dict = message.body as? [String: AnyObject],
+                  let payloadDict = dict["payload"] as?  [String: AnyObject],
+                  let payloadJson = try? JSONSerialization.data(withJSONObject: payloadDict, options: []),
+                  let payloadString = String(data: payloadJson, encoding: .utf8),
+                  let description = dict["descr"] as? String else {
+                      logger.error("Failed to parse status update parameters \(message.body)")
+                      return
+                  }
+            
+            _ = dcContext.sendWebxdcStatusUpdate(msgId: messageId, payload: payloadString, description: description)
+        default:
+            logger.debug("another method was called")
+        }
+    }
+}
+
+extension WebxdcViewController: WKURLSchemeHandler {
+    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
+        if let url = urlSchemeTask.request.url, let scheme = url.scheme, scheme == INTERNALSCHEMA {
+            let file = url.path
+            let dcMsg = dcContext.getMessage(id: messageId)
+            var data: Data
+            if url.lastPathComponent == "webxdc.js" {
+                data = Data(webxdcbridge.utf8)
+            } else {
+                data = dcMsg.getWebxdcBlob(filename: file)
+            }
+            let mimeType = DcUtils.getMimeTypeForPath(path: file)
+            let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
+            
+            urlSchemeTask.didReceive(response)
+            urlSchemeTask.didReceive(data)
+            urlSchemeTask.didFinish()
+        } else {
+            logger.debug("not loading \(String(describing: urlSchemeTask.request.url))")
+        }
+    }
+    
+    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
+    }
+}

+ 9 - 0
deltachat-ios/DC/DcMsg+Extension.swift

@@ -11,4 +11,13 @@ extension DcMsg {
         let size = String(format: "%.1f", Double(filesize) / pow(1024, Double(digitGroups)))
         return "\(size) \(units[digitGroups])"
     }
+
+    public func getWebxdcInfoDict() -> [String: AnyObject] {
+        let jsonString = self.getWebxdcInfoJson()
+        if let data: Data = jsonString.data(using: .utf8),
+           let infoDict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: AnyObject] {
+               return infoDict
+           }
+        return [:]
+    }
 }

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

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

+ 4 - 0
deltachat-ios/Extensions/String+Extension.swift

@@ -81,4 +81,8 @@ extension String {
             return "\(number)"
         }
     }
+
+    func toBase64() -> String {
+        return Data(self.utf8).base64EncodedString()
+    }
 }