Add JSON-based account-persistence (IOS-192)
This is per user. Also: Fetch authenticated accounts regularly Also: Move Persistence-stuff to MastodonCore because.
This commit is contained in:
parent
d3c7ba2c7c
commit
60aafe6330
|
@ -60,7 +60,6 @@
|
|||
2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; };
|
||||
2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; };
|
||||
2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; };
|
||||
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */; };
|
||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||
|
@ -162,8 +161,6 @@
|
|||
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; };
|
||||
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
|
||||
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
|
||||
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */; };
|
||||
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98782B0F622B0045EC2B /* SearchHistory.swift */; };
|
||||
D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; };
|
||||
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; };
|
||||
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; };
|
||||
|
@ -694,7 +691,6 @@
|
|||
2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = "<group>"; };
|
||||
2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = "<group>"; };
|
||||
2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Timeline.swift"; sourceTree = "<group>"; };
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||
|
@ -833,8 +829,6 @@
|
|||
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
|
||||
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
|
||||
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SearchHistory.swift"; sourceTree = "<group>"; };
|
||||
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = "<group>"; };
|
||||
D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewToggleCell.swift; sourceTree = "<group>"; };
|
||||
D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
|
||||
|
@ -1892,21 +1886,10 @@
|
|||
D8AC98742B0F615E0045EC2B /* Persistence */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98772B0F62230045EC2B /* Model */,
|
||||
D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */,
|
||||
2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8AC98772B0F62230045EC2B /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D8AC98782B0F622B0045EC2B /* SearchHistory.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8E5C347296DB896007E76A7 /* Edit History */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3856,7 +3839,6 @@
|
|||
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */,
|
||||
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */,
|
||||
DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */,
|
||||
D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */,
|
||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
||||
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
|
@ -3864,7 +3846,6 @@
|
|||
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
|
||||
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||
D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */,
|
||||
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */,
|
||||
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */,
|
||||
DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */,
|
||||
|
@ -3969,7 +3950,6 @@
|
|||
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
|
||||
D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */,
|
||||
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
|
||||
2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */,
|
||||
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
|
||||
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
|
||||
DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */,
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
// Created by Marcus Kida on 17.11.22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension Persistence.SearchHistory {
|
||||
struct Item: Codable, Hashable, Equatable {
|
||||
let updatedAt: Date
|
||||
let userID: Mastodon.Entity.Account.ID
|
||||
|
||||
let account: Mastodon.Entity.Account?
|
||||
let hashtag: Mastodon.Entity.Tag?
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(userID)
|
||||
hasher.combine(account)
|
||||
hasher.combine(hashtag)
|
||||
}
|
||||
|
||||
public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool {
|
||||
return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,21 +39,19 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
|||
break
|
||||
case .reply:
|
||||
guard let replyToAccountID = status.entity.inReplyToAccountID else { return }
|
||||
#warning("TODO: Implement Domain")
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self,
|
||||
domain: "",
|
||||
accountID: replyToAccountID)
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self,
|
||||
domain: domain,
|
||||
accountID: replyToAccountID)
|
||||
|
||||
case .repost:
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
target: .reblog, // keep the wrapper for header author
|
||||
status: status
|
||||
)
|
||||
case .repost:
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
target: .reblog, // keep the wrapper for header author
|
||||
status: status
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - avatar button
|
||||
|
|
|
@ -92,6 +92,7 @@ extension HomeTimelineViewModel.LoadLatestState {
|
|||
}
|
||||
|
||||
do {
|
||||
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
|
||||
let response = try await viewModel.context.apiService.homeTimeline(
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension HomeTimelineViewModel {
|
||||
class LoadOldestState: GKState {
|
||||
|
@ -60,6 +61,8 @@ extension HomeTimelineViewModel.LoadOldestState {
|
|||
}
|
||||
|
||||
do {
|
||||
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
|
||||
|
||||
let response = try await viewModel.context.apiService.homeTimeline(
|
||||
maxID: maxID,
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
|
|
|
@ -147,7 +147,9 @@ extension HomeTimelineViewModel {
|
|||
// reconfigure item
|
||||
snapshot.reconfigureItems([item])
|
||||
await updateSnapshotUsingReloadData(snapshot: snapshot)
|
||||
|
||||
|
||||
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService)
|
||||
|
||||
// fetch data
|
||||
let maxID = status.id
|
||||
_ = try? await context.apiService.homeTimeline(
|
||||
|
|
|
@ -89,7 +89,7 @@ class MainTabBarController: UITabBarController {
|
|||
|
||||
@MainActor
|
||||
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) async -> UIViewController {
|
||||
guard let authContext = authContext else {
|
||||
guard let authContext, let me = authContext.mastodonAuthenticationBox.authentication.account() else {
|
||||
return UITableViewController()
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,6 @@ class MainTabBarController: UITabBarController {
|
|||
_viewController.viewModel = .init(context: context, authContext: authContext)
|
||||
viewController = _viewController
|
||||
case .me:
|
||||
let me = authContext.mastodonAuthenticationBox.authentication.account()
|
||||
let _viewController = ProfileViewController()
|
||||
_viewController.context = context
|
||||
_viewController.coordinator = coordinator
|
||||
|
@ -133,7 +132,6 @@ class MainTabBarController: UITabBarController {
|
|||
private(set) var isReadyForWizardAvatarButton = false
|
||||
|
||||
// output
|
||||
var avatarURLObserver: AnyCancellable?
|
||||
@Published var avatarURL: URL?
|
||||
|
||||
// haptic feedback
|
||||
|
@ -268,28 +266,20 @@ extension MainTabBarController {
|
|||
NotificationCenter.default.publisher(for: .userFetched)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) {
|
||||
self.avatarURLObserver = user.publisher(for: \.avatar)
|
||||
.sink { [weak self, weak user] _ in
|
||||
guard let self = self else { return }
|
||||
guard let user = user else { return }
|
||||
guard user.managedObjectContext != nil else { return }
|
||||
self.avatarURL = user.avatarImageURL()
|
||||
}
|
||||
guard let self else { return }
|
||||
if let account = self.authContext?.mastodonAuthenticationBox.authentication.account() {
|
||||
self.avatarURL = account.avatarImageURL()
|
||||
|
||||
// a11y
|
||||
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
|
||||
guard let profileTabItem = _profileTabItem else { return }
|
||||
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(user.displayNameWithFallback)
|
||||
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(account.displayNameWithFallback)
|
||||
|
||||
self.context.authenticationService.updateActiveUserAccountPublisher
|
||||
.sink { [weak self] in
|
||||
self?.updateUserAccount()
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
} else {
|
||||
self.avatarURLObserver = nil
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -122,8 +122,6 @@ public class AppContext: ObservableObject {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension AppContext {
|
||||
|
|
|
@ -54,21 +54,21 @@ public extension AuthenticationServiceProvider {
|
|||
func getAuthentication(matching userAccessToken: String) -> MastodonAuthentication? {
|
||||
authentications.first(where: { $0.userAccessToken == userAccessToken })
|
||||
}
|
||||
|
||||
|
||||
func authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this?
|
||||
return authentications.sorted(by: { $0.activedAt > $1.activedAt })
|
||||
}
|
||||
|
||||
|
||||
func restore() {
|
||||
authentications = Self.keychain.allKeys().compactMap {
|
||||
guard
|
||||
let encoded = Self.keychain[$0],
|
||||
let data = Data(base64Encoded: encoded)
|
||||
let data = Data(base64Encoded: encoded)
|
||||
else { return nil }
|
||||
return try? JSONDecoder().decode(MastodonAuthentication.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func migrateLegacyAuthentications(in context: NSManagedObjectContext) {
|
||||
do {
|
||||
let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest)
|
||||
|
@ -101,10 +101,29 @@ public extension AuthenticationServiceProvider {
|
|||
logger.log(level: .error, "Could not migrate legacy authentications")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var authenticationMigrationRequired: Bool {
|
||||
userDefaults.didMigrateAuthentications == false
|
||||
}
|
||||
|
||||
func fetchAccounts(apiService: APIService, completion: (() -> Void)? = nil) async {
|
||||
// FIXME: This is a dirty hack to make the performance-stuff work.
|
||||
// Problem is, that we don't persist the user on disk anymore. So we have to fetch
|
||||
// it when we need it to display on the home timeline.
|
||||
// We need this (also) for the Account-list, but it might be the wrong place. App Startup might be more appropriate
|
||||
for authentication in authentications {
|
||||
guard let account = try? await apiService.accountInfo(domain: authentication.domain,
|
||||
userID: authentication.userID,
|
||||
authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value else { continue }
|
||||
|
||||
FileManager.default.store(account: account, forUserID: authentication.userID)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .userFetched, object: nil)
|
||||
|
||||
completion?()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
|
|
@ -10,6 +10,7 @@ import Foundation
|
|||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
final public class FeedFetchedResultsController {
|
||||
|
||||
|
@ -80,15 +81,16 @@ final public class FeedFetchedResultsController {
|
|||
private extension FeedFetchedResultsController {
|
||||
func load(kind: MastodonFeed.Kind, sinceId: MastodonStatus.ID?) async throws -> [MastodonFeed] {
|
||||
switch kind {
|
||||
case .home:
|
||||
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
|
||||
case .notificationAll:
|
||||
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
.value.map { .fromNotification($0, kind: .notificationAll) }
|
||||
case .notificationMentions:
|
||||
return try await context.apiService.notifications(maxID: nil, scope: .mentions, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
.value.map { .fromNotification($0, kind: .notificationMentions) }
|
||||
case .home:
|
||||
await context.authenticationService.authenticationServiceProvider.fetchAccounts(apiService: context.apiService)
|
||||
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
|
||||
case .notificationAll:
|
||||
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
.value.map { .fromNotification($0, kind: .notificationAll) }
|
||||
case .notificationMentions:
|
||||
return try await context.apiService.notifications(maxID: nil, scope: .mentions, authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
.value.map { .fromNotification($0, kind: .notificationMentions) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,10 +99,10 @@ public struct MastodonAuthentication: Codable, Hashable {
|
|||
return MastodonUser.findOrFetch(in: context, matching: userPredicate)
|
||||
}
|
||||
|
||||
public func account() -> Mastodon.Entity.Account {
|
||||
// store accounts
|
||||
#warning("TODO: Implement")
|
||||
return Mastodon.Entity.Account.placeholder()
|
||||
public func account() -> Mastodon.Entity.Account? {
|
||||
let account = FileManager.default.accounts(forUserID: userID).first(where: { $0.id == userID })
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
func updating(instance: Instance) -> Self {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
extension FileManager {
|
||||
public func store(account: Mastodon.Entity.Account, forUserID userID: String) {
|
||||
// store accounts for each loged in user
|
||||
var accounts = accounts(forUserID: userID)
|
||||
|
||||
if let index = accounts.firstIndex(of: account) {
|
||||
accounts.remove(at: index)
|
||||
}
|
||||
|
||||
accounts.append(account)
|
||||
|
||||
storeJSON(accounts, userID: userID)
|
||||
}
|
||||
|
||||
public func accounts(forUserID userID: String) -> [Mastodon.Entity.Account] {
|
||||
guard let documentsDirectory else { return [] }
|
||||
|
||||
let accountPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory)
|
||||
|
||||
guard let data = try? Data(contentsOf: accountPath) else { return [] }
|
||||
|
||||
let jsonDecoder = JSONDecoder()
|
||||
jsonDecoder.dateDecodingStrategy = .iso8601
|
||||
|
||||
do {
|
||||
let accounts = try jsonDecoder.decode([Mastodon.Entity.Account].self, from: data)
|
||||
return accounts
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func storeJSON(_ encodable: Encodable, userID: String) {
|
||||
guard let documentsDirectory else { return }
|
||||
|
||||
let jsonEncoder = JSONEncoder()
|
||||
jsonEncoder.dateEncodingStrategy = .iso8601
|
||||
do {
|
||||
let data = try jsonEncoder.encode(encodable)
|
||||
|
||||
let accountsPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory)
|
||||
try data.write(to: accountsPath)
|
||||
} catch {
|
||||
debugPrint(error.localizedDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -4,11 +4,11 @@ import Foundation
|
|||
import MastodonCore
|
||||
|
||||
extension FileManager {
|
||||
func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] {
|
||||
public func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] {
|
||||
return try searchItems().filter { $0.userID == userID }
|
||||
}
|
||||
|
||||
func searchItems() throws -> [Persistence.SearchHistory.Item] {
|
||||
public func searchItems() throws -> [Persistence.SearchHistory.Item] {
|
||||
guard let documentsDirectory else { return [] }
|
||||
|
||||
let searchHistoryPath = Persistence.searchHistory.filepath(baseURL: documentsDirectory)
|
||||
|
@ -28,9 +28,7 @@ extension FileManager {
|
|||
}
|
||||
}
|
||||
|
||||
func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws {
|
||||
guard let documentsDirectory else { return }
|
||||
|
||||
public func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws {
|
||||
var searchItems = (try? searchItems()) ?? []
|
||||
|
||||
if let index = searchItems.firstIndex(of: newSearchItem) {
|
||||
|
@ -58,10 +56,8 @@ extension FileManager {
|
|||
|
||||
}
|
||||
|
||||
func removeSearchHistory(forUser userID: String) {
|
||||
guard let documentsDirectory else { return }
|
||||
|
||||
var searchItems = (try? searchItems()) ?? []
|
||||
public func removeSearchHistory(forUser userID: String) {
|
||||
let searchItems = (try? searchItems()) ?? []
|
||||
let newSearchItems = searchItems.filter { $0.userID != userID }
|
||||
|
||||
storeJSON(newSearchItems, .searchHistory)
|
|
@ -1,22 +1,21 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension FileManager {
|
||||
private static let cacheItemsLimit: Int = 100 // max number of items to cache
|
||||
|
||||
// Retrieve
|
||||
func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] {
|
||||
public func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] {
|
||||
try cached(timeline: .homeTimeline(userId)).map(MastodonStatus.fromEntity)
|
||||
}
|
||||
|
||||
func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
|
||||
public func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
|
||||
try cached(timeline: .notificationsAll(userId))
|
||||
}
|
||||
|
||||
func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
|
||||
public func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] {
|
||||
try cached(timeline: .notificationsMentions(userId))
|
||||
}
|
||||
|
||||
|
@ -38,15 +37,15 @@ extension FileManager {
|
|||
}
|
||||
|
||||
// Create
|
||||
func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) {
|
||||
public func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) {
|
||||
cache(items.map { $0.entity }, timeline: .homeTimeline(userIdentifier))
|
||||
}
|
||||
|
||||
func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
|
||||
public func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
|
||||
cache(items, timeline: .notificationsAll(userIdentifier))
|
||||
}
|
||||
|
||||
func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
|
||||
public func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) {
|
||||
cache(items, timeline: .notificationsMentions(userIdentifier))
|
||||
}
|
||||
|
||||
|
@ -71,15 +70,15 @@ extension FileManager {
|
|||
}
|
||||
|
||||
// Delete
|
||||
func invalidateHomeTimelineCache(for userId: UserIdentifier) {
|
||||
public func invalidateHomeTimelineCache(for userId: UserIdentifier) {
|
||||
invalidate(timeline: .homeTimeline(userId))
|
||||
}
|
||||
|
||||
func invalidateNotificationsAll(for userId: UserIdentifier) {
|
||||
public func invalidateNotificationsAll(for userId: UserIdentifier) {
|
||||
invalidate(timeline: .notificationsAll(userId))
|
||||
}
|
||||
|
||||
func invalidateNotificationsMentions(for userId: UserIdentifier) {
|
||||
public func invalidateNotificationsMentions(for userId: UserIdentifier) {
|
||||
invalidate(timeline: .notificationsMentions(userId))
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
extension Persistence.SearchHistory {
|
||||
public struct Item: Codable, Hashable, Equatable {
|
||||
public let updatedAt: Date
|
||||
public let userID: Mastodon.Entity.Account.ID
|
||||
|
||||
public let account: Mastodon.Entity.Account?
|
||||
public let hashtag: Mastodon.Entity.Tag?
|
||||
|
||||
public init(updatedAt: Date, userID: Mastodon.Entity.Account.ID, account: Mastodon.Entity.Account?, hashtag: Mastodon.Entity.Tag?) {
|
||||
self.updatedAt = updatedAt
|
||||
self.userID = userID
|
||||
self.account = account
|
||||
self.hashtag = hashtag
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(userID)
|
||||
hasher.combine(account)
|
||||
hasher.combine(hashtag)
|
||||
}
|
||||
|
||||
public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool {
|
||||
return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,12 @@ public enum Persistence {
|
|||
case homeTimeline(UserIdentifier)
|
||||
case notificationsMentions(UserIdentifier)
|
||||
case notificationsAll(UserIdentifier)
|
||||
|
||||
case accounts(userID: String)
|
||||
|
||||
private func uniqueUserDomainIdentifier(for userIdentifier: UserIdentifier) -> String {
|
||||
"\(userIdentifier.userID)@\(userIdentifier.domain)"
|
||||
}
|
||||
|
||||
private var filename: String {
|
||||
switch self {
|
||||
case .searchHistory:
|
||||
|
@ -23,7 +28,9 @@ public enum Persistence {
|
|||
case let .notificationsMentions(userIdentifier):
|
||||
return "notifications_mentions_\(userIdentifier.uniqueUserDomainIdentifier)"
|
||||
case let .notificationsAll(userIdentifier):
|
||||
return "notifications_all_\(userIdentifier.uniqueUserDomainIdentifier)"
|
||||
return "notifications_all_\(uniqueUserDomainIdentifier(for: userIdentifier))"
|
||||
case .accounts(let userID):
|
||||
return "account_\(userID)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,29 +34,6 @@ extension APIService {
|
|||
authorization: authorization
|
||||
).singleOutput()
|
||||
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
try await managedObjectContext.performChanges {
|
||||
let me = authenticationBox.authentication.user(in: managedObjectContext)
|
||||
|
||||
for entity in response.value {
|
||||
let result = Persistence.MastodonUser.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: domain,
|
||||
entity: entity,
|
||||
cache: nil,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
|
||||
if let me = me {
|
||||
let user = result.user
|
||||
user.update(isFollowing: true, by: me)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ extension APIService {
|
|||
limit: limit,
|
||||
local: local
|
||||
)
|
||||
|
||||
|
||||
let response = try await Mastodon.API.Timeline.home(
|
||||
session: session,
|
||||
domain: domain,
|
||||
|
@ -54,18 +54,6 @@ extension APIService {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is a dirty hack to make the performance-stuff work.
|
||||
// Problem is, that we don't persist the user on disk anymore. So we have to fetch
|
||||
// it when we need it to display on the home timeline.
|
||||
// We need this (also) for the Account-list, but it might be the wrong place. App Startup might be more appropriate
|
||||
for authentication in AuthenticationServiceProvider.shared.authentications {
|
||||
_ = try? await accountInfo(domain: authentication.domain,
|
||||
userID: authentication.userID,
|
||||
authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .userFetched, object: nil)
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -25,13 +25,13 @@ public final class AuthenticationService: NSObject {
|
|||
|
||||
// output
|
||||
@Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = []
|
||||
|
||||
|
||||
private func fetchFollowedBlockedUserIds(
|
||||
_ authBox: MastodonAuthenticationBox,
|
||||
_ previousFollowingIDs: [String]? = nil,
|
||||
_ maxID: String? = nil
|
||||
) async throws {
|
||||
guard let apiService = apiService else { return }
|
||||
guard let apiService else { return }
|
||||
|
||||
let followingResponse = try await fetchFollowing(maxID, apiService, authBox)
|
||||
let followingIds = (previousFollowingIDs ?? []) + followingResponse.ids
|
||||
|
|
Loading…
Reference in New Issue