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)
|
||||
- [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)
|
||||
|
|
|
@ -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" */;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,9 +148,7 @@ extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryCommunityViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension DiscoveryCommunityViewController {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -168,8 +168,6 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryForYouViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
|
|
|
@ -127,9 +127,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryHashtagsViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension DiscoveryHashtagsViewController {
|
||||
|
|
|
@ -127,9 +127,7 @@ extension DiscoveryNewsViewController: UITableViewDelegate {
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryNewsViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension DiscoveryNewsViewController {
|
||||
|
|
|
@ -160,9 +160,7 @@ extension DiscoveryPostsViewController: StatusTableViewCellDelegate { }
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryPostsViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
// MARK: - DiscoveryIntroBannerViewDelegate
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -203,9 +203,7 @@ extension NotificationTimelineViewController: NotificationTableViewCellDelegate
|
|||
|
||||
// MARK: - ScrollViewContainer
|
||||
extension NotificationTimelineViewController: ScrollViewContainer {
|
||||
|
||||
var scrollView: UIScrollView? { tableView }
|
||||
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewController {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
pod 'UITextField+Shake', '~> 1.2'
|
||||
pod 'XLPagerTabStrip', '~> 9.0.0'
|
||||
|
||||
# misc
|
||||
pod 'SwiftGen', '~> 6.4.0'
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue