Move account files to the documents directory and out of the shared container. Issue #1784

This commit is contained in:
Maurice Parker
2020-02-09 13:08:11 -08:00
parent 31b72221f8
commit 2ae021960b
23 changed files with 817 additions and 318 deletions

24
iOS/AccountMigrator.swift Normal file
View File

@@ -0,0 +1,24 @@
//
// AccountMigrator.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/9/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
struct AccountMigrator {
static func migrate() {
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
let containerAccountsURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
let containerAccountsFolder = containerAccountsURL!.appendingPathComponent("Accounts")
let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts")
try? FileManager.default.moveItem(at: containerAccountsFolder, to: documentAccountsFolder)
}
}

View File

@@ -41,6 +41,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
var imageDownloader: ImageDownloader!
var authorAvatarDownloader: AuthorAvatarDownloader!
var webFeedIconDownloader: WebFeedIconDownloader!
var extensionContainersFile: ExtensionContainersFile!
var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile!
var unreadCount = 0 {
didSet {
@@ -58,7 +60,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
super.init()
appDelegate = self
AccountManager.shared = AccountManager()
AccountMigrator.migrate()
let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts").absoluteString
let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
@@ -97,6 +104,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
UNUserNotificationCenter.current().delegate = self
userNotificationManager = UserNotificationManager()
extensionContainersFile = ExtensionContainersFile()
extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
syncTimer = ArticleStatusSyncTimer()
#if DEBUG
@@ -132,6 +142,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func prepareAccountsForBackground() {
extensionFeedAddRequestFile.suspend()
syncTimer?.invalidate()
scheduleBackgroundFeedRefresh()
syncArticleStatus()
@@ -139,6 +150,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func prepareAccountsForForeground() {
extensionFeedAddRequestFile.resume()
if let lastRefresh = AppDefaults.lastRefresh {
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)

View File

@@ -519,7 +519,14 @@ private extension WebViewController {
func reloadArticleImage() {
guard let article = article else { return }
webView?.evaluateJavaScript("reloadArticleImage(\"\(article.articleID)\")")
var components = URLComponents()
components.scheme = ArticleRenderer.imageIconScheme
components.path = article.articleID
if let imageSrc = components.string {
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
}
}
func imageWasClicked(body: String?) {

View File

@@ -0,0 +1,94 @@
//
// ExtensionContainers.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/10/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
protocol ExtensionContainer: ContainerIdentifiable, Codable {
var name: String { get }
var accountID: String { get }
}
struct ExtensionContainers: Codable {
enum CodingKeys: String, CodingKey {
case accounts
}
let accounts: [ExtensionAccount]
var flattened: [ExtensionContainer] {
return accounts.reduce([ExtensionContainer](), { (containers, account) in
var result = containers
result.append(account)
result.append(contentsOf: account.folders)
return result
})
}
func findAccount(forName name: String) -> ExtensionAccount? {
return accounts.first(where: { $0.name == name })
}
}
struct ExtensionAccount: ExtensionContainer {
enum CodingKeys: String, CodingKey {
case name
case accountID
case type
case disallowFeedInRootFolder
case containerID
case folders
}
let name: String
let accountID: String
let type: AccountType
let disallowFeedInRootFolder: Bool
let containerID: ContainerIdentifier?
let folders: [ExtensionFolder]
init(account: Account) {
self.name = account.nameForDisplay
self.accountID = account.accountID
self.type = account.type
self.disallowFeedInRootFolder = account.behaviors.contains(.disallowFeedInRootFolder)
self.containerID = account.containerID
self.folders = account.sortedFolders?.map { ExtensionFolder(folder: $0) } ?? [ExtensionFolder]()
}
func findFolder(forName name: String) -> ExtensionFolder? {
return folders.first(where: { $0.name == name })
}
}
struct ExtensionFolder: ExtensionContainer {
enum CodingKeys: String, CodingKey {
case accountName
case accountID
case name
case containerID
}
let accountName: String
let accountID: String
let name: String
let containerID: ContainerIdentifier?
init(folder: Folder) {
self.accountName = folder.account?.nameForDisplay ?? ""
self.accountID = folder.account?.accountID ?? ""
self.name = folder.nameForDisplay
self.containerID = folder.containerID
}
}

View File

@@ -0,0 +1,107 @@
//
// ExtensionContainersFile.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/10/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import os.log
import RSCore
import RSParser
import Account
final class ExtensionContainersFile {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile")
private static var filePath: String = {
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
return containerURL!.appendingPathComponent("extension_containers.plist").path
}()
private var isDirty = false {
didSet {
queueSaveToDiskIfNeeded()
}
}
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
init() {
if !FileManager.default.fileExists(atPath: ExtensionContainersFile.filePath) {
save()
}
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .UserDidAddAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .UserDidDeleteAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .AccountStateDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .ChildrenDidChange, object: nil)
}
/// Reads and decodes the shared plist file.
static func read() -> ExtensionContainers? {
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
let fileURL = URL(fileURLWithPath: ExtensionContainersFile.filePath)
var extensionContainers: ExtensionContainers? = nil
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
if let fileData = try? Data(contentsOf: readURL) {
let decoder = PropertyListDecoder()
extensionContainers = try? decoder.decode(ExtensionContainers.self, from: fileData)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
}
return extensionContainers
}
}
private extension ExtensionContainersFile {
@objc func markAsDirty() {
isDirty = true
}
func queueSaveToDiskIfNeeded() {
saveQueue.add(self, #selector(saveToDiskIfNeeded))
}
@objc func saveToDiskIfNeeded() {
if isDirty {
isDirty = false
save()
}
}
func save() {
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
let fileURL = URL(fileURLWithPath: ExtensionContainersFile.filePath)
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
do {
let extensionAccounts = AccountManager.shared.sortedActiveAccounts.map { ExtensionAccount(account: $0) }
let extensionContainers = ExtensionContainers(accounts: extensionAccounts)
let data = try encoder.encode(extensionContainers)
try data.write(to: writeURL)
} catch let error as NSError {
os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription)
}
}
}

View File

@@ -0,0 +1,24 @@
//
// ExtensionFeedAddRequest.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/10/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
struct ExtensionFeedAddRequest: Codable {
enum CodingKeys: String, CodingKey {
case name
case feedURL
case destinationContainerID
}
let name: String?
let feedURL: URL
let destinationContainerID: ContainerIdentifier
}

View File

@@ -0,0 +1,160 @@
//
// ExtensionFeedAddRequestFile.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/11/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import os.log
import Account
final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile")
private static var filePath: String = {
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
return containerURL!.appendingPathComponent("extension_feed_add_request.plist").path
}()
private let operationQueue: OperationQueue
var presentedItemURL: URL? {
return URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath)
}
var presentedItemOperationQueue: OperationQueue {
return operationQueue
}
override init() {
operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
super.init()
NSFileCoordinator.addFilePresenter(self)
process()
}
func presentedItemDidChange() {
DispatchQueue.main.async {
self.process()
}
}
func resume() {
NSFileCoordinator.addFilePresenter(self)
process()
}
func suspend() {
NSFileCoordinator.removeFilePresenter(self)
}
static func save(_ feedAddRequest: ExtensionFeedAddRequest) {
let decoder = PropertyListDecoder()
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator()
let fileURL = URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath)
fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forMerging], error: errorPointer, byAccessor: { url in
do {
var requests: [ExtensionFeedAddRequest]
if let fileData = try? Data(contentsOf: url),
let decodedRequests = try? decoder.decode([ExtensionFeedAddRequest].self, from: fileData) {
requests = decodedRequests
} else {
requests = [ExtensionFeedAddRequest]()
}
requests.append(feedAddRequest)
let data = try encoder.encode(requests)
try data.write(to: url)
} catch let error as NSError {
os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription)
}
}
}
private extension ExtensionFeedAddRequestFile {
func process() {
let decoder = PropertyListDecoder()
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator(filePresenter: self)
let fileURL = URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath)
var requests: [ExtensionFeedAddRequest]? = nil
fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forMerging], error: errorPointer, byAccessor: { url in
do {
if let fileData = try? Data(contentsOf: url),
let decodedRequests = try? decoder.decode([ExtensionFeedAddRequest].self, from: fileData) {
requests = decodedRequests
}
let data = try encoder.encode([ExtensionFeedAddRequest]())
try data.write(to: url)
} catch let error as NSError {
os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription)
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription)
}
requests?.forEach { processRequest($0) }
}
func processRequest(_ request: ExtensionFeedAddRequest) {
var destinationAccountID: String? = nil
switch request.destinationContainerID {
case .account(let accountID):
destinationAccountID = accountID
case .folder(let accountID, _):
destinationAccountID = accountID
default:
break
}
guard let accountID = destinationAccountID, let account = AccountManager.shared.existingAccount(with: accountID) else {
return
}
var destinationContainer: Container? = nil
if account.containerID == request.destinationContainerID {
destinationContainer = account
} else {
destinationContainer = account.folders?.first(where: { $0.containerID == request.destinationContainerID })
}
guard let container = destinationContainer else { return }
account.createWebFeed(url: request.feedURL.absoluteString, name: request.name, container: container) { _ in }
}
}

View File

@@ -7,15 +7,24 @@
//
import Intents
import Account
public enum AddWebFeedIntentHandlerError: LocalizedError {
case communicationFailure
public var errorDescription: String? {
switch self {
case .communicationFailure:
return NSLocalizedString("Unable to communicate with NetNewsWire.", comment: "Communication failure")
}
}
}
public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
override init() {
super.init()
DispatchQueue.main.sync {
AccountManager.shared = AccountManager()
}
}
public func resolveUrl(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedUrlResolutionResult) -> Void) {
@@ -27,10 +36,13 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
}
public func provideAccountNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) {
DispatchQueue.main.async {
let accountNames = AccountManager.shared.activeAccounts.compactMap { $0.nameForDisplay }
completion(accountNames, nil)
guard let extensionContainers = ExtensionContainersFile.read() else {
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
return
}
let accountNames = extensionContainers.accounts.map { $0.name }
completion(accountNames, nil)
}
public func resolveAccountName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedAccountNameResolutionResult) -> Void) {
@@ -38,25 +50,32 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
completion(AddWebFeedAccountNameResolutionResult.notRequired())
return
}
DispatchQueue.main.async {
if AccountManager.shared.findActiveAccount(forDisplayName: accountName) == nil {
completion(.unsupported(forReason: .invalid))
} else {
completion(.success(with: accountName))
}
guard let extensionContainers = ExtensionContainersFile.read() else {
completion(.unsupported(forReason: .communication))
return
}
if extensionContainers.findAccount(forName: accountName) == nil {
completion(.unsupported(forReason: .invalid))
} else {
completion(.success(with: accountName))
}
}
public func provideFolderNameOptions(for intent: AddWebFeedIntent, with completion: @escaping ([String]?, Error?) -> Void) {
DispatchQueue.main.async {
guard let accountName = intent.accountName, let account = AccountManager.shared.findActiveAccount(forDisplayName: accountName) else {
completion([String](), nil)
return
}
let folderNames = account.folders?.map { $0.nameForDisplay }
completion(folderNames, nil)
guard let extensionContainers = ExtensionContainersFile.read() else {
completion(nil, AddWebFeedIntentHandlerError.communicationFailure)
return
}
guard let accountName = intent.accountName, let account = extensionContainers.findAccount(forName: accountName) else {
completion([String](), nil)
return
}
let folderNames = account.folders.map { $0.name }
completion(folderNames, nil)
}
public func resolveFolderName(for intent: AddWebFeedIntent, with completion: @escaping (AddWebFeedFolderNameResolutionResult) -> Void) {
@@ -65,73 +84,60 @@ public class AddWebFeedIntentHandler: NSObject, AddWebFeedIntentHandling {
return
}
DispatchQueue.main.async {
guard let account = AccountManager.shared.findActiveAccount(forDisplayName: accountName) else {
completion(.unsupported(forReason: .invalid))
return
}
if account.findFolder(withDisplayName: folderName) == nil {
completion(.unsupported(forReason: .invalid))
} else {
completion(.success(with: folderName))
}
guard let extensionContainers = ExtensionContainersFile.read() else {
completion(.unsupported(forReason: .communication))
return
}
guard let account = extensionContainers.findAccount(forName: accountName) else {
completion(.unsupported(forReason: .invalid))
return
}
if account.findFolder(forName: folderName) == nil {
completion(.unsupported(forReason: .invalid))
} else {
completion(.success(with: folderName))
}
return
}
public func handle(intent: AddWebFeedIntent, completion: @escaping (AddWebFeedIntentResponse) -> Void) {
guard let url = intent.url else {
guard let url = intent.url, let extensionContainers = ExtensionContainersFile.read() else {
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
return
}
DispatchQueue.main.async {
let account: Account? = {
if let accountName = intent.accountName {
return AccountManager.shared.findActiveAccount(forDisplayName: accountName)
} else {
return AccountManager.shared.sortedActiveAccounts.first
}
}()
guard let validAccount = account else {
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
return
}
let container: Container? = {
if let folderName = intent.folderName {
return validAccount.findFolder(withDisplayName: folderName)
} else {
return validAccount
}
}()
guard let validContainer = container else {
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
return
let account: ExtensionAccount? = {
if let accountName = intent.accountName {
return extensionContainers.findAccount(forName: accountName)
} else {
return extensionContainers.accounts.first
}
}()
validAccount.createWebFeed(url: url.absoluteString, name: nil, container: validContainer) { result in
switch result {
case .success:
AccountManager.shared.suspendNetworkAll()
AccountManager.shared.suspendDatabaseAll()
completion(AddWebFeedIntentResponse(code: .success, userActivity: nil))
case .failure(let error):
switch error {
case AccountError.createErrorNotFound:
completion(AddWebFeedIntentResponse(code: .feedNotFound, userActivity: nil))
case AccountError.createErrorAlreadySubscribed:
completion(AddWebFeedIntentResponse(code: .alreadySubscribed, userActivity: nil))
default:
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
}
}
}
guard let validAccount = account else {
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
return
}
let container: ExtensionContainer? = {
if let folderName = intent.folderName {
return validAccount.findFolder(forName: folderName)
} else {
return validAccount
}
}()
guard let validContainer = container, let containerID = validContainer.containerID else {
completion(AddWebFeedIntentResponse(code: .failure, userActivity: nil))
return
}
let request = ExtensionFeedAddRequest(name: nil, feedURL: url, destinationContainerID: containerID)
ExtensionFeedAddRequestFile.save(request)
completion(AddWebFeedIntentResponse(code: .success, userActivity: nil))
}
}

View File

@@ -9,7 +9,7 @@
<key>INIntentDefinitionNamespace</key>
<string>U6u7RF</string>
<key>INIntentDefinitionSystemVersion</key>
<string>19B88</string>
<string>19D76</string>
<key>INIntentDefinitionToolsBuildVersion</key>
<string>11B53</string>
<key>INIntentDefinitionToolsVersion</key>
@@ -177,6 +177,16 @@
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
<string>JGkCuS</string>
</dict>
<dict>
<key>INIntentParameterUnsupportedReasonCode</key>
<string>communication</string>
<key>INIntentParameterUnsupportedReasonCustom</key>
<true/>
<key>INIntentParameterUnsupportedReasonFormatString</key>
<string>Unable to communicate with NetNewsWire.</string>
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
<string>uSfloN</string>
</dict>
</array>
</dict>
<dict>
@@ -259,6 +269,16 @@
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
<string>ef5kBt</string>
</dict>
<dict>
<key>INIntentParameterUnsupportedReasonCode</key>
<string>communication</string>
<key>INIntentParameterUnsupportedReasonCustom</key>
<true/>
<key>INIntentParameterUnsupportedReasonFormatString</key>
<string>Unable to communicate with NetNewsWire.</string>
<key>INIntentParameterUnsupportedReasonFormatStringID</key>
<string>ExjqcE</string>
</dict>
</array>
</dict>
</array>
@@ -276,30 +296,6 @@
<key>INIntentResponseCodeName</key>
<string>failure</string>
</dict>
<dict>
<key>INIntentResponseCodeConciseFormatString</key>
<string>You are already subscribed to this feed in this account.</string>
<key>INIntentResponseCodeConciseFormatStringID</key>
<string>srME8b</string>
<key>INIntentResponseCodeFormatString</key>
<string>You are already subscribed to this feed in this account.</string>
<key>INIntentResponseCodeFormatStringID</key>
<string>UGGPkp</string>
<key>INIntentResponseCodeName</key>
<string>alreadySubscribed</string>
</dict>
<dict>
<key>INIntentResponseCodeConciseFormatString</key>
<string>No feed was found at the specified URL.</string>
<key>INIntentResponseCodeConciseFormatStringID</key>
<string>8Dh9Yy</string>
<key>INIntentResponseCodeFormatString</key>
<string>No feed was found at the specified URL.</string>
<key>INIntentResponseCodeFormatStringID</key>
<string>drQfaI</string>
<key>INIntentResponseCodeName</key>
<string>feedNotFound</string>
</dict>
</array>
</dict>
<key>INIntentTitle</key>

View File

@@ -58,7 +58,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(webFeedMetadataDidChange(_:)), name: .WebFeedMetadataDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)

View File

@@ -60,10 +60,10 @@
<string>Grant permission to save images from the article.</string>
<key>NSUserActivityTypes</key>
<array>
<string>Restoration</string>
<string>AddWebFeedIntent</string>
<string>NextUnread</string>
<string>ReadArticle</string>
<string>Restoration</string>
<string>SelectFeed</string>
</array>
<key>UIApplicationSceneManifest</key>

View File

@@ -0,0 +1,49 @@
//
// ShareDefaultContainer.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 2/11/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
struct ShareDefaultContainer {
static func defaultContainer(containers: ExtensionContainers) -> ExtensionContainer? {
if let accountID = AppDefaults.addWebFeedAccountID, let account = containers.accounts.first(where: { $0.accountID == accountID }) {
if let folderName = AppDefaults.addWebFeedFolderName, let folder = account.folders.first(where: { $0.name == folderName }) {
return folder
} else {
return substituteContainerIfNeeded(account: account)
}
} else if let account = containers.accounts.first {
return substituteContainerIfNeeded(account: account)
} else {
return nil
}
}
static func saveDefaultContainer(_ container: ExtensionContainer) {
AppDefaults.addWebFeedAccountID = container.accountID
if let folder = container as? ExtensionFolder {
AppDefaults.addWebFeedFolderName = folder.name
} else {
AppDefaults.addWebFeedFolderName = nil
}
}
private static func substituteContainerIfNeeded(account: ExtensionAccount) -> ExtensionContainer? {
if !account.disallowFeedInRootFolder {
return account
} else {
if let folder = account.folders.first {
return folder
} else {
return nil
}
}
}
}

View File

@@ -7,30 +7,24 @@
//
import UIKit
import RSCore
import Account
import RSCore
protocol ShareFolderPickerControllerDelegate: class {
func shareFolderPickerDidSelect(_ container: Container)
func shareFolderPickerDidSelect(_ container: ExtensionContainer)
}
class ShareFolderPickerController: UITableViewController {
var selectedContainer: Container?
var containers = [Container]()
var containers: [ExtensionContainer]?
var selectedContainerID: ContainerIdentifier?
weak var delegate: ShareFolderPickerControllerDelegate?
override func viewDidLoad() {
for account in AccountManager.shared.sortedActiveAccounts {
containers.append(account)
if let sortedFolders = account.sortedFolders {
containers.append(contentsOf: sortedFolders)
}
}
tableView.register(UINib(nibName: "ShareFolderPickerAccountCell", bundle: Bundle.main), forCellReuseIdentifier: "AccountCell")
tableView.register(UINib(nibName: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
@@ -38,30 +32,28 @@ class ShareFolderPickerController: UITableViewController {
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return containers.count
return containers?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let container = containers[indexPath.row]
let container = containers?[indexPath.row]
let cell: ShareFolderPickerCell = {
if container is Account {
if container is ExtensionAccount {
return tableView.dequeueReusableCell(withIdentifier: "AccountCell", for: indexPath) as! ShareFolderPickerCell
} else {
return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! ShareFolderPickerCell
}
}()
if let account = container as? Account {
if let account = container as? ExtensionAccount {
cell.icon.image = AppAssets.image(for: account.type)
} else {
cell.icon.image = AppAssets.masterFolderImage.image
}
if let displayNameProvider = container as? DisplayNameProvider {
cell.label?.text = displayNameProvider.nameForDisplay
}
if let compContainer = selectedContainer, container === compContainer {
cell.label?.text = container?.name ?? ""
if let containerID = container?.containerID, containerID == selectedContainerID {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
@@ -71,9 +63,9 @@ class ShareFolderPickerController: UITableViewController {
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let container = containers[indexPath.row]
guard let container = containers?[indexPath.row] else { return }
if let account = container as? Account, account.behaviors.contains(.disallowFeedInRootFolder) {
if let account = container as? ExtensionAccount, account.disallowFeedInRootFolder {
tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
} else {
let cell = tableView.cellForRow(at: indexPath)

View File

@@ -8,23 +8,26 @@
import UIKit
import MobileCoreServices
import Social
import Account
import Articles
import Social
import RSCore
import RSTree
class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate {
private var url: URL?
private var container: Container?
private var extensionContainers: ExtensionContainers?
private var flattenedContainers: [ExtensionContainer]!
private var selectedContainer: ExtensionContainer?
private var folderItem: SLComposeSheetConfigurationItem!
override func viewDidLoad() {
AccountManager.shared = AccountManager()
container = AddWebFeedDefaultContainer.defaultContainer
extensionContainers = ExtensionContainersFile.read()
flattenedContainers = extensionContainers?.flattened ?? [ExtensionContainer]()
if let extensionContainers = extensionContainers {
selectedContainer = ShareDefaultContainer.defaultContainer(containers: extensionContainers)
}
title = "NetNewsWire"
placeholder = "Feed Name (Optional)"
@@ -32,14 +35,14 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
button.title = "Add Feed"
button.isEnabled = true
}
// Hack the bottom table rows to be smaller since the controller itself doesn't have enough sense to size itself correctly
if let nav = self.children.first as? UINavigationController, let tableView = nav.children.first?.view.subviews.first as? UITableView {
tableView.rowHeight = 38
}
var provider: NSItemProvider? = nil
// Try to get any HTML that is maybe passed in
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
for itemProvider in item.attachments! {
@@ -48,7 +51,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
}
}
}
if provider != nil {
provider!.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil, completionHandler: { [weak self] (pList, error) in
if error != nil {
@@ -66,7 +69,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
})
return
}
// Try to get the URL if it is passed in
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
for itemProvider in item.attachments! {
@@ -75,7 +78,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
}
}
}
if provider != nil {
provider!.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { [weak self] (urlCoded, error) in
if error != nil {
@@ -91,53 +94,25 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
}
override func isContentValid() -> Bool {
return url != nil && container != nil
return url != nil && selectedContainer != nil
}
override func didSelectPost() {
guard let url = url, let container = container else {
guard let url = url, let selectedContainer = selectedContainer, let containerID = selectedContainer.containerID else {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
return
}
var account: Account?
if let containerAccount = container as? Account {
account = containerAccount
} else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
account = containerAccount
} else {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
return
}
if account!.hasWebFeed(withURL: url.absoluteString) {
let errorTitle = NSLocalizedString("Error", comment: "Error")
presentError(title: errorTitle, message: AccountError.createErrorAlreadySubscribed.localizedDescription)
self.extensionContext!.cancelRequest(withError: AccountError.createErrorAlreadySubscribed)
return
}
let feedName = contentText.isEmpty ? nil : contentText
ProcessInfo.processInfo.performExpiringActivity(withReason: "Adding web feed to account.") { expired in
guard !expired else { return }
DispatchQueue.main.async {
account!.createWebFeed(url: url.absoluteString, name: feedName, container: container) { result in
account!.save()
AccountManager.shared.suspendNetworkAll()
AccountManager.shared.suspendDatabaseAll()
}
}
}
let name = contentText.isEmpty ? nil : contentText
let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID)
ExtensionFeedAddRequestFile.save(request)
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func shareFolderPickerDidSelect(_ container: Container) {
AddWebFeedDefaultContainer.saveDefaultContainer(container)
self.container = container
func shareFolderPickerDidSelect(_ container: ExtensionContainer) {
ShareDefaultContainer.saveDefaultContainer(container)
self.selectedContainer = container
updateFolderItemValue()
self.popConfigurationViewController()
}
@@ -159,7 +134,8 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder")
folderPickerController.delegate = self
folderPickerController.selectedContainer = self.container
folderPickerController.containers = self.flattenedContainers
folderPickerController.selectedContainerID = self.selectedContainer?.containerID
self.pushConfigurationViewController(folderPickerController)
@@ -174,12 +150,10 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
private extension ShareViewController {
func updateFolderItemValue() {
if let containerName = (container as? DisplayNameProvider)?.nameForDisplay {
if container is Folder {
self.folderItem.value = "\(container?.account?.nameForDisplay ?? "") / \(containerName)"
} else {
self.folderItem.value = containerName
}
if let account = selectedContainer as? ExtensionAccount {
self.folderItem.value = account.name
} else if let folder = selectedContainer as? ExtensionFolder {
self.folderItem.value = "\(folder.accountName) / \(folder.name)"
}
}