Merge branch 'ios-release' of https://github.com/Ranchero-Software/NetNewsWire into ios-release
This commit is contained in:
commit
1306db6c69
|
@ -279,7 +279,19 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||||
webFeedMetadataFile.load()
|
webFeedMetadataFile.load()
|
||||||
opmlFile.load()
|
opmlFile.load()
|
||||||
|
|
||||||
|
var shouldHandleRetentionPolicyChange = false
|
||||||
|
if type == .onMyMac {
|
||||||
|
let didHandlePolicyChange = metadata.performedApril2020RetentionPolicyChange ?? false
|
||||||
|
shouldHandleRetentionPolicyChange = !didHandlePolicyChange
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
if shouldHandleRetentionPolicyChange {
|
||||||
|
// Handle one-time database changes made necessary by April 2020 retention policy change.
|
||||||
|
self.database.performApril2020RetentionPolicyChange()
|
||||||
|
self.metadata.performedApril2020RetentionPolicyChange = true
|
||||||
|
}
|
||||||
|
|
||||||
self.database.cleanupDatabaseAtStartup(subscribedToWebFeedIDs: self.flattenedWebFeeds().webFeedIDs())
|
self.database.cleanupDatabaseAtStartup(subscribedToWebFeedIDs: self.flattenedWebFeeds().webFeedIDs())
|
||||||
self.fetchAllUnreadCounts()
|
self.fetchAllUnreadCounts()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ final class AccountMetadata: Codable {
|
||||||
case lastArticleFetchStartTime = "lastArticleFetch"
|
case lastArticleFetchStartTime = "lastArticleFetch"
|
||||||
case lastArticleFetchEndTime
|
case lastArticleFetchEndTime
|
||||||
case endpointURL
|
case endpointURL
|
||||||
|
case lastCredentialRenewTime = "lastCredentialRenewTime"
|
||||||
|
case performedApril2020RetentionPolicyChange
|
||||||
}
|
}
|
||||||
|
|
||||||
var name: String? {
|
var name: String? {
|
||||||
|
@ -80,6 +82,24 @@ final class AccountMetadata: Codable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The last moment an account successfully renewed its credentials, or `nil` if no such moment exists.
|
||||||
|
/// An account delegate can use this value to decide when to next ask the service provider to renew credentials.
|
||||||
|
var lastCredentialRenewTime: Date? {
|
||||||
|
didSet {
|
||||||
|
if lastCredentialRenewTime != oldValue {
|
||||||
|
valueDidChange(.lastCredentialRenewTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var performedApril2020RetentionPolicyChange: Bool? {
|
||||||
|
didSet {
|
||||||
|
if performedApril2020RetentionPolicyChange != oldValue {
|
||||||
|
valueDidChange(.performedApril2020RetentionPolicyChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
weak var delegate: AccountMetadataDelegate?
|
weak var delegate: AccountMetadataDelegate?
|
||||||
|
|
||||||
|
|
|
@ -111,11 +111,20 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
let log = self.log
|
let log = self.log
|
||||||
let operation = FeedlySyncAllOperation(account: account, credentials: credentials, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress, log: log)
|
|
||||||
|
|
||||||
operation.downloadProgress = refreshProgress
|
let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, refreshDate: Date(), log: log)
|
||||||
|
refreshAccessToken.downloadProgress = refreshProgress
|
||||||
|
operationQueue.add(refreshAccessToken)
|
||||||
|
|
||||||
|
let syncAllOperation = FeedlySyncAllOperation(account: account, feedlyUserId: credentials.username, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress, log: log)
|
||||||
|
|
||||||
|
syncAllOperation.downloadProgress = refreshProgress
|
||||||
|
|
||||||
|
// Ensure the sync uses the latest credential.
|
||||||
|
syncAllOperation.addDependency(refreshAccessToken)
|
||||||
|
|
||||||
let date = Date()
|
let date = Date()
|
||||||
operation.syncCompletionHandler = { [weak self] result in
|
syncAllOperation.syncCompletionHandler = { [weak self] result in
|
||||||
if case .success = result {
|
if case .success = result {
|
||||||
self?.accountMetadata?.lastArticleFetchStartTime = date
|
self?.accountMetadata?.lastArticleFetchStartTime = date
|
||||||
self?.accountMetadata?.lastArticleFetchEndTime = Date()
|
self?.accountMetadata?.lastArticleFetchEndTime = Date()
|
||||||
|
@ -125,9 +134,9 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
completion(result)
|
completion(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSyncAllOperation = operation
|
currentSyncAllOperation = syncAllOperation
|
||||||
|
|
||||||
operationQueue.add(operation)
|
operationQueue.add(syncAllOperation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||||
|
@ -155,7 +164,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: caller, database: database, newerThan: nil, log: log)
|
let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: credentials.username, service: caller, database: database, newerThan: nil, log: log)
|
||||||
|
|
||||||
group.enter()
|
group.enter()
|
||||||
ingestUnread.completionBlock = { _ in
|
ingestUnread.completionBlock = { _ in
|
||||||
|
@ -163,7 +172,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: caller, database: database, newerThan: nil, log: log)
|
let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, userId: credentials.username, service: caller, database: database, newerThan: nil, log: log)
|
||||||
|
|
||||||
group.enter()
|
group.enter()
|
||||||
ingestStarred.completionBlock = { _ in
|
ingestStarred.completionBlock = { _ in
|
||||||
|
@ -492,9 +501,6 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
func accountDidInitialize(_ account: Account) {
|
func accountDidInitialize(_ account: Account) {
|
||||||
credentials = try? account.retrieveCredentials(type: .oauthAccessToken)
|
credentials = try? account.retrieveCredentials(type: .oauthAccessToken)
|
||||||
|
|
||||||
let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, log: log)
|
|
||||||
operationQueue.add(refreshAccessToken)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountWillBeDeleted(_ account: Account) {
|
func accountWillBeDeleted(_ account: Account) {
|
||||||
|
|
|
@ -89,7 +89,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
|
||||||
createFeeds.downloadProgress = downloadProgress
|
createFeeds.downloadProgress = downloadProgress
|
||||||
operationQueue.add(createFeeds)
|
operationQueue.add(createFeeds)
|
||||||
|
|
||||||
let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, database: database, newerThan: nil, log: log)
|
let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: credentials.username, service: syncUnreadIdsService, database: database, newerThan: nil, log: log)
|
||||||
syncUnread.addDependency(createFeeds)
|
syncUnread.addDependency(createFeeds)
|
||||||
syncUnread.downloadProgress = downloadProgress
|
syncUnread.downloadProgress = downloadProgress
|
||||||
syncUnread.delegate = self
|
syncUnread.delegate = self
|
||||||
|
|
|
@ -29,8 +29,8 @@ class FeedlyGetUpdatedArticleIdsOperation: FeedlyOperation, FeedlyEntryIdentifie
|
||||||
self.log = log
|
self.log = log
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) {
|
||||||
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
let all = FeedlyCategoryResourceId.Global.all(for: userId)
|
||||||
self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log)
|
self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,8 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation {
|
||||||
private var remoteEntryIds = Set<String>()
|
private var remoteEntryIds = Set<String>()
|
||||||
private let log: OSLog
|
private let log: OSLog
|
||||||
|
|
||||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
||||||
let resource = FeedlyTagResourceId.Global.saved(for: credentials.username)
|
let resource = FeedlyTagResourceId.Global.saved(for: userId)
|
||||||
self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log)
|
self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation {
|
||||||
self.log = log
|
self.log = log
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, log: OSLog) {
|
convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, log: OSLog) {
|
||||||
let all = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
let all = FeedlyCategoryResourceId.Global.all(for: userId)
|
||||||
self.init(account: account, resource: all, service: service, log: log)
|
self.init(account: account, resource: all, service: service, log: log)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,8 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation {
|
||||||
private var remoteEntryIds = Set<String>()
|
private var remoteEntryIds = Set<String>()
|
||||||
private let log: OSLog
|
private let log: OSLog
|
||||||
|
|
||||||
convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
||||||
let resource = FeedlyCategoryResourceId.Global.all(for: credentials.username)
|
let resource = FeedlyCategoryResourceId.Global.all(for: userId)
|
||||||
self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log)
|
self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,14 +17,32 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation {
|
||||||
let account: Account
|
let account: Account
|
||||||
let log: OSLog
|
let log: OSLog
|
||||||
|
|
||||||
init(account: Account, service: OAuthAccessTokenRefreshing, oauthClient: OAuthAuthorizationClient, log: OSLog) {
|
/// The moment the refresh is being requested. The token will refresh only if the account's `lastCredentialRenewTime` is not on the same day as this moment. When nil, the operation will always refresh the token.
|
||||||
|
let refreshDate: Date?
|
||||||
|
|
||||||
|
init(account: Account, service: OAuthAccessTokenRefreshing, oauthClient: OAuthAuthorizationClient, refreshDate: Date?, log: OSLog) {
|
||||||
self.oauthClient = oauthClient
|
self.oauthClient = oauthClient
|
||||||
self.service = service
|
self.service = service
|
||||||
self.account = account
|
self.account = account
|
||||||
|
self.refreshDate = refreshDate
|
||||||
self.log = log
|
self.log = log
|
||||||
}
|
}
|
||||||
|
|
||||||
override func run() {
|
override func run() {
|
||||||
|
// Only refresh the token if these dates are not on the same day.
|
||||||
|
let shouldRefresh: Bool = {
|
||||||
|
guard let date = refreshDate, let lastRenewDate = account.metadata.lastCredentialRenewTime else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !Calendar.current.isDate(lastRenewDate, equalTo: date, toGranularity: .day)
|
||||||
|
}()
|
||||||
|
|
||||||
|
guard shouldRefresh else {
|
||||||
|
os_log(.debug, log: log, "Skipping access token renewal.")
|
||||||
|
didFinish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let refreshToken: Credentials
|
let refreshToken: Credentials
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -64,6 +82,8 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation {
|
||||||
// Now store the access token because we want the account delegate to use it.
|
// Now store the access token because we want the account delegate to use it.
|
||||||
try account.storeCredentials(grant.accessToken)
|
try account.storeCredentials(grant.accessToken)
|
||||||
|
|
||||||
|
account.metadata.lastCredentialRenewTime = Date()
|
||||||
|
|
||||||
didFinish()
|
didFinish()
|
||||||
} catch {
|
} catch {
|
||||||
didFinish(with: error)
|
didFinish(with: error)
|
||||||
|
|
|
@ -33,7 +33,7 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
||||||
///
|
///
|
||||||
/// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync).
|
/// Download articles for statuses at the union of those statuses without its corresponding article and those included in 3 (changed since last successful sync).
|
||||||
///
|
///
|
||||||
init(account: Account, credentials: Credentials, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
|
init(account: Account, feedlyUserId: String, lastSuccessfulFetchStartDate: Date?, markArticlesService: FeedlyMarkArticlesService, getUnreadService: FeedlyGetStreamIdsService, getCollectionsService: FeedlyGetCollectionsService, getStreamContentsService: FeedlyGetStreamContentsService, getStarredService: FeedlyGetStreamIdsService, getStreamIdsService: FeedlyGetStreamIdsService, getEntriesService: FeedlyGetEntriesService, database: SyncDatabase, downloadProgress: DownloadProgress, log: OSLog) {
|
||||||
self.syncUUID = UUID()
|
self.syncUUID = UUID()
|
||||||
self.log = log
|
self.log = log
|
||||||
self.operationQueue.suspend()
|
self.operationQueue.suspend()
|
||||||
|
@ -53,7 +53,7 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
||||||
getCollections.delegate = self
|
getCollections.delegate = self
|
||||||
getCollections.downloadProgress = downloadProgress
|
getCollections.downloadProgress = downloadProgress
|
||||||
getCollections.addDependency(sendArticleStatuses)
|
getCollections.addDependency(sendArticleStatuses)
|
||||||
self.operationQueue.add(getCollections)
|
self.operationQueue.add(getCollections)
|
||||||
|
|
||||||
// Ensure a folder exists for each Collection, removing Folders without a corresponding Collection.
|
// Ensure a folder exists for each Collection, removing Folders without a corresponding Collection.
|
||||||
let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: log)
|
let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: log)
|
||||||
|
@ -67,14 +67,14 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
||||||
createFeedsOperation.addDependency(mirrorCollectionsAsFolders)
|
createFeedsOperation.addDependency(mirrorCollectionsAsFolders)
|
||||||
self.operationQueue.add(createFeedsOperation)
|
self.operationQueue.add(createFeedsOperation)
|
||||||
|
|
||||||
let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, log: log)
|
let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, userId: feedlyUserId, service: getStreamIdsService, log: log)
|
||||||
getAllArticleIds.delegate = self
|
getAllArticleIds.delegate = self
|
||||||
getAllArticleIds.downloadProgress = downloadProgress
|
getAllArticleIds.downloadProgress = downloadProgress
|
||||||
getAllArticleIds.addDependency(createFeedsOperation)
|
getAllArticleIds.addDependency(createFeedsOperation)
|
||||||
self.operationQueue.add(getAllArticleIds)
|
self.operationQueue.add(getAllArticleIds)
|
||||||
|
|
||||||
// Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default).
|
// Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default).
|
||||||
let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: getUnreadService, database: database, newerThan: nil, log: log)
|
let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: feedlyUserId, service: getUnreadService, database: database, newerThan: nil, log: log)
|
||||||
getUnread.delegate = self
|
getUnread.delegate = self
|
||||||
getUnread.addDependency(getAllArticleIds)
|
getUnread.addDependency(getAllArticleIds)
|
||||||
getUnread.downloadProgress = downloadProgress
|
getUnread.downloadProgress = downloadProgress
|
||||||
|
@ -82,14 +82,14 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
||||||
|
|
||||||
// Get each page of the article ids which have been update since the last successful fetch start date.
|
// Get each page of the article ids which have been update since the last successful fetch start date.
|
||||||
// If the date is nil, this operation provides an empty set (everything is new, nothing is updated).
|
// If the date is nil, this operation provides an empty set (everything is new, nothing is updated).
|
||||||
let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, credentials: credentials, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log)
|
let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, userId: feedlyUserId, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log)
|
||||||
getUpdated.delegate = self
|
getUpdated.delegate = self
|
||||||
getUpdated.downloadProgress = downloadProgress
|
getUpdated.downloadProgress = downloadProgress
|
||||||
getUpdated.addDependency(createFeedsOperation)
|
getUpdated.addDependency(createFeedsOperation)
|
||||||
self.operationQueue.add(getUpdated)
|
self.operationQueue.add(getUpdated)
|
||||||
|
|
||||||
// Get each page of the article ids for starred articles.
|
// Get each page of the article ids for starred articles.
|
||||||
let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, credentials: credentials, service: getStarredService, database: database, newerThan: nil, log: log)
|
let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, userId: feedlyUserId, service: getStarredService, database: database, newerThan: nil, log: log)
|
||||||
getStarred.delegate = self
|
getStarred.delegate = self
|
||||||
getStarred.downloadProgress = downloadProgress
|
getStarred.downloadProgress = downloadProgress
|
||||||
getStarred.addDependency(createFeedsOperation)
|
getStarred.addDependency(createFeedsOperation)
|
||||||
|
@ -125,8 +125,8 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
||||||
self.operationQueue.add(finishOperation)
|
self.operationQueue.add(finishOperation)
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(account: Account, credentials: Credentials, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) {
|
convenience init(account: Account, feedlyUserId: String, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) {
|
||||||
self.init(account: account, credentials: credentials, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log)
|
self.init(account: account, feedlyUserId: feedlyUserId, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func run() {
|
override func run() {
|
||||||
|
|
|
@ -146,7 +146,7 @@ public final class ArticlesDatabase {
|
||||||
|
|
||||||
/// Fetch all non-zero unread counts.
|
/// Fetch all non-zero unread counts.
|
||||||
public func fetchAllUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
public func fetchAllUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
||||||
let operation = FetchAllUnreadCountsOperation(databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
|
let operation = FetchAllUnreadCountsOperation(databaseQueue: queue)
|
||||||
operationQueue.cancelOperations(named: operation.name!)
|
operationQueue.cancelOperations(named: operation.name!)
|
||||||
operation.completionBlock = { operation in
|
operation.completionBlock = { operation in
|
||||||
let fetchOperation = operation as! FetchAllUnreadCountsOperation
|
let fetchOperation = operation as! FetchAllUnreadCountsOperation
|
||||||
|
@ -167,7 +167,7 @@ public final class ArticlesDatabase {
|
||||||
|
|
||||||
/// Fetch non-zero unread counts for given webFeedIDs.
|
/// Fetch non-zero unread counts for given webFeedIDs.
|
||||||
public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
public func fetchUnreadCounts(for webFeedIDs: Set<String>, _ completion: @escaping UnreadCountDictionaryCompletionBlock) {
|
||||||
let operation = FetchUnreadCountsForFeedsOperation(webFeedIDs: webFeedIDs, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate)
|
let operation = FetchUnreadCountsForFeedsOperation(webFeedIDs: webFeedIDs, databaseQueue: queue)
|
||||||
operation.completionBlock = { operation in
|
operation.completionBlock = { operation in
|
||||||
let fetchOperation = operation as! FetchUnreadCountsForFeedsOperation
|
let fetchOperation = operation as! FetchUnreadCountsForFeedsOperation
|
||||||
completion(fetchOperation.result)
|
completion(fetchOperation.result)
|
||||||
|
@ -265,14 +265,35 @@ public final class ArticlesDatabase {
|
||||||
|
|
||||||
// MARK: - Cleanup
|
// MARK: - Cleanup
|
||||||
|
|
||||||
// These are to be used only at startup. These are to prevent the database from growing forever.
|
/// Calls the various clean-up functions. To be used only at startup.
|
||||||
|
///
|
||||||
/// Calls the various clean-up functions.
|
/// This prevents the database from growing forever. If we didn’t do this:
|
||||||
|
/// 1) The database would grow to an inordinate size, and
|
||||||
|
/// 2) the app would become very slow.
|
||||||
public func cleanupDatabaseAtStartup(subscribedToWebFeedIDs: Set<String>) {
|
public func cleanupDatabaseAtStartup(subscribedToWebFeedIDs: Set<String>) {
|
||||||
if retentionStyle == .syncSystem {
|
if retentionStyle == .syncSystem {
|
||||||
articlesTable.deleteOldArticles()
|
articlesTable.deleteOldArticles()
|
||||||
}
|
}
|
||||||
articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToWebFeedIDs)
|
articlesTable.deleteArticlesNotInSubscribedToFeedIDs(subscribedToWebFeedIDs)
|
||||||
|
articlesTable.deleteOldStatuses()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do database cleanups made necessary by the retention policy change in April 2020.
|
||||||
|
///
|
||||||
|
/// The retention policy for feed-based systems changed in April 2020:
|
||||||
|
/// we keep articles only for as long as they’re in the feed.
|
||||||
|
/// This change could result in a bunch of older articles suddenly
|
||||||
|
/// appearing as unread articles.
|
||||||
|
///
|
||||||
|
/// These are articles that were in the database,
|
||||||
|
/// but weren’t appearing in the UI because they were beyond the 90-day window.
|
||||||
|
/// (The previous retention policy used a 90-day window.)
|
||||||
|
///
|
||||||
|
/// This function marks everything as read that’s beyond that 90-day window.
|
||||||
|
/// It’s intended to be called only once on an account.
|
||||||
|
public func performApril2020RetentionPolicyChange() {
|
||||||
|
precondition(retentionStyle == .feedBased)
|
||||||
|
articlesTable.markOlderStatusesAsRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -181,7 +181,7 @@ final class ArticlesTable: DatabaseTable {
|
||||||
|
|
||||||
// 1. Ensure statuses for all the incoming articles.
|
// 1. Ensure statuses for all the incoming articles.
|
||||||
// 2. Create incoming articles with parsedItems.
|
// 2. Create incoming articles with parsedItems.
|
||||||
// 3. [Deleted - no longer needed]
|
// 3. [Deleted - this step is no longer needed]
|
||||||
// 4. Fetch all articles for the feed.
|
// 4. Fetch all articles for the feed.
|
||||||
// 5. Create array of Articles not in database and save them.
|
// 5. Create array of Articles not in database and save them.
|
||||||
// 6. Create array of updated Articles and save what’s changed.
|
// 6. Create array of updated Articles and save what’s changed.
|
||||||
|
@ -493,7 +493,9 @@ final class ArticlesTable: DatabaseTable {
|
||||||
/// Because deleting articles might block the database for too long,
|
/// Because deleting articles might block the database for too long,
|
||||||
/// we do this in a careful way: delete articles older than a year,
|
/// we do this in a careful way: delete articles older than a year,
|
||||||
/// check to see how much time has passed, then decide whether or not to continue.
|
/// check to see how much time has passed, then decide whether or not to continue.
|
||||||
/// Repeat for successively shorter time intervals.
|
/// Repeat for successively more-recent dates.
|
||||||
|
///
|
||||||
|
/// Returns `true` if it deleted old articles all the way up to the 90 day cutoff date.
|
||||||
func deleteOldArticles() {
|
func deleteOldArticles() {
|
||||||
precondition(retentionStyle == .syncSystem)
|
precondition(retentionStyle == .syncSystem)
|
||||||
|
|
||||||
|
@ -525,6 +527,30 @@ final class ArticlesTable: DatabaseTable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete old statuses.
|
||||||
|
func deleteOldStatuses() {
|
||||||
|
queue.runInTransaction { databaseResult in
|
||||||
|
guard let database = databaseResult.database else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql: String
|
||||||
|
let cutoffDate: Date
|
||||||
|
|
||||||
|
switch self.retentionStyle {
|
||||||
|
case .syncSystem:
|
||||||
|
sql = "delete from statuses where dateArrived<? and read=1 and starred=0 and articleID not in (select articleID from articles);"
|
||||||
|
cutoffDate = Date().bySubtracting(days: 180)
|
||||||
|
case .feedBased:
|
||||||
|
sql = "delete from statuses where dateArrived<? and starred=0 and articleID not in (select articleID from articles);"
|
||||||
|
cutoffDate = Date().bySubtracting(days: 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
let parameters = [cutoffDate] as [Any]
|
||||||
|
database.executeUpdate(sql, withArgumentsIn: parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete articles from feeds that are no longer in the current set of subscribed-to feeds.
|
/// Delete articles from feeds that are no longer in the current set of subscribed-to feeds.
|
||||||
/// This deletes from the articles and articleStatuses tables,
|
/// This deletes from the articles and articleStatuses tables,
|
||||||
/// and, via a trigger, it also deletes from the search index.
|
/// and, via a trigger, it also deletes from the search index.
|
||||||
|
@ -554,6 +580,22 @@ final class ArticlesTable: DatabaseTable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark statuses beyond the 90-day window as read.
|
||||||
|
///
|
||||||
|
/// This is not intended for wide use: this is part of implementing
|
||||||
|
/// the April 2020 retention policy change for feed-based accounts.
|
||||||
|
func markOlderStatusesAsRead() {
|
||||||
|
queue.runInDatabase { databaseResult in
|
||||||
|
guard let database = databaseResult.database else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = "update statuses set read = true where dateArrived<?;"
|
||||||
|
let parameters = [self.articleCutoffDate] as [Any]
|
||||||
|
database.executeUpdate(sql, withArgumentsIn: parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
@ -648,31 +690,11 @@ private extension ArticlesTable {
|
||||||
return cachedArticles.union(articlesWithFetchedAuthors)
|
return cachedArticles.union(articlesWithFetchedAuthors)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject], withLimits: Bool) -> Set<Article> {
|
func fetchArticlesWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]) -> Set<Article> {
|
||||||
// Don’t fetch articles that shouldn’t appear in the UI. The rules:
|
let sql = "select * from articles natural join statuses where \(whereClause);"
|
||||||
// * Must not be deleted.
|
return articlesWithSQL(sql, parameters, database)
|
||||||
// * Must be either 1) starred or 2) dateArrived must be newer than cutoff date.
|
|
||||||
|
|
||||||
if withLimits {
|
|
||||||
let sql = "select * from articles natural join statuses where \(whereClause) and (starred=1 or dateArrived>?);"
|
|
||||||
return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let sql = "select * from articles natural join statuses where \(whereClause);"
|
|
||||||
return articlesWithSQL(sql, parameters, database)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func fetchUnreadCount(_ webFeedID: String, _ database: FMDatabase) -> Int {
|
|
||||||
// // Count only the articles that would appear in the UI.
|
|
||||||
// // * Must be unread.
|
|
||||||
// // * Must not be deleted.
|
|
||||||
// // * Must be either 1) starred or 2) dateArrived must be newer than cutoff date.
|
|
||||||
//
|
|
||||||
// let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and (starred=1 or dateArrived>?);"
|
|
||||||
// return numberWithSQLAndParameters(sql, [webFeedID, articleCutoffDate], in: database)
|
|
||||||
// }
|
|
||||||
|
|
||||||
func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set<Article> {
|
func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set<Article> {
|
||||||
let sql = "select rowid from search where search match ?;"
|
let sql = "select rowid from search where search match ?;"
|
||||||
let sqlSearchString = sqliteSearchString(with: searchString)
|
let sqlSearchString = sqliteSearchString(with: searchString)
|
||||||
|
@ -688,7 +710,7 @@ private extension ArticlesTable {
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(searchRowIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(searchRowIDs.count))!
|
||||||
let whereClause = "searchRowID in \(placeholders)"
|
let whereClause = "searchRowID in \(placeholders)"
|
||||||
let parameters: [AnyObject] = Array(searchRowIDs) as [AnyObject]
|
let parameters: [AnyObject] = Array(searchRowIDs) as [AnyObject]
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqliteSearchString(with searchString: String) -> String {
|
func sqliteSearchString(with searchString: String) -> String {
|
||||||
|
@ -761,7 +783,7 @@ private extension ArticlesTable {
|
||||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||||
let whereClause = "feedID in \(placeholders)"
|
let whereClause = "feedID in \(placeholders)"
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||||
|
@ -772,11 +794,11 @@ private extension ArticlesTable {
|
||||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||||
let whereClause = "feedID in \(placeholders) and read=0"
|
let whereClause = "feedID in \(placeholders) and read=0"
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesForFeedID(_ webFeedID: String, withLimits: Bool, _ database: FMDatabase) -> Set<Article> {
|
func fetchArticlesForFeedID(_ webFeedID: String, withLimits: Bool, _ database: FMDatabase) -> Set<Article> {
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits)
|
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject])
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticles(articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
func fetchArticles(articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||||
|
@ -786,7 +808,7 @@ private extension ArticlesTable {
|
||||||
let parameters = articleIDs.map { $0 as AnyObject }
|
let parameters = articleIDs.map { $0 as AnyObject }
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(articleIDs.count))!
|
||||||
let whereClause = "articleID in \(placeholders)"
|
let whereClause = "articleID in \(placeholders)"
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ database: FMDatabase) -> Set<Article> {
|
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ database: FMDatabase) -> Set<Article> {
|
||||||
|
@ -799,7 +821,7 @@ private extension ArticlesTable {
|
||||||
let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject]
|
let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject]
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||||
let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))"
|
let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))"
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStarredArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
func fetchStarredArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||||
|
@ -810,7 +832,7 @@ private extension ArticlesTable {
|
||||||
let parameters = webFeedIDs.map { $0 as AnyObject }
|
let parameters = webFeedIDs.map { $0 as AnyObject }
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||||
let whereClause = "feedID in \(placeholders) and starred=1"
|
let whereClause = "feedID in \(placeholders) and starred=1"
|
||||||
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
|
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
|
||||||
|
|
|
@ -22,11 +22,9 @@ public final class FetchAllUnreadCountsOperation: MainThreadOperation {
|
||||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||||
|
|
||||||
private let queue: DatabaseQueue
|
private let queue: DatabaseQueue
|
||||||
private let cutoffDate: Date
|
|
||||||
|
|
||||||
init(databaseQueue: DatabaseQueue, cutoffDate: Date) {
|
init(databaseQueue: DatabaseQueue) {
|
||||||
self.queue = databaseQueue
|
self.queue = databaseQueue
|
||||||
self.cutoffDate = cutoffDate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func run() {
|
public func run() {
|
||||||
|
@ -49,9 +47,9 @@ public final class FetchAllUnreadCountsOperation: MainThreadOperation {
|
||||||
private extension FetchAllUnreadCountsOperation {
|
private extension FetchAllUnreadCountsOperation {
|
||||||
|
|
||||||
func fetchUnreadCounts(_ database: FMDatabase) {
|
func fetchUnreadCounts(_ database: FMDatabase) {
|
||||||
let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and (starred=1 or dateArrived>?) group by feedID;"
|
let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 group by feedID;"
|
||||||
|
|
||||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else {
|
guard let resultSet = database.executeQuery(sql, withArgumentsIn: nil) else {
|
||||||
informOperationDelegateOfCompletion()
|
informOperationDelegateOfCompletion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,9 +52,9 @@ public final class FetchFeedUnreadCountOperation: MainThreadOperation {
|
||||||
private extension FetchFeedUnreadCountOperation {
|
private extension FetchFeedUnreadCountOperation {
|
||||||
|
|
||||||
func fetchUnreadCount(_ database: FMDatabase) {
|
func fetchUnreadCount(_ database: FMDatabase) {
|
||||||
let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and (starred=1 or dateArrived>?);"
|
let sql = "select count(*) from articles natural join statuses where feedID=? and read=0;"
|
||||||
|
|
||||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [webFeedID, cutoffDate]) else {
|
guard let resultSet = database.executeQuery(sql, withArgumentsIn: [webFeedID]) else {
|
||||||
informOperationDelegateOfCompletion()
|
informOperationDelegateOfCompletion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,11 @@ public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation {
|
||||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||||
|
|
||||||
private let queue: DatabaseQueue
|
private let queue: DatabaseQueue
|
||||||
private let cutoffDate: Date
|
|
||||||
private let webFeedIDs: Set<String>
|
private let webFeedIDs: Set<String>
|
||||||
|
|
||||||
init(webFeedIDs: Set<String>, databaseQueue: DatabaseQueue, cutoffDate: Date) {
|
init(webFeedIDs: Set<String>, databaseQueue: DatabaseQueue) {
|
||||||
self.webFeedIDs = webFeedIDs
|
self.webFeedIDs = webFeedIDs
|
||||||
self.queue = databaseQueue
|
self.queue = databaseQueue
|
||||||
self.cutoffDate = cutoffDate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func run() {
|
public func run() {
|
||||||
|
@ -53,11 +51,9 @@ private extension FetchUnreadCountsForFeedsOperation {
|
||||||
|
|
||||||
func fetchUnreadCounts(_ database: FMDatabase) {
|
func fetchUnreadCounts(_ database: FMDatabase) {
|
||||||
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))!
|
||||||
let sql = "select distinct feedID, count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and (starred=1 or dateArrived>?) group by feedID;"
|
let sql = "select distinct feedID, count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 group by feedID;"
|
||||||
|
|
||||||
var parameters = [Any]()
|
let parameters = Array(webFeedIDs) as [Any]
|
||||||
parameters += Array(webFeedIDs) as [Any]
|
|
||||||
parameters += [cutoffDate] as [Any]
|
|
||||||
|
|
||||||
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
|
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {
|
||||||
informOperationDelegateOfCompletion()
|
informOperationDelegateOfCompletion()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
||||||
MARKETING_VERSION = 5.0
|
MARKETING_VERSION = 5.0.1
|
||||||
CURRENT_PROJECT_VERSION = 39
|
CURRENT_PROJECT_VERSION = 40
|
||||||
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||||
|
|
Loading…
Reference in New Issue