Remove me/remote-profile-viewmodel (IOS-192)

This commit is contained in:
Nathan Mattes 2023-12-08 15:15:36 +01:00
parent 8918d237ca
commit 34b962e3ca
7 changed files with 144 additions and 234 deletions

View File

@ -427,7 +427,6 @@
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */; };
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; };
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */; };
@ -441,7 +440,6 @@
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */; };
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */; };
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; };
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; };
DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; };
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
@ -1167,7 +1165,6 @@
DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = "<group>"; };
DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = "<group>"; };
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewController.swift; sourceTree = "<group>"; };
DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = "<group>"; };
@ -1180,7 +1177,6 @@
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewModel.swift; sourceTree = "<group>"; };
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = "<group>"; };
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = "<group>"; };
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = "<group>"; };
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
@ -2782,8 +2778,6 @@
DBFEEC97279BDC6A004F81DD /* About */,
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */,
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */,
);
path = Profile;
sourceTree = "<group>";
@ -3748,7 +3742,6 @@
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
D81A94172B07A1D30067A19D /* ProfileCardView+Configuration.swift in Sources */,
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */,
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
D82BD7552ABC73AF009A374A /* NotificationPolicyTableViewCell.swift in Sources */,
DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */,
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
@ -3850,7 +3843,6 @@
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */,
DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */,
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */,
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */,

View File

@ -49,7 +49,8 @@ final public class SceneCoordinator {
appContext.notificationService.requestRevealNotificationPublisher
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] pushNotification in
.sink(receiveValue: {
[weak self] pushNotification in
guard let self = self else { return }
Task {
guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return }
@ -101,20 +102,44 @@ final public class SceneCoordinator {
switch type {
case .follow:
let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
Task {
let account = try await appContext.apiService.notification(
notificationID: notificationID,
authenticationBox: authContext.mastodonAuthenticationBox
).value.account
let profileViewModel = ProfileViewModel(
context: appContext,
authContext: authContext,
account: account
)
_ = self.present(
scene: .profile(viewModel: profileViewModel),
from: from,
transition: .show
)
}
case .followRequest:
// do nothing
break
case .mention, .reblog, .favourite, .poll, .status:
let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
let threadViewModel = RemoteThreadViewModel(
context: appContext,
authContext: authContext,
notificationID: notificationID
)
_ = self.present(
scene: .thread(viewModel: threadViewModel),
from: from,
transition: .show
)
case ._other:
assertionFailure()
break
}
} // end DispatchQueue.main.async
} catch {
assertionFailure(error.localizedDescription)
return

View File

@ -1,41 +0,0 @@
//
// MeProfileViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-30.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK
final class MeProfileViewModel: ProfileViewModel {
@MainActor
init(context: AppContext, authContext: AuthContext) {
let me = authContext.mastodonAuthenticationBox.authentication.account()
super.init(
context: context,
authContext: authContext,
account: me
)
}
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
let account = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value
self.account = account
self.me = account
} catch {
// do nothing?
}
}
}
}

View File

@ -56,10 +56,10 @@ class ProfileViewModel: NSObject {
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
@MainActor
init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account?) {
init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account) {
self.context = context
self.authContext = authContext
self.account = account!
self.account = account
self.postsUserTimelineViewModel = UserTimelineViewModel(
context: context,
authContext: authContext,
@ -87,7 +87,7 @@ class ProfileViewModel: NSObject {
// bind user
$account
.map { user -> UserIdentifier? in
guard let account, let domain = account.domain else { return nil }
guard let domain = account.domain else { return nil }
return MastodonUserIdentifier(domain: domain, userID: account.id)
}
.assign(to: &$userIdentifier)

View File

@ -1,91 +0,0 @@
//
// RemoteProfileViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-4-2.
//
import Foundation
import Combine
import CoreDataStack
import MastodonSDK
import MastodonCore
final class RemoteProfileViewModel: ProfileViewModel {
@MainActor
init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) {
super.init(context: context, authContext: authContext, account: nil)
let domain = authContext.mastodonAuthenticationBox.domain
let authorization = authContext.mastodonAuthenticationBox.userAuthorization
Just(userID)
.asyncMap { userID in
try await context.apiService.accountInfo(
domain: domain,
userID: userID,
authorization: authorization
)
}
.retry(3)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(_):
// TODO: handle error
break
case .finished:
break
}
} receiveValue: { [weak self] response in
self?.account = response.value
}
.store(in: &disposeBag)
}
@MainActor
init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) {
super.init(context: context, authContext: authContext, account: nil)
Task { @MainActor in
let response = try await context.apiService.notification(
notificationID: notificationID,
authenticationBox: authContext.mastodonAuthenticationBox
)
self.account = response.value.account
} // end Task
}
@MainActor
init(context: AppContext, authContext: AuthContext, acct: String){
super.init(context: context, authContext: authContext, account: nil)
let domain = authContext.mastodonAuthenticationBox.domain
let authenticationBox = authContext.mastodonAuthenticationBox
Just(acct)
.asyncMap { acct -> Mastodon.Response.Content<Mastodon.Entity.Account?> in
try await context.apiService.search(
query: .init(q: acct, type: .accounts, resolve: true),
authenticationBox: authenticationBox
).map { $0.accounts.first }
}
.retry(3)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(_):
// TODO: handle error
break
case .finished:
break
}
} receiveValue: { [weak self] response in
guard let account = response.value else { return }
self?.account = account
}
.store(in: &disposeBag)
}
}

View File

@ -95,32 +95,33 @@ class MainTabBarController: UITabBarController {
let viewController: UIViewController
switch self {
case .home:
let _viewController = HomeTimelineViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .search:
let _viewController = SearchViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .compose:
viewController = UIViewController()
case .notifications:
let _viewController = NotificationViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .me:
let _viewController = ProfileViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = MeProfileViewModel(context: context, authContext: authContext)
viewController = _viewController
case .home:
let _viewController = HomeTimelineViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .search:
let _viewController = SearchViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .compose:
viewController = UIViewController()
case .notifications:
let _viewController = NotificationViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .me:
let me = authContext.mastodonAuthenticationBox.authentication.account()
let _viewController = ProfileViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me)
viewController = _viewController
}
viewController.title = self.title
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)

View File

@ -137,16 +137,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
switch (profile, statusID) {
case (profile, nil):
let profileViewModel = RemoteProfileViewModel(
context: AppContext.shared,
authContext: authContext,
acct: incomingURL.absoluteString
)
self.coordinator?.present(
scene: .profile(viewModel: profileViewModel),
from: nil,
transition: .show
)
Task {
let domain = authContext.mastodonAuthenticationBox.domain
let authenticationBox = authContext.mastodonAuthenticationBox
guard let account = try await AppContext.shared.apiService.search(
query: .init(q: incomingURL.absoluteString, type: .accounts, resolve: true),
authenticationBox: authenticationBox
).value.accounts.first else { return }
let profileViewModel = ProfileViewModel(
context: AppContext.shared,
authContext: authContext,
account: account
)
_ = self.coordinator?.present(
scene: .profile(viewModel: profileViewModel),
from: nil,
transition: .show
)
}
case (profile, statusID):
Task {
@ -248,58 +258,72 @@ extension SceneDelegate {
if !UIApplication.shared.canOpenURL(url) { return }
#if DEBUG
#if DEBUG
print("source application = \(sendingAppID ?? "Unknown")")
print("url = \(url)")
#endif
#endif
switch url.host {
case "post":
showComposeViewController()
case "profile":
let components = url.pathComponents
guard
components.count == 2,
components[0] == "/",
let authContext = coordinator?.authContext
else { return }
let profileViewModel = RemoteProfileViewModel(
context: AppContext.shared,
authContext: authContext,
acct: components[1]
)
self.coordinator?.present(
scene: .profile(viewModel: profileViewModel),
from: nil,
transition: .show
)
case "status":
let components = url.pathComponents
guard
components.count == 2,
components[0] == "/",
let authContext = coordinator?.authContext
else { return }
let statusId = components[1]
// View post from user
let threadViewModel = RemoteThreadViewModel(
context: AppContext.shared,
authContext: authContext,
statusID: statusId
)
coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
case "search":
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
guard
let authContext = coordinator?.authContext,
let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value
else { return }
let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery)
coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show)
default:
return
case "post":
showComposeViewController()
case "profile":
let components = url.pathComponents
guard
components.count == 2,
components[0] == "/",
let authContext = coordinator?.authContext
else { return }
Task {
do {
let domain = authContext.mastodonAuthenticationBox.domain
let authenticationBox = authContext.mastodonAuthenticationBox
guard let account = try await AppContext.shared.apiService.search(
query: .init(q: components[1], type: .accounts, resolve: true),
authenticationBox: authenticationBox
).value.accounts.first else { return }
let profileViewModel = ProfileViewModel(
context: AppContext.shared,
authContext: authContext,
account: account
)
self.coordinator?.present(
scene: .profile(viewModel: profileViewModel),
from: nil,
transition: .show
)
} catch {
// fail silently
}
}
case "status":
let components = url.pathComponents
guard
components.count == 2,
components[0] == "/",
let authContext = coordinator?.authContext
else { return }
let statusId = components[1]
// View post from user
let threadViewModel = RemoteThreadViewModel(
context: AppContext.shared,
authContext: authContext,
statusID: statusId
)
coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
case "search":
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
guard
let authContext = coordinator?.authContext,
let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value
else { return }
let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery)
coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show)
default:
return
}
}
}