diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index fa5eb2b3b..72a26806b 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -11,6 +11,7 @@ import RSCore import RSWeb import Articles import ArticlesDatabase +import RSDatabase // Main thread only. @@ -339,35 +340,45 @@ public final class AccountManager: UnreadCountProvider { return articles } - public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) { - precondition(Thread.isMainThread) - - var allFetchedArticles = Set
() - let numberOfAccounts = activeAccounts.count - var accountsReporting = 0 - - guard numberOfAccounts > 0 else { - completion(.success(allFetchedArticles)) - return - } - - for account in activeAccounts { - account.fetchArticlesAsync(fetchType) { (articleSetResult) in - accountsReporting += 1 - - switch articleSetResult { - case .success(let articles): - allFetchedArticles.formUnion(articles) - if accountsReporting == numberOfAccounts { - completion(.success(allFetchedArticles)) - } - case .failure(let databaseError): - completion(.failure(databaseError)) - return - } - } - } - } + public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) { + precondition(Thread.isMainThread) + + guard activeAccounts.count > 0 else { + completion(.success(Set
())) + return + } + + var allFetchedArticles = Set
() + var databaseError: DatabaseError? + let dispatchGroup = DispatchGroup() + + for account in activeAccounts { + + dispatchGroup.enter() + + account.fetchArticlesAsync(fetchType) { (articleSetResult) in + precondition(Thread.isMainThread) + + switch articleSetResult { + case .success(let articles): + allFetchedArticles.formUnion(articles) + case .failure(let error): + databaseError = error + } + + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + if let databaseError { + completion(.failure(databaseError)) + } + else { + completion(.success(allFetchedArticles)) + } + } + } public func fetchUnreadArticlesBetween(limit: Int? = nil, before: Date? = nil, after: Date? = nil) throws -> Set
{ precondition(Thread.isMainThread) diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterEntities.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterEntities.swift deleted file mode 100644 index 15d4c39ee..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterEntities.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// TwitterEntities.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -protocol TwitterEntity { - var indices: [Int]? { get } - func renderAsHTML() -> String -} - -extension TwitterEntity { - - var startIndex: Int { - if let indices = indices, indices.count > 0 { - return indices[0] - } - return 0 - } - - var endIndex: Int { - if let indices = indices, indices.count > 1 { - return indices[1] - } - return 0 - } - -} - -struct TwitterEntities: Codable { - - let hashtags: [TwitterHashtag]? - let urls: [TwitterURL]? - let userMentions: [TwitterMention]? - let symbols: [TwitterSymbol]? - let media: [TwitterMedia]? - - enum CodingKeys: String, CodingKey { - case hashtags = "hashtags" - case urls = "urls" - case userMentions = "user_mentions" - case symbols = "symbols" - case media = "media" - } - - func combineAndSort() -> [TwitterEntity] { - var entities = [TwitterEntity]() - if let hashtags = hashtags { - entities.append(contentsOf: hashtags) - } - if let urls = urls { - entities.append(contentsOf: urls) - } - if let userMentions = userMentions { - entities.append(contentsOf: userMentions) - } - if let symbols = symbols { - entities.append(contentsOf: symbols) - } - if let media = media { - entities.append(contentsOf: media) - } - return entities.sorted(by: { $0.startIndex < $1.startIndex }) - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterExtendedEntities.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterExtendedEntities.swift deleted file mode 100644 index e449e66ca..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterExtendedEntities.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// TwitterExtendedEntities.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterExtendedEntities: Codable { - - let medias: [TwitterExtendedMedia]? - - enum CodingKeys: String, CodingKey { - case medias = "media" - } - - func renderAsHTML() -> String { - var html = String() - if let medias = medias { - for media in medias { - html += media.renderAsHTML() - } - } - return html - } -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterExtendedMedia.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterExtendedMedia.swift deleted file mode 100644 index 614fd5438..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterExtendedMedia.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// TwitterExtendedMedia.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterExtendedMedia: Codable { - - let idStr: String? - let indices: [Int]? - let mediaURL: String? - let httpsMediaURL: String? - let url: String? - let displayURL: String? - let type: String? - let video: TwitterVideo? - - enum CodingKeys: String, CodingKey { - case idStr = "idStr" - case indices = "indices" - case mediaURL = "media_url" - case httpsMediaURL = "media_url_https" - case url = "url" - case displayURL = "display_url" - case type = "type" - case video = "video_info" - } - - func renderAsHTML() -> String { - var html = String() - - switch type { - case "photo": - html += renderPhotoAsHTML() - case "video": - html += renderVideoAsHTML() - case "animated_gif": - html += renderAnimatedGIFAsHTML() - default: - break - } - - return html - } - -} - -private extension TwitterExtendedMedia { - - func renderPhotoAsHTML() -> String { - if let httpsMediaURL = httpsMediaURL { - return "
" - } - if let mediaURL = mediaURL { - return "
" - } - return "" - } - - func renderVideoAsHTML() -> String { - guard let bestVariantURL = findBestVariant()?.url else { return "" } - - var html = "" - return html - } - - func renderAnimatedGIFAsHTML() -> String { - guard let bestVariantURL = findBestVariant()?.url else { return "" } - - var html = "" - return html - } - - func findBestVariant() -> TwitterVideo.Variant? { - var best: TwitterVideo.Variant? = nil - if let variants = video?.variants { - for variant in variants { - if let currentBest = best { - if variant.bitrate ?? 0 > currentBest.bitrate ?? 0 { - best = variant - } - } else { - best = variant - } - } - } - return best - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift deleted file mode 100644 index a2f477385..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterFeedProvider.swift +++ /dev/null @@ -1,485 +0,0 @@ -// -// TwitterFeedProvider.swift -// FeedProvider -// -// Created by Maurice Parker on 4/7/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import Secrets -import OAuthSwift -import RSParser -import RSWeb - -public enum TwitterFeedProviderError: LocalizedError { - case rateLimitExceeded - case screenNameNotFound - case unknown - - public var errorDescription: String? { - switch self { - case .rateLimitExceeded: - return NSLocalizedString("Twitter API rate limit has been exceeded. Please wait a short time and try again.", comment: "Rate Limit") - case .screenNameNotFound: - return NSLocalizedString("Unable to determine screen name.", comment: "Screen name") - case .unknown: - return NSLocalizedString("An unknown Twitter Feed Provider error has occurred.", comment: "Unknown error") - } - } -} - -public enum TwitterFeedType: Int { - case homeTimeline = 0 - case mentions = 1 - case screenName = 2 - case search = 3 -} - -public final class TwitterFeedProvider: FeedProvider { - - private static let homeURL = "https://www.twitter.com" - private static let iconURL = "https://abs.twimg.com/favicons/twitter.ico" - private static let server = "api.twitter.com" - private static let apiBase = "https://api.twitter.com/1.1/" - private static let userAgentHeaders = UserAgent.headers() as! [String: String] - private static let dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" - - private static let userPaths = ["/", "/home", "/notifications"] - private static let reservedPaths = ["/search", "/explore", "/messages", "/i", "/compose", "/notifications/mentions"] - - private var parsingQueue = DispatchQueue(label: "TwitterFeedProvider parse queue") - - public var screenName: String - - private var oauthToken: String - private var oauthTokenSecret: String - - private var client: OAuthSwiftClient - - private var rateLimitRemaining: Int? - private var rateLimitReset: Date? - - public init?(tokenSuccess: OAuthSwift.TokenSuccess) { - guard let screenName = tokenSuccess.parameters["screen_name"] as? String else { - return nil - } - - self.screenName = screenName - self.oauthToken = tokenSuccess.credential.oauthToken - self.oauthTokenSecret = tokenSuccess.credential.oauthTokenSecret - - let tokenCredentials = Credentials(type: .oauthAccessToken, username: screenName, secret: oauthToken) - try? CredentialsManager.storeCredentials(tokenCredentials, server: Self.server) - - let tokenSecretCredentials = Credentials(type: .oauthAccessTokenSecret, username: screenName, secret: oauthTokenSecret) - try? CredentialsManager.storeCredentials(tokenSecretCredentials, server: Self.server) - - client = OAuthSwiftClient(consumerKey: SecretsManager.provider.twitterConsumerKey, - consumerSecret: SecretsManager.provider.twitterConsumerSecret, - oauthToken: oauthToken, - oauthTokenSecret: oauthTokenSecret, - version: .oauth1) - } - - public init?(screenName: String) { - self.screenName = screenName - - guard let tokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessToken, server: Self.server, username: screenName), - let tokenSecretCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessTokenSecret, server: Self.server, username: screenName) else { - return nil - } - - self.oauthToken = tokenCredentials.secret - self.oauthTokenSecret = tokenSecretCredentials.secret - - client = OAuthSwiftClient(consumerKey: SecretsManager.provider.twitterConsumerKey, - consumerSecret: SecretsManager.provider.twitterConsumerSecret, - oauthToken: oauthToken, - oauthTokenSecret: oauthTokenSecret, - version: .oauth1) - } - - public func ability(_ urlComponents: URLComponents) -> FeedProviderAbility { - guard urlComponents.host?.hasSuffix("twitter.com") ?? false else { - return .none - } - - if let username = urlComponents.user { - if username == screenName { - return .owner - } else { - return .none - } - } - - return .available - } - - public func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { - if let screenName = deriveScreenName(urlComponents) { - retrieveUser(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(.success(Self.iconURL)) - } - } - - public func metaData(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { - switch urlComponents.path { - - case "", "/", "/home": - let name = NSLocalizedString("Twitter Timeline", comment: "Twitter Timeline") - completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL))) - - case "/notifications/mentions": - let name = NSLocalizedString("Twitter Mentions", comment: "Twitter Mentions") - completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL))) - - case "/search": - if let query = urlComponents.queryItems?.first(where: { $0.name == "q" })?.value { - let localized = NSLocalizedString("Twitter Search: %@", comment: "Twitter Search") - let name = NSString.localizedStringWithFormat(localized as NSString, query) as String - completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL))) - } else { - let name = NSLocalizedString("Twitter Search", comment: "Twitter Search") - completion(.success(FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL))) - } - - default: - if let hashtag = deriveHashtag(urlComponents) { - completion(.success(FeedProviderFeedMetaData(name: "#\(hashtag)", homePageURL: Self.homeURL))) - } else if let listID = deriveListID(urlComponents) { - retrieveList(listID: listID) { result in - switch result { - case .success(let list): - if let userName = list.name { - var urlComponents = URLComponents(string: Self.homeURL) - urlComponents?.path = "/i/lists/\(listID)" - completion(.success(FeedProviderFeedMetaData(name: userName, homePageURL: urlComponents?.url?.absoluteString))) - } else { - completion(.failure(TwitterFeedProviderError.screenNameNotFound)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } else if let screenName = deriveScreenName(urlComponents) { - retrieveUser(screenName: screenName) { result in - switch result { - case .success(let user): - if let userName = user.name { - var urlComponents = URLComponents(string: Self.homeURL) - urlComponents?.path = "/\(screenName)" - completion(.success(FeedProviderFeedMetaData(name: userName, homePageURL: urlComponents?.url?.absoluteString))) - } 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) { - guard let urlComponents = URLComponents(string: webFeed.url) else { - completion(.failure(TwitterFeedProviderError.unknown)) - return - } - - let api: String - var parameters = [String: Any]() - var isSearch = false - - parameters["count"] = 20 - - switch urlComponents.path { - case "", "/", "/home": - parameters["count"] = 100 - if let sinceToken = webFeed.sinceToken, let sinceID = Int(sinceToken) { - parameters["since_id"] = sinceID - } - api = "statuses/home_timeline.json" - case "/notifications/mentions": - api = "statuses/mentions_timeline.json" - case "/search": - api = "search/tweets.json" - if let query = urlComponents.queryItems?.first(where: { $0.name == "q" })?.value { - parameters["q"] = query - } - isSearch = true - default: - if let hashtag = deriveHashtag(urlComponents) { - api = "search/tweets.json" - parameters["q"] = "#\(hashtag)" - isSearch = true - } else if let listID = deriveListID(urlComponents) { - api = "lists/statuses.json" - parameters["list_id"] = listID - } else if let screenName = deriveScreenName(urlComponents) { - api = "statuses/user_timeline.json" - parameters["exclude_replies"] = true - parameters["screen_name"] = screenName - } else { - completion(.failure(TwitterFeedProviderError.unknown)) - return - } - } - - retrieveTweets(api: api, parameters: parameters, isSearch: isSearch) { result in - switch result { - case .success(let statuses): - if let sinceID = statuses.first?.idStr { - webFeed.sinceToken = sinceID - } - self.parsingQueue.async { - let parsedItems = self.makeParsedItems(webFeed.url, statuses) - DispatchQueue.main.async { - completion(.success(parsedItems)) - } - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - public static func buildURL(_ type: TwitterFeedType, username: String?, screenName: String?, searchField: String?) -> URL? { - var components = URLComponents() - components.scheme = "https" - components.host = "twitter.com" - - switch type { - case .homeTimeline: - guard let username = username else { - return nil - } - components.user = username - case .mentions: - guard let username = username else { - return nil - } - components.user = username - components.path = "/notifications/mentions" - case .screenName: - guard let screenName = screenName else { - return nil - } - components.path = "/\(screenName)" - case .search: - guard let searchField = searchField else { - return nil - } - components.path = "/search" - components.queryItems = [URLQueryItem(name: "q", value: searchField)] - } - - return components.url - } - -} - -// MARK: OAuth1SwiftProvider - -extension TwitterFeedProvider: OAuth1SwiftProvider { - - public static var callbackURL: URL { - return URL(string: "netnewswire://")! - } - - public static var oauth1Swift: OAuth1Swift { - return OAuth1Swift( - consumerKey: SecretsManager.provider.twitterConsumerKey, - consumerSecret: SecretsManager.provider.twitterConsumerSecret, - requestTokenUrl: "https://api.twitter.com/oauth/request_token", - authorizeUrl: "https://api.twitter.com/oauth/authorize", - accessTokenUrl: "https://api.twitter.com/oauth/access_token" - ) - } - -} - -// MARK: Private - -private extension TwitterFeedProvider { - - func deriveHashtag(_ urlComponents: URLComponents) -> String? { - let path = urlComponents.path - if path.starts(with: "/hashtag/"), let startIndex = path.index(path.startIndex, offsetBy: 9, limitedBy: path.endIndex), startIndex < path.endIndex { - return String(path[startIndex.. String? { - let path = urlComponents.path - guard !Self.reservedPaths.contains(path) else { return nil } - - if path.isEmpty || Self.userPaths.contains(path) { - return screenName - } else { - return String(path.suffix(from: path.index(path.startIndex, offsetBy: 1))) - } - } - - func deriveListID(_ urlComponents: URLComponents) -> String? { - let path = urlComponents.path - guard path.starts(with: "/i/lists/") else { return nil } - return String(path.suffix(from: path.index(path.startIndex, offsetBy: 9))) - } - - func retrieveUser(screenName: String, completion: @escaping (Result) -> Void) { - let url = "\(Self.apiBase)users/show.json" - let parameters = ["screen_name": screenName] - - client.get(url, parameters: parameters, headers: Self.userAgentHeaders) { result in - switch result { - case .success(let response): - 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)) - } - } - } - - func retrieveList(listID: String, completion: @escaping (Result) -> Void) { - let url = "\(Self.apiBase)lists/show.json" - let parameters = ["list_id": listID] - - client.get(url, parameters: parameters, headers: Self.userAgentHeaders) { result in - switch result { - case .success(let response): - let decoder = JSONDecoder() - do { - let collection = try decoder.decode(TwitterList.self, from: response.data) - completion(.success(collection)) - } catch { - completion(.failure(error)) - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - func retrieveTweets(api: String, parameters: [String: Any], isSearch: Bool, completion: @escaping (Result<[TwitterStatus], Error>) -> Void) { - let url = "\(Self.apiBase)\(api)" - var expandedParameters = parameters - expandedParameters["tweet_mode"] = "extended" - - if let remaining = rateLimitRemaining, let reset = rateLimitReset, remaining < 1 && reset > Date() { - completion(.failure(TwitterFeedProviderError.rateLimitExceeded)) - return - } - - client.get(url, parameters: expandedParameters, headers: Self.userAgentHeaders) { result in - switch result { - case .success(let response): - - let decoder = JSONDecoder() - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale.init(identifier: "en_US_POSIX") - dateFormatter.dateFormat = Self.dateFormat - decoder.dateDecodingStrategy = .formatted(dateFormatter) - - if let remaining = response.response.value(forHTTPHeaderField: "x-rate-limit-remaining") { - self.rateLimitRemaining = Int(remaining) ?? 0 - } - if let reset = response.response.value(forHTTPHeaderField: "x-rate-limit-reset") { - self.rateLimitReset = Date(timeIntervalSince1970: Double(reset) ?? 0) - } - - self.parsingQueue.async { - do { - let tweets: [TwitterStatus] - if isSearch { - let searchResult = try decoder.decode(TwitterSearchResult.self, from: response.data) - if let statuses = searchResult.statuses { - tweets = statuses - } else { - tweets = [TwitterStatus]() - } - } else { - tweets = try decoder.decode([TwitterStatus].self, from: response.data) - } - DispatchQueue.main.async { - completion(.success(tweets)) - } - } catch { - DispatchQueue.main.async { - completion(.failure(error)) - } - } - } - - case .failure(let error): - if error.errorCode == -11 { - // Eat these errors. They are old or invalid URL requests. - completion(.success([TwitterStatus]())) - } else { - completion(.failure(error)) - } - } - } - } - - func makeParsedItems(_ webFeedURL: String, _ statuses: [TwitterStatus]) -> Set { - var parsedItems = Set() - - for status in statuses { - guard let idStr = status.idStr, let statusURL = status.url else { continue } - - let parsedItem = ParsedItem(syncServiceID: nil, - uniqueID: idStr, - feedURL: webFeedURL, - url: statusURL, - externalURL: nil, - title: nil, - language: nil, - contentHTML: status.renderAsHTML(), - contentText: status.renderAsText(), - summary: nil, - imageURL: nil, - bannerImageURL: nil, - datePublished: status.createdAt, - dateModified: nil, - authors: makeParsedAuthors(status.user), - tags: nil, - attachments: nil) - parsedItems.insert(parsedItem) - } - - return parsedItems - } - - func makeUserURL(_ screenName: String) -> String { - return "https://twitter.com/\(screenName)" - } - - func makeParsedAuthors(_ user: TwitterUser?) -> Set? { - guard let user = user else { return nil } - return Set([ParsedAuthor(name: user.name, url: user.url, avatarURL: user.avatarURL, emailAddress: nil)]) - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterHashtag.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterHashtag.swift deleted file mode 100644 index 029f56c43..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterHashtag.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// TwitterHashtag.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterHashtag: Codable, TwitterEntity { - - let text: String? - let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case text = "text" - case indices = "indices" - } - - func renderAsHTML() -> String { - var html = String() - if let text = text { - html += "#\(text)" - } - return html - } -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterList.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterList.swift deleted file mode 100644 index 96427fe94..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterList.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// TwitterList.swift -// -// -// Created by Maurice Parker on 8/14/20. -// - -import Foundation - -struct TwitterList: Codable { - - let name: String? - - enum CodingKeys: String, CodingKey { - case name = "name" - } -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterMedia.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterMedia.swift deleted file mode 100644 index 57f279a19..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterMedia.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// TwitterMedia.swift -// Account -// -// Created by Maurice Parker on 4/20/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterMedia: Codable, TwitterEntity { - - let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case indices = "indices" - } - - func renderAsHTML() -> String { - return String() - } -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterMention.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterMention.swift deleted file mode 100644 index 4cd00551f..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterMention.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// TwitterMention.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterMention: Codable, TwitterEntity { - - let name: String? - let indices: [Int]? - let screenName: String? - let idStr: String? - - enum CodingKeys: String, CodingKey { - case name = "url" - case indices = "indices" - case screenName = "screen_name" - case idStr = "idStr" - } - - func renderAsHTML() -> String { - var html = String() - if let screenName = screenName { - html += "@\(screenName)" - } - return html - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterSearchResult.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterSearchResult.swift deleted file mode 100644 index 6a8a1373f..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterSearchResult.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// TwitterSearchResult.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterSearchResult: Codable { - - let statuses: [TwitterStatus]? - - enum CodingKeys: String, CodingKey { - case statuses = "statuses" - } -} - diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift deleted file mode 100644 index fa89f005c..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// TwitterStatus.swift -// Account -// -// Created by Maurice Parker on 4/16/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -final class TwitterStatus: Codable { - - let createdAt: Date? - let idStr: String? - let fullText: String? - let displayTextRange: [Int]? - let user: TwitterUser? - let truncated: Bool? - let retweeted: Bool? - let retweetedStatus: TwitterStatus? - let quotedStatus: TwitterStatus? - let entities: TwitterEntities? - let extendedEntities: TwitterExtendedEntities? - - enum CodingKeys: String, CodingKey { - case createdAt = "created_at" - case idStr = "id_str" - case fullText = "full_text" - case displayTextRange = "display_text_range" - case user = "user" - case truncated = "truncated" - case retweeted = "retweeted" - case retweetedStatus = "retweeted_status" - case quotedStatus = "quoted_status" - case entities = "entities" - case extendedEntities = "extended_entities" - } - - var url: String? { - guard let userURL = user?.url, let idStr = idStr else { return nil } - return "\(userURL)/status/\(idStr)" - } - - func renderAsText() -> String? { - let statusToRender = retweetedStatus != nil ? retweetedStatus! : self - return statusToRender.displayText - } - - func renderAsHTML(topLevel: Bool = true) -> String { - if let retweetedStatus = retweetedStatus { - return renderAsRetweetHTML(retweetedStatus) - } - if let quotedStatus = quotedStatus { - return renderAsQuoteHTML(quotedStatus, topLevel: topLevel) - } - return renderAsOriginalHTML(topLevel: topLevel) - } - -} - -private extension TwitterStatus { - - var displayText: String? { - if let text = fullText, let displayRange = displayTextRange, displayRange.count > 1, - let startIndex = text.index(text.startIndex, offsetBy: displayRange[0], limitedBy: text.endIndex), - let endIndex = text.index(text.startIndex, offsetBy: displayRange[1], limitedBy: text.endIndex) { - return String(text[startIndex.. 1, let entities = entities?.combineAndSort() { - - let displayStartIndex = text.index(text.startIndex, offsetBy: displayRange[0], limitedBy: text.endIndex) ?? text.startIndex - let displayEndIndex = text.index(text.startIndex, offsetBy: displayRange[1], limitedBy: text.endIndex) ?? text.endIndex - - var html = String() - var prevIndex = displayStartIndex - var unicodeScalarOffset = 0 - - for entity in entities { - - // The twitter indices are messed up by characters with more than one scalar, we are going to adjust for that here. - let endIndex = text.index(text.startIndex, offsetBy: entity.endIndex, limitedBy: text.endIndex) ?? text.endIndex - if prevIndex < endIndex { - let characters = String(text[prevIndex..") - } - - // We drop off any URL which is just pointing to the quoted status. It is redundant. - if let twitterURL = entity as? TwitterURL, let expandedURL = twitterURL.expandedURL, let quotedURL = quotedStatus?.url { - if expandedURL.caseInsensitiveCompare(quotedURL) != .orderedSame { - html += entity.renderAsHTML() - } - } else { - html += entity.renderAsHTML() - } - - prevIndex = entityEndIndex - - } - - if prevIndex < displayEndIndex { - html += String(text[prevIndex..") - } - - return html - } else { - return displayText - } - } - - func renderAsTweetHTML(_ status: TwitterStatus, topLevel: Bool) -> String { - var html = "
\(status.displayHTML ?? "")
" - - if !topLevel, let createdAt = status.createdAt, let url = status.url { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .short - html += "\(dateFormatter.string(from: createdAt))" - } - - return html - } - - func renderAsOriginalHTML(topLevel: Bool) -> String { - var html = renderAsTweetHTML(self, topLevel: topLevel) - html += extendedEntities?.renderAsHTML() ?? "" - return html - } - - func renderAsRetweetHTML(_ status: TwitterStatus) -> String { - var html = "
" - if let userHTML = status.user?.renderAsHTML() { - html += userHTML - } - html += status.renderAsHTML(topLevel: false) - html += "
" - return html - } - - func renderAsQuoteHTML(_ quotedStatus: TwitterStatus, topLevel: Bool) -> String { - var html = String() - html += renderAsTweetHTML(self, topLevel: topLevel) - html += extendedEntities?.renderAsHTML() ?? "" - html += "
" - if let userHTML = quotedStatus.user?.renderAsHTML() { - html += userHTML - } - html += quotedStatus.renderAsHTML(topLevel: false) - html += "
" - return html - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterSymbol.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterSymbol.swift deleted file mode 100644 index 9b1119743..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterSymbol.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// TwitterSymbol.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterSymbol: Codable, TwitterEntity { - - let text: String? - let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case text = "text" - case indices = "indices" - } - - func renderAsHTML() -> String { - var html = String() - if let text = text { - html += "$\(text)" - } - return html - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterURL.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterURL.swift deleted file mode 100644 index d7f80c6b6..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterURL.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// TwitterURL.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct TwitterURL: Codable, TwitterEntity { - - let url: String? - let indices: [Int]? - let displayURL: String? - let expandedURL: String? - - enum CodingKeys: String, CodingKey { - case url = "url" - case indices = "indices" - case displayURL = "display_url" - case expandedURL = "expanded_url" - } - - func renderAsHTML() -> String { - var html = String() - if let expandedURL = expandedURL, let displayURL = displayURL { - html += "\(displayURL)" - } - return html - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterUser.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterUser.swift deleted file mode 100644 index e87b21bcb..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterUser.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 screenName: String? - let avatarURL: String? - - enum CodingKeys: String, CodingKey { - case name = "name" - case screenName = "screen_name" - case avatarURL = "profile_image_url_https" - } - - var url: String { - return "https://twitter.com/\(screenName ?? "")" - } - - func renderAsHTML() -> String? { - var html = String() - html += "" - return html - } - -} diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterVideo.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterVideo.swift deleted file mode 100644 index a6df96292..000000000 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterVideo.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// TwitterVideoInfo.swift -// Account -// -// Created by Maurice Parker on 4/18/20. -// Copyright © 2020 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - - -struct TwitterVideo: Codable { - - let variants: [Variant]? - - enum CodingKeys: String, CodingKey { - case variants = "variants" - } - - struct Variant: Codable { - - let bitrate: Int? - let contentType: String? - let url: String? - - enum CodingKeys: String, CodingKey { - case bitrate = "bitrate" - case contentType = "content_type" - case url = "url" - } - - } - -} diff --git a/Account/Sources/Account/WebFeed.swift b/Account/Sources/Account/WebFeed.swift index cd10c3478..17335d600 100644 --- a/Account/Sources/Account/WebFeed.swift +++ b/Account/Sources/Account/WebFeed.swift @@ -253,17 +253,13 @@ public final class WebFeed: Feed, Renamable, Hashable { // MARK: - NotificationDisplayName public var notificationDisplayName: String { #if os(macOS) - if self.url.contains("twitter.com") { - return NSLocalizedString("Show notifications for new tweets", comment: "notifyNameDisplay / Twitter") - } else if self.url.contains("www.reddit.com") { + if self.url.contains("www.reddit.com") { return NSLocalizedString("Show notifications for new posts", comment: "notifyNameDisplay / Reddit") } else { return NSLocalizedString("Show notifications for new articles", comment: "notifyNameDisplay / Default") } #else - if self.url.contains("twitter.com") { - return NSLocalizedString("Notify about new tweets", comment: "notifyNameDisplay / Twitter") - } else if self.url.contains("www.reddit.com") { + if self.url.contains("www.reddit.com") { return NSLocalizedString("Notify about new posts", comment: "notifyNameDisplay / Reddit") } else { return NSLocalizedString("Notify about new articles", comment: "notifyNameDisplay / Default") diff --git a/Account/Tests/AccountTests/Feedly/FeedlyTestSecrets.swift b/Account/Tests/AccountTests/Feedly/FeedlyTestSecrets.swift index 237540e8e..490772d2e 100644 --- a/Account/Tests/AccountTests/Feedly/FeedlyTestSecrets.swift +++ b/Account/Tests/AccountTests/Feedly/FeedlyTestSecrets.swift @@ -13,8 +13,6 @@ struct FeedlyTestSecrets: SecretsProvider { var mercuryClientSecret = "" var feedlyClientId = "" var feedlyClientSecret = "" - var twitterConsumerKey = "" - var twitterConsumerSecret = "" var redditConsumerKey = "" var inoreaderAppId = "" var inoreaderAppKey = "" diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index 21c22c35a..aa074e4f6 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -84,10 +84,6 @@ struct AppAssets { return RSImage(named: "extensionPointReddit")! }() - static var extensionPointTwitter: RSImage = { - return RSImage(named: "extensionPointTwitter")! - }() - static var faviconTemplateImage: RSImage = { return RSImage(named: "faviconTemplateImage")! }() diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 7cf49670e..d6e14da41 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -520,26 +520,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return ExtensionPointManager.shared.isRedditEnabled } - if item.action == #selector(showAddTwitterFeedWindow(_:)) { - guard !isDisplayingSheet && isSpecialAccountAvailable && ExtensionPointManager.shared.isTwitterEnabled else { - return false - } - return ExtensionPointManager.shared.isTwitterEnabled - } - - #if !DEBUG - if item.action == #selector(debugDropConditionalGetInfo(_:)) { - return false - } - #endif - - if item.action == #selector(debugTestCrashReporterWindow(_:)) || - item.action == #selector(debugTestCrashReportSending(_:)) || - item.action == #selector(forceCrash(_:)) { - let appIDPrefix = Bundle.main.infoDictionary?["AppIdentifierPrefix"] as! String - return appIDPrefix == "M8L2WTLA8W." - } - #if !MAC_APP_STORE if item.action == #selector(toggleWebInspectorEnabled(_:)) { (item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off @@ -619,12 +599,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, addFeedController?.showAddFeedSheet(.redditFeed) } - @IBAction func showAddTwitterFeedWindow(_ sender: Any?) { - createAndShowMainWindowIfNecessary() - addFeedController = AddFeedController(hostWindow: mainWindowController!.window!) - addFeedController?.showAddFeedSheet(.twitterFeed) - } - @IBAction func showAddFolderWindow(_ sender: Any?) { createAndShowMainWindowIfNecessary() showAddFolderSheetOnWindow(mainWindowController!.window!) diff --git a/Mac/Base.lproj/AddTwitterFeedSheet.xib b/Mac/Base.lproj/AddTwitterFeedSheet.xib deleted file mode 100644 index d449a42eb..000000000 --- a/Mac/Base.lproj/AddTwitterFeedSheet.xib +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index 46b22d53b..50342563a 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -80,12 +80,6 @@ - - - - - - diff --git a/Mac/MainWindow/AddFeed/AddFeedController.swift b/Mac/MainWindow/AddFeed/AddFeedController.swift index 6e82d40a9..941fca98b 100644 --- a/Mac/MainWindow/AddFeed/AddFeedController.swift +++ b/Mac/MainWindow/AddFeed/AddFeedController.swift @@ -48,9 +48,6 @@ class AddFeedController: AddFeedWindowControllerDelegate { case .redditFeed: addFeedWindowController = AddRedditFeedWindowController(folderTreeController: folderTreeController, delegate: self) - case .twitterFeed: - addFeedWindowController = AddTwitterFeedWindowController(folderTreeController: folderTreeController, - delegate: self) } addFeedWindowController!.runSheetOnWindow(hostWindow) diff --git a/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift b/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift index 1651ec79f..e574d7fa5 100644 --- a/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift +++ b/Mac/MainWindow/AddFeed/AddFeedWIndowController.swift @@ -12,7 +12,6 @@ import Account enum AddFeedWindowControllerType { case webFeed case redditFeed - case twitterFeed } protocol AddFeedWindowControllerDelegate: AnyObject { diff --git a/Mac/MainWindow/AddFeed/AddTwitterFeedWindowController.swift b/Mac/MainWindow/AddFeed/AddTwitterFeedWindowController.swift deleted file mode 100644 index 31748d6b0..000000000 --- a/Mac/MainWindow/AddFeed/AddTwitterFeedWindowController.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// AddTwitterFeedWindowController.swift -// NetNewsWire -// -// Created by Maurice Parker on 4/21/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import AppKit -import RSCore -import RSTree -import Articles -import Account - -class AddTwitterFeedWindowController : NSWindowController, AddFeedWindowController { - - @IBOutlet weak var typePopupButton: NSPopUpButton! - @IBOutlet weak var typeDescriptionLabel: NSTextField! - - @IBOutlet weak var accountLabel: NSTextField! - @IBOutlet weak var accountPopupButton: NSPopUpButton! - @IBOutlet weak var screenSearchTextField: NSTextField! - - @IBOutlet var nameTextField: NSTextField! - @IBOutlet var addButton: NSButton! - @IBOutlet var folderPopupButton: NSPopUpButton! - - private weak var delegate: AddFeedWindowControllerDelegate? - private var folderTreeController: TreeController! - - private var userEnteredScreenSearch: String? { - var s = screenSearchTextField.stringValue - s = s.collapsingWhitespace - if s.isEmpty { - return nil - } - return s - } - - private var userEnteredTitle: String? { - var s = nameTextField.stringValue - s = s.collapsingWhitespace - if s.isEmpty { - return nil - } - return s - } - - var hostWindow: NSWindow! - - convenience init(folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) { - self.init(windowNibName: NSNib.Name("AddTwitterFeedSheet")) - self.folderTreeController = folderTreeController - self.delegate = delegate - } - - func runSheetOnWindow(_ hostWindow: NSWindow) { - hostWindow.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in - } - } - - override func windowDidLoad() { - - let accountMenu = NSMenu() - for feedProvider in ExtensionPointManager.shared.activeFeedProviders { - if let twitterFeedProvider = feedProvider as? TwitterFeedProvider { - let accountMenuItem = NSMenuItem() - accountMenuItem.title = "@\(twitterFeedProvider.screenName)" - accountMenu.addItem(accountMenuItem) - } - } - accountPopupButton.menu = accountMenu - - folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode, restrictToSpecialAccounts: true) - - if let container = AddWebFeedDefaultContainer.defaultContainer { - if let folder = container as? Folder, let account = folder.account { - FolderTreeMenu.select(account: account, folder: folder, in: folderPopupButton) - } else { - if let account = container as? Account { - FolderTreeMenu.select(account: account, folder: nil, in: folderPopupButton) - } - } - } - - updateUI() - } - - // MARK: Actions - - @IBAction func selectedType(_ sender: Any) { - screenSearchTextField.stringValue = "" - updateUI() - } - - @IBAction func cancel(_ sender: Any?) { - cancelSheet() - } - - @IBAction func addFeed(_ sender: Any?) { - guard let type = TwitterFeedType(rawValue: typePopupButton.selectedItem?.tag ?? 0), - let atUsername = accountPopupButton.selectedItem?.title else { return } - - let username = String(atUsername[atUsername.index(atUsername.startIndex, offsetBy: 1).. Container? { - return folderPopupButton.selectedItem?.representedObject as? Container - } -} diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 36b89dddb..9e7e485e4 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -1452,11 +1452,6 @@ private extension MainWindowController { newRedditFeedItem.action = Selector(("showAddRedditFeedWindow:")) menu.addItem(newRedditFeedItem) - let newTwitterFeedItem = NSMenuItem() - newTwitterFeedItem.title = NSLocalizedString("New Twitter Feed…", comment: "New Twitter Feed") - newTwitterFeedItem.action = Selector(("showAddTwitterFeedWindow:")) - menu.addItem(newTwitterFeedItem) - let newFolderFeedItem = NSMenuItem() newFolderFeedItem.title = NSLocalizedString("New Folder…", comment: "New Folder") newFolderFeedItem.action = Selector(("showAddFolderWindow:")) diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index 8dbd08563..0d571f8ed 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -376,7 +376,10 @@ protocol SidebarDelegate: AnyObject { } func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool { - let node = item as! Node + guard let node = item as? Node else { + assertionFailure("Expected item to be a Node.") + return false + } return node.isGroupItem } diff --git a/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/Contents.json b/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/Contents.json deleted file mode 100644 index 37bb7c41d..000000000 --- a/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "twitter24x24.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "twitter48x48.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/twitter24x24.png b/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/twitter24x24.png deleted file mode 100644 index 4d26fe41f..000000000 Binary files a/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/twitter24x24.png and /dev/null differ diff --git a/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/twitter48x48.png b/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/twitter48x48.png deleted file mode 100644 index a396124f5..000000000 Binary files a/Mac/Resources/Assets.xcassets/extensionPointTwitter.imageset/twitter48x48.png and /dev/null differ diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index b30af17ee..2799b0e1a 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -206,10 +206,6 @@ 5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; }; 5148F44B2336DB4700F8CD8B /* MasterTimelineTitleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */; }; 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */; }; - 514A89A2244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */; }; - 514A89A3244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */; }; - 514A89A5244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */; }; - 514A89A6244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */; }; 514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; }; 514C16CE24D2E63F009A3AFA /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16CD24D2E63F009A3AFA /* Account */; }; 514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; }; @@ -219,8 +215,6 @@ 5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; 515A50E6243D07A90089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; }; 515A50E7243D07A90089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; }; - 515A5107243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; }; - 515A5108243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; }; 515A5148243E64BA0089E588 /* ExtensionPointEnableWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */; }; 515A5149243E64BA0089E588 /* ExtensionPointEnableWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */; }; 515A516E243E7F950089E588 /* ExtensionPointDetail.xib in Resources */ = {isa = PBXBuildFile; fileRef = 515A516D243E7F950089E588 /* ExtensionPointDetail.xib */; }; @@ -231,7 +225,6 @@ 515A5178243E90200089E588 /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; }; 515A517B243E90260089E588 /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; }; 515A517C243E90260089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; }; - 515A5180243E90260089E588 /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; }; 515A5181243E90260089E588 /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; }; 515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; }; 515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; }; @@ -1241,11 +1234,8 @@ 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedbin.xib; sourceTree = ""; }; 5148F44A2336DB4700F8CD8B /* MasterTimelineTitleView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MasterTimelineTitleView.xib; sourceTree = ""; }; 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineTitleView.swift; sourceTree = ""; }; - 514A8980244FD63F0085E65D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Mac/Base.lproj/AddTwitterFeedSheet.xib; sourceTree = SOURCE_ROOT; }; - 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AddTwitterFeedWindowController.swift; path = AddFeed/AddTwitterFeedWindowController.swift; sourceTree = ""; }; 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointManager.swift; sourceTree = ""; }; - 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwitterFeedProvider-Extensions.swift"; sourceTree = ""; }; 515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointEnableWindowController.swift; sourceTree = ""; }; 515A516D243E7F950089E588 /* ExtensionPointDetail.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ExtensionPointDetail.xib; sourceTree = ""; }; 515A5170243E802B0089E588 /* ExtensionPointDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointDetailViewController.swift; sourceTree = ""; }; @@ -1848,7 +1838,6 @@ 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */, 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */, 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */, - 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */, 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */, ); path = ExtensionPoints; @@ -2389,8 +2378,6 @@ 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */, 51333D392468615D00EB5C91 /* AddRedditFeedSheet.xib */, 51333D1524685D2E00EB5C91 /* AddRedditFeedWindowController.swift */, - 514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */, - 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */, 848363002262A3BC00DA1D35 /* AddWebFeedSheet.xib */, 849A97521ED9EAC0007D329B /* AddWebFeedWindowController.swift */, 51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */, @@ -3401,7 +3388,6 @@ 65ED405C235DEF6C0081F399 /* ImportOPMLSheet.xib in Resources */, 51DEE81926FBFF84006DAA56 /* Promenade.nnwtheme in Resources */, 65ED405D235DEF6C0081F399 /* SidebarKeyboardShortcuts.plist in Resources */, - 514A89A3244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */, 51D0214726ED617100FF2E0F /* core.css in Resources */, DDF9E1D828EDF2FC000BC355 /* notificationSoundBlip.mp3 in Resources */, 5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */, @@ -3526,7 +3512,6 @@ 49F40DF82335B71000552BF4 /* newsfoot.js in Resources */, 51333D3B2468615D00EB5C91 /* AddRedditFeedSheet.xib in Resources */, BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */, - 514A89A2244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */, 5103A9982421643300410853 /* blank.html in Resources */, B20180AB28E3B76F0059686A /* Localizable.stringsdict in Resources */, 515A516E243E7F950089E588 /* ExtensionPointDetail.xib in Resources */, @@ -3915,7 +3900,6 @@ 65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */, 518C3194237B00DA004D740F /* DetailIconSchemeHandler.swift in Sources */, 65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */, - 514A89A6244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */, 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */, 653813182680E152007A082C /* AccountType+Helpers.swift in Sources */, 65ED3FEF235DEF6C0081F399 /* ScriptingObjectContainer.swift in Sources */, @@ -3991,7 +3975,6 @@ 65ED4028235DEF6C0081F399 /* ExtractedArticle.swift in Sources */, 65ED4029235DEF6C0081F399 /* DeleteCommand.swift in Sources */, 65ED402A235DEF6C0081F399 /* AddWebFeedWindowController.swift in Sources */, - 515A5108243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */, 65ED402B235DEF6C0081F399 /* ImportOPMLWindowController.swift in Sources */, 65ED402C235DEF6C0081F399 /* TimelineTableView.swift in Sources */, 178A9F9E2549449F00AB7E9D /* AddAccountsView.swift in Sources */, @@ -4164,7 +4147,6 @@ 512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */, 5110C37D2373A8D100A9C04F /* InspectorIconHeaderView.swift in Sources */, 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */, - 515A5180243E90260089E588 /* TwitterFeedProvider-Extensions.swift in Sources */, 51C452AC22650FD200C03939 /* AppNotifications.swift in Sources */, 51EF0F7E2277A57D0050506E /* MasterTimelineAccessibilityCellLayout.swift in Sources */, 51A1699B235E10D700EB091F /* AccountInspectorViewController.swift in Sources */, @@ -4251,7 +4233,6 @@ 849C78922362AB04009A71E4 /* ExportOPMLWindowController.swift in Sources */, 84CC88181FE59CBF00644329 /* SmartFeedsController.swift in Sources */, 849A97661ED9EB96007D329B /* SidebarViewController.swift in Sources */, - 514A89A5244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */, 849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */, 5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */, 8405DD9922153B6B008CE1BF /* TimelineContainerView.swift in Sources */, @@ -4318,7 +4299,6 @@ 5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */, 84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */, 845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */, - 515A5107243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift in Sources */, 845EE7B11FC2366500854A1F /* StarredFeedDelegate.swift in Sources */, DFC14F1228EA5DC500F6EE86 /* AboutData.swift in Sources */, 848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */, @@ -4537,12 +4517,12 @@ name = MainInterface.storyboard; sourceTree = ""; }; - 514A897F244FD63F0085E65D /* AddTwitterFeedSheet.xib */ = { + 51C0307F257D815A00609262 /* UnifiedWindow.storyboard */ = { isa = PBXVariantGroup; children = ( - 514A8980244FD63F0085E65D /* Base */, + 51C03080257D815A00609262 /* Base */, ); - name = AddTwitterFeedSheet.xib; + name = UnifiedWindow.storyboard; sourceTree = ""; }; 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */ = { diff --git a/Secrets/Sources/Secrets/SecretsProvider.swift b/Secrets/Sources/Secrets/SecretsProvider.swift index 008921d10..09fd49e83 100644 --- a/Secrets/Sources/Secrets/SecretsProvider.swift +++ b/Secrets/Sources/Secrets/SecretsProvider.swift @@ -12,8 +12,6 @@ public protocol SecretsProvider { var mercuryClientSecret: String { get } var feedlyClientId: String { get } var feedlyClientSecret: String { get } - var twitterConsumerKey: String { get } - var twitterConsumerSecret: String { get } var redditConsumerKey: String { get } var inoreaderAppId: String { get } var inoreaderAppKey: String { get } diff --git a/Shared/Article Rendering/stylesheet.css b/Shared/Article Rendering/stylesheet.css index 902be2fa6..ae37b23ca 100644 --- a/Shared/Article Rendering/stylesheet.css +++ b/Shared/Article Rendering/stylesheet.css @@ -286,30 +286,6 @@ blockquote { border-top: 1px solid var(--header-table-border-color); } -/* Twitter */ - -.twitterAvatar { - vertical-align: middle; - border-radius: 4px; - height: 1.7em; - width: 1.7em; -} - -.twitterUsername { - line-height: 1.2; - margin-left: 4px; - display: inline-block; - vertical-align: middle; -} - -.twitterScreenName { - font-size: 66%; -} - -.twitterTimestamp { - font-size: 66%; -} - /* Newsfoot theme for light mode (default) */ .newsfoot-footnote-popover { background: #ccc; diff --git a/Shared/ExtensionPoints/ExtensionPointIdentifer.swift b/Shared/ExtensionPoints/ExtensionPointIdentifer.swift index 39217dd81..db2a2d6b0 100644 --- a/Shared/ExtensionPoints/ExtensionPointIdentifer.swift +++ b/Shared/ExtensionPoints/ExtensionPointIdentifer.swift @@ -15,7 +15,6 @@ enum ExtensionPointIdentifer: Hashable { case marsEdit case microblog #endif - case twitter(String) case reddit(String) var extensionPointType: ExtensionPoint.Type { @@ -26,8 +25,6 @@ enum ExtensionPointIdentifer: Hashable { case .microblog: return SendToMicroBlogCommand.self #endif - case .twitter: - return TwitterFeedProvider.self case .reddit: return RedditFeedProvider.self } @@ -45,11 +42,6 @@ enum ExtensionPointIdentifer: Hashable { "type": "microblog" ] #endif - case .twitter(let screenName): - return [ - "type": "twitter", - "screenName": screenName - ] case .reddit(let username): return [ "type": "reddit", @@ -68,9 +60,6 @@ enum ExtensionPointIdentifer: Hashable { case "microblog": self = ExtensionPointIdentifer.microblog #endif - case "twitter": - guard let screenName = userInfo["screenName"] as? String else { return nil } - self = ExtensionPointIdentifer.twitter(screenName) case "reddit": guard let username = userInfo["username"] as? String else { return nil } self = ExtensionPointIdentifer.reddit(username) @@ -87,9 +76,6 @@ enum ExtensionPointIdentifer: Hashable { case .microblog: hasher.combine("microblog") #endif - case .twitter(let screenName): - hasher.combine("twitter") - hasher.combine(screenName) case .reddit(let username): hasher.combine("reddit") hasher.combine(username) diff --git a/Shared/ExtensionPoints/ExtensionPointManager.swift b/Shared/ExtensionPoints/ExtensionPointManager.swift index bf8cb0402..39e177fcf 100644 --- a/Shared/ExtensionPoints/ExtensionPointManager.swift +++ b/Shared/ExtensionPoints/ExtensionPointManager.swift @@ -69,16 +69,12 @@ final class ExtensionPointManager: FeedProviderManagerDelegate { return activeExtensionPoints.values.compactMap({ return $0 as? FeedProvider }) } - var isTwitterEnabled: Bool { - return activeExtensionPoints.values.contains(where: { $0 is TwitterFeedProvider }) - } - var isRedditEnabled: Bool { return activeExtensionPoints.values.contains(where: { $0 is RedditFeedProvider }) } init() { - possibleExtensionPointTypes = [TwitterFeedProvider.self, RedditFeedProvider.self] + possibleExtensionPointTypes = [RedditFeedProvider.self] loadExtensionPoints() } @@ -121,12 +117,6 @@ private extension ExtensionPointManager { func extensionPoint(for extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess?, completion: @escaping (Result) -> Void) { switch extensionPointType { - case is TwitterFeedProvider.Type: - if let tokenSuccess = tokenSuccess, let twitter = TwitterFeedProvider(tokenSuccess: tokenSuccess) { - completion(.success(twitter)) - } else { - completion(.failure(ExtensionPointManagerError.unableToCreate)) - } case is RedditFeedProvider.Type: if let tokenSuccess = tokenSuccess { RedditFeedProvider.create(tokenSuccess: tokenSuccess) { result in @@ -147,8 +137,6 @@ private extension ExtensionPointManager { func extensionPoint(for extensionPointID: ExtensionPointIdentifer) -> ExtensionPoint? { switch extensionPointID { - case .twitter(let screenName): - return TwitterFeedProvider(screenName: screenName) case .reddit(let username): return RedditFeedProvider(username: username) #if os(macOS) diff --git a/Shared/Secrets.swift.gyb b/Shared/Secrets.swift.gyb index 328919ad1..4884ef0c7 100644 --- a/Shared/Secrets.swift.gyb +++ b/Shared/Secrets.swift.gyb @@ -2,7 +2,11 @@ %{ import os +<<<<<<< HEAD secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'TWITTER_CONSUMER_KEY', 'TWITTER_CONSUMER_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY'] +======= +secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY'] +>>>>>>> mac-release def chunks(seq, size): return (seq[i:(i + size)] for i in range(0, len(seq), size)) diff --git a/Technotes/Dependencies.markdown b/Technotes/Dependencies.markdown index 88bb60df8..480e97958 100644 --- a/Technotes/Dependencies.markdown +++ b/Technotes/Dependencies.markdown @@ -20,7 +20,7 @@ RSDatabase uses FMDB for SQLite persistence. Required by OAuthSwift as a testing dependency. Not shipped in NNW. ## [OAuthSwift](https://github.com/OAuthSwift/OAuthSwift) -Our Reddit and Twitter use the OAuth framework to authenticate with the services +Our Reddit integration uses the OAuth framework to authenticate with the services and then service requests to them. ## [PLCrashReporter](https://github.com/microsoft/plcrashreporter)