Remove more Reddit references, including code in the Account framework.
This commit is contained in:
parent
6c1891f7fd
commit
8e8fdcf6d9
Account/Sources/Account/FeedProvider/Reddit
RedditFeedProvider.swiftRedditFeedProviderTokenRefreshOperation.swiftRedditGalleryData.swiftRedditLink.swiftRedditLinkListing.swiftRedditMe.swiftRedditMedia.swiftRedditMediaEmbed.swiftRedditMediaMetadata.swiftRedditPreview.swiftRedditSort.swiftRedditSubreddit.swift
NetNewsWire.xcodeproj
Shared/ExtensionPoints
@ -1,445 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditFeedProvider.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/2/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import os.log
|
|
||||||
import OAuthSwift
|
|
||||||
import Secrets
|
|
||||||
import RSCore
|
|
||||||
import RSParser
|
|
||||||
import RSWeb
|
|
||||||
|
|
||||||
public enum RedditFeedProviderError: LocalizedError {
|
|
||||||
case rateLimitExceeded
|
|
||||||
case accessFailure(Error)
|
|
||||||
case unknown
|
|
||||||
|
|
||||||
public var errorDescription: String? {
|
|
||||||
switch self {
|
|
||||||
case .rateLimitExceeded:
|
|
||||||
return NSLocalizedString("Reddit API rate limit has been exceeded. Please wait a short time and try again.", comment: "Rate Limit")
|
|
||||||
case .accessFailure(let error):
|
|
||||||
return NSLocalizedString("An attempt to access your Reddit feed(s) failed.\n\nIf this problem persists, please deactivate and reactivate the Reddit extension to fix this problem.\n\n\(error.localizedDescription)", comment: "Reddit Access")
|
|
||||||
case .unknown:
|
|
||||||
return NSLocalizedString("A Reddit Feed Provider error has occurred.", comment: "Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum RedditFeedType: Int {
|
|
||||||
case home = 0
|
|
||||||
case popular = 1
|
|
||||||
case all = 2
|
|
||||||
case subreddit = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class RedditFeedProvider: FeedProvider, RedditFeedProviderTokenRefreshOperationDelegate {
|
|
||||||
|
|
||||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "RedditFeedProvider")
|
|
||||||
|
|
||||||
private static let homeURL = "https://www.reddit.com"
|
|
||||||
private static let server = "www.reddit.com"
|
|
||||||
private static let apiBase = "https://oauth.reddit.com"
|
|
||||||
private static let userAgentHeaders = UserAgent.headers() as! [String: String]
|
|
||||||
|
|
||||||
private static let pseudoSubreddits = [
|
|
||||||
"popular": NSLocalizedString("Popular", comment: "Popular"),
|
|
||||||
"all": NSLocalizedString("All", comment: "All")
|
|
||||||
]
|
|
||||||
|
|
||||||
private let operationQueue = MainThreadOperationQueue()
|
|
||||||
private var parsingQueue = DispatchQueue(label: "RedditFeedProvider parse queue")
|
|
||||||
|
|
||||||
public var username: String?
|
|
||||||
|
|
||||||
var oauthTokenLastRefresh: Date?
|
|
||||||
var oauthToken: String
|
|
||||||
var oauthRefreshToken: String
|
|
||||||
|
|
||||||
var oauthSwift: OAuth2Swift?
|
|
||||||
private var client: OAuthSwiftClient? {
|
|
||||||
return oauthSwift?.client
|
|
||||||
}
|
|
||||||
|
|
||||||
private var rateLimitRemaining: Int?
|
|
||||||
private var rateLimitReset: Date?
|
|
||||||
|
|
||||||
public convenience init?(username: String) {
|
|
||||||
guard let tokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessToken, server: Self.server, username: username),
|
|
||||||
let refreshTokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthRefreshToken, server: Self.server, username: username) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.init(oauthToken: tokenCredentials.secret, oauthRefreshToken: refreshTokenCredentials.secret)
|
|
||||||
self.username = username
|
|
||||||
}
|
|
||||||
|
|
||||||
init(oauthToken: String, oauthRefreshToken: String) {
|
|
||||||
self.oauthToken = oauthToken
|
|
||||||
self.oauthRefreshToken = oauthRefreshToken
|
|
||||||
oauthSwift = Self.oauth2Swift
|
|
||||||
oauthSwift!.client.credential.oauthToken = oauthToken
|
|
||||||
oauthSwift!.client.credential.oauthRefreshToken = oauthRefreshToken
|
|
||||||
}
|
|
||||||
|
|
||||||
public func ability(_ urlComponents: URLComponents) -> FeedProviderAbility {
|
|
||||||
guard urlComponents.host?.hasSuffix("reddit.com") ?? false else {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
if let username = urlComponents.user {
|
|
||||||
if username == username {
|
|
||||||
return .owner
|
|
||||||
} else {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .available
|
|
||||||
}
|
|
||||||
|
|
||||||
public func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result<String, Error>) -> Void) {
|
|
||||||
guard urlComponents.path.hasPrefix("/r/") else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subreddit(urlComponents) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let subreddit):
|
|
||||||
if let iconURL = subreddit.data?.iconURL, !iconURL.isEmpty {
|
|
||||||
completion(.success(iconURL))
|
|
||||||
} else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func metaData(_ urlComponents: URLComponents, completion: @escaping (Result<FeedProviderFeedMetaData, Error>) -> Void) {
|
|
||||||
let path = urlComponents.path
|
|
||||||
|
|
||||||
// Reddit Home
|
|
||||||
let splitPath = path.split(separator: "/")
|
|
||||||
if path == "" || path == "/" || (splitPath.count == 1 && RedditSort(rawValue: String(splitPath[0])) != nil) {
|
|
||||||
let name = NSLocalizedString("Reddit Home", comment: "Reddit Home")
|
|
||||||
let metaData = FeedProviderFeedMetaData(name: name, homePageURL: Self.homeURL)
|
|
||||||
completion(.success(metaData))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subreddits
|
|
||||||
guard splitPath.count > 1, splitPath.count < 4, splitPath[0] == "r" else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if splitPath.count == 3 && RedditSort(rawValue: String(splitPath[2])) == nil {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let homePageURL = "https://www.reddit.com/\(splitPath[0])/\(splitPath[1])"
|
|
||||||
|
|
||||||
// Reddit Popular, Reddit All, etc...
|
|
||||||
if let subredditName = Self.pseudoSubreddits[String(splitPath[1])] {
|
|
||||||
let localized = NSLocalizedString("Reddit %@", comment: "Reddit")
|
|
||||||
let name = NSString.localizedStringWithFormat(localized as NSString, subredditName) as String
|
|
||||||
let metaData = FeedProviderFeedMetaData(name: name, homePageURL: homePageURL)
|
|
||||||
completion(.success(metaData))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subreddit(urlComponents) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let subreddit):
|
|
||||||
if let displayName = subreddit.data?.displayName {
|
|
||||||
completion(.success(FeedProviderFeedMetaData(name: displayName, homePageURL: homePageURL)))
|
|
||||||
} else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public func refresh(_ webFeed: WebFeed, completion: @escaping (Result<Set<ParsedItem>, Error>) -> Void) {
|
|
||||||
guard let urlComponents = URLComponents(string: webFeed.url) else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let api: String
|
|
||||||
if urlComponents.path.isEmpty {
|
|
||||||
api = "/.json"
|
|
||||||
} else {
|
|
||||||
api = "\(urlComponents.path).json"
|
|
||||||
}
|
|
||||||
|
|
||||||
let splitPath = urlComponents.path.split(separator: "/")
|
|
||||||
let identifySubreddit: Bool
|
|
||||||
if splitPath.count > 1 {
|
|
||||||
if Self.pseudoSubreddits.keys.contains(String(splitPath[1])) {
|
|
||||||
identifySubreddit = true
|
|
||||||
} else {
|
|
||||||
identifySubreddit = !urlComponents.path.hasPrefix("/r/")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
identifySubreddit = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(api: api, parameters: [:], resultType: RedditLinkListing.self) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let linkListing):
|
|
||||||
self.parsingQueue.async {
|
|
||||||
let parsedItems = self.makeParsedItems(webFeed.url, identifySubreddit, linkListing)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.success(parsedItems))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
if (error as? OAuthSwiftError)?.errorCode == -11 {
|
|
||||||
completion(.success(Set<ParsedItem>()))
|
|
||||||
} else {
|
|
||||||
completion(.failure(RedditFeedProviderError.accessFailure(error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func create(tokenSuccess: OAuthSwift.TokenSuccess, completion: @escaping (Result<RedditFeedProvider, Error>) -> Void) {
|
|
||||||
let oauthToken = tokenSuccess.credential.oauthToken
|
|
||||||
let oauthRefreshToken = tokenSuccess.credential.oauthRefreshToken
|
|
||||||
let redditFeedProvider = RedditFeedProvider(oauthToken: oauthToken, oauthRefreshToken: oauthRefreshToken)
|
|
||||||
|
|
||||||
redditFeedProvider.fetch(api: "/api/v1/me", resultType: RedditMe.self) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let user):
|
|
||||||
guard let username = user.name else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
redditFeedProvider.username = username
|
|
||||||
try storeCredentials(username: username, oauthToken: oauthToken, oauthRefreshToken: oauthRefreshToken)
|
|
||||||
completion(.success(redditFeedProvider))
|
|
||||||
} catch {
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func buildURL(_ type: RedditFeedType, username: String?, subreddit: String?, sort: RedditSort) -> URL? {
|
|
||||||
var components = URLComponents()
|
|
||||||
components.scheme = "https"
|
|
||||||
components.host = "www.reddit.com"
|
|
||||||
|
|
||||||
switch type {
|
|
||||||
case .home:
|
|
||||||
guard let username = username else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
components.user = username
|
|
||||||
components.path = "/\(sort.rawValue)"
|
|
||||||
case .popular:
|
|
||||||
components.path = "/r/popular/\(sort.rawValue)"
|
|
||||||
case .all:
|
|
||||||
components.path = "/r/all/\(sort.rawValue)"
|
|
||||||
case .subreddit:
|
|
||||||
guard let subreddit = subreddit else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
components.path = "/r/\(subreddit)/\(sort.rawValue)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return components.url
|
|
||||||
}
|
|
||||||
|
|
||||||
static func storeCredentials(username: String, oauthToken: String, oauthRefreshToken: String) throws {
|
|
||||||
let tokenCredentials = Credentials(type: .oauthAccessToken, username: username, secret: oauthToken)
|
|
||||||
try CredentialsManager.storeCredentials(tokenCredentials, server: Self.server)
|
|
||||||
let tokenSecretCredentials = Credentials(type: .oauthRefreshToken, username: username, secret: oauthRefreshToken)
|
|
||||||
try CredentialsManager.storeCredentials(tokenSecretCredentials, server: Self.server)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: OAuth1SwiftProvider
|
|
||||||
|
|
||||||
extension RedditFeedProvider: OAuth2SwiftProvider {
|
|
||||||
|
|
||||||
public static var oauth2Swift: OAuth2Swift {
|
|
||||||
let oauth2 = OAuth2Swift(consumerKey: SecretsManager.provider.redditConsumerKey,
|
|
||||||
consumerSecret: "",
|
|
||||||
authorizeUrl: "https://www.reddit.com/api/v1/authorize.compact?",
|
|
||||||
accessTokenUrl: "https://www.reddit.com/api/v1/access_token",
|
|
||||||
responseType: "code")
|
|
||||||
oauth2.accessTokenBasicAuthentification = true
|
|
||||||
return oauth2
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var callbackURL: URL {
|
|
||||||
return URL(string: "netnewswire://success")!
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var oauth2Vars: (state: String, scope: String, params: [String : String]) {
|
|
||||||
let state = generateState(withLength: 20)
|
|
||||||
let scope = "identity mysubreddits read"
|
|
||||||
let params = [
|
|
||||||
"duration" : "permanent",
|
|
||||||
]
|
|
||||||
return (state: state, scope: scope, params: params)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension RedditFeedProvider {
|
|
||||||
|
|
||||||
func subreddit(_ urlComponents: URLComponents, completion: @escaping (Result<RedditSubreddit, Error>) -> Void) {
|
|
||||||
let splitPath = urlComponents.path.split(separator: "/")
|
|
||||||
guard splitPath.count > 1 else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let secondElement = String(splitPath[1])
|
|
||||||
let api = "/r/\(secondElement)/about.json"
|
|
||||||
|
|
||||||
fetch(api: api, parameters: [:], resultType: RedditSubreddit.self, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetch<R: Decodable>(api: String, parameters: [String: Any] = [:], resultType: R.Type, completion: @escaping (Result<R, Error>) -> Void) {
|
|
||||||
guard let client = client else {
|
|
||||||
completion(.failure(RedditFeedProviderError.unknown))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let remaining = rateLimitRemaining, let reset = rateLimitReset, remaining < 1 && reset > Date() {
|
|
||||||
completion(.failure(RedditFeedProviderError.rateLimitExceeded))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = "\(Self.apiBase)\(api)"
|
|
||||||
|
|
||||||
var expandedParameters = parameters
|
|
||||||
expandedParameters["raw_json"] = "1"
|
|
||||||
|
|
||||||
client.get(url, parameters: expandedParameters, headers: Self.userAgentHeaders) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
|
|
||||||
if let remaining = response.response.value(forHTTPHeaderField: "X-Ratelimit-Remaining") {
|
|
||||||
self.rateLimitRemaining = Int(remaining)
|
|
||||||
} else {
|
|
||||||
self.rateLimitRemaining = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let reset = response.response.value(forHTTPHeaderField: "X-Ratelimit-Reset") {
|
|
||||||
self.rateLimitReset = Date(timeIntervalSinceNow: Double(reset) ?? 0)
|
|
||||||
} else {
|
|
||||||
self.rateLimitReset = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.parsingQueue.async {
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
do {
|
|
||||||
let result = try decoder.decode(resultType, from: response.data)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.success(result))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .failure(let oathError):
|
|
||||||
self.handleFailure(error: oathError) { error in
|
|
||||||
if let error = error {
|
|
||||||
completion(.failure(error))
|
|
||||||
} else {
|
|
||||||
self.fetch(api: api, parameters: parameters, resultType: resultType, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeParsedItems(_ webFeedURL: String,_ identifySubreddit: Bool, _ linkListing: RedditLinkListing) -> Set<ParsedItem> {
|
|
||||||
var parsedItems = Set<ParsedItem>()
|
|
||||||
|
|
||||||
guard let linkDatas = linkListing.data?.children?.compactMap({ $0.data }), !linkDatas.isEmpty else {
|
|
||||||
return parsedItems
|
|
||||||
}
|
|
||||||
|
|
||||||
for linkData in linkDatas {
|
|
||||||
guard let permalink = linkData.permalink else { continue }
|
|
||||||
|
|
||||||
let parsedItem = ParsedItem(syncServiceID: nil,
|
|
||||||
uniqueID: permalink,
|
|
||||||
feedURL: webFeedURL,
|
|
||||||
url: "https://www.reddit.com\(permalink)",
|
|
||||||
externalURL: linkData.url,
|
|
||||||
title: linkData.title,
|
|
||||||
language: nil,
|
|
||||||
contentHTML: linkData.renderAsHTML(identifySubreddit: identifySubreddit),
|
|
||||||
contentText: linkData.selfText,
|
|
||||||
summary: nil,
|
|
||||||
imageURL: nil,
|
|
||||||
bannerImageURL: nil,
|
|
||||||
datePublished: linkData.createdDate,
|
|
||||||
dateModified: nil,
|
|
||||||
authors: makeParsedAuthors(linkData.author),
|
|
||||||
tags: nil,
|
|
||||||
attachments: nil)
|
|
||||||
parsedItems.insert(parsedItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedItems
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeParsedAuthors(_ username: String?) -> Set<ParsedAuthor>? {
|
|
||||||
guard let username = username else { return nil }
|
|
||||||
var urlComponents = URLComponents(string: "https://www.reddit.com")
|
|
||||||
urlComponents?.path = "/u/\(username)"
|
|
||||||
let userURL = urlComponents?.url?.absoluteString
|
|
||||||
return Set([ParsedAuthor(name: "u/\(username)", url: userURL, avatarURL: nil, emailAddress: nil)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFailure(error: OAuthSwiftError, completion: @escaping (Error?) -> Void) {
|
|
||||||
if case .tokenExpired = error {
|
|
||||||
|
|
||||||
let op = RedditFeedProviderTokenRefreshOperation(delegate: self)
|
|
||||||
|
|
||||||
op.completionBlock = { operation in
|
|
||||||
let refreshOperation = operation as! RedditFeedProviderTokenRefreshOperation
|
|
||||||
if let error = refreshOperation.error {
|
|
||||||
completion(error)
|
|
||||||
} else {
|
|
||||||
completion(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operationQueue.add(op)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
completion(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditFeedProviderTokenRefreshOperation.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 8/12/20.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import os.log
|
|
||||||
import RSCore
|
|
||||||
import OAuthSwift
|
|
||||||
import Secrets
|
|
||||||
|
|
||||||
protocol RedditFeedProviderTokenRefreshOperationDelegate: AnyObject {
|
|
||||||
var username: String? { get }
|
|
||||||
var oauthTokenLastRefresh: Date? { get set }
|
|
||||||
var oauthToken: String { get set }
|
|
||||||
var oauthRefreshToken: String { get set }
|
|
||||||
var oauthSwift: OAuth2Swift? { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
class RedditFeedProviderTokenRefreshOperation: MainThreadOperation {
|
|
||||||
|
|
||||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "RedditFeedProvider")
|
|
||||||
|
|
||||||
public var isCanceled = false
|
|
||||||
public var id: Int?
|
|
||||||
public weak var operationDelegate: MainThreadOperationDelegate?
|
|
||||||
public var name: String? = "WebViewProviderReplenishQueueOperation"
|
|
||||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
|
||||||
|
|
||||||
private weak var delegate: RedditFeedProviderTokenRefreshOperationDelegate?
|
|
||||||
|
|
||||||
var error: Error?
|
|
||||||
|
|
||||||
init(delegate: RedditFeedProviderTokenRefreshOperationDelegate) {
|
|
||||||
self.delegate = delegate
|
|
||||||
}
|
|
||||||
|
|
||||||
func run() {
|
|
||||||
guard let delegate = delegate, let username = delegate.username else {
|
|
||||||
self.operationDelegate?.operationDidComplete(self)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If another operation has recently refreshed the token, we don't need to do it again
|
|
||||||
if let lastRefresh = delegate.oauthTokenLastRefresh, Date().timeIntervalSince(lastRefresh) < 120 {
|
|
||||||
self.operationDelegate?.operationDidComplete(self)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
os_log(.debug, log: self.log, "Access token expired, attempting to renew...")
|
|
||||||
|
|
||||||
delegate.oauthSwift?.renewAccessToken(withRefreshToken: delegate.oauthRefreshToken) { [weak self] result in
|
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success(let tokenSuccess):
|
|
||||||
delegate.oauthToken = tokenSuccess.credential.oauthToken
|
|
||||||
delegate.oauthRefreshToken = tokenSuccess.credential.oauthRefreshToken
|
|
||||||
do {
|
|
||||||
try RedditFeedProvider.storeCredentials(username: username, oauthToken: delegate.oauthToken, oauthRefreshToken: delegate.oauthRefreshToken)
|
|
||||||
delegate.oauthTokenLastRefresh = Date()
|
|
||||||
os_log(.debug, log: self.log, "Access token renewed.")
|
|
||||||
} catch {
|
|
||||||
self.error = error
|
|
||||||
self.operationDelegate?.operationDidComplete(self)
|
|
||||||
}
|
|
||||||
self.operationDelegate?.operationDidComplete(self)
|
|
||||||
case .failure(let oathError):
|
|
||||||
self.error = oathError
|
|
||||||
self.operationDelegate?.operationDidComplete(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditGalleryData.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 7/27/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditGalleryData: Codable {
|
|
||||||
|
|
||||||
let items: [RedditGalleryDataItem]?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case items = "items"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditGalleryDataItem: Codable {
|
|
||||||
|
|
||||||
let mediaID: String?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case mediaID = "media_id"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,173 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditLink.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/4/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
final class RedditLink: Codable {
|
|
||||||
|
|
||||||
let kind: String?
|
|
||||||
let data: RedditLinkData?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case kind = "kind"
|
|
||||||
case data = "data"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
final class RedditLinkData: Codable {
|
|
||||||
|
|
||||||
let title: String?
|
|
||||||
let permalink: String?
|
|
||||||
let url: String?
|
|
||||||
let id: String?
|
|
||||||
let subredditNamePrefixed: String?
|
|
||||||
let selfHTML: String?
|
|
||||||
let selfText: String?
|
|
||||||
let postHint: String?
|
|
||||||
let author: String?
|
|
||||||
let created: Double?
|
|
||||||
let isVideo: Bool?
|
|
||||||
let media: RedditMedia?
|
|
||||||
let mediaEmbed: RedditMediaEmbed?
|
|
||||||
let preview: RedditPreview?
|
|
||||||
let crossPostParents: [RedditLinkData]?
|
|
||||||
let galleryData: RedditGalleryData?
|
|
||||||
let mediaMetadata: [String: RedditMediaMetadata]?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case title = "title"
|
|
||||||
case permalink = "permalink"
|
|
||||||
case url = "url"
|
|
||||||
case id = "id"
|
|
||||||
case subredditNamePrefixed = "subreddit_name_prefixed"
|
|
||||||
case selfHTML = "selftext_html"
|
|
||||||
case selfText = "selftext"
|
|
||||||
case postHint = "post_hint"
|
|
||||||
case author = "author"
|
|
||||||
case created = "created_utc"
|
|
||||||
case isVideo = "is_video"
|
|
||||||
case media = "media"
|
|
||||||
case mediaEmbed = "media_embed"
|
|
||||||
case preview = "preview"
|
|
||||||
case crossPostParents = "crosspost_parent_list"
|
|
||||||
case galleryData = "gallery_data"
|
|
||||||
case mediaMetadata = "media_metadata"
|
|
||||||
}
|
|
||||||
|
|
||||||
var createdDate: Date? {
|
|
||||||
guard let created = created else { return nil }
|
|
||||||
return Date(timeIntervalSince1970: created)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderAsHTML(identifySubreddit: Bool) -> String {
|
|
||||||
var html = String()
|
|
||||||
|
|
||||||
if identifySubreddit, let subredditNamePrefixed = subredditNamePrefixed {
|
|
||||||
html += "<h3><a href=\"https://www.reddit.com/\(subredditNamePrefixed)\">\(subredditNamePrefixed)</a></h3>"
|
|
||||||
}
|
|
||||||
|
|
||||||
if let parent = crossPostParents?.first {
|
|
||||||
html += "<blockquote>"
|
|
||||||
if let subreddit = parent.subredditNamePrefixed {
|
|
||||||
html += "<p><a href=\"https://www.reddit.com/\(subreddit)\">\(subreddit)</a></p>"
|
|
||||||
}
|
|
||||||
let parentHTML = parent.renderAsHTML(identifySubreddit: false)
|
|
||||||
if parentHTML.isEmpty {
|
|
||||||
html += renderURLAsHTML()
|
|
||||||
} else {
|
|
||||||
html += parentHTML
|
|
||||||
}
|
|
||||||
html += "</blockquote>"
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
if let selfHTML = selfHTML {
|
|
||||||
html += selfHTML
|
|
||||||
}
|
|
||||||
html += renderURLAsHTML()
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderURLAsHTML() -> String {
|
|
||||||
guard let url = url else { return "" }
|
|
||||||
|
|
||||||
if url.hasSuffix(".gif") {
|
|
||||||
return "<img class=\"nnw-nozoom\" src=\"\(url)\">"
|
|
||||||
}
|
|
||||||
|
|
||||||
if isVideo ?? false, let videoURL = media?.video?.hlsURL {
|
|
||||||
var html = "<figure><video "
|
|
||||||
if let previewImageURL = preview?.images?.first?.source?.url {
|
|
||||||
html += "poster=\"\(previewImageURL)\" "
|
|
||||||
}
|
|
||||||
if let width = media?.video?.width, let height = media?.video?.height {
|
|
||||||
html += "width=\"\(width)\" height=\"\(height)\" "
|
|
||||||
}
|
|
||||||
html += "src=\"\(videoURL)\"></video></figure>"
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
if let imageVariantURL = preview?.images?.first?.variants?.mp4?.source?.url {
|
|
||||||
var html = "<figure><video class=\"nnwAnimatedGIF\" "
|
|
||||||
if let previewImageURL = preview?.images?.first?.source?.url {
|
|
||||||
html += "poster=\"\(previewImageURL)\" "
|
|
||||||
}
|
|
||||||
if let width = preview?.images?.first?.variants?.mp4?.source?.width, let height = preview?.images?.first?.variants?.mp4?.source?.height {
|
|
||||||
html += "width=\"\(width)\" height=\"\(height)\" "
|
|
||||||
}
|
|
||||||
html += "src=\"\(imageVariantURL)\" autoplay muted loop></video></figure>"
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
if let videoPreviewURL = preview?.videoPreview?.url {
|
|
||||||
var html = "<figure><video class=\"nnwAnimatedGIF\" "
|
|
||||||
if let previewImageURL = preview?.images?.first?.source?.url {
|
|
||||||
html += "poster=\"\(previewImageURL)\" "
|
|
||||||
}
|
|
||||||
if let width = preview?.videoPreview?.width, let height = preview?.videoPreview?.height {
|
|
||||||
html += "width=\"\(width)\" height=\"\(height)\" "
|
|
||||||
}
|
|
||||||
html += "src=\"\(videoPreviewURL)\" autoplay muted loop></video></figure>"
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
if !url.hasPrefix("https://imgur.com"), let mediaEmbedContent = mediaEmbed?.content {
|
|
||||||
return mediaEmbedContent
|
|
||||||
}
|
|
||||||
|
|
||||||
if let imageSource = preview?.images?.first?.source, let imageURL = imageSource.url {
|
|
||||||
var html = "<figure><img src=\"\(imageURL)\" "
|
|
||||||
if postHint == "link" {
|
|
||||||
html += "class=\"nnw-nozoom\" "
|
|
||||||
}
|
|
||||||
if let width = imageSource.width, let height = imageSource.height {
|
|
||||||
html += "width=\"\(width)\" height=\"\(height)\" "
|
|
||||||
}
|
|
||||||
html += "></figure>"
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
if let galleryDataItems = galleryData?.items, let mediaMetadata = mediaMetadata {
|
|
||||||
var html = ""
|
|
||||||
for item in galleryDataItems {
|
|
||||||
if let mediaID = item.mediaID, let itemMetadata = mediaMetadata[mediaID], let imageURL = itemMetadata.image?.url {
|
|
||||||
html += "<figure><img src=\"\(imageURL)\" "
|
|
||||||
if let width = itemMetadata.image?.width, let height = itemMetadata.image?.height {
|
|
||||||
html += "width=\"\(width)\" height=\"\(height)\" "
|
|
||||||
}
|
|
||||||
html += "></figure>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditLinkListing.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/3/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditLinkListing: Codable {
|
|
||||||
|
|
||||||
let kind: String?
|
|
||||||
let data: RedditLinkListingData?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case kind
|
|
||||||
case data
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditLinkListingData: Codable {
|
|
||||||
|
|
||||||
let children: [RedditLink]?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case children = "children"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditMe.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/4/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditMe: Codable {
|
|
||||||
|
|
||||||
let name: String?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case name = "name"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditMedia.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/4/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditMedia: Codable {
|
|
||||||
|
|
||||||
let video: RedditVideo?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case video = "reddit_video"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditVideo: Codable {
|
|
||||||
|
|
||||||
let fallbackURL: String?
|
|
||||||
let hlsURL: String?
|
|
||||||
let height: Int?
|
|
||||||
let width: Int?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case fallbackURL = "fallback_url"
|
|
||||||
case hlsURL = "hls_url"
|
|
||||||
case height = "height"
|
|
||||||
case width = "width"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditMediaEmbed.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/4/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditMediaEmbed: Codable {
|
|
||||||
|
|
||||||
let content: String?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case content = "content"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditMediaMetadata.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 7/27/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditMediaMetadata: Codable {
|
|
||||||
|
|
||||||
let image: RedditMediaMetadataS?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case image = "s"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditMediaMetadataS: Codable {
|
|
||||||
|
|
||||||
let url: String?
|
|
||||||
let height: Int?
|
|
||||||
let width: Int?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case url = "u"
|
|
||||||
case height = "y"
|
|
||||||
case width = "x"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditPreview.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/5/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditPreview: Codable {
|
|
||||||
|
|
||||||
let images: [RedditPreviewImage]?
|
|
||||||
let videoPreview: RedditVideoPreview?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case images = "images"
|
|
||||||
case videoPreview = "reddit_video_preview"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditPreviewImage: Codable {
|
|
||||||
|
|
||||||
let source: RedditPreviewImageSource?
|
|
||||||
let variants: RedditPreviewImageVariants?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case source = "source"
|
|
||||||
case variants = "variants"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditPreviewImageSource: Codable {
|
|
||||||
|
|
||||||
let url: String?
|
|
||||||
let width: Int?
|
|
||||||
let height: Int?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case url = "url"
|
|
||||||
case width = "width"
|
|
||||||
case height = "height"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditPreviewImageVariants: Codable {
|
|
||||||
|
|
||||||
let mp4: RedditPreviewImageVariantsMP4?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case mp4 = "mp4"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditPreviewImageVariantsMP4: Codable {
|
|
||||||
|
|
||||||
let source: RedditPreviewImageVariantsMP4Source?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case source = "source"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditPreviewImageVariantsMP4Source: Codable {
|
|
||||||
|
|
||||||
let url: String?
|
|
||||||
let width: Int?
|
|
||||||
let height: Int?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case url = "url"
|
|
||||||
case width = "width"
|
|
||||||
case height = "height"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditVideoPreview: Codable {
|
|
||||||
|
|
||||||
let url: String?
|
|
||||||
let width: Int?
|
|
||||||
let height: Int?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case url = "fallback_url"
|
|
||||||
case width = "width"
|
|
||||||
case height = "height"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditSort.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/7/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public enum RedditSort: String, CaseIterable {
|
|
||||||
case best
|
|
||||||
case rising
|
|
||||||
case hot
|
|
||||||
case new
|
|
||||||
case top
|
|
||||||
|
|
||||||
var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .best:
|
|
||||||
return NSLocalizedString("Best", comment: "Best")
|
|
||||||
case .rising:
|
|
||||||
return NSLocalizedString("Rising", comment: "Rising")
|
|
||||||
case .hot:
|
|
||||||
return NSLocalizedString("Hot", comment: "Hot")
|
|
||||||
case .new:
|
|
||||||
return NSLocalizedString("New", comment: "New")
|
|
||||||
case .top:
|
|
||||||
return NSLocalizedString("Top", comment: "Top")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditSubreddit.swift
|
|
||||||
// Account
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/4/20.
|
|
||||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct RedditSubreddit: Codable {
|
|
||||||
|
|
||||||
let kind: String?
|
|
||||||
let data: RedditSubredditData?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case kind = "kind"
|
|
||||||
case data = "data"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RedditSubredditData: Codable {
|
|
||||||
|
|
||||||
let displayName: String?
|
|
||||||
let iconImg: String?
|
|
||||||
let communityIcon: String?
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case displayName = "display_name_prefixed"
|
|
||||||
case iconImg = "icon_img"
|
|
||||||
case communityIcon = "community_icon"
|
|
||||||
}
|
|
||||||
|
|
||||||
var iconURL: String? {
|
|
||||||
if let communityIcon = communityIcon, !communityIcon.isEmpty {
|
|
||||||
return communityIcon
|
|
||||||
} else {
|
|
||||||
return iconImg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -256,9 +256,6 @@
|
|||||||
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; };
|
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; };
|
||||||
51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
|
51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
|
||||||
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
|
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
|
||||||
5193CD58245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
|
|
||||||
5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
|
|
||||||
5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
|
|
||||||
5195C1DA2720205F00888867 /* ShadowTableChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1D92720205F00888867 /* ShadowTableChanges.swift */; };
|
5195C1DA2720205F00888867 /* ShadowTableChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1D92720205F00888867 /* ShadowTableChanges.swift */; };
|
||||||
5195C1DC2720BD3000888867 /* MasterFeedRowIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */; };
|
5195C1DC2720BD3000888867 /* MasterFeedRowIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */; };
|
||||||
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; };
|
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; };
|
||||||
@ -1246,7 +1243,6 @@
|
|||||||
51934CC1230F5963006127BE /* InteractiveNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveNavigationController.swift; sourceTree = "<group>"; };
|
51934CC1230F5963006127BE /* InteractiveNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveNavigationController.swift; sourceTree = "<group>"; };
|
||||||
51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; };
|
51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; };
|
||||||
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; };
|
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; };
|
||||||
5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RedditFeedProvider-Extensions.swift"; sourceTree = "<group>"; };
|
|
||||||
5195C1D92720205F00888867 /* ShadowTableChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowTableChanges.swift; sourceTree = "<group>"; };
|
5195C1D92720205F00888867 /* ShadowTableChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowTableChanges.swift; sourceTree = "<group>"; };
|
||||||
5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedRowIdentifier.swift; sourceTree = "<group>"; };
|
5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedRowIdentifier.swift; sourceTree = "<group>"; };
|
||||||
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
|
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
|
||||||
@ -1807,7 +1803,6 @@
|
|||||||
515A50E5243D07A90089E588 /* ExtensionPointManager.swift */,
|
515A50E5243D07A90089E588 /* ExtensionPointManager.swift */,
|
||||||
84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */,
|
84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */,
|
||||||
84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */,
|
84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */,
|
||||||
5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */,
|
|
||||||
);
|
);
|
||||||
path = ExtensionPoints;
|
path = ExtensionPoints;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -3860,7 +3855,6 @@
|
|||||||
65ED3FE3235DEF6C0081F399 /* ArticleUtilities.swift in Sources */,
|
65ED3FE3235DEF6C0081F399 /* ArticleUtilities.swift in Sources */,
|
||||||
65ED3FE4235DEF6C0081F399 /* NNW3OpenPanelAccessoryViewController.swift in Sources */,
|
65ED3FE4235DEF6C0081F399 /* NNW3OpenPanelAccessoryViewController.swift in Sources */,
|
||||||
65ED3FE5235DEF6C0081F399 /* DefaultFeedsImporter.swift in Sources */,
|
65ED3FE5235DEF6C0081F399 /* DefaultFeedsImporter.swift in Sources */,
|
||||||
5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */,
|
|
||||||
65ED3FE6235DEF6C0081F399 /* RenameWindowController.swift in Sources */,
|
65ED3FE6235DEF6C0081F399 /* RenameWindowController.swift in Sources */,
|
||||||
65ED3FE7235DEF6C0081F399 /* SendToMicroBlogCommand.swift in Sources */,
|
65ED3FE7235DEF6C0081F399 /* SendToMicroBlogCommand.swift in Sources */,
|
||||||
65ED3FE8235DEF6C0081F399 /* ArticleTheme.swift in Sources */,
|
65ED3FE8235DEF6C0081F399 /* ArticleTheme.swift in Sources */,
|
||||||
@ -4041,7 +4035,6 @@
|
|||||||
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
|
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
|
||||||
5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */,
|
5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */,
|
||||||
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
|
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
|
||||||
5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */,
|
|
||||||
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
|
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
|
||||||
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
|
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
|
||||||
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
||||||
@ -4273,7 +4266,6 @@
|
|||||||
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */,
|
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */,
|
||||||
5183CCE8226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
|
5183CCE8226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
|
||||||
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
|
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
|
||||||
5193CD58245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */,
|
|
||||||
51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
|
51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
|
||||||
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
|
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
|
||||||
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
|
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
|
||||||
|
@ -11,28 +11,25 @@ import Account
|
|||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
enum ExtensionPointIdentifer: Hashable {
|
enum ExtensionPointIdentifer: Hashable {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case marsEdit
|
case marsEdit
|
||||||
case microblog
|
case microblog
|
||||||
#endif
|
#endif
|
||||||
case reddit(String)
|
|
||||||
|
|
||||||
var extensionPointType: ExtensionPoint.Type {
|
var extensionPointType: ExtensionPoint.Type {
|
||||||
switch self {
|
switch self {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case .marsEdit:
|
case .marsEdit:
|
||||||
return SendToMarsEditCommand.self
|
return SendToMarsEditCommand.self
|
||||||
case .microblog:
|
case .microblog:
|
||||||
return SendToMicroBlogCommand.self
|
return SendToMicroBlogCommand.self
|
||||||
#endif
|
#endif
|
||||||
case .reddit:
|
|
||||||
return RedditFeedProvider.self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var userInfo: [AnyHashable: AnyHashable] {
|
public var userInfo: [AnyHashable: AnyHashable] {
|
||||||
switch self {
|
switch self {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case .marsEdit:
|
case .marsEdit:
|
||||||
return [
|
return [
|
||||||
"type": "marsEdit"
|
"type": "marsEdit"
|
||||||
@ -41,12 +38,7 @@ enum ExtensionPointIdentifer: Hashable {
|
|||||||
return [
|
return [
|
||||||
"type": "microblog"
|
"type": "microblog"
|
||||||
]
|
]
|
||||||
#endif
|
#endif
|
||||||
case .reddit(let username):
|
|
||||||
return [
|
|
||||||
"type": "reddit",
|
|
||||||
"username": username
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,15 +46,12 @@ enum ExtensionPointIdentifer: Hashable {
|
|||||||
guard let type = userInfo["type"] as? String else { return nil }
|
guard let type = userInfo["type"] as? String else { return nil }
|
||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case "marsEdit":
|
case "marsEdit":
|
||||||
self = ExtensionPointIdentifer.marsEdit
|
self = ExtensionPointIdentifer.marsEdit
|
||||||
case "microblog":
|
case "microblog":
|
||||||
self = ExtensionPointIdentifer.microblog
|
self = ExtensionPointIdentifer.microblog
|
||||||
#endif
|
#endif
|
||||||
case "reddit":
|
|
||||||
guard let username = userInfo["username"] as? String else { return nil }
|
|
||||||
self = ExtensionPointIdentifer.reddit(username)
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -70,16 +59,12 @@ enum ExtensionPointIdentifer: Hashable {
|
|||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
case .marsEdit:
|
case .marsEdit:
|
||||||
hasher.combine("marsEdit")
|
hasher.combine("marsEdit")
|
||||||
case .microblog:
|
case .microblog:
|
||||||
hasher.combine("microblog")
|
hasher.combine("microblog")
|
||||||
#endif
|
#endif
|
||||||
case .reddit(let username):
|
|
||||||
hasher.combine("reddit")
|
|
||||||
hasher.combine(username)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -69,12 +69,8 @@ final class ExtensionPointManager: FeedProviderManagerDelegate {
|
|||||||
return activeExtensionPoints.values.compactMap({ return $0 as? FeedProvider })
|
return activeExtensionPoints.values.compactMap({ return $0 as? FeedProvider })
|
||||||
}
|
}
|
||||||
|
|
||||||
var isRedditEnabled: Bool {
|
|
||||||
return activeExtensionPoints.values.contains(where: { $0 is RedditFeedProvider })
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
possibleExtensionPointTypes = [RedditFeedProvider.self]
|
possibleExtensionPointTypes = []
|
||||||
loadExtensionPoints()
|
loadExtensionPoints()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,19 +113,6 @@ private extension ExtensionPointManager {
|
|||||||
|
|
||||||
func extensionPoint(for extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess?, completion: @escaping (Result<ExtensionPoint, Error>) -> Void) {
|
func extensionPoint(for extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess?, completion: @escaping (Result<ExtensionPoint, Error>) -> Void) {
|
||||||
switch extensionPointType {
|
switch extensionPointType {
|
||||||
case is RedditFeedProvider.Type:
|
|
||||||
if let tokenSuccess = tokenSuccess {
|
|
||||||
RedditFeedProvider.create(tokenSuccess: tokenSuccess) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let reddit):
|
|
||||||
completion(.success(reddit))
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completion(.failure(ExtensionPointManagerError.unableToCreate))
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -137,8 +120,6 @@ private extension ExtensionPointManager {
|
|||||||
|
|
||||||
func extensionPoint(for extensionPointID: ExtensionPointIdentifer) -> ExtensionPoint? {
|
func extensionPoint(for extensionPointID: ExtensionPointIdentifer) -> ExtensionPoint? {
|
||||||
switch extensionPointID {
|
switch extensionPointID {
|
||||||
case .reddit(let username):
|
|
||||||
return RedditFeedProvider(username: username)
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
//
|
|
||||||
// RedditFeedProvider-Extensions.swift
|
|
||||||
// NetNewsWire
|
|
||||||
//
|
|
||||||
// Created by Maurice Parker on 5/2/20.
|
|
||||||
// Copyright © 2020 Ranchero Software. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Account
|
|
||||||
|
|
||||||
extension RedditFeedProvider: ExtensionPoint {
|
|
||||||
|
|
||||||
static var isSinglton = false
|
|
||||||
static var isDeveloperBuildRestricted = true
|
|
||||||
static var title = NSLocalizedString("Reddit", comment: "Reddit")
|
|
||||||
static var image = AppAssets.extensionPointReddit
|
|
||||||
static var description: NSAttributedString = {
|
|
||||||
return RedditFeedProvider.makeAttrString("This extension enables you to subscribe to Reddit URLs as if they were RSS feeds. It only works with \(Account.defaultLocalAccountName) or iCloud accounts.")
|
|
||||||
}()
|
|
||||||
|
|
||||||
var extensionPointID: ExtensionPointIdentifer {
|
|
||||||
guard let username = username else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
return ExtensionPointIdentifer.reddit(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
guard let username = username else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
return "u/\(username)"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user