// // ShareViewController.swift // Share Extension // // Created by David Sinclair on 2021-07-18. // Copyright © 2021 NewsBlur. All rights reserved. // import UIKit import MobileCoreServices class ShareViewController: UIViewController { @IBOutlet var delegate: ShareViewDelegate! @IBOutlet weak var modeSegmentedControl: UISegmentedControl! @IBOutlet weak var tableView: UITableView! /// The group preferences, shared with the main app. lazy var prefs: UserDefaults = { return UserDefaults(suiteName: "group.com.newsblur.NewsBlur-Group") ?? UserDefaults.standard }() /// Whether we are saving the story privately or sharing publicly. enum Mode { /// Save privately. case save /// Share publicly. case share } /// Whether we are saving the story privately or sharing publicly. var mode: Mode = .save /// Dictionary representation of a tag. typealias TagDict = [String : Any] /// Dictionary of tag dictionaries. typealias TagsDict = [String : TagDict] /// Tag structure. struct Tag: Identifiable, Hashable { /// Identifier of the tag. let id: String /// Name of the tag. let name: String /// Count of stories with this tag. let count: Int } /// An array of tags, from the main app. var tags = [Tag]() /// New tag to add, if any. var newTag = "" /// User-entered comments, only used when sharing. var comments = "" /// Title of the item being shared. var itemTitle: String? = nil /// The index path of the new tag field. lazy var indexPathForNewTag: IndexPath = { return IndexPath(item: tags.count, section: 0) }() override func viewDidLoad() { super.viewDidLoad() tableView.isEditing = mode == .save if let dicts = prefs.object(forKey: "share:tags") as? TagsDict { tags = dicts.map { (key: String, value: TagDict) in return Tag(id: key, name: value["feed_title"] as? String ?? "tag", count: value["ps"] as? Int ?? 0) } tags.sort { tag1, tag2 in return tag1.name.lowercased() < tag2.name.lowercased() } } updateSaveButtonState() NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow(notification:)), name: UIResponder.keyboardDidShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) } func updateSaveButtonState() { if mode == .save { if let rows = tableView.indexPathsForSelectedRows { navigationItem.rightBarButtonItem?.isEnabled = !rows.isEmpty } else { navigationItem.rightBarButtonItem?.isEnabled = false } } else { navigationItem.rightBarButtonItem?.isEnabled = true } } @objc private func keyboardDidShow(notification: NSNotification) { if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height + tableView.rowHeight, right: 0) } } @objc private func keyboardWillHide(notification: NSNotification) { tableView.contentInset = .zero } @IBAction func newTagFieldChanged(_ sender: UITextField) { newTag = sender.text ?? "" if newTag.isEmpty { tableView.deselectRow(at: indexPathForNewTag, animated: false) } else { tableView.selectRow(at: indexPathForNewTag, animated: false, scrollPosition: .none) } updateSaveButtonState() } @IBAction func newTagFieldReturn(_ sender: UITextField) { sender.resignFirstResponder() } @IBAction func cancel(_ sender: Any) { extensionContext?.cancelRequest(withError: NSError(domain: Bundle.main.bundleIdentifier!, code: 0)) } @IBAction func save(_ sender: Any) { itemTitle = nil if let itemProvider = providerWithURL { itemProvider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { item, error in if let url = item as? URL { self.send(url: url) } self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } } else if let itemProvider = providerWithText { itemProvider.loadItem(forTypeIdentifier: kUTTypeText as String, options: nil) { item, error in if let text = item as? String { self.send(text: text) } self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } } } @IBAction func changedMode(_ sender: Any) { mode = modeSegmentedControl.selectedSegmentIndex == 0 ? .save : .share navigationItem.rightBarButtonItem?.title = mode == .save ? "Save" : "Share" tableView.isEditing = mode == .save tableView.reloadData() updateSaveButtonState() } } private extension ShareViewController { var providerWithURL: NSItemProvider? { guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else { return nil } for extensionItem in extensionItems { if let itemProviders = extensionItem.attachments { for itemProvider in itemProviders { if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) { itemTitle = extensionItem.attributedTitle?.string if itemTitle == nil { itemTitle = extensionItem.attributedContentText?.string } return itemProvider } } } } return nil } var providerWithText: NSItemProvider? { guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else { return nil } for extensionItem in extensionItems { if let itemProviders = extensionItem.attachments { for itemProvider in itemProviders { if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) { return itemProvider } } } } return nil } func send(url: URL? = nil, text: String? = nil) { guard let host = prefs.object(forKey: "share:host") as? String, let token = prefs.object(forKey: "share:token") as? String, let requestURL = URL(string: "\(host)/\(requestPath)/\(token)") else { return } let postBody = postBody(url: url, text: text) var request = URLRequest(url: requestURL) request.httpMethod = "POST" request.httpBody = postBody.data(using: .utf8) let config = URLSessionConfiguration.background(withIdentifier: "group.com.newsblur.share") config.sharedContainerIdentifier = "group.com.newsblur.NewsBlur-Group" let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) let task = session.dataTask(with: request as URLRequest) task.resume() } var requestPath: String { return mode == .save ? "api/save_story" : "api/share_story" } func encoded(_ string: String?) -> String { return string?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? "" } func postBody(url: URL?, text: String?) -> String { let title = itemTitle let encodedURL = encoded(url?.absoluteString) let encodedTitle = encoded(title) let encodedContent = encoded(text) if mode == .save { let indexPaths = tableView.indexPathsForSelectedRows ?? [] var selectedTagsArray = [String]() for index in 0..