chore: [WIP] refactor profile UI

This commit is contained in:
CMK 2022-05-13 17:23:35 +08:00
parent c21b6e6a89
commit 503fcfab2a
28 changed files with 1586 additions and 1417 deletions

View File

@ -27,6 +27,7 @@
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
- [Tabman](https://github.com/uias/Tabman)
- [TabBarPager](https://github.com/TwidereProject/TabBarPager)
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
- [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController)

View File

@ -267,6 +267,7 @@
DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; };
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; };
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; };
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DB486C0E282E41F200F69423 /* TabBarPager */; };
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; };
DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; };
DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
@ -1429,6 +1430,7 @@
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */,
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
@ -3448,6 +3450,7 @@
DBA5A52E26F07ED800CACBAA /* PanModal */,
DB3EA911281BBEA800598866 /* AlamofireImage */,
DB3EA913281BBEA800598866 /* Alamofire */,
DB486C0E282E41F200F69423 /* TabBarPager */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -3670,6 +3673,7 @@
DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */,
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */,
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@ -5795,6 +5799,14 @@
minimumVersion = 5.4.0;
};
};
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/TwidereProject/TabBarPager.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.1.0;
};
};
DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-collections.git";
@ -5959,6 +5971,11 @@
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
DB486C0E282E41F200F69423 /* TabBarPager */ = {
isa = XCSwiftPackageProductDependency;
package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */;
productName = TabBarPager;
};
DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = {
isa = XCSwiftPackageProductDependency;
package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */;

View File

@ -208,6 +208,15 @@
"version": "5.0.1"
}
},
{
"package": "TabBarPager",
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
"state": {
"branch": null,
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
"version": "0.1.0"
}
},
{
"package": "Tabman",
"repositoryURL": "https://github.com/uias/Tabman",

View File

@ -8,12 +8,12 @@
import UIKit
protocol ScrollViewContainer: UIViewController {
var scrollView: UIScrollView? { get }
var scrollView: UIScrollView { get }
func scrollToTop(animated: Bool)
}
extension ScrollViewContainer {
func scrollToTop(animated: Bool) {
scrollView?.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated)
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated)
}
}

View File

@ -148,9 +148,7 @@ extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer
extension DiscoveryCommunityViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
var scrollView: UIScrollView { tableView }
}
extension DiscoveryCommunityViewController {

View File

@ -130,8 +130,8 @@ extension DiscoveryViewController {
// MARK: - ScrollViewContainer
extension DiscoveryViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
return (currentViewController as? ScrollViewContainer)?.scrollView
var scrollView: UIScrollView {
return (currentViewController as? ScrollViewContainer)?.scrollView ?? UIScrollView()
}
}

View File

@ -168,8 +168,6 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
// MARK: ScrollViewContainer
extension DiscoveryForYouViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
var scrollView: UIScrollView { tableView }
}

View File

@ -127,9 +127,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer
extension DiscoveryHashtagsViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
var scrollView: UIScrollView { tableView }
}
extension DiscoveryHashtagsViewController {

View File

@ -127,9 +127,7 @@ extension DiscoveryNewsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer
extension DiscoveryNewsViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
var scrollView: UIScrollView { tableView }
}
extension DiscoveryNewsViewController {

View File

@ -160,9 +160,7 @@ extension DiscoveryPostsViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer
extension DiscoveryPostsViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
var scrollView: UIScrollView { tableView }
}
// MARK: - DiscoveryIntroBannerViewDelegate

View File

@ -537,13 +537,9 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate
// MARK: - ScrollViewContainer
extension HomeTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView? { return tableView }
var scrollView: UIScrollView { return tableView }
func scrollToTop(animated: Bool) {
guard let scrollView = scrollView else {
return
}
if scrollView.contentOffset.y < scrollView.frame.height,
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,

View File

@ -203,9 +203,7 @@ extension NotificationTimelineViewController: NotificationTableViewCellDelegate
// MARK: - ScrollViewContainer
extension NotificationTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView? { tableView }
var scrollView: UIScrollView { tableView }
}
extension NotificationTimelineViewController {

View File

@ -170,9 +170,9 @@ extension NotificationViewController {
// MARK: - ScrollViewContainer
extension NotificationViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
var scrollView: UIScrollView {
guard let viewController = currentViewController as? NotificationTimelineViewController else {
return nil
return UIScrollView()
}
return viewController.scrollView
}

View File

@ -9,6 +9,9 @@ import os.log
import UIKit
import Combine
import MetaTextKit
import MastodonLocalization
import TabBarPager
import XLPagerTabStrip
protocol ProfileAboutViewControllerDelegate: AnyObject {
func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
@ -162,7 +165,17 @@ extension ProfileAboutViewController: ProfileFieldEditCollectionViewCellDelegate
// MARK: - ScrollViewContainer
extension ProfileAboutViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
collectionView
var scrollView: UIScrollView { collectionView }
}
// MARK: - TabBarPage
extension ProfileAboutViewController: TabBarPage {
var pageScrollView: UIScrollView { scrollView }
}
// MARK: - IndicatorInfoProvider
extension ProfileAboutViewController: IndicatorInfoProvider {
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title: L10n.Scene.Profile.SegmentedControl.about)
}
}

View File

@ -15,7 +15,7 @@ import MastodonMeta
import MetaTextKit
import MastodonAsset
import MastodonLocalization
import Tabman
import TabBarPager
protocol ProfileHeaderViewControllerDelegate: AnyObject {
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
@ -28,6 +28,7 @@ final class ProfileHeaderViewController: UIViewController {
var disposeBag = Set<AnyCancellable>()
weak var delegate: ProfileHeaderViewControllerDelegate?
weak var headerDelegate: TabBarPagerHeaderDelegate?
var viewModel: ProfileHeaderViewModel!
@ -44,35 +45,35 @@ final class ProfileHeaderViewController: UIViewController {
let profileHeaderView = ProfileHeaderView()
let buttonBar: TMBar.ButtonBar = {
let buttonBar = TMBar.ButtonBar()
buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
buttonBar.backgroundView.style = .clear
buttonBar.layout.contentInset = .zero
return buttonBar
}()
// let buttonBar: TMBar.ButtonBar = {
// let buttonBar = TMBar.ButtonBar()
// buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
// buttonBar.backgroundView.style = .clear
// buttonBar.layout.contentInset = .zero
// return buttonBar
// }()
func customizeButtonBarAppearance() {
// The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
// Needs trigger update when `userInterfaceStyle` chagnes
let userInterfaceStyle = traitCollection.userInterfaceStyle
buttonBar.buttons.customize { button in
switch userInterfaceStyle {
case .dark:
// Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0)
default:
// Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6)
}
button.backgroundColor = .clear
}
}
// func customizeButtonBarAppearance() {
// // The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
// // Needs trigger update when `userInterfaceStyle` chagnes
// let userInterfaceStyle = traitCollection.userInterfaceStyle
// buttonBar.buttons.customize { button in
// switch userInterfaceStyle {
// case .dark:
// // Asset.Colors.Label.primary.color
// button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0)
// // Asset.Colors.Label.secondary.color
// button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0)
// default:
// // Asset.Colors.Label.primary.color
// button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0)
// // Asset.Colors.Label.secondary.color
// button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6)
// }
//
// button.backgroundColor = .clear
// }
// }
private var isBannerPinned = false
private var bottomShadowAlpha: CGFloat = 0.0
@ -113,7 +114,7 @@ extension ProfileHeaderViewController {
override func viewDidLoad() {
super.viewDidLoad()
customizeButtonBarAppearance()
// customizeButtonBarAppearance()
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
ThemeService.shared.currentTheme
@ -130,6 +131,7 @@ extension ProfileHeaderViewController {
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: profileHeaderView.bottomAnchor),
])
profileHeaderView.preservesSuperviewLayoutMargins = true
@ -262,14 +264,19 @@ extension ProfileHeaderViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view)
setupBottomShadow()
switch UIApplication.shared.applicationState {
case .active:
headerDelegate?.viewLayoutDidUpdate(self)
setupBottomShadow()
default:
break
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
customizeButtonBarAppearance()
// customizeButtonBarAppearance()
}
}
@ -338,63 +345,63 @@ extension ProfileHeaderViewController {
}
}
func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
updateHeaderBottomShadow(progress: progress)
let bannerImageView = profileHeaderView.bannerImageView
guard bannerImageView.bounds != .zero else {
// wait layout finish
return
}
let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
// scroll from bottom to top: 1 -> 2 -> 3
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
// 1
// banner top pin to window top and expand
bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
// 3
// banner bottom pin to navigation bar bottom and
// the `progress` growth to 1 then segmented control pin to top
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
bannerImageView.frame.size.height = bannerImageHeight
} else {
// 2
// banner move with scrolling from bottom to top until the
// banner bottom higher than navigation bar bottom
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
}
// set title view offset
let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
let transformY = max(0, titleViewContentOffset)
titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height
if viewModel.viewDidAppear.value {
viewModel.isTitleViewContentOffsetSet.value = true
}
// set avatar fade
if progress > 0 {
setProfileAvatar(alpha: 0)
} else if progress > -abs(throttle) {
// y = -(1/0.8T)x
let alpha = -1 / abs(0.8 * throttle) * progress
setProfileAvatar(alpha: alpha)
} else {
setProfileAvatar(alpha: 1)
}
}
// func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
// // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
// updateHeaderBottomShadow(progress: progress)
//
// let bannerImageView = profileHeaderView.bannerImageView
// guard bannerImageView.bounds != .zero else {
// // wait layout finish
// return
// }
//
// let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
// let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
//
// // scroll from bottom to top: 1 -> 2 -> 3
// if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
// // 1
// // banner top pin to window top and expand
// bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
// bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
// } else if bannerContainerBottomOffset < containerSafeAreaInset.top {
// // 3
// // banner bottom pin to navigation bar bottom and
// // the `progress` growth to 1 then segmented control pin to top
// bannerImageView.frame.origin.y = -containerSafeAreaInset.top
// let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
// bannerImageView.frame.size.height = bannerImageHeight
// } else {
// // 2
// // banner move with scrolling from bottom to top until the
// // banner bottom higher than navigation bar bottom
// bannerImageView.frame.origin.y = -containerSafeAreaInset.top
// bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
// }
//
// // set title view offset
// let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
// let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
// let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
// let transformY = max(0, titleViewContentOffset)
// titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
// viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height
//
// if viewModel.viewDidAppear.value {
// viewModel.isTitleViewContentOffsetSet.value = true
// }
//
// // set avatar fade
// if progress > 0 {
// setProfileAvatar(alpha: 0)
// } else if progress > -abs(throttle) {
// // y = -(1/0.8T)x
// let alpha = -1 / abs(0.8 * throttle) * progress
// setProfileAvatar(alpha: alpha)
// } else {
// setProfileAvatar(alpha: 1)
// }
// }
private func setProfileAvatar(alpha: CGFloat) {
profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
@ -488,3 +495,6 @@ extension ProfileHeaderViewController: CropViewControllerDelegate {
cropViewController.dismiss(animated: true, completion: nil)
}
}
// MARK: - TabBarPagerHeader
extension ProfileHeaderViewController: TabBarPagerHeader { }

File diff suppressed because it is too large Load Diff

View File

@ -27,149 +27,181 @@ class ProfileViewModel: NSObject {
private var mastodonUserObserver: AnyCancellable?
private var currentMastodonUserObserver: AnyCancellable?
let postsUserTimelineViewModel: UserTimelineViewModel
let repliesUserTimelineViewModel: UserTimelineViewModel
let mediaUserTimelineViewModel: UserTimelineViewModel
let profileAboutViewModel: ProfileAboutViewModel
// input
let context: AppContext
@Published var me: MastodonUser?
@Published var user: MastodonUser?
let viewDidAppear = PassthroughSubject<Void, Never>()
@Published var isEditing = false
@Published var isUpdating = false
// output
let domain: CurrentValueSubject<String?, Never>
let userID: CurrentValueSubject<UserID?, Never>
let bannerImageURL: CurrentValueSubject<URL?, Never>
let avatarImageURL: CurrentValueSubject<URL?, Never>
let name: CurrentValueSubject<String?, Never>
let username: CurrentValueSubject<String?, Never>
let bioDescription: CurrentValueSubject<String?, Never>
let url: CurrentValueSubject<String?, Never>
let statusesCount: CurrentValueSubject<Int?, Never>
let followingCount: CurrentValueSubject<Int?, Never>
let followersCount: CurrentValueSubject<Int?, Never>
let fields: CurrentValueSubject<[MastodonField], Never>
let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
@Published var userIdentifier: UserIdentifier? = nil
// let domain: CurrentValueSubject<String?, Never>
// let userID: CurrentValueSubject<UserID?, Never>
// let bannerImageURL: CurrentValueSubject<URL?, Never>
// let avatarImageURL: CurrentValueSubject<URL?, Never>
// let name: CurrentValueSubject<String?, Never>
// let username: CurrentValueSubject<String?, Never>
// let bioDescription: CurrentValueSubject<String?, Never>
// let url: CurrentValueSubject<String?, Never>
// let statusesCount: CurrentValueSubject<Int?, Never>
// let followingCount: CurrentValueSubject<Int?, Never>
// let followersCount: CurrentValueSubject<Int?, Never>
// let fields: CurrentValueSubject<[MastodonField], Never>
// let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
// fulfill this before editing
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
let protected: CurrentValueSubject<Bool?, Never>
let suspended: CurrentValueSubject<Bool, Never>
// let protected: CurrentValueSubject<Bool?, Never>
// let suspended: CurrentValueSubject<Bool, Never>
let isEditing = CurrentValueSubject<Bool, Never>(false)
let isUpdating = CurrentValueSubject<Bool, Never>(false)
let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none)
let isFollowedBy = CurrentValueSubject<Bool, Never>(false)
let isMuting = CurrentValueSubject<Bool, Never>(false)
let isBlocking = CurrentValueSubject<Bool, Never>(false)
let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
let isRelationshipActionButtonHidden = CurrentValueSubject<Bool, Never>(true)
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true)
let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
let needsPagingEnabled = CurrentValueSubject<Bool, Never>(true)
let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
//
// let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none)
// let isFollowedBy = CurrentValueSubject<Bool, Never>(false)
// let isMuting = CurrentValueSubject<Bool, Never>(false)
// let isBlocking = CurrentValueSubject<Bool, Never>(false)
// let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
//
// let isRelationshipActionButtonHidden = CurrentValueSubject<Bool, Never>(true)
// let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
// let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
// let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true)
//
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
// let needsPagingEnabled = CurrentValueSubject<Bool, Never>(true)
// let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
self.context = context
self.user = mastodonUser
self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
self.userID = CurrentValueSubject(mastodonUser?.id)
self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
self.bioDescription = CurrentValueSubject(mastodonUser?.note)
self.url = CurrentValueSubject(mastodonUser?.url)
self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) })
self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) })
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) })
self.protected = CurrentValueSubject(mastodonUser?.locked)
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
self.emojiMeta = CurrentValueSubject(mastodonUser?.emojis.asDictionary ?? [:])
// self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
// self.userID = CurrentValueSubject(mastodonUser?.id)
// self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
// self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
// self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
// self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
// self.bioDescription = CurrentValueSubject(mastodonUser?.note)
// self.url = CurrentValueSubject(mastodonUser?.url)
// self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) })
// self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) })
// self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) })
// self.protected = CurrentValueSubject(mastodonUser?.locked)
// self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
// self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
// self.emojiMeta = CurrentValueSubject(mastodonUser?.emojis.asDictionary ?? [:])
self.postsUserTimelineViewModel = UserTimelineViewModel(
context: context,
queryFilter: .init(excludeReplies: true)
)
self.repliesUserTimelineViewModel = UserTimelineViewModel(
context: context,
queryFilter: .init(excludeReplies: true)
)
self.mediaUserTimelineViewModel = UserTimelineViewModel(
context: context,
queryFilter: .init(onlyMedia: true)
)
self.profileAboutViewModel = ProfileAboutViewModel(context: context)
super.init()
relationshipActionOptionSet
.compactMap { $0.highPriorityAction(except: []) }
.map { $0 == .none }
.assign(to: \.value, on: isRelationshipActionButtonHidden)
.store(in: &disposeBag)
// bind active authentication
// bind me
context.authenticationService.activeMastodonAuthenticationBox
.receive(on: DispatchQueue.main)
.sink { [weak self] authenticationBox in
guard let self = self else { return }
guard let authenticationBox = authenticationBox else {
self.domain.value = nil
self.me = nil
return
}
self.domain.value = authenticationBox.domain
self.me = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user
}
.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)
// observe friendship
Publishers.CombineLatest3(
userRecord,
context.authenticationService.activeMastodonAuthenticationBox,
pendingRetryPublisher
)
.sink { [weak self] userRecord, authenticationBox, _ in
guard let self = self else { return }
guard let userRecord = userRecord,
let authenticationBox = authenticationBox
else { return }
Task {
do {
let response = try await self.updateRelationship(
record: userRecord,
authenticationBox: 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)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function)
}
}
} catch {
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship failure: \(error.localizedDescription)")
}
// bind user
$user
.map { user -> UserIdentifier? in
guard let user = user else { return nil }
return MastodonUserIdentifier(domain: user.domain, userID: user.id)
}
}
.store(in: &disposeBag)
let isBlockingOrBlocked = Publishers.CombineLatest(
isBlocking,
isBlockedBy
)
.map { $0 || $1 }
.share()
.assign(to: &$userIdentifier)
$userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
$userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
$userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
// $userIdentifier.assign(to: &profileAboutViewModel.$userIdentifier)
// relationshipActionOptionSet
// .compactMap { $0.highPriorityAction(except: []) }
// .map { $0 == .none }
// .assign(to: \.value, on: isRelationshipActionButtonHidden)
// .store(in: &disposeBag)
//
isBlockingOrBlocked
.map { !$0 }
.assign(to: \.value, on: needsPagingEnabled)
.store(in: &disposeBag)
isBlockingOrBlocked
.map { $0 }
.assign(to: \.value, on: needsImageOverlayBlurred)
.store(in: &disposeBag)
setup()
//
// // query relationship
// let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
// user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
// }
// let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
//
// // observe friendship
// Publishers.CombineLatest3(
// userRecord,
// context.authenticationService.activeMastodonAuthenticationBox,
// pendingRetryPublisher
// )
// .sink { [weak self] userRecord, authenticationBox, _ in
// guard let self = self else { return }
// guard let userRecord = userRecord,
// let authenticationBox = authenticationBox
// else { return }
// Task {
// do {
// let response = try await self.updateRelationship(
// record: userRecord,
// authenticationBox: 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)
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function)
// }
// }
// } catch {
// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship failure: \(error.localizedDescription)")
// }
// }
// }
// .store(in: &disposeBag)
//
// let isBlockingOrBlocked = Publishers.CombineLatest(
// isBlocking,
// isBlockedBy
// )
// .map { $0 || $1 }
// .share()
//
// isBlockingOrBlocked
// .map { !$0 }
// .assign(to: \.value, on: needsPagingEnabled)
// .store(in: &disposeBag)
//
// isBlockingOrBlocked
// .map { $0 }
// .assign(to: \.value, on: needsImageOverlayBlurred)
// .store(in: &disposeBag)
//
// setup()
}
}
@ -245,101 +277,101 @@ extension ProfileViewModel {
}
private func update(mastodonUser: MastodonUser?) {
self.userID.value = mastodonUser?.id
self.bannerImageURL.value = mastodonUser?.headerImageURL()
self.avatarImageURL.value = mastodonUser?.avatarImageURL()
self.name.value = mastodonUser?.displayNameWithFallback
self.username.value = mastodonUser?.acctWithDomain
self.bioDescription.value = mastodonUser?.note
self.url.value = mastodonUser?.url
self.statusesCount.value = mastodonUser.flatMap { Int($0.statusesCount) }
self.followingCount.value = mastodonUser.flatMap { Int($0.followingCount) }
self.followersCount.value = mastodonUser.flatMap { Int($0.followersCount) }
self.protected.value = mastodonUser?.locked
self.suspended.value = mastodonUser?.suspended ?? false
self.fields.value = mastodonUser?.fields ?? []
self.emojiMeta.value = mastodonUser?.emojis.asDictionary ?? [:]
// self.userID.value = mastodonUser?.id
// self.bannerImageURL.value = mastodonUser?.headerImageURL()
// self.avatarImageURL.value = mastodonUser?.avatarImageURL()
// self.name.value = mastodonUser?.displayNameWithFallback
// self.username.value = mastodonUser?.acctWithDomain
// self.bioDescription.value = mastodonUser?.note
// self.url.value = mastodonUser?.url
// self.statusesCount.value = mastodonUser.flatMap { Int($0.statusesCount) }
// self.followingCount.value = mastodonUser.flatMap { Int($0.followingCount) }
// self.followersCount.value = mastodonUser.flatMap { Int($0.followersCount) }
// self.protected.value = mastodonUser?.locked
// self.suspended.value = mastodonUser?.suspended ?? false
// self.fields.value = mastodonUser?.fields ?? []
// self.emojiMeta.value = mastodonUser?.emojis.asDictionary ?? [:]
}
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
guard let mastodonUser = mastodonUser,
let currentMastodonUser = currentMastodonUser else {
// set relationship
self.relationshipActionOptionSet.value = .none
self.isFollowedBy.value = false
self.isMuting.value = false
self.isBlocking.value = false
self.isBlockedBy.value = false
// set bar button item state
self.isReplyBarButtonItemHidden.value = true
self.isMoreMenuBarButtonItemHidden.value = true
self.isMeBarButtonItemsHidden.value = true
return
}
if mastodonUser == currentMastodonUser {
self.relationshipActionOptionSet.value = [.edit]
// set bar button item state
self.isReplyBarButtonItemHidden.value = true
self.isMoreMenuBarButtonItemHidden.value = true
self.isMeBarButtonItemsHidden.value = false
} else {
// set with follow action default
var relationshipActionSet = RelationshipActionOptionSet([.follow])
if mastodonUser.locked {
relationshipActionSet.insert(.request)
}
if mastodonUser.suspended {
relationshipActionSet.insert(.suspended)
}
let isFollowing = mastodonUser.followingBy.contains(currentMastodonUser)
if isFollowing {
relationshipActionSet.insert(.following)
}
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowing: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowing.description)
let isPending = mastodonUser.followRequestedBy.contains(currentMastodonUser)
if isPending {
relationshipActionSet.insert(.pending)
}
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isPending: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isPending.description)
let isFollowedBy = currentMastodonUser.followingBy.contains(mastodonUser)
self.isFollowedBy.value = isFollowedBy
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowedBy.description)
let isMuting = mastodonUser.mutingBy.contains(currentMastodonUser)
if isMuting {
relationshipActionSet.insert(.muting)
}
self.isMuting.value = isMuting
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isMuting: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isMuting.description)
let isBlocking = mastodonUser.blockingBy.contains(currentMastodonUser)
if isBlocking {
relationshipActionSet.insert(.blocking)
}
self.isBlocking.value = isBlocking
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlocking: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlocking.description)
let isBlockedBy = currentMastodonUser.blockingBy.contains(mastodonUser)
if isBlockedBy {
relationshipActionSet.insert(.blocked)
}
self.isBlockedBy.value = isBlockedBy
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlockedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlockedBy.description)
self.relationshipActionOptionSet.value = relationshipActionSet
// set bar button item state
self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy
self.isMoreMenuBarButtonItemHidden.value = false
self.isMeBarButtonItemsHidden.value = true
}
// guard let mastodonUser = mastodonUser,
// let currentMastodonUser = currentMastodonUser else {
// // set relationship
// self.relationshipActionOptionSet.value = .none
// self.isFollowedBy.value = false
// self.isMuting.value = false
// self.isBlocking.value = false
// self.isBlockedBy.value = false
//
// // set bar button item state
// self.isReplyBarButtonItemHidden.value = true
// self.isMoreMenuBarButtonItemHidden.value = true
// self.isMeBarButtonItemsHidden.value = true
// return
// }
//
// if mastodonUser == currentMastodonUser {
// self.relationshipActionOptionSet.value = [.edit]
// // set bar button item state
// self.isReplyBarButtonItemHidden.value = true
// self.isMoreMenuBarButtonItemHidden.value = true
// self.isMeBarButtonItemsHidden.value = false
// } else {
// // set with follow action default
// var relationshipActionSet = RelationshipActionOptionSet([.follow])
//
// if mastodonUser.locked {
// relationshipActionSet.insert(.request)
// }
//
// if mastodonUser.suspended {
// relationshipActionSet.insert(.suspended)
// }
//
// let isFollowing = mastodonUser.followingBy.contains(currentMastodonUser)
// if isFollowing {
// relationshipActionSet.insert(.following)
// }
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowing: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowing.description)
//
// let isPending = mastodonUser.followRequestedBy.contains(currentMastodonUser)
// if isPending {
// relationshipActionSet.insert(.pending)
// }
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isPending: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isPending.description)
//
// let isFollowedBy = currentMastodonUser.followingBy.contains(mastodonUser)
// self.isFollowedBy.value = isFollowedBy
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowedBy.description)
//
// let isMuting = mastodonUser.mutingBy.contains(currentMastodonUser)
// if isMuting {
// relationshipActionSet.insert(.muting)
// }
// self.isMuting.value = isMuting
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isMuting: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isMuting.description)
//
// let isBlocking = mastodonUser.blockingBy.contains(currentMastodonUser)
// if isBlocking {
// relationshipActionSet.insert(.blocking)
// }
// self.isBlocking.value = isBlocking
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlocking: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlocking.description)
//
// let isBlockedBy = currentMastodonUser.blockingBy.contains(mastodonUser)
// if isBlockedBy {
// relationshipActionSet.insert(.blocked)
// }
// self.isBlockedBy.value = isBlockedBy
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlockedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlockedBy.description)
//
// self.relationshipActionOptionSet.value = relationshipActionSet
//
// // set bar button item state
// self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy
// self.isMoreMenuBarButtonItemHidden.value = false
// self.isMeBarButtonItemsHidden.value = true
// }
}
}

View File

@ -7,40 +7,42 @@
import os.log
import UIKit
import Pageboy
import Tabman
import XLPagerTabStrip
import TabBarPager
protocol ProfilePagingViewControllerDelegate: AnyObject {
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int)
}
final class ProfilePagingViewController: TabmanViewController {
final class ProfilePagingViewController: ButtonBarPagerTabStripViewController, TabBarPageViewController {
weak var tabBarPageViewDelegate: TabBarPageViewDelegate?
weak var pagingDelegate: ProfilePagingViewControllerDelegate?
var viewModel: ProfilePagingViewModel!
// MARK: - TabBarPageViewController
// MARK: - PageboyViewControllerDelegate
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didCancelScrollToPageAt index: PageboyViewController.PageIndex, returnToPageAt previousIndex: PageboyViewController.PageIndex) {
super.pageboyViewController(pageboyViewController, didCancelScrollToPageAt: index, returnToPageAt: previousIndex)
// Fix the SDK bug for table view get row selected during swipe but cancel paging
guard previousIndex < viewModel.viewControllers.count else { return }
let viewController = viewModel.viewControllers[previousIndex]
if let tableView = viewController.scrollView as? UITableView {
for cell in tableView.visibleCells {
cell.setHighlighted(false, animated: false)
}
}
var currentPage: TabBarPage? {
return viewModel.viewControllers[currentIndex]
}
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) {
super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated)
var currentPageIndex: Int? {
currentIndex
}
// MARK: - ButtonBarPagerTabStripViewController
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
return viewModel.viewControllers
}
override func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
super.updateIndicator(for: viewController, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: progressPercentage, indexWasChanged: indexWasChanged)
let viewController = viewModel.viewControllers[index]
(viewController as? StatusTableViewControllerNavigateable)?.overrideNavigationScrollPosition = .top
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
guard indexWasChanged else { return }
let page = viewModel.viewControllers[toIndex]
tabBarPageViewDelegate?.pageViewController(self, didPresentingTabBarPage: page, at: toIndex)
}
// make key commands works
@ -49,7 +51,7 @@ final class ProfilePagingViewController: TabmanViewController {
}
deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
@ -59,8 +61,8 @@ extension ProfilePagingViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
dataSource = viewModel
// view.backgroundColor = .clear
// dataSource = viewModel
}
override func viewDidAppear(_ animated: Bool) {
@ -74,17 +76,17 @@ extension ProfilePagingViewController {
// workaround to fix tab man responder chain issue
extension ProfilePagingViewController {
override var keyCommands: [UIKeyCommand]? {
return currentViewController?.keyCommands
}
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
}
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
}
// override var keyCommands: [UIKeyCommand]? {
// return currentPage?.keyCommands
// }
//
// @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
// (currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
//
// }
//
// @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
// (currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
// }
}

View File

@ -7,10 +7,9 @@
import os.log
import UIKit
import Pageboy
import Tabman
import MastodonAsset
import MastodonLocalization
import TabBarPager
final class ProfilePagingViewModel: NSObject {
@ -32,7 +31,7 @@ final class ProfilePagingViewModel: NSObject {
super.init()
}
var viewControllers: [ScrollViewContainer] {
var viewControllers: [UIViewController & TabBarPage] {
return [
postUserTimelineViewController,
repliesUserTimelineViewController,
@ -41,42 +40,42 @@ final class ProfilePagingViewModel: NSObject {
]
}
let barItems: [TMBarItemable] = {
let items = [
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.postsAndReplies),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.about),
]
return items
}()
// let barItems: [TMBarItemable] = {
// let items = [
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.postsAndReplies),
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.about),
// ]
// return items
// }()
deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
// MARK: - PageboyViewControllerDataSource
extension ProfilePagingViewModel: PageboyViewControllerDataSource {
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
return viewControllers.count
}
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
return viewControllers[index]
}
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
return .first
}
}
// MARK: - TMBarDataSource
extension ProfilePagingViewModel: TMBarDataSource {
func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
return barItems[index]
}
}
//// MARK: - PageboyViewControllerDataSource
//extension ProfilePagingViewModel: PageboyViewControllerDataSource {
//
// func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
// return viewControllers.count
// }
//
// func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
// return viewControllers[index]
// }
//
// func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
// return .first
// }
//
//}
//
//// MARK: - TMBarDataSource
//extension ProfilePagingViewModel: TMBarDataSource {
// func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
// return barItems[index]
// }
//}

View File

@ -11,6 +11,8 @@ import AVKit
import Combine
import CoreDataStack
import GameplayKit
import TabBarPager
import XLPagerTabStrip
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -143,7 +145,14 @@ extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableView
// MARK: - CustomScrollViewContainerController
extension UserTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView? { return tableView }
var scrollView: UIScrollView { return tableView }
}
// MARK: - TabBarPage
extension UserTimelineViewController: TabBarPage {
var pageScrollView: UIScrollView {
scrollView
}
}
// MARK: - StatusTableViewCellDelegate
@ -165,3 +174,10 @@ extension UserTimelineViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender)
}
}
// MARK: - IndicatorInfoProvider
extension UserTimelineViewController: IndicatorInfoProvider {
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title: "Hello")
}
}

View File

@ -30,17 +30,14 @@ extension UserTimelineViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
// trigger user timeline loading
Publishers.CombineLatest(
$domain.removeDuplicates(),
$userID.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
}
.store(in: &disposeBag)
// trigger timeline reloading
$userIdentifier
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
}
.store(in: &disposeBag)
let needsTimelineHidden = Publishers.CombineLatest3(
isBlocking,

View File

@ -50,7 +50,7 @@ extension UserTimelineViewModel.State {
guard let viewModel = viewModel else { return false }
switch stateClass {
case is Reloading.Type:
return viewModel.userID != nil
return viewModel.userIdentifier != nil
default:
return false
}
@ -132,7 +132,7 @@ extension UserTimelineViewModel.State {
let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last
guard let userID = viewModel.userID, !userID.isEmpty else {
guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
}

View File

@ -19,8 +19,7 @@ final class UserTimelineViewModel {
// input
let context: AppContext
@Published var domain: String?
@Published var userID: String?
@Published var userIdentifier: UserIdentifier?
@Published var queryFilter: QueryFilter
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -48,30 +47,25 @@ final class UserTimelineViewModel {
init(
context: AppContext,
domain: String?,
userID: String?,
queryFilter: QueryFilter
) {
self.context = context
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: domain,
additionalTweetPredicate: Status.notDeleted()
domain: nil,
additionalTweetPredicate: nil
)
self.domain = domain
self.userID = userID
self.queryFilter = queryFilter
// super.init()
$domain
context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.value, on: statusFetchedResultsController.domain)
.store(in: &disposeBag)
}
deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
@ -92,5 +86,4 @@ extension UserTimelineViewModel {
self.onlyMedia = onlyMedia
}
}
}

View File

@ -0,0 +1,28 @@
//
// UserIdentifier.swift
//
//
// Created by MainasuK on 2022-5-13.
//
import Foundation
import MastodonSDK
public protocol UserIdentifier {
var domain: String { get }
var userID: Mastodon.Entity.Account.ID { get }
}
public struct MastodonUserIdentifier: UserIdentifier {
public let domain: String
public var userID: Mastodon.Entity.Account.ID
public init(
domain: String,
userID: Mastodon.Entity.Account.ID
) {
self.domain = domain
self.userID = userID
}
}

View File

@ -1,14 +0,0 @@
//
// UserIdentifier.swift
//
//
// Created by MainasuK on 2022-1-12.
//
import Foundation
import MastodonSDK
public protocol UserIdentifier {
var domain: String { get }
var userID: Mastodon.Entity.Account.ID { get }
}

View File

@ -8,6 +8,7 @@ target 'Mastodon' do
# UI
pod 'UITextField+Shake', '~> 1.2'
pod 'XLPagerTabStrip', '~> 9.0.0'
# misc
pod 'SwiftGen', '~> 6.4.0'

View File

@ -8,6 +8,7 @@ PODS:
- Sourcery/CLI-Only (1.6.1)
- SwiftGen (6.4.0)
- "UITextField+Shake (1.2.1)"
- XLPagerTabStrip (9.0.0)
DEPENDENCIES:
- DateToolsSwift (~> 5.0.0)
@ -17,6 +18,7 @@ DEPENDENCIES:
- Sourcery (~> 1.6.1)
- SwiftGen (~> 6.4.0)
- "UITextField+Shake (~> 1.2)"
- XLPagerTabStrip (~> 9.0.0)
SPEC REPOS:
trunk:
@ -26,6 +28,7 @@ SPEC REPOS:
- Sourcery
- SwiftGen
- "UITextField+Shake"
- XLPagerTabStrip
EXTERNAL SOURCES:
Keys:
@ -39,7 +42,8 @@ SPEC CHECKSUMS:
Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5
PODFILE CHECKSUM: 335d0ca70493d4c280d0f8fd7f26fe9be6a4e289
PODFILE CHECKSUM: 1ac960a2c981ef98f7c24a3bba57bdabc1f66103
COCOAPODS: 1.11.3