Refactor: Immutable view model for ProfileViewController
Goal: Begin to centralize the locus of view updates for easier bug fixing and future design flexibility. Remaining: The supplementary views are still heavily using Combine. Fixes #1372 [BUG] Cannot save new profile fields Fixes iOS-340
This commit is contained in:
parent
c927ca278b
commit
af6272014a
@ -103,15 +103,9 @@ final public class SceneCoordinator {
|
||||
|
||||
let relationship = try await APIService.shared.relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: appContext,
|
||||
authenticationBox: authenticationBox,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
let profileType: ProfileViewController.ProfileType = me == account ? .me(me) : .notMe(me: me, displayAccount: account, relationship: relationship)
|
||||
_ = self.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
scene: .profile(profileType),
|
||||
from: from,
|
||||
transition: .show
|
||||
)
|
||||
@ -190,7 +184,7 @@ extension SceneCoordinator {
|
||||
|
||||
// profile
|
||||
case accountList(viewModel: AccountListViewModel)
|
||||
case profile(viewModel: ProfileViewModel)
|
||||
case profile(ProfileViewController.ProfileType)
|
||||
case favorite(viewModel: FavoriteViewModel)
|
||||
case follower(viewModel: FollowerListViewModel)
|
||||
case following(viewModel: FollowingListViewModel)
|
||||
@ -449,9 +443,8 @@ private extension SceneCoordinator {
|
||||
let accountListViewController = AccountListViewController()
|
||||
accountListViewController.viewModel = viewModel
|
||||
viewController = accountListViewController
|
||||
case .profile(let viewModel):
|
||||
let _viewController = ProfileViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
case .profile(let profileType):
|
||||
let _viewController = ProfileViewController(profileType, authenticationBox: AuthenticationServiceProvider.shared.currentActiveUser.value!)
|
||||
viewController = _viewController
|
||||
case .bookmark(let viewModel):
|
||||
let _viewController = BookmarkViewController()
|
||||
|
@ -26,7 +26,6 @@ extension ProfileFieldSection {
|
||||
|
||||
static func diffableDataSource(
|
||||
collectionView: UICollectionView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem> {
|
||||
collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer)
|
||||
|
@ -120,16 +120,9 @@ extension DataSourceFacade {
|
||||
|
||||
coordinator.hideLoading()
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: AppContext.shared,
|
||||
authenticationBox: provider.authenticationBox,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
|
||||
let profileType: ProfileViewController.ProfileType = me == account ? .me(me) : .notMe(me: me, displayAccount: account, relationship: relationship)
|
||||
_ = coordinator.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
scene: .profile(profileType),
|
||||
from: provider,
|
||||
transition: .show
|
||||
)
|
||||
|
@ -33,7 +33,9 @@ final class ProfileAboutViewController: UIViewController {
|
||||
return collectionView
|
||||
}()
|
||||
|
||||
|
||||
public var currentEditableFields: [ (String, String) ] {
|
||||
return viewModel.profileInfoEditing.editedFields
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileAboutViewController {
|
||||
|
@ -18,7 +18,6 @@ extension ProfileAboutViewModel {
|
||||
) {
|
||||
let diffableDataSource = ProfileFieldSection.diffableDataSource(
|
||||
collectionView: collectionView,
|
||||
context: context,
|
||||
configuration: ProfileFieldSection.Configuration(
|
||||
profileFieldCollectionViewCellDelegate: profileFieldCollectionViewCellDelegate,
|
||||
profileFieldEditCollectionViewCellDelegate: profileFieldEditCollectionViewCellDelegate
|
||||
|
@ -18,7 +18,6 @@ final class ProfileAboutViewModel {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
@Published var account: Mastodon.Entity.Account
|
||||
@Published var isEditing = false
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
@ -32,9 +31,8 @@ final class ProfileAboutViewModel {
|
||||
@Published var emojiMeta: MastodonContent.Emojis = [:]
|
||||
@Published var createdAt: Date = Date()
|
||||
|
||||
init(context: AppContext, account: Mastodon.Entity.Account) {
|
||||
init(account: Mastodon.Entity.Account) {
|
||||
self.account = account
|
||||
self.context = context
|
||||
|
||||
emojiMeta = account.emojiMeta
|
||||
fields = account.mastodonFields
|
||||
@ -79,6 +77,12 @@ final class ProfileAboutViewModel {
|
||||
extension ProfileAboutViewModel {
|
||||
class ProfileInfo {
|
||||
@Published var fields: [ProfileFieldItem.FieldValue] = []
|
||||
|
||||
var editedFields: [ (String, String) ] {
|
||||
let edited = fields.map { return ($0.name.value, $0.value.value)
|
||||
}
|
||||
return edited
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +104,7 @@ extension ProfileAboutViewModel {
|
||||
}
|
||||
|
||||
// MARK: - ProfileViewModelEditable
|
||||
extension ProfileAboutViewModel: ProfileViewModelEditable {
|
||||
extension ProfileAboutViewModel {
|
||||
var isEdited: Bool {
|
||||
guard isEditing else { return false }
|
||||
|
||||
|
@ -32,6 +32,9 @@ final class ProfileHeaderViewController: UIViewController, MediaPreviewableViewC
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
let viewModel: ProfileHeaderViewModel
|
||||
var editedDetails: ProfileHeaderDetails {
|
||||
return viewModel.editedDetails
|
||||
}
|
||||
|
||||
weak var delegate: ProfileHeaderViewControllerDelegate?
|
||||
weak var headerDelegate: TabBarPagerHeaderDelegate?
|
||||
@ -79,9 +82,9 @@ final class ProfileHeaderViewController: UIViewController, MediaPreviewableViewC
|
||||
return documentPickerController
|
||||
}()
|
||||
|
||||
init(authenticationBox: MastodonAuthenticationBox, profileViewModel: ProfileViewModel) {
|
||||
self.viewModel = ProfileHeaderViewModel(authenticationBox: authenticationBox, account: profileViewModel.account, me: profileViewModel.me, relationship: profileViewModel.relationship)
|
||||
self.profileHeaderView = ProfileHeaderView(account: profileViewModel.account, me: profileViewModel.me, relationship: profileViewModel.relationship)
|
||||
init(authenticationBox: MastodonAuthenticationBox, account: Mastodon.Entity.Account, me: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) {
|
||||
self.viewModel = ProfileHeaderViewModel(authenticationBox: authenticationBox, account: account, me: me, relationship: relationship)
|
||||
self.profileHeaderView = ProfileHeaderView(account: account, me: me, relationship: relationship)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
|
@ -77,6 +77,10 @@ extension ProfileHeaderViewModel {
|
||||
@Published var name: String?
|
||||
@Published var note: String?
|
||||
}
|
||||
|
||||
var editedDetails: ProfileHeaderDetails {
|
||||
return ProfileHeaderDetails(bannerImage: profileInfoEditing.header, avatarImage: profileInfoEditing.avatar, displayName: profileInfoEditing.name, bioText: profileInfoEditing.note)
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileHeaderViewModel {
|
||||
@ -96,7 +100,7 @@ extension ProfileHeaderViewModel {
|
||||
}
|
||||
|
||||
// MARK: - ProfileViewModelEditable
|
||||
extension ProfileHeaderViewModel: ProfileViewModelEditable {
|
||||
extension ProfileHeaderViewModel {
|
||||
var isEdited: Bool {
|
||||
guard isEditing else { return false }
|
||||
|
||||
|
@ -475,11 +475,9 @@ extension ProfileHeaderView {
|
||||
let margin: CGFloat = {
|
||||
switch traitCollection.userInterfaceIdiom {
|
||||
case .phone:
|
||||
return ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
|
||||
return ProfileViewController.containerViewMargin(forHorizontalSizeClass: .compact)
|
||||
default:
|
||||
return traitCollection.horizontalSizeClass == .regular ?
|
||||
ProfileViewController.containerViewMarginForRegularHorizontalSizeClass :
|
||||
ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
|
||||
return ProfileViewController.containerViewMargin(forHorizontalSizeClass: traitCollection.horizontalSizeClass)
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -136,11 +136,9 @@ extension ProfilePagingViewController {
|
||||
let margin: CGFloat = {
|
||||
switch traitCollection.userInterfaceIdiom {
|
||||
case .phone:
|
||||
return ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
|
||||
return ProfileViewController.containerViewMargin(forHorizontalSizeClass: .compact)
|
||||
default:
|
||||
return traitCollection.horizontalSizeClass == .regular ?
|
||||
ProfileViewController.containerViewMarginForRegularHorizontalSizeClass :
|
||||
ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
|
||||
return ProfileViewController.containerViewMargin(forHorizontalSizeClass: traitCollection.horizontalSizeClass)
|
||||
}
|
||||
}()
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,228 +15,124 @@ import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
// please override this base class
|
||||
@MainActor
|
||||
class ProfileViewModel: NSObject {
|
||||
|
||||
typealias UserID = String
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
private var mastodonUserObserver: AnyCancellable?
|
||||
private var currentMastodonUserObserver: AnyCancellable?
|
||||
|
||||
let postsUserTimelineViewModel: UserTimelineViewModel
|
||||
let repliesUserTimelineViewModel: UserTimelineViewModel
|
||||
let mediaUserTimelineViewModel: UserTimelineViewModel
|
||||
let profileAboutViewModel: ProfileAboutViewModel
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authenticationBox: MastodonAuthenticationBox
|
||||
enum ServerHostedImage: Equatable {
|
||||
case fetchable(URL)
|
||||
// case fetching(URL)
|
||||
// case fetched(URL, UIImage)
|
||||
// case fetchError(URL, Error)
|
||||
case local(UIImage)
|
||||
}
|
||||
|
||||
@Published var me: Mastodon.Entity.Account
|
||||
@Published var account: Mastodon.Entity.Account
|
||||
@Published var relationship: Mastodon.Entity.Relationship?
|
||||
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
struct ProfileHeaderDetails: Equatable {
|
||||
let bannerImage: ServerHostedImage?
|
||||
let avatarImage: ServerHostedImage?
|
||||
let displayName: String?
|
||||
let bioText: String?
|
||||
|
||||
@Published var isEditing = false
|
||||
@Published var isUpdating = false
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
|
||||
@Published var userIdentifier: UserIdentifier? = nil
|
||||
|
||||
@Published var isReplyBarButtonItemHidden: Bool = true
|
||||
@Published var isMoreMenuBarButtonItemHidden: Bool = true
|
||||
@Published var isMeBarButtonItemsHidden: Bool = true
|
||||
@Published var isPagingEnabled = true
|
||||
|
||||
// @Published var protected: Bool? = nil
|
||||
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
@MainActor
|
||||
init(context: AppContext, authenticationBox: MastodonAuthenticationBox, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, me: Mastodon.Entity.Account) {
|
||||
self.context = context
|
||||
self.authenticationBox = authenticationBox
|
||||
self.account = account
|
||||
self.relationship = relationship
|
||||
self.me = me
|
||||
|
||||
self.postsUserTimelineViewModel = UserTimelineViewModel(
|
||||
context: context,
|
||||
authenticationBox: authenticationBox,
|
||||
title: L10n.Scene.Profile.SegmentedControl.posts,
|
||||
queryFilter: .init(excludeReplies: true)
|
||||
)
|
||||
self.repliesUserTimelineViewModel = UserTimelineViewModel(
|
||||
context: context,
|
||||
authenticationBox: authenticationBox,
|
||||
title: L10n.Scene.Profile.SegmentedControl.postsAndReplies,
|
||||
queryFilter: .init(excludeReplies: false)
|
||||
)
|
||||
self.mediaUserTimelineViewModel = UserTimelineViewModel(
|
||||
context: context,
|
||||
authenticationBox: authenticationBox,
|
||||
title: L10n.Scene.Profile.SegmentedControl.media,
|
||||
queryFilter: .init(onlyMedia: true)
|
||||
)
|
||||
self.profileAboutViewModel = ProfileAboutViewModel(context: context, account: account)
|
||||
super.init()
|
||||
|
||||
init(_ account: Mastodon.Entity.Account) {
|
||||
bannerImage = account.headerImageURL().flatMap { .fetchable($0) }
|
||||
if let domain = account.domain {
|
||||
userIdentifier = MastodonUserIdentifier(domain: domain, userID: account.id)
|
||||
avatarImage = .fetchable(account.avatarImageURLWithFallback(domain: domain))
|
||||
} else {
|
||||
userIdentifier = nil
|
||||
avatarImage = account.avatarImageURL().flatMap { .fetchable($0) } // TODO: there is a fallback option here. what is it for?
|
||||
}
|
||||
|
||||
// bind userIdentifier
|
||||
$userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
|
||||
$userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
|
||||
$userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
|
||||
|
||||
// bind bar button items
|
||||
Publishers.CombineLatest3($account, $me, $relationship)
|
||||
.sink(receiveValue: { [weak self] account, me, relationship in
|
||||
guard let self else {
|
||||
self?.isReplyBarButtonItemHidden = true
|
||||
self?.isMoreMenuBarButtonItemHidden = true
|
||||
self?.isMeBarButtonItemsHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
let isMyself = (account == me)
|
||||
self.isReplyBarButtonItemHidden = isMyself
|
||||
self.isMoreMenuBarButtonItemHidden = isMyself
|
||||
self.isMeBarButtonItemsHidden = (isMyself == false)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewDidAppear
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
self.isReplyBarButtonItemHidden = self.isReplyBarButtonItemHidden
|
||||
self.isMoreMenuBarButtonItemHidden = self.isMoreMenuBarButtonItemHidden
|
||||
self.isMeBarButtonItemsHidden = self.isMeBarButtonItemsHidden
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// query relationship
|
||||
|
||||
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
|
||||
|
||||
// observe friendship
|
||||
Publishers.CombineLatest(
|
||||
$account,
|
||||
pendingRetryPublisher
|
||||
)
|
||||
.sink { [weak self] account, _ in
|
||||
guard let self else { return }
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response = try await APIService.shared.relationship(
|
||||
forAccounts: [account],
|
||||
authenticationBox: self.authenticationBox
|
||||
)
|
||||
|
||||
// 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.CombineLatest3(
|
||||
(relationship?.blocking ?? false).publisher,
|
||||
(relationship?.blockedBy ?? false).publisher,
|
||||
(relationship?.domainBlocking ?? false).publisher
|
||||
)
|
||||
.map { $0 || $1 || $2 }
|
||||
.share()
|
||||
|
||||
Publishers.CombineLatest(
|
||||
isBlockingOrBlocked,
|
||||
$isEditing
|
||||
)
|
||||
.map { !$0 && !$1 }
|
||||
.assign(to: &$isPagingEnabled)
|
||||
displayName = account.displayNameWithFallback
|
||||
bioText = account.note
|
||||
}
|
||||
|
||||
// fetch profile info before edit
|
||||
func fetchEditProfileInfo() -> AnyPublisher<Mastodon.Entity.Account, Error> {
|
||||
guard let domain = me.domain else {
|
||||
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let mastodonAuthentication = authenticationBox.authentication
|
||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken)
|
||||
return APIService.shared.verifyAndActivateUser(domain: domain,
|
||||
clientID: mastodonAuthentication.clientID,
|
||||
clientSecret: mastodonAuthentication.clientSecret,
|
||||
authorization: authorization)
|
||||
.tryMap { (account, _) in
|
||||
return account
|
||||
}.eraseToAnyPublisher()
|
||||
init(bannerImage: UIImage?, avatarImage: UIImage?, displayName: String?, bioText: String?) {
|
||||
self.bannerImage = bannerImage.flatMap{ .local($0) }
|
||||
self.avatarImage = avatarImage.flatMap{ .local($0) }
|
||||
self.displayName = displayName
|
||||
self.bioText = bioText
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileViewModel {
|
||||
func updateProfileInfo(
|
||||
headerProfileInfo: ProfileHeaderViewModel.ProfileInfo,
|
||||
aboutProfileInfo: ProfileAboutViewModel.ProfileInfo
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Account> {
|
||||
let authenticationBox = authenticationBox
|
||||
let domain = authenticationBox.domain
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
// TODO: constrain size?
|
||||
let _header: UIImage? = {
|
||||
guard let image = headerProfileInfo.header else { return nil }
|
||||
guard image.size.width <= ProfileHeaderViewModel.bannerImageMaxSizeInPixel.width else {
|
||||
return image.af.imageScaled(to: ProfileHeaderViewModel.bannerImageMaxSizeInPixel)
|
||||
}
|
||||
return image
|
||||
}()
|
||||
|
||||
let _avatar: UIImage? = {
|
||||
guard let image = headerProfileInfo.avatar else { return nil }
|
||||
guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else {
|
||||
return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
|
||||
}
|
||||
return image
|
||||
}()
|
||||
|
||||
let fieldsAttributes = aboutProfileInfo.fields.map { field in
|
||||
Mastodon.Entity.Field(name: field.name.value, value: field.value.value)
|
||||
}
|
||||
|
||||
let query = Mastodon.API.Account.UpdateCredentialQuery(
|
||||
discoverable: nil,
|
||||
bot: nil,
|
||||
displayName: headerProfileInfo.name,
|
||||
note: headerProfileInfo.note,
|
||||
avatar: _avatar.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
|
||||
header: _header.flatMap { Mastodon.Query.MediaAttachment.png($0.pngData()) },
|
||||
locked: nil,
|
||||
source: nil,
|
||||
fieldsAttributes: fieldsAttributes
|
||||
)
|
||||
let response = try await APIService.shared.accountUpdateCredentials(
|
||||
domain: domain,
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
NotificationCenter.default.post(name: .userFetched, object: nil)
|
||||
|
||||
return response
|
||||
struct ProfileAboutDetails {
|
||||
let createdAt: Date
|
||||
let fields: [ String : String ]
|
||||
|
||||
init(_ account: Mastodon.Entity.Account) {
|
||||
createdAt = account.createdAt
|
||||
fields = profileFields(account)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func profileFields(_ account: Mastodon.Entity.Account) -> [ String : String ] {
|
||||
var result = [ String : String ]()
|
||||
for field in account.fields ?? [] {
|
||||
result[field.name] = field.value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public struct ProfileViewModelImmutable {
|
||||
|
||||
let profileType: ProfileViewController.ProfileType
|
||||
let state: ProfileInteractionState
|
||||
|
||||
var headerDetails: ProfileHeaderDetails {
|
||||
return ProfileHeaderDetails(profileType.accountToDisplay)
|
||||
}
|
||||
var aboutDetails: ProfileAboutDetails {
|
||||
return ProfileAboutDetails(profileType.accountToDisplay)
|
||||
}
|
||||
|
||||
public enum ProfileInteractionState {
|
||||
case idle
|
||||
case updating
|
||||
case editing
|
||||
case pushingEdits
|
||||
|
||||
var actionButtonEnabled: Bool {
|
||||
switch self {
|
||||
case .updating, .pushingEdits:
|
||||
return false
|
||||
case .idle, .editing:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var isEditing: Bool {
|
||||
switch self {
|
||||
case .editing, .pushingEdits:
|
||||
return true
|
||||
case .idle, .updating:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isUpdating: Bool {
|
||||
switch self {
|
||||
case .editing, .idle:
|
||||
return false
|
||||
case .pushingEdits, .updating:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hideReplyBarButtonItem: Bool {
|
||||
return profileType.isMe
|
||||
}
|
||||
|
||||
var hideMoreMenuBarButtonItem: Bool {
|
||||
return profileType.isMe
|
||||
}
|
||||
|
||||
var hideIsMeBarButtonItems: Bool {
|
||||
return !profileType.isMe
|
||||
}
|
||||
|
||||
var isPagingEnabled: Bool {
|
||||
guard !state.isEditing else { return false }
|
||||
guard let relationship = profileType.myRelationshipToDisplayedAccount else { return true }
|
||||
return !relationship.isBlockingOrBlocked
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Mastodon.Entity.Relationship {
|
||||
var isBlockingOrBlocked: Bool {
|
||||
return blocking || blockedBy || domainBlocking
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ final class UserTimelineViewModel {
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let authenticationBox: MastodonAuthenticationBox
|
||||
let title: String
|
||||
let dataController: StatusDataController
|
||||
@ -50,12 +49,10 @@ final class UserTimelineViewModel {
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
context: AppContext,
|
||||
authenticationBox: MastodonAuthenticationBox,
|
||||
title: String,
|
||||
queryFilter: QueryFilter
|
||||
) {
|
||||
self.context = context
|
||||
self.authenticationBox = authenticationBox
|
||||
self.title = title
|
||||
self.dataController = StatusDataController()
|
||||
|
@ -36,7 +36,7 @@ class MainTabBarController: UITabBarController {
|
||||
let searchViewController: SearchViewController
|
||||
let composeViewController: UIViewController // placeholder
|
||||
let notificationViewController: NotificationViewController
|
||||
let meProfileViewController: ProfileViewController
|
||||
var meProfileViewController: UIViewController // placeholder
|
||||
|
||||
private(set) var isReadyForWizardAvatarButton = false
|
||||
|
||||
@ -63,17 +63,13 @@ class MainTabBarController: UITabBarController {
|
||||
notificationViewController = NotificationViewController()
|
||||
notificationViewController.configureTabBarItem(with: .notifications)
|
||||
|
||||
meProfileViewController = ProfileViewController()
|
||||
meProfileViewController = UIViewController()
|
||||
meProfileViewController.configureTabBarItem(with: .me)
|
||||
|
||||
if let authenticationBox {
|
||||
notificationViewController.viewModel = NotificationViewModel(context: AppContext.shared, authenticationBox: authenticationBox)
|
||||
homeTimelineViewController.viewModel = HomeTimelineViewModel(authenticationBox: authenticationBox)
|
||||
searchViewController.viewModel = SearchViewModel(authenticationBox: authenticationBox)
|
||||
|
||||
if let account = authenticationBox.cachedAccount {
|
||||
meProfileViewController.viewModel = ProfileViewModel(context: AppContext.shared, authenticationBox: authenticationBox, account: account, relationship: nil, me: account)
|
||||
}
|
||||
}
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@ -84,6 +80,12 @@ class MainTabBarController: UITabBarController {
|
||||
layoutAvatarButton()
|
||||
}
|
||||
|
||||
private func replace(_ oldVC: UIViewController, with newVC: UIViewController) {
|
||||
guard let navControllers = viewControllers as? [UINavigationController] else { return }
|
||||
guard let toReplace = navControllers.first(where: { $0.viewControllers[0] == oldVC }) else { return }
|
||||
toReplace.viewControllers = [newVC]
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
}
|
||||
|
||||
@ -93,16 +95,6 @@ extension MainTabBarController {
|
||||
return selectedViewController
|
||||
}
|
||||
|
||||
override var selectedViewController: UIViewController? {
|
||||
willSet {
|
||||
if let profileView = (newValue as? UINavigationController)?.topViewController as? ProfileViewController{
|
||||
guard let authenticationBox,
|
||||
let account = authenticationBox.cachedAccount else { return }
|
||||
profileView.viewModel = ProfileViewModel(context: AppContext.shared, authenticationBox: authenticationBox, account: account, relationship: nil, me: account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@ -171,6 +163,30 @@ extension MainTabBarController {
|
||||
notificationViewController.navigationController?.tabBarItem.image = image.imageWithoutBaseline()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$currentTab
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] currentTab in
|
||||
guard let self else { return }
|
||||
|
||||
if currentTab == .me {
|
||||
guard let authBox = authenticationBox, let myAccount = authBox.cachedAccount else { return }
|
||||
let oldMe = meProfileViewController
|
||||
let updatedProfile = ProfileViewController(.me(myAccount), authenticationBox: authBox)
|
||||
meProfileViewController = updatedProfile
|
||||
updatedProfile.configureTabBarItem(with: .me)
|
||||
self.replace(oldMe, with: updatedProfile)
|
||||
if let domain = myAccount.domain ?? myAccount.domainFromAcct {
|
||||
self.avatarURL = myAccount.avatarImageURLWithFallback(domain: domain)
|
||||
} else {
|
||||
self.avatarURL = myAccount.avatarImageURL()
|
||||
}
|
||||
|
||||
self.avatarButton.removeFromSuperview()
|
||||
self.layoutAvatarButton()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$avatarURL
|
||||
.receive(on: DispatchQueue.main)
|
||||
@ -203,10 +219,6 @@ extension MainTabBarController {
|
||||
self?.updateUserAccount()
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
|
||||
if let currentViewModel = self.meProfileViewController.viewModel, currentViewModel.account.id == account.id, !currentViewModel.isEditing {
|
||||
self.meProfileViewController.viewModel = ProfileViewModel(context: AppContext.shared, authenticationBox: authenticationBox, account: account, relationship: nil, me: account)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
@ -330,6 +342,7 @@ extension MainTabBarController {
|
||||
}
|
||||
anchorImageView.alpha = 0
|
||||
|
||||
accountSwitcherChevron.removeFromSuperview()
|
||||
accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(accountSwitcherChevron)
|
||||
|
||||
|
@ -167,15 +167,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
authenticationBox: authenticationBox
|
||||
).value.first else { return }
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: AppContext.shared,
|
||||
authenticationBox: authenticationBox,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
let profileType: ProfileViewController.ProfileType = me == account ? .me(me) : .notMe(me: me, displayAccount: account, relationship: relationship)
|
||||
_ = self.coordinator?.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
scene: .profile(profileType),
|
||||
from: nil,
|
||||
transition: .show
|
||||
)
|
||||
@ -308,16 +302,9 @@ extension SceneDelegate {
|
||||
authenticationBox: authenticationBox
|
||||
).value.first else { return }
|
||||
|
||||
let profileViewModel = ProfileViewModel(
|
||||
context: AppContext.shared,
|
||||
authenticationBox: authenticationBox,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
me: me
|
||||
)
|
||||
|
||||
let profileType: ProfileViewController.ProfileType = me == account ? .me(me) : .notMe(me: me, displayAccount: account, relationship: relationship)
|
||||
self.coordinator?.present(
|
||||
scene: .profile(viewModel: profileViewModel),
|
||||
scene: .profile(profileType),
|
||||
from: nil,
|
||||
transition: .show
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user