From 42f63808df17c75be307fed7f62b0e9dd8882e94 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 27 Apr 2021 14:27:27 +0800 Subject: [PATCH 01/10] feature: add follow request notification --- Localization/app.json | 3 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Section/NotificationSection.swift | 1 + .../Mastodon+Entity+Notification+Type.swift | 6 +++ Mastodon/Generated/Strings.swift | 2 + .../Resources/en.lproj/Localizable.strings | 1 + ...otificationViewModel+LoadLatestState.swift | 2 +- .../NotificationTableViewCell.swift | 52 ++++++++++++++++--- 8 files changed, 58 insertions(+), 11 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 1f5ccade3..6526ca4b8 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -347,7 +347,8 @@ "favourite": "favorited your post", "reblog": "rebloged your post", "poll": "Your poll has ended", - "mention": "mentioned you" + "mention": "mentioned you", + "follow_request": "request to follow you" }, }, "thread": { diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 741947371..4c5c26898 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,7 +51,7 @@ "repositoryURL": "https://github.com/onevcat/Kingfisher.git", "state": { "branch": null, - "revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e", + "revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8", "version": "6.2.1" } }, diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 9c59350b4..57c755818 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -108,6 +108,7 @@ extension NotificationSection { if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) { cell.actionImageView.image = actionImage } + cell.buttonStackView.isHidden = (type != .followRequest) return cell } case .bottomLoader: diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift index 77a7b412e..2037f54a2 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift @@ -24,6 +24,8 @@ extension Mastodon.Entity.Notification.NotificationType { color = Asset.Colors.Notification.mention.color case .poll: color = Asset.Colors.brandBlue.color + case .followRequest: + color = Asset.Colors.brandBlue.color default: color = .clear } @@ -45,6 +47,8 @@ extension Mastodon.Entity.Notification.NotificationType { actionText = L10n.Scene.Notification.Action.mention case .poll: actionText = L10n.Scene.Notification.Action.poll + case .followRequest: + actionText = L10n.Scene.Notification.Action.followRequest default: actionText = "" } @@ -66,6 +70,8 @@ extension Mastodon.Entity.Notification.NotificationType { actionImageName = "at" case .poll: actionImageName = "list.bullet" + case .followRequest: + actionImageName = "person.crop.circle" default: actionImageName = "" } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 6d7af089d..0b657949f 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -371,6 +371,8 @@ internal enum L10n { internal static let favourite = L10n.tr("Localizable", "Scene.Notification.Action.Favourite") /// followed you internal static let follow = L10n.tr("Localizable", "Scene.Notification.Action.Follow") + /// request to follow you + internal static let followRequest = L10n.tr("Localizable", "Scene.Notification.Action.FollowRequest") /// mentioned you internal static let mention = L10n.tr("Localizable", "Scene.Notification.Action.Mention") /// Your poll has ended diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index ce7a3a2fe..e87982016 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -125,6 +125,7 @@ tap the link to confirm your account."; "Scene.HomeTimeline.Title" = "Home"; "Scene.Notification.Action.Favourite" = "favorited your post"; "Scene.Notification.Action.Follow" = "followed you"; +"Scene.Notification.Action.FollowRequest" = "request to follow you"; "Scene.Notification.Action.Mention" = "mentioned you"; "Scene.Notification.Action.Poll" = "Your poll has ended"; "Scene.Notification.Action.Reblog" = "rebloged your post"; diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift index 0e6b0d622..e2b04804d 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift @@ -53,7 +53,7 @@ extension NotificationViewModel.LoadLatestState { sinceID: nil, minID: nil, limit: nil, - excludeTypes: [.followRequest], + excludeTypes: [], accountID: nil ) viewModel.context.apiService.allNotifications( diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 619bffa17..809dc9b2b 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -21,6 +21,10 @@ protocol NotificationTableViewCellDelegate: AnyObject { func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) +// func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) +// +// func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, notification: MastodonNotification, denyButtonDidPressed button: UIButton) + } final class NotificationTableViewCell: UITableViewCell { @@ -76,6 +80,24 @@ final class NotificationTableViewCell: UITableViewCell { return label }() + let acceptButton: UIButton = { + let button = UIButton(type: .custom) + let actionImage = UIImage(systemName: "checkmark.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .semibold))?.withRenderingMode(.alwaysTemplate) + button.setImage(actionImage, for: .normal) + button.tintColor = Asset.Colors.Label.secondary.color + return button + }() + + let rejectButton: UIButton = { + let button = UIButton(type: .custom) + let actionImage = UIImage(systemName: "xmark.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .semibold))?.withRenderingMode(.alwaysTemplate) + button.setImage(actionImage, for: .normal) + button.tintColor = Asset.Colors.Label.secondary.color + return button + }() + + let buttonStackView = UIStackView() + override func prepareForReuse() { super.prepareForReuse() avatatImageView.af.cancelImageRequest() @@ -97,9 +119,8 @@ extension NotificationTableViewCell { func configure() { let containerStackView = UIStackView() - containerStackView.axis = .horizontal - containerStackView.alignment = .center - containerStackView.spacing = 4 + containerStackView.axis = .vertical + containerStackView.alignment = .fill containerStackView.layoutMargins = UIEdgeInsets(top: 14, left: 0, bottom: 12, right: 0) containerStackView.isLayoutMarginsRelativeArrangement = true containerStackView.translatesAutoresizingMaskIntoConstraints = false @@ -110,8 +131,13 @@ extension NotificationTableViewCell { contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor), ]) + + let horizontalStackView = UIStackView() + horizontalStackView.translatesAutoresizingMaskIntoConstraints = false + horizontalStackView.axis = .horizontal + horizontalStackView.spacing = 6 - containerStackView.addArrangedSubview(avatarContainer) + horizontalStackView.addArrangedSubview(avatarContainer) avatarContainer.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ avatarContainer.heightAnchor.constraint(equalToConstant: 47).priority(.required - 1), @@ -144,13 +170,23 @@ extension NotificationTableViewCell { ]) nameLabel.translatesAutoresizingMaskIntoConstraints = false - containerStackView.addArrangedSubview(nameLabel) + horizontalStackView.addArrangedSubview(nameLabel) actionLabel.translatesAutoresizingMaskIntoConstraints = false - containerStackView.addArrangedSubview(actionLabel) - nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + horizontalStackView.addArrangedSubview(actionLabel) + nameLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) + nameLabel.setContentHuggingPriority(.required - 1, for: .horizontal) actionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + containerStackView.addArrangedSubview(horizontalStackView) + + buttonStackView.translatesAutoresizingMaskIntoConstraints = false + buttonStackView.axis = .horizontal + buttonStackView.distribution = .fillEqually + acceptButton.translatesAutoresizingMaskIntoConstraints = false + denyButton.translatesAutoresizingMaskIntoConstraints = false + buttonStackView.addArrangedSubview(acceptButton) + buttonStackView.addArrangedSubview(rejectButton) + containerStackView.addArrangedSubview(buttonStackView) } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { From 124d4eef0a863ad37c9462e15a90b9090b0a6391 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 27 Apr 2021 14:43:38 +0800 Subject: [PATCH 02/10] feature: add followRequest API --- .../NotificationTableViewCell.swift | 2 +- .../Mastodon+API+Account+FollowRequest.swift | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index 809dc9b2b..dc5c4c19c 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -183,7 +183,7 @@ extension NotificationTableViewCell { buttonStackView.axis = .horizontal buttonStackView.distribution = .fillEqually acceptButton.translatesAutoresizingMaskIntoConstraints = false - denyButton.translatesAutoresizingMaskIntoConstraints = false + rejectButton.translatesAutoresizingMaskIntoConstraints = false buttonStackView.addArrangedSubview(acceptButton) buttonStackView.addArrangedSubview(rejectButton) containerStackView.addArrangedSubview(buttonStackView) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift new file mode 100644 index 000000000..447ce714f --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift @@ -0,0 +1,89 @@ +// +// Mastodon+API+Account+FollowRequest.swift +// +// +// Created by sxiaojian on 2021/4/27. +// + +import Foundation +import Combine + +// MARK: - Account credentials +extension Mastodon.API.Account { + + static func acceptFollowRequestEndpointURL(domain: String, userID: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests") + .appendingPathComponent(userID) + .appendingPathComponent("authorize") + } + + static func rejectFollowRequestEndpointURL(domain: String, userID: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests") + .appendingPathComponent(userID) + .appendingPathComponent("reject") + } + + /// Accept Follow + /// + /// + /// - Since: 0.0.0 + /// - Version: 3.0.0 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - userID: ID of the account in the database + /// - authorization: App token + /// - Returns: `AnyPublisher` contains `Account` nested in the response + public static func acceptFollowRequest( + session: URLSession, + domain: String, + userID: String, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.post( + url: acceptFollowRequestEndpointURL(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() + } + + /// Reject Follow + /// + /// + /// - Since: 0.0.0 + /// - Version: 3.0.0 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - userID: ID of the account in the database + /// - authorization: App token + /// - Returns: `AnyPublisher` contains `Account` nested in the response + public static func rejectFollowRequest( + session: URLSession, + domain: String, + userID: String, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.post( + url: rejectFollowRequestEndpointURL(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() + } +} From 381bf379267955976da26599a05a2c575842741b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 27 Apr 2021 15:33:47 +0800 Subject: [PATCH 03/10] fix: delete old notifications in CoreData --- Mastodon.xcodeproj/project.pbxproj | 4 + .../UserProvider/UserProviderFacade.swift | 3 +- .../SuggestionAccountViewModel.swift | 3 +- .../APIService/APIService+Follow.swift | 23 ++-- .../APIService/APIService+FollowRequest.swift | 105 ++++++++++++++++++ .../APIService/APIService+Notification.swift | 8 ++ .../Mastodon+API+Account+FollowRequest.swift | 12 +- 7 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 Mastodon/Service/APIService/APIService+FollowRequest.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index d9af89e1b..ad9f94dad 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */; }; 2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */; }; 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; }; + 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */; }; 2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; }; 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; }; 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; @@ -528,6 +529,7 @@ 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = ""; }; 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleView.swift; sourceTree = ""; }; 2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = ""; }; + 2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+FollowRequest.swift"; sourceTree = ""; }; 2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; 2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; @@ -1477,6 +1479,7 @@ 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */, DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */, DBAE3F932616E28B004B8251 /* APIService+Follow.swift */, + 2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */, DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */, DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */, 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */, @@ -2406,6 +2409,7 @@ DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */, DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */, DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, + 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, 2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */, 2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */, diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index b64bfe79d..b5f4dd32f 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -44,8 +44,7 @@ extension UserProviderFacade { return context.apiService.toggleFollow( for: mastodonUser, - activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, - needFeedback: true + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox ) } .switchToLatest() diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift index d5ef6f6c7..7a508fc75 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift @@ -188,8 +188,7 @@ final class SuggestionAccountViewModel: NSObject { let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser return context.apiService.toggleFollow( for: mastodonUser, - activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, - needFeedback: false + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox ) } diff --git a/Mastodon/Service/APIService/APIService+Follow.swift b/Mastodon/Service/APIService/APIService+Follow.swift index 6db612942..53634ab4b 100644 --- a/Mastodon/Service/APIService/APIService+Follow.swift +++ b/Mastodon/Service/APIService/APIService+Follow.swift @@ -24,15 +24,12 @@ extension APIService { /// - Returns: publisher for `Relationship` func toggleFollow( for mastodonUser: MastodonUser, - activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, - needFeedback: Bool + activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox ) -> AnyPublisher, Error> { - var impactFeedbackGenerator: UIImpactFeedbackGenerator? - var notificationFeedbackGenerator: UINotificationFeedbackGenerator? - if needFeedback { - impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) - notificationFeedbackGenerator = UINotificationFeedbackGenerator() - } + + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + let notificationFeedbackGenerator = UINotificationFeedbackGenerator() + return followUpdateLocal( mastodonUserObjectID: mastodonUser.objectID, @@ -40,9 +37,9 @@ extension APIService { ) .receive(on: DispatchQueue.main) .handleEvents { _ in - impactFeedbackGenerator?.prepare() + impactFeedbackGenerator.prepare() } receiveOutput: { _ in - impactFeedbackGenerator?.impactOccurred() + impactFeedbackGenerator.impactOccurred() } receiveCompletion: { completion in switch completion { case .failure(let error): @@ -79,13 +76,13 @@ extension APIService { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Friendship] rollback finish", ((#file as NSString).lastPathComponent), #line, #function) } receiveValue: { _ in // do nothing - notificationFeedbackGenerator?.prepare() - notificationFeedbackGenerator?.notificationOccurred(.error) + notificationFeedbackGenerator.prepare() + notificationFeedbackGenerator.notificationOccurred(.error) } .store(in: &self.disposeBag) case .finished: - notificationFeedbackGenerator?.notificationOccurred(.success) + notificationFeedbackGenerator.notificationOccurred(.success) os_log("%{public}s[%{public}ld], %{public}s: [Friendship] remote friendship update success", ((#file as NSString).lastPathComponent), #line, #function) } }) diff --git a/Mastodon/Service/APIService/APIService+FollowRequest.swift b/Mastodon/Service/APIService/APIService+FollowRequest.swift new file mode 100644 index 000000000..c40fcad52 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+FollowRequest.swift @@ -0,0 +1,105 @@ +// +// APIService+FollowRequest.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/27. +// + +import Foundation + +import UIKit +import Combine +import CoreData +import CoreDataStack +import CommonOSLog +import MastodonSDK + +extension APIService { + func acceptFollowRequest( + mastodonUserID: MastodonUser.ID, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let domain = mastodonAuthenticationBox.domain + let authorization = mastodonAuthenticationBox.userAuthorization + let requestMastodonUserID = mastodonAuthenticationBox.userID + + return Mastodon.API.Account.acceptFollowRequest( + session: session, + domain: domain, + userID: mastodonUserID, + authorization: authorization) + .flatMap { response -> AnyPublisher, 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 } + + let lookUpMastodonUserRequest = MastodonUser.sortedFetchRequest + lookUpMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: mastodonUserID) + lookUpMastodonUserRequest.fetchLimit = 1 + let lookUpMastodonuser = managedObjectContext.safeFetch(lookUpMastodonUserRequest).first + + if let lookUpMastodonuser = lookUpMastodonuser { + let entity = response.value + APIService.CoreData.update(user: lookUpMastodonuser, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) + } + } + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + func rejectFollowRequest( + mastodonUserID: MastodonUser.ID, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let domain = mastodonAuthenticationBox.domain + let authorization = mastodonAuthenticationBox.userAuthorization + let requestMastodonUserID = mastodonAuthenticationBox.userID + + return Mastodon.API.Account.rejectFollowRequest( + session: session, + domain: domain, + userID: mastodonUserID, + authorization: authorization) + .flatMap { response -> AnyPublisher, 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 } + + let lookUpMastodonUserRequest = MastodonUser.sortedFetchRequest + lookUpMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: mastodonUserID) + lookUpMastodonUserRequest.fetchLimit = 1 + let lookUpMastodonuser = managedObjectContext.safeFetch(lookUpMastodonUserRequest).first + + if let lookUpMastodonuser = lookUpMastodonuser { + let entity = response.value + APIService.CoreData.update(user: lookUpMastodonuser, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) + } + } + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +} diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index ee8f5186c..a27aae2ae 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -28,6 +28,14 @@ extension APIService { ) .flatMap { response -> AnyPublisher, Error> in let log = OSLog.api + if query.maxID == nil { + let requestMastodonNotificationRequest = MastodonNotification.sortedFetchRequest + requestMastodonNotificationRequest.predicate = MastodonNotification.predicate(domain: domain, userID: userID) + let oldNotifications = self.backgroundManagedObjectContext.safeFetch(requestMastodonNotificationRequest) + oldNotifications.forEach { notification in + self.backgroundManagedObjectContext.delete(notification) + } + } return self.backgroundManagedObjectContext.performChanges { response.value.forEach { notification in let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift index 447ce714f..f08e888b5 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift @@ -35,13 +35,13 @@ extension Mastodon.API.Account { /// - domain: Mastodon instance domain. e.g. "example.com" /// - userID: ID of the account in the database /// - authorization: App token - /// - Returns: `AnyPublisher` contains `Account` nested in the response + /// - Returns: `AnyPublisher` contains `Relationship` nested in the response public static func acceptFollowRequest( session: URLSession, domain: String, userID: String, authorization: Mastodon.API.OAuth.Authorization - ) -> AnyPublisher, Error> { + ) -> AnyPublisher, Error> { let request = Mastodon.API.post( url: acceptFollowRequestEndpointURL(domain: domain, userID: userID), query: nil, @@ -49,7 +49,7 @@ extension Mastodon.API.Account { ) return session.dataTaskPublisher(for: request) .tryMap { data, response in - let value = try Mastodon.API.decode(type: Mastodon.Entity.Account.self, from: data, response: response) + let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } .eraseToAnyPublisher() @@ -67,13 +67,13 @@ extension Mastodon.API.Account { /// - domain: Mastodon instance domain. e.g. "example.com" /// - userID: ID of the account in the database /// - authorization: App token - /// - Returns: `AnyPublisher` contains `Account` nested in the response + /// - Returns: `AnyPublisher` contains `Relationship` nested in the response public static func rejectFollowRequest( session: URLSession, domain: String, userID: String, authorization: Mastodon.API.OAuth.Authorization - ) -> AnyPublisher, Error> { + ) -> AnyPublisher, Error> { let request = Mastodon.API.post( url: rejectFollowRequestEndpointURL(domain: domain, userID: userID), query: nil, @@ -81,7 +81,7 @@ extension Mastodon.API.Account { ) return session.dataTaskPublisher(for: request) .tryMap { data, response in - let value = try Mastodon.API.decode(type: Mastodon.Entity.Account.self, from: data, response: response) + let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } .eraseToAnyPublisher() From d03346c0de35bd4c97441fd209c76a2b4be58c55 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 27 Apr 2021 16:54:23 +0800 Subject: [PATCH 04/10] fix: add followrequest cell action --- .../Section/NotificationSection.swift | 12 +++++++ .../NotificationViewController.swift | 8 +++++ .../Notification/NotificationViewModel.swift | 33 +++++++++++++++++++ .../NotificationTableViewCell.swift | 6 ++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index 57c755818..d2df0a78b 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -91,6 +91,18 @@ extension NotificationSection { cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) + cell.acceptButton.publisher(for: .touchUpInside) + .sink { [weak cell] _ in + guard let cell = cell else { return } + cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton) + } + .store(in: &cell.disposeBag) + cell.rejectButton.publisher(for: .touchUpInside) + .sink { [weak cell] _ in + guard let cell = cell else { return } + cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.acceptButton) + } + .store(in: &cell.disposeBag) cell.actionImageBackground.backgroundColor = color cell.actionLabel.text = actionText + " · " + timeText cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 57b5dc639..f3b143f52 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -205,6 +205,14 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl // MARK: - NotificationTableViewCellDelegate extension NotificationViewController: NotificationTableViewCellDelegate { + func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) { + viewModel.acceptFollowRequest(notification: notification) + } + + func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, rejectButtonDidPressed button: UIButton) { + viewModel.rejectFollowRequest(notification: notification) + } + func userAvatarDidPressed(notification: MastodonNotification) { let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account) DispatchQueue.main.async { diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index e026af732..f60e3d76d 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -12,6 +12,7 @@ import Foundation import GameplayKit import MastodonSDK import UIKit +import OSLog final class NotificationViewModel: NSObject { var disposeBag = Set() @@ -120,6 +121,38 @@ final class NotificationViewModel: NSObject { } .store(in: &disposeBag) } + + func acceptFollowRequest(notification: MastodonNotification) { + guard let activeMastodonAuthenticationBox = self.activeMastodonAuthenticationBox.value else { return } + context.apiService.acceptFollowRequest(mastodonUserID: notification.account.id, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { [weak self] completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: accept FollowRequest fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + self?.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) + } + } receiveValue: { _ in + + } + .store(in: &disposeBag) + } + + func rejectFollowRequest(notification: MastodonNotification) { + guard let activeMastodonAuthenticationBox = self.activeMastodonAuthenticationBox.value else { return } + context.apiService.acceptFollowRequest(mastodonUserID: notification.account.id, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + .sink { [weak self] completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: reject FollowRequest fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + self?.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) + } + } receiveValue: { _ in + + } + .store(in: &disposeBag) + } } extension NotificationViewModel { diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift index dc5c4c19c..c049b961e 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationTableViewCell.swift @@ -21,9 +21,9 @@ protocol NotificationTableViewCellDelegate: AnyObject { func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) -// func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) -// -// func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, notification: MastodonNotification, denyButtonDidPressed button: UIButton) + func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) + + func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, rejectButtonDidPressed button: UIButton) } From 193b69b6b10338ae367e16a781cd6dfa4683c75b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 27 Apr 2021 19:41:55 +0800 Subject: [PATCH 05/10] fix: change accept to reject --- Mastodon/Diffiable/Section/NotificationSection.swift | 2 +- Mastodon/Scene/Notification/NotificationViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index d2df0a78b..ead5d48f8 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -100,7 +100,7 @@ extension NotificationSection { cell.rejectButton.publisher(for: .touchUpInside) .sink { [weak cell] _ in guard let cell = cell else { return } - cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.acceptButton) + cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton) } .store(in: &cell.disposeBag) cell.actionImageBackground.backgroundColor = color diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index f60e3d76d..f535c5598 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -140,7 +140,7 @@ final class NotificationViewModel: NSObject { func rejectFollowRequest(notification: MastodonNotification) { guard let activeMastodonAuthenticationBox = self.activeMastodonAuthenticationBox.value else { return } - context.apiService.acceptFollowRequest(mastodonUserID: notification.account.id, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + context.apiService.rejectFollowRequest(mastodonUserID: notification.account.id, mastodonAuthenticationBox: activeMastodonAuthenticationBox) .sink { [weak self] completion in switch completion { case .failure(let error): From a9fdd2efa3f6beb47258906dde41c48037923dac Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 2 Mar 2021 13:27:53 +0800 Subject: [PATCH 06/10] fix: acct lookup support --- .../MastodonPickServerViewController.swift | 1 + .../Register/MastodonRegisterViewModel.swift | 35 +++++++++++++ .../MastodonServerRulesViewController.swift | 2 +- .../APIService/APIService+Account.swift | 13 +++++ .../API/Mastodon+API+Account.swift | 51 +++++++++++++++++++ 5 files changed, 101 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 685709719..638734c11 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -336,6 +336,7 @@ extension MastodonPickServerViewController { } else { let mastodonRegisterViewModel = MastodonRegisterViewModel( domain: server.domain, + context: self.context, authenticateInfo: response.authenticateInfo, instance: response.instance.value, applicationToken: response.applicationToken.value diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index cd6106c23..919443f2d 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -18,6 +18,7 @@ final class MastodonRegisterViewModel { let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let instance: Mastodon.Entity.Instance let applicationToken: Mastodon.Entity.Token + let context: AppContext let username = CurrentValueSubject("") let displayName = CurrentValueSubject("") @@ -46,11 +47,13 @@ final class MastodonRegisterViewModel { init( domain: String, + context: AppContext, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, instance: Mastodon.Entity.Instance, applicationToken: Mastodon.Entity.Token ) { self.domain = domain + self.context = context self.authenticateInfo = authenticateInfo self.instance = instance self.applicationToken = applicationToken @@ -78,6 +81,21 @@ final class MastodonRegisterViewModel { } .assign(to: \.value, on: usernameValidateState) .store(in: &disposeBag) + + username.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates() + .sink { [weak self] text in + self?.lookupAccount(by: text) + } + .store(in: &disposeBag) + + usernameValidateState + .sink { [weak self] validateState in + if validateState == .valid { + self?.usernameErrorPrompt.value = nil + } + } + .store(in: &disposeBag) + displayName .map { displayname in guard !displayname.isEmpty else { return .empty } @@ -145,6 +163,23 @@ final class MastodonRegisterViewModel { .assign(to: \.value, on: isAllValid) .store(in: &disposeBag) } + + func lookupAccount(by acct: String) { + if acct.isEmpty { + return + } + let query = Mastodon.API.Account.AccountLookupQuery(acct: acct) + context.apiService.accountLookup(domain: domain, query: query, authorization: applicationAuthorization) + .sink { _ in + + } receiveValue: { [weak self] account in + guard let self = self else { return } + let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) + self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text) + } + .store(in: &disposeBag) + + } } extension MastodonRegisterViewModel { diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index fb86e81e1..447896c22 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -204,7 +204,7 @@ extension MastodonServerRulesViewController { @objc private func confirmButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) + let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain,context: self.context, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } } diff --git a/Mastodon/Service/APIService/APIService+Account.swift b/Mastodon/Service/APIService/APIService+Account.swift index 04908514b..7638f2444 100644 --- a/Mastodon/Service/APIService/APIService+Account.swift +++ b/Mastodon/Service/APIService/APIService+Account.swift @@ -152,4 +152,17 @@ extension APIService { ) } + func accountLookup( + domain: String, + query: Mastodon.API.Account.AccountLookupQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + return Mastodon.API.Account.lookupAccount( + session: session, + domain: domain, + query: query, + authorization: authorization + ) + } + } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index 0f98dbe05..78f338b6f 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -132,3 +132,54 @@ extension Mastodon.API.Account { } } + +extension Mastodon.API.Account { + static func accountsLookupEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/lookup") + } + + public struct AccountLookupQuery: GetQuery { + + public var acct: String + + public init(acct: String) { + self.acct = acct + } + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + items.append(URLQueryItem(name: "acct", value: acct)) + return items + } + } + + /// lookup account by acct. + /// + /// - Version: 3.3.1 + + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `AccountInfoQuery` with account query information, + /// - authorization: user token + /// - Returns: `AnyPublisher` contains `Account` nested in the response + public static func lookupAccount( + session: URLSession, + domain: String, + query: AccountLookupQuery, + authorization: Mastodon.API.OAuth.Authorization? + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: accountsLookupEndpointURL(domain: domain), + query: query, + 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() + } + +} From 9768721247437a9780b6c6e944cce70fc0373c56 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 29 Apr 2021 16:20:18 +0800 Subject: [PATCH 07/10] fix: the Core Data thread-safe issue --- .../APIService/APIService+Notification.swift | 16 ++++++++-------- .../API/Mastodon+API+Account+FollowRequest.swift | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift index a27aae2ae..6e8af70bc 100644 --- a/Mastodon/Service/APIService/APIService+Notification.swift +++ b/Mastodon/Service/APIService/APIService+Notification.swift @@ -28,15 +28,15 @@ extension APIService { ) .flatMap { response -> AnyPublisher, Error> in let log = OSLog.api - if query.maxID == nil { - let requestMastodonNotificationRequest = MastodonNotification.sortedFetchRequest - requestMastodonNotificationRequest.predicate = MastodonNotification.predicate(domain: domain, userID: userID) - let oldNotifications = self.backgroundManagedObjectContext.safeFetch(requestMastodonNotificationRequest) - oldNotifications.forEach { notification in - self.backgroundManagedObjectContext.delete(notification) - } - } return self.backgroundManagedObjectContext.performChanges { + if query.maxID == nil { + let requestMastodonNotificationRequest = MastodonNotification.sortedFetchRequest + requestMastodonNotificationRequest.predicate = MastodonNotification.predicate(domain: domain, userID: userID) + let oldNotifications = self.backgroundManagedObjectContext.safeFetch(requestMastodonNotificationRequest) + oldNotifications.forEach { notification in + self.backgroundManagedObjectContext.delete(notification) + } + } response.value.forEach { notification in let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log) var status: Status? diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift index f08e888b5..004197143 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift @@ -11,13 +11,13 @@ import Combine // MARK: - Account credentials extension Mastodon.API.Account { - static func acceptFollowRequestEndpointURL(domain: String, userID: String) -> URL { + static func acceptFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests") .appendingPathComponent(userID) .appendingPathComponent("authorize") } - static func rejectFollowRequestEndpointURL(domain: String, userID: String) -> URL { + static func rejectFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests") .appendingPathComponent(userID) .appendingPathComponent("reject") @@ -34,12 +34,12 @@ extension Mastodon.API.Account { /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - userID: ID of the account in the database - /// - authorization: App token + /// - authorization: User token /// - Returns: `AnyPublisher` contains `Relationship` nested in the response public static func acceptFollowRequest( session: URLSession, domain: String, - userID: String, + userID: Mastodon.Entity.Account.ID, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { let request = Mastodon.API.post( @@ -66,12 +66,12 @@ extension Mastodon.API.Account { /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - userID: ID of the account in the database - /// - authorization: App token + /// - authorization: User token /// - Returns: `AnyPublisher` contains `Relationship` nested in the response public static func rejectFollowRequest( session: URLSession, domain: String, - userID: String, + userID: Mastodon.Entity.Account.ID, authorization: Mastodon.API.OAuth.Authorization ) -> AnyPublisher, Error> { let request = Mastodon.API.post( From 40e62a8a439c407d166167741913734f276bd817 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 29 Apr 2021 17:02:46 +0800 Subject: [PATCH 08/10] fix: change version of followRequest --- .../MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift index 004197143..87c879ea0 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift @@ -27,7 +27,7 @@ extension Mastodon.API.Account { /// /// /// - Since: 0.0.0 - /// - Version: 3.0.0 + /// - Version: 3.3.0 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/) /// - Parameters: @@ -59,7 +59,7 @@ extension Mastodon.API.Account { /// /// /// - Since: 0.0.0 - /// - Version: 3.0.0 + /// - Version: 3.3.0 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/accounts/follow_requests/) /// - Parameters: From 1e5daf5a7714c4a85aa8e4807fa3dfb26ebcda3f Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 29 Apr 2021 16:12:04 +0800 Subject: [PATCH 09/10] fix: the race-condition issue in username checking --- .../Register/MastodonRegisterViewModel.swift | 56 +++++++++++-------- .../MastodonServerRulesViewController.swift | 2 +- .../API/Mastodon+API+Account.swift | 2 +- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 919443f2d..5fd8c31b6 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -82,10 +82,36 @@ final class MastodonRegisterViewModel { .assign(to: \.value, on: usernameValidateState) .store(in: &disposeBag) - username.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates() - .sink { [weak self] text in - self?.lookupAccount(by: text) + username + .filter { !$0.isEmpty } + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) + .removeDuplicates() + .compactMap { [weak self] text -> AnyPublisher, Error>, Never>? in + guard let self = self else { return nil } + let query = Mastodon.API.Account.AccountLookupQuery(acct: text) + return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization) + .map { + response -> Result, Error>in + Result.success(response) + } + .catch { error in + Just(Result.failure(error)) + } + .eraseToAnyPublisher() } + .switchToLatest() + .sink(receiveCompletion: { _ in + + }, receiveValue: { [weak self] result in + guard let self = self else { return } + switch result { + case .success: + let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) + self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text) + case .failure: + break + } + }) .store(in: &disposeBag) usernameValidateState @@ -133,7 +159,8 @@ final class MastodonRegisterViewModel { let error = error as? Mastodon.API.Error let mastodonError = error?.mastodonError if case let .generic(genericMastodonError) = mastodonError, - let details = genericMastodonError.details { + let details = genericMastodonError.details + { self.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } self.emailErrorPrompt.value = details.emailErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) } @@ -157,29 +184,12 @@ final class MastodonRegisterViewModel { Publishers.CombineLatest( publisherOne, - approvalRequired ? reasonValidateState.map {$0 == .valid}.eraseToAnyPublisher() : Just(true).eraseToAnyPublisher() + approvalRequired ? reasonValidateState.map { $0 == .valid }.eraseToAnyPublisher() : Just(true).eraseToAnyPublisher() ) .map { $0 && $1 } .assign(to: \.value, on: isAllValid) .store(in: &disposeBag) } - - func lookupAccount(by acct: String) { - if acct.isEmpty { - return - } - let query = Mastodon.API.Account.AccountLookupQuery(acct: acct) - context.apiService.accountLookup(domain: domain, query: query, authorization: applicationAuthorization) - .sink { _ in - - } receiveValue: { [weak self] account in - guard let self = self else { return } - let text = L10n.Scene.Register.Error.Reason.taken(L10n.Scene.Register.Error.Item.username) - self.usernameErrorPrompt.value = MastodonRegisterViewModel.errorPromptAttributedString(for: text) - } - .store(in: &disposeBag) - - } } extension MastodonRegisterViewModel { @@ -191,7 +201,6 @@ extension MastodonRegisterViewModel { } extension MastodonRegisterViewModel { - static func isValidEmail(_ email: String) -> Bool { let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" @@ -241,5 +250,4 @@ extension MastodonRegisterViewModel { return attributeString } - } diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index 447896c22..d8638421a 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -204,7 +204,7 @@ extension MastodonServerRulesViewController { @objc private func confirmButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain,context: self.context, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) + let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain, context: self.context, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index 78f338b6f..d1c5458c4 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -161,7 +161,7 @@ extension Mastodon.API.Account { /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `AccountInfoQuery` with account query information, - /// - authorization: user token + /// - authorization: app token /// - Returns: `AnyPublisher` contains `Account` nested in the response public static func lookupAccount( session: URLSession, From ca320c555aa22d7322ffd35304c85ddbf7d32f91 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Thu, 29 Apr 2021 19:24:03 +0800 Subject: [PATCH 10/10] fix: code format --- .../Onboarding/Register/MastodonRegisterViewModel.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 5fd8c31b6..309204a9a 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -91,7 +91,7 @@ final class MastodonRegisterViewModel { let query = Mastodon.API.Account.AccountLookupQuery(acct: text) return context.apiService.accountLookup(domain: domain, query: query, authorization: self.applicationAuthorization) .map { - response -> Result, Error>in + response -> Result, Error> in Result.success(response) } .catch { error in @@ -100,9 +100,7 @@ final class MastodonRegisterViewModel { .eraseToAnyPublisher() } .switchToLatest() - .sink(receiveCompletion: { _ in - - }, receiveValue: { [weak self] result in + .sink { [weak self] result in guard let self = self else { return } switch result { case .success: @@ -111,7 +109,7 @@ final class MastodonRegisterViewModel { case .failure: break } - }) + } .store(in: &disposeBag) usernameValidateState