From bcfdaf2ca7940a65fd4024a4c9d60845d834ac55 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 28 Jun 2022 19:00:39 +0800 Subject: [PATCH] feat: add interaction for follow request notification --- .../xcschemes/xcschememanagement.plist | 6 +- .../Provider/DataSourceFacade+Block.swift | 2 +- .../Provider/DataSourceFacade+Favorite.swift | 2 +- .../Provider/DataSourceFacade+Follow.swift | 33 ++++- .../Provider/DataSourceFacade+Mute.swift | 2 +- .../Provider/DataSourceFacade+Reblog.swift | 2 +- ...er+NotificationTableViewCellDelegate.swift | 62 +++++++++ .../NotificationTableViewCellDelegate.swift | 10 ++ .../NotificationView+Configuration.swift | 23 ++-- .../APIService/APIService+FollowRequest.swift | 123 ++++++------------ .../Transient/MastodonNotificationType.swift | 4 +- .../Assets.xcassets/Editing/Contents.json | 9 ++ .../Editing/checkmark.imageset/Contents.json | 15 +++ .../Editing/checkmark.imageset/checkmark.pdf | 76 +++++++++++ .../Editing/xmark.imageset/Contents.json | 15 +++ .../Editing/xmark.imageset/xmark.pdf | 89 +++++++++++++ .../MastodonAsset/Generated/Assets.swift | 4 + .../Mastodon+API+Account+FollowRequest.swift | 34 +++++ .../View/Content/NotificationView.swift | 99 ++++++++++++++ 19 files changed, 504 insertions(+), 106 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/checkmark.pdf create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/xmark.pdf diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 02dc81778..1399af14d 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 34 + 24 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 32 + 23 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 33 + 22 SuppressBuildableAutocreation diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index a1bf3136f..e9a0b02c0 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -14,7 +14,7 @@ extension DataSourceFacade { user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleBlock( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift index a248ed42c..fba4f697b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -15,7 +15,7 @@ extension DataSourceFacade { status: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await provider.context.apiService.favorite( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index b4f2362c3..83c5b41ba 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -7,6 +7,8 @@ import UIKit import CoreDataStack +import class CoreDataStack.Notification +import MastodonSDK extension DataSourceFacade { static func responseToUserFollowAction( @@ -14,7 +16,7 @@ extension DataSourceFacade { user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleFollow( @@ -23,3 +25,32 @@ extension DataSourceFacade { ) } // end func } + +extension DataSourceFacade { + static func responseToUserFollowRequestAction( + dependency: NeedsDependency, + notification: ManagedObjectRecord, + query: Mastodon.API.Account.FollowReqeustQuery, + authenticationBox: MastodonAuthenticationBox + ) async throws { + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() + await selectionFeedbackGenerator.selectionChanged() + + let managedObjectContext = dependency.context.managedObjectContext + let _userID: MastodonUser.ID? = try await managedObjectContext.perform { + guard let notification = notification.object(in: managedObjectContext) else { return nil } + return notification.account.id + } + + guard let userID = _userID else { + assertionFailure() + throw APIService.APIError.implicit(.badRequest) + } + + _ = try await dependency.context.apiService.followRequest( + userID: userID, + query: query, + authenticationBox: authenticationBox + ) + } // end func +} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 421d5046c..b5b2dec97 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -14,7 +14,7 @@ extension DataSourceFacade { user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleMute( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index 359b285d4..7eda84599 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -15,7 +15,7 @@ extension DataSourceFacade { status: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await provider.context.apiService.reblog( diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index ca7fdeb18..f525f7be1 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -87,6 +87,68 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider { } } +extension NotificationTableViewCellDelegate where Self: DataSourceProvider { + + func tableViewCell( + _ cell: UITableViewCell, + notificationView: NotificationView, + acceptFollowRequestButtonDidPressed button: UIButton + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .notification(notification) = item else { + assertionFailure("only works for status data provider") + return + } + + guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + + try await DataSourceFacade.responseToUserFollowRequestAction( + dependency: self, + notification: notification, + query: .accept, + authenticationBox: authenticationBox + ) + } // end Task + } + + func tableViewCell( + _ cell: UITableViewCell, + notificationView: NotificationView, + rejectFollowRequestButtonDidPressed button: UIButton + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .notification(notification) = item else { + assertionFailure("only works for status data provider") + return + } + + guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + + try await DataSourceFacade.responseToUserFollowRequestAction( + dependency: self, + notification: notification, + query: .reject, + authenticationBox: authenticationBox + ) + } // end Task + } + +} + // MARK: - Status Content extension NotificationTableViewCellDelegate where Self: DataSourceProvider { func tableViewCell( diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift index d13ce7195..7a603d5f0 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCellDelegate.swift @@ -25,6 +25,8 @@ protocol NotificationTableViewCellDelegate: AnyObject, AutoGenerateProtocolDeleg // sourcery:inline:NotificationTableViewCellDelegate.AutoGenerateProtocolDelegate func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, authorAvatarButtonDidPressed button: AvatarButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action) + func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, acceptFollowRequestButtonDidPressed button: UIButton) + func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, rejectFollowRequestButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) @@ -49,6 +51,14 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie delegate?.tableViewCell(self, notificationView: notificationView, menuButton: button, didSelectAction: action) } + func notificationView(_ notificationView: NotificationView, acceptFollowRequestButtonDidPressed button: UIButton) { + delegate?.tableViewCell(self, notificationView: notificationView, acceptFollowRequestButtonDidPressed: button) + } + + func notificationView(_ notificationView: NotificationView, rejectFollowRequestButtonDidPressed button: UIButton) { + delegate?.tableViewCell(self, notificationView: notificationView, rejectFollowRequestButtonDidPressed: button) + } + func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) { delegate?.tableViewCell(self, notificationView: notificationView, statusView: statusView, metaText: metaText, didSelectMeta: meta) } diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index 052dc44c2..ff2451505 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -36,23 +36,26 @@ extension NotificationView { return } - if let status = notification.status { - switch type { - case .follow, .followRequest: - setAuthorContainerBottomPaddingViewDisplay() - case .mention, .status: + switch type { + case .follow: + setAuthorContainerBottomPaddingViewDisplay() + case .followRequest: + setFollowRequestAdaptiveMarginContainerViewDisplay() + case .mention, .status: + if let status = notification.status { statusView.configure(status: status) setStatusViewDisplay() - case .reblog, .favourite, .poll: + } + case .reblog, .favourite, .poll: + if let status = notification.status { quoteStatusView.configure(status: status) setQuoteStatusViewDisplay() - case ._other: - setAuthorContainerBottomPaddingViewDisplay() - assertionFailure() } - } else { + case ._other: setAuthorContainerBottomPaddingViewDisplay() + assertionFailure() } + } } diff --git a/Mastodon/Service/APIService/APIService+FollowRequest.swift b/Mastodon/Service/APIService/APIService+FollowRequest.swift index b2029f3db..5c91d282c 100644 --- a/Mastodon/Service/APIService/APIService+FollowRequest.swift +++ b/Mastodon/Service/APIService/APIService+FollowRequest.swift @@ -15,91 +15,42 @@ import CommonOSLog import MastodonSDK extension APIService { -// func acceptFollowRequest( -// mastodonUserID: MastodonUser.ID, -// mastodonAuthenticationBox: 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: 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() -// } + func followRequest( + userID: Mastodon.Entity.Account.ID, + query: Mastodon.API.Account.FollowReqeustQuery, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + let response = try await Mastodon.API.Account.followRequest( + session: session, + domain: authenticationBox.domain, + userID: userID, + query: query, + authorization: authenticationBox.userAuthorization + ).singleOutput() + + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate( + domain: authenticationBox.domain, + id: authenticationBox.userID + ) + request.fetchLimit = 1 + guard let user = managedObjectContext.safeFetch(request).first else { return } + guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return } + + Persistence.MastodonUser.update( + mastodonUser: user, + context: Persistence.MastodonUser.RelationshipContext( + entity: response.value, + me: me, + networkDate: response.networkDate + ) + ) + } + + return response + } + } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift index a982fda93..9e3029f26 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift @@ -21,7 +21,7 @@ public enum MastodonNotificationType: RawRepresentable { public init?(rawValue: String) { switch rawValue { case "follow": self = .follow - case "followRequest": self = .followRequest + case "follow_request": self = .followRequest case "mention": self = .mention case "reblog": self = .reblog case "favourite": self = .favourite @@ -34,7 +34,7 @@ public enum MastodonNotificationType: RawRepresentable { public var rawValue: String { switch self { case .follow: return "follow" - case .followRequest: return "followRequest" + case .followRequest: return "follow_request" case .mention: return "mention" case .reblog: return "reblog" case .favourite: return "favourite" diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/Contents.json new file mode 100644 index 000000000..8879a2751 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "checkmark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/checkmark.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/checkmark.pdf new file mode 100644 index 000000000..c481ec4c5 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/checkmark.imageset/checkmark.pdf @@ -0,0 +1,76 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.250000 5.105469 cm +0.129412 0.129412 0.129412 scn +1.280330 5.924861 m +0.987437 6.217754 0.512563 6.217754 0.219670 5.924861 c +-0.073223 5.631968 -0.073223 5.157095 0.219670 4.864202 c +4.719670 0.364201 l +5.012563 0.071307 5.487436 0.071307 5.780330 0.364201 c +16.780331 11.364201 l +17.073223 11.657094 17.073223 12.131968 16.780331 12.424861 c +16.487438 12.717754 16.012562 12.717754 15.719669 12.424861 c +5.250000 1.955191 l +1.280330 5.924861 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 523 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000613 00000 n +0000000635 00000 n +0000000808 00000 n +0000000882 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +941 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/Contents.json new file mode 100644 index 000000000..df9d264a2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "xmark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/xmark.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/xmark.pdf new file mode 100644 index 000000000..6558a81f7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Editing/xmark.imageset/xmark.pdf @@ -0,0 +1,89 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.250000 4.105469 cm +0.129412 0.129412 0.129412 scn +0.147052 15.340743 m +0.219670 15.424861 l +0.485936 15.691128 0.902600 15.715334 1.196212 15.497479 c +1.280330 15.424861 l +7.750000 8.955531 l +14.219669 15.424861 l +14.512563 15.717754 14.987437 15.717754 15.280331 15.424861 c +15.573224 15.131968 15.573224 14.657094 15.280331 14.364201 c +8.811000 7.894531 l +15.280331 1.424862 l +15.546597 1.158595 15.570804 0.741932 15.352949 0.448320 c +15.280331 0.364201 l +15.014064 0.097934 14.597401 0.073728 14.303789 0.291582 c +14.219669 0.364201 l +7.750000 6.833531 l +1.280330 0.364201 l +0.987437 0.071307 0.512563 0.071307 0.219670 0.364201 c +-0.073223 0.657094 -0.073223 1.131968 0.219670 1.424862 c +6.689000 7.894531 l +0.219670 14.364201 l +-0.046597 14.630467 -0.070803 15.047132 0.147052 15.340743 c +0.219670 15.424861 l +0.147052 15.340743 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 914 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001004 00000 n +0000001026 00000 n +0000001199 00000 n +0000001273 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1332 +%%EOF \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index b594f9209..893f5db98 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -94,6 +94,10 @@ public enum Asset { public enum Connectivity { public static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split") } + public enum Editing { + public static let checkmark = ImageAsset(name: "Editing/checkmark") + public static let xmark = ImageAsset(name: "Editing/xmark") + } public enum Human { public static let eyeCircleFill = ImageAsset(name: "Human/eye.circle.fill") public static let eyeSlashCircleFill = ImageAsset(name: "Human/eye.slash.circle.fill") diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift index 7adbcdeff..ddde4ffdc 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift @@ -89,3 +89,37 @@ extension Mastodon.API.Account { .eraseToAnyPublisher() } } + +extension Mastodon.API.Account { + + public enum FollowReqeustQuery { + case accept + case reject + } + + public static func followRequest( + session: URLSession, + domain: String, + userID: Mastodon.Entity.Account.ID, + query: FollowReqeustQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + switch query { + case .accept: + return acceptFollowRequest( + session: session, + domain: domain, + userID: userID, + authorization: authorization + ) + case .reject: + return rejectFollowRequest( + session: session, + domain: domain, + userID: userID, + authorization: authorization + ) + } // end switch + } + +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 8714c7cd0..6cef7cfe3 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -17,6 +17,9 @@ public protocol NotificationViewDelegate: AnyObject { func notificationView(_ notificationView: NotificationView, authorAvatarButtonDidPressed button: AvatarButton) func notificationView(_ notificationView: NotificationView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action) + func notificationView(_ notificationView: NotificationView, acceptFollowRequestButtonDidPressed button: UIButton) + func notificationView(_ notificationView: NotificationView, rejectFollowRequestButtonDidPressed button: UIButton) + func notificationView(_ notificationView: NotificationView, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) func notificationView(_ notificationView: NotificationView, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView) func notificationView(_ notificationView: NotificationView, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaView: MediaView, didSelectMediaViewAt index: Int) @@ -101,6 +104,44 @@ public final class NotificationView: UIView { // notification type indicator imageView public let notificationTypeIndicatorLabel = MetaLabel(style: .notificationTitle) + // follow request + let followRequestAdaptiveMarginContainerView = AdaptiveMarginContainerView() + let followRequestContainerView = UIView() + + let acceptFollowRequestButtonShadowBackgroundContainer = ShadowBackgroundContainer() + private(set) lazy var acceptFollowRequestButton: UIButton = { + let button = UIButton() + button.setImage(Asset.Editing.checkmark.image.withRenderingMode(.alwaysTemplate), for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.setBackgroundImage(.placeholder(color: .systemGreen), for: .normal) + button.tintColor = .white + button.layer.masksToBounds = true + button.layer.cornerCurve = .continuous + button.layer.cornerRadius = 4 + acceptFollowRequestButtonShadowBackgroundContainer.cornerRadius = 4 + acceptFollowRequestButtonShadowBackgroundContainer.shadowAlpha = 0.1 + button.addTarget(self, action: #selector(NotificationView.acceptFollowRequestButtonDidPressed(_:)), for: .touchUpInside) + return button + }() + + let rejectFollowRequestButtonShadowBackgroundContainer = ShadowBackgroundContainer() + private(set) lazy var rejectFollowRequestButton: UIButton = { + let button = UIButton() + button.setImage(Asset.Editing.xmark.image.withRenderingMode(.alwaysTemplate), for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.imageEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) // tweak xmark size + button.setBackgroundImage(.placeholder(color: .systemRed), for: .normal) + button.tintColor = .white + button.layer.masksToBounds = true + button.layer.cornerCurve = .continuous + button.layer.cornerRadius = 4 + rejectFollowRequestButtonShadowBackgroundContainer.cornerRadius = 4 + rejectFollowRequestButtonShadowBackgroundContainer.shadowAlpha = 0.1 + button.addTarget(self, action: #selector(NotificationView.rejectFollowRequestButtonDidPressed(_:)), for: .touchUpInside) + return button + }() + + // status public let statusView = StatusView() public let quoteStatusViewContainerView = UIView() @@ -115,6 +156,8 @@ public final class NotificationView: UIView { authorContainerViewBottomPaddingView.isHidden = true + followRequestAdaptiveMarginContainerView.isHidden = true + statusView.isHidden = true statusView.prepareForReuse() @@ -222,6 +265,46 @@ extension NotificationView { ]) authorContainerViewBottomPaddingView.isHidden = true + // follow reqeust + followRequestAdaptiveMarginContainerView.contentView = followRequestContainerView + followRequestAdaptiveMarginContainerView.margin = StatusView.containerLayoutMargin + containerStackView.addArrangedSubview(followRequestAdaptiveMarginContainerView) + + acceptFollowRequestButton.translatesAutoresizingMaskIntoConstraints = false + acceptFollowRequestButtonShadowBackgroundContainer.addSubview(acceptFollowRequestButton) + NSLayoutConstraint.activate([ + acceptFollowRequestButton.topAnchor.constraint(equalTo: acceptFollowRequestButtonShadowBackgroundContainer.topAnchor), + acceptFollowRequestButton.leadingAnchor.constraint(equalTo: acceptFollowRequestButtonShadowBackgroundContainer.leadingAnchor), + acceptFollowRequestButton.trailingAnchor.constraint(equalTo: acceptFollowRequestButtonShadowBackgroundContainer.trailingAnchor), + acceptFollowRequestButton.bottomAnchor.constraint(equalTo: acceptFollowRequestButtonShadowBackgroundContainer.bottomAnchor), + ]) + + rejectFollowRequestButton.translatesAutoresizingMaskIntoConstraints = false + rejectFollowRequestButtonShadowBackgroundContainer.addSubview(rejectFollowRequestButton) + NSLayoutConstraint.activate([ + rejectFollowRequestButton.topAnchor.constraint(equalTo: rejectFollowRequestButtonShadowBackgroundContainer.topAnchor), + rejectFollowRequestButton.leadingAnchor.constraint(equalTo: rejectFollowRequestButtonShadowBackgroundContainer.leadingAnchor), + rejectFollowRequestButton.trailingAnchor.constraint(equalTo: rejectFollowRequestButtonShadowBackgroundContainer.trailingAnchor), + rejectFollowRequestButton.bottomAnchor.constraint(equalTo: rejectFollowRequestButtonShadowBackgroundContainer.bottomAnchor), + ]) + + let followReqeustContainerBottomMargin: CGFloat = 8 + acceptFollowRequestButtonShadowBackgroundContainer.translatesAutoresizingMaskIntoConstraints = false + followRequestContainerView.addSubview(acceptFollowRequestButtonShadowBackgroundContainer) + rejectFollowRequestButtonShadowBackgroundContainer.translatesAutoresizingMaskIntoConstraints = false + followRequestContainerView.addSubview(rejectFollowRequestButtonShadowBackgroundContainer) + NSLayoutConstraint.activate([ + acceptFollowRequestButtonShadowBackgroundContainer.topAnchor.constraint(equalTo: followRequestContainerView.topAnchor), + acceptFollowRequestButtonShadowBackgroundContainer.leadingAnchor.constraint(equalTo: followRequestContainerView.leadingAnchor), + followRequestContainerView.bottomAnchor.constraint(equalTo: acceptFollowRequestButtonShadowBackgroundContainer.bottomAnchor, constant: followReqeustContainerBottomMargin), + rejectFollowRequestButtonShadowBackgroundContainer.topAnchor.constraint(equalTo: followRequestContainerView.topAnchor), + rejectFollowRequestButtonShadowBackgroundContainer.leadingAnchor.constraint(equalTo: acceptFollowRequestButtonShadowBackgroundContainer.trailingAnchor, constant: 8), + followRequestContainerView.trailingAnchor.constraint(equalTo: rejectFollowRequestButtonShadowBackgroundContainer.trailingAnchor), + followRequestContainerView.bottomAnchor.constraint(equalTo: rejectFollowRequestButtonShadowBackgroundContainer.bottomAnchor, constant: followReqeustContainerBottomMargin), + acceptFollowRequestButtonShadowBackgroundContainer.widthAnchor.constraint(equalTo: rejectFollowRequestButtonShadowBackgroundContainer.widthAnchor), + ]) + followRequestAdaptiveMarginContainerView.isHidden = true + // statusView containerStackView.addArrangedSubview(statusView) statusView.setup(style: .notification) @@ -271,10 +354,22 @@ extension NotificationView { } extension NotificationView { + @objc private func avatarButtonDidPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") delegate?.notificationView(self, authorAvatarButtonDidPressed: avatarButton) } + + @objc private func acceptFollowRequestButtonDidPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + delegate?.notificationView(self, acceptFollowRequestButtonDidPressed: sender) + } + + @objc private func rejectFollowRequestButtonDidPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + delegate?.notificationView(self, rejectFollowRequestButtonDidPressed: sender) + } + } extension NotificationView { @@ -282,6 +377,10 @@ extension NotificationView { public func setAuthorContainerBottomPaddingViewDisplay() { authorContainerViewBottomPaddingView.isHidden = false } + + public func setFollowRequestAdaptiveMarginContainerViewDisplay() { + followRequestAdaptiveMarginContainerView.isHidden = false + } public func setStatusViewDisplay() { statusView.isHidden = false