WIP: Comment out and replace user with status (IOS-192)

This commit is contained in:
Nathan Mattes 2023-12-07 13:35:33 +01:00
parent a9fc62eda4
commit 2be8d5b8df
32 changed files with 732 additions and 1112 deletions

View File

@ -120,11 +120,12 @@ extension DataSourceFacade {
extension DataSourceFacade {
static func responseToShowHideReblogAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
account: Mastodon.Entity.Account
) async throws {
_ = try await dependency.context.apiService.toggleShowReblogs(
for: user,
authenticationBox: dependency.authContext.mastodonAuthenticationBox)
#warning("TODO: Implement")
// _ = try await dependency.context.apiService.toggleShowReblogs(
// for: user,
// authenticationBox: dependency.authContext.mastodonAuthenticationBox)
}
static func responseToShowHideReblogAction(

View File

@ -6,20 +6,20 @@
//
import UIKit
import CoreDataStack
import MastodonSDK
import MastodonCore
extension DataSourceFacade {
static func responseToUserMuteAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
account: Mastodon.Entity.Account
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleMute(
user: user,
authenticationBox: dependency.authContext.mastodonAuthenticationBox
authenticationBox: dependency.authContext.mastodonAuthenticationBox,
account: account
)
} // end func
}

View File

@ -45,21 +45,67 @@ extension DataSourceFacade {
account: redirectRecord
)
}
@MainActor
static func coordinateToProfileScene(
provider: ViewControllerWithDependencies & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
username: String,
domain: String
) async {
guard let user = user.object(in: provider.context.managedObjectContext) else {
assertionFailure()
return
provider.coordinator.showLoading()
Task {
do {
guard let account = try await provider.context.apiService.fetchUser(username: username,
domain: domain,
authenticationBox: provider.authContext.mastodonAuthenticationBox) else {
return provider.coordinator.hideLoading()
}
provider.coordinator.hideLoading()
await coordinateToProfileScene(provider: provider, account: account)
} catch {
provider.coordinator.hideLoading()
}
}
}
@MainActor
static func coordinateToProfileScene(
provider: ViewControllerWithDependencies & AuthContextProvider,
domain: String,
accountID: String
) async {
provider.coordinator.showLoading()
Task {
do {
let account = try await provider.context.apiService.accountInfo(
domain: domain,
userID: accountID,
authorization: provider.authContext.mastodonAuthenticationBox.userAuthorization
).value
provider.coordinator.hideLoading()
await coordinateToProfileScene(provider: provider, account: account)
} catch {
provider.coordinator.hideLoading()
}
}
}
@MainActor
static func coordinateToProfileScene(
provider: ViewControllerWithDependencies & AuthContextProvider,
account: Mastodon.Entity.Account
) {
let profileViewModel = ProfileViewModel(
context: provider.context,
authContext: provider.authContext,
optionalMastodonUser: user
account: account
)
_ = provider.coordinator.present(
@ -68,31 +114,6 @@ extension DataSourceFacade {
transition: .show
)
}
@MainActor
static func coordinateToProfileScene(
provider: ViewControllerWithDependencies & AuthContextProvider,
account: Mastodon.Entity.Account
) async {
provider.coordinator.showLoading()
guard let domain = account.domain else { return provider.coordinator.hideLoading() }
Task {
do {
let user = try await provider.context.apiService.fetchUser(username: account.username,
domain: domain,
authenticationBox: provider.authContext.mastodonAuthenticationBox)
provider.coordinator.hideLoading()
if let user {
await coordinateToProfileScene(provider: provider, user: user.asRecord)
}
} catch {
provider.coordinator.hideLoading()
}
}
}
}
extension DataSourceFacade {
@ -112,42 +133,31 @@ extension DataSourceFacade {
else {
return
}
let mentions = status.entity.mentions ?? []
guard let mention = mentions.first(where: { $0.url == href }) else {
_ = provider.coordinator.present(
_ = provider.coordinator.present(
scene: .safari(url: url),
from: provider,
transition: .safariPresent(animated: true, completion: nil)
)
return
}
let userID = mention.id
let profileViewModel: ProfileViewModel = {
// check if self
guard userID != provider.authContext.mastodonAuthenticationBox.userID else {
return MeProfileViewModel(context: provider.context, authContext: provider.authContext)
}
let request = MastodonUser.sortedFetchRequest
request.fetchLimit = 1
request.predicate = MastodonUser.predicate(domain: domain, id: userID)
let _user = provider.context.managedObjectContext.safeFetch(request).first
if let user = _user {
return ProfileViewModel(context: provider.context, authContext: provider.authContext, optionalMastodonUser: user)
} else {
return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID)
}
}()
_ = provider.coordinator.present(
scene: .profile(viewModel: profileViewModel),
from: provider,
transition: .show
)
#warning("TODO: Implement")
await DataSourceFacade.coordinateToProfileScene(provider: provider, domain: "", accountID: mention.id)
// let profileViewModel = ProfileViewModel(
// context: provider.context,
// authContext: provider.authContext,
// account: status.entity.account
// )
//
// _ = provider.coordinator.present(
// scene: .profile(viewModel: profileViewModel),
// from: provider,
// transition: .show
// )
}
}
@ -166,20 +176,11 @@ extension DataSourceFacade {
static func createActivityViewController(
dependency: NeedsDependency,
user: ManagedObjectRecord<MastodonUser>
) async throws -> UIActivityViewController? {
let managedObjectContext = dependency.context.managedObjectContext
let activityItems: [Any] = try await managedObjectContext.perform {
guard let user = user.object(in: managedObjectContext) else { return [] }
return user.activityItems
}
guard !activityItems.isEmpty else {
assertionFailure()
return nil
}
let activityViewController = await UIActivityViewController(
activityItems: activityItems,
account: Mastodon.Entity.Account
) -> UIActivityViewController {
let activityViewController = UIActivityViewController(
activityItems: [account.url],
applicationActivities: [SafariActivity(sceneCoordinator: dependency.coordinator)]
)
return activityViewController

View File

@ -144,8 +144,7 @@ extension DataSourceFacade {
extension DataSourceFacade {
struct MenuContext {
let author: ManagedObjectRecord<MastodonUser>? // todo: Remove once IOS-192 is ready
let authorEntity: Mastodon.Entity.Account?
let author: Mastodon.Entity.Account
let statusViewModel: StatusView.ViewModel?
let button: UIButton?
let barButtonItem: UIBarButtonItem?
@ -176,17 +175,9 @@ extension DataSourceFacade {
guard let dependency else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
try await DataSourceFacade.responseToShowHideReblogAction(
dependency: dependency,
user: user
account: menuContext.author
)
}
}
@ -207,17 +198,11 @@ extension DataSourceFacade {
title: actionContext.isMuting ? L10n.Common.Controls.Friendship.unmute : L10n.Common.Controls.Friendship.mute,
style: .destructive
) { [weak dependency] _ in
guard let dependency = dependency else { return }
guard let dependency else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
try await DataSourceFacade.responseToUserMuteAction(
dependency: dependency,
user: user
account: menuContext.author
)
} // end Task
}
@ -235,19 +220,13 @@ extension DataSourceFacade {
title: actionContext.isBlocking ? L10n.Common.Controls.Friendship.unblock : L10n.Common.Controls.Friendship.block,
style: .destructive
) { [weak dependency] _ in
guard let dependency = dependency else { return }
guard let dependency else { return }
Task {
let managedObjectContext = dependency.context.managedObjectContext
let _user: ManagedObjectRecord<MastodonUser>? = try? await managedObjectContext.perform {
guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil }
return ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
}
guard let user = _user else { return }
try await DataSourceFacade.responseToUserBlockAction(
dependency: dependency,
user: user
user: menuContext.author
)
} // end Task
}
}
alertController.addAction(confirmAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
@ -255,12 +234,11 @@ extension DataSourceFacade {
dependency.present(alertController, animated: true)
case .reportUser:
Task {
guard let user = menuContext.author else { return }
let reportViewModel = ReportViewModel(
context: dependency.context,
authContext: dependency.authContext,
user: user,
account: menuContext.author,
status: menuContext.statusViewModel?.originalStatus
)
@ -272,15 +250,11 @@ extension DataSourceFacade {
} // end Task
case .shareUser:
guard let user = menuContext.author else {
assertionFailure()
return
}
let _activityViewController = try await DataSourceFacade.createActivityViewController(
let activityViewController = DataSourceFacade.createActivityViewController(
dependency: dependency,
user: user
account: menuContext.author
)
guard let activityViewController = _activityViewController else { return }
_ = dependency.coordinator.present(
scene: .activityViewController(
activityViewController: activityViewController,
@ -303,7 +277,6 @@ extension DataSourceFacade {
} // end Task
case .shareStatus:
Task {
let managedObjectContext = dependency.context.managedObjectContext
guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else {
assertionFailure()
return
@ -380,11 +353,8 @@ extension DataSourceFacade {
// do nothing, as the translation is reverted in `StatusTableViewCellDelegate` in `DataSourceProvider+StatusTableViewCellDelegate.swift`.
break
case .followUser(_):
guard let author = menuContext.author else { return }
try await DataSourceFacade.responseToUserFollowAction(dependency: dependency,
user: author)
user: menuContext.author)
}
} // end func
}

View File

@ -31,20 +31,11 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
return
}
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
return .init(objectID: notification.account.objectID)
}
guard let author = _author else {
assertionFailure()
return
}
try await DataSourceFacade.responseToMenuAction(
dependency: self,
action: action,
menuContext: .init(
author: author,
authorEntity: notification.entity.account,
author: notification.entity.account,
statusViewModel: nil,
button: button,
barButtonItem: nil
@ -71,16 +62,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
assertionFailure("only works for status data provider")
return
}
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
return .init(objectID: notification.account.objectID)
}
guard let author = _author else {
assertionFailure()
return
}
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: author
account: notification.entity.account
)
} // end Task
}
@ -322,7 +307,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: notification.account.asRecord
account: notification.entity.account
)
} // end Task
}
@ -494,21 +479,20 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
return
}
switch item {
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .user(let user):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: user
)
case .notification:
assertionFailure("TODO")
default:
assertionFailure("TODO")
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .user(let user):
break
case .account(let account, let relationship):
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
case .notification:
assertionFailure("TODO")
case .hashtag(_):
assertionFailure("TODO")
}
} // end Task
}

View File

@ -35,26 +35,14 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
}
switch await statusView.viewModel.header {
case .none:
break
case .reply:
let _replyToAuthor: ManagedObjectRecord<MastodonUser>? = try? await context.managedObjectContext.perform {
guard let inReplyToAccountID = status.entity.inReplyToAccountID else { return nil }
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: domain, id: inReplyToAccountID)
request.fetchLimit = 1
guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil }
return .init(objectID: author.objectID)
}
guard let replyToAuthor = _replyToAuthor else {
assertionFailure()
return
}
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: replyToAuthor
)
case .none:
break
case .reply:
guard let replyToAccountID = status.entity.inReplyToAccountID else { return }
#warning("TODO: Implement Domain")
await DataSourceFacade.coordinateToProfileScene(provider: self,
domain: "",
accountID: replyToAccountID)
case .repost:
await DataSourceFacade.coordinateToProfileScene(
@ -469,21 +457,9 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
assertionFailure("only works for status data provider")
return
}
let status = _status.reblog ?? _status
let _author: ManagedObjectRecord<MastodonUser>? = try await self.context.managedObjectContext.perform {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: self.authContext.mastodonAuthenticationBox.domain, id: status.entity.account.id)
request.fetchLimit = 1
guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil }
return .init(objectID: author.objectID)
}
guard let author = _author else {
assertionFailure()
return
}
if case .translateStatus = action {
DispatchQueue.main.async {
if let cell = cell as? StatusTableViewCell {
@ -517,8 +493,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
dependency: self,
action: action,
menuContext: .init(
author: author,
authorEntity: status.entity.account,
author: status.entity.account,
statusViewModel: statusViewModel,
button: button,
barButtonItem: nil
@ -709,21 +684,23 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
return
}
switch item {
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .user(let user):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: user
)
case .notification:
assertionFailure("TODO")
default:
assertionFailure("TODO")
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .account(let account, _):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
account: account
)
case .user(_):
assertionFailure("TODO")
case .notification:
assertionFailure("TODO")
case .hashtag(_):
assertionFailure("TODO")
}
}
}

View File

@ -24,43 +24,37 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
switch item {
case .account(let account, relationship: _):
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .user(let user):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: user
)
case .hashtag(let tag):
await DataSourceFacade.coordinateToHashtagScene(
provider: self,
tag: tag
)
case .notification(let notification):
let _status: MastodonStatus? = notification.status
if let status = _status {
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
target: .status, // remove reblog wrapper
status: status
)
} else {
let _author: ManagedObjectRecord<MastodonUser>? = notification.account.asRecord
if let author = _author {
case .user(let user):
break
case .hashtag(let tag):
await DataSourceFacade.coordinateToHashtagScene(
provider: self,
tag: tag
)
case .notification(let notification):
let managedObjectContext = context.managedObjectContext
let _status: MastodonStatus? = notification.status
if let status = _status {
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
} else {
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: author
)
account: notification.entity.account)
}
}
}
} // end Task
} // end func
} // end Task
} // end func
}
}
extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableViewController {

View File

@ -13,6 +13,7 @@ import class CoreDataStack.Notification
enum DataSourceItem: Hashable {
case status(record: MastodonStatus)
@available(*, deprecated, message: "Use .account")
case user(record: ManagedObjectRecord<MastodonUser>)
case hashtag(tag: Mastodon.Entity.Tag)
case notification(record: MastodonNotification)

View File

@ -295,25 +295,17 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
transition: .show
)
} else {
context.managedObjectContext.perform {
let mastodonUserRequest = MastodonUser.sortedFetchRequest
mastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: notification.account.id)
mastodonUserRequest.fetchLimit = 1
guard let mastodonUser = try? self.context.managedObjectContext.fetch(mastodonUserRequest).first else {
return
}
let profileViewModel = ProfileViewModel(
context: self.context,
authContext: self.viewModel.authContext,
optionalMastodonUser: mastodonUser
)
_ = self.coordinator.present(
scene: .profile(viewModel: profileViewModel),
from: self,
transition: .show
)
}
let profileViewModel = ProfileViewModel(
context: self.context,
authContext: self.viewModel.authContext,
account: notification.account
)
_ = self.coordinator.present(
scene: .profile(viewModel: profileViewModel),
from: self,
transition: .show
)
}
default:
break

View File

@ -19,7 +19,7 @@ final class ProfileAboutViewModel {
// input
let context: AppContext
@Published var user: MastodonUser?
@Published var account: Mastodon.Entity.Account?
@Published var isEditing = false
@Published var accountForEdit: Mastodon.Entity.Account?
@ -34,23 +34,22 @@ final class ProfileAboutViewModel {
init(context: AppContext) {
self.context = context
// end init
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.emojis) }
.map { $0.asDictionary }
.assign(to: &$emojiMeta)
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.fields) }
.assign(to: &$fields)
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.createdAt) }
.assign(to: &$createdAt)
#warning("TODO: Implement")
// $account
// .compactMap { $0 }
// .flatMap { $0.publisher(for: \.emojis) }
// .map { $0.asDictionary }
// .assign(to: &$emojiMeta)
//
// $account
// .compactMap { $0 }
// .flatMap { $0.publisher(for: \.fields) }
// .assign(to: &$fields)
//
// $account
// .compactMap { $0 }
// .flatMap { $0.publisher(for: \.createdAt) }
// .assign(to: &$createdAt)
Publishers.CombineLatest(
$fields,

View File

@ -128,17 +128,17 @@ extension ProfileHeaderViewController {
self.titleView.subtitleLabel.alpha = isTitleViewContentOffsetDidSet ? 1 : 0
}
.store(in: &disposeBag)
viewModel.$user
viewModel.$account
.receive(on: DispatchQueue.main)
.sink { [weak self] user in
guard let self = self else { return }
guard let user = user else { return }
.sink { [weak self] account in
guard let self, let account else { return }
self.profileHeaderView.prepareForReuse()
self.profileHeaderView.configuration(user: user)
self.profileHeaderView.configuration(account: account)
}
.store(in: &disposeBag)
viewModel.$relationshipActionOptionSet
.assign(to: \.relationshipActionOptionSet, on: profileHeaderView.viewModel)
viewModel.$relationship
.assign(to: \.relationship, on: profileHeaderView.viewModel)
.store(in: &disposeBag)
viewModel.$isMyself
.assign(to: \.isMyself, on: profileHeaderView.viewModel)
@ -269,35 +269,37 @@ extension ProfileHeaderViewController {
// MARK: - ProfileHeaderViewDelegate
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,
previewContext: DataSourceFacade.ImagePreviewContext(
imageView: button.avatarImageView,
containerView: .profileAvatar(profileHeaderView)
)
)
} // end Task
#warning("TODO: Implement")
// guard let user = viewModel.user else { return }
// let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
//
// Task {
// try await DataSourceFacade.coordinateToMediaPreviewScene(
// dependency: self,
// user: record,
// previewContext: DataSourceFacade.ImagePreviewContext(
// imageView: button.avatarImageView,
// containerView: .profileAvatar(profileHeaderView)
// )
// )
// } // end Task
}
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,
previewContext: DataSourceFacade.ImagePreviewContext(
imageView: imageView,
containerView: .profileBanner(profileHeaderView)
)
)
} // end Task
#warning("TODO: Implement")
// guard let account = viewModel.account else { return }
// let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
//
// Task {
// try await DataSourceFacade.coordinateToMediaPreviewScene(
// dependency: self,
// user: record,
// previewContext: DataSourceFacade.ImagePreviewContext(
// imageView: imageView,
// containerView: .profileBanner(profileHeaderView)
// )
// )
// } // end Task
}
func profileHeaderView(
@ -331,35 +333,39 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
// do nothing
break
case .follower:
guard let domain = viewModel.user?.domain,
let userID = viewModel.user?.id
else { return }
let followerListViewModel = FollowerListViewModel(
context: context,
authContext: viewModel.authContext,
domain: domain,
userID: userID
)
_ = coordinator.present(
scene: .follower(viewModel: followerListViewModel),
from: self,
transition: .show
)
#warning("TODO: Implement")
// guard let domain = viewModel.account.domain,
// let userID = viewModel.account.id
// else { return }
// let followerListViewModel = FollowerListViewModel(
// context: context,
// authContext: viewModel.authContext,
// domain: domain,
// userID: userID
// )
// _ = coordinator.present(
// scene: .follower(viewModel: followerListViewModel),
// from: self,
// transition: .show
// )
break
case .following:
guard let domain = viewModel.user?.domain,
let userID = viewModel.user?.id
else { return }
let followingListViewModel = FollowingListViewModel(
context: context,
authContext: viewModel.authContext,
domain: domain,
userID: userID
)
_ = coordinator.present(
scene: .following(viewModel: followingListViewModel),
from: self,
transition: .show
)
#warning("TODO: Implement")
// guard let domain = viewModel.account.domain,
// let userID = viewModel.account.id
// else { return }
// let followingListViewModel = FollowingListViewModel(
// context: context,
// authContext: viewModel.authContext,
// domain: domain,
// userID: userID
// )
// _ = coordinator.present(
// scene: .following(viewModel: followingListViewModel),
// from: self,
// transition: .show
// )
break
}
}

View File

@ -26,8 +26,8 @@ final class ProfileHeaderViewModel {
let context: AppContext
let authContext: AuthContext
@Published var user: MastodonUser?
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
@Published var account: Mastodon.Entity.Account?
@Published var relationship: Mastodon.Entity.Relationship?
@Published var isMyself = false
@Published var isEditing = false

View File

@ -7,49 +7,48 @@
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
extension ProfileHeaderView {
func configuration(user: MastodonUser) {
// header
user.publisher(for: \.header)
.map { _ in user.headerImageURL() }
.assign(to: \.headerImageURL, on: viewModel)
.store(in: &disposeBag)
// avatar
user.publisher(for: \.avatar)
.map { _ in user.avatarImageURL() }
.assign(to: \.avatarImageURL, on: viewModel)
.store(in: &disposeBag)
// emojiMeta
user.publisher(for: \.emojis)
.map { $0.asDictionary }
.assign(to: \.emojiMeta, on: viewModel)
.store(in: &disposeBag)
// name
user.publisher(for: \.displayName)
.map { _ in user.displayNameWithFallback }
.assign(to: \.name, on: viewModel)
.store(in: &disposeBag)
// username
viewModel.acct = user.acctWithDomain
// bio
user.publisher(for: \.note)
.assign(to: \.note, on: viewModel)
.store(in: &disposeBag)
// 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)
func configuration(account: Mastodon.Entity.Account) {
#warning("TODO: Implement")
// // header
// account.header.publisher
// .assign(to: \.headerImageURL, on: viewModel)
// .store(in: &disposeBag)
// // avatar
// account.avatar.publisher
// .assign(to: \.avatarImageURL, on: viewModel)
// .store(in: &disposeBag)
// // emojiMeta
// account.emojis.publisher
// .map { $0.asDictionary }
// .assign(to: \.emojiMeta, on: viewModel)
// .store(in: &disposeBag)
// // name
// account.publisher(for: \.displayName)
// .map { _ in account.displayNameWithFallback }
// .assign(to: \.name, on: viewModel)
// .store(in: &disposeBag)
// // username
// viewModel.acct = account.acctWithDomain
// // bio
// account.publisher(for: \.note)
// .assign(to: \.note, on: viewModel)
// .store(in: &disposeBag)
// // dashboard
// account.publisher(for: \.statusesCount)
// .map { Int($0) }
// .assign(to: \.statusesCount, on: viewModel)
// .store(in: &disposeBag)
// account.publisher(for: \.followingCount)
// .map { Int($0) }
// .assign(to: \.followingCount, on: viewModel)
// .store(in: &disposeBag)
// account.publisher(for: \.followersCount)
// .map { Int($0) }
// .assign(to: \.followersCount, on: viewModel)
// .store(in: &disposeBag)
}
}

View File

@ -14,6 +14,7 @@ import MastodonCore
import MastodonUI
import MastodonAsset
import MastodonLocalization
import MastodonSDK
extension ProfileHeaderView {
class ViewModel: ObservableObject {
@ -45,15 +46,16 @@ extension ProfileHeaderView {
@Published var fields: [MastodonField] = []
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
@Published var relationship: Mastodon.Entity.Relationship?
@Published var isRelationshipActionButtonHidden = false
@Published var isMyself = false
init() {
$relationshipActionOptionSet
.compactMap { $0.highPriorityAction(except: []) }
.map { $0 == .none }
.assign(to: &$isRelationshipActionButtonHidden)
#warning("TODO: Implement")
// $relationshipActionOptionSet
// .compactMap { $0.highPriorityAction(except: []) }
// .map { $0 == .none }
// .assign(to: &$isRelationshipActionButtonHidden)
}
}
}
@ -96,13 +98,14 @@ extension ProfileHeaderView.ViewModel {
}
.store(in: &disposeBag)
// follows you
$relationshipActionOptionSet
.map { $0.contains(.followingBy) && !$0.contains(.isMyself) }
.receive(on: DispatchQueue.main)
.sink { isFollowingBy in
view.followsYouBlurEffectView.isHidden = !isFollowingBy
}
.store(in: &disposeBag)
#warning("TODO: Implement")
// $relationshipActionOptionSet
// .map { $0.contains(.followingBy) && !$0.contains(.isMyself) }
// .receive(on: DispatchQueue.main)
// .sink { isFollowingBy in
// view.followsYouBlurEffectView.isHidden = !isFollowingBy
// }
// .store(in: &disposeBag)
// avatar
Publishers.CombineLatest4(
$avatarImageURL,
@ -117,18 +120,19 @@ extension ProfileHeaderView.ViewModel {
))
}
.store(in: &disposeBag)
// blur for blocking & blockingBy
$relationshipActionOptionSet
.map { $0.contains(.blocking) || $0.contains(.blockingBy) }
.sink { needsImageOverlayBlurred in
UIView.animate(withDuration: 0.33) {
let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil
view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect
let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil
view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect
}
}
.store(in: &disposeBag)
#warning("TODO: Implement")
// // blur for blocking & blockingBy
// $relationshipActionOptionSet
// .map { $0.contains(.blocking) || $0.contains(.blockingBy) }
// .sink { needsImageOverlayBlurred in
// UIView.animate(withDuration: 0.33) {
// let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil
// view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect
// let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil
// view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect
// }
// }
// .store(in: &disposeBag)
// name
Publishers.CombineLatest4(
$isEditing.removeDuplicates(),
@ -182,17 +186,18 @@ extension ProfileHeaderView.ViewModel {
view.bioMetaText.configure(content: metaContent)
}
.store(in: &disposeBag)
$relationshipActionOptionSet
.receive(on: DispatchQueue.main)
.sink { optionSet in
let isBlocking = optionSet.contains(.blocking)
let isBlockedBy = optionSet.contains(.blockingBy)
let isSuspended = optionSet.contains(.suspended)
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
view.bioMetaText.textView.isHidden = isNeedsHidden
}
.store(in: &disposeBag)
#warning("TODO: Implement")
// $relationshipActionOptionSet
// .receive(on: DispatchQueue.main)
// .sink { optionSet in
// let isBlocking = optionSet.contains(.blocking)
// let isBlockedBy = optionSet.contains(.blockingBy)
// let isSuspended = optionSet.contains(.suspended)
// let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
//
// view.bioMetaText.textView.isHidden = isNeedsHidden
// }
// .store(in: &disposeBag)
// dashboard
$isMyself
.receive(on: DispatchQueue.main)
@ -245,22 +250,23 @@ extension ProfileHeaderView.ViewModel {
$isRelationshipActionButtonHidden
.assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer)
.store(in: &disposeBag)
Publishers.CombineLatest3(
$relationshipActionOptionSet,
$isEditing,
$isUpdating
)
.receive(on: DispatchQueue.main)
.sink { relationshipActionOptionSet, isEditing, isUpdating in
if relationshipActionOptionSet.contains(.edit) {
// check .edit state and set .editing when isEditing
view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit))
view.configure(state: isEditing ? .editing : .normal)
} else {
view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet)
}
}
.store(in: &disposeBag)
#warning("TODO: Implement")
// Publishers.CombineLatest2(
// $relationshipActionOptionSet,
// $isEditing,
// $isUpdating
// )
// .receive(on: DispatchQueue.main)
// .sink { relationshipActionOptionSet, isEditing, isUpdating in
// if relationshipActionOptionSet.contains(.edit) {
// // check .edit state and set .editing when isEditing
// view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit))
// view.configure(state: isEditing ? .editing : .normal)
// } else {
// view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet)
// }
// }
// .store(in: &disposeBag)
}
}

View File

@ -542,27 +542,3 @@ extension ProfileHeaderView: ProfileStatusDashboardViewDelegate {
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, dashboardMeterViewDidPressed: dashboardMeterView, meter: meter)
}
}
#if DEBUG
import SwiftUI
struct ProfileHeaderView_Previews: PreviewProvider {
static var previews: some View {
Group {
UIViewPreview(width: 375) {
let banner = ProfileHeaderView()
banner.bannerImageView.image = UIImage(named: "lucas-ludwig")
return banner
}
.previewLayout(.fixed(width: 375, height: 800))
UIViewPreview(width: 375) {
let banner = ProfileHeaderView()
//banner.bannerImageView.image = UIImage(named: "peter-luo")
return banner
}
.preferredColorScheme(.dark)
.previewLayout(.fixed(width: 375, height: 800))
}
}
}
#endif

View File

@ -16,19 +16,12 @@ final class MeProfileViewModel: ProfileViewModel {
@MainActor
init(context: AppContext, authContext: AuthContext) {
let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
let me = authContext.mastodonAuthenticationBox.authentication.account()
super.init(
context: context,
authContext: authContext,
optionalMastodonUser: user
account: me
)
$me
.sink { [weak self] me in
guard let self = self else { return }
self.user = me
}
.store(in: &disposeBag)
}
override func viewDidLoad() {
@ -37,17 +30,9 @@ final class MeProfileViewModel: ProfileViewModel {
Task {
do {
_ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
try await context.managedObjectContext.performChanges {
guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else {
assertionFailure()
return
}
self.me = me
}
let account = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
self.account = account
self.me = account
} catch {
// do nothing?
}

View File

@ -203,7 +203,7 @@ extension ProfileViewController {
.store(in: &disposeBag)
Publishers.CombineLatest4 (
viewModel.relationshipViewModel.$isSuspended,
viewModel.account.suspended.publisher,
profileHeaderViewController.viewModel.$isTitleViewDisplaying,
editingAndUpdatingPublisher.eraseToAnyPublisher(),
barButtonItemHiddenPublisher.eraseToAnyPublisher()
@ -296,43 +296,43 @@ extension ProfileViewController {
private func bindViewModel() {
// header
#warning("TODO: Implement")
let headerViewModel = profileHeaderViewController.viewModel!
viewModel.$user
.assign(to: \.user, on: headerViewModel)
.store(in: &disposeBag)
// viewModel.$account
// .assign(to: \.account, on: headerViewModel)
// .store(in: &disposeBag)
viewModel.$isEditing
.assign(to: \.isEditing, on: headerViewModel)
.store(in: &disposeBag)
viewModel.$isUpdating
.assign(to: \.isUpdating, on: headerViewModel)
.store(in: &disposeBag)
viewModel.relationshipViewModel.$isMyself
.assign(to: \.isMyself, on: headerViewModel)
.store(in: &disposeBag)
viewModel.relationshipViewModel.$optionSet
.map { $0 ?? .none }
.assign(to: \.relationshipActionOptionSet, on: headerViewModel)
// viewModel.relationshipViewModel.$isMyself
// .assign(to: \.isMyself, on: headerViewModel)
// .store(in: &disposeBag)
viewModel.$relationship
.assign(to: \.relationship, on: headerViewModel)
.store(in: &disposeBag)
viewModel.$accountForEdit
.assign(to: \.accountForEdit, on: headerViewModel)
.store(in: &disposeBag)
#warning("TODO: Implement")
// timeline
[
viewModel.postsUserTimelineViewModel,
viewModel.repliesUserTimelineViewModel,
viewModel.mediaUserTimelineViewModel,
].forEach { userTimelineViewModel in
viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag)
viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag)
viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag)
}
// [
// viewModel.postsUserTimelineViewModel,
// viewModel.repliesUserTimelineViewModel,
// viewModel.mediaUserTimelineViewModel,
// ].forEach { userTimelineViewModel in
// viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag)
// viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag)
// viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag)
// }
// about
let aboutViewModel = viewModel.profileAboutViewModel
viewModel.$user
.assign(to: \.user, on: aboutViewModel)
.store(in: &disposeBag)
// viewModel.$account
// .assign(to: \.account, on: aboutViewModel)
// .store(in: &disposeBag)
viewModel.$isEditing
.assign(to: \.isEditing, on: aboutViewModel)
.store(in: &disposeBag)
@ -374,7 +374,7 @@ extension ProfileViewController {
}
.store(in: &disposeBag)
Publishers.CombineLatest(
profileHeaderViewController.viewModel.$user,
profileHeaderViewController.viewModel.$account,
profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear
)
.sink { [weak self] (user, _) in
@ -382,7 +382,7 @@ extension ProfileViewController {
Task {
_ = try await self.context.apiService.fetchUser(
username: user.username,
domain: user.domainFromAcct,
domain: "user.domainFromAcct",
authenticationBox: self.authContext.mastodonAuthenticationBox
)
}
@ -392,26 +392,23 @@ extension ProfileViewController {
private func bindMoreBarButtonItem() {
Publishers.CombineLatest(
viewModel.$user,
viewModel.relationshipViewModel.$optionSet
viewModel.$account,
viewModel.$relationship
)
.asyncMap { [weak self] user, relationshipSet -> UIMenu? in
guard let self = self else { return nil }
guard let user = user else {
return nil
}
.asyncMap { [weak self] user, relationship -> UIMenu? in
guard let self, let relationship else { 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)),
.blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
.muteUser(.init(name: name, isMuting: relationship.muting ?? false)),
.blockUser(.init(name: name, isBlocking: relationship.blocking)),
.reportUser(.init(name: name)),
.shareUser(.init(name: name)),
]
if let me = self.viewModel?.me, me.following.contains(user) {
let showReblogs = me.showingReblogsBy.contains(user)
if relationship.following {
let showReblogs = relationship.showingReblogs ?? false// me.showingReblogsBy.contains(user)
let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs)
menuActions.insert(.hideReblogs(context), at: 1)
}
@ -473,26 +470,6 @@ extension ProfileViewController {
.store(in: &disposeBag)
}
// private func bindProfileRelationship() {
//
// Publishers.CombineLatest3(
// viewModel.isBlocking.eraseToAnyPublisher(),
// viewModel.isBlockedBy.eraseToAnyPublisher(),
// viewModel.suspended.eraseToAnyPublisher()
// )
// .receive(on: DispatchQueue.main)
// .sink { [weak self] isBlocking, isBlockedBy, suspended in
// guard let self = self else { return }
// let isNeedSetHidden = isBlocking || isBlockedBy || suspended
// self.profileHeaderViewController.viewModel.needsSetupBottomShadow.value = !isNeedSetHidden
// self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden
// self.profileHeaderViewController.viewModel.needsFiledCollectionViewHidden.value = isNeedSetHidden
// self.profileHeaderViewController.buttonBar.isUserInteractionEnabled = !isNeedSetHidden
// self.viewModel.needsPagePinToTop.value = isNeedSetHidden
// }
// .store(in: &disposeBag)
// } // end func bindProfileRelationship
private func handleMetaPress(_ meta: Meta) {
switch meta {
case .url(_, _, let url, _):
@ -525,24 +502,19 @@ 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
)
guard let activityViewController = _activityViewController else { return }
_ = self.coordinator.present(
scene: .activityViewController(
activityViewController: activityViewController,
sourceView: nil,
barButtonItem: sender
),
from: self,
transition: .activityViewControllerPresent(animated: true, completion: nil)
)
} // end Task
let activityViewController = DataSourceFacade.createActivityViewController(
dependency: self,
account: viewModel.account
)
_ = self.coordinator.present(
scene: .activityViewController(
activityViewController: activityViewController,
sourceView: nil,
barButtonItem: sender
),
from: self,
transition: .activityViewControllerPresent(animated: true, completion: nil)
)
}
@objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) {
@ -556,8 +528,8 @@ extension ProfileViewController {
}
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
guard let mastodonUser = viewModel.user else { return }
let mention = "@" + mastodonUser.acct
let mention = "@" + viewModel.account.acct
UITextChecker.learnWord(mention)
let composeViewModel = ComposeViewModel(
context: context,
@ -683,34 +655,6 @@ extension ProfileViewController: TabBarPagerDataSource {
}
}
//// MARK: - UIScrollViewDelegate
//extension ProfileViewController: UIScrollViewDelegate {
//
// func scrollViewDidScroll(_ scrollView: UIScrollView) {
// contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y
// let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top
// if scrollView.contentOffset.y < topMaxContentOffsetY {
// self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
// for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers {
// postTimelineView.scrollView?.contentOffset.y = 0
// }
// contentOffsets.removeAll()
// } else {
// containerScrollView.contentOffset.y = topMaxContentOffsetY
// if viewModel.needsPagePinToTop.value {
// // do nothing
// } else {
// if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer {
// let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y
// customScrollViewContainerController.scrollView?.contentOffset.y = contentOffsetY
// }
// }
//
// }
// }
//
//}
// MARK: - AuthContextProvider
extension ProfileViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
@ -723,140 +667,140 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
profileHeaderView: ProfileHeaderView,
relationshipButtonDidPressed button: ProfileRelationshipActionButton
) {
let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none
// let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none
#warning("TODO: Implement")
// handle edit logic for editable profile
// handle relationship logic for non-editable profile
if relationshipActionSet.contains(.edit) {
// do nothing when updating
guard !viewModel.isUpdating else { return }
guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return }
guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return }
let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited
if isEdited {
// update profile when edited
viewModel.isUpdating = true
Task { @MainActor in
do {
// TODO: handle error
_ = try await viewModel.updateProfileInfo(
headerProfileInfo: profileHeaderViewModel.profileInfoEditing,
aboutProfileInfo: profileAboutViewModel.profileInfoEditing
)
self.viewModel.isEditing = false
} catch {
let alertController = UIAlertController(
for: error,
title: L10n.Common.Alerts.EditProfileFailure.title,
preferredStyle: .alert
)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
alertController.addAction(okAction)
self.present(alertController, animated: true)
}
// finish updating
self.viewModel.isUpdating = false
} // end Task
} else {
// set `updating` then toggle `edit` state
viewModel.isUpdating = true
viewModel.fetchEditProfileInfo()
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
defer {
// finish updating
self.viewModel.isUpdating = false
}
switch completion {
case .failure(let error):
let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert)
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
alertController.addAction(okAction)
_ = self.coordinator.present(
scene: .alertController(alertController: alertController),
from: nil,
transition: .alertController(animated: true, completion: nil)
)
case .finished:
// enter editing mode
self.viewModel.isEditing.toggle()
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
self.viewModel.accountForEdit = response.value
}
.store(in: &disposeBag)
}
} else {
guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return }
switch relationshipAction {
case .none:
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
)
}
case .muting:
guard let user = viewModel.user else { return }
let name = user.displayNameWithFallback
let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
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
)
}
}
alertController.addAction(unmuteAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
case .blocking:
guard let user = viewModel.user else { return }
let name = user.displayNameWithFallback
let alertController = UIAlertController(
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title,
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
)
}
}
alertController.addAction(unblockAction)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating:
break
}
}
// if relationshipActionSet.contains(.edit) {
// // do nothing when updating
// guard !viewModel.isUpdating else { return }
//
// guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return }
// guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return }
//
// let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited
//
// if isEdited {
// // update profile when edited
// viewModel.isUpdating = true
// Task { @MainActor in
// do {
// // TODO: handle error
// _ = try await viewModel.updateProfileInfo(
// headerProfileInfo: profileHeaderViewModel.profileInfoEditing,
// aboutProfileInfo: profileAboutViewModel.profileInfoEditing
// )
// self.viewModel.isEditing = false
//
// } catch {
// let alertController = UIAlertController(
// for: error,
// title: L10n.Common.Alerts.EditProfileFailure.title,
// preferredStyle: .alert
// )
// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
// alertController.addAction(okAction)
// self.present(alertController, animated: true)
// }
//
// // finish updating
// self.viewModel.isUpdating = false
// } // end Task
// } else {
// // set `updating` then toggle `edit` state
// viewModel.isUpdating = true
// viewModel.fetchEditProfileInfo()
// .receive(on: DispatchQueue.main)
// .sink { [weak self] completion in
// guard let self = self else { return }
// defer {
// // finish updating
// self.viewModel.isUpdating = false
// }
// switch completion {
// case .failure(let error):
// let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert)
// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
// alertController.addAction(okAction)
// _ = self.coordinator.present(
// scene: .alertController(alertController: alertController),
// from: nil,
// transition: .alertController(animated: true, completion: nil)
// )
// case .finished:
// // enter editing mode
// self.viewModel.isEditing.toggle()
// }
// } receiveValue: { [weak self] response in
// guard let self = self else { return }
// self.viewModel.accountForEdit = response.value
// }
// .store(in: &disposeBag)
// }
// } else {
// guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return }
// switch relationshipAction {
// case .none:
// 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
// )
// }
// case .muting:
// guard let user = viewModel.user else { return }
// let name = user.displayNameWithFallback
//
// let alertController = UIAlertController(
// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
// 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
// )
// }
// }
// alertController.addAction(unmuteAction)
// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
// alertController.addAction(cancelAction)
// present(alertController, animated: true, completion: nil)
// case .blocking:
// guard let user = viewModel.user else { return }
// let name = user.displayNameWithFallback
//
// let alertController = UIAlertController(
// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title,
// 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
// )
// }
// }
// alertController.addAction(unblockAction)
// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
// alertController.addAction(cancelAction)
// present(alertController, animated: true, completion: nil)
// case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating:
// break
// }
// }
}
@ -885,23 +829,19 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate {
// MARK: - MastodonMenuDelegate
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,
authorEntity: nil,
author: viewModel.account,
statusViewModel: nil,
button: nil,
barButtonItem: self.moreMenuBarButtonItem
)
)
} // end Task
}
}
}

View File

@ -34,21 +34,16 @@ class ProfileViewModel: NSObject {
let context: AppContext
let authContext: AuthContext
@available(*, deprecated, message: "Replace with Account")
@Published var me: MastodonUser?
@Published var me: Mastodon.Entity.Account?
@Published var account: Mastodon.Entity.Account
@Published var relationship: Mastodon.Entity.Relationship?
@available(*, deprecated, message: "Replace with Account")
@Published var user: MastodonUser?
let viewDidAppear = PassthroughSubject<Void, Never>()
@Published var isEditing = false
@Published var isUpdating = false
@Published var accountForEdit: Mastodon.Entity.Account?
// output
let relationshipViewModel = RelationshipViewModel()
@Published var userIdentifier: UserIdentifier? = nil
@Published var isRelationshipActionButtonHidden: Bool = true
@ -61,10 +56,10 @@ class ProfileViewModel: NSObject {
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
@MainActor
init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) {
init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account?) {
self.context = context
self.authContext = authContext
self.user = mastodonUser
self.account = account!
self.postsUserTimelineViewModel = UserTimelineViewModel(
context: context,
authContext: authContext,
@ -87,93 +82,86 @@ class ProfileViewModel: NSObject {
super.init()
// bind me
self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext)
$me
.assign(to: \.me, on: relationshipViewModel)
.store(in: &disposeBag)
self.me = authContext.mastodonAuthenticationBox.authentication.account()
// bind user
$user
$account
.map { user -> UserIdentifier? in
guard let user = user else { return nil }
return MastodonUserIdentifier(domain: user.domain, userID: user.id)
guard let account, let domain = account.domain else { return nil }
return MastodonUserIdentifier(domain: domain, userID: account.id)
}
.assign(to: &$userIdentifier)
$user
.assign(to: \.user, on: relationshipViewModel)
.store(in: &disposeBag)
// bind userIdentifier
$userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
$userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
$userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
// bind bar button items
relationshipViewModel.$optionSet
.sink { [weak self] optionSet in
guard let self = self else { return }
guard let optionSet = optionSet, !optionSet.contains(.none) else {
self.isReplyBarButtonItemHidden = true
self.isMoreMenuBarButtonItemHidden = true
self.isMeBarButtonItemsHidden = true
return
}
let isMyself = optionSet.contains(.isMyself)
self.isReplyBarButtonItemHidden = isMyself
self.isMoreMenuBarButtonItemHidden = isMyself
self.isMeBarButtonItemsHidden = !isMyself
}
.store(in: &disposeBag)
#warning("TODO: Implement")
// relationshipViewModel.$optionSet
// .sink { [weak self] optionSet in
// guard let self = self else { return }
// guard let optionSet = optionSet, !optionSet.contains(.none) else {
// self.isReplyBarButtonItemHidden = true
// self.isMoreMenuBarButtonItemHidden = true
// self.isMeBarButtonItemsHidden = true
// return
// }
//
// let isMyself = optionSet.contains(.isMyself)
// self.isReplyBarButtonItemHidden = isMyself
// self.isMoreMenuBarButtonItemHidden = isMyself
// self.isMeBarButtonItemsHidden = !isMyself
// }
// .store(in: &disposeBag)
// query relationship
let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
}
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
#warning("TODO: Implement")
// let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
// observe friendship
Publishers.CombineLatest(
userRecord,
pendingRetryPublisher
)
.sink { [weak self] userRecord, _ in
guard let self = self else { return }
guard let userRecord = userRecord else { return }
Task {
do {
let response = try await self.updateRelationship(
record: userRecord,
authenticationBox: self.authContext.mastodonAuthenticationBox
)
// there are seconds delay after request follow before requested -> following. Query again when needs
guard let relationship = response.value.first else { return }
if relationship.requested == true {
let delay = pendingRetryPublisher.value
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let _ = self else { return }
pendingRetryPublisher.value = min(2 * delay, 60)
}
}
} catch {
}
} // end Task
}
.store(in: &disposeBag)
// // observe friendship
// Publishers.CombineLatest(
// account,
// pendingRetryPublisher
// )
// .sink { [weak self] account, _ in
// guard let self, let account else { return }
//
// Task {
// do {
// let response = try await self.updateRelationship(
// account: account,
// authenticationBox: self.authContext.mastodonAuthenticationBox
// )
// // there are seconds delay after request follow before requested -> following. Query again when needs
// guard let relationship = response.value.first else { return }
// if relationship.requested == true {
// let delay = pendingRetryPublisher.value
// DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
// guard let _ = self else { return }
// pendingRetryPublisher.value = min(2 * delay, 60)
// }
// }
// } catch {
// }
// } // end Task
// }
// .store(in: &disposeBag)
let isBlockingOrBlocked = Publishers.CombineLatest(
relationshipViewModel.$isBlocking,
relationshipViewModel.$isBlockingBy
)
.map { $0 || $1 }
.share()
Publishers.CombineLatest(
isBlockingOrBlocked,
$isEditing
)
.map { !$0 && !$1 }
.assign(to: &$isPagingEnabled)
// let isBlockingOrBlocked = Publishers.CombineLatest(
// relationshipViewModel.$isBlocking,
// relationshipViewModel.$isBlockingBy
// )
// .map { $0 || $1 }
// .share()
//
// Publishers.CombineLatest(
// isBlockingOrBlocked,
// $isEditing
// )
// .map { !$0 && !$1 }
// .assign(to: &$isPagingEnabled)
}
@ -183,21 +171,21 @@ class ProfileViewModel: NSObject {
// fetch profile info before edit
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
guard let me else {
guard let me, let domain = me.domain else {
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
}
let mastodonAuthentication = authContext.mastodonAuthenticationBox.authentication
let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization)
return context.apiService.accountVerifyCredentials(domain: domain, authorization: authorization)
}
private func updateRelationship(
record: ManagedObjectRecord<MastodonUser>,
account: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
let response = try await context.apiService.relationship(
records: [record],
forAccounts: [account],
authenticationBox: authenticationBox
)
return response

View File

@ -15,7 +15,7 @@ final class RemoteProfileViewModel: ProfileViewModel {
@MainActor
init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) {
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
super.init(context: context, authContext: authContext, account: nil)
let domain = authContext.mastodonAuthenticationBox.domain
let authorization = authContext.mastodonAuthenticationBox.userAuthorization
@ -38,62 +38,28 @@ final class RemoteProfileViewModel: ProfileViewModel {
break
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
let managedObjectContext = context.managedObjectContext
let request = MastodonUser.sortedFetchRequest
request.fetchLimit = 1
request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id)
guard let mastodonUser = managedObjectContext.safeFetch(request).first else {
assertionFailure()
return
}
self.user = mastodonUser
self?.account = response.value
}
.store(in: &disposeBag)
}
@MainActor
init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) {
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
super.init(context: context, authContext: authContext, account: nil)
Task { @MainActor in
let response = try await context.apiService.notification(
notificationID: notificationID,
authenticationBox: authContext.mastodonAuthenticationBox
)
let userID = response.value.account.id
let _user: MastodonUser? = try await context.managedObjectContext.perform {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
request.fetchLimit = 1
return context.managedObjectContext.safeFetch(request).first
}
if let user = _user {
self.user = user
} else {
_ = try await context.apiService.accountInfo(
domain: authContext.mastodonAuthenticationBox.domain,
userID: userID,
authorization: authContext.mastodonAuthenticationBox.userAuthorization
)
let _user: MastodonUser? = try await context.managedObjectContext.perform {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
request.fetchLimit = 1
return context.managedObjectContext.safeFetch(request).first
}
self.user = _user
}
self.account = response.value.account
} // end Task
}
@MainActor
init(context: AppContext, authContext: AuthContext, acct: String){
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
super.init(context: context, authContext: authContext, account: nil)
let domain = authContext.mastodonAuthenticationBox.domain
let authenticationBox = authContext.mastodonAuthenticationBox
@ -116,16 +82,9 @@ final class RemoteProfileViewModel: ProfileViewModel {
break
}
} receiveValue: { [weak self] response in
guard let self = self, let value = response.value else { return }
let managedObjectContext = context.managedObjectContext
let request = MastodonUser.sortedFetchRequest
request.fetchLimit = 1
request.predicate = MastodonUser.predicate(domain: domain, id: value.id)
guard let mastodonUser = managedObjectContext.safeFetch(request).first else {
assertionFailure()
return
}
self.user = mastodonUser
guard let account = response.value else { return }
self?.account = account
}
.store(in: &disposeBag)
}

View File

@ -84,7 +84,7 @@ extension ReportViewController: ReportReasonViewControllerDelegate {
let reportResultViewModel = ReportResultViewModel(
context: context,
authContext: viewModel.authContext,
user: viewModel.user,
account: viewModel.account,
isReported: false
)
_ = coordinator.present(
@ -160,7 +160,7 @@ extension ReportViewController: ReportSupplementaryViewControllerDelegate {
let reportResultViewModel = ReportResultViewModel(
context: context,
authContext: viewModel.authContext,
user: viewModel.user,
account: viewModel.account,
isReported: true
)

View File

@ -28,7 +28,7 @@ class ReportViewModel {
// input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let account: Mastodon.Entity.Account
let status: MastodonStatus?
// output
@ -39,17 +39,17 @@ class ReportViewModel {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>,
account: Mastodon.Entity.Account,
status: MastodonStatus?
) {
self.context = context
self.authContext = authContext
self.user = user
self.account = account
self.status = status
self.reportReasonViewModel = ReportReasonViewModel(context: context)
self.reportServerRulesViewModel = ReportServerRulesViewModel(context: context)
self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, user: user, status: status)
self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, user: user)
self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, account: account, status: status)
self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, account: account)
// end init
// setup reason viewModel
@ -57,17 +57,8 @@ class ReportViewModel {
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisPost
} else {
Task { @MainActor in
let managedObjectContext = context.managedObjectContext
let _username: String? = try? await managedObjectContext.perform {
let user = user.object(in: managedObjectContext)
return user?.acctWithDomain
}
if let username = _username {
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(username)
} else {
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisAccount
}
} // end Task
reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(account.username)
}
}
// bind server rules
@ -96,73 +87,63 @@ extension ReportViewModel {
func report() async throws {
guard !isReporting else { return }
let managedObjectContext = context.managedObjectContext
let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform {
guard let user = self.user.object(in: managedObjectContext) else { return nil }
// the status picker is essential step in report flow
// only check isSkip or not
let statusIDs: [MastodonStatus.ID]? = {
if self.reportStatusViewModel.isSkip {
let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in
return record.id
}
return _id.flatMap { [$0] } ?? []
} else {
return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in
return record.id
}
let account = self.account
// the status picker is essential step in report flow
// only check isSkip or not
let statusIDs: [MastodonStatus.ID]? = {
if self.reportStatusViewModel.isSkip {
let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in
return record.id
}
}()
// the user comment is essential step in report flow
// only check isSkip or not
let comment: String? = {
let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment
if let comment = _comment, !comment.isEmpty {
return comment
} else {
return nil
return _id.flatMap { [$0] } ?? []
} else {
return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in
return record.id
}
}()
return Mastodon.API.Reports.FileReportQuery(
accountID: user.id,
statusIDs: statusIDs,
comment: comment,
forward: true,
category: {
switch self.reportReasonViewModel.selectReason {
}
}()
// the user comment is essential step in report flow
// only check isSkip or not
let comment: String? = {
let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment
if let comment = _comment, !comment.isEmpty {
return comment
} else {
return nil
}
}()
let query = Mastodon.API.Reports.FileReportQuery(
accountID: account.id,
statusIDs: statusIDs,
comment: comment,
forward: true,
category: {
switch self.reportReasonViewModel.selectReason {
case .dislike: return nil
case .spam: return .spam
case .violateRule: return .violation
case .other: return .other
case .none: return nil
}
}(),
ruleIDs: {
switch self.reportReasonViewModel.selectReason {
}
}(),
ruleIDs: {
switch self.reportReasonViewModel.selectReason {
case .violateRule:
let ruleIDs = self.reportServerRulesViewModel.selectRules.map { $0.id }.sorted()
return ruleIDs
default:
return nil
}
}()
)
}
guard let query = _query else { return }
}
}()
)
do {
isReporting = true
#if DEBUG
try await Task.sleep(nanoseconds: .second * 3)
#else
let _ = try await context.apiService.report(
query: query,
authenticationBox: authContext.mastodonAuthenticationBox
)
#endif
isReportSuccess = true
} catch {
isReporting = false

View File

@ -90,7 +90,7 @@ extension ReportResultViewController {
do {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: self.viewModel.user
user: self.viewModel.account
)
} catch {
// handle error
@ -110,7 +110,7 @@ extension ReportResultViewController {
do {
try await DataSourceFacade.responseToUserMuteAction(
dependency: self,
user: self.viewModel.user
account: self.viewModel.account
)
} catch {
// handle error
@ -130,7 +130,7 @@ extension ReportResultViewController {
do {
try await DataSourceFacade.responseToUserBlockAction(
dependency: self,
user: self.viewModel.user
user: self.viewModel.account
)
} catch {
// handle error

View File

@ -23,7 +23,7 @@ class ReportResultViewModel: ObservableObject {
// input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let account: Mastodon.Entity.Account
let isReported: Bool
var headline: String {
@ -48,24 +48,20 @@ class ReportResultViewModel: ObservableObject {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>,
account: Mastodon.Entity.Account,
isReported: Bool
) {
self.context = context
self.authContext = authContext
self.user = user
self.account = account
self.isReported = isReported
// end init
Task { @MainActor in
guard let user = user.object(in: context.managedObjectContext) else { return }
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return }
self.relationshipViewModel.user = user
self.relationshipViewModel.me = me
self.avatarURL = user.avatarImageURL()
self.username = user.acctWithDomain
self.avatarURL = account.avatarImageURL()
self.username = account.username
} // end Task
}

View File

@ -68,19 +68,9 @@ extension ReportStatusViewModel.State {
Task {
let maxID = await viewModel.statusFetchedResultsController.records.last?.id
let managedObjectContext = viewModel.context.managedObjectContext
let _userID: MastodonUser.ID? = try await managedObjectContext.perform {
guard let user = viewModel.user.object(in: managedObjectContext) else { return nil }
return user.id
}
guard let userID = _userID else {
await enter(state: Fail.self)
return
}
do {
let response = try await viewModel.context.apiService.userTimeline(
accountID: userID,
accountID: viewModel.account.id,
maxID: maxID,
sinceID: nil,
excludeReplies: true,

View File

@ -24,7 +24,7 @@ class ReportStatusViewModel {
// input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let account: Mastodon.Entity.Account
let status: MastodonStatus?
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -52,12 +52,12 @@ class ReportStatusViewModel {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>,
account: Mastodon.Entity.Account,
status: MastodonStatus?
) {
self.context = context
self.authContext = authContext
self.user = user
self.account = account
self.status = status
self.statusFetchedResultsController = StatusFetchedResultsController()
// end init

View File

@ -18,7 +18,7 @@ class ReportSupplementaryViewModel {
// Input
let context: AppContext
let authContext: AuthContext
let user: ManagedObjectRecord<MastodonUser>
let account: Mastodon.Entity.Account
let commentContext = ReportItem.CommentContext()
@Published var isSkip = false
@ -31,11 +31,11 @@ class ReportSupplementaryViewModel {
init(
context: AppContext,
authContext: AuthContext,
user: ManagedObjectRecord<MastodonUser>
account: Mastodon.Entity.Account
) {
self.context = context
self.authContext = authContext
self.user = user
self.account = account
// end init
Publishers.CombineLatest(

View File

@ -70,10 +70,7 @@ extension SearchResultViewController {
status: status
)
case .user(let user):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: user
)
assertionFailure()
case .hashtag(let tag):
await DataSourceFacade.coordinateToHashtagScene(
provider: self,

View File

@ -217,15 +217,8 @@ extension SettingsCoordinator: ServerDetailsViewControllerDelegate {
extension SettingsCoordinator: AboutInstanceViewControllerDelegate {
@MainActor func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) {
Task {
let user = try await appContext.apiService.fetchUser(username: account.username, domain: authContext.mastodonAuthenticationBox.domain, authenticationBox: authContext.mastodonAuthenticationBox)
let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, optionalMastodonUser: user)
_ = await MainActor.run {
sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show)
}
}
let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, account: account)
sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show)
}
func sendEmailToAdmin(_ viewController: AboutInstanceViewController, emailAddress: String) {

View File

@ -98,7 +98,13 @@ public struct MastodonAuthentication: Codable, Hashable {
let userPredicate = MastodonUser.predicate(domain: domain, id: userID)
return MastodonUser.findOrFetch(in: context, matching: userPredicate)
}
public func account() -> Mastodon.Entity.Account? {
// store accounts
#warning("TODO: Implement")
return nil
}
func updating(instance: Instance) -> Self {
copy(instanceObjectIdURI: instance.objectID.uriRepresentation())
}

View File

@ -34,19 +34,6 @@ extension APIService {
authorization: authorization
).singleOutput()
let managedObjectContext = self.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
_ = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: domain,
entity: response.value,
cache: nil,
networkDate: response.networkDate
)
)
}
return response
}
@ -171,7 +158,7 @@ extension APIService {
extension APIService {
public func fetchUser(username: String, domain: String, authenticationBox: MastodonAuthenticationBox)
async throws -> MastodonUser? {
async throws -> Mastodon.Entity.Account? {
let query = Mastodon.API.Account.AccountLookupQuery(acct: "\(username)@\(domain)")
let authorization = authenticationBox.userAuthorization
@ -182,21 +169,6 @@ extension APIService {
authorization: authorization
).singleOutput()
// user
let managedObjectContext = self.backgroundManagedObjectContext
var result: MastodonUser?
try await managedObjectContext.performChanges {
result = Persistence.MastodonUser.createOrMerge(
in: managedObjectContext,
context: Persistence.MastodonUser.PersistContext(
domain: domain,
entity: response.value,
cache: nil,
networkDate: response.networkDate
)
).user
}
return result
return response.value
}
}

View File

@ -145,64 +145,10 @@ extension APIService {
return response
}
public func toggleShowReblogs(
for user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
guard let user = user.object(in: managedObjectContext),
let me = authenticationBox.authentication.user(in: managedObjectContext)
else { throw APIError.implicit(.badRequest) }
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
let oldShowReblogs = me.showingReblogsBy.contains(user)
let newShowReblogs = (oldShowReblogs == false)
do {
let response = try await Mastodon.API.Account.follow(
session: session,
domain: authenticationBox.domain,
accountID: user.id,
followQueryType: .follow(query: .init(reblogs: newShowReblogs)),
authorization: authenticationBox.userAuthorization
).singleOutput()
result = .success(response)
} catch {
result = .failure(error)
}
try await managedObjectContext.performChanges {
guard let me = authenticationBox.authentication.user(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(isShowingReblogs: oldShowReblogs, by: me)
}
}
return try result.get()
}
public func toggleShowReblogs(
for user: Mastodon.Entity.Account,
authenticationBox: MastodonAuthenticationBox
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
let relationship = try await Mastodon.API.Account.relationships(
session: session,
domain: authenticationBox.domain,
@ -213,20 +159,14 @@ extension APIService {
let oldShowReblogs = relationship?.showingReblogs == true
let newShowReblogs = (oldShowReblogs == false)
do {
let response = try await Mastodon.API.Account.follow(
session: session,
domain: authenticationBox.domain,
accountID: user.id,
followQueryType: .follow(query: .init(reblogs: newShowReblogs)),
authorization: authenticationBox.userAuthorization
).singleOutput()
let response = try await Mastodon.API.Account.follow(
session: session,
domain: authenticationBox.domain,
accountID: user.id,
followQueryType: .follow(query: .init(reblogs: newShowReblogs)),
authorization: authenticationBox.userAuthorization
).singleOutput()
result = .success(response)
} catch {
result = .failure(error)
}
return try result.get()
return response
}
}

View File

@ -14,7 +14,6 @@ import MastodonSDK
extension APIService {
private struct MastodonMuteContext {
let sourceUserID: MastodonUser.ID
let targetUserID: MastodonUser.ID
let targetUsername: String
let isMuting: Bool
@ -60,33 +59,23 @@ extension APIService {
}
public func toggleMute(
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
authenticationBox: MastodonAuthenticationBox,
account: Mastodon.Entity.Account
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
let managedObjectContext = backgroundManagedObjectContext
let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges {
let authentication = authenticationBox.authentication
guard let relationship = try await Mastodon.API.Account.relationships(
session: session,
domain: authenticationBox.domain,
query: .init(ids: [account.id]),
authorization: authenticationBox.userAuthorization
).singleOutput().value.first else { throw APIError.implicit(.badRequest) }
let muteContext = MastodonMuteContext(
targetUserID: account.id,
targetUsername: account.username,
isMuting: relationship.muting ?? false
)
guard
let user = user.object(in: managedObjectContext),
let me = authentication.user(in: managedObjectContext)
else {
throw APIError.implicit(.badRequest)
}
let isMuting = user.mutingBy.contains(me)
// toggle mute state
user.update(isMuting: !isMuting, by: me)
return MastodonMuteContext(
sourceUserID: me.id,
targetUserID: user.id,
targetUsername: user.username,
isMuting: isMuting
)
}
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
do {
if muteContext.isMuting {
@ -96,7 +85,7 @@ extension APIService {
accountID: muteContext.targetUserID,
authorization: authenticationBox.userAuthorization
).singleOutput()
try await getMutes(authenticationBox: authenticationBox)
result = .success(response)
} else {
let response = try await Mastodon.API.Account.mute(
@ -105,38 +94,16 @@ extension APIService {
accountID: muteContext.targetUserID,
authorization: authenticationBox.userAuthorization
).singleOutput()
try await getMutes(authenticationBox: authenticationBox)
result = .success(response)
}
} catch {
result = .failure(error)
}
try await managedObjectContext.performChanges {
guard let user = user.object(in: managedObjectContext),
let me = authenticationBox.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(isMuting: muteContext.isMuting, by: me)
}
}
let response = try result.get()
return response
}
}