Merge pull request #1136 from mastodon/remove_coredata/following
Don't persist following
This commit is contained in:
commit
7004a22e72
|
@ -145,8 +145,10 @@
|
|||
D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */; };
|
||||
D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A892A4468DC00C0FB73 /* AboutViewController.swift */; };
|
||||
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; };
|
||||
D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; };
|
||||
D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; };
|
||||
D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */; };
|
||||
D87364F92AE28DB500C8F919 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = D87364F82AE28DB500C8F919 /* Kanna */; };
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; };
|
||||
D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */; };
|
||||
|
@ -316,7 +318,6 @@
|
|||
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; };
|
||||
DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; };
|
||||
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */; };
|
||||
DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */; };
|
||||
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; };
|
||||
DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; };
|
||||
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; };
|
||||
|
@ -1021,7 +1022,6 @@
|
|||
DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = "<group>"; };
|
||||
DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
|
||||
DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
|
||||
DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1289,6 +1289,8 @@
|
|||
files = (
|
||||
357FEEAF29523D470021C9DC /* MastodonSDKDynamic in Frameworks */,
|
||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||
D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */,
|
||||
D87364F92AE28DB500C8F919 /* Kanna in Frameworks */,
|
||||
71458AF57697DB405CFEC37C /* Pods_Mastodon.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -2316,7 +2318,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB5B7294273112B100081888 /* FollowingListViewController.swift */,
|
||||
DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */,
|
||||
DB5B7297273112C800081888 /* FollowingListViewModel.swift */,
|
||||
DB5B729B273113C200081888 /* FollowingListViewModel+Diffable.swift */,
|
||||
DB5B729D273113F300081888 /* FollowingListViewModel+State.swift */,
|
||||
|
@ -3120,7 +3121,6 @@
|
|||
buildConfigurationList = DB427DFC25BAA00100D1B89D /* Build configuration list for PBXNativeTarget "Mastodon" */;
|
||||
buildPhases = (
|
||||
7A04933A2AB1D5B758D4F908 /* [CP] Check Pods Manifest.lock */,
|
||||
5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */,
|
||||
DB427DD025BAA00100D1B89D /* Resources */,
|
||||
DB427DCE25BAA00100D1B89D /* Sources */,
|
||||
DB427DCF25BAA00100D1B89D /* Frameworks */,
|
||||
|
@ -3142,6 +3142,8 @@
|
|||
name = Mastodon;
|
||||
packageProductDependencies = (
|
||||
357FEEAE29523D470021C9DC /* MastodonSDKDynamic */,
|
||||
D87364F82AE28DB500C8F919 /* Kanna */,
|
||||
D84FA0922AE6915800987F47 /* MBProgressHUD */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -3178,7 +3180,6 @@
|
|||
DB427DEF25BAA00100D1B89D /* Sources */,
|
||||
DB427DF025BAA00100D1B89D /* Frameworks */,
|
||||
DB427DF125BAA00100D1B89D /* Resources */,
|
||||
ECC2E90D421B45415C311BED /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -3326,6 +3327,8 @@
|
|||
mainGroup = DB427DC925BAA00100D1B89D;
|
||||
packageReferences = (
|
||||
2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */,
|
||||
D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */,
|
||||
D84FA0912AE6915800987F47 /* XCRemoteSwiftPackageReference "MBProgressHUD" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -3419,23 +3422,6 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Mastodon/Pods-Mastodon-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Mastodon/Pods-Mastodon-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mastodon/Pods-Mastodon-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6E033728B42BA1C0018B6131 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -3556,23 +3542,6 @@
|
|||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
ECC2E90D421B45415C311BED /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
@ -3997,7 +3966,6 @@
|
|||
DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */,
|
||||
DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */,
|
||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
||||
DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */,
|
||||
DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */,
|
||||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||
D8F917122A4C6B67008A5370 /* GeneralSettingsViewController.swift in Sources */,
|
||||
|
@ -5463,6 +5431,22 @@
|
|||
kind = branch;
|
||||
};
|
||||
};
|
||||
D84FA0912AE6915800987F47 /* XCRemoteSwiftPackageReference "MBProgressHUD" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/jdg/MBProgressHUD.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.2.0;
|
||||
};
|
||||
};
|
||||
D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/tid-kijyun/Kanna.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.2.7;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -5499,6 +5483,16 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MastodonSDKDynamic;
|
||||
};
|
||||
D84FA0922AE6915800987F47 /* MBProgressHUD */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D84FA0912AE6915800987F47 /* XCRemoteSwiftPackageReference "MBProgressHUD" */;
|
||||
productName = MBProgressHUD;
|
||||
};
|
||||
D87364F82AE28DB500C8F919 /* Kanna */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */;
|
||||
productName = Kanna;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = DB427DCA25BAA00100D1B89D /* Project object */;
|
||||
|
|
|
@ -55,6 +55,15 @@
|
|||
"version": "1.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Kanna",
|
||||
"repositoryURL": "https://github.com/tid-kijyun/Kanna.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f9e4922223dd0d3dfbf02ca70812cf5531fc0593",
|
||||
"version": "5.2.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "KeychainAccess",
|
||||
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
|
@ -73,6 +82,15 @@
|
|||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "MBProgressHUD",
|
||||
"repositoryURL": "https://github.com/jdg/MBProgressHUD.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "bca42b801100b2b3a4eda0ba8dd33d858c780b0d",
|
||||
"version": "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "MetaTextKit",
|
||||
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
|
||||
|
|
|
@ -13,6 +13,7 @@ import MastodonSDK
|
|||
import MastodonCore
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MBProgressHUD
|
||||
|
||||
final public class SceneCoordinator {
|
||||
|
||||
|
@ -28,7 +29,8 @@ final public class SceneCoordinator {
|
|||
|
||||
private(set) weak var tabBarController: MainTabBarController!
|
||||
private(set) weak var splitViewController: RootSplitViewController?
|
||||
|
||||
private(set) weak var rootViewController: UIViewController?
|
||||
|
||||
private(set) var secondaryStackHashValues = Set<Int>()
|
||||
var childCoordinator: Coordinator?
|
||||
|
||||
|
@ -198,7 +200,7 @@ extension SceneCoordinator {
|
|||
case safari(url: URL)
|
||||
case alertController(alertController: UIAlertController)
|
||||
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
|
||||
|
||||
|
||||
var isOnboarding: Bool {
|
||||
switch self {
|
||||
case .welcome,
|
||||
|
@ -239,6 +241,7 @@ extension SceneCoordinator {
|
|||
rootViewController = splitViewController
|
||||
}
|
||||
sceneDelegate.window?.rootViewController = rootViewController // base: main
|
||||
self.rootViewController = rootViewController
|
||||
|
||||
if _authContext == nil { // entry #1: welcome
|
||||
DispatchQueue.main.async {
|
||||
|
@ -465,9 +468,8 @@ private extension SceneCoordinator {
|
|||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .following(let viewModel):
|
||||
let _viewController = FollowingListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext)
|
||||
viewController = followingListViewController
|
||||
case .familiarFollowers(let viewModel):
|
||||
let _viewController = FamiliarFollowersViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
@ -559,13 +561,30 @@ private extension SceneCoordinator {
|
|||
|
||||
return viewController
|
||||
}
|
||||
|
||||
|
||||
private func setupDependency(for needs: NeedsDependency?) {
|
||||
needs?.context = appContext
|
||||
needs?.coordinator = self
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Loading
|
||||
|
||||
public extension SceneCoordinator {
|
||||
func showLoading() {
|
||||
guard let rootViewController else { return }
|
||||
|
||||
MBProgressHUD.showAdded(to: rootViewController.view, animated: true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func hideLoading() {
|
||||
guard let rootViewController else { return }
|
||||
|
||||
MBProgressHUD.hide(for: rootViewController.view, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - MastodonLoginViewControllerDelegate
|
||||
|
||||
extension SceneCoordinator: MastodonLoginViewControllerDelegate {
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
enum UserItem: Hashable {
|
||||
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||
case bottomLoader
|
||||
case bottomHeader(text: String)
|
||||
}
|
||||
|
|
|
@ -19,22 +19,37 @@ enum UserSection: Hashable {
|
|||
}
|
||||
|
||||
extension UserSection {
|
||||
struct Configuration {
|
||||
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||
}
|
||||
|
||||
static func diffableDataSource(
|
||||
tableView: UITableView,
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
configuration: Configuration
|
||||
userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||
) -> UITableViewDiffableDataSource<UserSection, UserItem> {
|
||||
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))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
return UITableViewDiffableDataSource(tableView: tableView) {
|
||||
tableView,
|
||||
indexPath,
|
||||
item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .account(let account, let relationship):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
|
||||
guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell }
|
||||
|
||||
cell.userView.setButtonState(.loading)
|
||||
cell.configure(
|
||||
me: me,
|
||||
tableView: tableView,
|
||||
account: account,
|
||||
relationship: relationship,
|
||||
delegate: userTableViewCellDelegate
|
||||
)
|
||||
|
||||
return cell
|
||||
|
||||
case .user(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
|
@ -50,7 +65,7 @@ extension UserSection {
|
|||
blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(),
|
||||
followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher()
|
||||
),
|
||||
configuration: configuration
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -60,13 +75,12 @@ extension UserSection {
|
|||
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 { … }
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell
|
||||
cell.messageLabel.text = text
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UserSection {
|
||||
|
@ -77,13 +91,13 @@ extension UserSection {
|
|||
tableView: UITableView,
|
||||
cell: UserTableViewCell,
|
||||
viewModel: UserTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
userTableViewCellDelegate: UserTableViewCellDelegate?
|
||||
) {
|
||||
cell.configure(
|
||||
me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext),
|
||||
tableView: tableView,
|
||||
viewModel: viewModel,
|
||||
delegate: configuration.userTableViewCellDelegate
|
||||
delegate: userTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserBlockAction(
|
||||
|
@ -29,5 +30,26 @@ extension DataSourceFacade {
|
|||
authenticationBox: authBox
|
||||
)
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
} // end func
|
||||
}
|
||||
|
||||
static func responseToUserBlockAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: Mastodon.Entity.Account
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let apiService = dependency.context.apiService
|
||||
let authBox = dependency.authContext.mastodonAuthenticationBox
|
||||
|
||||
_ = try await apiService.toggleBlock(
|
||||
user: user,
|
||||
authenticationBox: authBox
|
||||
)
|
||||
|
||||
try await dependency.context.apiService.getBlocked(
|
||||
authenticationBox: authBox
|
||||
)
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,22 @@ extension DataSourceFacade {
|
|||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
} // end func
|
||||
}
|
||||
|
||||
static func responseToUserFollowAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: Mastodon.Entity.Account
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
_ = try await dependency.context.apiService.toggleFollow(
|
||||
user: user,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import UIKit
|
||||
import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
||||
|
@ -53,7 +54,31 @@ extension DataSourceFacade {
|
|||
transition: .show
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
static func coordinateToProfileScene(
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider,
|
||||
account: Mastodon.Entity.Account
|
||||
) async {
|
||||
provider.coordinator.showLoading()
|
||||
|
||||
guard let domain = account.domain else { return provider.coordinator.hideLoading() }
|
||||
|
||||
Task {
|
||||
do {
|
||||
let user = try await provider.context.apiService.fetchUser(username: account.username,
|
||||
domain: domain,
|
||||
authenticationBox: provider.authContext.mastodonAuthenticationBox)
|
||||
provider.coordinator.hideLoading()
|
||||
|
||||
if let user {
|
||||
await coordinateToProfileScene(provider: provider, user: user.asRecord)
|
||||
}
|
||||
} catch {
|
||||
provider.coordinator.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DataSourceFacade {
|
||||
|
|
|
@ -17,7 +17,8 @@ extension DataSourceFacade {
|
|||
item: DataSourceItem
|
||||
) async {
|
||||
switch item {
|
||||
case .status:
|
||||
|
||||
case .status, .account(_, _):
|
||||
break // not create search history for status
|
||||
case .user(let record):
|
||||
let authenticationBox = provider.authContext.mastodonAuthenticationBox
|
||||
|
|
|
@ -64,16 +64,52 @@ extension DataSourceFacade {
|
|||
break //no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider {
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
|
||||
Task {
|
||||
try await DataSourceFacade.responseToUserViewButtonAction(
|
||||
dependency: self,
|
||||
user: user.asRecord,
|
||||
buttonState: state
|
||||
)
|
||||
static func responseToUserViewButtonAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
user: Mastodon.Entity.Account,
|
||||
buttonState: UserView.ButtonState
|
||||
) async throws {
|
||||
switch buttonState {
|
||||
case .follow:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id)
|
||||
|
||||
case .request:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id)
|
||||
case .unfollow:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == user.id })
|
||||
case .blocked:
|
||||
try await DataSourceFacade.responseToUserBlockAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id)
|
||||
|
||||
case .pending:
|
||||
try await DataSourceFacade.responseToUserFollowAction(
|
||||
dependency: dependency,
|
||||
user: user
|
||||
)
|
||||
|
||||
dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == user.id })
|
||||
case .none, .loading:
|
||||
break //no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,12 +14,15 @@ import MastodonLocalization
|
|||
extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider {
|
||||
|
||||
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
Task {
|
||||
let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath)
|
||||
guard let item = await item(from: source) else {
|
||||
return
|
||||
}
|
||||
switch item {
|
||||
case .account(let account, relationship: _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
case .status(let status):
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
|
|
|
@ -16,6 +16,7 @@ enum DataSourceItem: Hashable {
|
|||
case user(record: ManagedObjectRecord<MastodonUser>)
|
||||
case hashtag(tag: TagKind)
|
||||
case notification(record: ManagedObjectRecord<Notification>)
|
||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||
}
|
||||
|
||||
extension DataSourceItem {
|
||||
|
|
|
@ -17,9 +17,7 @@ extension FamiliarFollowersViewModel {
|
|||
tableView: tableView,
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
configuration: UserSection.Configuration(
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
|
||||
userFetchedResultsController.$records
|
||||
|
|
|
@ -18,9 +18,7 @@ extension FollowerListViewModel {
|
|||
tableView: tableView,
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
configuration: UserSection.Configuration(
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
|
||||
// workaround to append loader wrong animation issue
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
//
|
||||
// FollowingListViewController+DataSourceProvider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-1-20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension FollowingListViewController: DataSourceProvider {
|
||||
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
|
||||
var _indexPath = source.indexPath
|
||||
if _indexPath == nil, let cell = source.tableViewCell {
|
||||
_indexPath = await self.indexPath(for: cell)
|
||||
}
|
||||
guard let indexPath = _indexPath else { return nil }
|
||||
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .user(let record):
|
||||
return .user(record: record)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
}
|
||||
}
|
|
@ -15,51 +15,64 @@ import CoreDataStack
|
|||
|
||||
final class FollowingListViewController: UIViewController, NeedsDependency {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var context: AppContext!
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: FollowingListViewModel!
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
var viewModel: FollowingListViewModel
|
||||
|
||||
let refreshControl: UIRefreshControl
|
||||
let tableView: UITableView
|
||||
|
||||
init(viewModel: FollowingListViewModel, coordinator: SceneCoordinator, context: AppContext) {
|
||||
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
self.viewModel = viewModel
|
||||
|
||||
tableView = UITableView()
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
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
|
||||
}()
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension FollowingListViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
tableView.refreshControl = refreshControl
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
title = L10n.Scene.Following.title
|
||||
|
||||
|
||||
view.backgroundColor = .secondarySystemBackground
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.pinToParent()
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.refreshControl?.addTarget(self, action: #selector(FollowingListViewController.refresh(_:)), for: .valueChanged)
|
||||
|
||||
viewModel.tableView = tableView
|
||||
|
||||
refreshControl.addTarget(self, action: #selector(FollowingListViewController.refresh(_:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
viewModel.setupDiffableDataSource(
|
||||
tableView: tableView,
|
||||
userTableViewCellDelegate: self
|
||||
)
|
||||
|
||||
|
||||
// setup batch fetch
|
||||
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
|
||||
viewModel.listBatchFetchViewModel.shouldFetch
|
||||
viewModel.shouldFetch
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.viewModel.stateMachine.enter(FollowingListViewModel.State.Loading.self)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
@ -75,6 +88,8 @@ extension FollowingListViewController {
|
|||
self.viewModel.stateMachine.enter(FollowingListViewModel.State.Reloading.self)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
tableView.refreshControl = UIRefreshControl()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -82,7 +97,13 @@ extension FollowingListViewController {
|
|||
|
||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
//MARK: - Actions
|
||||
|
||||
@objc
|
||||
func refresh(_ sender: UIRefreshControl) {
|
||||
viewModel.stateMachine.enter(FollowingListViewModel.State.Reloading.self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AuthContextProvider
|
||||
|
@ -105,3 +126,54 @@ extension FollowingListViewController: UITableViewDelegate, AutoGenerateTableVie
|
|||
|
||||
// MARK: - UserTableViewCellDelegate
|
||||
extension FollowingListViewController: UserTableViewCellDelegate {}
|
||||
|
||||
|
||||
// MARK: - DataSourceProvider
|
||||
extension FollowingListViewController: DataSourceProvider {
|
||||
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
|
||||
var _indexPath = source.indexPath
|
||||
if _indexPath == nil, let cell = source.tableViewCell {
|
||||
_indexPath = await self.indexPath(for: cell)
|
||||
}
|
||||
guard let indexPath = _indexPath else { return nil }
|
||||
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .account(let account, let relationship):
|
||||
return .account(account: account, relationship: relationship)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||
return tableView.indexPath(for: cell)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UIScrollViewDelegate
|
||||
|
||||
extension FollowingListViewController: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
|
||||
if scrollView.isDragging || scrollView.isTracking { return }
|
||||
|
||||
let frame = scrollView.frame
|
||||
let contentOffset = scrollView.contentOffset
|
||||
let contentSize = scrollView.contentSize
|
||||
|
||||
let visibleBottomY = contentOffset.y + frame.height
|
||||
let offset = 2 * frame.height
|
||||
let fetchThrottleOffsetY = contentSize.height - offset
|
||||
|
||||
if visibleBottomY > fetchThrottleOffsetY {
|
||||
viewModel.shouldFetch.send()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
extension FollowingListViewModel {
|
||||
func setupDiffableDataSource(
|
||||
|
@ -19,9 +20,7 @@ extension FollowingListViewModel {
|
|||
tableView: tableView,
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
configuration: UserSection.Configuration(
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
|
||||
// workaround to append loader wrong animation issue
|
||||
|
@ -31,30 +30,39 @@ extension FollowingListViewModel {
|
|||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
diffableDataSource?.applySnapshotUsingReloadData(snapshot)
|
||||
|
||||
userFetchedResultsController.$records
|
||||
$accounts
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] records in
|
||||
guard let self = self else { return }
|
||||
.sink { [weak self] accounts in
|
||||
guard let self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<UserSection, UserItem>()
|
||||
snapshot.appendSections([.main])
|
||||
let items = records.map { UserItem.user(record: $0) }
|
||||
|
||||
let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in
|
||||
guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)}
|
||||
|
||||
return (account: account, relationship: relationship)
|
||||
}
|
||||
|
||||
let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) }
|
||||
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:
|
||||
guard let userID = self.userID,
|
||||
userID != self.authContext.mastodonAuthenticationBox.userID
|
||||
else { break }
|
||||
// display footer exclude self
|
||||
let text = L10n.Scene.Following.footer
|
||||
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
|
||||
default:
|
||||
break
|
||||
case is State.Loading:
|
||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
case is State.NoMore:
|
||||
guard let userID = self.userID,
|
||||
userID != self.authContext.mastodonAuthenticationBox.userID
|
||||
else { break }
|
||||
// display footer exclude self
|
||||
let text = L10n.Scene.Following.footer
|
||||
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
|
||||
case is State.Idle, is State.Fail:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import MastodonSDK
|
|||
|
||||
extension FollowingListViewModel {
|
||||
class State: GKState {
|
||||
|
||||
|
||||
let id = UUID()
|
||||
|
||||
weak var viewModel: FollowingListViewModel?
|
||||
|
@ -32,10 +32,10 @@ extension FollowingListViewModel.State {
|
|||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
guard let viewModel = viewModel else { return false }
|
||||
switch stateClass {
|
||||
case is Reloading.Type:
|
||||
return viewModel.userID != nil
|
||||
default:
|
||||
return false
|
||||
case is Reloading.Type:
|
||||
return viewModel.userID != nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,20 +43,21 @@ extension FollowingListViewModel.State {
|
|||
class Reloading: FollowingListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
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 }
|
||||
guard let viewModel, let stateMachine else { return }
|
||||
|
||||
// reset
|
||||
viewModel.userFetchedResultsController.userIDs = []
|
||||
|
||||
viewModel.accounts = []
|
||||
viewModel.relationships = []
|
||||
|
||||
stateMachine.enter(Loading.self)
|
||||
}
|
||||
}
|
||||
|
@ -65,10 +66,10 @@ extension FollowingListViewModel.State {
|
|||
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,12 +86,18 @@ extension FollowingListViewModel.State {
|
|||
class Idle: FollowingListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Reloading.Type, is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
case is Reloading.Type, is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
viewModel?.tableView?.refreshControl?.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
class Loading: FollowingListViewModel.State {
|
||||
|
@ -99,14 +106,14 @@ extension FollowingListViewModel.State {
|
|||
|
||||
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
|
||||
case is Fail.Type:
|
||||
return true
|
||||
case is Idle.Type:
|
||||
return true
|
||||
case is NoMore.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,58 +124,80 @@ extension FollowingListViewModel.State {
|
|||
maxID = nil
|
||||
}
|
||||
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
guard let viewModel, let stateMachine else { return }
|
||||
|
||||
guard let userID = viewModel.userID, !userID.isEmpty else {
|
||||
guard let userID = viewModel.userID, userID.isEmpty == false else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response = try await viewModel.context.apiService.following(
|
||||
let accountResponse = try await viewModel.context.apiService.following(
|
||||
userID: userID,
|
||||
maxID: maxID,
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
||||
if accountResponse.value.isEmpty {
|
||||
await enter(state: NoMore.self)
|
||||
|
||||
viewModel.accounts = []
|
||||
viewModel.relationships = []
|
||||
return
|
||||
}
|
||||
|
||||
var hasNewAppend = false
|
||||
var userIDs = viewModel.userFetchedResultsController.userIDs
|
||||
for user in response.value {
|
||||
guard !userIDs.contains(user.id) else { continue }
|
||||
userIDs.append(user.id)
|
||||
|
||||
let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
|
||||
|
||||
var accounts = viewModel.accounts
|
||||
|
||||
for user in accountResponse.value {
|
||||
guard accounts.contains(user) == false else { continue }
|
||||
accounts.append(user)
|
||||
hasNewAppend = true
|
||||
}
|
||||
|
||||
let maxID = response.link?.maxID
|
||||
|
||||
|
||||
var relationships = viewModel.relationships
|
||||
|
||||
for relationship in newRelationships.value {
|
||||
guard relationships.contains(relationship) == false else { continue }
|
||||
relationships.append(relationship)
|
||||
}
|
||||
|
||||
let maxID = accountResponse.link?.maxID
|
||||
|
||||
if hasNewAppend, maxID != nil {
|
||||
await enter(state: Idle.self)
|
||||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
self.maxID = maxID
|
||||
viewModel.userFetchedResultsController.userIDs = userIDs
|
||||
|
||||
viewModel.accounts = accounts
|
||||
viewModel.relationships = relationships
|
||||
self.maxID = maxID
|
||||
} catch {
|
||||
await enter(state: Fail.self)
|
||||
}
|
||||
} // end Task
|
||||
} // end func didEnter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoMore: FollowingListViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Reloading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
case is Reloading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
viewModel?.tableView?.refreshControl?.endRefreshing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
@ -20,12 +18,16 @@ final class FollowingListViewModel {
|
|||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let userFetchedResultsController: UserFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
@Published var accounts: [Mastodon.Entity.Account]
|
||||
@Published var relationships: [Mastodon.Entity.Relationship]
|
||||
|
||||
@Published var domain: String?
|
||||
@Published var userID: String?
|
||||
|
||||
|
||||
let shouldFetch = PassthroughSubject<Void, Never>()
|
||||
|
||||
var tableView: UITableView?
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
|
||||
private(set) lazy var stateMachine: GKStateMachine = {
|
||||
|
@ -49,14 +51,9 @@ final class FollowingListViewModel {
|
|||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.userFetchedResultsController = UserFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: domain,
|
||||
additionalPredicate: nil
|
||||
)
|
||||
self.domain = domain
|
||||
self.userID = userID
|
||||
// super.init()
|
||||
|
||||
self.accounts = []
|
||||
self.relationships = []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -377,12 +377,12 @@ extension ProfileViewController {
|
|||
profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear
|
||||
)
|
||||
.sink { [weak self] (user, _) in
|
||||
guard let self = self, let user = user else { return }
|
||||
guard let self, let user else { return }
|
||||
Task {
|
||||
_ = try await self.context.apiService.accountInfo(
|
||||
domain: user.domain,
|
||||
userID: user.id,
|
||||
authorization: self.authContext.mastodonAuthenticationBox.userAuthorization
|
||||
_ = try await self.context.apiService.fetchUser(
|
||||
username: user.username,
|
||||
domain: user.domainFromAcct,
|
||||
authenticationBox: self.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,7 @@ extension UserListViewModel {
|
|||
tableView: tableView,
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
configuration: UserSection.Configuration(
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
userTableViewCellDelegate: userTableViewCellDelegate
|
||||
)
|
||||
|
||||
// workaround to append loader wrong animation issue
|
||||
|
|
|
@ -52,6 +52,9 @@ extension SearchResultViewController {
|
|||
)
|
||||
|
||||
switch item {
|
||||
case .account(account: _, relationship: _):
|
||||
// do nothing
|
||||
break
|
||||
case .status(let status):
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
|
|
|
@ -13,11 +13,15 @@ import MastodonLocalization
|
|||
import MastodonMeta
|
||||
import MastodonCore
|
||||
import Meta
|
||||
import MastodonSDK
|
||||
import MastodonAsset
|
||||
|
||||
extension UserView {
|
||||
public func configure(user: MastodonUser, delegate: UserViewDelegate?) {
|
||||
self.delegate = delegate
|
||||
viewModel.user = user
|
||||
viewModel.account = nil
|
||||
viewModel.relationship = nil
|
||||
|
||||
Publishers.CombineLatest(
|
||||
user.publisher(for: \.avatar),
|
||||
|
@ -63,4 +67,55 @@ extension UserView {
|
|||
.assign(to: \.authorVerifiedLink, on: viewModel)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func configure(with account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, delegate: UserViewDelegate?) {
|
||||
viewModel.account = account
|
||||
viewModel.relationship = relationship
|
||||
viewModel.user = nil
|
||||
self.delegate = delegate
|
||||
|
||||
let authorUsername = PlaintextMetaContent(string: "@\(account.username)")
|
||||
authorUsernameLabel.configure(content: authorUsername)
|
||||
|
||||
do {
|
||||
let emojis = account.emojis?.asDictionary ?? [:]
|
||||
let content = MastodonContent(content: account.displayNameWithFallback, emojis: emojis)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
authorNameLabel.configure(content: metaContent)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback)
|
||||
authorNameLabel.configure(content: metaContent)
|
||||
}
|
||||
|
||||
if let imageURL = account.avatarImageURL() {
|
||||
avatarButton.avatarImageView.af.setImage(withURL: imageURL)
|
||||
}
|
||||
|
||||
let count = account.followersCount
|
||||
authorFollowersLabel.attributedText = NSAttributedString(
|
||||
format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]),
|
||||
args: NSAttributedString(string: UserView.metricFormatter.string(from: count) ?? count.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))])
|
||||
)
|
||||
|
||||
if let verifiedLinkField = account.verifiedLink {
|
||||
let link = verifiedLinkField.value
|
||||
|
||||
authorVerifiedImageView.image = UIImage(systemName: "checkmark")
|
||||
authorVerifiedImageView.tintColor = Asset.Colors.Brand.blurple.color
|
||||
authorVerifiedLabel.textColor = Asset.Colors.Brand.blurple.color
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: link, emojis: [:])
|
||||
let content = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
authorVerifiedLabel.configure(content: content)
|
||||
} catch {
|
||||
let content = PlaintextMetaContent(string: link)
|
||||
authorVerifiedLabel.configure(content: content)
|
||||
}
|
||||
} else {
|
||||
authorVerifiedImageView.image = UIImage(systemName: "questionmark.circle")
|
||||
authorVerifiedImageView.tintColor = .secondaryLabel
|
||||
authorVerifiedLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink))
|
||||
authorVerifiedLabel.textColor = .secondaryLabel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import UIKit
|
|||
import CoreDataStack
|
||||
import MastodonUI
|
||||
import Combine
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension UserTableViewCell {
|
||||
final class ViewModel {
|
||||
|
@ -29,6 +31,21 @@ extension UserTableViewCell {
|
|||
|
||||
extension UserTableViewCell {
|
||||
|
||||
func configure(
|
||||
me: MastodonUser,
|
||||
tableView: UITableView,
|
||||
account: Mastodon.Entity.Account,
|
||||
relationship: Mastodon.Entity.Relationship?,
|
||||
delegate: UserTableViewCellDelegate?
|
||||
) {
|
||||
userView.configure(with: account, relationship: relationship, delegate: delegate)
|
||||
|
||||
let isMe = account.id == me.id
|
||||
userView.updateButtonState(with: relationship, isMe: isMe)
|
||||
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
func configure(
|
||||
me: MastodonUser? = nil,
|
||||
tableView: UITableView,
|
||||
|
@ -72,5 +89,46 @@ extension UserTableViewCell {
|
|||
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider {
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
|
||||
Task {
|
||||
try await DataSourceFacade.responseToUserViewButtonAction(
|
||||
dependency: self,
|
||||
user: user.asRecord,
|
||||
buttonState: state
|
||||
)
|
||||
}
|
||||
}
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: MastodonUser?) {
|
||||
Task {
|
||||
await MainActor.run { view.setButtonState(.loading) }
|
||||
|
||||
try await DataSourceFacade.responseToUserViewButtonAction(
|
||||
dependency: self,
|
||||
user: account,
|
||||
buttonState: state
|
||||
)
|
||||
|
||||
// this is a dirty hack to give the backend enough time to process the relationship-change
|
||||
// Otherwise the relationship might still be `pending`
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
|
||||
let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first
|
||||
|
||||
let isMe: Bool
|
||||
if let me {
|
||||
isMe = account.id == me.id
|
||||
} else {
|
||||
isMe = false
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
view.viewModel.relationship = relationship
|
||||
view.updateButtonState(with: relationship, isMe: isMe)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import CoreData
|
||||
import Foundation
|
||||
|
||||
/// See also `CoreDataStack.MastodonUser`, this extension contains several
|
||||
final public class MastodonUser: NSManagedObject {
|
||||
|
||||
public typealias ID = String
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.216",
|
||||
"green" : "0.173",
|
||||
"red" : "0.157"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.933",
|
||||
"green" : "0.933",
|
||||
"red" : "0.933"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.933",
|
||||
"green" : "0.933",
|
||||
"red" : "0.933"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.106",
|
||||
"green" : "0.082",
|
||||
"red" : "0.075"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.729",
|
||||
"green" : "0.729",
|
||||
"red" : "0.729"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.729",
|
||||
"green" : "0.729",
|
||||
"red" : "0.729"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.106",
|
||||
"green" : "0.082",
|
||||
"red" : "0.075"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.216",
|
||||
"green" : "0.173",
|
||||
"red" : "0.157"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -190,10 +190,8 @@ public enum Asset {
|
|||
public static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray")
|
||||
}
|
||||
public enum RelationshipButton {
|
||||
public static let backgroundDark = ColorAsset(name: "Scene/Profile/RelationshipButton/background.dark")
|
||||
public static let backgroundHighlightedDark = ColorAsset(name: "Scene/Profile/RelationshipButton/background.highlighted.dark")
|
||||
public static let backgroundHighlightedLight = ColorAsset(name: "Scene/Profile/RelationshipButton/background.highlighted.light")
|
||||
public static let backgroundLight = ColorAsset(name: "Scene/Profile/RelationshipButton/background.light")
|
||||
public static let background = ColorAsset(name: "Scene/Profile/RelationshipButton/background")
|
||||
public static let backgroundHighlighted = ColorAsset(name: "Scene/Profile/RelationshipButton/background.highlighted")
|
||||
}
|
||||
}
|
||||
public enum Report {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
//
|
||||
// Mastodon+Entity+History.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/4/2.
|
||||
//
|
||||
|
||||
import MastodonSDK
|
||||
|
||||
extension Mastodon.Entity.History: Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(uses)
|
||||
hasher.combine(accounts)
|
||||
hasher.combine(day)
|
||||
}
|
||||
|
||||
public static func == (lhs: Mastodon.Entity.History, rhs: Mastodon.Entity.History) -> Bool {
|
||||
return lhs.uses == rhs.uses && lhs.uses == rhs.uses && lhs.day == rhs.day
|
||||
}
|
||||
}
|
|
@ -201,8 +201,9 @@ extension APIService {
|
|||
|
||||
// user
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
var result: MastodonUser?
|
||||
try await managedObjectContext.performChanges {
|
||||
_ = Persistence.MastodonUser.createOrMerge(
|
||||
result = Persistence.MastodonUser.createOrMerge(
|
||||
in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: domain,
|
||||
|
@ -210,18 +211,9 @@ extension APIService {
|
|||
cache: nil,
|
||||
networkDate: response.networkDate
|
||||
)
|
||||
)
|
||||
}
|
||||
var result: MastodonUser?
|
||||
try await managedObjectContext.perform {
|
||||
result = Persistence.MastodonUser.fetch(in: managedObjectContext,
|
||||
context: Persistence.MastodonUser.PersistContext(
|
||||
domain: domain,
|
||||
entity: response.value,
|
||||
cache: nil,
|
||||
networkDate: response.networkDate
|
||||
))
|
||||
).user
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,7 +148,35 @@ extension APIService {
|
|||
let response = try result.get()
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
public func toggleBlock(
|
||||
user: Mastodon.Entity.Account,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
|
||||
guard let relationship = try await relationship(forAccounts: [user], authenticationBox: authenticationBox).value.first else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
let response: Mastodon.Response.Content<Mastodon.Entity.Relationship>
|
||||
|
||||
if relationship.blocking {
|
||||
response = try await Mastodon.API.Account.unblock(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
accountID: user.id,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
} else {
|
||||
response = try await Mastodon.API.Account.block(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
accountID: user.id,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonUser {
|
||||
|
|
|
@ -38,11 +38,11 @@ extension APIService {
|
|||
let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges {
|
||||
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil }
|
||||
guard let user = user.object(in: managedObjectContext) else { return nil }
|
||||
|
||||
|
||||
let isFollowing = user.followingBy.contains(me)
|
||||
let isPending = user.followRequestedBy.contains(me)
|
||||
let needsUnfollow = isFollowing || isPending
|
||||
|
||||
|
||||
if needsUnfollow {
|
||||
// unfollow
|
||||
user.update(isFollowing: false, by: me)
|
||||
|
@ -66,11 +66,11 @@ extension APIService {
|
|||
)
|
||||
return context
|
||||
}
|
||||
|
||||
|
||||
guard let followContext = _followContext else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
|
||||
// request follow or unfollow
|
||||
let result: Result<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>
|
||||
do {
|
||||
|
@ -85,13 +85,13 @@ extension APIService {
|
|||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
|
||||
|
||||
// update friendship state
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let me = authenticationBox.authentication.user(in: managedObjectContext),
|
||||
let user = user.object(in: managedObjectContext)
|
||||
else { return }
|
||||
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
Persistence.MastodonUser.update(
|
||||
|
@ -108,11 +108,43 @@ extension APIService {
|
|||
user.update(isFollowRequested: followContext.isPending, by: me)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let response = try result.get()
|
||||
return response
|
||||
}
|
||||
|
||||
public func toggleFollow(
|
||||
user: Mastodon.Entity.Account,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Relationship> {
|
||||
|
||||
guard let relationship = try await relationship(forAccounts: [user], authenticationBox: authenticationBox).value.first else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
let response: Mastodon.Response.Content<Mastodon.Entity.Relationship>
|
||||
|
||||
if relationship.following || (relationship.requested ?? false) {
|
||||
// unfollow
|
||||
response = try await Mastodon.API.Account.unfollow(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
accountID: user.id,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
} else {
|
||||
response = try await Mastodon.API.Account.follow(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
accountID: user.id,
|
||||
followQueryType: .follow(query: .init()),
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
public func toggleShowReblogs(
|
||||
for user: ManagedObjectRecord<MastodonUser>,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
|
|
|
@ -18,7 +18,7 @@ extension APIService {
|
|||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
|
||||
|
||||
let _query: Mastodon.API.Account.RelationshipQuery? = try? await managedObjectContext.perform {
|
||||
var ids: [MastodonUser.ID] = []
|
||||
for record in records {
|
||||
|
@ -32,14 +32,14 @@ extension APIService {
|
|||
guard let query = _query else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
|
||||
|
||||
let response = try await Mastodon.API.Account.relationships(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
query: query,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
|
||||
|
||||
try await managedObjectContext.performChanges {
|
||||
guard let me = authenticationBox.authentication.user(in: managedObjectContext) else {
|
||||
// assertionFailure()
|
||||
|
@ -50,7 +50,7 @@ extension APIService {
|
|||
for record in records {
|
||||
guard let user = record.object(in: managedObjectContext) else { continue }
|
||||
guard let relationship = relationships.first(where: { $0.id == user.id }) else { continue }
|
||||
|
||||
|
||||
Persistence.MastodonUser.update(
|
||||
mastodonUser: user,
|
||||
context: Persistence.MastodonUser.RelationshipContext(
|
||||
|
@ -64,5 +64,27 @@ extension APIService {
|
|||
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func relationship(
|
||||
forAccounts accounts: [Mastodon.Entity.Account],
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> {
|
||||
|
||||
let ids: [MastodonUser.ID] = accounts.compactMap { $0.id }
|
||||
|
||||
guard ids.isEmpty == false else { throw APIError.implicit(.badRequest) }
|
||||
|
||||
let query = Mastodon.API.Account.RelationshipQuery(ids: ids)
|
||||
|
||||
let response = try await Mastodon.API.Account.relationships(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
query: query,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -91,11 +91,15 @@ extension Mastodon.Entity.Account {
|
|||
}
|
||||
return acct
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Account {
|
||||
public var verifiedLink: Mastodon.Entity.Field? {
|
||||
let firstVerified = fields?.first(where: { $0.verifiedAt != nil })
|
||||
return firstVerified
|
||||
}
|
||||
|
||||
public var domain: String? {
|
||||
guard let components = URLComponents(string: url) else { return nil }
|
||||
|
||||
return components.host
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ extension Mastodon.Entity {
|
|||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/relationship/)
|
||||
public struct Relationship: Codable, Sendable {
|
||||
public struct Relationship: Codable, Sendable, Equatable, Hashable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
|
||||
public final class FollowButton: RoundedEdgesButton {
|
||||
|
||||
public init() {
|
||||
super.init(frame: .zero)
|
||||
configureAppearance()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func configureAppearance() {
|
||||
setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
||||
setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted)
|
||||
switch traitCollection.userInterfaceStyle {
|
||||
case .dark:
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundDark.color), for: .normal)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .disabled)
|
||||
default:
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundLight.color), for: .normal)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import MastodonCore
|
|||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
extension UserView {
|
||||
public final class ViewModel: ObservableObject {
|
||||
|
@ -26,6 +27,8 @@ extension UserView {
|
|||
@Published public var authorFollowers: Int?
|
||||
@Published public var authorVerifiedLink: String?
|
||||
@Published public var user: MastodonUser?
|
||||
@Published public var account: Mastodon.Entity.Account?
|
||||
@Published public var relationship: Mastodon.Entity.Relationship?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@ import MastodonAsset
|
|||
import MastodonLocalization
|
||||
import os
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
public protocol UserViewDelegate: AnyObject {
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser)
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account, me: MastodonUser?)
|
||||
}
|
||||
|
||||
public final class UserView: UIView {
|
||||
|
@ -24,7 +26,8 @@ public final class UserView: UIView {
|
|||
}
|
||||
|
||||
private var currentButtonState: ButtonState = .none
|
||||
|
||||
public static var metricFormatter = MastodonMetricFormatter()
|
||||
|
||||
public weak var delegate: UserViewDelegate?
|
||||
|
||||
public var disposeBag = Set<AnyCancellable>()
|
||||
|
@ -100,10 +103,12 @@ public final class UserView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
private let followButtonWrapper = UIView()
|
||||
private let followButton: FollowButton = {
|
||||
let button = FollowButton()
|
||||
button.cornerRadius = 10
|
||||
public let followButtonWrapper = UIView()
|
||||
public let followButton: UIButton = {
|
||||
var buttonConfiguration = UIButton.Configuration.filled()
|
||||
buttonConfiguration.background.cornerRadius = 10
|
||||
|
||||
let button = UIButton(configuration: buttonConfiguration)
|
||||
button.isHidden = true
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
|
@ -249,66 +254,108 @@ public extension UserView {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func didTapButton() {
|
||||
guard let user = viewModel.user else { return }
|
||||
delegate?.userView(self, didTapButtonWith: currentButtonState, for: user)
|
||||
@objc private func didTapFollowButton() {
|
||||
if let user = viewModel.user {
|
||||
delegate?.userView(self, didTapButtonWith: currentButtonState, for: user)
|
||||
} else if let account = viewModel.account {
|
||||
delegate?.userView(self, didTapButtonWith: currentButtonState, for: account, me: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateButtonState(with relationship: Mastodon.Entity.Relationship?, isMe: Bool) {
|
||||
let buttonState: UserView.ButtonState
|
||||
|
||||
if let relationship {
|
||||
if isMe {
|
||||
buttonState = .none
|
||||
} else if relationship.following {
|
||||
buttonState = .unfollow
|
||||
} else if relationship.blocking || (relationship.domainBlocking ?? false) {
|
||||
buttonState = .blocked
|
||||
} else if relationship.requested ?? false {
|
||||
buttonState = .pending
|
||||
} else {
|
||||
buttonState = .follow
|
||||
}
|
||||
} else {
|
||||
buttonState = .none
|
||||
}
|
||||
|
||||
setButtonState(buttonState)
|
||||
|
||||
}
|
||||
|
||||
func setButtonState(_ state: ButtonState) {
|
||||
currentButtonState = state
|
||||
prepareButtonStateLayout(for: state)
|
||||
|
||||
|
||||
switch state {
|
||||
|
||||
case .loading:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(nil, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.disabled.color, for: .normal)
|
||||
|
||||
case .follow:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
|
||||
followButton.setTitleColor(.white, for: .normal)
|
||||
case .loading:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.configuration?.title = nil
|
||||
followButton.configuration?.showsActivityIndicator = true
|
||||
followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollowing.color
|
||||
followButton.configuration?.baseForegroundColor = Asset.Colors.Brand.blurple.color
|
||||
followButton.isEnabled = false
|
||||
|
||||
case .request:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.request, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
|
||||
followButton.setTitleColor(.white, for: .normal)
|
||||
case .follow:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.configuration?.title = L10n.Common.Controls.Friendship.follow
|
||||
followButton.configuration?.showsActivityIndicator = false
|
||||
followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollow.color
|
||||
followButton.configuration?.baseForegroundColor = .white
|
||||
followButton.isEnabled = true
|
||||
|
||||
case .pending:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.pending, for: .normal)
|
||||
followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal)
|
||||
case .request:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.configuration?.title = L10n.Common.Controls.Friendship.request
|
||||
followButton.configuration?.showsActivityIndicator = false
|
||||
followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollow.color
|
||||
followButton.configuration?.baseForegroundColor = .white
|
||||
followButton.isEnabled = true
|
||||
|
||||
case .unfollow:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal)
|
||||
followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal)
|
||||
|
||||
case .blocked:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.setTitle(L10n.Common.Controls.Friendship.blocked, for: .normal)
|
||||
followButton.setBackgroundColor(Asset.Colors.Button.userBlocked.color, for: .normal)
|
||||
followButton.setTitleColor(.systemRed, for: .normal)
|
||||
case .pending:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.configuration?.title = L10n.Common.Controls.Friendship.pending
|
||||
followButton.configuration?.baseForegroundColor = Asset.Colors.Button.userFollowingTitle.color
|
||||
followButton.configuration?.showsActivityIndicator = false
|
||||
followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollowing.color
|
||||
followButton.isEnabled = true
|
||||
|
||||
case .none:
|
||||
followButtonWrapper.isHidden = true
|
||||
followButton.isHidden = true
|
||||
followButton.setTitle(nil, for: .normal)
|
||||
followButton.setBackgroundColor(.clear, for: .normal)
|
||||
case .unfollow:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.configuration?.title = L10n.Common.Controls.Friendship.following
|
||||
followButton.configuration?.showsActivityIndicator = false
|
||||
followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollowing.color
|
||||
followButton.configuration?.baseForegroundColor = Asset.Colors.Button.userFollowingTitle.color
|
||||
followButton.isEnabled = true
|
||||
|
||||
case .blocked:
|
||||
followButtonWrapper.isHidden = false
|
||||
followButton.isHidden = false
|
||||
followButton.configuration?.title = L10n.Common.Controls.Friendship.blocked
|
||||
followButton.configuration?.showsActivityIndicator = false
|
||||
followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userBlocked.color
|
||||
followButton.configuration?.baseForegroundColor = .systemRed
|
||||
followButton.isEnabled = true
|
||||
|
||||
case .none:
|
||||
followButtonWrapper.isHidden = true
|
||||
followButton.isHidden = true
|
||||
followButton.configuration?.title = nil
|
||||
followButton.configuration?.showsActivityIndicator = false
|
||||
followButton.configuration?.background.backgroundColor = .clear
|
||||
followButton.isEnabled = false
|
||||
}
|
||||
|
||||
followButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
|
||||
|
||||
followButton.addTarget(self, action: #selector(didTapFollowButton), for: .touchUpInside)
|
||||
followButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -77,15 +77,8 @@ extension ProfileRelationshipActionButton {
|
|||
private func configureAppearance() {
|
||||
setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
|
||||
setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted)
|
||||
switch traitCollection.userInterfaceStyle {
|
||||
case .dark:
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundDark.color), for: .normal)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .disabled)
|
||||
default:
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundLight.color), for: .normal)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled)
|
||||
}
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.background.color), for: .normal)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlighted.color), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlighted.color), for: .disabled)
|
||||
}
|
||||
}
|
||||
|
|
1
Podfile
1
Podfile
|
@ -9,7 +9,6 @@ target 'Mastodon' do
|
|||
|
||||
# misc
|
||||
pod 'SwiftGen', '~> 6.6.2'
|
||||
pod 'Kanna', '~> 5.2.2'
|
||||
pod 'Sourcery', '~> 1.9'
|
||||
|
||||
target 'MastodonTests' do
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
PODS:
|
||||
- Kanna (5.2.7)
|
||||
- Sourcery (1.9.2):
|
||||
- Sourcery/CLI-Only (= 1.9.2)
|
||||
- Sourcery/CLI-Only (1.9.2)
|
||||
- SwiftGen (6.6.2)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Kanna (~> 5.2.2)
|
||||
- Sourcery (~> 1.9)
|
||||
- SwiftGen (~> 6.6.2)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Kanna
|
||||
- Sourcery
|
||||
- SwiftGen
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234
|
||||
Sourcery: 179539341c2261068528cd15a31837b7238fd901
|
||||
SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c
|
||||
|
||||
PODFILE CHECKSUM: 597c21d7aa08efec996048577c3c4fbeffbb6305
|
||||
PODFILE CHECKSUM: ee2c03fbf7eb6e4ee75d97b1309e9ed6dfcf5cdd
|
||||
|
||||
COCOAPODS: 1.13.0
|
||||
|
|
Loading…
Reference in New Issue