Przeglądaj źródła

add basic webxdc selector UI

cyberta 2 lat temu
rodzic
commit
b2d9e0c2fa

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

@@ -21,6 +21,8 @@
 		30152CA025A5D97900377714 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961832346125000C80F33 /* UIEdgeInsets+Extensions.swift */; };
 		30152CA025A5D97900377714 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961832346125000C80F33 /* UIEdgeInsets+Extensions.swift */; };
 		3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */; };
 		3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */; };
 		3022E6BE22E8768800763272 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3022E6C022E8768800763272 /* InfoPlist.strings */; };
 		3022E6BE22E8768800763272 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3022E6C022E8768800763272 /* InfoPlist.strings */; };
+		30238CFB28A501C300EF14AC /* WebxdcSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30238CFA28A501C300EF14AC /* WebxdcSelector.swift */; };
+		30238CFD28A5028300EF14AC /* WebxdcGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30238CFC28A5028300EF14AC /* WebxdcGridCell.swift */; };
 		302589FF2452FA280086C1CD /* ShareAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302589FE2452FA280086C1CD /* ShareAttachment.swift */; };
 		302589FF2452FA280086C1CD /* ShareAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302589FE2452FA280086C1CD /* ShareAttachment.swift */; };
 		30260CA7238F02F700D8D52C /* MultilineTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30260CA6238F02F700D8D52C /* MultilineTextFieldCell.swift */; };
 		30260CA7238F02F700D8D52C /* MultilineTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30260CA6238F02F700D8D52C /* MultilineTextFieldCell.swift */; };
 		302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AC265E237F1807002A943F /* AvatarHelper.swift */; };
 		302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AC265E237F1807002A943F /* AvatarHelper.swift */; };
@@ -265,6 +267,8 @@
 		3022E6D022E8769D00763272 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6D022E8769D00763272 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6D122E8769E00763272 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6D122E8769E00763272 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6D322E876A100763272 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		3022E6D322E876A100763272 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		30238CFA28A501C300EF14AC /* WebxdcSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcSelector.swift; sourceTree = "<group>"; };
+		30238CFC28A5028300EF14AC /* WebxdcGridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebxdcGridCell.swift; sourceTree = "<group>"; };
 		302589FE2452FA280086C1CD /* ShareAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAttachment.swift; sourceTree = "<group>"; };
 		302589FE2452FA280086C1CD /* ShareAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAttachment.swift; sourceTree = "<group>"; };
 		30260CA6238F02F700D8D52C /* MultilineTextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldCell.swift; sourceTree = "<group>"; };
 		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>"; };
 		302B84C42396627F001C261F /* RelayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayHelper.swift; sourceTree = "<group>"; };
@@ -871,6 +875,7 @@
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
 				AE0AA951247800E700D42A7F /* GalleryCell.swift */,
 				AE0AA951247800E700D42A7F /* GalleryCell.swift */,
+				30238CFC28A5028300EF14AC /* WebxdcGridCell.swift */,
 				AE8DD450249D1DFB009A4BC1 /* DocumentGalleryFileCell.swift */,
 				AE8DD450249D1DFB009A4BC1 /* DocumentGalleryFileCell.swift */,
 			);
 			);
 			path = Cell;
 			path = Cell;
@@ -939,6 +944,7 @@
 				302D5453268B84CB00A8B271 /* SettingsVideoChatViewController.swift */,
 				302D5453268B84CB00A8B271 /* SettingsVideoChatViewController.swift */,
 				30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */,
 				30DAF71B275901610073C154 /* SettingsBackgroundSelectionController.swift */,
 				AE8F503424753DFE007FEE0B /* GalleryViewController.swift */,
 				AE8F503424753DFE007FEE0B /* GalleryViewController.swift */,
+				30238CFA28A501C300EF14AC /* WebxdcSelector.swift */,
 				30734325249A280B00BF9AD1 /* MediaQualityController.swift */,
 				30734325249A280B00BF9AD1 /* MediaQualityController.swift */,
 				30860EE826F49E64002651A6 /* DownloadOnDemandViewController.swift */,
 				30860EE826F49E64002651A6 /* DownloadOnDemandViewController.swift */,
 				AED423D2249F578B00B6B2BB /* AddGroupMembersViewController.swift */,
 				AED423D2249F578B00B6B2BB /* AddGroupMembersViewController.swift */,
@@ -1413,6 +1419,7 @@
 				7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
 				7070FB9B2101ECBB000DC258 /* NewGroupController.swift in Sources */,
 				3080A037277DE30100E74565 /* UITextView+Extensions.swift in Sources */,
 				3080A037277DE30100E74565 /* UITextView+Extensions.swift in Sources */,
 				3080A027277DE12D00E74565 /* InputBarButtonItem.swift in Sources */,
 				3080A027277DE12D00E74565 /* InputBarButtonItem.swift in Sources */,
+				30238CFB28A501C300EF14AC /* WebxdcSelector.swift in Sources */,
 				AE57C084255310BB003CFE70 /* ContextMenuController.swift in Sources */,
 				AE57C084255310BB003CFE70 /* ContextMenuController.swift in Sources */,
 				304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
 				304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
 				AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
 				AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
@@ -1452,6 +1459,7 @@
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
 				B26B3BC7236DC3DC008ED35A /* SwitchCell.swift in Sources */,
 				AEE700252438E0E500D6992E /* ProgressAlertHandler.swift in Sources */,
 				AEE700252438E0E500D6992E /* ProgressAlertHandler.swift in Sources */,
 				30E348E524F6647D005C93D1 /* FileTextCell.swift in Sources */,
 				30E348E524F6647D005C93D1 /* FileTextCell.swift in Sources */,
+				30238CFD28A5028300EF14AC /* WebxdcGridCell.swift in Sources */,
 				306C32322445CDE9001D89F3 /* DcLogger.swift in Sources */,
 				306C32322445CDE9001D89F3 /* DcLogger.swift in Sources */,
 				3080A036277DE30100E74565 /* NSMutableAttributedString+Extensions.swift in Sources */,
 				3080A036277DE30100E74565 /* NSMutableAttributedString+Extensions.swift in Sources */,
 				307A82CC25B8D26700748B57 /* ChatEditingBar.swift in Sources */,
 				307A82CC25B8D26700748B57 /* ChatEditingBar.swift in Sources */,

+ 12 - 0
deltachat-ios/Chat/ChatViewController.swift

@@ -1340,6 +1340,7 @@ class ChatViewController: UITableViewController {
         let galleryAction = PhotoPickerAlertAction(title: String.localized("gallery"), style: .default, handler: galleryButtonPressed(_:))
         let galleryAction = PhotoPickerAlertAction(title: String.localized("gallery"), style: .default, handler: galleryButtonPressed(_:))
         let cameraAction = PhotoPickerAlertAction(title: String.localized("camera"), style: .default, handler: cameraButtonPressed(_:))
         let cameraAction = PhotoPickerAlertAction(title: String.localized("camera"), style: .default, handler: cameraButtonPressed(_:))
         let documentAction = UIAlertAction(title: String.localized("files"), style: .default, handler: documentActionPressed(_:))
         let documentAction = UIAlertAction(title: String.localized("files"), style: .default, handler: documentActionPressed(_:))
+        let webxdcAction = UIAlertAction(title: String.localized("webxdcs"), style: .default, handler: webxdcButtonPressed(_:))
         let voiceMessageAction = UIAlertAction(title: String.localized("voice_message"), style: .default, handler: voiceMessageButtonPressed(_:))
         let voiceMessageAction = UIAlertAction(title: String.localized("voice_message"), style: .default, handler: voiceMessageButtonPressed(_:))
         let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
         let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
         let locationStreamingAction = UIAlertAction(title: isLocationStreaming ? String.localized("stop_sharing_location") : String.localized("location"),
         let locationStreamingAction = UIAlertAction(title: isLocationStreaming ? String.localized("stop_sharing_location") : String.localized("location"),
@@ -1348,6 +1349,7 @@ class ChatViewController: UITableViewController {
 
 
         alert.addAction(cameraAction)
         alert.addAction(cameraAction)
         alert.addAction(galleryAction)
         alert.addAction(galleryAction)
+        alert.addAction(webxdcAction)
         alert.addAction(documentAction)
         alert.addAction(documentAction)
         alert.addAction(voiceMessageAction)
         alert.addAction(voiceMessageAction)
 
 
@@ -1482,6 +1484,12 @@ class ChatViewController: UITableViewController {
         }
         }
     }
     }
 
 
+    private func showWebxdcSelector() {
+        let msgIds = dcContext.getChatMedia(chatId: 0, messageType: DC_MSG_WEBXDC, messageType2: 0, messageType3: 0)
+        let webxdcSelector = WebxdcSelector(context: dcContext, mediaMessageIds: msgIds)
+        navigationController?.present(webxdcSelector, animated: true)
+    }
+
     private func showDocumentLibrary() {
     private func showDocumentLibrary() {
         mediaPicker?.showDocumentLibrary()
         mediaPicker?.showDocumentLibrary()
     }
     }
@@ -1532,6 +1540,10 @@ class ChatViewController: UITableViewController {
         navigationController?.present(nav, animated: true)
         navigationController?.present(nav, animated: true)
     }
     }
 
 
+    private func webxdcButtonPressed(_ action: UIAlertAction) {
+        showWebxdcSelector()
+    }
+
     private func documentActionPressed(_ action: UIAlertAction) {
     private func documentActionPressed(_ action: UIAlertAction) {
         showDocumentLibrary()
         showDocumentLibrary()
     }
     }

+ 173 - 0
deltachat-ios/Controller/WebxdcSelector.swift

@@ -0,0 +1,173 @@
+import UIKit
+import DcCore
+import QuickLook
+
+class WebxdcSelector: UIViewController {
+
+    private let dcContext: DcContext
+    // MARK: - data
+    private var mediaMessageIds: [Int]
+    private var items: [Int: GalleryItem] = [:]
+
+    // MARK: - subview specs
+    private let gridDefaultSpacing: CGFloat = 5
+
+    private lazy var gridLayout: GridCollectionViewFlowLayout = {
+        let layout = GridCollectionViewFlowLayout()
+        layout.minimumLineSpacing = gridDefaultSpacing
+        layout.minimumInteritemSpacing = gridDefaultSpacing
+        layout.format = .square
+        return layout
+    }()
+
+    private lazy var grid: UICollectionView = {
+        let collection = UICollectionView(frame: .zero, collectionViewLayout: gridLayout)
+        collection.dataSource = self
+        collection.delegate = self
+        collection.register(WebxdcGridCell.self, forCellWithReuseIdentifier: WebxdcGridCell.reuseIdentifier)
+        collection.contentInset = UIEdgeInsets(top: gridDefaultSpacing, left: gridDefaultSpacing, bottom: gridDefaultSpacing, right: gridDefaultSpacing)
+        collection.backgroundColor = DcColors.defaultBackgroundColor
+        collection.delaysContentTouches = false
+        collection.alwaysBounceVertical = true
+        collection.isPrefetchingEnabled = true
+        collection.prefetchDataSource = self
+        return collection
+    }()
+
+    private lazy var emptyStateView: EmptyStateLabel = {
+        let label = EmptyStateLabel()
+        label.text = String.localized("tab_gallery_empty_hint")
+        label.isHidden = true
+        return label
+    }()
+
+       init(context: DcContext, mediaMessageIds: [Int]) {
+        self.dcContext = context
+        self.mediaMessageIds = mediaMessageIds
+        super.init(nibName: nil, bundle: nil)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    // MARK: - lifecycle
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        setupSubviews()
+        title = String.localized("webxdcs")
+        if mediaMessageIds.isEmpty {
+            emptyStateView.isHidden = false
+        }
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        grid.reloadData()
+    }
+
+    override func viewWillLayoutSubviews() {
+        super.viewWillLayoutSubviews()
+        self.reloadCollectionViewLayout()
+    }
+
+    // MARK: - setup
+    private func setupSubviews() {
+        view.addSubview(grid)
+        grid.translatesAutoresizingMaskIntoConstraints = false
+        grid.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
+        grid.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
+        grid.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
+        grid.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
+
+        emptyStateView.addCenteredTo(parentView: view)
+    }
+}
+
+extension WebxdcSelector: UICollectionViewDataSourcePrefetching {
+    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
+        indexPaths.forEach { if items[$0.row] == nil {
+            let message = dcContext.getMessage(id: mediaMessageIds[$0.row])
+            let item = GalleryItem(msg: message)
+            items[$0.row] = item
+        }}
+    }
+}
+
+// MARK: - UICollectionViewDataSource, UICollectionViewDelegate
+extension WebxdcSelector: UICollectionViewDataSource, UICollectionViewDelegate {
+
+    func numberOfSections(in collectionView: UICollectionView) -> Int {
+        return 1
+    }
+
+    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        return mediaMessageIds.count
+    }
+
+    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        guard let webxdcGridCell = collectionView.dequeueReusableCell(
+                withReuseIdentifier: WebxdcGridCell.reuseIdentifier,
+                for: indexPath) as? WebxdcGridCell else {
+            return UICollectionViewCell()
+        }
+
+        let msgId = mediaMessageIds[indexPath.row]
+        var item: GalleryItem
+        if let galleryItem = items[indexPath.row] {
+            item = galleryItem
+        } else {
+            let message = dcContext.getMessage(id: msgId)
+            let galleryItem = GalleryItem(msg: message)
+            items[indexPath.row] = galleryItem
+            item = galleryItem
+        }
+        webxdcGridCell.update(item: item)
+        UIMenuController.shared.setMenuVisible(false, animated: true)
+        return webxdcGridCell
+    }
+
+    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        let msgId = mediaMessageIds[indexPath.row]
+        // TODO: implement callback
+        collectionView.deselectItem(at: indexPath, animated: true)
+        UIMenuController.shared.setMenuVisible(false, animated: true)
+    }
+}
+
+// MARK: - grid layout + updates
+private extension WebxdcSelector {
+    func reloadCollectionViewLayout() {
+
+        // columns specification
+        let phonePortrait = 3
+        let phoneLandscape = 4
+        let padPortrait = 5
+        let padLandscape = 8
+
+        let orientation = UIApplication.shared.statusBarOrientation
+        let deviceType = UIDevice.current.userInterfaceIdiom
+
+        var gridDisplay: GridDisplay?
+        if deviceType == .phone {
+            if orientation.isPortrait {
+                gridDisplay = .grid(columns: phonePortrait)
+            } else {
+                gridDisplay = .grid(columns: phoneLandscape)
+            }
+        } else if deviceType == .pad {
+            if orientation.isPortrait {
+                gridDisplay = .grid(columns: padPortrait)
+            } else {
+                gridDisplay = .grid(columns: padLandscape)
+            }
+        }
+
+        if let gridDisplay = gridDisplay {
+            gridLayout.display = gridDisplay
+        } else {
+            safe_fatalError("undefined format")
+        }
+        let containerWidth = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right - 2 * gridDefaultSpacing
+        gridLayout.containerWidth = containerWidth
+    }
+}

+ 18 - 0
deltachat-ios/Model/GalleryItem.swift

@@ -12,6 +12,8 @@ class GalleryItem: ContextMenuItem {
         return msg.fileURL
         return msg.fileURL
     }
     }
 
 
+    var description: String?
+
     var thumbnailImage: UIImage? {
     var thumbnailImage: UIImage? {
         get {
         get {
             if let fileUrl = self.fileUrl {
             if let fileUrl = self.fileUrl {
@@ -51,6 +53,9 @@ class GalleryItem: ContextMenuItem {
         } else {
         } else {
             loadThumbnail()
             loadThumbnail()
         }
         }
+        if msg.viewtype == .webxdc {
+            description = msg.getWebxdcInfoDict()["name"] as? String ?? "ErrName"
+        }
     }
     }
 
 
     private func loadThumbnail() {
     private func loadThumbnail() {
@@ -62,6 +67,8 @@ class GalleryItem: ContextMenuItem {
             loadImageThumbnail(from: url)
             loadImageThumbnail(from: url)
         case .video:
         case .video:
             loadVideoThumbnail(from: url)
             loadVideoThumbnail(from: url)
+        case .webxdc:
+            loadWebxdcThumbnail(from: msg)
         default:
         default:
             safe_fatalError("unsupported viewtype - viewtype \(viewtype) not supported.")
             safe_fatalError("unsupported viewtype - viewtype \(viewtype) not supported.")
         }
         }
@@ -86,4 +93,15 @@ class GalleryItem: ContextMenuItem {
             }
             }
         }
         }
     }
     }
+
+    private func loadWebxdcThumbnail(from message: DcMsg) {
+        DispatchQueue.global(qos: .userInteractive).async {
+            let image = message.getWebxdcPreviewImage()
+            if let image = image {
+                DispatchQueue.main.async { [weak self] in
+                    self?.thumbnailImage = image
+                }
+            }
+        }
+    }
 }
 }

+ 77 - 0
deltachat-ios/View/Cell/WebxdcGridCell.swift

@@ -0,0 +1,77 @@
+import UIKit
+import DcCore
+import SDWebImage
+
+class WebxdcGridCell: UICollectionViewCell {
+    static let reuseIdentifier = "webxdc_cell"
+
+    weak var item: GalleryItem?
+
+    private lazy var imageView: SDAnimatedImageView = {
+        let view = SDAnimatedImageView()
+        view.contentMode = .scaleAspectFill
+        view.clipsToBounds = true
+        view.isAccessibilityElement = false
+        view.translatesAutoresizingMaskIntoConstraints = false
+        return view
+    }()
+
+    private lazy var descriptionLabel: UILabel = {
+        let label = UILabel()
+        label.translatesAutoresizingMaskIntoConstraints = false
+        label.font = UIFont.preferredFont(for: .caption1, weight: .light)
+        label.lineBreakMode = .byTruncatingTail
+        label.textColor = DcColors.defaultInverseColor
+        label.backgroundColor = DcColors.defaultBackgroundColor
+        return label
+    }()
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        backgroundColor = DcColors.defaultBackgroundColor
+        setupSubviews()
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func prepareForReuse() {
+        super.prepareForReuse()
+        item?.onImageLoaded = nil
+        item = nil
+        imageView.image = nil
+        descriptionLabel.text = nil
+    }
+
+    private func setupSubviews() {
+        contentView.addSubview(imageView)
+        contentView.addSubview(descriptionLabel)
+        addConstraints([
+            imageView.constraintAlignLeadingToAnchor(contentView.leadingAnchor),
+            imageView.constraintAlignTrailingToAnchor(contentView.trailingAnchor),
+            imageView.constraintAlignTopToAnchor( contentView.topAnchor),
+            descriptionLabel.constraintAlignLeadingTo(imageView),
+            descriptionLabel.constraintToBottomOf(imageView),
+            descriptionLabel.constraintAlignTrailingTo(imageView),
+            descriptionLabel.constraintAlignBottomToAnchor(contentView.bottomAnchor),
+        ])
+    }
+
+    func update(item: GalleryItem) {
+        self.item = item
+        item.onImageLoaded = { [weak self] image in
+            self?.imageView.image = image
+        }
+        imageView.image = item.thumbnailImage
+        descriptionLabel.text = item.description
+    }
+
+    override var isSelected: Bool {
+        willSet {
+            // to provide visual feedback on select events
+            contentView.backgroundColor = newValue ? DcColors.primary : .white
+            imageView.alpha = newValue ? 0.75 : 1.0
+        }
+    }
+}

+ 1 - 0
deltachat-ios/en.lproj/Localizable.strings

@@ -892,3 +892,4 @@
 // device messages for updates
 // device messages for updates
 "update_1_30" = "Faster. More stable.\n\nFor 1.30 releases, we focused on speed and reliability, fixing dozens of bugs. Check our changelogs if your favorite one is fixed: https://get.delta.chat/#changelogs 🚀";
 "update_1_30" = "Faster. More stable.\n\nFor 1.30 releases, we focused on speed and reliability, fixing dozens of bugs. Check our changelogs if your favorite one is fixed: https://get.delta.chat/#changelogs 🚀";
 
 
+"webxdcs" = "Apps";