2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

Fix crash on launch due to infinite loop

And more honestly about singletons.
This commit is contained in:
shannon 2024-11-15 12:25:58 -05:00
parent 77f3c5a64d
commit c4442fe8a9
108 changed files with 288 additions and 368 deletions

View File

@ -46,7 +46,7 @@ final public class SceneCoordinator {
scene.session.sceneCoordinator = self
appContext.notificationService.requestRevealNotificationPublisher
NotificationService.shared.requestRevealNotificationPublisher
.receive(on: DispatchQueue.main)
.sink(receiveValue: {
[weak self] pushNotification in
@ -98,12 +98,12 @@ final public class SceneCoordinator {
switch type {
case .follow:
let account = try await appContext.apiService.notification(
let account = try await APIService.shared.notification(
notificationID: notificationID,
authenticationBox: authenticationBox
).value.account
let relationship = try await appContext.apiService.relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first
let relationship = try await APIService.shared.relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first
let profileViewModel = ProfileViewModel(
context: appContext,
@ -645,7 +645,7 @@ extension SceneCoordinator: SettingsCoordinatorDelegate {
let signOutAction = UIAlertAction(title: L10n.Common.Alerts.SignOut.confirm, style: .destructive) { [weak self] _ in
guard let self, let authenticationBox = self.authenticationBox else { return }
self.appContext.notificationService.clearNotificationCountForActiveUser()
NotificationService.shared.clearNotificationCountForActiveUser()
Task { @MainActor in
try await AuthenticationServiceProvider.shared.signOutMastodonUser(

View File

@ -17,7 +17,7 @@ extension DataSourceFacade {
) async throws -> Mastodon.Entity.Relationship {
FeedbackGenerator.shared.generate(.selectionChanged)
let apiService = dependency.context.apiService
let apiService = APIService.shared
let authBox = dependency.authenticationBox
let response = try await apiService.toggleBlock(
@ -40,7 +40,7 @@ extension DataSourceFacade {
) async throws -> Mastodon.Entity.Empty {
FeedbackGenerator.shared.generate(.selectionChanged)
let apiService = dependency.context.apiService
let apiService = APIService.shared
let authBox = dependency.authenticationBox
let response = try await apiService.toggleDomainBlock(account: account, authenticationBox: authBox)

View File

@ -19,7 +19,7 @@ extension DataSourceFacade {
) async throws {
FeedbackGenerator.shared.generate(.selectionChanged)
let updatedStatus = try await provider.context.apiService.bookmark(
let updatedStatus = try await APIService.shared.bookmark(
record: status,
authenticationBox: provider.authenticationBox
).value

View File

@ -18,7 +18,7 @@ extension DataSourceFacade {
) async throws {
FeedbackGenerator.shared.generate(.selectionChanged)
let updatedStatus = try await provider.context.apiService.favorite(
let updatedStatus = try await APIService.shared.favorite(
status: status,
authenticationBox: provider.authenticationBox
).value

View File

@ -18,7 +18,7 @@ extension DataSourceFacade {
account: Mastodon.Entity.Account
) async throws -> Mastodon.Entity.Relationship {
let authBox = dependency.authenticationBox
let relationship = try await dependency.context.apiService.relationship(
let relationship = try await APIService.shared.relationship(
forAccounts: [account], authenticationBox: authBox
).value.first
@ -27,7 +27,7 @@ extension DataSourceFacade {
let performAction = {
FeedbackGenerator.shared.generate(.selectionChanged)
let response = try await dependency.context.apiService.toggleFollow(
let response = try await APIService.shared.toggleFollow(
account: account,
authenticationBox: dependency.authenticationBox
).value
@ -100,7 +100,7 @@ extension DataSourceFacade {
await notificationView.configure(notification: notification, authenticationBox: dependency.authenticationBox)
do {
let newRelationship = try await dependency.context.apiService.followRequest(
let newRelationship = try await APIService.shared.followRequest(
userID: userID,
query: query,
authenticationBox: dependency.authenticationBox
@ -149,7 +149,7 @@ extension DataSourceFacade {
dependency: NeedsDependency & AuthContextProvider,
account: Mastodon.Entity.Account
) async throws {
let newRelationship = try await dependency.context.apiService.toggleShowReblogs(
let newRelationship = try await APIService.shared.toggleShowReblogs(
for: account,
authenticationBox: dependency.authenticationBox
)

View File

@ -16,7 +16,7 @@ extension DataSourceFacade {
) async throws -> Mastodon.Entity.Relationship {
FeedbackGenerator.shared.generate(.selectionChanged)
let response = try await dependency.context.apiService.toggleMute(
let response = try await APIService.shared.toggleMute(
authenticationBox: dependency.authenticationBox,
account: account
)

View File

@ -12,7 +12,7 @@ extension DataSourceFacade {
provider.coordinator.showLoading()
do {
let notificationRequests = try await provider.context.apiService.notificationRequests(authenticationBox: provider.authenticationBox).value
let notificationRequests = try await APIService.shared.notificationRequests(authenticationBox: provider.authenticationBox).value
let viewModel = NotificationRequestsViewModel(appContext: provider.context, authenticationBox: provider.authenticationBox, coordinator: provider.coordinator, requests: notificationRequests)
provider.coordinator.hideLoading()

View File

@ -57,7 +57,7 @@ extension DataSourceFacade {
provider.coordinator.showLoading()
do {
guard let account = try await provider.context.apiService.fetchUser(
guard let account = try await APIService.shared.fetchUser(
username: username,
domain: domain,
authenticationBox: provider.authenticationBox
@ -82,7 +82,7 @@ extension DataSourceFacade {
provider.coordinator.showLoading()
do {
let account = try await provider.context.apiService.accountInfo(
let account = try await APIService.shared.accountInfo(
domain: domain,
userID: accountID,
authorization: provider.authenticationBox.userAuthorization
@ -104,7 +104,7 @@ extension DataSourceFacade {
provider.coordinator.showLoading()
guard let me = provider.authenticationBox.authentication.account(),
let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authenticationBox).value.first else {
let relationship = try? await APIService.shared.relationship(forAccounts: [account], authenticationBox: provider.authenticationBox).value.first else {
return provider.coordinator.hideLoading()
}

View File

@ -49,7 +49,7 @@ private extension DataSourceFacade {
) async throws {
FeedbackGenerator.shared.generate(.selectionChanged)
let updatedStatus = try await provider.context.apiService.reblog(
let updatedStatus = try await APIService.shared.reblog(
status: status,
authenticationBox: provider.authenticationBox
).value

View File

@ -10,7 +10,7 @@ extension DataSourceFacade {
forStatus status: Status,
provider: NeedsDependency & AuthContextProvider
) async throws -> [Mastodon.Entity.StatusEdit] {
let reponse = try await provider.context.apiService.getHistory(forStatusID: status.id, authenticationBox: provider.authenticationBox)
let reponse = try await APIService.shared.getHistory(forStatusID: status.id, authenticationBox: provider.authenticationBox)
return reponse.value
}

View File

@ -23,7 +23,7 @@ extension DataSourceFacade {
dependency: NeedsDependency & AuthContextProvider & DataSourceProvider,
status: MastodonStatus
) async throws {
let deletedStatus = try await dependency.context.apiService.deleteStatus(
let deletedStatus = try await APIService.shared.deleteStatus(
status: status,
authenticationBox: dependency.authenticationBox
).value.asMastodonStatus
@ -234,7 +234,7 @@ extension DataSourceFacade {
alertController.addAction(cancelAction)
dependency.present(alertController, animated: true)
case .reportUser:
guard let relationship = try? await dependency.context.apiService.relationship(forAccounts: [menuContext.author], authenticationBox: dependency.authenticationBox).value.first else { return }
guard let relationship = try? await APIService.shared.relationship(forAccounts: [menuContext.author], authenticationBox: dependency.authenticationBox).value.first else { return }
let reportViewModel = ReportViewModel(
context: dependency.context,
@ -333,7 +333,7 @@ extension DataSourceFacade {
guard let status = menuContext.statusViewModel?.originalStatus else { return }
let statusSource = try await dependency.context.apiService.getStatusSource(
let statusSource = try await APIService.shared.getStatusSource(
forStatusID: status.id,
authenticationBox: dependency.authenticationBox
).value

View File

@ -25,8 +25,7 @@ extension DataSourceFacade {
FeedbackGenerator.shared.generate(.selectionChanged)
do {
let value = try await provider.context
.apiService
let value = try await APIService.shared
.translateStatus(
statusID: status.id,
authenticationBox: provider.authenticationBox

View File

@ -568,7 +568,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
.compactMap { poll.options.firstIndex(of: $0) }
do {
let newPoll = try await context.apiService.vote(
let newPoll = try await APIService.shared.vote(
poll: poll.entity,
choices: choices,
authenticationBox: authenticationBox

View File

@ -310,7 +310,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
.compactMap { poll.options.firstIndex(of: $0) }
do {
let newPoll = try await context.apiService.vote(
let newPoll = try await APIService.shared.vote(
poll: poll.entity,
choices: choices,
authenticationBox: authenticationBox
@ -584,7 +584,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
}
do {
let edits = try await context.apiService.getHistory(forStatusID: status.id, authenticationBox: authenticationBox).value
let edits = try await APIService.shared.getHistory(forStatusID: status.id, authenticationBox: authenticationBox).value
await coordinator.hideLoading()

View File

@ -124,7 +124,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableV
guard let self = self else { return }
Task { @MainActor in
do {
try await self.context.photoLibraryService.save(
try await PhotoLibraryService.shared.save(
imageSource: .url(assetURL)
).singleOutput()
} catch {
@ -153,7 +153,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableV
) { [weak self] _ in
guard let self = self else { return }
Task {
try await self.context.photoLibraryService.copy(
try await PhotoLibraryService.shared.copy(
imageSource: .url(assetURL)
).singleOutput()
}

View File

@ -245,7 +245,7 @@ extension ComposeViewController {
private func enqueuePublishStatus() {
do {
let statusPublisher = try composeContentViewModel.statusPublisher()
viewModel.context.publisherService.enqueue(
PublisherService.shared.enqueue(
statusPublisher: statusPublisher,
authenticationBox: viewModel.authenticationBox
)
@ -294,7 +294,7 @@ extension ComposeViewController {
private func enqueuePublishStatusEdit() {
do {
guard let editStatusPublisher = try composeContentViewModel.statusEditPublisher() else { return }
viewModel.context.publisherService.enqueue(
PublisherService.shared.enqueue(
statusPublisher: editStatusPublisher,
authenticationBox: viewModel.authenticationBox
)
@ -325,7 +325,6 @@ extension ComposeViewController {
if UIPasteboard.general.hasImages, let images = UIPasteboard.general.images {
let attachmentViewModels = images.map { image in
return AttachmentViewModel(
api: viewModel.context.apiService,
authenticationBox: viewModel.authenticationBox,
input: .image(image),
sizeLimit: composeContentViewModel.sizeLimit,

View File

@ -145,7 +145,7 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
do {
let userID = account.id
let familiarFollowers = viewModel.familiarFollowers.first(where: { $0.id == userID })?.accounts ?? []
let relationships = try await context.apiService.relationship(forAccounts: familiarFollowers, authenticationBox: authenticationBox).value
let relationships = try await APIService.shared.relationship(forAccounts: familiarFollowers, authenticationBox: authenticationBox).value
coordinator.hideLoading()

View File

@ -48,12 +48,12 @@ extension DiscoveryForYouViewModel {
do {
let suggestedAccounts = try await fetchSuggestionAccounts()
let familiarFollowersResponse = try? await context.apiService.familiarFollowers(
let familiarFollowersResponse = try? await APIService.shared.familiarFollowers(
query: .init(ids: suggestedAccounts.compactMap { $0.id }),
authenticationBox: authenticationBox
).value
let relationships = try? await context.apiService.relationship(
let relationships = try? await APIService.shared.relationship(
forAccounts: suggestedAccounts,
authenticationBox: authenticationBox
).value
@ -85,14 +85,14 @@ extension DiscoveryForYouViewModel {
private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account] {
do {
let response = try await context.apiService.suggestionAccountV2(
let response = try await APIService.shared.suggestionAccountV2(
query: nil,
authenticationBox: authenticationBox
).value
return response.compactMap { $0.account }
} catch {
// fallback V1
let response = try await context.apiService.suggestionAccount(
let response = try await APIService.shared.suggestionAccount(
query: nil,
authenticationBox: authenticationBox
).value

View File

@ -35,7 +35,7 @@ final class DiscoveryHashtagsViewModel {
.throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
.asyncMap { _ in
let authenticationBox = authenticationBox
return try await context.apiService.trendHashtags(domain: authenticationBox.domain,
return try await APIService.shared.trendHashtags(domain: authenticationBox.domain,
query: nil,
authenticationBox: authenticationBox
)
@ -63,7 +63,7 @@ extension DiscoveryHashtagsViewModel {
func fetch() async throws {
let authenticationBox = authenticationBox
let response = try await context.apiService.trendHashtags(domain: authenticationBox.domain,
let response = try await APIService.shared.trendHashtags(domain: authenticationBox.domain,
query: nil,
authenticationBox: authenticationBox
)

View File

@ -8,6 +8,7 @@
import Foundation
import GameplayKit
import MastodonSDK
import MastodonCore
extension DiscoveryNewsViewModel {
class State: GKState {
@ -123,7 +124,7 @@ extension DiscoveryNewsViewModel.State {
Task {
do {
let response = try await viewModel.context.apiService.trendLinks(
let response = try await APIService.shared.trendLinks(
domain: viewModel.authenticationBox.domain,
query: Mastodon.API.Trends.StatusQuery(
offset: offset,

View File

@ -55,7 +55,7 @@ final class DiscoveryNewsViewModel {
extension DiscoveryNewsViewModel {
func checkServerEndpoint() async {
do {
_ = try await context.apiService.trendLinks(
_ = try await APIService.shared.trendLinks(
domain: authenticationBox.domain,
query: .init(offset: nil, limit: nil),
authenticationBox: authenticationBox

View File

@ -122,7 +122,7 @@ extension DiscoveryPostsViewModel.State {
Task {
do {
let response = try await viewModel.context.apiService.trendStatuses(
let response = try await APIService.shared.trendStatuses(
domain: viewModel.authenticationBox.domain,
query: Mastodon.API.Trends.StatusQuery(
offset: offset,

View File

@ -55,7 +55,7 @@ final class DiscoveryPostsViewModel {
extension DiscoveryPostsViewModel {
func checkServerEndpoint() async {
do {
_ = try await context.apiService.trendStatuses(
_ = try await APIService.shared.trendStatuses(
domain: authenticationBox.domain,
query: .init(offset: nil, limit: nil),
authenticationBox: authenticationBox

View File

@ -9,6 +9,7 @@ import Foundation
import GameplayKit
import CoreDataStack
import MastodonSDK
import MastodonCore
extension HashtagTimelineViewModel {
class State: GKState {
@ -126,7 +127,7 @@ extension HashtagTimelineViewModel.State {
Task {
do {
let response = try await viewModel.context.apiService.hashtagTimeline(
let response = try await APIService.shared.hashtagTimeline(
maxID: maxID,
hashtag: viewModel.hashtag,
authenticationBox: viewModel.authenticationBox

View File

@ -68,7 +68,7 @@ extension HashtagTimelineViewModel {
func followTag() {
self.hashtagDetails.send(hashtagDetails.value?.copy(following: true))
Task { @MainActor in
let tag = try? await context.apiService.followTag(
let tag = try? await APIService.shared.followTag(
for: hashtag,
authenticationBox: authenticationBox
).value
@ -79,7 +79,7 @@ extension HashtagTimelineViewModel {
func unfollowTag() {
self.hashtagDetails.send(hashtagDetails.value?.copy(following: false))
Task { @MainActor in
let tag = try? await context.apiService.unfollowTag(
let tag = try? await APIService.shared.unfollowTag(
for: hashtag,
authenticationBox: authenticationBox
).value
@ -91,7 +91,7 @@ extension HashtagTimelineViewModel {
private extension HashtagTimelineViewModel {
func updateTagInformation() {
Task { @MainActor in
let tag = try? await context.apiService.getTagInformation(
let tag = try? await APIService.shared.getTagInformation(
for: hashtag,
authenticationBox: authenticationBox
).value

View File

@ -308,14 +308,14 @@ extension HomeTimelineViewController {
}
.store(in: &disposeBag)
context.publisherService.statusPublishResult.receive(on: DispatchQueue.main).sink { result in
PublisherService.shared.statusPublishResult.receive(on: DispatchQueue.main).sink { result in
if case .success(.edit(let status)) = result {
self.viewModel?.hasPendingStatusEditReload = true
self.viewModel?.dataController.update(status: .fromEntity(status.value), intent: .edit)
}
}.store(in: &disposeBag)
context.publisherService.$currentPublishProgress
PublisherService.shared.$currentPublishProgress
.receive(on: DispatchQueue.main)
.sink { [weak self] progress in
guard let self = self else { return }
@ -434,7 +434,7 @@ extension HomeTimelineViewController {
})
.store(in: &disposeBag)
context.publisherService.statusPublishResult.prepend(.failure(AppError.badRequest))
PublisherService.shared.statusPublishResult.prepend(.failure(AppError.badRequest))
.receive(on: DispatchQueue.main)
.sink { [weak self] publishResult in
guard let self else { return }
@ -625,7 +625,7 @@ extension HomeTimelineViewController {
}
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
guard let setting = context.settingService.currentSetting.value else { return }
guard let setting = SettingService.shared.currentSetting.value else { return }
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
}
@ -638,7 +638,6 @@ extension HomeTimelineViewController {
}
@objc func signOutAction(_ sender: UIAction) {
guard let authContext = viewModel?.authenticationBox else { return }
Task { @MainActor in
try await AuthenticationServiceProvider.shared.signOutMastodonUser(authentication: authenticationBox.authentication)

View File

@ -8,6 +8,7 @@
import UIKit
import MastodonUI
import MastodonSDK
import MastodonCore
extension HomeTimelineViewModel {
@ -25,7 +26,7 @@ extension HomeTimelineViewModel {
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
filterContext: .home,
activeFilters: context.statusFilterService.$activeFilters
activeFilters: StatusFilterService.shared.$activeFilters
)
)

View File

@ -11,7 +11,7 @@ extension HomeTimelineViewModel {
let userAuthentication = authenticationBox
.authentication
guard let accountCreatedAt = userAuthentication.accountCreatedAt else {
let updated = try? await context.apiService.accountVerifyCredentials(domain: userAuthentication.domain, authorization: authenticationBox.userAuthorization)
let updated = try? await APIService.shared.accountVerifyCredentials(domain: userAuthentication.domain, authorization: authenticationBox.userAuthorization)
guard let accountCreatedAt = updated?.createdAt else { return }
AuthenticationServiceProvider.shared.updateAccountCreatedAt(accountCreatedAt, forAuthentication: userAuthentication)
return
@ -31,7 +31,7 @@ extension HomeTimelineViewModel {
guard let self else { return }
do {
let campaign = try await self.context.apiService
let campaign = try await APIService.shared
.getDonationCampaign(seed: seed, source: nil).value
guard !Mastodon.Entity.DonationCampaign.hasPreviouslyDismissed(campaign.id) && !Mastodon.Entity.DonationCampaign.hasPreviouslyContributed(campaign.id) else { return }
onPresentDonationCampaign.send(campaign)

View File

@ -115,7 +115,7 @@ extension HomeTimelineViewModel.LoadLatestState {
}
do {
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
await AuthenticationServiceProvider.shared.fetchAccounts()
let response: Mastodon.Response.Content<[Mastodon.Entity.Status]>
/// To find out wether or not we need to show the "Load More" button
@ -124,23 +124,23 @@ extension HomeTimelineViewModel.LoadLatestState {
switch viewModel.timelineContext {
case .home:
response = try await viewModel.context.apiService.homeTimeline(
response = try await APIService.shared.homeTimeline(
sinceID: sinceID,
authenticationBox: viewModel.authenticationBox
)
case .public:
response = try await viewModel.context.apiService.publicTimeline(
response = try await APIService.shared.publicTimeline(
query: .init(local: true, sinceID: sinceID),
authenticationBox: viewModel.authenticationBox
)
case let .list(id):
response = try await viewModel.context.apiService.listTimeline(
response = try await APIService.shared.listTimeline(
id: id,
query: .init(sinceID: sinceID),
authenticationBox: viewModel.authenticationBox
)
case let .hashtag(tag):
response = try await viewModel.context.apiService.hashtagTimeline(
response = try await APIService.shared.hashtagTimeline(
hashtag: tag,
authenticationBox: viewModel.authenticationBox
)

View File

@ -59,29 +59,29 @@ extension HomeTimelineViewModel.LoadOldestState {
}
do {
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService)
await AuthenticationServiceProvider.shared.fetchAccounts()
let response: Mastodon.Response.Content<[Mastodon.Entity.Status]>
switch viewModel.timelineContext {
case .home:
response = try await viewModel.context.apiService.homeTimeline(
response = try await APIService.shared.homeTimeline(
maxID: maxID,
authenticationBox: viewModel.authenticationBox
)
case .public:
response = try await viewModel.context.apiService.publicTimeline(
response = try await APIService.shared.publicTimeline(
query: .init(local: true, maxID: maxID),
authenticationBox: viewModel.authenticationBox
)
case let .list(id):
response = try await viewModel.context.apiService.listTimeline(
id: id,
response = try await APIService.shared.listTimeline(
id: id,
query: .init(local: true, maxID: maxID),
authenticationBox: viewModel.authenticationBox
)
case let .hashtag(tag):
response = try await viewModel.context.apiService.hashtagTimeline(
response = try await APIService.shared.hashtagTimeline(
hashtag: tag,
authenticationBox: viewModel.authenticationBox
)

View File

@ -163,30 +163,30 @@ extension HomeTimelineViewModel {
guard let status = record.status else { return }
record.isLoadingMore = true
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService)
await AuthenticationServiceProvider.shared.fetchAccounts()
// fetch data
let response: Mastodon.Response.Content<[Mastodon.Entity.Status]>?
switch timelineContext {
case .home:
response = try? await context.apiService.homeTimeline(
response = try? await APIService.shared.homeTimeline(
maxID: status.id,
authenticationBox: authenticationBox
)
case .public:
response = try? await context.apiService.publicTimeline(
response = try? await APIService.shared.publicTimeline(
query: .init(local: true, maxID: status.id),
authenticationBox: authenticationBox
)
case let .list(id):
response = try? await context.apiService.listTimeline(
id: id,
response = try? await APIService.shared.listTimeline(
id: id,
query: .init(local: true, maxID: status.id),
authenticationBox: authenticationBox
)
case let .hashtag(tag):
response = try? await context.apiService.hashtagTimeline(
response = try? await APIService.shared.hashtagTimeline(
hashtag: tag,
authenticationBox: authenticationBox
)

View File

@ -274,7 +274,7 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate {
switch action {
case .savePhoto:
guard let assetURL = viewController.viewModel.item.assetURL else { return }
context.photoLibraryService.save(imageSource: .url(assetURL))
PhotoLibraryService.shared.save(imageSource: .url(assetURL))
.sink { [weak self] completion in
guard let self = self else { return }
switch completion {
@ -300,7 +300,7 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate {
case .copyPhoto:
guard let assetURL = viewController.viewModel.item.assetURL else { return }
context.photoLibraryService.copy(imageSource: .url(assetURL))
PhotoLibraryService.shared.copy(imageSource: .url(assetURL))
.sink { completion in
switch completion {
case .failure(_):

View File

@ -149,7 +149,7 @@ class NotificationPolicyViewController: UIViewController {
guard let self else { return }
do {
let updatedPolicy = try await viewModel.appContext.apiService.updateNotificationPolicy(
let updatedPolicy = try await APIService.shared.updateNotificationPolicy(
authenticationBox: authenticationBox,
filterNotFollowing: viewModel.notFollowing,
filterNotFollowers: viewModel.noFollower,

View File

@ -136,10 +136,10 @@ extension NotificationRequestsTableViewController: NotificationRequestTableViewC
}
private func acceptNotificationRequest(_ notificationRequest: MastodonSDK.Mastodon.Entity.NotificationRequest) async throws {
_ = try await context.apiService.acceptNotificationRequests(authenticationBox: authenticationBox,
_ = try await APIService.shared.acceptNotificationRequests(authenticationBox: authenticationBox,
id: notificationRequest.id)
let requests = try await context.apiService.notificationRequests(authenticationBox: authenticationBox).value
let requests = try await APIService.shared.notificationRequests(authenticationBox: authenticationBox).value
NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil)
@ -183,10 +183,10 @@ extension NotificationRequestsTableViewController: NotificationRequestTableViewC
}
private func rejectNotificationRequest(_ notificationRequest: MastodonSDK.Mastodon.Entity.NotificationRequest) async throws {
_ = try await context.apiService.rejectNotificationRequests(authenticationBox: authenticationBox,
_ = try await APIService.shared.rejectNotificationRequests(authenticationBox: authenticationBox,
id: notificationRequest.id)
let requests = try await context.apiService.notificationRequests(authenticationBox: authenticationBox).value
let requests = try await APIService.shared.notificationRequests(authenticationBox: authenticationBox).value
NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil)

View File

@ -126,7 +126,7 @@ extension NotificationTimelineViewController {
@objc private func refreshControlValueChanged(_ sender: RefreshControl) {
Task {
let policy = try? await context.apiService.notificationPolicy(authenticationBox: authenticationBox)
let policy = try? await APIService.shared.notificationPolicy(authenticationBox: authenticationBox)
viewModel.notificationPolicy = policy?.value
await viewModel.loadLatest()

View File

@ -8,6 +8,7 @@
import UIKit
import CoreData
import MastodonSDK
import MastodonCore
extension NotificationTimelineViewModel {
@ -22,7 +23,7 @@ extension NotificationTimelineViewModel {
authenticationBox: authenticationBox,
notificationTableViewCellDelegate: notificationTableViewCellDelegate,
filterContext: .notifications,
activeFilters: context.statusFilterService.$activeFilters
activeFilters: StatusFilterService.shared.$activeFilters
)
)

View File

@ -77,7 +77,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
}
do {
let response = try await viewModel.context.apiService.notifications(
let response = try await APIService.shared.notifications(
maxID: maxID,
accountID: accountID,
scope: scope,

View File

@ -99,7 +99,7 @@ final class NotificationTimelineViewModel {
Task { [weak self] in
guard let self else { return }
let policy = try await self.context.apiService.notificationPolicy(authenticationBox: self.authenticationBox)
let policy = try await APIService.shared.notificationPolicy(authenticationBox: self.authenticationBox)
self.notificationPolicy = policy.value
await self.loadLatest()

View File

@ -100,14 +100,14 @@ extension NotificationViewController {
super.viewDidAppear(animated)
// reset notification count
context.notificationService.clearNotificationCountForActiveUser()
NotificationService.shared.clearNotificationCountForActiveUser()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// reset notification count
context.notificationService.clearNotificationCountForActiveUser()
NotificationService.shared.clearNotificationCountForActiveUser()
}
override func viewDidDisappear(_ animated: Bool) {

View File

@ -56,7 +56,7 @@ final class NotificationViewModel {
// end init
Task {
do {
let policy = try await context.apiService.notificationPolicy(authenticationBox: authenticationBox)
let policy = try await APIService.shared.notificationPolicy(authenticationBox: authenticationBox)
self.notificationPolicy = policy.value
} catch {
// we won't show the filtering-options.

View File

@ -109,7 +109,7 @@ extension MastodonConfirmEmailViewController {
// upload avatar and set display name in the background
Just(self.viewModel.userToken.accessToken)
.asyncMap { token in
try await self.context.apiService.accountUpdateCredentials(
try await APIService.shared.accountUpdateCredentials(
domain: self.viewModel.authenticateInfo.domain,
query: self.viewModel.updateCredentialQuery,
authorization: Mastodon.API.OAuth.Authorization(accessToken: token)

View File

@ -137,7 +137,7 @@ class MastodonLoginViewController: UIViewController, NeedsDependency {
.store(in: &disposeBag)
authenticationViewModel.isAuthenticating.send(true)
context.apiService.createApplication(domain: server.domain)
APIService.shared.createApplication(domain: server.domain)
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
let application = response.value
guard let info = AuthenticationViewModel.AuthenticateInfo(

View File

@ -28,7 +28,7 @@ class MastodonLoginViewModel {
}
func updateServers() {
appContext?.apiService.servers(registrations: "all").sink(receiveCompletion: { [weak self] completion in
APIService.shared.servers(registrations: "all").sink(receiveCompletion: { [weak self] completion in
switch completion {
case .finished:
guard let self = self else { return }

View File

@ -271,13 +271,13 @@ extension MastodonPickServerViewController {
authenticationViewModel.isAuthenticating.send(true)
context.apiService.instance(domain: server.domain, authenticationBox: nil)
APIService.shared.instance(domain: server.domain, authenticationBox: nil)
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
guard let self = self else { return nil }
guard response.value.registrations != false else {
return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher()
}
return self.context.apiService.createApplication(domain: server.domain)
return APIService.shared.createApplication(domain: server.domain)
.map { MastodonPickServerViewModel.SignUpResponseFirst(instance: response, application: $0) }
.eraseToAnyPublisher()
}
@ -299,7 +299,7 @@ extension MastodonPickServerViewController {
guard let self = self else { return nil }
let instance = response.instance
let authenticateInfo = response.authenticateInfo
return self.context.apiService.applicationAccessToken(
return APIService.shared.applicationAccessToken(
domain: server.domain,
clientID: authenticateInfo.clientID,
clientSecret: authenticateInfo.clientSecret,

View File

@ -8,6 +8,7 @@
import Foundation
import GameplayKit
import MastodonSDK
import MastodonCore
extension MastodonPickServerViewModel {
class LoadIndexedServerState: GKState {
@ -37,7 +38,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return }
viewModel.isLoadingIndexedServers.value = true
viewModel.context.apiService.servers(language: nil, category: nil)
APIService.shared.servers(language: nil, category: nil)
.sink { completion in
switch completion {
case .failure(let error):

View File

@ -85,7 +85,7 @@ extension MastodonPickServerViewModel {
private func configure() {
context.apiService.languages().sink { completion in
APIService.shared.languages().sink { completion in
} receiveValue: { response in
self.allLanguages.value = response.value
@ -165,9 +165,9 @@ extension MastodonPickServerViewModel {
return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher()
}
self.unindexedServers.value = nil
return self.context.apiService.webFinger(domain: domain)
return APIService.shared.webFinger(domain: domain)
.flatMap { domain -> AnyPublisher<Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>, Never> in
return self.context.apiService.instance(domain: domain, authenticationBox: nil)
return APIService.shared.instance(domain: domain, authenticationBox: nil)
.map { response -> Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>in
let newResponse = response.map { [Mastodon.Entity.Server(domain: domain, instance: $0)] }
return Result.success(newResponse)

View File

@ -197,7 +197,7 @@ extension MastodonRegisterViewController {
var retryCount = 0
// register without show server rules
context.apiService.accountRegister(
APIService.shared.accountRegister(
domain: viewModel.domain,
query: query,
authorization: viewModel.applicationAuthorization
@ -222,7 +222,7 @@ extension MastodonRegisterViewController {
locale: self.viewModel.instance.languages?.first ?? "en"
)
retryCount += 1
return self.context.apiService.accountRegister(
return APIService.shared.accountRegister(
domain: self.viewModel.domain,
query: retryQuery,
authorization: self.viewModel.applicationAuthorization

View File

@ -110,7 +110,7 @@ final class MastodonRegisterViewModel: ObservableObject {
.compactMap { [weak self] text -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>, Never>? in
guard let self = self else { return nil }
let query = Mastodon.API.Account.AccountLookupQuery(acct: text)
return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization)
return APIService.shared.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization)
.map {
response -> Result<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
Result.success(response)

View File

@ -22,9 +22,9 @@ extension MastodonRegisterViewController {
viewController.context = context
viewController.coordinator = coordinator
let instanceResponse = try await context.apiService.instance(domain: domain, authenticationBox: nil).singleOutput()
let applicationResponse = try await context.apiService.createApplication(domain: domain).singleOutput()
let accessTokenResponse = try await context.apiService.applicationAccessToken(
let instanceResponse = try await APIService.shared.instance(domain: domain, authenticationBox: nil).singleOutput()
let applicationResponse = try await APIService.shared.createApplication(domain: domain).singleOutput()
let accessTokenResponse = try await APIService.shared.applicationAccessToken(
domain: domain,
clientID: applicationResponse.value.clientID!,
clientSecret: applicationResponse.value.clientSecret!,

View File

@ -143,7 +143,7 @@ extension AuthenticationViewModel {
})
.compactMap { [weak self] code -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>? in
guard let self = self else { return nil }
return self.context.apiService
return APIService.shared
.userAccessToken(
domain: info.domain,
clientID: info.clientID,
@ -188,7 +188,7 @@ extension AuthenticationViewModel {
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
return context.apiService.accountVerifyCredentials(
return APIService.shared.accountVerifyCredentials(
domain: info.domain,
authorization: authorization
)
@ -224,7 +224,7 @@ extension AuthenticationViewModel {
) async throws -> Mastodon.Entity.Account {
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken)
let account = try await context.apiService.accountVerifyCredentials(
let account = try await APIService.shared.accountVerifyCredentials(
domain: domain,
authorization: authorization
)

View File

@ -283,13 +283,13 @@ extension WelcomeViewController {
authenticationViewModel.isAuthenticating.send(true)
context.apiService.instance(domain: server.domain, authenticationBox: nil)
APIService.shared.instance(domain: server.domain, authenticationBox: nil)
.compactMap { [weak self] response -> AnyPublisher<MastodonPickServerViewModel.SignUpResponseFirst, Error>? in
guard let self = self else { return nil }
guard response.value.registrations != false else {
return Fail(error: AuthenticationViewModel.AuthenticationError.registrationClosed).eraseToAnyPublisher()
}
return self.context.apiService.createApplication(domain: server.domain)
return APIService.shared.createApplication(domain: server.domain)
.map { MastodonPickServerViewModel.SignUpResponseFirst(instance: response, application: $0) }
.eraseToAnyPublisher()
}
@ -311,7 +311,7 @@ extension WelcomeViewController {
guard let self = self else { return nil }
let instance = response.instance
let authenticateInfo = response.authenticateInfo
return self.context.apiService.applicationAccessToken(
return APIService.shared.applicationAccessToken(
domain: server.domain,
clientID: authenticateInfo.clientID,
clientSecret: authenticateInfo.clientSecret,

View File

@ -31,7 +31,7 @@ final class WelcomeViewModel {
}
func downloadDefaultServer(completion: (() -> Void)? = nil) {
context.apiService.defaultServers()
APIService.shared.defaultServers()
.timeout(.milliseconds(500) , scheduler: DispatchQueue.main)
.sink { [weak self] result in

View File

@ -124,7 +124,7 @@ extension BookmarkViewModel.State {
Task {
do {
let response = try await viewModel.context.apiService.bookmarkedStatuses(
let response = try await APIService.shared.bookmarkedStatuses(
maxID: maxID,
authenticationBox: viewModel.authenticationBox
)

View File

@ -123,7 +123,7 @@ extension FavoriteViewModel.State {
Task {
do {
let response = try await viewModel.context.apiService.favoritedStatuses(
let response = try await APIService.shared.favoritedStatuses(
maxID: maxID,
authenticationBox: viewModel.authenticationBox
)

View File

@ -38,7 +38,7 @@ extension FollowedTagsViewModel {
func fetchFollowedTags(completion: (() -> Void)? = nil ) {
Task { @MainActor in
do {
followedTags = try await context.apiService.getFollowedTags(
followedTags = try await APIService.shared.getFollowedTags(
domain: authenticationBox.domain,
query: Mastodon.API.Account.FollowedTagsQuery(limit: nil),
authenticationBox: authenticationBox
@ -59,12 +59,12 @@ extension FollowedTagsViewModel {
func followOrUnfollow(_ tag: Mastodon.Entity.Tag) {
Task { @MainActor in
if tag.following ?? false {
_ = try? await context.apiService.unfollowTag(
_ = try? await APIService.shared.unfollowTag(
for: tag.name,
authenticationBox: authenticationBox
)
} else {
_ = try? await context.apiService.followTag(
_ = try? await APIService.shared.followTag(
for: tag.name,
authenticationBox: authenticationBox
)

View File

@ -138,7 +138,7 @@ extension FollowerListViewModel.State {
Task {
do {
let accountResponse = try await viewModel.context.apiService.followers(
let accountResponse = try await APIService.shared.followers(
userID: userID,
maxID: maxID,
authenticationBox: viewModel.authenticationBox
@ -154,7 +154,7 @@ extension FollowerListViewModel.State {
var hasNewAppend = false
let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authenticationBox)
let newRelationships = try await APIService.shared.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authenticationBox)
var accounts = viewModel.accounts

View File

@ -8,6 +8,7 @@
import Foundation
import GameplayKit
import MastodonSDK
import MastodonCore
extension FollowingListViewModel {
class State: GKState {
@ -133,7 +134,7 @@ extension FollowingListViewModel.State {
Task {
do {
let accountResponse = try await viewModel.context.apiService.following(
let accountResponse = try await APIService.shared.following(
userID: userID,
maxID: maxID,
authenticationBox: viewModel.authenticationBox
@ -149,7 +150,7 @@ extension FollowingListViewModel.State {
var hasNewAppend = false
let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authenticationBox)
let newRelationships = try await APIService.shared.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authenticationBox)
var accounts = viewModel.accounts

View File

@ -405,7 +405,7 @@ extension ProfileViewController {
}
.store(in: &disposeBag)
context.publisherService.statusPublishResult.sink { [weak self] result in
PublisherService.shared.statusPublishResult.sink { [weak self] result in
if case .success(.edit(let status)) = result {
self?.updateViewModelsWithDataControllers(status: .fromEntity(status.value), intent: .edit)
}
@ -576,7 +576,7 @@ extension ProfileViewController {
}
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
guard let setting = context.settingService.currentSetting.value else { return }
guard let setting = SettingService.shared.currentSetting.value else { return }
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
}
@ -645,15 +645,15 @@ extension ProfileViewController {
let account = viewModel.account
if let domain = account.domain,
let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: viewModel.authenticationBox),
let updatedRelationship = try? await context.apiService.relationship(forAccounts: [updatedAccount], authenticationBox: viewModel.authenticationBox).value.first
let updatedAccount = try? await APIService.shared.fetchUser(username: account.acct, domain: domain, authenticationBox: viewModel.authenticationBox),
let updatedRelationship = try? await APIService.shared.relationship(forAccounts: [updatedAccount], authenticationBox: viewModel.authenticationBox).value.first
{
viewModel.account = updatedAccount
viewModel.relationship = updatedRelationship
viewModel.profileAboutViewModel.fields = updatedAccount.mastodonFields
}
if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: viewModel.authenticationBox).value {
if let updatedMe = try? await APIService.shared.authenticatedUserInfo(authenticationBox: viewModel.authenticationBox).value {
viewModel.me = updatedMe
FileManager.default.store(account: updatedMe, forUserID: viewModel.authenticationBox.authentication.userIdentifier())
}
@ -916,7 +916,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
Task {
_ = try await DataSourceFacade.responseToDomainBlockAction(dependency: self, account: account)
guard let newRelationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: viewModel.authenticationBox).value.first else { return }
guard let newRelationship = try await APIService.shared.relationship(forAccounts: [account], authenticationBox: viewModel.authenticationBox).value.first else { return }
viewModel.isUpdating = false
@ -1084,7 +1084,7 @@ extension ProfileViewController {
Task {
let account = viewModel.account
if let domain = account.domain,
let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: viewModel.authenticationBox) {
let updatedAccount = try? await APIService.shared.fetchUser(username: account.acct, domain: domain, authenticationBox: viewModel.authenticationBox) {
viewModel.account = updatedAccount
viewModel.relationship = relationship
@ -1097,7 +1097,7 @@ extension ProfileViewController {
} else if viewModel.account == viewModel.me {
// update my profile
Task {
if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: viewModel.authenticationBox).value {
if let updatedMe = try? await APIService.shared.authenticatedUserInfo(authenticationBox: viewModel.authenticationBox).value {
viewModel.me = updatedMe
viewModel.account = updatedMe
FileManager.default.store(account: updatedMe, forUserID: viewModel.authenticationBox.authentication.userIdentifier())

View File

@ -134,7 +134,7 @@ class ProfileViewModel: NSObject {
Task {
do {
let response = try await self.context.apiService.relationship(
let response = try await APIService.shared.relationship(
forAccounts: [account],
authenticationBox: self.authenticationBox
)
@ -178,7 +178,7 @@ class ProfileViewModel: NSObject {
let mastodonAuthentication = authenticationBox.authentication
let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
return context.apiService.accountVerifyCredentials(domain: domain, authorization: authorization)
return APIService.shared.accountVerifyCredentials(domain: domain, authorization: authorization)
.tryMap { response in
FileManager.default.store(account: response.value, forUserID: mastodonAuthentication.userIdentifier())
return response
@ -227,7 +227,7 @@ extension ProfileViewModel {
source: nil,
fieldsAttributes: fieldsAttributes
)
let response = try await context.apiService.accountUpdateCredentials(
let response = try await APIService.shared.accountUpdateCredentials(
domain: domain,
query: query,
authorization: authorization

View File

@ -126,7 +126,7 @@ extension UserTimelineViewModel.State {
let queryFilter = viewModel.queryFilter
do {
let response = try await viewModel.context.apiService.userTimeline(
let response = try await APIService.shared.userTimeline(
accountID: userID,
maxID: maxID,
sinceID: nil,

View File

@ -8,6 +8,7 @@
import Foundation
import GameplayKit
import MastodonSDK
import MastodonCore
extension UserListViewModel {
class State: GKState {
@ -128,13 +129,13 @@ extension UserListViewModel.State {
let accountResponse: Mastodon.Response.Content<[Mastodon.Entity.Account]>
switch viewModel.kind {
case .favoritedBy(let status):
accountResponse = try await viewModel.context.apiService.favoritedBy(
accountResponse = try await APIService.shared.favoritedBy(
status: status,
query: .init(maxID: maxID, limit: nil),
authenticationBox: authenticationBox
)
case .rebloggedBy(let status):
accountResponse = try await viewModel.context.apiService.rebloggedBy(
accountResponse = try await APIService.shared.rebloggedBy(
status: status,
query: .init(maxID: maxID, limit: nil),
authenticationBox: authenticationBox
@ -151,7 +152,7 @@ extension UserListViewModel.State {
var hasNewAppend = false
let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authenticationBox)
let newRelationships = try await APIService.shared.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authenticationBox)
var accounts = viewModel.accounts

View File

@ -67,7 +67,7 @@ class ReportViewModel {
// bind server rules
Task { @MainActor in
do {
let response = try await context.apiService.instance(domain: authenticationBox.domain, authenticationBox: authenticationBox)
let response = try await APIService.shared.instance(domain: authenticationBox.domain, authenticationBox: authenticationBox)
.timeout(3, scheduler: DispatchQueue.main)
.singleOutput()
let rules = response.value.rules ?? []
@ -143,7 +143,7 @@ extension ReportViewModel {
do {
isReporting = true
let _ = try await context.apiService.report(
let _ = try await APIService.shared.report(
query: query,
authenticationBox: authenticationBox
)

View File

@ -10,6 +10,7 @@ import Foundation
import CoreData
import CoreDataStack
import GameplayKit
import MastodonCore
extension ReportStatusViewModel {
class State: GKState {
@ -69,7 +70,7 @@ extension ReportStatusViewModel.State {
let maxID = await viewModel.dataController.records.last?.id
do {
let response = try await viewModel.context.apiService.userTimeline(
let response = try await APIService.shared.userTimeline(
accountID: viewModel.account.id,
maxID: maxID,
sinceID: nil,

View File

@ -136,7 +136,7 @@ extension MainTabBarController {
}
}
context.apiService.error
APIService.shared.error
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
guard let self, let coordinator = self.coordinator else { return }
@ -161,7 +161,7 @@ extension MainTabBarController {
// handle push notification.
// toggle entry when finish fetch latest notification
Publishers.CombineLatest(
context.notificationService.unreadNotificationCountDidUpdate,
NotificationService.shared.unreadNotificationCountDidUpdate,
$currentTab
)
.receive(on: DispatchQueue.main)
@ -380,7 +380,7 @@ extension MainTabBarController {
guard let authenticationBox else { return }
Task { @MainActor in
let profileResponse = try await context.apiService.authenticatedUserInfo(authenticationBox: authenticationBox)
let profileResponse = try await APIService.shared.authenticatedUserInfo(authenticationBox: authenticationBox)
FileManager.default.store(account: profileResponse.value, forUserID: authenticationBox.authentication.userIdentifier())
}
}
@ -519,7 +519,7 @@ extension MainTabBarController {
}
// open settings
if context.settingService.currentSetting.value != nil {
if SettingService.shared.currentSetting.value != nil {
commands.append(openSettingsKeyCommand)
}
}
@ -562,7 +562,7 @@ extension MainTabBarController {
}
@objc private func openSettingsKeyCommandHandler(_ sender: UIKeyCommand) {
guard let setting = context.settingService.currentSetting.value else { return }
guard let setting = SettingService.shared.currentSetting.value else { return }
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
}

View File

@ -198,7 +198,7 @@ extension SidebarViewController: UICollectionViewDelegate {
case .tab(let tab):
delegate?.sidebarViewController(self, didSelectTab: tab)
case .setting:
guard let setting = context.settingService.currentSetting.value else { return }
guard let setting = SettingService.shared.currentSetting.value else { return }
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
case .compose:

View File

@ -107,7 +107,7 @@ extension SidebarViewModel {
switch item {
case .notifications:
Publishers.CombineLatest(
self.context.notificationService.unreadNotificationCountDidUpdate,
NotificationService.shared.unreadNotificationCountDidUpdate,
self.$currentTab
)
.receive(on: DispatchQueue.main)

View File

@ -71,7 +71,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
let authenticationBox = self.authenticationBox
Task {
let searchResult = try await context.apiService.search(
let searchResult = try await APIService.shared.search(
query: query,
authenticationBox: authenticationBox
).value
@ -124,7 +124,7 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl
)
Task {
let searchResult = try await context.apiService.search(
let searchResult = try await APIService.shared.search(
query: query,
authenticationBox: authenticationBox
).value

View File

@ -156,7 +156,7 @@ class SearchResultsOverviewTableViewController: UIViewController, NeedsDependenc
let searchTask = Task {
do {
let searchResult = try await context.apiService.search(
let searchResult = try await APIService.shared.search(
query: query,
authenticationBox: authenticationBox
).value

View File

@ -113,7 +113,7 @@ extension SearchResultViewModel.State {
Task {
do {
let searchResults = try await viewModel.context.apiService.search(
let searchResults = try await APIService.shared.search(
query: query,
authenticationBox: viewModel.authenticationBox
).value
@ -129,7 +129,7 @@ extension SearchResultViewModel.State {
let relationships: [Mastodon.Entity.Relationship]
if accounts.isNotEmpty {
relationships = try await viewModel.context.apiService.relationship(
relationships = try await APIService.shared.relationship(
forAccounts: accounts,
authenticationBox: viewModel.authenticationBox
).value

View File

@ -145,7 +145,7 @@ extension PrivacySafetyViewModel {
let domain = authenticationBox.domain
let userAuthorization = authenticationBox.userAuthorization
let account = try await appContext.apiService.accountVerifyCredentials(
let account = try await APIService.shared.accountVerifyCredentials(
domain: domain,
authorization: userAuthorization
).singleOutput().value
@ -172,7 +172,7 @@ extension PrivacySafetyViewModel {
let domain = authenticationBox.domain
let userAuthorization = authenticationBox.userAuthorization
let _ = try await appContext.apiService.accountUpdateCredentials(
let _ = try await APIService.shared.accountUpdateCredentials(
domain: domain,
query: .init(
discoverable: suggestMyAccountToOthers,

View File

@ -48,7 +48,7 @@ class SettingsCoordinator: NSObject, Coordinator {
let userAuthentication = s.authenticationBox.authentication
let seed = Mastodon.Entity.DonationCampaign.donationSeed(username: userAuthentication.username, domain: userAuthentication.domain)
do {
let campaign = try await s.appContext.apiService.getDonationCampaign(seed: seed, source: nil).value
let campaign = try await APIService.shared.getDonationCampaign(seed: seed, source: nil).value
await MainActor.run {
s.settingsViewController.donationCampaign = campaign
@ -84,8 +84,8 @@ extension SettingsCoordinator: SettingsViewControllerDelegate {
navigationController.pushViewController(generalSettingsViewController, animated: true)
case .notifications:
let currentSetting = appContext.settingService.currentSetting.value
let notificationsEnabled = appContext.notificationService.isNotificationPermissionGranted.value
let currentSetting = SettingService.shared.currentSetting.value
let notificationsEnabled = NotificationService.shared.isNotificationPermissionGranted.value
let notificationViewController = NotificationSettingsViewController(currentSetting: currentSetting, notificationsEnabled: notificationsEnabled)
notificationViewController.delegate = self
@ -101,7 +101,7 @@ extension SettingsCoordinator: SettingsViewControllerDelegate {
let serverDetailsViewController = ServerDetailsViewController(domain: domain, appContext: appContext, authenticationBox: authenticationBox, sceneCoordinator: sceneCoordinator)
serverDetailsViewController.delegate = self
appContext.apiService.instanceV2(domain: domain, authenticationBox: authenticationBox)
APIService.shared.instanceV2(domain: domain, authenticationBox: authenticationBox)
.sink { _ in
} receiveValue: { content in
@ -109,7 +109,7 @@ extension SettingsCoordinator: SettingsViewControllerDelegate {
}
.store(in: &disposeBag)
appContext.apiService.extendedDescription(domain: domain, authenticationBox: authenticationBox)
APIService.shared.extendedDescription(domain: domain, authenticationBox: authenticationBox)
.sink { _ in
} receiveValue: { content in
@ -219,7 +219,7 @@ extension SettingsCoordinator: NotificationSettingsViewControllerDelegate {
guard let subscription = setting.activeSubscription,
setting.domain == authenticationBox.domain,
setting.userID == authenticationBox.userID,
let legacyViewModel = appContext.notificationService.dequeueNotificationViewModel(mastodonAuthenticationBox: authenticationBox), let deviceToken = appContext.notificationService.deviceToken.value else { return }
let legacyViewModel = NotificationService.shared.dequeueNotificationViewModel(mastodonAuthenticationBox: authenticationBox), let deviceToken = NotificationService.shared.deviceToken.value else { return }
let queryData = Mastodon.API.Subscriptions.QueryData(
policy: viewModel.selectedPolicy.subscriptionPolicy,
@ -237,7 +237,7 @@ extension SettingsCoordinator: NotificationSettingsViewControllerDelegate {
mastodonAuthenticationBox: authenticationBox
)
appContext.apiService.createSubscription(
APIService.shared.createSubscription(
subscriptionObjectID: subscription.objectID,
query: query,
mastodonAuthenticationBox: authenticationBox
@ -260,7 +260,7 @@ extension SettingsCoordinator: NotificationSettingsViewControllerDelegate {
extension SettingsCoordinator: PolicySelectionViewControllerDelegate {
func newPolicySelected(_ viewController: PolicySelectionViewController, newPolicy: NotificationPolicy) {
self.setting.activeSubscription?.policyRaw = newPolicy.subscriptionPolicy.rawValue
try? self.appContext.managedObjectContext.save()
try? PersistenceManager.shared.managedObjectContext.save()
}
}

View File

@ -62,7 +62,7 @@ extension UserTableViewCellDelegate where Self: ViewControllerWithDependencies &
// Otherwise the relationship might still be `pending`
try await Task.sleep(for: .seconds(1))
let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first
let relationship = try await APIService.shared.relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first
let isMe: Bool
if let me {

View File

@ -50,7 +50,7 @@ final class SuggestionAccountViewModel: NSObject {
Task {
var suggestedAccounts: [Mastodon.Entity.V2.SuggestionAccount] = []
do {
let response = try await context.apiService.suggestionAccountV2(
let response = try await APIService.shared.suggestionAccountV2(
query: .init(limit: 5),
authenticationBox: authenticationBox
)
@ -60,7 +60,7 @@ final class SuggestionAccountViewModel: NSObject {
let accounts = suggestedAccounts.compactMap { $0.account }
let relationships = try await context.apiService.relationship(
let relationships = try await APIService.shared.relationship(
forAccounts: accounts,
authenticationBox: authenticationBox
).value

View File

@ -24,7 +24,7 @@ final class RemoteThreadViewModel: ThreadViewModel {
)
Task { @MainActor in
let response = try await context.apiService.status(
let response = try await APIService.shared.status(
statusID: statusID,
authenticationBox: authenticationBox
)
@ -47,7 +47,7 @@ final class RemoteThreadViewModel: ThreadViewModel {
)
Task { @MainActor in
let response = try await context.apiService.notification(
let response = try await APIService.shared.notification(
notificationID: notificationID,
authenticationBox: authenticationBox
)

View File

@ -29,7 +29,7 @@ extension ThreadViewModel {
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .thread,
activeFilters: context.statusFilterService.$activeFilters
activeFilters: StatusFilterService.shared.$activeFilters
)
)
@ -78,7 +78,7 @@ extension ThreadViewModel {
newSnapshot.appendSections([.main])
// top loader
let _hasReplyTo: Bool? = try? await self.context.managedObjectContext.perform {
let _hasReplyTo: Bool? = try? await PersistenceManager.shared.managedObjectContext.perform {
guard case let .root(threadContext) = root else { return nil }
return threadContext.status.entity.inReplyToID != nil
}

View File

@ -10,6 +10,7 @@ import Combine
import GameplayKit
import CoreDataStack
import MastodonSDK
import MastodonCore
extension ThreadViewModel {
class LoadThreadState: GKState {
@ -60,7 +61,7 @@ extension ThreadViewModel.LoadThreadState {
Task { @MainActor in
do {
let response = try await viewModel.context.apiService.statusContext(
let response = try await APIService.shared.statusContext(
statusID: threadContext.statusID,
authenticationBox: viewModel.authenticationBox
)
@ -70,7 +71,7 @@ extension ThreadViewModel.LoadThreadState {
// assert(!Thread.isMainThread)
// await Task.sleep(1_000_000_000) // 1s delay to prevent UI render issue
_ = try await viewModel.context.apiService.getHistory(forStatusID: threadContext.statusID,
_ = try await APIService.shared.getHistory(forStatusID: threadContext.statusID,
authenticationBox: viewModel.authenticationBox)
viewModel.mastodonStatusThreadViewModel.appendAncestor(

View File

@ -81,7 +81,7 @@ class ThreadViewModel {
}
.store(in: &disposeBag)
context.publisherService
PublisherService.shared
.statusPublishResult
.sink { [weak self] value in
guard let self else { return }

View File

@ -65,7 +65,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
extension AppDelegate {
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
appContext.notificationService.deviceToken.value = deviceToken
NotificationService.shared.deviceToken.value = deviceToken
}
}
@ -85,16 +85,16 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let accessToken = pushNotification.accessToken
UserDefaults.shared.increaseNotificationCount(accessToken: accessToken)
appContext.notificationService.applicationIconBadgeNeedsUpdate.send()
NotificationService.shared.applicationIconBadgeNeedsUpdate.send()
appContext.notificationService.handle(pushNotification: pushNotification)
NotificationService.shared.handle(pushNotification: pushNotification)
completionHandler([.sound])
}
// notification present in the background (or resume from background)
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult {
let shortcutItems = try? await appContext.notificationService.unreadApplicationShortcutItems()
let shortcutItems = try? await NotificationService.shared.unreadApplicationShortcutItems()
UIApplication.shared.shortcutItems = shortcutItems
return .noData
}
@ -111,8 +111,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
return
}
appContext.notificationService.handle(pushNotification: pushNotification)
appContext.notificationService.requestRevealNotificationPublisher.send(pushNotification)
NotificationService.shared.handle(pushNotification: pushNotification)
NotificationService.shared.requestRevealNotificationPublisher.send(pushNotification)
completionHandler()
}
@ -126,10 +126,3 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
}
}
extension AppContext {
static var shared: AppContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.appContext
}
}

View File

@ -89,10 +89,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
// update application badge
AppContext.shared.notificationService.applicationIconBadgeNeedsUpdate.send()
NotificationService.shared.applicationIconBadgeNeedsUpdate.send()
// trigger status filter update
AppContext.shared.statusFilterService.filterUpdatePublisher.send()
StatusFilterService.shared.filterUpdatePublisher.send()
// trigger authenticated user account update
AuthenticationServiceProvider.shared.updateActiveUserAccountPublisher.send()
@ -141,12 +141,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
Task {
guard let me = authenticationBox.authentication.account() else { return }
guard let account = try await AppContext.shared.apiService.search(
guard let account = try await APIService.shared.search(
query: .init(q: incomingURL.absoluteString, type: .accounts, resolve: true),
authenticationBox: authenticationBox
).value.accounts.first else { return }
guard let relationship = try await AppContext.shared.apiService.relationship(
guard let relationship = try await APIService.shared.relationship(
forAccounts: [account],
authenticationBox: authenticationBox
).value.first else { return }
@ -167,7 +167,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
case (profile, statusID):
Task {
guard let statusOnMyInstance = try await AppContext.shared.apiService.search(query: .init(q: incomingURL.absoluteString, resolve: true), authenticationBox: authenticationBox).value.statuses.first else { return }
guard let statusOnMyInstance = try await APIService.shared.search(query: .init(q: incomingURL.absoluteString, resolve: true), authenticationBox: authenticationBox).value.statuses.first else { return }
let threadViewModel = RemoteThreadViewModel(
context: AppContext.shared,
@ -285,12 +285,12 @@ extension SceneDelegate {
do {
guard let me = authenticationBox.authentication.account() else { return }
guard let account = try await AppContext.shared.apiService.search(
guard let account = try await APIService.shared.search(
query: .init(q: components[1], type: .accounts, resolve: true),
authenticationBox: authenticationBox
).value.accounts.first else { return }
guard let relationship = try await AppContext.shared.apiService.relationship(
guard let relationship = try await APIService.shared.relationship(
forAccounts: [account],
authenticationBox: authenticationBox
).value.first else { return }

View File

@ -23,8 +23,7 @@ class FollowersCountIntentHandler: INExtension, FollowersCountIntentHandling {
return INObjectCollection(items: [])
}
let results = try await AppContext.shared
.apiService
let results = try await APIService.shared
.search(query: .init(q: searchTerm), authenticationBox: authenticationBox)
return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomainIfMissing(authenticationBox.domain) as NSString })

View File

@ -16,7 +16,7 @@ class HashtagIntentHandler: INExtension, HashtagIntentHandling {
var results: [NSString] = []
if let searchTerm, searchTerm.isEmpty == false {
let searchResults = try await AppContext.shared.apiService
let searchResults = try await APIService.shared
.search(query: .init(q: searchTerm, type: .hashtags), authenticationBox: authenticationBox)
.value
.hashtags
@ -25,7 +25,7 @@ class HashtagIntentHandler: INExtension, HashtagIntentHandling {
results = searchResults
} else {
let followedTags = try await AppContext.shared.apiService.getFollowedTags(
let followedTags = try await APIService.shared.getFollowedTags(
domain: authenticationBox.domain,
query: Mastodon.API.Account.FollowedTagsQuery(limit: nil),
authenticationBox: authenticationBox)

View File

@ -15,8 +15,7 @@ class MultiFollowersCountIntentHandler: INExtension, MultiFollowersCountIntentHa
return INObjectCollection(items: [])
}
let results = try await AppContext.shared
.apiService
let results = try await APIService.shared
.search(query: .init(q: searchTerm), authenticationBox: authenticationBox)
return INObjectCollection(items: results.value.accounts.map { $0.acctWithDomainIfMissing(authenticationBox.domain) as NSString })

View File

@ -20,10 +20,7 @@ final class SendPostIntentHandler: NSObject {
let coreDataStack = CoreDataStack()
lazy var managedObjectContext = coreDataStack.persistentContainer.viewContext
lazy var api: APIService = {
let backgroundManagedObjectContext = coreDataStack.newTaskContext()
return APIService(
backgroundManagedObjectContext: backgroundManagedObjectContext
)
return APIService.isolatedService()
}()
}

View File

@ -12,26 +12,39 @@ import CoreData
import CoreDataStack
import AlamofireImage
public class AppContext: ObservableObject {
public static let shared = AppContext()
public var disposeBag = Set<AnyCancellable>()
public class PersistenceManager {
public static let shared = { PersistenceManager() }()
public let coreDataStack: CoreDataStack
public let managedObjectContext: NSManagedObjectContext
public let backgroundManagedObjectContext: NSManagedObjectContext
public let apiService: APIService
public let emojiService: EmojiService
private var disposeBag = Set<AnyCancellable>()
private init() {
let _coreDataStack = CoreDataStack()
let _managedObjectContext = _coreDataStack.persistentContainer.viewContext
let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext()
coreDataStack = _coreDataStack
managedObjectContext = _managedObjectContext
backgroundManagedObjectContext = _backgroundManagedObjectContext
backgroundManagedObjectContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: backgroundManagedObjectContext)
.sink { [weak self] notification in
guard let self = self else { return }
self.managedObjectContext.perform {
self.managedObjectContext.mergeChanges(fromContextDidSave: notification)
}
}
.store(in: &disposeBag)
}
}
public let publisherService: PublisherService
public let notificationService: NotificationService
public let settingService: SettingService
public let instanceService: InstanceService
public let blockDomainService: BlockDomainService
public let statusFilterService: StatusFilterService
public let photoLibraryService = PhotoLibraryService()
public class AppContext: ObservableObject {
public static let shared = { AppContext() }()
public var disposeBag = Set<AnyCancellable>()
public let placeholderImageCacheService = PlaceholderImageCacheService()
public let blurhashImageCacheService = BlurhashImageCacheService.shared
@ -48,57 +61,6 @@ public class AppContext: ObservableObject {
private init() {
let authProvider = AuthenticationServiceProvider.shared
let _coreDataStack = CoreDataStack()
if authProvider.authenticationMigrationRequired {
authProvider.migrateLegacyAuthentications(
in: _coreDataStack.persistentContainer.viewContext
)
}
let _managedObjectContext = _coreDataStack.persistentContainer.viewContext
let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext()
coreDataStack = _coreDataStack
managedObjectContext = _managedObjectContext
backgroundManagedObjectContext = _backgroundManagedObjectContext
let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext)
apiService = _apiService
// let _authenticationService = AuthenticationService(
// managedObjectContext: _managedObjectContext,
// backgroundManagedObjectContext: _backgroundManagedObjectContext,
// apiService: _apiService
// )
// authenticationService = _authenticationService
emojiService = EmojiService(
apiService: apiService
)
publisherService = .init(apiService: _apiService)
let _notificationService = NotificationService(
apiService: _apiService
)
notificationService = _notificationService
settingService = SettingService(
apiService: _apiService,
notificationService: _notificationService
)
instanceService = InstanceService(
apiService: _apiService
)
blockDomainService = BlockDomainService(
backgroundManagedObjectContext: _backgroundManagedObjectContext
)
statusFilterService = StatusFilterService(
apiService: _apiService
)
documentStore = DocumentStore()
documentStoreSubscription = documentStore.objectWillChange
@ -106,16 +68,6 @@ public class AppContext: ObservableObject {
.sink { [unowned self] in
self.objectWillChange.send()
}
backgroundManagedObjectContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: backgroundManagedObjectContext)
.sink { [weak self] notification in
guard let self = self else { return }
self.managedObjectContext.perform {
self.managedObjectContext.mergeChanges(fromContextDidSave: notification)
}
}
.store(in: &disposeBag)
}
}

View File

@ -47,6 +47,11 @@ public class AuthenticationServiceProvider: ObservableObject {
.assign(to: &$mastodonAuthenticationBoxes)
Task {
if authenticationMigrationRequired {
migrateLegacyAuthentications(
in: PersistenceManager.shared.managedObjectContext
)
}
await prepareForUse()
authentications = authenticationSortedByActivation()
}
@ -146,7 +151,7 @@ public extension AuthenticationServiceProvider {
func signOutMastodonUser(authentication: MastodonAuthentication) async throws {
try await AuthenticationServiceProvider.shared.delete(authentication: authentication)
_ = try await AppContext.shared.apiService.cancelSubscription(domain: authentication.domain, authorization: authentication.authorization)
_ = try await APIService.shared.cancelSubscription(domain: authentication.domain, authorization: authentication.authorization)
}
@MainActor
@ -217,13 +222,13 @@ public extension AuthenticationServiceProvider {
userDefaults.didMigrateAuthentications == false
}
func fetchAccounts(apiService: APIService) async {
func fetchAccounts() 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,
guard let account = try? await APIService.shared.accountInfo(domain: authentication.domain,
userID: authentication.userID,
authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value else { continue }
@ -250,7 +255,7 @@ private extension AuthenticationServiceProvider {
_ previousFollowingIDs: [String]? = nil,
_ maxID: String? = nil
) async throws {
let apiService = AppContext.shared.apiService
let apiService = APIService.shared
let followingResponse = try await fetchFollowing(maxID, apiService, authBox)
let followingIds = (previousFollowingIDs ?? []) + followingResponse.ids

View File

@ -169,29 +169,29 @@ private extension FeedDataController {
func load(kind: MastodonFeed.Kind, maxID: MastodonStatus.ID?) async throws -> [MastodonFeed] {
switch kind {
case .home(let timeline):
await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService)
await AuthenticationServiceProvider.shared.fetchAccounts()
let response: Mastodon.Response.Content<[Mastodon.Entity.Status]>
switch timeline {
case .home:
response = try await context.apiService.homeTimeline(
response = try await APIService.shared.homeTimeline(
maxID: maxID,
authenticationBox: authenticationBox
)
case .public:
response = try await context.apiService.publicTimeline(
response = try await APIService.shared.publicTimeline(
query: .init(local: true, maxID: maxID),
authenticationBox: authenticationBox
)
case let .list(id):
response = try await context.apiService.listTimeline(
response = try await APIService.shared.listTimeline(
id: id,
query: .init(maxID: maxID),
authenticationBox: authenticationBox
)
case let .hashtag(tag):
response = try await context.apiService.hashtagTimeline(
response = try await APIService.shared.hashtagTimeline(
hashtag: tag,
authenticationBox: authenticationBox
)
@ -209,10 +209,10 @@ private extension FeedDataController {
private func getFeeds(with scope: APIService.MastodonNotificationScope?, accountID: String? = nil) async throws -> [MastodonFeed] {
let notifications = try await context.apiService.notifications(maxID: nil, accountID: accountID, scope: scope, authenticationBox: authenticationBox).value
let notifications = try await APIService.shared.notifications(maxID: nil, accountID: accountID, scope: scope, authenticationBox: authenticationBox).value
let accounts = notifications.map { $0.account }
let relationships = try await context.apiService.relationship(forAccounts: accounts, authenticationBox: authenticationBox).value
let relationships = try await APIService.shared.relationship(forAccounts: accounts, authenticationBox: authenticationBox).value
let notificationsWithRelationship: [(notification: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?)] = notifications.compactMap { notification in
guard let relationship = relationships.first(where: {$0.id == notification.account.id }) else { return (notification: notification, relationship: nil)}

View File

@ -22,7 +22,7 @@ public final class SettingFetchedResultController: NSObject {
// output
public let settings = CurrentValueSubject<[Setting], Never>([])
public init(managedObjectContext: NSManagedObjectContext, additionalPredicate: NSPredicate?) {
public init(additionalPredicate: NSPredicate?) {
self.fetchedResultsController = {
let fetchRequest = Setting.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
@ -32,7 +32,7 @@ public final class SettingFetchedResultController: NSObject {
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: managedObjectContext,
managedObjectContext: PersistenceManager.shared.managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)

View File

@ -15,6 +15,9 @@ import AlamofireImage
public final class APIService {
@MainActor
public static let shared = { APIService(backgroundContext: PersistenceManager.shared.backgroundManagedObjectContext) }()
public static let callbackURLScheme = "mastodon"
public static let oauthCallbackURL = "mastodon://joinmastodon.org/oauth"
@ -23,15 +26,13 @@ public final class APIService {
// internal
let session: URLSession
// input
public let backgroundManagedObjectContext: NSManagedObjectContext
// output
public let error = PassthroughSubject<APIError, Never>()
public init(backgroundManagedObjectContext: NSManagedObjectContext) {
self.backgroundManagedObjectContext = backgroundManagedObjectContext
private init(backgroundContext: NSManagedObjectContext) {
backgroundManagedObjectContext = backgroundContext
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown"
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = ["User-Agent" : "mastodon-ios/" + appVersion]
@ -48,6 +49,10 @@ public final class APIService {
UIImageView.af.sharedImageDownloader = ImageDownloader(downloadPrioritization: .lifo)
}
public static func isolatedService() -> APIService {
let taskContext = PersistenceManager.shared.coreDataStack.newTaskContext()
return APIService(backgroundContext: taskContext)
}
}
extension APIService {

View File

@ -37,7 +37,7 @@ extension EmojiService.CustomEmojiViewModel.LoadState {
let authenticationBox = AuthenticationServiceProvider.shared.activeAuthentication,
let stateMachine else { return }
let apiService = viewModel.service.apiService
let apiService = APIService.shared
apiService.customEmoji(domain: viewModel.domain, authenticationBox: authenticationBox)
// .receive(on: DispatchQueue.main)

View File

@ -10,15 +10,9 @@ import Combine
import MastodonSDK
public final class EmojiService {
let apiService: APIService
public static let shared = { EmojiService() }()
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.EmojiService.working-queue")
private(set) var customEmojiViewModelDict: [String: CustomEmojiViewModel] = [:]
init(apiService: APIService) {
self.apiService = apiService
}
}
extension EmojiService {

View File

@ -67,7 +67,7 @@ extension MastodonAttachmentService.UploadState {
)
// and needs clone the `query` if needs retry
service.context.apiService.uploadMedia(
APIService.shared.uploadMedia(
domain: authenticationBox.domain,
query: query,
mastodonAuthenticationBox: authenticationBox,
@ -126,7 +126,7 @@ extension MastodonAttachmentService.UploadState {
return
}
service.context.apiService.getMedia(
APIService.shared.getMedia(
attachmentID: attachment.id,
mastodonAuthenticationBox: authenticationBox
)

View File

@ -15,6 +15,8 @@ import MastodonLocalization
public final class NotificationService {
public static let shared = { NotificationService() }()
public static let unreadShortcutItemIdentifier = "org.joinmastodon.app.NotificationService.unread-shortcut"
var disposeBag = Set<AnyCancellable>()
@ -22,7 +24,6 @@ public final class NotificationService {
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue")
// input
weak var apiService: APIService?
public let isNotificationPermissionGranted = CurrentValueSubject<Bool, Never>(false)
public let deviceToken = CurrentValueSubject<Data?, Never>(nil)
public let applicationIconBadgeNeedsUpdate = CurrentValueSubject<Void, Never>(Void())
@ -33,11 +34,7 @@ public final class NotificationService {
public let unreadNotificationCountDidUpdate = CurrentValueSubject<Void, Never>(Void())
public let requestRevealNotificationPublisher = PassthroughSubject<MastodonPushNotification, Never>()
init(
apiService: APIService
) {
self.apiService = apiService
private init() {
AuthenticationServiceProvider.shared.$authentications
.sink(receiveValue: { [weak self] mastodonAuthentications in
guard let self = self else { return }
@ -171,10 +168,9 @@ extension NotificationService {
private func fetchLatestNotifications(
pushNotification: MastodonPushNotification
) async throws {
guard let apiService = apiService else { return }
guard let authenticationBox = try await authenticationBox(for: pushNotification) else { return }
_ = try await apiService.notifications(
_ = try await APIService.shared.notifications(
maxID: nil,
scope: .everything,
authenticationBox: authenticationBox
@ -186,8 +182,7 @@ extension NotificationService {
) async throws {
// Subscription maybe failed to cancel when sign-out
// Try cancel again if receive that kind push notification
let managedObjectContext = AppContext.shared.managedObjectContext
guard let apiService = apiService else { return }
let managedObjectContext = PersistenceManager.shared.managedObjectContext
let userAccessToken = pushNotification.accessToken
@ -204,7 +199,7 @@ extension NotificationService {
guard let domain = try await domain(for: pushNotification) else { return }
do {
_ = try await apiService.cancelSubscription(
_ = try await APIService.shared.cancelSubscription(
domain: domain,
authorization: .init(accessToken: userAccessToken)
)
@ -213,7 +208,7 @@ extension NotificationService {
}
private func domain(for pushNotification: MastodonPushNotification) async throws -> String? {
let managedObjectContext = AppContext.shared.managedObjectContext
let managedObjectContext = PersistenceManager.shared.managedObjectContext
return try await managedObjectContext.perform {
let subscriptionRequest = NotificationSubscription.sortedFetchRequest
subscriptionRequest.predicate = NotificationSubscription.predicate(userToken: pushNotification.accessToken)

View File

@ -12,6 +12,7 @@ import Alamofire
import AlamofireImage
public final class PhotoLibraryService: NSObject {
public static let shared = { PhotoLibraryService() }()
}

View File

@ -10,11 +10,10 @@ import Combine
public final class PublisherService {
public static let shared = { PublisherService() }()
var disposeBag = Set<AnyCancellable>()
// input
let apiService: APIService
@Published public private(set) var statusPublishers: [StatusPublisher] = []
// output
@ -23,11 +22,7 @@ public final class PublisherService {
var currentPublishProgressObservation: NSKeyValueObservation?
@Published public var currentPublishProgress: Double = 0
public init(
apiService: APIService
) {
self.apiService = apiService
private init() {
$statusPublishers
.receive(on: DispatchQueue.main)
.sink { [weak self] publishers in
@ -84,7 +79,7 @@ extension PublisherService {
Task {
do {
let result = try await publisher.publish(api: apiService, authenticationBox: authenticationBox)
let result = try await publisher.publish(api: APIService.shared, authenticationBox: authenticationBox)
self.statusPublishResult.send(.success(result))
self.statusPublishers.removeAll(where: { $0 === publisher })

View File

@ -15,24 +15,20 @@ import MastodonCommon
public final class SettingService {
public static let shared = { SettingService() }()
var disposeBag = Set<AnyCancellable>()
// input
weak var apiService: APIService?
weak var notificationService: NotificationService?
var apiService: APIService { APIService.shared }
var notificationService: NotificationService { NotificationService.shared
}
// output
let settingFetchedResultController: SettingFetchedResultController
public let currentSetting = CurrentValueSubject<Setting?, Never>(nil)
init(
apiService: APIService,
notificationService: NotificationService
) {
self.apiService = apiService
self.notificationService = notificationService
private init() {
self.settingFetchedResultController = SettingFetchedResultController(
managedObjectContext: AppContext.shared.managedObjectContext,
additionalPredicate: nil
)
@ -41,7 +37,7 @@ public final class SettingService {
.compactMap { [weak self] mastodonAuthenticationBoxes -> AnyPublisher<[MastodonAuthenticationBox], Never>? in
guard let self = self else { return nil }
let managedObjectContext = AppContext.shared.backgroundManagedObjectContext
let managedObjectContext = PersistenceManager.shared.backgroundManagedObjectContext
return managedObjectContext.performChanges {
for authenticationBox in mastodonAuthenticationBoxes {
let domain = authenticationBox.domain
@ -95,7 +91,7 @@ public final class SettingService {
guard setting.domain == authenticationBox.domain,
setting.userID == authenticationBox.userID else { return nil }
let _viewModel = self.notificationService?.dequeueNotificationViewModel(
let _viewModel = notificationService.dequeueNotificationViewModel(
mastodonAuthenticationBox: authenticationBox
)
guard let viewModel = _viewModel else { return nil }

View File

@ -13,21 +13,17 @@ import MastodonSDK
import MastodonMeta
public final class StatusFilterService {
public static let shared = { StatusFilterService() }()
var disposeBag = Set<AnyCancellable>()
// input
weak var apiService: APIService?
public let filterUpdatePublisher = PassthroughSubject<Void, Never>()
// output
@Published public var activeFilters: [Mastodon.Entity.Filter] = []
init(
apiService: APIService
) {
self.apiService = apiService
private init() {
// fetch account filters every 300s
// also trigger fetch when app resume from background
let filterUpdateTimerPublisher = Timer.publish(every: 300.0, on: .main, in: .common)
@ -48,7 +44,7 @@ public final class StatusFilterService {
guard let box = mastodonAuthenticationBoxes.first else {
return Just(Result { throw APIService.APIError.implicit(.authenticationMissing) }).eraseToAnyPublisher()
}
return apiService.filters(mastodonAuthenticationBox: box)
return APIService.shared.filters(mastodonAuthenticationBox: box)
.map { response in
let now = Date()
let newResponse = response.map { filters in

View File

@ -77,7 +77,7 @@ extension AttachmentViewModel {
do {
let result = try await upload(
context: .init(
apiService: self.api,
apiService: APIService.shared,
authenticationBox: self.authenticationBox
),
isRetry: isRetry

View File

@ -41,7 +41,6 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
}()
// input
public let api: APIService
public let authenticationBox: MastodonAuthenticationBox
public let input: Input
public let sizeLimit: SizeLimit
@ -72,7 +71,6 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
@Published var remainTimeLocalizedString: String?
public init(
api: APIService,
authenticationBox: MastodonAuthenticationBox,
input: Input,
sizeLimit: SizeLimit,
@ -80,7 +78,6 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
isEditing: Bool = false,
caption: String? = nil
) {
self.api = api
self.authenticationBox = authenticationBox
self.input = input
self.sizeLimit = sizeLimit

View File

@ -130,7 +130,7 @@ extension AutoCompleteViewModel.State {
)
do {
let response = try await viewModel.context.apiService.search(
let response = try await APIService.shared.search(
query: query,
authenticationBox: viewModel.authenticationBox
)

View File

@ -40,7 +40,7 @@ final class AutoCompleteViewModel {
init(context: AppContext, authenticationBox: MastodonAuthenticationBox) {
self.context = context
self.authenticationBox = authenticationBox
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authenticationBox.domain)
self.customEmojiViewModel = EmojiService.shared.dequeueCustomEmojiViewModel(for: authenticationBox.domain)
// end init
autoCompleteItems

Some files were not shown because too many files have changed in this diff Show More