feat: add follower list for user
This commit is contained in:
parent
d66dfccad0
commit
f0a570ea0c
|
@ -333,6 +333,17 @@
|
|||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
|
||||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
|
||||
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */; };
|
||||
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */; };
|
||||
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */; };
|
||||
DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */; };
|
||||
DB6B74F8272FBFB100C70B6E /* FollowerListViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F7272FBFB100C70B6E /* FollowerListViewController+Provider.swift */; };
|
||||
DB6B74FA272FC2B500C70B6E /* APIService+Follower.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */; };
|
||||
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FB272FF55800C70B6E /* UserSection.swift */; };
|
||||
DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FD272FF59000C70B6E /* UserItem.swift */; };
|
||||
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; };
|
||||
DB6B75022730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B75012730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift */; };
|
||||
DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */; };
|
||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; };
|
||||
DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; };
|
||||
|
@ -1132,6 +1143,17 @@
|
|||
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
|
||||
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewController.swift; sourceTree = "<group>"; };
|
||||
DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewModel.swift; sourceTree = "<group>"; };
|
||||
DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
DB6B74F7272FBFB100C70B6E /* FollowerListViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewController+Provider.swift"; sourceTree = "<group>"; };
|
||||
DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follower.swift"; sourceTree = "<group>"; };
|
||||
DB6B74FB272FF55800C70B6E /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = "<group>"; };
|
||||
DB6B74FD272FF59000C70B6E /* UserItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = "<group>"; };
|
||||
DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6B75012730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProviderFacade+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
||||
DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||
DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreference.swift; sourceTree = "<group>"; };
|
||||
DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1866,6 +1888,7 @@
|
|||
2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */,
|
||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
||||
DB6B74FB272FF55800C70B6E /* UserSection.swift */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1909,8 +1932,10 @@
|
|||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
|
||||
DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */,
|
||||
DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */,
|
||||
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */,
|
||||
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */,
|
||||
DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */,
|
||||
);
|
||||
path = TableviewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1919,6 +1944,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7631B225C159F700929FB9 /* Item.swift */,
|
||||
DB6B74FD272FF59000C70B6E /* UserItem.swift */,
|
||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */,
|
||||
DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */,
|
||||
2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */,
|
||||
|
@ -2289,6 +2315,7 @@
|
|||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
||||
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */,
|
||||
DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */,
|
||||
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
|
||||
5B24BBE1262DB19100A9381B /* APIService+Report.swift */,
|
||||
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
||||
|
@ -2508,6 +2535,18 @@
|
|||
path = NavigationController;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB6B74F0272FB55400C70B6E /* Follower */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */,
|
||||
DB6B74F7272FBFB100C70B6E /* FollowerListViewController+Provider.swift */,
|
||||
DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */,
|
||||
DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */,
|
||||
DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */,
|
||||
);
|
||||
path = Follower;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB6C8C0525F0921200AAA452 /* MastodonSDK */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2862,6 +2901,7 @@
|
|||
DBB525462611ED57002F1F29 /* Header */,
|
||||
DBB5253B2611ECF5002F1F29 /* Timeline */,
|
||||
DBE3CDF1261C6B3100430CC6 /* Favorite */,
|
||||
DB6B74F0272FB55400C70B6E /* Follower */,
|
||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
|
||||
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */,
|
||||
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
|
||||
|
@ -2982,6 +3022,7 @@
|
|||
children = (
|
||||
DBAE3F672615DD60004B8251 /* UserProvider.swift */,
|
||||
DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */,
|
||||
DB6B75012730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift */,
|
||||
);
|
||||
path = UserProvider;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3965,6 +4006,7 @@
|
|||
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */,
|
||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */,
|
||||
DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */,
|
||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
||||
DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */,
|
||||
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
|
||||
|
@ -4094,6 +4136,9 @@
|
|||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
|
||||
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
||||
DB6B74FA272FC2B500C70B6E /* APIService+Follower.swift in Sources */,
|
||||
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */,
|
||||
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */,
|
||||
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
|
||||
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */,
|
||||
|
@ -4106,6 +4151,7 @@
|
|||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||
DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */,
|
||||
DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */,
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||
DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */,
|
||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||
|
@ -4122,6 +4168,7 @@
|
|||
DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */,
|
||||
DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */,
|
||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||
DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */,
|
||||
DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */,
|
||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||
DB9F58EF26EF491E00E7BBE9 /* AccountListViewModel.swift in Sources */,
|
||||
|
@ -4139,6 +4186,7 @@
|
|||
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */,
|
||||
DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */,
|
||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
|
||||
5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */,
|
||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||
DBBC24C426A544B900398BB9 /* Theme.swift in Sources */,
|
||||
|
@ -4180,6 +4228,7 @@
|
|||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
DB6B75022730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift in Sources */,
|
||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
||||
2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */,
|
||||
DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */,
|
||||
|
@ -4245,6 +4294,7 @@
|
|||
DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */,
|
||||
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
|
||||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
||||
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
|
||||
5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */,
|
||||
|
@ -4261,6 +4311,7 @@
|
|||
0F202227261411BB000C64BF /* HashtagTimelineViewController+Provider.swift in Sources */,
|
||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */,
|
||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
|
||||
|
@ -4291,6 +4342,7 @@
|
|||
DB647C5726F1E97300F7F82C /* MainTabBarController+Wizard.swift in Sources */,
|
||||
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */,
|
||||
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
|
||||
DB6B74F8272FBFB100C70B6E /* FollowerListViewController+Provider.swift in Sources */,
|
||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||
DB4932B326F2054200EF46D4 /* CircleAvatarButton.swift in Sources */,
|
||||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>43</integer>
|
||||
<integer>35</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>42</integer>
|
||||
<integer>39</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -97,7 +97,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>41</integer>
|
||||
<integer>36</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>44</integer>
|
||||
<integer>37</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -178,6 +178,7 @@ extension SceneCoordinator {
|
|||
case accountList
|
||||
case profile(viewModel: ProfileViewModel)
|
||||
case favorite(viewModel: FavoriteViewModel)
|
||||
case follower(viewModel: FollowerListViewModel)
|
||||
|
||||
// setting
|
||||
case settings(viewModel: SettingsViewModel)
|
||||
|
@ -424,6 +425,10 @@ private extension SceneCoordinator {
|
|||
let _viewController = FavoriteViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .follower(let viewModel):
|
||||
let _viewController = FollowerListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .suggestionAccount(let viewModel):
|
||||
let _viewController = SuggestionAccountViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// UserItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
enum UserItem: Hashable {
|
||||
case follower(objectID: NSManagedObjectID)
|
||||
case bottomLoader
|
||||
case bottomHeader(text: String)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// UserSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
|
||||
enum UserSection: Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension UserSection {
|
||||
|
||||
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
) -> UITableViewDiffableDataSource<UserSection, UserItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak dependency
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let dependency = dependency else { return UITableViewCell() }
|
||||
switch item {
|
||||
case .follower(let objectID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
let user = managedObjectContext.object(with: objectID) as! MastodonUser
|
||||
configure(cell: cell, user: user)
|
||||
}
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.startAnimating()
|
||||
return cell
|
||||
case .bottomHeader(let text):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell
|
||||
cell.messageLabel.text = text
|
||||
return cell
|
||||
} // end switch
|
||||
} // end UITableViewDiffableDataSource
|
||||
} // end static func tableViewDiffableDataSource { … }
|
||||
|
||||
}
|
||||
|
||||
extension UserSection {
|
||||
|
||||
static func configure(
|
||||
cell: UserTableViewCell,
|
||||
user: MastodonUser
|
||||
) {
|
||||
cell.configure(user: user)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// UserProviderFacade+UITableViewDelegate.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
extension UserTableViewCellDelegate where Self: UserProvider {
|
||||
|
||||
func handleTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let cell = tableView.cellForRow(at: indexPath) else { return }
|
||||
let user = self.mastodonUser(for: cell)
|
||||
UserProviderFacade.coordinatorToUserProfileScene(provider: self, user: user)
|
||||
}
|
||||
|
||||
}
|
|
@ -440,3 +440,25 @@ extension UserProviderFacade {
|
|||
return activityViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension UserProviderFacade {
|
||||
static func coordinatorToUserProfileScene(provider: UserProvider, user: Future<MastodonUser?, Never>) {
|
||||
user
|
||||
.sink { [weak provider] mastodonUser in
|
||||
guard let provider = provider else { return }
|
||||
guard let mastodonUser = mastodonUser else { return }
|
||||
let profileViewModel = CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
|
||||
DispatchQueue.main.async {
|
||||
if provider.navigationController == nil {
|
||||
let from = provider.presentingViewController ?? provider
|
||||
provider.dismiss(animated: true) {
|
||||
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
|
||||
}
|
||||
} else {
|
||||
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &provider.disposeBag)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// FollowerListViewController+Provider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
extension FollowerListViewController: UserProvider {
|
||||
|
||||
func mastodonUser() -> Future<MastodonUser?, Never> {
|
||||
Future { promise in
|
||||
promise(.success(nil))
|
||||
}
|
||||
}
|
||||
|
||||
func mastodonUser(for cell: UITableViewCell?) -> Future<MastodonUser?, Never> {
|
||||
Future { [weak self] promise in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let cell = cell,
|
||||
let indexPath = self.tableView.indexPath(for: cell),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
|
||||
let managedObjectContext = self.viewModel.userFetchedResultsController.fetchedResultsController.managedObjectContext
|
||||
|
||||
switch item {
|
||||
case .follower(let objectID):
|
||||
managedObjectContext.perform {
|
||||
let user = managedObjectContext.object(with: objectID) as? MastodonUser
|
||||
promise(.success(user))
|
||||
}
|
||||
case .bottomLoader, .bottomHeader:
|
||||
promise(.success(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// FollowerListViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import AVKit
|
||||
import GameplayKit
|
||||
import Combine
|
||||
|
||||
final class FollowerListViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: FollowerListViewModel!
|
||||
|
||||
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
return tableView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FollowerListViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] theme in
|
||||
guard let self = self else { return }
|
||||
self.view.backgroundColor = theme.secondarySystemBackgroundColor
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self
|
||||
)
|
||||
// TODO: add UserTableViewCellDelegate
|
||||
|
||||
// trigger user timeline loading
|
||||
Publishers.CombineLatest(
|
||||
viewModel.domain.removeDuplicates().eraseToAnyPublisher(),
|
||||
viewModel.userID.removeDuplicates().eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LoadMoreConfigurableTableViewContainer
|
||||
extension FollowerListViewController: LoadMoreConfigurableTableViewContainer {
|
||||
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
typealias LoadingState = FollowerListViewModel.State.Loading
|
||||
var loadMoreConfigurableTableView: UITableView { tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.stateMachine }
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
extension FollowerListViewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
handleScrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension FollowerListViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
handleTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UserTableViewCellDelegate
|
||||
extension FollowerListViewController: UserTableViewCellDelegate { }
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// FollowerListViewModel+Diffable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension FollowerListViewModel {
|
||||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency
|
||||
) {
|
||||
diffableDataSource = UserSection.tableViewDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: dependency,
|
||||
managedObjectContext: userFetchedResultsController.fetchedResultsController.managedObjectContext
|
||||
)
|
||||
|
||||
// set empty section to make update animation top-to-bottom style
|
||||
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
||||
snapshot.appendSections([.main])
|
||||
diffableDataSource?.apply(snapshot)
|
||||
|
||||
// workaround to append loader wrong animation issue
|
||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
diffableDataSource?.apply(snapshot)
|
||||
|
||||
userFetchedResultsController.objectIDs.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] objectIDs in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
||||
snapshot.appendSections([.main])
|
||||
let items: [UserItem] = objectIDs.map {
|
||||
UserItem.follower(objectID: $0)
|
||||
}
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
|
||||
if let currentState = self.stateMachine.currentState {
|
||||
switch currentState {
|
||||
case is State.Idle, is State.Loading, is State.Fail:
|
||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
case is State.NoMore:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
diffableDataSource.apply(snapshot)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
//
|
||||
// FollowerListViewModel+State.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
extension FollowerListViewModel {
|
||||
class State: GKState {
|
||||
weak var viewModel: FollowerListViewModel?
|
||||
|
||||
init(viewModel: FollowerListViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FollowerListViewModel.State {
|
||||
class Initial: FollowerListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
guard let viewModel = viewModel else { return false }
|
||||
switch stateClass {
|
||||
case is Reloading.Type:
|
||||
return viewModel.userID.value != nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Reloading: FollowerListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
// reset
|
||||
viewModel.userFetchedResultsController.userIDs.value = []
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
}
|
||||
|
||||
class Fail: FollowerListViewModel.State {
|
||||
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let _ = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Idle: FollowerListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Reloading.Type, is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Loading: FollowerListViewModel.State {
|
||||
|
||||
var maxID: String?
|
||||
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Fail.Type:
|
||||
return true
|
||||
case is Idle.Type:
|
||||
return true
|
||||
case is NoMore.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
if previousState is Reloading {
|
||||
maxID = nil
|
||||
}
|
||||
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
|
||||
guard let userID = viewModel.userID.value, !userID.isEmpty else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.context.apiService.followers(
|
||||
userID: userID,
|
||||
maxID: maxID,
|
||||
authorizationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch user timeline fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
stateMachine.enter(Fail.self)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
var hasNewAppend = false
|
||||
var userIDs = viewModel.userFetchedResultsController.userIDs.value
|
||||
for user in response.value {
|
||||
guard !userIDs.contains(user.id) else { continue }
|
||||
userIDs.append(user.id)
|
||||
hasNewAppend = true
|
||||
}
|
||||
|
||||
let maxID = response.link?.maxID
|
||||
|
||||
if hasNewAppend, maxID != nil {
|
||||
stateMachine.enter(Idle.self)
|
||||
} else {
|
||||
stateMachine.enter(NoMore.self)
|
||||
}
|
||||
self.maxID = maxID
|
||||
viewModel.userFetchedResultsController.userIDs.value = userIDs
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
} // end func didEnter
|
||||
}
|
||||
|
||||
class NoMore: FollowerListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Reloading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let _ = stateMachine else { return }
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
snapshot.deleteItems([.bottomLoader])
|
||||
let header = UserItem.bottomHeader(text: "Followers from other servers are not displayed")
|
||||
snapshot.appendItems([header], toSection: .main)
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// FollowerListViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
|
||||
final class FollowerListViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let domain: CurrentValueSubject<String?, Never>
|
||||
let userID: CurrentValueSubject<String?, Never>
|
||||
let userFetchedResultsController: UserFetchedResultsController
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
|
||||
private(set) lazy var stateMachine: GKStateMachine = {
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
State.Initial(viewModel: self),
|
||||
State.Reloading(viewModel: self),
|
||||
State.Fail(viewModel: self),
|
||||
State.Idle(viewModel: self),
|
||||
State.Loading(viewModel: self),
|
||||
State.NoMore(viewModel: self),
|
||||
])
|
||||
stateMachine.enter(State.Initial.self)
|
||||
return stateMachine
|
||||
}()
|
||||
|
||||
init(context: AppContext, domain: String?, userID: String?) {
|
||||
self.context = context
|
||||
self.userFetchedResultsController = UserFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: domain,
|
||||
additionalTweetPredicate: nil
|
||||
)
|
||||
self.domain = CurrentValueSubject(domain)
|
||||
self.userID = CurrentValueSubject(userID)
|
||||
// super.init()
|
||||
|
||||
}
|
||||
}
|
|
@ -17,9 +17,7 @@ protocol ProfileHeaderViewDelegate: AnyObject {
|
|||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta)
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter)
|
||||
}
|
||||
|
||||
final class ProfileHeaderView: UIView {
|
||||
|
@ -443,6 +441,7 @@ extension ProfileHeaderView {
|
|||
bringSubviewToFront(bannerContainerView)
|
||||
bringSubviewToFront(nameContainerStackView)
|
||||
|
||||
statusDashboardView.delegate = self
|
||||
bioMetaText.textView.delegate = self
|
||||
bioMetaText.textView.linkDelegate = self
|
||||
|
||||
|
@ -549,19 +548,9 @@ extension ProfileHeaderView: MetaTextViewDelegate {
|
|||
|
||||
// MARK: - ProfileStatusDashboardViewDelegate
|
||||
extension ProfileHeaderView: ProfileStatusDashboardViewDelegate {
|
||||
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, postDashboardMeterViewDidPressed: dashboardMeterView)
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter) {
|
||||
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, dashboardMeterViewDidPressed: dashboardMeterView, meter: meter)
|
||||
}
|
||||
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followingDashboardMeterViewDidPressed: dashboardMeterView)
|
||||
}
|
||||
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followersDashboardMeterViewDidPressed: dashboardMeterView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AvatarConfigurableView
|
||||
|
|
|
@ -9,9 +9,7 @@ import os.log
|
|||
import UIKit
|
||||
|
||||
protocol ProfileStatusDashboardViewDelegate: AnyObject {
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter)
|
||||
}
|
||||
|
||||
final class ProfileStatusDashboardView: UIView {
|
||||
|
@ -34,6 +32,14 @@ final class ProfileStatusDashboardView: UIView {
|
|||
|
||||
}
|
||||
|
||||
extension ProfileStatusDashboardView {
|
||||
enum Meter: Hashable {
|
||||
case post
|
||||
case following
|
||||
case follower
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileStatusDashboardView {
|
||||
private func _init() {
|
||||
let containerStackView = UIStackView()
|
||||
|
@ -67,7 +73,6 @@ extension ProfileStatusDashboardView {
|
|||
tapGestureRecognizer.addTarget(self, action: #selector(ProfileStatusDashboardView.tapGestureRecognizerHandler(_:)))
|
||||
meterView.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,12 +83,15 @@ extension ProfileStatusDashboardView {
|
|||
assertionFailure()
|
||||
return
|
||||
}
|
||||
if sourceView === postDashboardMeterView {
|
||||
delegate?.profileStatusDashboardView(self, postDashboardMeterViewDidPressed: sourceView)
|
||||
} else if sourceView === followingDashboardMeterView {
|
||||
delegate?.profileStatusDashboardView(self, followingDashboardMeterViewDidPressed: sourceView)
|
||||
} else if sourceView === followersDashboardMeterView {
|
||||
delegate?.profileStatusDashboardView(self, followersDashboardMeterViewDidPressed: sourceView)
|
||||
switch sourceView {
|
||||
case postDashboardMeterView:
|
||||
delegate?.profileStatusDashboardView(self, dashboardMeterViewDidPressed: sourceView, meter: .post)
|
||||
case followingDashboardMeterView:
|
||||
delegate?.profileStatusDashboardView(self, dashboardMeterViewDidPressed: sourceView, meter: .following)
|
||||
case followersDashboardMeterView:
|
||||
delegate?.profileStatusDashboardView(self, dashboardMeterViewDidPressed: sourceView, meter: .follower)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -769,7 +769,6 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
|
|||
|
||||
// MARK: - ProfileHeaderViewDelegate
|
||||
extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) {
|
||||
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
||||
guard let avatar = imageView.image else { return }
|
||||
|
@ -982,15 +981,29 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter) {
|
||||
switch meter {
|
||||
case .post:
|
||||
// do nothing
|
||||
break
|
||||
case .follower:
|
||||
guard let domain = viewModel.domain.value,
|
||||
let userID = viewModel.userID.value
|
||||
else { return }
|
||||
let followerListViewModel = FollowerListViewModel(
|
||||
context: context,
|
||||
domain: domain,
|
||||
userID: userID
|
||||
)
|
||||
coordinator.present(
|
||||
scene: .follower(viewModel: followerListViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
case .following:
|
||||
// TODO:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import Combine
|
|||
import CoreDataStack
|
||||
import GameplayKit
|
||||
|
||||
// TODO: adopt MediaPreviewableViewController
|
||||
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// TimelineFooterTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class TimelineFooterTableViewCell: UITableViewCell {
|
||||
|
||||
let messageLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 17)
|
||||
label.textAlignment = .center
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = "info"
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TimelineFooterTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
messageLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(messageLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
messageLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
messageLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
messageLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
messageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
messageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 68).priority(.required - 1), // same height to bottom loader
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// UserTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import FLAnimatedImage
|
||||
|
||||
protocol UserTableViewCellDelegate: AnyObject { }
|
||||
|
||||
final class UserTableViewCell: UITableViewCell {
|
||||
|
||||
weak var delegate: UserTableViewCellDelegate?
|
||||
|
||||
let avatarImageView: AvatarImageView = {
|
||||
let imageView = AvatarImageView()
|
||||
imageView.tintColor = Asset.Colors.Label.primary.color
|
||||
imageView.layer.cornerRadius = 4
|
||||
imageView.clipsToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let nameLabel = MetaLabel(style: .statusName)
|
||||
|
||||
let usernameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.font = .preferredFont(forTextStyle: .body)
|
||||
return label
|
||||
}()
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
let containerStackView = UIStackView()
|
||||
containerStackView.axis = .horizontal
|
||||
containerStackView.distribution = .fill
|
||||
containerStackView.spacing = 12
|
||||
containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0)
|
||||
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(containerStackView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||
])
|
||||
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerStackView.addArrangedSubview(avatarImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1),
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1),
|
||||
])
|
||||
|
||||
let textStackView = UIStackView()
|
||||
textStackView.axis = .vertical
|
||||
textStackView.distribution = .fill
|
||||
textStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
textStackView.addArrangedSubview(nameLabel)
|
||||
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
textStackView.addArrangedSubview(usernameLabel)
|
||||
usernameLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical)
|
||||
|
||||
containerStackView.addArrangedSubview(textStackView)
|
||||
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
|
||||
])
|
||||
|
||||
|
||||
nameLabel.isUserInteractionEnabled = false
|
||||
usernameLabel.isUserInteractionEnabled = false
|
||||
avatarImageView.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AvatarStackedImageView
|
||||
extension UserTableViewCell: AvatarConfigurableView {
|
||||
static var configurableAvatarImageSize: CGSize { CGSize(width: 42, height: 42) }
|
||||
static var configurableAvatarImageCornerRadius: CGFloat { 4 }
|
||||
var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView }
|
||||
}
|
||||
|
||||
extension UserTableViewCell {
|
||||
func configure(user: MastodonUser) {
|
||||
// avatar
|
||||
configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: user.avatarImageURL()))
|
||||
// name
|
||||
let name = user.displayNameWithFallback
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: name, emojis: user.emojiMeta)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
nameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: name)
|
||||
nameLabel.configure(content: metaContent)
|
||||
}
|
||||
// username
|
||||
usernameLabel.text = "@" + user.acct
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// APIService+Follower.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func followers(
|
||||
userID: Mastodon.Entity.Account.ID,
|
||||
maxID: String?,
|
||||
authorizationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||
let domain = authorizationBox.domain
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
|
||||
return Mastodon.API.Account.followers(
|
||||
session: session,
|
||||
domain: domain,
|
||||
userID: userID,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> in
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
return managedObjectContext.performChanges {
|
||||
let requestMastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||
requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID)
|
||||
requestMastodonUserRequest.fetchLimit = 1
|
||||
guard let requestMastodonUser = managedObjectContext.safeFetch(requestMastodonUserRequest).first else { return }
|
||||
|
||||
for entity in response.value {
|
||||
_ = APIService.CoreData.createOrMergeMastodonUser(
|
||||
into: managedObjectContext,
|
||||
for: requestMastodonUser,
|
||||
in: domain,
|
||||
entity: entity,
|
||||
userCache: nil,
|
||||
networkDate: response.networkDate,
|
||||
log: .api
|
||||
)
|
||||
}
|
||||
}
|
||||
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Account]> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
|
@ -12,13 +12,15 @@ import Combine
|
|||
extension Mastodon.API.Account {
|
||||
|
||||
static func acceptFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests")
|
||||
return Mastodon.API.endpointURL(domain: domain)
|
||||
.appendingPathComponent("follow_requests")
|
||||
.appendingPathComponent(userID)
|
||||
.appendingPathComponent("authorize")
|
||||
}
|
||||
|
||||
static func rejectFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests")
|
||||
return Mastodon.API.endpointURL(domain: domain)
|
||||
.appendingPathComponent("follow_requests")
|
||||
.appendingPathComponent(userID)
|
||||
.appendingPathComponent("reject")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// Mastodon+API+Account+Followers.swift
|
||||
//
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-11-1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func followersEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain)
|
||||
.appendingPathComponent("accounts")
|
||||
.appendingPathComponent(userID)
|
||||
.appendingPathComponent("followers")
|
||||
}
|
||||
|
||||
/// Followers
|
||||
///
|
||||
/// Accounts which follow the given account, if network is not hidden by the account owner.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.4.1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - userID: ID of the account in the database
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `[Account]` nested in the response
|
||||
public static func followers(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
userID: Mastodon.Entity.Account.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: followersEndpointURL(domain: domain, userID: userID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public struct FollowerQuery: Codable, GetQuery {
|
||||
|
||||
public let maxID: String?
|
||||
public let limit: Int? // default 40
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case maxID = "max_id"
|
||||
case limit
|
||||
}
|
||||
|
||||
public init(
|
||||
maxID: String?,
|
||||
limit: Int?
|
||||
) {
|
||||
self.maxID = maxID
|
||||
self.limit = limit
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items: [URLQueryItem] = []
|
||||
maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) }
|
||||
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
|
||||
guard !items.isEmpty else { return nil }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue