Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App Picker #2499

Merged
merged 13 commits into from
Jan 17, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Fix scrolling issue when cancelling a screen (#2504)
- Fix layout of 'No contacts found' message (#2517)
- Don't hide the Apps-tab anymore (#2526)
- Use the new App-picker to share apps (#2450)

## v1.50.4
2025-01
Expand Down
8 changes: 4 additions & 4 deletions deltachat-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
30152CA025A5D97900377714 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305961832346125000C80F33 /* UIEdgeInsets+Extensions.swift */; };
3015634423A003BA00E9DEF4 /* AudioRecorderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3015634323A003BA00E9DEF4 /* AudioRecorderController.swift */; };
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 */; };
30238CFF28A5554C00EF14AC /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30238CFE28A5554C00EF14AC /* FileHelper.swift */; };
30238D0028A557E800EF14AC /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30238CFE28A5554C00EF14AC /* FileHelper.swift */; };
Expand Down Expand Up @@ -224,6 +223,7 @@
D84AED272B566C0700D753F6 /* ReactionsOverviewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84AED262B566C0700D753F6 /* ReactionsOverviewTableViewCell.swift */; };
D85DF9782C4A96CB00A01408 /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9772C4A96CB00A01408 /* UserDefaults+Extensions.swift */; };
D85DF9802C5250E200A01408 /* ProgressAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF97F2C5250E200A01408 /* ProgressAlertHandler.swift */; };
D86DF20C2D30318B00141B3E /* AppPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86DF20B2D30318B00141B3E /* AppPickerViewController.swift */; };
D878C4FD2CF72AA0009AF551 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D878C4FC2CF72AA0009AF551 /* WidgetKit.framework */; };
D878C4FF2CF72AA0009AF551 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D878C4FE2CF72AA0009AF551 /* SwiftUI.framework */; };
D878C50C2CF72AA1009AF551 /* DcWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D878C4FB2CF72AA0009AF551 /* DcWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -327,7 +327,6 @@
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>"; };
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>"; };
30238CFE28A5554C00EF14AC /* FileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = "<group>"; };
302589FE2452FA280086C1CD /* ShareAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAttachment.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -632,6 +631,7 @@
D84AED262B566C0700D753F6 /* ReactionsOverviewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsOverviewTableViewCell.swift; sourceTree = "<group>"; };
D85DF9772C4A96CB00A01408 /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = "<group>"; };
D85DF97F2C5250E200A01408 /* ProgressAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressAlertHandler.swift; sourceTree = "<group>"; };
D86DF20B2D30318B00141B3E /* AppPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPickerViewController.swift; sourceTree = "<group>"; };
D878C4FB2CF72AA0009AF551 /* DcWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DcWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D878C4FC2CF72AA0009AF551 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
D878C4FE2CF72AA0009AF551 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -865,6 +865,7 @@
3080A00D277DDA1400E74565 /* InputBarAccessoryView */,
30FDB6B524D193DD0066C48D /* Views */,
303492942565AABC00A523D0 /* DraftModel.swift */,
D86DF20B2D30318B00141B3E /* AppPickerViewController.swift */,
);
path = Chat;
sourceTree = "<group>";
Expand Down Expand Up @@ -1074,7 +1075,6 @@
30149D9222F21129003C12B5 /* QrViewController.swift */,
30B0ACF924AB5B99004D5E29 /* EphemeralMessagesViewController.swift */,
AE8F503424753DFE007FEE0B /* GalleryViewController.swift */,
30238CFA28A501C300EF14AC /* WebxdcSelector.swift */,
AED423D2249F578B00B6B2BB /* AddGroupMembersViewController.swift */,
AE39D322249CFC1A007346A1 /* FilesViewController.swift */,
AE57C083255310BB003CFE70 /* ContextMenuController.swift */,
Expand Down Expand Up @@ -1645,7 +1645,6 @@
3080A027277DE12D00E74565 /* InputBarButtonItem.swift in Sources */,
D85DF9782C4A96CB00A01408 /* UserDefaults+Extensions.swift in Sources */,
D8C19DD02C1C9FFE00B32F6D /* ContactCardPreview.swift in Sources */,
30238CFB28A501C300EF14AC /* WebxdcSelector.swift in Sources */,
AE57C084255310BB003CFE70 /* ContextMenuController.swift in Sources */,
304219D92440734A00516852 /* DcMsg+Extension.swift in Sources */,
AE52EA19229EB53C00C586C9 /* ContactDetailHeader.swift in Sources */,
Expand Down Expand Up @@ -1676,6 +1675,7 @@
30F4E2942859213400ACA0D8 /* ChatListEditingBar.swift in Sources */,
AE57C0802552BBD0003CFE70 /* GalleryItem.swift in Sources */,
AE25F09022807AD800CDEA66 /* AvatarSelectionCell.swift in Sources */,
D86DF20C2D30318B00141B3E /* AppPickerViewController.swift in Sources */,
30DDCBE928FCA1FA00465D22 /* PartialScreenPresentationController.swift in Sources */,
30A4149724F6EFBE00EC91EB /* InfoMessageCell.swift in Sources */,
302B84C6239676F0001C261F /* AvatarHelper.swift in Sources */,
Expand Down
151 changes: 151 additions & 0 deletions deltachat-ios/Chat/AppPickerViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import UIKit
import WebKit

protocol AppPickerViewControllerDelegate: AnyObject {
func pickedAnDownloadedApp(_ viewController: AppPickerViewController, fileURL: URL)
}

class AppPickerViewController: UIViewController {
weak var delegate: AppPickerViewControllerDelegate?
let webView: WKWebView
var defaultCloseButton: UIBarButtonItem?
let downloadingView: DownloadingView

init(url: URL = URL(string: "https://webxdc.org/apps/")!) {
webView = WKWebView(frame: .zero)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.load(URLRequest(url: url))

downloadingView = DownloadingView()
downloadingView.translatesAutoresizingMaskIntoConstraints = false
downloadingView.isHidden = true

super.init(nibName: nil, bundle: nil)

webView.navigationDelegate = self
view.addSubview(webView)
view.addSubview(downloadingView)
view.backgroundColor = .systemBackground
setupConstraints()
let closeButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel, target: self, action: #selector(AppPickerViewController.close(_:)))

title = String.localized("webxdc_apps")
navigationItem.leftBarButtonItem = closeButton
self.defaultCloseButton = closeButton
}

required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

private func setupConstraints() {
let constraints = [
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: webView.trailingAnchor),
view.bottomAnchor.constraint(equalTo: webView.bottomAnchor),

downloadingView.topAnchor.constraint(equalTo: view.topAnchor),
downloadingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: downloadingView.trailingAnchor),
view.bottomAnchor.constraint(equalTo: downloadingView.bottomAnchor),
]

NSLayoutConstraint.activate(constraints)
}

// MARK: - Actions

@objc func close(_ sender: Any) {
dismiss(animated: true)
}

@objc func showLoading() {
title = String.localized("Downloading...")
downloadingView.isHidden = false
downloadingView.activityIndicator.startAnimating()
downloadingView.activityIndicator.hidesWhenStopped = true
navigationItem.leftBarButtonItem = nil
}

@objc func hideLoading() {
title = String.localized("webxdc_apps")
downloadingView.isHidden = true
downloadingView.activityIndicator.stopAnimating()
navigationItem.leftBarButtonItem = defaultCloseButton
}
}

extension AppPickerViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
return decisionHandler(.cancel)
}

// if url ends with .xdc -> download and store in core and call delegate
if url.pathExtension == "xdc" {
Task { [weak self] in
guard let self else { return }
// show spinner instead of close-button
zeitschlag marked this conversation as resolved.
Show resolved Hide resolved
await MainActor.run {
self.showLoading()
}

guard let (data, _) = try? await URLSession.shared.data(from: url),
let filepath = FileHelper.saveData(data: data, name: url.lastPathComponent)
else {
await MainActor.run {
self.hideLoading()
}
return decisionHandler(.cancel)
}

let fileURL = NSURL(fileURLWithPath: filepath)
delegate?.pickedAnDownloadedApp(self, fileURL: fileURL as URL)
await MainActor.run {
self.dismiss(animated: true)
}

decisionHandler(.cancel)
}
} else if url.host == "webxdc.org" {
decisionHandler(.allow)
} else if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
decisionHandler(.cancel)
} else {
decisionHandler(.cancel)
}
}
}

class DownloadingView: UIView {
let activityIndicator: UIActivityIndicatorView
private let blurView: UIVisualEffectView

init() {
activityIndicator = UIActivityIndicatorView(style: .large)
activityIndicator.color = .label
activityIndicator.translatesAutoresizingMaskIntoConstraints = false

blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
blurView.translatesAutoresizingMaskIntoConstraints = false
blurView.layer.cornerRadius = 10
blurView.layer.masksToBounds = true

super.init(frame: .zero)

addSubview(blurView)
addSubview(activityIndicator)

NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),

activityIndicator.topAnchor.constraint(equalTo: blurView.topAnchor, constant: 20),
activityIndicator.leadingAnchor.constraint(equalTo: blurView.leadingAnchor, constant: 20),
blurView.trailingAnchor.constraint(equalTo: activityIndicator.trailingAnchor, constant: 20),
blurView.bottomAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 20),
])
}

required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
75 changes: 29 additions & 46 deletions deltachat-ios/Chat/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1216,9 +1216,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
let galleryImage = if #available(iOS 16, *) { "photo.stack" } else { "photo" }
actions.append(action(localized: "gallery", systemImage: galleryImage, handler: showPhotoVideoLibrary))
actions.append(action(localized: "files", systemImage: "folder", handler: showDocumentLibrary))
if dcContext.hasWebxdc() {
actions.append(action(localized: "webxdc_apps", systemImage: "square.grid.2x2", handler: showWebxdcSelector))
}
actions.append(action(localized: "webxdc_apps", systemImage: "square.grid.2x2", handler: showAppPicker))
actions.append(action(localized: "voice_message", systemImage: "mic", handler: showVoiceMessageRecorder))
if let config = dcContext.getConfig("webrtc_instance"), !config.isEmpty {
let videoChatImage = if #available(iOS 17, *) { "video.bubble" } else { "video" }
Expand Down Expand Up @@ -1252,6 +1250,8 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
let documentAction = action(localized: "files", handler: showDocumentLibrary)
let voiceMessageAction = action(localized: "voice_message", handler: showVoiceMessageRecorder)
let sendContactAction = action(localized: "contact", handler: showContactList)
let appPickerAction = action(localized: "webxdc_apps", handler: showAppPicker)

let isLocationStreaming = dcContext.isSendingLocationsToChat(chatId: chatId)
let locationStreamingAction = action(
localized: isLocationStreaming ? "stop_sharing_location" : "location",
Expand All @@ -1261,10 +1261,8 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {

alert.addAction(cameraAction)
alert.addAction(galleryAction)
alert.addAction(appPickerAction)
alert.addAction(documentAction)

let webxdcAction = action(localized: "webxdc_apps", handler: showWebxdcSelector)
alert.addAction(webxdcAction)
alert.addAction(voiceMessageAction)

if let config = dcContext.getConfig("webrtc_instance"), !config.isEmpty {
Expand Down Expand Up @@ -1434,20 +1432,6 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
}
}

private func showWebxdcSelector() {
let webxdcSelector = WebxdcSelector(context: dcContext)
webxdcSelector.delegate = self
let webxdcSelectorNavigationController = UINavigationController(rootViewController: webxdcSelector)
if #available(iOS 15.0, *) {
if let sheet = webxdcSelectorNavigationController.sheetPresentationController {
sheet.detents = [.medium()]
sheet.preferredCornerRadius = 20
}
}

self.present(webxdcSelectorNavigationController, animated: true)
}

private func showDocumentLibrary() {
mediaPicker?.showDocumentLibrary()
}
Expand Down Expand Up @@ -1528,6 +1512,20 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate {
navigationController?.present(alert, animated: true, completion: nil)
}

private func showAppPicker() {
let appPicker = AppPickerViewController()
appPicker.delegate = self
let navigationController = UINavigationController(rootViewController: appPicker)
navigationController.isModalInPresentation = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really dislike that you can't dismiss this interactively. It makes sense during download but not really any other time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean "swipe down to close"? idk, "files" and "gallery" have the same behavior, therefore i thought not too much about it. if it is an easy change, however, of couse, i would not be against having the same behavior as our contact or profile-switcher :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, that is actually an unintended side effect of the first responder fixes I did and did not notice :/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah! :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I attempted to fix that today in many different ways but I could not figure out how to do it without swizzling. I have a PR in draft that is working with swizzled methods. #2536


if #available(iOS 15.0, *), let sheet = navigationController.sheetPresentationController {
sheet.detents = [.large()]
sheet.preferredCornerRadius = 20
}

present(navigationController, animated: true)
}

private func showContactList() {
let contactList = SendContactViewController(dcContext: dcContext)
contactList.delegate = self
Expand Down Expand Up @@ -2652,32 +2650,6 @@ extension ChatViewController: ChatInputTextViewPasteDelegate {
}
}


extension ChatViewController: WebxdcSelectorDelegate {
func onWebxdcFromFilesSelected(url: NSURL) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.becomeFirstResponder()
self.onDocumentSelected(url: url)
}
}

func onWebxdcSelected(msgId: Int) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let message = self.dcContext.getMessage(id: msgId)
if let filename = message.fileURL {
let nsdata = NSData(contentsOf: filename)
guard let data = nsdata as? Data else { return }
let url = FileHelper.saveData(data: data, suffix: "xdc", directory: .cachesDirectory)
self.draft.setAttachment(viewType: DC_MSG_WEBXDC, path: url)
self.configureDraftArea(draft: self.draft)
self.focusInputTextView()
}
}
}
}

// MARK: - ChatDropInteractionDelegate
extension ChatViewController: ChatDropInteractionDelegate {
func onImageDragAndDropped(image: UIImage) {
Expand Down Expand Up @@ -2748,3 +2720,14 @@ extension ChatViewController: BackButtonUpdateable {
}
}
}

// MARK: - AppPickerViewControllerDelegate

extension ChatViewController: AppPickerViewControllerDelegate {
func pickedAnDownloadedApp(_ viewController: AppPickerViewController, fileURL url: URL) {
draft.setAttachment(viewType: DC_MSG_WEBXDC, path: url.relativePath)
configureDraftArea(draft: draft)
focusInputTextView()
FileHelper.deleteFile(atPath: url.relativePath)
}
}
Loading
Loading