feat: add remote profile load logic for profile scene
This commit is contained in:
parent
3b576badeb
commit
2f89471c78
|
@ -266,6 +266,7 @@
|
|||
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F932616E28B004B8251 /* APIService+Follow.swift */; };
|
||||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
|
||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; };
|
||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
||||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
||||
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
|
||||
|
@ -618,6 +619,7 @@
|
|||
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follow.swift"; sourceTree = "<group>"; };
|
||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -1524,6 +1526,7 @@
|
|||
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */,
|
||||
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
|
||||
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */,
|
||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */,
|
||||
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
|
@ -2098,6 +2101,7 @@
|
|||
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */,
|
||||
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||
|
|
|
@ -24,6 +24,10 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
StatusProviderFacade.responseToStatusActiveLabelAction(provider: self, cell: cell, activeLabel: activeLabel, didTapEntity: entity)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ActionToolbarContainerDelegate
|
||||
|
|
|
@ -62,6 +62,68 @@ extension StatusProviderFacade {
|
|||
}
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) {
|
||||
switch entity.type {
|
||||
case .hashtag(let text, let userInfo):
|
||||
break
|
||||
case .mention(let text, let userInfo):
|
||||
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text)
|
||||
case .url(_, _, let url, _):
|
||||
guard let url = URL(string: url) else { return }
|
||||
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell, mention: String) {
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
|
||||
provider.status(for: cell, indexPath: nil)
|
||||
.sink { [weak provider] status in
|
||||
guard let provider = provider else { return }
|
||||
let _status: Status? = {
|
||||
switch target {
|
||||
case .primary: return status?.reblog ?? status
|
||||
case .secondary: return status
|
||||
}
|
||||
}()
|
||||
guard let status = _status else { return }
|
||||
|
||||
// cannot continue without meta
|
||||
guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { return }
|
||||
|
||||
let userID = mentionMeta.id
|
||||
|
||||
let profileViewModel: ProfileViewModel = {
|
||||
// check if self
|
||||
guard userID != activeMastodonAuthenticationBox.userID else {
|
||||
return MeProfileViewModel(context: provider.context)
|
||||
}
|
||||
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.fetchLimit = 1
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: userID)
|
||||
let mastodonUser = provider.context.managedObjectContext.safeFetch(request).first
|
||||
|
||||
if let mastodonUser = mastodonUser {
|
||||
return CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
|
||||
} else {
|
||||
return RemoteProfileViewModel(context: provider.context, userID: userID)
|
||||
}
|
||||
}()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
|
||||
}
|
||||
}
|
||||
.store(in: &provider.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
static func responseToStatusLikeAction(provider: StatusProvider) {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// RemoteProfileViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-2.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
final class RemoteProfileViewModel: ProfileViewModel {
|
||||
|
||||
convenience init(context: AppContext, userID: Mastodon.Entity.Account.ID) {
|
||||
self.init(context: context, optionalMastodonUser: nil)
|
||||
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
let authorization = activeMastodonAuthenticationBox.userAuthorization
|
||||
context.apiService.accountInfo(
|
||||
domain: domain,
|
||||
userID: userID,
|
||||
authorization: authorization
|
||||
)
|
||||
.retry(3)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, userID, error.localizedDescription)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote user %s fetched", ((#file as NSString).lastPathComponent), #line, #function, userID)
|
||||
}
|
||||
} 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.mastodonUser.value = mastodonUser
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@ protocol StatusViewDelegate: class {
|
|||
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||
}
|
||||
|
||||
final class StatusView: UIView {
|
||||
|
@ -402,6 +403,7 @@ extension StatusView {
|
|||
statusContentWarningContainerStackView.isHidden = true
|
||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
||||
|
||||
activeTextLabel.delegate = self
|
||||
playerContainerView.delegate = self
|
||||
|
||||
headerInfoLabelTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerInfoLabelTapGestureRecognizerHandler(_:)))
|
||||
|
@ -475,6 +477,14 @@ extension StatusView {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - ActiveLabelDelegate
|
||||
extension StatusView: ActiveLabelDelegate {
|
||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
|
||||
delegate?.statusView(self, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PlayerContainerViewDelegate
|
||||
extension StatusView: PlayerContainerViewDelegate {
|
||||
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import AVKit
|
|||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import ActiveLabel
|
||||
|
||||
protocol StatusTableViewCellDelegate: class {
|
||||
var context: AppContext! { get }
|
||||
|
@ -18,18 +19,22 @@ protocol StatusTableViewCellDelegate: class {
|
|||
|
||||
func parent() -> UIViewController
|
||||
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarButtonDidPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath)
|
||||
}
|
||||
|
||||
|
@ -216,6 +221,10 @@ extension StatusTableViewCell: StatusViewDelegate {
|
|||
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
delegate?.statusTableViewCell(self, statusView: statusView, activeLabel: activeLabel, didSelectActiveEntity: entity)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MosaicImageViewDelegate
|
||||
|
|
|
@ -10,6 +10,52 @@ import Combine
|
|||
import CommonOSLog
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func accountInfo(
|
||||
domain: String,
|
||||
userID: Mastodon.Entity.Account.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||
return Mastodon.API.Account.accountInfo(
|
||||
session: session,
|
||||
domain: domain,
|
||||
userID: userID,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||
let log = OSLog.api
|
||||
let account = response.value
|
||||
|
||||
return self.backgroundManagedObjectContext.performChanges {
|
||||
let (mastodonUser, isCreated) = APIService.CoreData.createOrMergeMastodonUser(
|
||||
into: self.backgroundManagedObjectContext,
|
||||
for: nil,
|
||||
in: domain,
|
||||
entity: account,
|
||||
userCache: nil,
|
||||
networkDate: response.networkDate,
|
||||
log: log
|
||||
)
|
||||
let flag = isCreated ? "+" : "-"
|
||||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: fetch mastodon user [%s](%s)%s", ((#file as NSString).lastPathComponent), #line, #function, flag, mastodonUser.id, mastodonUser.username)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Account> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension APIService {
|
||||
|
||||
func accountVerifyCredentials(
|
||||
|
@ -33,12 +79,20 @@ extension APIService {
|
|||
entity: account,
|
||||
userCache: nil,
|
||||
networkDate: response.networkDate,
|
||||
log: log)
|
||||
log: log
|
||||
)
|
||||
let flag = isCreated ? "+" : "-"
|
||||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: mastodon user [%s](%s)%s verifed", ((#file as NSString).lastPathComponent), #line, #function, flag, mastodonUser.id, mastodonUser.username)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.map { _ in return response }
|
||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Account> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -72,7 +126,14 @@ extension APIService {
|
|||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: mastodon user [%s](%s)%s verifed", ((#file as NSString).lastPathComponent), #line, #function, flag, mastodonUser.id, mastodonUser.username)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.map { _ in return response }
|
||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Account> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
|
Loading…
Reference in New Issue