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

View File

@ -31,10 +31,9 @@ public extension Notification.Name {
static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
static let AccountStateDidChange = Notification.Name(rawValue: "AccountStateDidChange")
static let StatusesDidChange = Notification.Name(rawValue: "StatusesDidChange")
static let WebFeedMetadataDidChange = Notification.Name(rawValue: "WebFeedMetadataDidChange")
}
public enum AccountType: Int {
public enum AccountType: Int, Codable {
// Raw values should not change since theyre stored on disk.
case onMyMac = 1
case feedly = 16
@ -199,8 +198,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
typealias WebFeedMetadataDictionary = [String: WebFeedMetadata]
var webFeedMetadata = WebFeedMetadataDictionary()
var startingUp = true
public var unreadCount = 0 {
didSet {
if unreadCount != oldValue {
@ -287,7 +284,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
self.delegate.accountDidInitialize(self)
startingUp = false
}
// MARK: - API
@ -416,9 +412,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func suspendDatabase() {
database.cancelAndSuspend()
save()
metadataFile.suspend()
webFeedMetadataFile.suspend()
opmlFile.suspend()
}
/// Re-open the SQLite database and allow database calls.
@ -430,12 +423,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
/// Reload OPML, etc.
public func resume() {
metadataFile.resume()
webFeedMetadataFile.resume()
opmlFile.resume()
metadataFile.load()
webFeedMetadataFile.load()
opmlFile.load()
fetchAllUnreadCounts()
}
@ -488,14 +475,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
public func resetWebFeedMetadataAndUnreadCounts() {
for feed in flattenedWebFeeds() {
feed.metadata = webFeedMetadata(feedURL: feed.url, webFeedID: feed.webFeedID)
}
fetchAllUnreadCounts()
NotificationCenter.default.post(name: .WebFeedMetadataDidChange, object: self, userInfo: nil)
}
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
return delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag)
}
@ -691,9 +670,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func structureDidChange() {
// Feeds were added or deleted. Or folders added or deleted.
// Or feeds inside folders were added or deleted.
if !startingUp {
opmlFile.markAsDirty()
}
opmlFile.markAsDirty()
flattenedWebFeedsNeedUpdate = true
webFeedDictionaryNeedsUpdate = true
}

View File

@ -90,14 +90,6 @@ public final class AccountManager: UnreadCountProvider {
return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray)
}
public convenience init() {
let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String
let accountsURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
let accountsFolder = accountsURL!.appendingPathComponent("Accounts").absoluteString
let accountsFolderPath = accountsFolder.suffix(from: accountsFolder.index(accountsFolder.startIndex, offsetBy: 7))
self.init(accountsFolder: String(accountsFolderPath))
}
public init(accountsFolder: String) {
self.accountsFolder = accountsFolder

View File

@ -16,41 +16,26 @@ final class AccountMetadataFile {
private let fileURL: URL
private let account: Account
private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback)
private var isDirty = false {
didSet {
queueSaveToDiskIfNeeded()
}
}
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
init(filename: String, account: Account) {
self.fileURL = URL(fileURLWithPath: filename)
self.account = account
}
func markAsDirty() {
managedFile.markAsDirty()
isDirty = true
}
func load() {
managedFile.load()
}
func save() {
managedFile.saveIfNecessary()
}
func suspend() {
managedFile.suspend()
}
func resume() {
managedFile.resume()
}
}
private extension AccountMetadataFile {
func loadCallback() {
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
if let fileData = try? Data(contentsOf: readURL) {
@ -63,17 +48,16 @@ private extension AccountMetadataFile {
if let error = errorPointer?.pointee {
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
}
}
func saveCallback() {
func save() {
guard !account.isDeleted else { return }
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
do {
@ -90,3 +74,18 @@ private extension AccountMetadataFile {
}
}
private extension AccountMetadataFile {
func queueSaveToDiskIfNeeded() {
saveQueue.add(self, #selector(saveToDiskIfNeeded))
}
@objc func saveToDiskIfNeeded() {
if isDirty {
isDirty = false
save()
}
}
}

View File

@ -12,7 +12,7 @@ public protocol ContainerIdentifiable {
var containerID: ContainerIdentifier? { get }
}
public enum ContainerIdentifier: Hashable {
public enum ContainerIdentifier: Hashable, Equatable {
case smartFeedController
case account(String) // accountID
case folder(String, String) // accountID, folderName
@ -55,3 +55,47 @@ public enum ContainerIdentifier: Hashable {
}
}
extension ContainerIdentifier: Encodable {
enum CodingKeys: CodingKey {
case type
case accountID
case folderName
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .smartFeedController:
try container.encode("smartFeedController", forKey: .type)
case .account(let accountID):
try container.encode("account", forKey: .type)
try container.encode(accountID, forKey: .accountID)
case .folder(let accountID, let folderName):
try container.encode("folder", forKey: .type)
try container.encode(accountID, forKey: .accountID)
try container.encode(folderName, forKey: .folderName)
}
}
}
extension ContainerIdentifier: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "smartFeedController":
self = .smartFeedController
case "account":
let accountID = try container.decode(String.self, forKey: .accountID)
self = .account(accountID)
default:
let accountID = try container.decode(String.self, forKey: .accountID)
let folderName = try container.decode(String.self, forKey: .folderName)
self = .folder(accountID, folderName)
}
}
}

View File

@ -17,62 +17,40 @@ final class OPMLFile {
private let fileURL: URL
private let account: Account
private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback)
private var isDirty = false {
didSet {
queueSaveToDiskIfNeeded()
}
}
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
init(filename: String, account: Account) {
self.fileURL = URL(fileURLWithPath: filename)
self.account = account
}
func markAsDirty() {
managedFile.markAsDirty()
isDirty = true
}
func load() {
managedFile.load()
}
func save() {
managedFile.saveIfNecessary()
}
func suspend() {
managedFile.suspend()
}
func resume() {
managedFile.resume()
}
}
private extension OPMLFile {
func loadCallback() {
guard let fileData = opmlFileData() else {
guard let fileData = opmlFileData(), let opmlItems = parsedOPMLItems(fileData: fileData) else {
return
}
// Don't rebuild the account if the OPML hasn't changed since the last save
guard let opml = String(data: fileData, encoding: .utf8), opml != opmlDocument() else {
return
}
guard let opmlItems = parsedOPMLItems(fileData: fileData) else { return }
BatchUpdate.shared.perform {
account.topLevelWebFeeds.removeAll()
account.loadOPMLItems(opmlItems, parentFolder: nil)
}
}
func saveCallback() {
func save() {
guard !account.isDeleted else { return }
let opmlDocumentString = opmlDocument()
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
do {
@ -85,12 +63,28 @@ private extension OPMLFile {
if let error = errorPointer?.pointee {
os_log(.error, log: log, "OPML save to disk coordination failed: %@.", error.localizedDescription)
}
}
}
private extension OPMLFile {
func queueSaveToDiskIfNeeded() {
saveQueue.add(self, #selector(saveToDiskIfNeeded))
}
@objc func saveToDiskIfNeeded() {
if isDirty {
isDirty = false
save()
}
}
func opmlFileData() -> Data? {
var fileData: Data? = nil
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
do {

View File

@ -16,41 +16,26 @@ final class WebFeedMetadataFile {
private let fileURL: URL
private let account: Account
private lazy var managedFile = ManagedResourceFile(fileURL: fileURL, load: loadCallback, save: saveCallback)
private var isDirty = false {
didSet {
queueSaveToDiskIfNeeded()
}
}
private let saveQueue = CoalescingQueue(name: "Save Queue", interval: 0.5)
init(filename: String, account: Account) {
self.fileURL = URL(fileURLWithPath: filename)
self.account = account
}
func markAsDirty() {
managedFile.markAsDirty()
isDirty = true
}
func load() {
managedFile.load()
}
func save() {
managedFile.saveIfNecessary()
}
func suspend() {
managedFile.suspend()
}
func resume() {
managedFile.resume()
}
}
private extension WebFeedMetadataFile {
func loadCallback() {
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in
if let fileData = try? Data(contentsOf: readURL) {
@ -58,19 +43,14 @@ private extension WebFeedMetadataFile {
account.webFeedMetadata = (try? decoder.decode(Account.WebFeedMetadataDictionary.self, from: fileData)) ?? Account.WebFeedMetadataDictionary()
}
account.webFeedMetadata.values.forEach { $0.delegate = account }
if !account.startingUp {
account.resetWebFeedMetadataAndUnreadCounts()
}
})
if let error = errorPointer?.pointee {
os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription)
}
}
func saveCallback() {
func save() {
guard !account.isDeleted else { return }
let feedMetadata = metadataForOnlySubscribedToFeeds()
@ -79,7 +59,7 @@ private extension WebFeedMetadataFile {
encoder.outputFormat = .binary
let errorPointer: NSErrorPointer = nil
let fileCoordinator = NSFileCoordinator(filePresenter: managedFile)
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in
do {
@ -94,7 +74,22 @@ private extension WebFeedMetadataFile {
os_log(.error, log: log, "Save to disk coordination failed: %@.", error.localizedDescription)
}
}
}
private extension WebFeedMetadataFile {
func queueSaveToDiskIfNeeded() {
saveQueue.add(self, #selector(saveToDiskIfNeeded))
}
@objc func saveToDiskIfNeeded() {
if isDirty {
isDirty = false
save()
}
}
private func metadataForOnlySubscribedToFeeds() -> Account.WebFeedMetadataDictionary {
let webFeedIDs = account.idToWebFeedDictionary.keys
return account.webFeedMetadata.filter { (feedID: String, metadata: WebFeedMetadata) -> Bool in

View File

@ -207,7 +207,14 @@ private extension DetailWebViewController {
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 reloadHTML() {

View File

@ -137,7 +137,6 @@
51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; };
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; };
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
51A9A5E02380C3F10033AADF /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; };
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; };
51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; };
51A9A5E62380C8B20033AADF /* ShareFolderPickerFolderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */; };
@ -150,6 +149,19 @@
51A9A5F52380F6A60033AADF /* ModalNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */; };
51A9A60A2382FD240033AADF /* PoppableGestureRecognizerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */; };
51AB8AB323B7F4C6008F147D /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AB8AB223B7F4C6008F147D /* WebViewController.swift */; };
51B5C87723F22B8200032075 /* ExtensionContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87623F22B8200032075 /* ExtensionContainers.swift */; };
51B5C87B23F2317700032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; };
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */; };
51B5C8B923F368D000032075 /* ExtensionContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87623F22B8200032075 /* ExtensionContainers.swift */; };
51B5C8BA23F368D000032075 /* ExtensionContainersFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */; };
51B5C8BB23F368D000032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; };
51B5C8BE23F37B2400032075 /* ShareDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */; };
51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; };
51B5C8C123F3A0DB00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; };
51B5C8E423F4BBFA00032075 /* ExtensionContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87623F22B8200032075 /* ExtensionContainers.swift */; };
51B5C8E523F4BBFA00032075 /* ExtensionContainersFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */; };
51B5C8E623F4BBFA00032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; };
51B5C8E723F4BBFA00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; };
51B62E68233186730085F949 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* IconView.swift */; };
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
@ -225,6 +237,7 @@
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; };
51C9DE8623F09FAA003D5A6D /* AccountMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */; };
51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; };
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; };
@ -1331,6 +1344,11 @@
51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalNavigationController.swift; sourceTree = "<group>"; };
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoppableGestureRecognizerDelegate.swift; sourceTree = "<group>"; };
51AB8AB223B7F4C6008F147D /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
51B5C87623F22B8200032075 /* ExtensionContainers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionContainers.swift; sourceTree = "<group>"; };
51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionFeedAddRequest.swift; sourceTree = "<group>"; };
51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionContainersFile.swift; sourceTree = "<group>"; };
51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDefaultContainer.swift; sourceTree = "<group>"; };
51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionFeedAddRequestFile.swift; sourceTree = "<group>"; };
51B62E67233186730085F949 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = "<group>"; };
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
@ -1356,6 +1374,7 @@
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = "<group>"; };
51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMigrator.swift; sourceTree = "<group>"; };
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = "<group>"; };
51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = "<group>"; };
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
@ -1793,6 +1812,7 @@
isa = PBXGroup;
children = (
513C5CE8232571C2003D4054 /* ShareViewController.swift */,
51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */,
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */,
51A9A5E72380CA130033AADF /* ShareFolderPickerCell.swift */,
51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */,
@ -1881,6 +1901,17 @@
path = Activity;
sourceTree = "<group>";
};
51B5C85A23F22A7A00032075 /* CommonExtension */ = {
isa = PBXGroup;
children = (
51B5C87623F22B8200032075 /* ExtensionContainers.swift */,
51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */,
51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */,
51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */,
);
path = CommonExtension;
sourceTree = "<group>";
};
51C45245226506C800C03939 /* UIKit Extensions */ = {
isa = PBXGroup;
children = (
@ -2612,6 +2643,7 @@
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */,
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */,
51B62E67233186730085F949 /* IconView.swift */,
51C9DE8523F09FAA003D5A6D /* AccountMigrator.swift */,
51C4525D226508F600C03939 /* MasterFeed */,
51C4526D2265091600C03939 /* MasterTimeline */,
51C4527D2265092C00C03939 /* Article */,
@ -2621,6 +2653,7 @@
513145F9235A55A700387FDC /* Intents */,
5183CCEB227117C70010922C /* Settings */,
51C45245226506C800C03939 /* UIKit Extensions */,
51B5C85A23F22A7A00032075 /* CommonExtension */,
513C5CE7232571C2003D4054 /* ShareExtension */,
51314643235A7C2300387FDC /* IntentsExtension */,
84C9FC9A2262A1A900D921D6 /* Resources */,
@ -3644,8 +3677,12 @@
buildActionMask = 2147483647;
files = (
513146B3235A81A400387FDC /* AddWebFeedIntentHandler.swift in Sources */,
51B5C8E623F4BBFA00032075 /* ExtensionFeedAddRequest.swift in Sources */,
51314705235C41FC00387FDC /* Intents.intentdefinition in Sources */,
51B5C8E523F4BBFA00032075 /* ExtensionContainersFile.swift in Sources */,
51314668235A7E4600387FDC /* IntentHandler.swift in Sources */,
51B5C8E423F4BBFA00032075 /* ExtensionContainers.swift in Sources */,
51B5C8E723F4BBFA00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -3654,13 +3691,17 @@
buildActionMask = 2147483647;
files = (
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */,
51B5C8B923F368D000032075 /* ExtensionContainers.swift in Sources */,
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */,
513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */,
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */,
51A9A5E02380C3F10033AADF /* AddWebFeedDefaultContainer.swift in Sources */,
51B5C8BE23F37B2400032075 /* ShareDefaultContainer.swift in Sources */,
51B5C8BA23F368D000032075 /* ExtensionContainersFile.swift in Sources */,
51B5C8BB23F368D000032075 /* ExtensionFeedAddRequest.swift in Sources */,
51A9A5E82380CA130033AADF /* ShareFolderPickerCell.swift in Sources */,
51A9A5EF2380D63B0033AADF /* IconImage.swift in Sources */,
51A9A5ED2380D6000033AADF /* AppAssets.swift in Sources */,
51B5C8C123F3A0DB00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -3899,9 +3940,11 @@
51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */,
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */,
51B5C87723F22B8200032075 /* ExtensionContainers.swift in Sources */,
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */,
51B5C87B23F2317700032075 /* ExtensionFeedAddRequest.swift in Sources */,
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */,
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */,
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
@ -3925,6 +3968,7 @@
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */,
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */,
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
C5A6ED6D23C9B0C800AB6BE2 /* UIActivityViewController-Extensions.swift in Sources */,
@ -3949,8 +3993,10 @@
51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */,
5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */,
51C452882265093600C03939 /* AddWebFeedViewController.swift in Sources */,
51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */,
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */,
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
51C9DE8623F09FAA003D5A6D /* AccountMigrator.swift in Sources */,
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */,

View File

@ -79,9 +79,9 @@ function flattenPreElements() {
ElementUnwrapper.unwrapAppropriateChildren("div.articleBody td > pre");
}
function reloadArticleImage(articleID) {
function reloadArticleImage(imageSrc) {
var image = document.getElementById("nnwImageIcon");
image.src = "nnwImageIcon://" + articleID;
image.src = imageSrc;
}
function error() {

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)"
}
}