Continue refactoring of MastodonUser and Status (IOS-176, IOS-189)

This commit is contained in:
Marcus Kida 2023-11-17 13:59:22 +01:00
parent 36091e9628
commit b00877d5da
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
47 changed files with 538 additions and 775 deletions

View File

@ -6,8 +6,6 @@
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
@ -48,12 +46,11 @@ extension ReportSection {
case .status(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell
context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: .init(value: status),
viewModel: .init(value: record),
configuration: configuration
)
}
@ -78,8 +75,7 @@ extension ReportSection {
case .result(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
cell.avatarImageView.configure(configuration: .init(url: user.avatarImageURL()))
cell.avatarImageView.configure(configuration: .init(url: record.avatarImageURL()))
}
return cell
case .bottomLoader:

View File

@ -6,32 +6,10 @@
//
import UIKit
import CoreDataStack
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
static func responseToUserBlockAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
let apiService = dependency.context.apiService
let authBox = dependency.authContext.mastodonAuthenticationBox
_ = try await apiService.toggleBlock(
user: user,
authenticationBox: authBox
)
try await dependency.context.apiService.getBlocked(
authenticationBox: authBox
)
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
}
static func responseToUserBlockAction(
dependency: NeedsDependency & AuthContextProvider,
user: Mastodon.Entity.Account

View File

@ -6,7 +6,6 @@
//
import UIKit
import CoreDataStack
import MastodonUI
import MastodonLocalization
import MastodonSDK
@ -66,10 +65,8 @@ extension DataSourceFacade {
previewContext: AttachmentPreviewContext
) async throws {
let managedObjectContext = dependency.context.managedObjectContext
let attachments: [MastodonAttachment] = try await managedObjectContext.perform {
let status = status.reblog ?? status
return status.mastodonAttachments
}
let status = status.reblog ?? status
let attachments = status.mastodonAttachments
let thumbnails = await previewContext.thumbnails()
@ -150,20 +147,14 @@ extension DataSourceFacade {
@MainActor
static func coordinateToMediaPreviewScene(
dependency: NeedsDependency & MediaPreviewableViewController,
user: ManagedObjectRecord<MastodonUser>,
user: Mastodon.Entity.Account,
previewContext: ImagePreviewContext
) async throws {
let managedObjectContext = dependency.context.managedObjectContext
var _avatarAssetURL: String?
var _headerAssetURL: String?
try await managedObjectContext.perform {
guard let user = user.object(in: managedObjectContext) else { return }
_avatarAssetURL = user.avatar
_headerAssetURL = user.header
}
var _avatarAssetURL: String? = user.avatar
var _headerAssetURL: String? = user.header
let thumbnail = await previewContext.thumbnail()
let source: MediaPreviewTransitionItem.Source = {

View File

@ -6,7 +6,6 @@
//
import Foundation
import CoreDataStack
import MastodonCore
import UIKit
@ -86,21 +85,10 @@ extension DataSourceFacade {
provider: DataSourceProvider & AuthContextProvider
) async throws {
let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let _ = authenticationBox.authentication.user(in: managedObjectContext) else { return }
let request = SearchHistory.sortedFetchRequest
request.predicate = SearchHistory.predicate(
domain: authenticationBox.domain,
userID: authenticationBox.userID
)
let searchHistories = managedObjectContext.safeFetch(request)
for searchHistory in searchHistories {
managedObjectContext.delete(searchHistory)
}
} // end try await managedObjectContext.performChanges { }
guard let _ = try? await authenticationBox.authentication.me() else { return }
#warning("re-implement search history")
} // end func
}

View File

@ -78,12 +78,13 @@ extension AccountListViewModel {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
if let activeAuthentication = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first
{
AccountListViewModel.configure(
in: managedObjectContext,
cell: cell,
authentication: record,
activeAuthentication: activeAuthentication
)
Task { @MainActor in
await AccountListViewModel.configure(
in: managedObjectContext,
cell: cell,
authentication: record,
activeAuthentication: activeAuthentication
)}
}
return cell
case .addAccount:
@ -97,13 +98,14 @@ extension AccountListViewModel {
diffableDataSource?.apply(snapshot)
}
@MainActor
static func configure(
in context: NSManagedObjectContext,
cell: AccountListTableViewCell,
authentication: MastodonAuthentication,
activeAuthentication: MastodonAuthentication
) {
guard let user = authentication.user(in: context) else { return }
) async {
guard let user = try? await authentication.me() else { return }
// avatar
cell.avatarButton.avatarImageView.configure(
@ -112,7 +114,7 @@ extension AccountListViewModel {
// name
do {
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content)
cell.nameLabel.configure(content: metaContent)
} catch {

View File

@ -8,8 +8,6 @@
import UIKit
import AVKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import AlamofireImage
@ -206,24 +204,25 @@ extension HomeTimelineViewController {
viewModel.timelineIsEmpty
.receive(on: DispatchQueue.main)
.sink { [weak self] isEmpty in
if isEmpty {
self?.showEmptyView()
let userDoesntFollowPeople: Bool
if let managedObjectContext = self?.context.managedObjectContext,
let authContext = self?.authContext,
let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){
userDoesntFollowPeople = me.followersCount == 0
Task { @MainActor in
if isEmpty {
self?.showEmptyView()
let userDoesntFollowPeople: Bool
if let authContext = self?.authContext,
let me = try? await authContext.mastodonAuthenticationBox.authentication.me() {
userDoesntFollowPeople = me.followersCount == 0
} else {
userDoesntFollowPeople = true
}
if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople {
self?.findPeopleButtonPressed(self)
self?.viewModel.presentedSuggestions = true
}
} else {
userDoesntFollowPeople = true
self?.emptyView.removeFromSuperview()
}
if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople {
self?.findPeopleButtonPressed(self)
self?.viewModel.presentedSuggestions = true
}
} else {
self?.emptyView.removeFromSuperview()
}
}
.store(in: &disposeBag)

View File

@ -6,8 +6,6 @@
//
import UIKit
import CoreData
import CoreDataStack
import Combine
import MastodonSDK
import MastodonCore
@ -188,31 +186,36 @@ extension AuthenticationViewModel {
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
let managedObjectContext = context.backgroundManagedObjectContext
#warning("what happens if instancev2 is not reachable (errors out)??")
return context.apiService.accountVerifyCredentials(
domain: info.domain,
authorization: authorization
)
.tryMap { response -> Mastodon.Response.Content<Mastodon.Entity.Account> in
let account = response.value
let mastodonUserRequest = MastodonUser.sortedFetchRequest
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
mastodonUserRequest.fetchLimit = 1
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
throw AuthenticationError.badCredentials
}
.flatMap { response in
Publishers.CombineLatest3(
Just(response).setFailureType(to: Error.self).eraseToAnyPublisher(),
Mastodon.API.Instance.instance(session: .shared, domain: info.domain).eraseToAnyPublisher(),
Mastodon.API.V2.Instance.instance(session: .shared, domain: info.domain).eraseToAnyPublisher()
).eraseToAnyPublisher()
}
.tryMap { (accountResponse, instanceV1Response, instanceV2Response) -> Mastodon.Response.Content<Mastodon.Entity.Account> in
let account = accountResponse.value
let instanceV1 = instanceV1Response.value
let instanceV2 = instanceV2Response.value
AuthenticationServiceProvider.shared
.authentications
.insert(MastodonAuthentication.createFrom(domain: info.domain,
userID: mastodonUser.id,
username: mastodonUser.username,
userID: account.id,
username: account.username,
appAccessToken: userToken.accessToken, // TODO: swap app token
userAccessToken: userToken.accessToken,
clientID: info.clientID,
clientSecret: info.clientSecret), at: 0)
clientSecret: info.clientSecret,
instance: instanceV1,
instanceV2: instanceV2), at: 0)
return response
return accountResponse
}
.eraseToAnyPublisher()
}

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
import MastodonMeta
import MastodonCore
@ -19,7 +18,7 @@ final class ProfileAboutViewModel {
// input
let context: AppContext
@Published var user: MastodonUser?
@Published var user: Mastodon.Entity.Account?
@Published var isEditing = false
@Published var accountForEdit: Mastodon.Entity.Account?
@ -28,7 +27,7 @@ final class ProfileAboutViewModel {
let profileInfo = ProfileInfo()
let profileInfoEditing = ProfileInfo()
@Published var fields: [MastodonField] = []
@Published var fields: [Mastodon.Entity.Field] = []
@Published var emojiMeta: MastodonContent.Emojis = [:]
@Published var createdAt: Date = Date()
@ -38,18 +37,18 @@ final class ProfileAboutViewModel {
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.emojis) }
.compactMap { $0.emojis }
.map { $0.asDictionary }
.assign(to: &$emojiMeta)
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.fields) }
.compactMap { $0.fields }
.assign(to: &$fields)
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.createdAt) }
.compactMap { $0.createdAt }
.assign(to: &$createdAt)
Publishers.CombineLatest(

View File

@ -6,13 +6,13 @@
//
import UIKit
import CoreDataStack
import MastodonSDK
final class FollowedTagsTableViewCell: UITableViewCell {
private var hashtagView: HashtagTimelineHeaderView!
private let separatorLine = UIView.separatorLine
private weak var viewModel: FollowedTagsViewModel?
private weak var hashtag: Tag?
private var hashtag: Mastodon.Entity.Tag?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
@ -67,7 +67,7 @@ private extension FollowedTagsTableViewCell {
}
extension FollowedTagsTableViewCell {
func populate(with tag: Tag) {
func populate(with tag: Mastodon.Entity.Tag) {
self.hashtag = tag
hashtagView.update(HashtagTimelineHeaderView.Data.from(tag))
}

View File

@ -18,7 +18,7 @@ extension FollowedTagsViewModel {
}
enum Item: Hashable {
case hashtag(Tag)
case hashtag(Mastodon.Entity.Tag)
}
func tableViewDiffableDataSource(

View File

@ -8,14 +8,11 @@
import os
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import MastodonCore
final class FollowedTagsViewModel: NSObject {
var disposeBag = Set<AnyCancellable>()
let fetchedResultsController: FollowedTagsFetchedResultController
private weak var tableView: UITableView?
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>?
@ -24,22 +21,18 @@ final class FollowedTagsViewModel: NSObject {
let context: AppContext
let authContext: AuthContext
@Published var records = [Mastodon.Entity.Tag]()
// output
let presentHashtagTimeline = PassthroughSubject<HashtagTimelineViewModel, Never>()
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.fetchedResultsController = FollowedTagsFetchedResultController(
managedObjectContext: context.managedObjectContext,
domain: authContext.mastodonAuthenticationBox.domain,
user: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)! // fixme:
)
super.init()
self.fetchedResultsController
.$records
$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }
@ -71,15 +64,17 @@ extension FollowedTagsViewModel {
}
}
func followOrUnfollow(_ tag: Tag) {
func followOrUnfollow(_ tag: Mastodon.Entity.Tag) {
Task { @MainActor in
switch tag.following {
case true:
case .none:
break
case .some(true):
_ = try? await context.apiService.unfollowTag(
for: tag.name,
authenticationBox: authContext.mastodonAuthenticationBox
)
case false:
case .some(false):
_ = try? await context.apiService.followTag(
for: tag.name,
authenticationBox: authContext.mastodonAuthenticationBox
@ -94,7 +89,7 @@ extension FollowedTagsViewModel: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let object = fetchedResultsController.records[indexPath.row]
let object = records[indexPath.row]
let hashtagTimelineViewModel = HashtagTimelineViewModel(
context: self.context,

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import PhotosUI
import AlamofireImage
import CropViewController
@ -270,12 +269,11 @@ extension ProfileHeaderViewController {
extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) {
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self,
user: record,
user: user,
previewContext: DataSourceFacade.ImagePreviewContext(
imageView: button.avatarImageView,
containerView: .profileAvatar(profileHeaderView)
@ -286,12 +284,11 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) {
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self,
user: record,
user: user,
previewContext: DataSourceFacade.ImagePreviewContext(
imageView: imageView,
containerView: .profileBanner(profileHeaderView)

View File

@ -7,7 +7,6 @@
import UIKit
import Combine
import CoreDataStack
import Kanna
import MastodonSDK
import MastodonMeta
@ -26,7 +25,7 @@ final class ProfileHeaderViewModel {
let context: AppContext
let authContext: AuthContext
@Published var user: MastodonUser?
@Published var user: Mastodon.Entity.Account?
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
@Published var isMyself = false

View File

@ -7,49 +7,33 @@
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
extension ProfileHeaderView {
func configuration(user: MastodonUser) {
func configuration(user: Mastodon.Entity.Account) {
// header
user.publisher(for: \.header)
.map { _ in user.headerImageURL() }
.assign(to: \.headerImageURL, on: viewModel)
.store(in: &disposeBag)
viewModel.headerImageURL = URL(string: user.header)
// avatar
user.publisher(for: \.avatar)
.map { _ in user.avatarImageURL() }
.assign(to: \.avatarImageURL, on: viewModel)
.store(in: &disposeBag)
viewModel.avatarImageURL = user.avatarImageURL()
// emojiMeta
user.publisher(for: \.emojis)
.map { $0.asDictionary }
.assign(to: \.emojiMeta, on: viewModel)
.store(in: &disposeBag)
viewModel.emojiMeta = user.emojiMeta
// name
user.publisher(for: \.displayName)
.map { _ in user.displayNameWithFallback }
.assign(to: \.name, on: viewModel)
.store(in: &disposeBag)
viewModel.name = user.displayNameWithFallback
// username
viewModel.acct = user.acctWithDomain
// bio
user.publisher(for: \.note)
.assign(to: \.note, on: viewModel)
.store(in: &disposeBag)
viewModel.note = user.note
// dashboard
user.publisher(for: \.statusesCount)
.map { Int($0) }
.assign(to: \.statusesCount, on: viewModel)
.store(in: &disposeBag)
user.publisher(for: \.followingCount)
.map { Int($0) }
.assign(to: \.followingCount, on: viewModel)
.store(in: &disposeBag)
user.publisher(for: \.followersCount)
.map { Int($0) }
.assign(to: \.followersCount, on: viewModel)
.store(in: &disposeBag)
viewModel.statusesCount = user.statusesCount
viewModel.followingCount = user.followingCount
viewModel.followersCount = user.followersCount
}
}

View File

@ -13,9 +13,9 @@ import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
import CoreDataStack
import TabBarPager
import XLPagerTabStrip
import MastodonSDK
protocol ProfileViewModelEditable {
var isEdited: Bool { get }
@ -237,7 +237,7 @@ extension ProfileViewController {
items.append(self.favoriteBarButtonItem)
items.append(self.bookmarkBarButtonItem)
if self.currentInstance?.canFollowTags == true {
if self.currentInstance?.version?.majorServerVersion(greaterThanOrEquals: 4) ?? false == true {
items.append(self.followedTagsBarButtonItem)
}
@ -400,7 +400,6 @@ extension ProfileViewController {
return nil
}
let name = user.displayNameWithFallback
let _ = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
var menuActions: [MastodonMenu.Action] = [
.muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
@ -408,9 +407,14 @@ extension ProfileViewController {
.reportUser(.init(name: name)),
.shareUser(.init(name: name)),
]
let relationship = try await context.apiService.relationship(
forAccounts: [user],
authenticationBox: authContext.mastodonAuthenticationBox
).value.first
if let me = self.viewModel?.me, me.following.contains(user) {
let showReblogs = me.showingReblogsBy.contains(user)
if let relationship, relationship.following {
let showReblogs = relationship.showingReblogs == true
let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs)
menuActions.insert(.hideReblogs(context), at: 1)
}
@ -525,11 +529,10 @@ extension ProfileViewController {
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
let _activityViewController = try await DataSourceFacade.createActivityViewController(
dependency: self,
user: record
user: user
)
guard let activityViewController = _activityViewController else { return }
_ = self.coordinator.present(
@ -799,11 +802,10 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
break
case .follow, .request, .pending, .following:
guard let user = viewModel.user else { return }
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
Task {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: record
user: user
)
}
case .muting:
@ -815,13 +817,12 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
preferredStyle: .alert
)
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in
guard let self = self else { return }
Task {
try await DataSourceFacade.responseToUserMuteAction(
dependency: self,
user: record
user: user
)
}
}
@ -838,13 +839,12 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name),
preferredStyle: .alert
)
let record = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in
guard let self = self else { return }
Task {
try await DataSourceFacade.responseToUserBlockAction(
dependency: self,
user: record
user: user
)
}
}
@ -886,14 +886,12 @@ extension ProfileViewController: MastodonMenuDelegate {
func menuAction(_ action: MastodonMenu.Action) {
guard let user = viewModel.user else { return }
let userRecord: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
try await DataSourceFacade.responseToMenuAction(
dependency: self,
action: action,
menuContext: DataSourceFacade.MenuContext(
author: userRecord,
author: user,
statusViewModel: nil,
button: nil,
barButtonItem: self.moreMenuBarButtonItem
@ -936,7 +934,7 @@ extension ProfileViewController: PagerTabStripNavigateable {
}
private extension ProfileViewController {
var currentInstance: Instance? {
authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext)
var currentInstance: Mastodon.Entity.V2.Instance? {
authContext.mastodonAuthenticationBox.authentication.instanceV2
}
}

View File

@ -7,12 +7,12 @@
import UIKit
import Combine
import CoreDataStack
import SafariServices
import MastodonAsset
import MastodonCore
import MastodonLocalization
import MastodonUI
import MastodonSDK
class MainTabBarController: UITabBarController {
@ -131,7 +131,6 @@ class MainTabBarController: UITabBarController {
private(set) var isReadyForWizardAvatarButton = false
// output
var avatarURLObserver: AnyCancellable?
@Published var avatarURL: URL?
// haptic feedback
@ -262,14 +261,8 @@ extension MainTabBarController {
.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()
}
if let user = self.authContext?.mastodonAuthenticationBox.inMemoryCache.meAccount {
self.avatarURL = user.avatarImageURL()
// a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
@ -281,8 +274,6 @@ extension MainTabBarController {
self?.updateUserAccount()
}
.store(in: &self.disposeBag)
} else {
self.avatarURLObserver = nil
}
}
.store(in: &disposeBag)
@ -457,16 +448,7 @@ extension MainTabBarController {
authenticationBox: authContext.mastodonAuthenticationBox
)
if let user = authContext.mastodonAuthenticationBox.authentication.user(
in: context.managedObjectContext
) {
user.update(
property: .init(
entity: profileResponse.value,
domain: authContext.mastodonAuthenticationBox.domain
)
)
}
authContext.mastodonAuthenticationBox.inMemoryCache.meAccount = profileResponse.value
}
}
}

View File

@ -75,7 +75,7 @@ extension SidebarViewModel {
let imageURL: URL? = {
switch item {
case .me:
let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext)
let user = self.authContext?.mastodonAuthenticationBox.inMemoryCache.meAccount
return user?.avatarImageURL()
default:
return nil
@ -132,7 +132,7 @@ extension SidebarViewModel {
}
.store(in: &cell.disposeBag)
case .me:
guard let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return }
guard let user = self.authContext?.mastodonAuthenticationBox.inMemoryCache.meAccount else { return }
let currentUserDisplayName = user.displayNameWithFallback
cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
default:

View File

@ -6,9 +6,9 @@
//
import UIKit
import CoreDataStack
import MastodonCore
import MastodonAsset
import MastodonSDK
enum SearchHistorySection: Hashable {
case main
@ -28,20 +28,18 @@ extension SearchHistorySection {
configuration: Configuration
) -> UICollectionViewDiffableDataSource<SearchHistorySection, SearchHistoryItem> {
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, ManagedObjectRecord<MastodonUser>> { cell, indexPath, item in
let userCellRegister = UICollectionView.CellRegistration<SearchHistoryUserCollectionViewCell, Mastodon.Entity.Account> { cell, indexPath, item in
context.managedObjectContext.performAndWait {
guard let user = item.object(in: context.managedObjectContext) else { return }
cell.condensedUserView.configure(with: user)
cell.condensedUserView.configure(with: item)
}
}
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, ManagedObjectRecord<Tag>> { cell, indexPath, item in
let hashtagCellRegister = UICollectionView.CellRegistration<UICollectionViewListCell, Mastodon.Entity.Tag> { cell, indexPath, item in
context.managedObjectContext.performAndWait {
guard let hashtag = item.object(in: context.managedObjectContext) else { return }
var contentConfiguration = cell.defaultContentConfiguration()
contentConfiguration.image = UIImage(systemName: "magnifyingglass")
contentConfiguration.imageProperties.tintColor = Asset.Colors.Brand.blurple.color
contentConfiguration.text = "#" + hashtag.name
contentConfiguration.text = "#" + item.name
cell.contentConfiguration = contentConfiguration
}

View File

@ -44,13 +44,12 @@ extension SearchResultSection {
case .user(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
authContext: authContext,
tableView: tableView,
cell: cell,
viewModel: UserTableViewCell.ViewModel(user: user,
viewModel: UserTableViewCell.ViewModel(user: record,
followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(),
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()),
@ -61,12 +60,11 @@ extension SearchResultSection {
case .status(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
context.managedObjectContext.performAndWait {
guard let status = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: StatusTableViewCell.ViewModel(value: .status(status)),
viewModel: StatusTableViewCell.ViewModel(value: .status(record)),
configuration: configuration
)
}
@ -126,7 +124,7 @@ extension SearchResultSection {
configuration: Configuration
) {
cell.configure(
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
me: authContext.mastodonAuthenticationBox.inMemoryCache.meAccount,
tableView: tableView,
viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate

View File

@ -136,13 +136,13 @@ extension SearchResultViewModel.State {
// reset data source when the search is refresh
if offset == nil {
viewModel.userFetchedResultsController.userIDs = []
viewModel.statusFetchedResultsController.statusIDs = []
viewModel.statusRecords = []
viewModel.userRecords = []
viewModel.hashtags = []
}
viewModel.userFetchedResultsController.append(userIDs: userIDs)
viewModel.statusFetchedResultsController.append(statusIDs: statusIDs)
viewModel.userRecords.append(contentsOf: response.value.accounts)
viewModel.statusRecords.append(contentsOf: response.value.statuses)
var hashtags = viewModel.hashtags
for hashtag in response.value.hashtags where !hashtags.contains(hashtag) {

View File

@ -8,8 +8,6 @@
import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import MastodonCore
import MastodonMeta
@ -20,7 +18,7 @@ final class MastodonStatusThreadViewModel {
// input
let context: AppContext
@Published private(set) var deletedObjectIDs: Set<NSManagedObjectID> = Set()
@Published private(set) var deletedObjectIDs: Set<Mastodon.Entity.Status.ID> = Set()
// output
@Published var __ancestors: [StatusItem] = []
@ -41,7 +39,7 @@ final class MastodonStatusThreadViewModel {
let newItems = items.filter { item in
switch item {
case .thread(let thread):
return !deletedObjectIDs.contains(thread.record.objectID)
return !deletedObjectIDs.contains(thread.record.id)
default:
assertionFailure()
return false
@ -60,7 +58,7 @@ final class MastodonStatusThreadViewModel {
let newItems = items.filter { item in
switch item {
case .thread(let thread):
return !deletedObjectIDs.contains(thread.record.objectID)
return !deletedObjectIDs.contains(thread.record.id)
default:
assertionFailure()
return false
@ -81,14 +79,16 @@ extension MastodonStatusThreadViewModel {
nodes: [Node]
) {
let ids = nodes.map { $0.statusID }
var dictionary: [Status.ID: Status] = [:]
var dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:]
do {
let request = Status.sortedFetchRequest
request.predicate = Status.predicate(domain: domain, ids: ids)
let statuses = try self.context.managedObjectContext.fetch(request)
for status in statuses {
dictionary[status.id] = status
}
// let request = Status.sortedFetchRequest
// request.predicate = Status.predicate(domain: domain, ids: ids)
// let statuses = try self.context.managedObjectContext.fetch(request)
#warning("figure out what this does")
// for status in statuses {
// dictionary[status.id] = status
// }
} catch {
return
}
@ -98,9 +98,9 @@ extension MastodonStatusThreadViewModel {
guard let status = dictionary[node.statusID] else { continue }
let isLast = i == nodes.count - 1
let record = ManagedObjectRecord<Status>(objectID: status.objectID)
// let record = ManagedObjectRecord<Status>(objectID: status.objectID)
let context = StatusItem.Thread.Context(
status: record,
status: status,
displayUpperConversationLink: !isLast,
displayBottomConversationLink: true
)
@ -119,14 +119,15 @@ extension MastodonStatusThreadViewModel {
let childrenIDs = nodes
.map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } }
.flatMap { $0 }
var dictionary: [Status.ID: Status] = [:]
var dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:]
do {
let request = Status.sortedFetchRequest
request.predicate = Status.predicate(domain: domain, ids: childrenIDs)
let statuses = try self.context.managedObjectContext.fetch(request)
for status in statuses {
dictionary[status.id] = status
}
// let request = Status.sortedFetchRequest
// request.predicate = Status.predicate(domain: domain, ids: childrenIDs)
// let statuses = try self.context.managedObjectContext.fetch(request)
#warning("what is this???")
// for status in statuses {
// dictionary[status.id] = status
// }
} catch {
return
}
@ -135,9 +136,9 @@ extension MastodonStatusThreadViewModel {
for node in nodes {
guard let status = dictionary[node.statusID] else { continue }
// first tier
let record = ManagedObjectRecord<Status>(objectID: status.objectID)
// let record = ManagedObjectRecord<Status>(objectID: status.objectID)
let context = StatusItem.Thread.Context(
status: record
status: status
)
let item = StatusItem.thread(.leaf(context: context))
newItems.append(item)
@ -145,9 +146,9 @@ extension MastodonStatusThreadViewModel {
// second tier
if let child = node.children.first {
guard let secondaryStatus = dictionary[child.statusID] else { continue }
let secondaryRecord = ManagedObjectRecord<Status>(objectID: secondaryStatus.objectID)
// let secondaryRecord = ManagedObjectRecord<Status>(objectID: secondaryStatus.objectID)
let secondaryContext = StatusItem.Thread.Context(
status: secondaryRecord,
status: secondaryStatus,
displayUpperConversationLink: true
)
let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext))
@ -263,7 +264,7 @@ extension MastodonStatusThreadViewModel.Node {
}
extension MastodonStatusThreadViewModel {
func delete(objectIDs: [NSManagedObjectID]) {
func delete(objectIDs: [Mastodon.Entity.Status.ID]) {
var set = deletedObjectIDs
for objectID in objectIDs {
set.insert(objectID)

View File

@ -16,24 +16,22 @@ extension Account {
@MainActor
static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] {
// get accounts
let accounts: [Account] = try await managedObjectContext.perform {
let results = AuthenticationServiceProvider.shared.authentications
let accounts = results.compactMap { mastodonAuthentication -> Account? in
guard let user = mastodonAuthentication.user(in: managedObjectContext) else {
return nil
}
let account = Account(
identifier: mastodonAuthentication.identifier.uuidString,
display: user.displayNameWithFallback,
subtitle: user.acctWithDomain,
image: user.avatarImageURL().flatMap { INImage(url: $0) }
)
account.name = user.displayNameWithFallback
account.username = user.acctWithDomain
return account
let results = AuthenticationServiceProvider.shared.authentications
var accounts = [Account]()
for mastodonAuthentication in results {
guard let user = try? await mastodonAuthentication.me() else {
continue
}
return accounts
} // end managedObjectContext.perform
let account = Account(
identifier: mastodonAuthentication.identifier.uuidString,
display: user.displayNameWithFallback,
subtitle: user.acctWithDomain,
image: user.avatarImageURL().flatMap { INImage(url: $0) }
)
account.name = user.displayNameWithFallback
account.username = user.acctWithDomain
accounts.append(account)
}
return accounts
}

View File

@ -23,13 +23,20 @@ public class AuthenticationServiceProvider: ObservableObject {
}
}
func update(instance: Instance, where domain: String) {
func update(instance: Mastodon.Entity.Instance, where domain: String) {
authentications = authentications.map { authentication in
guard authentication.domain == domain else { return authentication }
return authentication.updating(instance: instance)
}
}
func update(instanceV2: Mastodon.Entity.V2.Instance, where domain: String) {
authentications = authentications.map { authentication in
guard authentication.domain == domain else { return authentication }
return authentication.updating(instanceV2: instanceV2)
}
}
func delete(authentication: MastodonAuthentication) {
authentications.removeAll(where: { $0 == authentication })
}

View File

@ -1,7 +1,6 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import CoreDataStack
import MastodonSDK
public struct MastodonAuthentication: Codable, Hashable {
@ -21,7 +20,9 @@ public struct MastodonAuthentication: Codable, Hashable {
public private(set) var activedAt: Date
public private(set) var userID: String
public private(set) var instanceObjectIdURI: URL?
public private(set) var instance: Mastodon.Entity.Instance?
public private(set) var instanceV2: Mastodon.Entity.V2.Instance?
internal var persistenceIdentifier: String {
"\(username)@\(domain)"
@ -34,7 +35,9 @@ public struct MastodonAuthentication: Codable, Hashable {
appAccessToken: String,
userAccessToken: String,
clientID: String,
clientSecret: String
clientSecret: String,
instance: Mastodon.Entity.Instance?,
instanceV2: Mastodon.Entity.V2.Instance?
) -> Self {
let now = Date()
return MastodonAuthentication(
@ -49,7 +52,8 @@ public struct MastodonAuthentication: Codable, Hashable {
updatedAt: now,
activedAt: now,
userID: userID,
instanceObjectIdURI: nil
instance: instance,
instanceV2: instanceV2
)
}
@ -65,7 +69,8 @@ public struct MastodonAuthentication: Codable, Hashable {
updatedAt: Date? = nil,
activedAt: Date? = nil,
userID: String? = nil,
instanceObjectIdURI: URL? = nil
instance: Mastodon.Entity.Instance? = nil,
instanceV2: Mastodon.Entity.V2.Instance? = nil
) -> Self {
MastodonAuthentication(
identifier: identifier ?? self.identifier,
@ -79,31 +84,28 @@ public struct MastodonAuthentication: Codable, Hashable {
updatedAt: updatedAt ?? self.updatedAt,
activedAt: activedAt ?? self.activedAt,
userID: userID ?? self.userID,
instanceObjectIdURI: instanceObjectIdURI ?? self.instanceObjectIdURI
instance: instance,
instanceV2: instanceV2
)
}
public func instance(in context: NSManagedObjectContext) -> Instance? {
guard let instanceObjectIdURI,
let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: instanceObjectIdURI)
else {
return nil
}
let instance = try? context.existingObject(with: objectID) as? Instance
return instance
func updating(instance: Mastodon.Entity.Instance) -> Self {
copy(instance: instance)
}
public func user(in context: NSManagedObjectContext) -> MastodonUser? {
let userPredicate = MastodonUser.predicate(domain: domain, id: userID)
return MastodonUser.findOrFetch(in: context, matching: userPredicate)
}
func updating(instance: Instance) -> Self {
copy(instanceObjectIdURI: instance.objectID.uriRepresentation())
func updating(instanceV2: Mastodon.Entity.V2.Instance) -> Self {
copy(instanceV2: instanceV2)
}
func updating(activatedAt: Date) -> Self {
copy(activedAt: activatedAt)
}
public func me() async throws -> Mastodon.Entity.Account {
try await Mastodon.API.Account.lookupAccount(
session: .shared, domain: domain,
query: .init(acct: userID),
authorization: Mastodon.API.OAuth.Authorization(accessToken: userAccessToken)
).singleOutput().value
}
}

View File

@ -7,15 +7,13 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService {
private struct MastodonBlockContext {
let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID
let sourceUserID: Mastodon.Entity.Account.ID
let targetUserID: Mastodon.Entity.Account.ID
let targetUsername: String
let isBlocking: Bool
let isFollowing: Bool
@ -41,113 +39,92 @@ extension APIService {
limit: limit,
authorization: authenticationBox.userAuthorization
).singleOutput()
let userIDs = response.value.map { $0.id }
let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs)
let fetchRequest = MastodonUser.fetchRequest()
fetchRequest.predicate = predicate
fetchRequest.includesPropertyValues = false
try await managedObjectContext.performChanges {
let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser]
for user in users {
user.deleteStatusAndNotificationFeeds(in: managedObjectContext)
}
}
return response
}
public func toggleBlock(
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let isBlocking = user.blockingBy.contains(me)
let isFollowing = user.followingBy.contains(me)
// toggle block state
user.update(isBlocking: !isBlocking, by: me)
// update follow state implicitly
if !isBlocking {
// will do block action. set to unfollow
user.update(isFollowing: false, by: me)
}
return MastodonBlockContext(
sourceUserID: me.id,
targetUserID: user.id,
targetUsername: user.username,
isBlocking: isBlocking,
isFollowing: isFollowing
)
}
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
do {
if blockContext.isBlocking {
let response = try await Mastodon.API.Account.unblock(
session: session,
domain: authenticationBox.domain,
accountID: blockContext.targetUserID,
authorization: authenticationBox.userAuthorization
).singleOutput()
result = .success(response)
} else {
let response = try await Mastodon.API.Account.block(
session: session,
domain: authenticationBox.domain,
accountID: blockContext.targetUserID,
authorization: authenticationBox.userAuthorization
).singleOutput()
result = .success(response)
}
} catch {
result = .failure(error)
}
try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else { return }
switch result {
case .success(let response):
let relationship = response.value
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: relationship,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isBlocking: blockContext.isBlocking, by: me)
user.update(isFollowing: blockContext.isFollowing, by: me)
}
}
let response = try result.get()
return response
}
// public func toggleBlock(
// user: Mastodon.Entity.Account,
// authenticationBox: MastodonAuthenticationBox
// ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
//
//// let managedObjectContext = backgroundManagedObjectContext
//// let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges {
//// let authentication = authenticationBox.authentication
////
// guard
//// let user = user.object(in: managedObjectContext),
// let me = authenticationBox.inMemoryCache.meAccount,
// let relationship = try await Mastodon.API.Account.relationships(
// session: session,
// domain: authenticationBox.domain,
// query: .init(ids: [user.id]),
// authorization: authenticationBox.userAuthorization
// ).singleOutput().value.first
// else {
// throw APIError.implicit(.badRequest)
// }
////
//// let isBlocking = user.blockingBy.contains(me)
//// let isFollowing = user.followingBy.contains(me)
//// // toggle block state
//// user.update(isBlocking: !isBlocking, by: me)
//// // update follow state implicitly
//// if !isBlocking {
//// // will do block action. set to unfollow
//// user.update(isFollowing: false, by: me)
//// }
////
//// return MastodonBlockContext(
//// sourceUserID: me.id,
//// targetUserID: user.id,
//// targetUsername: user.username,
//// isBlocking: isBlocking,
//// isFollowing: isFollowing
//// )
//// }
//
//
//
// let blockContext = MastodonBlockContext(
// sourceUserID: me.id,
// targetUserID: user.id,
// targetUsername: user.username,
// isBlocking: !relationship.blocking,
// isFollowing: {
// if !relationship.blocking {
// return false
// }
// return relationship.following
// }()
// )
//
// let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
// do {
// if blockContext.isBlocking {
// let response = try await Mastodon.API.Account.unblock(
// session: session,
// domain: authenticationBox.domain,
// accountID: blockContext.targetUserID,
// authorization: authenticationBox.userAuthorization
// ).singleOutput()
// result = .success(response)
// } else {
// let response = try await Mastodon.API.Account.block(
// session: session,
// domain: authenticationBox.domain,
// accountID: blockContext.targetUserID,
// authorization: authenticationBox.userAuthorization
// ).singleOutput()
// result = .success(response)
// }
// } catch {
// result = .failure(error)
// }
//
// let response = try result.get()
// return response
// }
public func toggleBlock(
user: Mastodon.Entity.Account,
@ -178,21 +155,3 @@ extension APIService {
return response
}
}
extension MastodonUser {
func deleteStatusAndNotificationFeeds(in context: NSManagedObjectContext) {
statuses.map {
$0.feeds
.union($0.reblogFrom.map { $0.feeds }.flatMap { $0 })
.union($0.notifications.map { $0.feeds }.flatMap { $0 })
}
.flatMap { $0 }
.forEach(context.delete)
notifications.map {
$0.feeds
}
.flatMap { $0 }
.forEach(context.delete)
}
}

View File

@ -7,111 +7,109 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService {
private struct MastodonFollowContext {
let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID
let sourceUserID: Mastodon.Entity.Account.ID
let targetUserID: Mastodon.Entity.Account.ID
let isFollowing: Bool
let isPending: Bool
let needsUnfollow: Bool
}
/// Toggle friendship between target MastodonUser and current MastodonUser
///
/// Following / Following pending <-> Unfollow
///
/// - Parameters:
/// - mastodonUser: target MastodonUser
/// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox`
/// - Returns: publisher for `Relationship`
public func toggleFollow(
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil }
guard let user = user.object(in: managedObjectContext) else { return nil }
let isFollowing = user.followingBy.contains(me)
let isPending = user.followRequestedBy.contains(me)
let needsUnfollow = isFollowing || isPending
if needsUnfollow {
// unfollow
user.update(isFollowing: false, by: me)
user.update(isFollowRequested: false, by: me)
} else {
// follow
if user.locked {
user.update(isFollowing: false, by: me)
user.update(isFollowRequested: true, by: me)
} else {
user.update(isFollowing: true, by: me)
user.update(isFollowRequested: false, by: me)
}
}
let context = MastodonFollowContext(
sourceUserID: me.id,
targetUserID: user.id,
isFollowing: isFollowing,
isPending: isPending,
needsUnfollow: needsUnfollow
)
return context
}
guard let followContext = _followContext else {
throw APIError.implicit(.badRequest)
}
// request follow or unfollow
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
do {
let response = try await Mastodon.API.Account.follow(
session: session,
domain: authenticationBox.domain,
accountID: followContext.targetUserID,
followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()),
authorization: authenticationBox.userAuthorization
).singleOutput()
result = .success(response)
} catch {
result = .failure(error)
}
// update friendship state
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext),
let user = user.object(in: managedObjectContext)
else { return }
switch result {
case .success(let response):
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
case .failure:
// rollback
user.update(isFollowing: followContext.isFollowing, by: me)
user.update(isFollowRequested: followContext.isPending, by: me)
}
}
let response = try result.get()
return response
}
// /// Toggle friendship between target MastodonUser and current MastodonUser
// ///
// /// Following / Following pending <-> Unfollow
// ///
// /// - Parameters:
// /// - mastodonUser: target MastodonUser
// /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox`
// /// - Returns: publisher for `Relationship`
// public func toggleFollow(
// user: ManagedObjectRecord<MastodonUser>,
// authenticationBox: MastodonAuthenticationBox
// ) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
//
// let managedObjectContext = backgroundManagedObjectContext
// let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
// guard let me = authenticationBox.inMemoryCache.meAccount else { return nil }
// guard let user = user.object(in: managedObjectContext) else { return nil }
//
// let isFollowing = user.followingBy.contains(me)
// let isPending = user.followRequestedBy.contains(me)
// let needsUnfollow = isFollowing || isPending
//
// if needsUnfollow {
// // unfollow
// user.update(isFollowing: false, by: me)
// user.update(isFollowRequested: false, by: me)
// } else {
// // follow
// if user.locked {
// user.update(isFollowing: false, by: me)
// user.update(isFollowRequested: true, by: me)
// } else {
// user.update(isFollowing: true, by: me)
// user.update(isFollowRequested: false, by: me)
// }
// }
// let context = MastodonFollowContext(
// sourceUserID: me.id,
// targetUserID: user.id,
// isFollowing: isFollowing,
// isPending: isPending,
// needsUnfollow: needsUnfollow
// )
// return context
// }
//
// guard let followContext = _followContext else {
// throw APIError.implicit(.badRequest)
// }
//
// // request follow or unfollow
// let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
// do {
// let response = try await Mastodon.API.Account.follow(
// session: session,
// domain: authenticationBox.domain,
// accountID: followContext.targetUserID,
// followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()),
// authorization: authenticationBox.userAuthorization
// ).singleOutput()
// result = .success(response)
// } catch {
// result = .failure(error)
// }
//
// // update friendship state
// try await managedObjectContext.performChanges {
// guard let me = authenticationBox.authentication.user(in: managedObjectContext),
// let user = user.object(in: managedObjectContext)
// else { return }
//
// switch result {
// case .success(let response):
// Persistence.MastodonUser.update(
// mastodonUser: user,
// context: Persistence.MastodonUser.RelationshipContext(
// entity: response.value,
// me: me,
// networkDate: response.networkDate
// )
// )
// case .failure:
// // rollback
// user.update(isFollowing: followContext.isFollowing, by: me)
// user.update(isFollowRequested: followContext.isPending, by: me)
// }
// }
//
// let response = try result.get()
// return response
// }
public func toggleFollow(
user: Mastodon.Entity.Account,

View File

@ -7,8 +7,6 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService {
@ -25,28 +23,7 @@ extension APIService {
query: query,
authorization: authenticationBox.userAuthorization
).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(
domain: authenticationBox.domain,
id: authenticationBox.userID
)
request.fetchLimit = 1
guard let user = managedObjectContext.safeFetch(request).first else { return }
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
Persistence.MastodonUser.update(
mastodonUser: user,
context: Persistence.MastodonUser.RelationshipContext(
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response
}

View File

@ -7,8 +7,6 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService {
@ -32,27 +30,7 @@ extension APIService {
query: query,
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
)
)
let user = result.user
me?.update(isFollowing: true, by: user)
}
}
return response
}

View File

@ -7,8 +7,6 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService {
@ -33,30 +31,7 @@ extension APIService {
query: query,
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
}

View File

@ -7,15 +7,13 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService {
private struct MastodonMuteContext {
let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID
let sourceUserID: Mastodon.Entity.Account.ID
let targetUserID: Mastodon.Entity.Account.ID
let targetUsername: String
let isMuting: Bool
}
@ -41,21 +39,6 @@ extension APIService {
authorization: authenticationBox.userAuthorization
).singleOutput()
let userIDs = response.value.map { $0.id }
let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs)
let fetchRequest = MastodonUser.fetchRequest()
fetchRequest.predicate = predicate
fetchRequest.includesPropertyValues = false
try await managedObjectContext.performChanges {
let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser]
for user in users {
user.deleteStatusAndNotificationFeeds(in: managedObjectContext)
}
}
return response
}

View File

@ -170,21 +170,7 @@ extension APIService {
notificationID: notificationID,
authorization: authorization
).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return }
_ = Persistence.Notification.createOrMerge(
in: managedObjectContext,
context: Persistence.Notification.PersistContext(
domain: domain,
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response
}

View File

@ -7,8 +7,6 @@
import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
extension APIService {
@ -27,7 +25,7 @@ extension APIService {
authorization: authorization
).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox)
return response
} // end func
public func followTag(
@ -44,7 +42,7 @@ extension APIService {
authorization: authorization
).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox)
return response
} // end func
public func unfollowTag(
@ -61,31 +59,6 @@ extension APIService {
authorization: authorization
).singleOutput()
return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox)
return response
} // end func
}
fileprivate extension APIService {
func persistTag(
from response: Mastodon.Response.Content<Mastodon.Entity.Tag>,
domain: String,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Tag> {
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
let me = authenticationBox.authentication.user(in: managedObjectContext)
_ = Persistence.Tag.createOrMerge(
in: managedObjectContext,
context: Persistence.Tag.PersistContext(
domain: domain,
entity: response.value,
me: me,
networkDate: response.networkDate
)
)
}
return response
}
}

View File

@ -7,8 +7,6 @@
import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
public final class InstanceService {
@ -16,7 +14,6 @@ public final class InstanceService {
var disposeBag = Set<AnyCancellable>()
// input
let backgroundManagedObjectContext: NSManagedObjectContext
weak var apiService: APIService?
weak var authenticationService: AuthenticationService?
@ -26,7 +23,6 @@ public final class InstanceService {
apiService: APIService,
authenticationService: AuthenticationService
) {
self.backgroundManagedObjectContext = apiService.backgroundManagedObjectContext
self.apiService = apiService
self.authenticationService = authenticationService
@ -68,57 +64,13 @@ extension InstanceService {
}
private func updateInstance(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
into: managedObjectContext,
domain: domain,
entity: response.value,
networkDate: response.networkDate
)
// update instance
AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
AuthenticationServiceProvider.shared.update(instance: response.value, where: domain)
return Just(Void()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
private func updateInstanceV2(domain: String, response: Mastodon.Response.Content<Mastodon.Entity.V2.Instance>) -> AnyPublisher<Void, Error> {
let managedObjectContext = self.backgroundManagedObjectContext
return managedObjectContext.performChanges {
// get instance
let (instance, _) = APIService.CoreData.createOrMergeInstance(
in: managedObjectContext,
context: .init(
domain: domain,
entity: response.value,
networkDate: response.networkDate
)
)
// update instance
AuthenticationServiceProvider.shared.update(instance: instance, where: domain)
}
.setFailureType(to: Error.self)
.tryMap { result in
switch result {
case .success:
break
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
AuthenticationServiceProvider.shared.update(instanceV2: response.value, where: domain)
return Just(Void()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
}

View File

@ -7,8 +7,6 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import MastodonCommon
import MastodonLocalization
@ -96,32 +94,29 @@ extension NotificationService {
extension NotificationService {
public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] {
guard let authenticationService = self.authenticationService else { return [] }
let managedObjectContext = authenticationService.managedObjectContext
return try await managedObjectContext.perform {
var items: [UIApplicationShortcutItem] = []
for authentication in AuthenticationServiceProvider.shared.authentications {
guard let user = authentication.user(in: managedObjectContext) else { continue }
let accessToken = authentication.userAccessToken
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
guard count > 0 else { continue }
let title = "@\(user.acctWithDomain)"
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
let item = UIApplicationShortcutItem(
type: NotificationService.unreadShortcutItemIdentifier,
localizedTitle: title,
localizedSubtitle: subtitle,
icon: nil,
userInfo: [
"accessToken": accessToken as NSSecureCoding
]
)
items.append(item)
}
return items
// guard let authenticationService = self.authenticationService else { return [] }
var items: [UIApplicationShortcutItem] = []
for authentication in AuthenticationServiceProvider.shared.authentications {
guard let user = try? await authentication.me() else { continue }
let accessToken = authentication.userAccessToken
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
guard count > 0 else { continue }
let title = "@\(user.acctWithDomain)"
let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
let item = UIApplicationShortcutItem(
type: NotificationService.unreadShortcutItemIdentifier,
localizedTitle: title,
localizedSubtitle: subtitle,
icon: nil,
userInfo: [
"accessToken": accessToken as NSSecureCoding
]
)
items.append(item)
}
return items
}
}

View File

@ -98,6 +98,15 @@ extension Mastodon.Entity.Account {
}
}
public var domainFromAcct: String {
if !acct.contains("@") {
return domain!
} else {
let domain = acct.split(separator: "@").last
return String(domain!)
}
}
public func acctWithDomainIfMissing(_ localDomain: String) -> String {
guard acct.contains("@") else {
return "\(acct)@\(localDomain)"

View File

@ -172,3 +172,21 @@ extension Mastodon.Entity.Instance.Configuration {
}
}
}
extension Mastodon.Entity.Instance: Hashable {
public static func == (lhs: Mastodon.Entity.Instance, rhs: Mastodon.Entity.Instance) -> Bool {
lhs.uri == rhs.uri
}
public func hash(into hasher: inout Hasher) {
hasher.combine(uri)
}
}
extension Mastodon.Entity.Instance.Configuration: Hashable {
public static func == (lhs: Mastodon.Entity.Instance.Configuration, rhs: Mastodon.Entity.Instance.Configuration) -> Bool {
true
}
public func hash(into hasher: inout Hasher) {}
}

View File

@ -110,3 +110,13 @@ extension Mastodon.Entity.V2.Instance {
public let account: Mastodon.Entity.Account?
}
}
extension Mastodon.Entity.V2.Instance: Hashable {
public static func == (lhs: Mastodon.Entity.V2.Instance, rhs: Mastodon.Entity.V2.Instance) -> Bool {
lhs.domain == rhs.domain
}
public func hash(into hasher: inout Hasher) {
hasher.combine(domain)
}
}

View File

@ -45,7 +45,6 @@ extension Mastodon.Entity {
case voted
case ownVotes = "own_votes"
case options
case isVoting
}
}
}

View File

@ -73,7 +73,6 @@ extension Mastodon.Entity {
case visibility
case sensitive
case sensitiveToggled
case spoilerText = "spoiler_text"
case mediaAttachments = "media_attachments"

View File

@ -153,21 +153,17 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
self.authContext = authContext
self.destination = destination
self.composeContext = composeContext
self.visibility = {
// default private when user locked
var visibility: Mastodon.Entity.Status.Visibility = {
guard let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else {
guard let author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount else {
return .public
}
return author.locked ? .private : .public
}()
// set visibility for reply post
if case .reply(let status) = destination {
// context.managedObjectContext.performAndWait {
// guard let status = record.object(in: context.managedObjectContext) else {
// assertionFailure()
// return
// }
let repliedStatusVisibility = status.visibility
switch repliedStatusVisibility {
case .public, .unlisted:
@ -225,7 +221,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// assertionFailure()
// return
// }
let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
let author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
var mentionAccts: [String] = []
if author?.id != status.account.id {
@ -258,11 +254,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// set limit
let _configuration: Mastodon.Entity.Instance.Configuration? = {
var configuration: Mastodon.Entity.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configuration
}
let authentication = authContext.mastodonAuthenticationBox.authentication
var configuration: Mastodon.Entity.Instance.Configuration? = authentication.instance?.configuration
return configuration
}()
if let configuration = _configuration {
@ -319,7 +312,7 @@ extension ComposeContentViewModel {
$authContext
.sink { [weak self] authContext in
guard let self = self else { return }
guard let user = authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return }
guard let user = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount else { return }
self.avatarURL = user.avatarImageURL()
self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback)
self.username = user.acctWithDomain
@ -563,10 +556,7 @@ extension ComposeContentViewModel {
// author
let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>?
managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
}
var _author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
guard let author = _author else {
throw AppError.badAuthentication
}
@ -619,10 +609,7 @@ extension ComposeContentViewModel {
// author
let managedObjectContext = self.context.managedObjectContext
var _author: ManagedObjectRecord<MastodonUser>?
managedObjectContext.performAndWait {
_author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord
}
var _author = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount
guard let author = _author else {
throw AppError.badAuthentication
}
@ -818,3 +805,28 @@ extension ComposeContentViewModel: AttachmentViewModelDelegate {
}
}
}
extension Mastodon.Entity.Account {
public var nameMetaContent: MastodonMetaContent? {
do {
let content = MastodonContent(content: displayNameWithFallback, emojis: emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure()
return nil
}
}
public var bioMetaContent: MastodonMetaContent? {
do {
let content = MastodonContent(content: note, emojis: emojis?.asDictionary ?? [:])
let metaContent = try MastodonMetaContent.convert(document: content)
return metaContent
} catch {
assertionFailure()
return nil
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK
import Combine
@ -11,7 +9,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
// Input
public let statusID: Mastodon.Entity.Status.ID
public let author: ManagedObjectRecord<MastodonUser>
public let author: Mastodon.Entity.Account
// content warning
public let isContentWarningComposing: Bool
@ -41,7 +39,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting {
public init(
statusID: Mastodon.Entity.Status.ID,
author: ManagedObjectRecord<MastodonUser>,
author: Mastodon.Entity.Account,
isContentWarningComposing: Bool,
contentWarning: String,
content: String,

View File

@ -7,8 +7,6 @@
import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK
@ -17,7 +15,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting {
// Input
// author
public let author: ManagedObjectRecord<MastodonUser>
public let author: Mastodon.Entity.Account?
// refer
public let replyTo: Mastodon.Entity.Status?
// content warning
@ -47,7 +45,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting {
public var reactor: StatusPublisherReactor?
public init(
author: ManagedObjectRecord<MastodonUser>,
author: Mastodon.Entity.Account,
replyTo: Mastodon.Entity.Status?,
isContentWarningComposing: Bool,
contentWarning: String,

View File

@ -220,7 +220,7 @@ extension NotificationView.ViewModel {
)
)
.sink { [weak self] authorName, isMuting, isBlocking, isMyselfIsTranslatedIsFollowed in
guard let name = authorName?.string, let self, let context = self.context, let authContext = self.authContext else {
guard let name = authorName?.string, let self, let authContext = self.authContext else {
notificationView.menuButton.menu = nil
return
}
@ -228,8 +228,7 @@ extension NotificationView.ViewModel {
let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed
let authentication = authContext.mastodonAuthenticationBox.authentication
let instance = authentication.instance(in: context.managedObjectContext)
let isTranslationEnabled = instance?.isTranslationEnabled ?? false
let isTranslationEnabled = authentication.instanceV2?.configuration?.translation?.enabled ?? false
let menuContext = NotificationView.AuthorMenuContext(
name: name,

View File

@ -7,8 +7,6 @@
import UIKit
import Combine
import CoreData
import CoreDataStack
import Meta
import MastodonAsset
import MastodonCore
@ -668,14 +666,13 @@ extension StatusView.ViewModel {
let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo
let (translatedFromLanguage, language) = tupleThree
guard let name = authorName?.string, let context = self.context, let authContext = self.authContext else {
guard let name = authorName?.string, let authContext = self.authContext else {
statusView.authorView.menuButton.menu = nil
return
}
let authentication = authContext.mastodonAuthenticationBox.authentication
let instance = authentication.instance(in: context.managedObjectContext)
let isTranslationEnabled = instance?.isTranslationEnabled ?? false
let isTranslationEnabled = authentication.instanceV2?.configuration?.translation?.enabled ?? false
let menuContext = StatusAuthorView.AuthorMenuContext(
name: name,

View File

@ -10,6 +10,11 @@ import Combine
import MastodonAsset
import MastodonLocalization
import MastodonSDK
import MastodonCore
enum RelationshipError: Error {
case FailedToResolveUser
}
public enum RelationshipAction: Int, CaseIterable {
case showReblogs
@ -127,8 +132,39 @@ public final class RelationshipViewModel {
relationshipUpdatePublisher
)
.receive(on: DispatchQueue.main)
.sink { [weak self] user, me, _ in
guard let self = self else { return }
.compactMap { user, me, _ -> Optional<(Mastodon.Entity.Account, Mastodon.Entity.Account, MastodonAuthentication)> in
guard let user, let me else { return nil }
guard let authBox = AuthenticationServiceProvider.shared.authenticationSortedByActivation().first else { return nil }
return (user, me, authBox)
}
.flatMap { (user, me, authBox) in
return Mastodon.API.Account.relationships(
session: .shared,
domain: authBox.domain,
query: .init(ids: [user.id]),
authorization: Mastodon.API.OAuth.Authorization(accessToken: authBox.userAccessToken)
).eraseToAnyPublisher()
}
.sink { completion in
// no-op
} receiveValue: { [weak self] response in
guard let self, let relationship = response.value.first else { return }
isMyself = relationship.id == me?.id
isFollowingBy = relationship.followedBy
isFollowing = relationship.following
isMuting = relationship.muting == true
isBlockingBy = relationship.blockedBy == true
isBlocking = relationship.blocking
showReblogs = relationship.showingReblogs == true
}
.store(in: &disposeBag)
// .sink { [weak self] relationship in
// guard let self = self else { return }
// self.update(user: user, me: me)
// guard let user = user, let me = me else {
@ -149,8 +185,7 @@ public final class RelationshipViewModel {
// guard let self = self else { return }
// self.relationshipUpdatePublisher.send()
// }
}
.store(in: &disposeBag)
// }
}
}

View File

@ -82,10 +82,10 @@ private extension FollowersCountWidgetProvider {
return completion(.unconfigured)
}
let meAcctDomain = try? await authBox.authentication.me().acctWithDomain
guard
let desiredAccount = configuration.account ?? authBox.authentication.user(
in: WidgetExtension.appContext.managedObjectContext
)?.acctWithDomain
let desiredAccount = configuration.account ?? meAcctDomain
else {
return completion(.unconfigured)
}

View File

@ -86,9 +86,7 @@ private extension MultiFollowersCountWidgetProvider {
if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) {
desiredAccounts = configuredAccounts
} else if let currentlyLoggedInAccount = authBox.authentication.user(
in: WidgetExtension.appContext.managedObjectContext
)?.acctWithDomain {
} else if let currentlyLoggedInAccount = try? await authBox.authentication.me().acctWithDomain {
desiredAccounts = [currentlyLoggedInAccount]
} else {
return completion(.unconfigured)