Merge branch 'develop' into feature/media-preview
This commit is contained in:
commit
678ea6b3fc
|
@ -349,7 +349,8 @@
|
||||||
"favourite": "favorited your post",
|
"favourite": "favorited your post",
|
||||||
"reblog": "rebloged your post",
|
"reblog": "rebloged your post",
|
||||||
"poll": "Your poll has ended",
|
"poll": "Your poll has ended",
|
||||||
"mention": "mentioned you"
|
"mention": "mentioned you",
|
||||||
|
"follow_request": "request to follow you"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"thread": {
|
"thread": {
|
||||||
|
|
|
@ -110,6 +110,7 @@
|
||||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */; };
|
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */; };
|
||||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */; };
|
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */; };
|
||||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.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 */; };
|
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; };
|
||||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
|
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; };
|
||||||
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
|
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
|
||||||
|
@ -663,6 +664,7 @@
|
||||||
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = "<group>"; };
|
2D8434F425FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleViewModel.swift; sourceTree = "<group>"; };
|
||||||
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleView.swift; sourceTree = "<group>"; };
|
2D8434FA25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineNavigationBarTitleView.swift; sourceTree = "<group>"; };
|
||||||
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = "<group>"; };
|
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = "<group>"; };
|
||||||
|
2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+FollowRequest.swift"; sourceTree = "<group>"; };
|
||||||
2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
|
2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
|
||||||
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||||
2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
|
2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1734,6 +1736,7 @@
|
||||||
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
|
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
|
||||||
5B24BBE1262DB19100A9381B /* APIService+Report.swift */,
|
5B24BBE1262DB19100A9381B /* APIService+Report.swift */,
|
||||||
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
||||||
|
2D8FCA072637EABB00137F46 /* APIService+FollowRequest.swift */,
|
||||||
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */,
|
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */,
|
||||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */,
|
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */,
|
||||||
5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */,
|
5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */,
|
||||||
|
@ -2902,6 +2905,7 @@
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||||
|
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
|
||||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||||
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
|
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
|
||||||
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */,
|
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */,
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "bbc4bc4def7eb05a7ba8e1219f80ee9be327334e",
|
"revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8",
|
||||||
"version": "6.2.1"
|
"version": "6.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -91,6 +91,18 @@ extension NotificationSection {
|
||||||
cell.actionLabel.text = actionText + " · " + timeText
|
cell.actionLabel.text = actionText + " · " + timeText
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.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.rejectButton)
|
||||||
|
}
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
cell.actionImageBackground.backgroundColor = color
|
cell.actionImageBackground.backgroundColor = color
|
||||||
cell.actionLabel.text = actionText + " · " + timeText
|
cell.actionLabel.text = actionText + " · " + timeText
|
||||||
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
||||||
|
@ -108,6 +120,7 @@ extension NotificationSection {
|
||||||
if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) {
|
if let actionImage = UIImage(systemName: actionImageName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold))?.withRenderingMode(.alwaysTemplate) {
|
||||||
cell.actionImageView.image = actionImage
|
cell.actionImageView.image = actionImage
|
||||||
}
|
}
|
||||||
|
cell.buttonStackView.isHidden = (type != .followRequest)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
|
|
|
@ -24,6 +24,8 @@ extension Mastodon.Entity.Notification.NotificationType {
|
||||||
color = Asset.Colors.Notification.mention.color
|
color = Asset.Colors.Notification.mention.color
|
||||||
case .poll:
|
case .poll:
|
||||||
color = Asset.Colors.brandBlue.color
|
color = Asset.Colors.brandBlue.color
|
||||||
|
case .followRequest:
|
||||||
|
color = Asset.Colors.brandBlue.color
|
||||||
default:
|
default:
|
||||||
color = .clear
|
color = .clear
|
||||||
}
|
}
|
||||||
|
@ -45,6 +47,8 @@ extension Mastodon.Entity.Notification.NotificationType {
|
||||||
actionText = L10n.Scene.Notification.Action.mention
|
actionText = L10n.Scene.Notification.Action.mention
|
||||||
case .poll:
|
case .poll:
|
||||||
actionText = L10n.Scene.Notification.Action.poll
|
actionText = L10n.Scene.Notification.Action.poll
|
||||||
|
case .followRequest:
|
||||||
|
actionText = L10n.Scene.Notification.Action.followRequest
|
||||||
default:
|
default:
|
||||||
actionText = ""
|
actionText = ""
|
||||||
}
|
}
|
||||||
|
@ -66,6 +70,8 @@ extension Mastodon.Entity.Notification.NotificationType {
|
||||||
actionImageName = "at"
|
actionImageName = "at"
|
||||||
case .poll:
|
case .poll:
|
||||||
actionImageName = "list.bullet"
|
actionImageName = "list.bullet"
|
||||||
|
case .followRequest:
|
||||||
|
actionImageName = "person.crop.circle"
|
||||||
default:
|
default:
|
||||||
actionImageName = ""
|
actionImageName = ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -377,6 +377,8 @@ internal enum L10n {
|
||||||
internal static let favourite = L10n.tr("Localizable", "Scene.Notification.Action.Favourite")
|
internal static let favourite = L10n.tr("Localizable", "Scene.Notification.Action.Favourite")
|
||||||
/// followed you
|
/// followed you
|
||||||
internal static let follow = L10n.tr("Localizable", "Scene.Notification.Action.Follow")
|
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
|
/// mentioned you
|
||||||
internal static let mention = L10n.tr("Localizable", "Scene.Notification.Action.Mention")
|
internal static let mention = L10n.tr("Localizable", "Scene.Notification.Action.Mention")
|
||||||
/// Your poll has ended
|
/// Your poll has ended
|
||||||
|
|
|
@ -44,8 +44,7 @@ extension UserProviderFacade {
|
||||||
|
|
||||||
return context.apiService.toggleFollow(
|
return context.apiService.toggleFollow(
|
||||||
for: mastodonUser,
|
for: mastodonUser,
|
||||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
needFeedback: true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.switchToLatest()
|
.switchToLatest()
|
||||||
|
|
|
@ -127,6 +127,7 @@ tap the link to confirm your account.";
|
||||||
"Scene.HomeTimeline.Title" = "Home";
|
"Scene.HomeTimeline.Title" = "Home";
|
||||||
"Scene.Notification.Action.Favourite" = "favorited your post";
|
"Scene.Notification.Action.Favourite" = "favorited your post";
|
||||||
"Scene.Notification.Action.Follow" = "followed you";
|
"Scene.Notification.Action.Follow" = "followed you";
|
||||||
|
"Scene.Notification.Action.FollowRequest" = "request to follow you";
|
||||||
"Scene.Notification.Action.Mention" = "mentioned you";
|
"Scene.Notification.Action.Mention" = "mentioned you";
|
||||||
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
"Scene.Notification.Action.Poll" = "Your poll has ended";
|
||||||
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
"Scene.Notification.Action.Reblog" = "rebloged your post";
|
||||||
|
|
|
@ -210,6 +210,14 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl
|
||||||
|
|
||||||
// MARK: - NotificationTableViewCellDelegate
|
// MARK: - NotificationTableViewCellDelegate
|
||||||
extension NotificationViewController: 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) {
|
func userAvatarDidPressed(notification: MastodonNotification) {
|
||||||
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account)
|
let viewModel = ProfileViewModel(context: context, optionalMastodonUser: notification.account)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
|
@ -53,7 +53,7 @@ extension NotificationViewModel.LoadLatestState {
|
||||||
sinceID: nil,
|
sinceID: nil,
|
||||||
minID: nil,
|
minID: nil,
|
||||||
limit: nil,
|
limit: nil,
|
||||||
excludeTypes: [.followRequest],
|
excludeTypes: [],
|
||||||
accountID: nil
|
accountID: nil
|
||||||
)
|
)
|
||||||
viewModel.context.apiService.allNotifications(
|
viewModel.context.apiService.allNotifications(
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Foundation
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import OSLog
|
||||||
|
|
||||||
final class NotificationViewModel: NSObject {
|
final class NotificationViewModel: NSObject {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
@ -120,6 +121,38 @@ final class NotificationViewModel: NSObject {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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.rejectFollowRequest(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 {
|
extension NotificationViewModel {
|
||||||
|
|
|
@ -21,6 +21,10 @@ protocol NotificationTableViewCellDelegate: AnyObject {
|
||||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
|
|
||||||
|
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton)
|
||||||
|
|
||||||
|
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, rejectButtonDidPressed button: UIButton)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class NotificationTableViewCell: UITableViewCell {
|
final class NotificationTableViewCell: UITableViewCell {
|
||||||
|
@ -76,6 +80,24 @@ final class NotificationTableViewCell: UITableViewCell {
|
||||||
return label
|
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() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
avatatImageView.af.cancelImageRequest()
|
avatatImageView.af.cancelImageRequest()
|
||||||
|
@ -97,9 +119,8 @@ extension NotificationTableViewCell {
|
||||||
func configure() {
|
func configure() {
|
||||||
|
|
||||||
let containerStackView = UIStackView()
|
let containerStackView = UIStackView()
|
||||||
containerStackView.axis = .horizontal
|
containerStackView.axis = .vertical
|
||||||
containerStackView.alignment = .center
|
containerStackView.alignment = .fill
|
||||||
containerStackView.spacing = 4
|
|
||||||
containerStackView.layoutMargins = UIEdgeInsets(top: 14, left: 0, bottom: 12, right: 0)
|
containerStackView.layoutMargins = UIEdgeInsets(top: 14, left: 0, bottom: 12, right: 0)
|
||||||
containerStackView.isLayoutMarginsRelativeArrangement = true
|
containerStackView.isLayoutMarginsRelativeArrangement = true
|
||||||
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -111,7 +132,12 @@ extension NotificationTableViewCell {
|
||||||
contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
containerStackView.addArrangedSubview(avatarContainer)
|
let horizontalStackView = UIStackView()
|
||||||
|
horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
horizontalStackView.axis = .horizontal
|
||||||
|
horizontalStackView.spacing = 6
|
||||||
|
|
||||||
|
horizontalStackView.addArrangedSubview(avatarContainer)
|
||||||
avatarContainer.translatesAutoresizingMaskIntoConstraints = false
|
avatarContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
avatarContainer.heightAnchor.constraint(equalToConstant: 47).priority(.required - 1),
|
avatarContainer.heightAnchor.constraint(equalToConstant: 47).priority(.required - 1),
|
||||||
|
@ -144,13 +170,23 @@ extension NotificationTableViewCell {
|
||||||
])
|
])
|
||||||
|
|
||||||
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
containerStackView.addArrangedSubview(nameLabel)
|
horizontalStackView.addArrangedSubview(nameLabel)
|
||||||
actionLabel.translatesAutoresizingMaskIntoConstraints = false
|
actionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
containerStackView.addArrangedSubview(actionLabel)
|
horizontalStackView.addArrangedSubview(actionLabel)
|
||||||
nameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
nameLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||||
nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
nameLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||||
actionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
actionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
|
containerStackView.addArrangedSubview(horizontalStackView)
|
||||||
|
|
||||||
|
buttonStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
buttonStackView.axis = .horizontal
|
||||||
|
buttonStackView.distribution = .fillEqually
|
||||||
|
acceptButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
rejectButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
buttonStackView.addArrangedSubview(acceptButton)
|
||||||
|
buttonStackView.addArrangedSubview(rejectButton)
|
||||||
|
containerStackView.addArrangedSubview(buttonStackView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
|
|
@ -336,6 +336,7 @@ extension MastodonPickServerViewController {
|
||||||
} else {
|
} else {
|
||||||
let mastodonRegisterViewModel = MastodonRegisterViewModel(
|
let mastodonRegisterViewModel = MastodonRegisterViewModel(
|
||||||
domain: server.domain,
|
domain: server.domain,
|
||||||
|
context: self.context,
|
||||||
authenticateInfo: response.authenticateInfo,
|
authenticateInfo: response.authenticateInfo,
|
||||||
instance: response.instance.value,
|
instance: response.instance.value,
|
||||||
applicationToken: response.applicationToken.value
|
applicationToken: response.applicationToken.value
|
||||||
|
|
|
@ -18,6 +18,7 @@ final class MastodonRegisterViewModel {
|
||||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||||
let instance: Mastodon.Entity.Instance
|
let instance: Mastodon.Entity.Instance
|
||||||
let applicationToken: Mastodon.Entity.Token
|
let applicationToken: Mastodon.Entity.Token
|
||||||
|
let context: AppContext
|
||||||
|
|
||||||
let username = CurrentValueSubject<String, Never>("")
|
let username = CurrentValueSubject<String, Never>("")
|
||||||
let displayName = CurrentValueSubject<String, Never>("")
|
let displayName = CurrentValueSubject<String, Never>("")
|
||||||
|
@ -46,11 +47,13 @@ final class MastodonRegisterViewModel {
|
||||||
|
|
||||||
init(
|
init(
|
||||||
domain: String,
|
domain: String,
|
||||||
|
context: AppContext,
|
||||||
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
||||||
instance: Mastodon.Entity.Instance,
|
instance: Mastodon.Entity.Instance,
|
||||||
applicationToken: Mastodon.Entity.Token
|
applicationToken: Mastodon.Entity.Token
|
||||||
) {
|
) {
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
|
self.context = context
|
||||||
self.authenticateInfo = authenticateInfo
|
self.authenticateInfo = authenticateInfo
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.applicationToken = applicationToken
|
self.applicationToken = applicationToken
|
||||||
|
@ -78,6 +81,45 @@ final class MastodonRegisterViewModel {
|
||||||
}
|
}
|
||||||
.assign(to: \.value, on: usernameValidateState)
|
.assign(to: \.value, on: usernameValidateState)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
username
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||||
|
.removeDuplicates()
|
||||||
|
.compactMap { [weak self] text -> AnyPublisher<Result<Mastodon.Response.Content<Mastodon.Entity.Account>, 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<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||||
|
Result.success(response)
|
||||||
|
}
|
||||||
|
.catch { error in
|
||||||
|
Just(Result.failure(error))
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.sink { [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
|
||||||
|
.sink { [weak self] validateState in
|
||||||
|
if validateState == .valid {
|
||||||
|
self?.usernameErrorPrompt.value = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
displayName
|
displayName
|
||||||
.map { displayname in
|
.map { displayname in
|
||||||
guard !displayname.isEmpty else { return .empty }
|
guard !displayname.isEmpty else { return .empty }
|
||||||
|
@ -115,7 +157,8 @@ final class MastodonRegisterViewModel {
|
||||||
let error = error as? Mastodon.API.Error
|
let error = error as? Mastodon.API.Error
|
||||||
let mastodonError = error?.mastodonError
|
let mastodonError = error?.mastodonError
|
||||||
if case let .generic(genericMastodonError) = 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.usernameErrorPrompt.value = details.usernameErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||||
self.emailErrorPrompt.value = details.emailErrorDescriptions.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) }
|
self.passwordErrorPrompt.value = details.passwordErrorDescriptions.first.flatMap { MastodonRegisterViewModel.errorPromptAttributedString(for: $0) }
|
||||||
|
@ -156,7 +199,6 @@ extension MastodonRegisterViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonRegisterViewModel {
|
extension MastodonRegisterViewModel {
|
||||||
|
|
||||||
static func isValidEmail(_ email: String) -> Bool {
|
static func isValidEmail(_ email: String) -> Bool {
|
||||||
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
|
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
|
||||||
|
|
||||||
|
@ -206,5 +248,4 @@ extension MastodonRegisterViewModel {
|
||||||
|
|
||||||
return attributeString
|
return attributeString
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,7 +204,7 @@ extension MastodonServerRulesViewController {
|
||||||
@objc private func confirmButtonPressed(_ sender: UIButton) {
|
@objc private func confirmButtonPressed(_ sender: UIButton) {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
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)
|
self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,8 +188,7 @@ final class SuggestionAccountViewModel: NSObject {
|
||||||
let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser
|
let mastodonUser = context.managedObjectContext.object(with: objectID) as! MastodonUser
|
||||||
return context.apiService.toggleFollow(
|
return context.apiService.toggleFollow(
|
||||||
for: mastodonUser,
|
for: mastodonUser,
|
||||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
needFeedback: false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -152,4 +152,17 @@ extension APIService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func accountLookup(
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Account.AccountLookupQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||||
|
return Mastodon.API.Account.lookupAccount(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,15 +24,12 @@ extension APIService {
|
||||||
/// - Returns: publisher for `Relationship`
|
/// - Returns: publisher for `Relationship`
|
||||||
func toggleFollow(
|
func toggleFollow(
|
||||||
for mastodonUser: MastodonUser,
|
for mastodonUser: MastodonUser,
|
||||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
needFeedback: Bool
|
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||||
var impactFeedbackGenerator: UIImpactFeedbackGenerator?
|
|
||||||
var notificationFeedbackGenerator: UINotificationFeedbackGenerator?
|
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||||
if needFeedback {
|
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||||
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
|
||||||
notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
|
||||||
}
|
|
||||||
|
|
||||||
return followUpdateLocal(
|
return followUpdateLocal(
|
||||||
mastodonUserObjectID: mastodonUser.objectID,
|
mastodonUserObjectID: mastodonUser.objectID,
|
||||||
|
@ -40,9 +37,9 @@ extension APIService {
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.handleEvents { _ in
|
.handleEvents { _ in
|
||||||
impactFeedbackGenerator?.prepare()
|
impactFeedbackGenerator.prepare()
|
||||||
} receiveOutput: { _ in
|
} receiveOutput: { _ in
|
||||||
impactFeedbackGenerator?.impactOccurred()
|
impactFeedbackGenerator.impactOccurred()
|
||||||
} receiveCompletion: { completion in
|
} receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
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)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Friendship] rollback finish", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
} receiveValue: { _ in
|
} receiveValue: { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
notificationFeedbackGenerator?.prepare()
|
notificationFeedbackGenerator.prepare()
|
||||||
notificationFeedbackGenerator?.notificationOccurred(.error)
|
notificationFeedbackGenerator.notificationOccurred(.error)
|
||||||
}
|
}
|
||||||
.store(in: &self.disposeBag)
|
.store(in: &self.disposeBag)
|
||||||
|
|
||||||
case .finished:
|
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)
|
os_log("%{public}s[%{public}ld], %{public}s: [Friendship] remote friendship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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<Mastodon.Entity.Relationship> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rejectFollowRequest(
|
||||||
|
mastodonUserID: MastodonUser.ID,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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<Mastodon.Entity.Relationship> in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
return response
|
||||||
|
case .failure(let error):
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,14 @@ extension APIService {
|
||||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> in
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> in
|
||||||
let log = OSLog.api
|
let log = OSLog.api
|
||||||
return self.backgroundManagedObjectContext.performChanges {
|
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
|
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)
|
let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.backgroundManagedObjectContext, for: nil, in: domain, entity: notification.account, userCache: nil, networkDate: Date(), log: log)
|
||||||
var status: Status?
|
var status: Status?
|
||||||
|
|
|
@ -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: Mastodon.Entity.Account.ID) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests")
|
||||||
|
.appendingPathComponent(userID)
|
||||||
|
.appendingPathComponent("authorize")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func rejectFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL {
|
||||||
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests")
|
||||||
|
.appendingPathComponent(userID)
|
||||||
|
.appendingPathComponent("reject")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept Follow
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.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: User token
|
||||||
|
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||||
|
public static func acceptFollowRequest(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
userID: Mastodon.Entity.Account.ID,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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.Relationship.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reject Follow
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// - Since: 0.0.0
|
||||||
|
/// - Version: 3.3.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: User token
|
||||||
|
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||||
|
public static func rejectFollowRequest(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
userID: Mastodon.Entity.Account.ID,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, 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.Relationship.self, from: data, response: response)
|
||||||
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: app 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<Mastodon.Response.Content<Mastodon.Entity.Account>, 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue