diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 7a5ce7774..7eea1c7c3 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 512DD4CD2431098700C17B1F /* CloudKitAccountZoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */; }; 5132AAC42448BAD90077840A /* FeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132AAC12448BAD90077840A /* FeedProvider.swift */; }; 5132AAC52448BAD90077840A /* TwitterFeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132AAC32448BAD90077840A /* TwitterFeedProvider.swift */; }; + 5132DE812449159100806ADE /* TwitterUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5132DE802449159100806ADE /* TwitterUser.swift */; }; 513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; }; 5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; }; 5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; }; @@ -276,6 +277,7 @@ 512DD4CC2431098700C17B1F /* CloudKitAccountZoneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAccountZoneDelegate.swift; sourceTree = ""; }; 5132AAC12448BAD90077840A /* FeedProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedProvider.swift; sourceTree = ""; }; 5132AAC32448BAD90077840A /* TwitterFeedProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwitterFeedProvider.swift; sourceTree = ""; }; + 5132DE802449159100806ADE /* TwitterUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterUser.swift; sourceTree = ""; }; 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = ""; }; 513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = ""; }; 5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = ""; }; @@ -563,6 +565,7 @@ isa = PBXGroup; children = ( 5132AAC32448BAD90077840A /* TwitterFeedProvider.swift */, + 5132DE802449159100806ADE /* TwitterUser.swift */, ); path = Twitter; sourceTree = ""; @@ -1217,6 +1220,7 @@ 84245C851FDDD8CB0074AFBB /* FeedbinSubscription.swift in Sources */, 9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */, 3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */, + 5132DE812449159100806ADE /* TwitterUser.swift in Sources */, 3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */, 769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */, 769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */, diff --git a/Frameworks/Account/FeedProvider/FeedProvider.swift b/Frameworks/Account/FeedProvider/FeedProvider.swift index 47ab5d468..a11858d39 100644 --- a/Frameworks/Account/FeedProvider/FeedProvider.swift +++ b/Frameworks/Account/FeedProvider/FeedProvider.swift @@ -24,8 +24,8 @@ public protocol FeedProvider { /// Provide the iconURL of the given URL func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) - /// Construct a ParsedFeed that can be used to create and store a new Feed - func provide(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) + /// Construct a Name for the new feed + func assignName(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) /// Refresh all the article entries (ParsedItems) func refresh(_ webFeed: WebFeed, completion: @escaping (Result, Error>) -> Void) diff --git a/Frameworks/Account/FeedProvider/Twitter/TwitterFeedProvider.swift b/Frameworks/Account/FeedProvider/Twitter/TwitterFeedProvider.swift index 89ee48d8b..654810e5b 100644 --- a/Frameworks/Account/FeedProvider/Twitter/TwitterFeedProvider.swift +++ b/Frameworks/Account/FeedProvider/Twitter/TwitterFeedProvider.swift @@ -82,7 +82,6 @@ public struct TwitterFeedProvider: FeedProvider { } let bestUserName = username != nil ? username : urlComponents.user - if bestUserName == userID { return .owner } @@ -92,14 +91,65 @@ public struct TwitterFeedProvider: FeedProvider { public func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { if let screenName = deriveScreenName(urlComponents) { - fetchIconURL(screenName: screenName, completion: completion) + fetchUser(screenName: screenName) { result in + switch result { + case .success(let user): + if let avatarURL = user.avatarURL { + completion(.success(avatarURL)) + } else { + completion(.failure(TwitterFeedProviderError.screenNameNotFound)) + } + case .failure(let error): + completion(.failure(error)) + } + } } else { completion(.failure(TwitterFeedProviderError.screenNameNotFound)) } } - public func provide(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { - // TODO: Finish implementation + public func assignName(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { + switch urlComponents.path { + + case "/", "/home": + let name = NSLocalizedString("Twitter Timeline", comment: "Twitter Timeline") + completion(.success(name)) + + case "/notifications/mentions": + let name = NSLocalizedString("Twitter Mentions", comment: "Twitter Mentions") + completion(.success(name)) + + case "/search": + if let query = urlComponents.queryItems?.first(where: { $0.name == "q" })?.value { + let localized = NSLocalizedString("Twitter Search: %@", comment: "Twitter Search") + let searchName = NSString.localizedStringWithFormat(localized as NSString, query) as String + completion(.success(searchName)) + } else { + let name = NSLocalizedString("Twitter Search", comment: "Twitter Search") + completion(.success(name)) + } + + default: + if let screenName = deriveScreenName(urlComponents) { + fetchUser(screenName: screenName) { result in + switch result { + case .success(let user): + if let userName = user.name { + let localized = NSLocalizedString("%@ on Twitter", comment: "Twitter Name") + let onName = NSString.localizedStringWithFormat(localized as NSString, userName) as String + completion(.success(onName)) + } else { + completion(.failure(TwitterFeedProviderError.screenNameNotFound)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } else { + completion(.failure(TwitterFeedProviderError.unknown)) + } + + } } public func refresh(_ webFeed: WebFeed, completion: @escaping (Result, Error>) -> Void) { @@ -139,17 +189,19 @@ private extension TwitterFeedProvider { } } - func fetchIconURL(screenName: String, completion: @escaping (Result) -> Void) { + func fetchUser(screenName: String, completion: @escaping (Result) -> Void) { let url = "\(Self.apiBase)users/show.json" let parameters = ["screen_name": screenName] client.get(url, parameters: parameters) { result in switch result { case .success(let response): - if let json = try? response.jsonObject() as? [String: Any], let url = json["profile_image_url_https"] as? String { - completion(.success(url)) - } else { - completion(.failure(TwitterFeedProviderError.unknown)) + let decoder = JSONDecoder() + do { + let user = try decoder.decode(TwitterUser.self, from: response.data) + completion(.success(user)) + } catch { + completion(.failure(error)) } case .failure(let error): completion(.failure(error)) diff --git a/Frameworks/Account/FeedProvider/Twitter/TwitterUser.swift b/Frameworks/Account/FeedProvider/Twitter/TwitterUser.swift new file mode 100644 index 000000000..2b456f4cb --- /dev/null +++ b/Frameworks/Account/FeedProvider/Twitter/TwitterUser.swift @@ -0,0 +1,21 @@ +// +// TwitterUser.swift +// Account +// +// Created by Maurice Parker on 4/16/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct TwitterUser: Codable { + + let name: String? + let avatarURL: String? + + enum CodingKeys: String, CodingKey { + case name = "name" + case avatarURL = "profile_image_url_https" + } + +} diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index c6e64a324..e5c269478 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -115,15 +115,30 @@ final class LocalAccountDelegate: AccountDelegate { // Username should be part of the URL on new feed adds if let feedProvider = FeedProviderManager.shared.best(for: urlComponents, with: nil) { - refreshProgress.addToNumberOfTasksAndRemaining(1) - feedProvider.provide(urlComponents) { result in + refreshProgress.addToNumberOfTasksAndRemaining(2) + + feedProvider.assignName(urlComponents) { result in self.refreshProgress.completeTask() switch result { - case .success(let parsedFeed): - let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil) - account.update(feed, with: parsedFeed, {_ in}) - case .failure: - completion(.failure(AccountError.createErrorNotFound)) + + case .success(let name): + let feed = account.createWebFeed(with: name, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil) + feed.editedName = name + + feedProvider.refresh(feed) { result in + self.refreshProgress.completeTask() + switch result { + case .success(let parsedItems): + account.update(urlString, with: parsedItems) { _ in + container.addWebFeed(feed) + } + case .failure: + completion(.failure(AccountError.createErrorNotFound)) + } + } + + case .failure(let error): + completion(.failure(error)) } }