chore: [WIP] refactor profile UI
This commit is contained in:
parent
c21b6e6a89
commit
503fcfab2a
|
@ -27,6 +27,7 @@
|
||||||
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
||||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||||
- [Tabman](https://github.com/uias/Tabman)
|
- [Tabman](https://github.com/uias/Tabman)
|
||||||
|
- [TabBarPager](https://github.com/TwidereProject/TabBarPager)
|
||||||
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
|
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
|
||||||
- [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
|
- [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
|
||||||
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController)
|
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController)
|
||||||
|
|
|
@ -267,6 +267,7 @@
|
||||||
DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; };
|
DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; };
|
||||||
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.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 */; };
|
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 */; };
|
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; };
|
||||||
DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; };
|
DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; };
|
||||||
DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
|
DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
|
||||||
|
@ -1429,6 +1430,7 @@
|
||||||
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
|
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
|
||||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||||
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
|
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
|
||||||
|
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */,
|
||||||
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
|
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
|
||||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||||
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
|
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
|
||||||
|
@ -3448,6 +3450,7 @@
|
||||||
DBA5A52E26F07ED800CACBAA /* PanModal */,
|
DBA5A52E26F07ED800CACBAA /* PanModal */,
|
||||||
DB3EA911281BBEA800598866 /* AlamofireImage */,
|
DB3EA911281BBEA800598866 /* AlamofireImage */,
|
||||||
DB3EA913281BBEA800598866 /* Alamofire */,
|
DB3EA913281BBEA800598866 /* Alamofire */,
|
||||||
|
DB486C0E282E41F200F69423 /* TabBarPager */,
|
||||||
);
|
);
|
||||||
productName = Mastodon;
|
productName = Mastodon;
|
||||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||||
|
@ -3670,6 +3673,7 @@
|
||||||
DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */,
|
DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */,
|
||||||
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||||
DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||||
|
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */,
|
||||||
);
|
);
|
||||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -5795,6 +5799,14 @@
|
||||||
minimumVersion = 5.4.0;
|
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" */ = {
|
DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/apple/swift-collections.git";
|
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||||
|
@ -5959,6 +5971,11 @@
|
||||||
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
|
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||||
productName = Alamofire;
|
productName = Alamofire;
|
||||||
};
|
};
|
||||||
|
DB486C0E282E41F200F69423 /* TabBarPager */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */;
|
||||||
|
productName = TabBarPager;
|
||||||
|
};
|
||||||
DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = {
|
DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
|
|
@ -208,6 +208,15 @@
|
||||||
"version": "5.0.1"
|
"version": "5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "TabBarPager",
|
||||||
|
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "Tabman",
|
"package": "Tabman",
|
||||||
"repositoryURL": "https://github.com/uias/Tabman",
|
"repositoryURL": "https://github.com/uias/Tabman",
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol ScrollViewContainer: UIViewController {
|
protocol ScrollViewContainer: UIViewController {
|
||||||
var scrollView: UIScrollView? { get }
|
var scrollView: UIScrollView { get }
|
||||||
func scrollToTop(animated: Bool)
|
func scrollToTop(animated: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ScrollViewContainer {
|
extension ScrollViewContainer {
|
||||||
func scrollToTop(animated: Bool) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,9 +148,7 @@ extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
|
||||||
|
|
||||||
// MARK: ScrollViewContainer
|
// MARK: ScrollViewContainer
|
||||||
extension DiscoveryCommunityViewController: ScrollViewContainer {
|
extension DiscoveryCommunityViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView { tableView }
|
||||||
tableView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DiscoveryCommunityViewController {
|
extension DiscoveryCommunityViewController {
|
||||||
|
|
|
@ -130,8 +130,8 @@ extension DiscoveryViewController {
|
||||||
|
|
||||||
// MARK: - ScrollViewContainer
|
// MARK: - ScrollViewContainer
|
||||||
extension DiscoveryViewController: ScrollViewContainer {
|
extension DiscoveryViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView {
|
||||||
return (currentViewController as? ScrollViewContainer)?.scrollView
|
return (currentViewController as? ScrollViewContainer)?.scrollView ?? UIScrollView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,8 +168,6 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
|
||||||
|
|
||||||
// MARK: ScrollViewContainer
|
// MARK: ScrollViewContainer
|
||||||
extension DiscoveryForYouViewController: ScrollViewContainer {
|
extension DiscoveryForYouViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView { tableView }
|
||||||
tableView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,9 +127,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
|
||||||
|
|
||||||
// MARK: ScrollViewContainer
|
// MARK: ScrollViewContainer
|
||||||
extension DiscoveryHashtagsViewController: ScrollViewContainer {
|
extension DiscoveryHashtagsViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView { tableView }
|
||||||
tableView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DiscoveryHashtagsViewController {
|
extension DiscoveryHashtagsViewController {
|
||||||
|
|
|
@ -127,9 +127,7 @@ extension DiscoveryNewsViewController: UITableViewDelegate {
|
||||||
|
|
||||||
// MARK: ScrollViewContainer
|
// MARK: ScrollViewContainer
|
||||||
extension DiscoveryNewsViewController: ScrollViewContainer {
|
extension DiscoveryNewsViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView { tableView }
|
||||||
tableView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DiscoveryNewsViewController {
|
extension DiscoveryNewsViewController {
|
||||||
|
|
|
@ -160,9 +160,7 @@ extension DiscoveryPostsViewController: StatusTableViewCellDelegate { }
|
||||||
|
|
||||||
// MARK: ScrollViewContainer
|
// MARK: ScrollViewContainer
|
||||||
extension DiscoveryPostsViewController: ScrollViewContainer {
|
extension DiscoveryPostsViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView { tableView }
|
||||||
tableView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - DiscoveryIntroBannerViewDelegate
|
// MARK: - DiscoveryIntroBannerViewDelegate
|
||||||
|
|
|
@ -537,13 +537,9 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate
|
||||||
// MARK: - ScrollViewContainer
|
// MARK: - ScrollViewContainer
|
||||||
extension HomeTimelineViewController: ScrollViewContainer {
|
extension HomeTimelineViewController: ScrollViewContainer {
|
||||||
|
|
||||||
var scrollView: UIScrollView? { return tableView }
|
var scrollView: UIScrollView { return tableView }
|
||||||
|
|
||||||
func scrollToTop(animated: Bool) {
|
func scrollToTop(animated: Bool) {
|
||||||
guard let scrollView = scrollView else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if scrollView.contentOffset.y < scrollView.frame.height,
|
if scrollView.contentOffset.y < scrollView.frame.height,
|
||||||
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
|
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
|
||||||
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,
|
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,
|
||||||
|
|
|
@ -203,9 +203,7 @@ extension NotificationTimelineViewController: NotificationTableViewCellDelegate
|
||||||
|
|
||||||
// MARK: - ScrollViewContainer
|
// MARK: - ScrollViewContainer
|
||||||
extension NotificationTimelineViewController: ScrollViewContainer {
|
extension NotificationTimelineViewController: ScrollViewContainer {
|
||||||
|
var scrollView: UIScrollView { tableView }
|
||||||
var scrollView: UIScrollView? { tableView }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationTimelineViewController {
|
extension NotificationTimelineViewController {
|
||||||
|
|
|
@ -170,9 +170,9 @@ extension NotificationViewController {
|
||||||
|
|
||||||
// MARK: - ScrollViewContainer
|
// MARK: - ScrollViewContainer
|
||||||
extension NotificationViewController: ScrollViewContainer {
|
extension NotificationViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView {
|
||||||
guard let viewController = currentViewController as? NotificationTimelineViewController else {
|
guard let viewController = currentViewController as? NotificationTimelineViewController else {
|
||||||
return nil
|
return UIScrollView()
|
||||||
}
|
}
|
||||||
return viewController.scrollView
|
return viewController.scrollView
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import MetaTextKit
|
import MetaTextKit
|
||||||
|
import MastodonLocalization
|
||||||
|
import TabBarPager
|
||||||
|
import XLPagerTabStrip
|
||||||
|
|
||||||
protocol ProfileAboutViewControllerDelegate: AnyObject {
|
protocol ProfileAboutViewControllerDelegate: AnyObject {
|
||||||
func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
|
func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
|
||||||
|
@ -162,7 +165,17 @@ extension ProfileAboutViewController: ProfileFieldEditCollectionViewCellDelegate
|
||||||
|
|
||||||
// MARK: - ScrollViewContainer
|
// MARK: - ScrollViewContainer
|
||||||
extension ProfileAboutViewController: ScrollViewContainer {
|
extension ProfileAboutViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView { collectionView }
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import MastodonMeta
|
||||||
import MetaTextKit
|
import MetaTextKit
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import Tabman
|
import TabBarPager
|
||||||
|
|
||||||
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
||||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
||||||
|
@ -28,6 +28,7 @@ final class ProfileHeaderViewController: UIViewController {
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
weak var delegate: ProfileHeaderViewControllerDelegate?
|
weak var delegate: ProfileHeaderViewControllerDelegate?
|
||||||
|
weak var headerDelegate: TabBarPagerHeaderDelegate?
|
||||||
|
|
||||||
var viewModel: ProfileHeaderViewModel!
|
var viewModel: ProfileHeaderViewModel!
|
||||||
|
|
||||||
|
@ -44,35 +45,35 @@ final class ProfileHeaderViewController: UIViewController {
|
||||||
|
|
||||||
let profileHeaderView = ProfileHeaderView()
|
let profileHeaderView = ProfileHeaderView()
|
||||||
|
|
||||||
let buttonBar: TMBar.ButtonBar = {
|
// let buttonBar: TMBar.ButtonBar = {
|
||||||
let buttonBar = TMBar.ButtonBar()
|
// let buttonBar = TMBar.ButtonBar()
|
||||||
buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
|
// buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
|
||||||
buttonBar.backgroundView.style = .clear
|
// buttonBar.backgroundView.style = .clear
|
||||||
buttonBar.layout.contentInset = .zero
|
// buttonBar.layout.contentInset = .zero
|
||||||
return buttonBar
|
// return buttonBar
|
||||||
}()
|
// }()
|
||||||
|
|
||||||
func customizeButtonBarAppearance() {
|
// func customizeButtonBarAppearance() {
|
||||||
// The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
|
// // The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
|
||||||
// Needs trigger update when `userInterfaceStyle` chagnes
|
// // Needs trigger update when `userInterfaceStyle` chagnes
|
||||||
let userInterfaceStyle = traitCollection.userInterfaceStyle
|
// let userInterfaceStyle = traitCollection.userInterfaceStyle
|
||||||
buttonBar.buttons.customize { button in
|
// buttonBar.buttons.customize { button in
|
||||||
switch userInterfaceStyle {
|
// switch userInterfaceStyle {
|
||||||
case .dark:
|
// case .dark:
|
||||||
// Asset.Colors.Label.primary.color
|
// // 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)
|
// 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
|
// // 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)
|
// button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0)
|
||||||
default:
|
// default:
|
||||||
// Asset.Colors.Label.primary.color
|
// // 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)
|
// 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
|
// // 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.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
button.backgroundColor = .clear
|
// button.backgroundColor = .clear
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private var isBannerPinned = false
|
private var isBannerPinned = false
|
||||||
private var bottomShadowAlpha: CGFloat = 0.0
|
private var bottomShadowAlpha: CGFloat = 0.0
|
||||||
|
@ -113,7 +114,7 @@ extension ProfileHeaderViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
customizeButtonBarAppearance()
|
// customizeButtonBarAppearance()
|
||||||
|
|
||||||
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
|
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
|
||||||
ThemeService.shared.currentTheme
|
ThemeService.shared.currentTheme
|
||||||
|
@ -130,6 +131,7 @@ extension ProfileHeaderViewController {
|
||||||
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
|
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: profileHeaderView.bottomAnchor),
|
||||||
])
|
])
|
||||||
profileHeaderView.preservesSuperviewLayoutMargins = true
|
profileHeaderView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
|
@ -262,14 +264,19 @@ extension ProfileHeaderViewController {
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view)
|
switch UIApplication.shared.applicationState {
|
||||||
setupBottomShadow()
|
case .active:
|
||||||
|
headerDelegate?.viewLayoutDidUpdate(self)
|
||||||
|
setupBottomShadow()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
customizeButtonBarAppearance()
|
// customizeButtonBarAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -338,63 +345,63 @@ extension ProfileHeaderViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
|
// 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)
|
// // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
|
||||||
updateHeaderBottomShadow(progress: progress)
|
// updateHeaderBottomShadow(progress: progress)
|
||||||
|
//
|
||||||
let bannerImageView = profileHeaderView.bannerImageView
|
// let bannerImageView = profileHeaderView.bannerImageView
|
||||||
guard bannerImageView.bounds != .zero else {
|
// guard bannerImageView.bounds != .zero else {
|
||||||
// wait layout finish
|
// // wait layout finish
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
|
// let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
|
||||||
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
|
// let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
|
||||||
|
//
|
||||||
// scroll from bottom to top: 1 -> 2 -> 3
|
// // scroll from bottom to top: 1 -> 2 -> 3
|
||||||
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
|
// if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
|
||||||
// 1
|
// // 1
|
||||||
// banner top pin to window top and expand
|
// // banner top pin to window top and expand
|
||||||
bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
|
// bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
|
||||||
bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
|
// bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
|
||||||
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
|
// } else if bannerContainerBottomOffset < containerSafeAreaInset.top {
|
||||||
// 3
|
// // 3
|
||||||
// banner bottom pin to navigation bar bottom and
|
// // banner bottom pin to navigation bar bottom and
|
||||||
// the `progress` growth to 1 then segmented control pin to top
|
// // the `progress` growth to 1 then segmented control pin to top
|
||||||
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
// bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||||
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
// let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
||||||
bannerImageView.frame.size.height = bannerImageHeight
|
// bannerImageView.frame.size.height = bannerImageHeight
|
||||||
} else {
|
// } else {
|
||||||
// 2
|
// // 2
|
||||||
// banner move with scrolling from bottom to top until the
|
// // banner move with scrolling from bottom to top until the
|
||||||
// banner bottom higher than navigation bar bottom
|
// // banner bottom higher than navigation bar bottom
|
||||||
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
// bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||||
bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
|
// bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// set title view offset
|
// // set title view offset
|
||||||
let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
|
// let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
|
||||||
let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
|
// let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
|
||||||
let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
|
// let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
|
||||||
let transformY = max(0, titleViewContentOffset)
|
// let transformY = max(0, titleViewContentOffset)
|
||||||
titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
|
// titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
|
||||||
viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height
|
// viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height
|
||||||
|
//
|
||||||
if viewModel.viewDidAppear.value {
|
// if viewModel.viewDidAppear.value {
|
||||||
viewModel.isTitleViewContentOffsetSet.value = true
|
// viewModel.isTitleViewContentOffsetSet.value = true
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// set avatar fade
|
// // set avatar fade
|
||||||
if progress > 0 {
|
// if progress > 0 {
|
||||||
setProfileAvatar(alpha: 0)
|
// setProfileAvatar(alpha: 0)
|
||||||
} else if progress > -abs(throttle) {
|
// } else if progress > -abs(throttle) {
|
||||||
// y = -(1/0.8T)x
|
// // y = -(1/0.8T)x
|
||||||
let alpha = -1 / abs(0.8 * throttle) * progress
|
// let alpha = -1 / abs(0.8 * throttle) * progress
|
||||||
setProfileAvatar(alpha: alpha)
|
// setProfileAvatar(alpha: alpha)
|
||||||
} else {
|
// } else {
|
||||||
setProfileAvatar(alpha: 1)
|
// setProfileAvatar(alpha: 1)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private func setProfileAvatar(alpha: CGFloat) {
|
private func setProfileAvatar(alpha: CGFloat) {
|
||||||
profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
|
profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
|
||||||
|
@ -488,3 +495,6 @@ extension ProfileHeaderViewController: CropViewControllerDelegate {
|
||||||
cropViewController.dismiss(animated: true, completion: nil)
|
cropViewController.dismiss(animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - TabBarPagerHeader
|
||||||
|
extension ProfileHeaderViewController: TabBarPagerHeader { }
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -27,149 +27,181 @@ class ProfileViewModel: NSObject {
|
||||||
private var mastodonUserObserver: AnyCancellable?
|
private var mastodonUserObserver: AnyCancellable?
|
||||||
private var currentMastodonUserObserver: AnyCancellable?
|
private var currentMastodonUserObserver: AnyCancellable?
|
||||||
|
|
||||||
|
let postsUserTimelineViewModel: UserTimelineViewModel
|
||||||
|
let repliesUserTimelineViewModel: UserTimelineViewModel
|
||||||
|
let mediaUserTimelineViewModel: UserTimelineViewModel
|
||||||
|
let profileAboutViewModel: ProfileAboutViewModel
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
@Published var me: MastodonUser?
|
@Published var me: MastodonUser?
|
||||||
@Published var user: MastodonUser?
|
@Published var user: MastodonUser?
|
||||||
|
|
||||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
@Published var isEditing = false
|
||||||
|
@Published var isUpdating = false
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let domain: CurrentValueSubject<String?, Never>
|
@Published var userIdentifier: UserIdentifier? = nil
|
||||||
let userID: CurrentValueSubject<UserID?, Never>
|
|
||||||
let bannerImageURL: CurrentValueSubject<URL?, Never>
|
// let domain: CurrentValueSubject<String?, Never>
|
||||||
let avatarImageURL: CurrentValueSubject<URL?, Never>
|
// let userID: CurrentValueSubject<UserID?, Never>
|
||||||
let name: CurrentValueSubject<String?, Never>
|
// let bannerImageURL: CurrentValueSubject<URL?, Never>
|
||||||
let username: CurrentValueSubject<String?, Never>
|
// let avatarImageURL: CurrentValueSubject<URL?, Never>
|
||||||
let bioDescription: CurrentValueSubject<String?, Never>
|
// let name: CurrentValueSubject<String?, Never>
|
||||||
let url: CurrentValueSubject<String?, Never>
|
// let username: CurrentValueSubject<String?, Never>
|
||||||
let statusesCount: CurrentValueSubject<Int?, Never>
|
// let bioDescription: CurrentValueSubject<String?, Never>
|
||||||
let followingCount: CurrentValueSubject<Int?, Never>
|
// let url: CurrentValueSubject<String?, Never>
|
||||||
let followersCount: CurrentValueSubject<Int?, Never>
|
// let statusesCount: CurrentValueSubject<Int?, Never>
|
||||||
let fields: CurrentValueSubject<[MastodonField], Never>
|
// let followingCount: CurrentValueSubject<Int?, Never>
|
||||||
let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
|
// let followersCount: CurrentValueSubject<Int?, Never>
|
||||||
|
// let fields: CurrentValueSubject<[MastodonField], Never>
|
||||||
|
// let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
|
||||||
|
|
||||||
// fulfill this before editing
|
// fulfill this before editing
|
||||||
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
|
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
|
||||||
|
|
||||||
let protected: CurrentValueSubject<Bool?, Never>
|
// let protected: CurrentValueSubject<Bool?, Never>
|
||||||
let suspended: 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 relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none)
|
// let isMuting = CurrentValueSubject<Bool, Never>(false)
|
||||||
let isFollowedBy = CurrentValueSubject<Bool, Never>(false)
|
// let isBlocking = CurrentValueSubject<Bool, Never>(false)
|
||||||
let isMuting = CurrentValueSubject<Bool, Never>(false)
|
// let isBlockedBy = 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 isRelationshipActionButtonHidden = CurrentValueSubject<Bool, Never>(true)
|
// let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
// let isMeBarButtonItemsHidden = 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 needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
// let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
|
||||||
let needsPagingEnabled = CurrentValueSubject<Bool, Never>(true)
|
|
||||||
let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
|
|
||||||
|
|
||||||
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.user = mastodonUser
|
self.user = mastodonUser
|
||||||
self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
|
// self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
|
||||||
self.userID = CurrentValueSubject(mastodonUser?.id)
|
// self.userID = CurrentValueSubject(mastodonUser?.id)
|
||||||
self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
|
// self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
|
||||||
self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
|
// self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
|
||||||
self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
|
// self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
|
||||||
self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
|
// self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
|
||||||
self.bioDescription = CurrentValueSubject(mastodonUser?.note)
|
// self.bioDescription = CurrentValueSubject(mastodonUser?.note)
|
||||||
self.url = CurrentValueSubject(mastodonUser?.url)
|
// self.url = CurrentValueSubject(mastodonUser?.url)
|
||||||
self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) })
|
// self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) })
|
||||||
self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) })
|
// self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) })
|
||||||
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) })
|
// self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) })
|
||||||
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
// self.protected = CurrentValueSubject(mastodonUser?.locked)
|
||||||
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
// self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
||||||
self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
|
// self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
|
||||||
self.emojiMeta = CurrentValueSubject(mastodonUser?.emojis.asDictionary ?? [:])
|
// 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()
|
super.init()
|
||||||
|
|
||||||
relationshipActionOptionSet
|
// bind me
|
||||||
.compactMap { $0.highPriorityAction(except: []) }
|
|
||||||
.map { $0 == .none }
|
|
||||||
.assign(to: \.value, on: isRelationshipActionButtonHidden)
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
// bind active authentication
|
|
||||||
context.authenticationService.activeMastodonAuthenticationBox
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] authenticationBox in
|
.sink { [weak self] authenticationBox in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let authenticationBox = authenticationBox else {
|
self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||||
self.domain.value = nil
|
|
||||||
self.me = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.domain.value = authenticationBox.domain
|
|
||||||
self.me = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// query relationship
|
// bind user
|
||||||
let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
|
$user
|
||||||
user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
|
.map { user -> UserIdentifier? in
|
||||||
}
|
guard let user = user else { return nil }
|
||||||
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
|
return MastodonUserIdentifier(domain: user.domain, userID: user.id)
|
||||||
|
|
||||||
// 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)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
.assign(to: &$userIdentifier)
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
$userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
|
||||||
let isBlockingOrBlocked = Publishers.CombineLatest(
|
$userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
|
||||||
isBlocking,
|
$userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
|
||||||
isBlockedBy
|
// $userIdentifier.assign(to: &profileAboutViewModel.$userIdentifier)
|
||||||
)
|
|
||||||
.map { $0 || $1 }
|
// relationshipActionOptionSet
|
||||||
.share()
|
// .compactMap { $0.highPriorityAction(except: []) }
|
||||||
|
// .map { $0 == .none }
|
||||||
|
// .assign(to: \.value, on: isRelationshipActionButtonHidden)
|
||||||
|
// .store(in: &disposeBag)
|
||||||
|
//
|
||||||
|
|
||||||
isBlockingOrBlocked
|
//
|
||||||
.map { !$0 }
|
// // query relationship
|
||||||
.assign(to: \.value, on: needsPagingEnabled)
|
// let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
|
||||||
.store(in: &disposeBag)
|
// user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
|
||||||
|
// }
|
||||||
isBlockingOrBlocked
|
// let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
|
||||||
.map { $0 }
|
//
|
||||||
.assign(to: \.value, on: needsImageOverlayBlurred)
|
// // observe friendship
|
||||||
.store(in: &disposeBag)
|
// Publishers.CombineLatest3(
|
||||||
|
// userRecord,
|
||||||
setup()
|
// 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?) {
|
private func update(mastodonUser: MastodonUser?) {
|
||||||
self.userID.value = mastodonUser?.id
|
// self.userID.value = mastodonUser?.id
|
||||||
self.bannerImageURL.value = mastodonUser?.headerImageURL()
|
// self.bannerImageURL.value = mastodonUser?.headerImageURL()
|
||||||
self.avatarImageURL.value = mastodonUser?.avatarImageURL()
|
// self.avatarImageURL.value = mastodonUser?.avatarImageURL()
|
||||||
self.name.value = mastodonUser?.displayNameWithFallback
|
// self.name.value = mastodonUser?.displayNameWithFallback
|
||||||
self.username.value = mastodonUser?.acctWithDomain
|
// self.username.value = mastodonUser?.acctWithDomain
|
||||||
self.bioDescription.value = mastodonUser?.note
|
// self.bioDescription.value = mastodonUser?.note
|
||||||
self.url.value = mastodonUser?.url
|
// self.url.value = mastodonUser?.url
|
||||||
self.statusesCount.value = mastodonUser.flatMap { Int($0.statusesCount) }
|
// self.statusesCount.value = mastodonUser.flatMap { Int($0.statusesCount) }
|
||||||
self.followingCount.value = mastodonUser.flatMap { Int($0.followingCount) }
|
// self.followingCount.value = mastodonUser.flatMap { Int($0.followingCount) }
|
||||||
self.followersCount.value = mastodonUser.flatMap { Int($0.followersCount) }
|
// self.followersCount.value = mastodonUser.flatMap { Int($0.followersCount) }
|
||||||
self.protected.value = mastodonUser?.locked
|
// self.protected.value = mastodonUser?.locked
|
||||||
self.suspended.value = mastodonUser?.suspended ?? false
|
// self.suspended.value = mastodonUser?.suspended ?? false
|
||||||
self.fields.value = mastodonUser?.fields ?? []
|
// self.fields.value = mastodonUser?.fields ?? []
|
||||||
self.emojiMeta.value = mastodonUser?.emojis.asDictionary ?? [:]
|
// self.emojiMeta.value = mastodonUser?.emojis.asDictionary ?? [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
|
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
|
||||||
guard let mastodonUser = mastodonUser,
|
// guard let mastodonUser = mastodonUser,
|
||||||
let currentMastodonUser = currentMastodonUser else {
|
// let currentMastodonUser = currentMastodonUser else {
|
||||||
// set relationship
|
// // set relationship
|
||||||
self.relationshipActionOptionSet.value = .none
|
// self.relationshipActionOptionSet.value = .none
|
||||||
self.isFollowedBy.value = false
|
// self.isFollowedBy.value = false
|
||||||
self.isMuting.value = false
|
// self.isMuting.value = false
|
||||||
self.isBlocking.value = false
|
// self.isBlocking.value = false
|
||||||
self.isBlockedBy.value = false
|
// self.isBlockedBy.value = false
|
||||||
|
//
|
||||||
// set bar button item state
|
// // set bar button item state
|
||||||
self.isReplyBarButtonItemHidden.value = true
|
// self.isReplyBarButtonItemHidden.value = true
|
||||||
self.isMoreMenuBarButtonItemHidden.value = true
|
// self.isMoreMenuBarButtonItemHidden.value = true
|
||||||
self.isMeBarButtonItemsHidden.value = true
|
// self.isMeBarButtonItemsHidden.value = true
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if mastodonUser == currentMastodonUser {
|
// if mastodonUser == currentMastodonUser {
|
||||||
self.relationshipActionOptionSet.value = [.edit]
|
// self.relationshipActionOptionSet.value = [.edit]
|
||||||
// set bar button item state
|
// // set bar button item state
|
||||||
self.isReplyBarButtonItemHidden.value = true
|
// self.isReplyBarButtonItemHidden.value = true
|
||||||
self.isMoreMenuBarButtonItemHidden.value = true
|
// self.isMoreMenuBarButtonItemHidden.value = true
|
||||||
self.isMeBarButtonItemsHidden.value = false
|
// self.isMeBarButtonItemsHidden.value = false
|
||||||
} else {
|
// } else {
|
||||||
// set with follow action default
|
// // set with follow action default
|
||||||
var relationshipActionSet = RelationshipActionOptionSet([.follow])
|
// var relationshipActionSet = RelationshipActionOptionSet([.follow])
|
||||||
|
//
|
||||||
if mastodonUser.locked {
|
// if mastodonUser.locked {
|
||||||
relationshipActionSet.insert(.request)
|
// relationshipActionSet.insert(.request)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if mastodonUser.suspended {
|
// if mastodonUser.suspended {
|
||||||
relationshipActionSet.insert(.suspended)
|
// relationshipActionSet.insert(.suspended)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
let isFollowing = mastodonUser.followingBy.contains(currentMastodonUser)
|
// let isFollowing = mastodonUser.followingBy.contains(currentMastodonUser)
|
||||||
if isFollowing {
|
// if isFollowing {
|
||||||
relationshipActionSet.insert(.following)
|
// 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)
|
// 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)
|
// let isPending = mastodonUser.followRequestedBy.contains(currentMastodonUser)
|
||||||
if isPending {
|
// if isPending {
|
||||||
relationshipActionSet.insert(.pending)
|
// 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)
|
// 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)
|
// let isFollowedBy = currentMastodonUser.followingBy.contains(mastodonUser)
|
||||||
self.isFollowedBy.value = isFollowedBy
|
// 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)
|
// 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)
|
// let isMuting = mastodonUser.mutingBy.contains(currentMastodonUser)
|
||||||
if isMuting {
|
// if isMuting {
|
||||||
relationshipActionSet.insert(.muting)
|
// relationshipActionSet.insert(.muting)
|
||||||
}
|
// }
|
||||||
self.isMuting.value = isMuting
|
// 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)
|
// 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)
|
// let isBlocking = mastodonUser.blockingBy.contains(currentMastodonUser)
|
||||||
if isBlocking {
|
// if isBlocking {
|
||||||
relationshipActionSet.insert(.blocking)
|
// relationshipActionSet.insert(.blocking)
|
||||||
}
|
// }
|
||||||
self.isBlocking.value = isBlocking
|
// 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)
|
// 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)
|
// let isBlockedBy = currentMastodonUser.blockingBy.contains(mastodonUser)
|
||||||
if isBlockedBy {
|
// if isBlockedBy {
|
||||||
relationshipActionSet.insert(.blocked)
|
// relationshipActionSet.insert(.blocked)
|
||||||
}
|
// }
|
||||||
self.isBlockedBy.value = isBlockedBy
|
// 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)
|
// 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
|
// self.relationshipActionOptionSet.value = relationshipActionSet
|
||||||
|
//
|
||||||
// set bar button item state
|
// // set bar button item state
|
||||||
self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy
|
// self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy
|
||||||
self.isMoreMenuBarButtonItemHidden.value = false
|
// self.isMoreMenuBarButtonItemHidden.value = false
|
||||||
self.isMeBarButtonItemsHidden.value = true
|
// self.isMeBarButtonItemsHidden.value = true
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,40 +7,42 @@
|
||||||
|
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pageboy
|
import XLPagerTabStrip
|
||||||
import Tabman
|
import TabBarPager
|
||||||
|
|
||||||
protocol ProfilePagingViewControllerDelegate: AnyObject {
|
protocol ProfilePagingViewControllerDelegate: AnyObject {
|
||||||
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int)
|
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?
|
weak var pagingDelegate: ProfilePagingViewControllerDelegate?
|
||||||
|
|
||||||
var viewModel: ProfilePagingViewModel!
|
var viewModel: ProfilePagingViewModel!
|
||||||
|
|
||||||
|
// MARK: - TabBarPageViewController
|
||||||
|
|
||||||
// MARK: - PageboyViewControllerDelegate
|
var currentPage: TabBarPage? {
|
||||||
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didCancelScrollToPageAt index: PageboyViewController.PageIndex, returnToPageAt previousIndex: PageboyViewController.PageIndex) {
|
return viewModel.viewControllers[currentIndex]
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) {
|
var currentPageIndex: Int? {
|
||||||
super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated)
|
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]
|
guard indexWasChanged else { return }
|
||||||
(viewController as? StatusTableViewControllerNavigateable)?.overrideNavigationScrollPosition = .top
|
let page = viewModel.viewControllers[toIndex]
|
||||||
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
|
tabBarPageViewDelegate?.pageViewController(self, didPresentingTabBarPage: page, at: toIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make key commands works
|
// make key commands works
|
||||||
|
@ -49,7 +51,7 @@ final class ProfilePagingViewController: TabmanViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
view.backgroundColor = .clear
|
// view.backgroundColor = .clear
|
||||||
dataSource = viewModel
|
// dataSource = viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -74,17 +76,17 @@ extension ProfilePagingViewController {
|
||||||
// workaround to fix tab man responder chain issue
|
// workaround to fix tab man responder chain issue
|
||||||
extension ProfilePagingViewController {
|
extension ProfilePagingViewController {
|
||||||
|
|
||||||
override var keyCommands: [UIKeyCommand]? {
|
// override var keyCommands: [UIKeyCommand]? {
|
||||||
return currentViewController?.keyCommands
|
// return currentPage?.keyCommands
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
// @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
(currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
|
// (currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
// @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
|
||||||
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
|
// (currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,9 @@
|
||||||
|
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pageboy
|
|
||||||
import Tabman
|
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
import TabBarPager
|
||||||
|
|
||||||
final class ProfilePagingViewModel: NSObject {
|
final class ProfilePagingViewModel: NSObject {
|
||||||
|
|
||||||
|
@ -32,7 +31,7 @@ final class ProfilePagingViewModel: NSObject {
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewControllers: [ScrollViewContainer] {
|
var viewControllers: [UIViewController & TabBarPage] {
|
||||||
return [
|
return [
|
||||||
postUserTimelineViewController,
|
postUserTimelineViewController,
|
||||||
repliesUserTimelineViewController,
|
repliesUserTimelineViewController,
|
||||||
|
@ -41,42 +40,42 @@ final class ProfilePagingViewModel: NSObject {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
let barItems: [TMBarItemable] = {
|
// let barItems: [TMBarItemable] = {
|
||||||
let items = [
|
// let items = [
|
||||||
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
|
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
|
||||||
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.postsAndReplies),
|
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.postsAndReplies),
|
||||||
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
|
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
|
||||||
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.about),
|
// TMBarItem(title: L10n.Scene.Profile.SegmentedControl.about),
|
||||||
]
|
// ]
|
||||||
return items
|
// return items
|
||||||
}()
|
// }()
|
||||||
|
|
||||||
deinit {
|
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
|
//// MARK: - PageboyViewControllerDataSource
|
||||||
extension ProfilePagingViewModel: PageboyViewControllerDataSource {
|
//extension ProfilePagingViewModel: PageboyViewControllerDataSource {
|
||||||
|
//
|
||||||
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
// func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
|
||||||
return viewControllers.count
|
// return viewControllers.count
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
// func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
|
||||||
return viewControllers[index]
|
// return viewControllers[index]
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
|
// func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
|
||||||
return .first
|
// return .first
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
// MARK: - TMBarDataSource
|
//// MARK: - TMBarDataSource
|
||||||
extension ProfilePagingViewModel: TMBarDataSource {
|
//extension ProfilePagingViewModel: TMBarDataSource {
|
||||||
func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
|
// func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
|
||||||
return barItems[index]
|
// return barItems[index]
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import AVKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
|
import TabBarPager
|
||||||
|
import XLPagerTabStrip
|
||||||
|
|
||||||
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||||
|
|
||||||
|
@ -143,7 +145,14 @@ extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableView
|
||||||
|
|
||||||
// MARK: - CustomScrollViewContainerController
|
// MARK: - CustomScrollViewContainerController
|
||||||
extension UserTimelineViewController: ScrollViewContainer {
|
extension UserTimelineViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? { return tableView }
|
var scrollView: UIScrollView { return tableView }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TabBarPage
|
||||||
|
extension UserTimelineViewController: TabBarPage {
|
||||||
|
var pageScrollView: UIScrollView {
|
||||||
|
scrollView
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - StatusTableViewCellDelegate
|
// MARK: - StatusTableViewCellDelegate
|
||||||
|
@ -165,3 +174,10 @@ extension UserTimelineViewController: StatusTableViewControllerNavigateable {
|
||||||
statusKeyCommandHandler(sender)
|
statusKeyCommandHandler(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - IndicatorInfoProvider
|
||||||
|
extension UserTimelineViewController: IndicatorInfoProvider {
|
||||||
|
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
|
||||||
|
return IndicatorInfo(title: "Hello")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -30,17 +30,14 @@ extension UserTimelineViewModel {
|
||||||
snapshot.appendSections([.main])
|
snapshot.appendSections([.main])
|
||||||
diffableDataSource?.apply(snapshot)
|
diffableDataSource?.apply(snapshot)
|
||||||
|
|
||||||
// trigger user timeline loading
|
// trigger timeline reloading
|
||||||
Publishers.CombineLatest(
|
$userIdentifier
|
||||||
$domain.removeDuplicates(),
|
.receive(on: DispatchQueue.main)
|
||||||
$userID.removeDuplicates()
|
.sink { [weak self] _ in
|
||||||
)
|
guard let self = self else { return }
|
||||||
.receive(on: DispatchQueue.main)
|
self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
|
||||||
.sink { [weak self] _ in
|
}
|
||||||
guard let self = self else { return }
|
.store(in: &disposeBag)
|
||||||
self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
let needsTimelineHidden = Publishers.CombineLatest3(
|
let needsTimelineHidden = Publishers.CombineLatest3(
|
||||||
isBlocking,
|
isBlocking,
|
||||||
|
|
|
@ -50,7 +50,7 @@ extension UserTimelineViewModel.State {
|
||||||
guard let viewModel = viewModel else { return false }
|
guard let viewModel = viewModel else { return false }
|
||||||
switch stateClass {
|
switch stateClass {
|
||||||
case is Reloading.Type:
|
case is Reloading.Type:
|
||||||
return viewModel.userID != nil
|
return viewModel.userIdentifier != nil
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ extension UserTimelineViewModel.State {
|
||||||
|
|
||||||
let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last
|
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)
|
stateMachine.enter(Fail.self)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,7 @@ final class UserTimelineViewModel {
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
@Published var domain: String?
|
@Published var userIdentifier: UserIdentifier?
|
||||||
@Published var userID: String?
|
|
||||||
@Published var queryFilter: QueryFilter
|
@Published var queryFilter: QueryFilter
|
||||||
let statusFetchedResultsController: StatusFetchedResultsController
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||||
|
@ -48,30 +47,25 @@ final class UserTimelineViewModel {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
domain: String?,
|
|
||||||
userID: String?,
|
|
||||||
queryFilter: QueryFilter
|
queryFilter: QueryFilter
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.statusFetchedResultsController = StatusFetchedResultsController(
|
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||||
managedObjectContext: context.managedObjectContext,
|
managedObjectContext: context.managedObjectContext,
|
||||||
domain: domain,
|
domain: nil,
|
||||||
additionalTweetPredicate: Status.notDeleted()
|
additionalTweetPredicate: nil
|
||||||
)
|
)
|
||||||
self.domain = domain
|
|
||||||
self.userID = userID
|
|
||||||
self.queryFilter = queryFilter
|
self.queryFilter = queryFilter
|
||||||
// super.init()
|
// super.init()
|
||||||
|
|
||||||
$domain
|
context.authenticationService.activeMastodonAuthenticationBox
|
||||||
|
.map { $0?.domain }
|
||||||
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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
|
self.onlyMedia = onlyMedia
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
|
||||||
}
|
|
1
Podfile
1
Podfile
|
@ -8,6 +8,7 @@ target 'Mastodon' do
|
||||||
|
|
||||||
# UI
|
# UI
|
||||||
pod 'UITextField+Shake', '~> 1.2'
|
pod 'UITextField+Shake', '~> 1.2'
|
||||||
|
pod 'XLPagerTabStrip', '~> 9.0.0'
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
pod 'SwiftGen', '~> 6.4.0'
|
pod 'SwiftGen', '~> 6.4.0'
|
||||||
|
|
|
@ -8,6 +8,7 @@ PODS:
|
||||||
- Sourcery/CLI-Only (1.6.1)
|
- Sourcery/CLI-Only (1.6.1)
|
||||||
- SwiftGen (6.4.0)
|
- SwiftGen (6.4.0)
|
||||||
- "UITextField+Shake (1.2.1)"
|
- "UITextField+Shake (1.2.1)"
|
||||||
|
- XLPagerTabStrip (9.0.0)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- DateToolsSwift (~> 5.0.0)
|
- DateToolsSwift (~> 5.0.0)
|
||||||
|
@ -17,6 +18,7 @@ DEPENDENCIES:
|
||||||
- Sourcery (~> 1.6.1)
|
- Sourcery (~> 1.6.1)
|
||||||
- SwiftGen (~> 6.4.0)
|
- SwiftGen (~> 6.4.0)
|
||||||
- "UITextField+Shake (~> 1.2)"
|
- "UITextField+Shake (~> 1.2)"
|
||||||
|
- XLPagerTabStrip (~> 9.0.0)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
|
@ -26,6 +28,7 @@ SPEC REPOS:
|
||||||
- Sourcery
|
- Sourcery
|
||||||
- SwiftGen
|
- SwiftGen
|
||||||
- "UITextField+Shake"
|
- "UITextField+Shake"
|
||||||
|
- XLPagerTabStrip
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Keys:
|
Keys:
|
||||||
|
@ -39,7 +42,8 @@ SPEC CHECKSUMS:
|
||||||
Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac
|
Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac
|
||||||
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
||||||
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
||||||
|
XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5
|
||||||
|
|
||||||
PODFILE CHECKSUM: 335d0ca70493d4c280d0f8fd7f26fe9be6a4e289
|
PODFILE CHECKSUM: 1ac960a2c981ef98f7c24a3bba57bdabc1f66103
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.11.3
|
||||||
|
|
Loading…
Reference in New Issue