diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 69c30e990..93d6e4731 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -24,6 +24,19 @@ + + + + + + + + + + + + + @@ -253,6 +266,7 @@ + diff --git a/CoreDataStack/Entity/DomainBlock.swift b/CoreDataStack/Entity/DomainBlock.swift new file mode 100644 index 000000000..3dd244c75 --- /dev/null +++ b/CoreDataStack/Entity/DomainBlock.swift @@ -0,0 +1,73 @@ +// +// DomainBlock.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/4/29. +// + +import CoreData +import Foundation + +public final class DomainBlock: NSManagedObject { + @NSManaged public private(set) var blockedDomain: String + @NSManaged public private(set) var createAt: Date + + @NSManaged public private(set) var domain: String + @NSManaged public private(set) var userID: String + + override public func awakeFromInsert() { + super.awakeFromInsert() + setPrimitiveValue(Date(), forKey: #keyPath(DomainBlock.createAt)) + } +} + +extension DomainBlock { + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + blockedDomain: String, + domain: String, + userID: String + ) -> DomainBlock { + let domainBlock: DomainBlock = context.insertObject() + domainBlock.domain = domain + domainBlock.blockedDomain = blockedDomain + domainBlock.userID = userID + return domainBlock + } +} + +extension DomainBlock: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + [NSSortDescriptor(keyPath: \DomainBlock.createAt, ascending: false)] + } +} + +extension DomainBlock { + static func predicate(domain: String) -> NSPredicate { + NSPredicate(format: "%K == %@", #keyPath(DomainBlock.domain), domain) + } + + static func predicate(userID: String) -> NSPredicate { + NSPredicate(format: "%K == %@", #keyPath(DomainBlock.userID), userID) + } + + static func predicate(blockedDomain: String) -> NSPredicate { + NSPredicate(format: "%K == %@", #keyPath(DomainBlock.blockedDomain), blockedDomain) + } + + public static func predicate(domain: String, userID: String) -> NSPredicate { + NSCompoundPredicate(andPredicateWithSubpredicates: [ + DomainBlock.predicate(domain: domain), + DomainBlock.predicate(userID: userID) + ]) + } + + public static func predicate(domain: String, userID: String, blockedDomain: String) -> NSPredicate { + NSCompoundPredicate(andPredicateWithSubpredicates: [ + DomainBlock.predicate(domain: domain), + DomainBlock.predicate(userID: userID), + DomainBlock.predicate(blockedDomain: blockedDomain) + ]) + } +} diff --git a/Localization/app.json b/Localization/app.json index 4d6dcbb2d..fbc670da6 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -28,6 +28,10 @@ "message": "Are you sure you want to sign out?", "confirm": "Sign Out" }, + "block_domain": { + "message": "Are you really, really sure you want to block the entire %s? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "block_entire_domain": "Block entire domain" + }, "save_photo_failure": { "title": "Save Photo Failure", "message": "Please enable photo libaray access permission to save photo." @@ -55,11 +59,14 @@ "preview": "Preview", "share": "Share", "share_user": "Share %s", + "share_post": "Share post", "open_in_safari": "Open in Safari", "find_people": "Find people to follow", "manually_search": "Manually search instead", "skip": "Skip", "report_user": "Report %s", + "block_domain": "Block %s", + "unblock_domain": "Unblock %s", "settings": "Settings" }, "status": { @@ -416,4 +423,4 @@ "text_placeholder": "Type or paste additional comments" } } -} \ No newline at end of file +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 6b1b9ed18..ff407b6ed 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ 0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; }; 0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220C26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift */; }; 0F202213261351F5000C64BF /* APIService+HashtagTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */; }; - 0F202227261411BB000C64BF /* HashtagTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */; }; + 0F202227261411BB000C64BF /* HashtagTimelineViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202226261411BA000C64BF /* HashtagTimelineViewController+Provider.swift */; }; 0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */; }; 0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223226145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift */; }; 0F20223926146553000C64BF /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; @@ -59,7 +59,7 @@ 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; }; 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; }; 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */; }; - 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */; }; + 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+Provider.swift */; }; 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1E425CD46C100561493 /* HomeTimelineViewModel.swift */; }; 2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */; }; 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */; }; @@ -98,7 +98,7 @@ 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; - 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; }; + 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+Provider.swift */; }; 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */; }; 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; }; 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */; }; @@ -118,6 +118,9 @@ 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; + 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */; }; + 2D9DB969263A833E007C1D71 /* DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB968263A833E007C1D71 /* DomainBlock.swift */; }; + 2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */; }; 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA504682601ADE7008F4E6C /* SawToothView.swift */; }; 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA6054625F716A2006356F9 /* PlaybackState.swift */; }; 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA6055025F74407006356F9 /* AudioContainerViewModel.swift */; }; @@ -238,7 +241,7 @@ DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; }; DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; }; DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; }; - DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */; }; + DB482A45261335BA008AE74C /* UserTimelineViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A44261335BA008AE74C /* UserTimelineViewController+Provider.swift */; }; DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; }; DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; }; DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; }; @@ -360,7 +363,7 @@ DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; }; DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */; }; DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; }; - DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */; }; + DB938F25262438D600E5B6C1 /* ThreadViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F24262438D600E5B6C1 /* ThreadViewController+Provider.swift */; }; DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; }; DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; }; DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; }; @@ -431,7 +434,7 @@ DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CDFA261C6CA500430CC6 /* FavoriteViewModel.swift */; }; DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE00261D623D00430CC6 /* FavoriteViewModel+State.swift */; }; DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE06261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift */; }; - DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE0C261D767100430CC6 /* FavoriteViewController+StatusProvider.swift */; }; + DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE0C261D767100430CC6 /* FavoriteViewController+Provider.swift */; }; DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */; }; DBE54ABF2636C889004E7C0B /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B23263684C600ACB481 /* UserDefaults.swift */; }; DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; @@ -571,7 +574,7 @@ 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = ""; }; 0F20220C26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadLatestState.swift"; sourceTree = ""; }; 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HashtagTimeline.swift"; sourceTree = ""; }; - 0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; + 0F202226261411BA000C64BF /* HashtagTimelineViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewController+Provider.swift"; sourceTree = ""; }; 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; 0F20223226145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; 0F20223826146553000C64BF /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; @@ -616,7 +619,7 @@ 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = ""; }; 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = ""; }; 2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewController.swift; sourceTree = ""; }; - 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; + 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+Provider.swift"; sourceTree = ""; }; 2D38F1E425CD46C100561493 /* HomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewModel.swift; sourceTree = ""; }; 2D38F1EA25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadLatestState.swift"; sourceTree = ""; }; 2D38F1F025CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; @@ -652,7 +655,7 @@ 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = ""; }; - 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; + 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+Provider.swift"; sourceTree = ""; }; 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+Diffable.swift"; sourceTree = ""; }; 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; 2D7631A725C1535600929FB9 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; @@ -671,6 +674,9 @@ 2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; 2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewController+Avatar.swift"; sourceTree = ""; }; + 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockDomainService.swift; sourceTree = ""; }; + 2D9DB968263A833E007C1D71 /* DomainBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainBlock.swift; sourceTree = ""; }; + 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+DomainBlock.swift"; sourceTree = ""; }; 2DA504682601ADE7008F4E6C /* SawToothView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SawToothView.swift; sourceTree = ""; }; 2DA6054625F716A2006356F9 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = ""; }; 2DA6055025F74407006356F9 /* AudioContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerViewModel.swift; sourceTree = ""; }; @@ -804,7 +810,7 @@ DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; }; DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; }; DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+State.swift"; sourceTree = ""; }; - DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; + DB482A44261335BA008AE74C /* UserTimelineViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewController+Provider.swift"; sourceTree = ""; }; DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+UserTimeline.swift"; sourceTree = ""; }; DB4924E126312AB200E9DB22 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; @@ -919,7 +925,7 @@ DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = ""; }; DB938F1426241FDF00E5B6C1 /* APIService+Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Thread.swift"; sourceTree = ""; }; DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; - DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+StatusProvider.swift"; sourceTree = ""; }; + DB938F24262438D600E5B6C1 /* ThreadViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+Provider.swift"; sourceTree = ""; }; DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; }; DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; @@ -989,7 +995,7 @@ DBE3CDFA261C6CA500430CC6 /* FavoriteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewModel.swift; sourceTree = ""; }; DBE3CE00261D623D00430CC6 /* FavoriteViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewModel+State.swift"; sourceTree = ""; }; DBE3CE06261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewModel+Diffable.swift"; sourceTree = ""; }; - DBE3CE0C261D767100430CC6 /* FavoriteViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewController+StatusProvider.swift"; sourceTree = ""; }; + DBE3CE0C261D767100430CC6 /* FavoriteViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteViewController+Provider.swift"; sourceTree = ""; }; DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerAspect.swift; sourceTree = ""; }; DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPreference.swift; sourceTree = ""; }; DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = ""; }; @@ -1086,7 +1092,7 @@ isa = PBXGroup; children = ( 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */, - 0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */, + 0F202226261411BA000C64BF /* HashtagTimelineViewController+Provider.swift */, 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */, 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */, 0F20220C26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift */, @@ -1212,7 +1218,7 @@ children = ( DB1F239626117C360057430E /* View */, 2D38F1D425CD465300561493 /* HomeTimelineViewController.swift */, - 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift */, + 2D38F1DE25CD46A400561493 /* HomeTimelineViewController+Provider.swift */, 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */, 2D38F1E425CD46C100561493 /* HomeTimelineViewModel.swift */, 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */, @@ -1300,6 +1306,7 @@ 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */, DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */, DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */, + 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */, DB4924E126312AB200E9DB22 /* NotificationService.swift */, DB6D9F6226357848008423CD /* SettingService.swift */, DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */, @@ -1338,7 +1345,7 @@ isa = PBXGroup; children = ( 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */, - 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */, + 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+Provider.swift */, 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */, 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */, 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */, @@ -1719,6 +1726,7 @@ DB98336A25C9420100AD9700 /* APIService+App.swift */, DB98337025C9443200AD9700 /* APIService+Authentication.swift */, DB98339B25C96DE600AD9700 /* APIService+Account.swift */, + 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */, 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */, DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */, DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */, @@ -1975,6 +1983,7 @@ isa = PBXGroup; children = ( DB89BA2625C110B4008580ED /* Status.swift */, + 2D9DB968263A833E007C1D71 /* DomainBlock.swift */, 2D6125462625436B00299647 /* Notification.swift */, 2D0B7A1C261D839600B44727 /* SearchHistory.swift */, DB8AF52425C131D1002E6C99 /* MastodonUser.swift */, @@ -2098,7 +2107,7 @@ isa = PBXGroup; children = ( DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */, - DB938F24262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift */, + DB938F24262438D600E5B6C1 /* ThreadViewController+Provider.swift */, DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */, DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */, DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */, @@ -2281,7 +2290,7 @@ isa = PBXGroup; children = ( DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */, - DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */, + DB482A44261335BA008AE74C /* UserTimelineViewController+Provider.swift */, DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */, DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */, DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */, @@ -2334,7 +2343,7 @@ isa = PBXGroup; children = ( DBE3CDEB261C6B2900430CC6 /* FavoriteViewController.swift */, - DBE3CE0C261D767100430CC6 /* FavoriteViewController+StatusProvider.swift */, + DBE3CE0C261D767100430CC6 /* FavoriteViewController+Provider.swift */, DBE3CDFA261C6CA500430CC6 /* FavoriteViewModel.swift */, DBE3CE06261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift */, DBE3CE00261D623D00430CC6 /* FavoriteViewModel+State.swift */, @@ -2862,6 +2871,7 @@ DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, + 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */, DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */, DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, @@ -2967,7 +2977,7 @@ DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */, DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */, DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, - DBE3CE0D261D767100430CC6 /* FavoriteViewController+StatusProvider.swift in Sources */, + DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */, 2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */, @@ -3003,8 +3013,9 @@ 2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */, 0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */, 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */, - 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */, + 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, + 2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */, 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */, 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, @@ -3083,7 +3094,7 @@ DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */, 2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */, DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */, - DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */, + DB482A45261335BA008AE74C /* UserTimelineViewController+Provider.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, @@ -3127,7 +3138,7 @@ DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */, 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */, 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */, - 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */, + 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+Provider.swift in Sources */, 0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */, 2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */, DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */, @@ -3144,7 +3155,7 @@ 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */, 2D198655261C3C4300F0B013 /* SearchViewModel+LoadOldestState.swift in Sources */, - 0F202227261411BB000C64BF /* HashtagTimelineViewController+StatusProvider.swift in Sources */, + 0F202227261411BB000C64BF /* HashtagTimelineViewController+Provider.swift in Sources */, 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */, 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */, DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */, @@ -3178,7 +3189,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */, 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */, - DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */, + DB938F25262438D600E5B6C1 /* ThreadViewController+Provider.swift in Sources */, DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */, DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, @@ -3251,6 +3262,7 @@ 2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */, 2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */, DB89BA1D25C1107F008580ED /* URL.swift in Sources */, + 2D9DB969263A833E007C1D71 /* DomainBlock.swift in Sources */, 2D0B7A1D261D839600B44727 /* SearchHistory.swift in Sources */, 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */, 5B90C46E26259B2C0002E742 /* Subscription.swift in Sources */, diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 519637b88..3994229ee 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -223,7 +223,6 @@ extension StatusSection { meta.blurhashImagePublisher() .receive(on: DispatchQueue.main) .sink { [weak cell] image in - guard let cell = cell else { return } blurhashOverlayImageView.image = image image?.pngData().flatMap { blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey) @@ -401,16 +400,15 @@ extension StatusSection { .store(in: &cell.disposeBag) } - // toolbar - StatusSection.configureActionToolBar( - cell: cell, - dependency: dependency, - status: status, - requestUserID: requestUserID - ) - - // separator line if let statusTableViewCell = cell as? StatusTableViewCell { + // toolbar + StatusSection.configureActionToolBar( + cell: statusTableViewCell, + dependency: dependency, + status: status, + requestUserID: requestUserID + ) + // separator line statusTableViewCell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden } @@ -430,12 +428,12 @@ extension StatusSection { .sink { _ in // do nothing } receiveValue: { [weak dependency, weak cell] change in - guard let cell = cell else { return } guard let dependency = dependency else { return } guard case .update(let object) = change.changeType, let status = object as? Status else { return } + guard let statusTableViewCell = cell as? StatusTableViewCell else { return } StatusSection.configureActionToolBar( - cell: cell, + cell: statusTableViewCell, dependency: dependency, status: status, requestUserID: requestUserID @@ -593,7 +591,7 @@ extension StatusSection { } static func configureActionToolBar( - cell: StatusCell, + cell: StatusTableViewCell, dependency: NeedsDependency, status: Status, requestUserID: String @@ -623,6 +621,26 @@ extension StatusSection { cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal) cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike + Publishers.CombineLatest( + dependency.context.blockDomainService.blockedDomains, + ManagedObjectObserver.observe(object: status.authorForUserProvider) + .assertNoFailure() + ) + .receive(on: DispatchQueue.main) + .sink { [weak dependency, weak cell] _,change in + guard let cell = cell else { return } + guard let dependency = dependency else { return } + switch change.changeType { + case .delete: + return + case .update(_): + break + case .none: + break + } + StatusSection.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status) + } + .store(in: &cell.disposeBag) self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status) } @@ -752,37 +770,36 @@ extension StatusSection { } private static func setupStatusMoreButtonMenu( - cell: StatusCell, + cell: StatusTableViewCell, dependency: NeedsDependency, status: Status) { - cell.statusView.actionToolbarContainer.moreButton.menu = nil + guard let userProvider = dependency as? UserProvider else { fatalError() } guard let authenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - let author = (status.reblog ?? status).author - guard authenticationBox.userID != author.id else { - return - } - var children: [UIMenuElement] = [] - let name = author.displayNameWithFallback - let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { - [weak dependency] _ in - guard let dependency = dependency else { return } - let viewModel = ReportViewModel( - context: dependency.context, - domain: authenticationBox.domain, - user: status.author, - status: status) - dependency.coordinator.present( - scene: .report(viewModel: viewModel), - from: nil, - transition: .modal(animated: true, completion: nil) - ) - } - children.append(reportAction) - cell.statusView.actionToolbarContainer.moreButton.menu = UIMenu(title: "", options: [], children: children) + let author = status.authorForUserProvider + let isMyself = authenticationBox.userID == author.id + let canReport = !isMyself + let isInSameDomain = authenticationBox.domain == author.domainFromAcct + let isMuting = (author.mutingBy ?? Set()).map(\.id).contains(authenticationBox.userID) + let isBlocking = (author.blockingBy ?? Set()).map(\.id).contains(authenticationBox.userID) + let isDomainBlocking = dependency.context.blockDomainService.blockedDomains.value.contains(author.domainFromAcct) cell.statusView.actionToolbarContainer.moreButton.showsMenuAsPrimaryAction = true + cell.statusView.actionToolbarContainer.moreButton.menu = UserProviderFacade.createProfileActionMenu( + for: author, + isMyself: isMyself, + isMuting: isMuting, + isBlocking: isBlocking, + isInSameDomain: isInSameDomain, + isDomainBlocking: isDomainBlocking, + provider: userProvider, + cell: cell, + sourceView: cell.statusView.actionToolbarContainer.moreButton, + barButtonItem: nil, + shareUser: nil, + shareStatus: status + ) } } diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift index 7035a987b..b780f5916 100644 --- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift +++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift @@ -50,6 +50,15 @@ extension MastodonUser { } } + var domainFromAcct: String { + if !acct.contains("@") { + return domain + } else { + let domain = acct.split(separator: "@").last + return String(domain!) + } + } + } extension MastodonUser { diff --git a/Mastodon/Extension/CoreDataStack/Status.swift b/Mastodon/Extension/CoreDataStack/Status.swift index 880be6fa3..1a909285d 100644 --- a/Mastodon/Extension/CoreDataStack/Status.swift +++ b/Mastodon/Extension/CoreDataStack/Status.swift @@ -5,8 +5,8 @@ // Created by MainasuK Cirno on 2021/2/4. // -import Foundation import CoreDataStack +import Foundation import MastodonSDK extension Status.Property { @@ -34,7 +34,6 @@ extension Status.Property { } extension Status { - enum SensitiveType { case none case all @@ -61,5 +60,29 @@ extension Status { // not sensitive return .none } - +} + +extension Status { + var authorForUserProvider: MastodonUser { + let author = (reblog ?? self).author + return author + } +} + +extension Status { + var statusURL: URL { + if let urlString = self.url, + let url = URL(string: urlString) + { + return url + } else { + return URL(string: "https://\(self.domain)/web/statuses/\(self.id)")! + } + } + + var activityItems: [Any] { + var items: [Any] = [] + items.append(self.statusURL) + return items + } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index b4908fea4..4ec5e7037 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -13,6 +13,14 @@ internal enum L10n { internal enum Common { internal enum Alerts { + internal enum BlockDomain { + /// Block entire domain + internal static let blockEntireDomain = L10n.tr("Localizable", "Common.Alerts.BlockDomain.BlockEntireDomain") + /// Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed. + internal static func message(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Message", String(describing: p1)) + } + } internal enum Common { /// Please try again. internal static let pleaseTryAgain = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgain") @@ -66,6 +74,10 @@ internal enum L10n { internal static let add = L10n.tr("Localizable", "Common.Controls.Actions.Add") /// Back internal static let back = L10n.tr("Localizable", "Common.Controls.Actions.Back") + /// Block %@ + internal static func blockDomain(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Actions.BlockDomain", String(describing: p1)) + } /// Cancel internal static let cancel = L10n.tr("Localizable", "Common.Controls.Actions.Cancel") /// Confirm @@ -104,6 +116,8 @@ internal enum L10n { internal static let settings = L10n.tr("Localizable", "Common.Controls.Actions.Settings") /// Share internal static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share") + /// Share post + internal static let sharePost = L10n.tr("Localizable", "Common.Controls.Actions.SharePost") /// Share %@ internal static func shareUser(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Actions.ShareUser", String(describing: p1)) @@ -118,6 +132,10 @@ internal enum L10n { internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") /// Try Again internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain") + /// Unblock %@ + internal static func unblockDomain(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Actions.UnblockDomain", String(describing: p1)) + } } internal enum Firendship { /// Block diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift index 46e4c5ab5..98fa2d2cd 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift @@ -114,7 +114,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { guard let imagePreviewPresentableCell = tableView.cellForRow(at: indexPath) as? ImagePreviewPresentableCell else { return nil } guard imagePreviewPresentableCell.isRevealing else { return nil } - let status = status(for: nil, indexPath: indexPath) + let status = self.status(for: nil, indexPath: indexPath) return contextMenuConfiguration(tableView, status: status, imagePreviewPresentableCell: imagePreviewPresentableCell, contextMenuConfigurationForRowAt: indexPath, point: point) } @@ -260,7 +260,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { guard index < imageViews.count else { return } let imageView = imageViews[index] - let status = status(for: nil, indexPath: indexPath) + let status = self.status(for: nil, indexPath: indexPath) let initialFrame: CGRect? = { guard let previewViewController = animator.previewViewController else { return nil } return UIView.findContextMenuPreviewFrameInWindow(previewController: previewViewController) diff --git a/Mastodon/Protocol/UserProvider/UserProvider.swift b/Mastodon/Protocol/UserProvider/UserProvider.swift index 63a1f8e68..f9939c740 100644 --- a/Mastodon/Protocol/UserProvider/UserProvider.swift +++ b/Mastodon/Protocol/UserProvider/UserProvider.swift @@ -5,12 +5,33 @@ // Created by MainasuK Cirno on 2021-4-1. // -import UIKit import Combine import CoreData import CoreDataStack +import UIKit protocol UserProvider: NeedsDependency & DisposeBagCollectable & UIViewController { // async func mastodonUser() -> Future + + func mastodonUser(for cell: UITableViewCell?) -> Future +} + +extension UserProvider where Self: StatusProvider { + func mastodonUser(for cell: UITableViewCell?) -> Future { + Future { [weak self] promise in + guard let self = self else { return } + self.status(for: cell, indexPath: nil) + .sink { status in + promise(.success(status?.authorForUserProvider)) + } + .store(in: &self.disposeBag) + } + } + + func mastodonUser() -> Future { + Future { promise in + promise(.success(nil)) + } + } } diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index 4f0a2bfee..9e20e414a 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -5,16 +5,15 @@ // Created by MainasuK Cirno on 2021-4-1. // -import UIKit import Combine import CoreData import CoreDataStack import MastodonSDK +import UIKit -enum UserProviderFacade { } +enum UserProviderFacade {} extension UserProviderFacade { - static func toggleUserFollowRelationship( provider: UserProvider ) -> AnyPublisher, Error> { @@ -50,25 +49,31 @@ extension UserProviderFacade { .switchToLatest() .eraseToAnyPublisher() } - } extension UserProviderFacade { - static func toggleUserBlockRelationship( - provider: UserProvider + provider: UserProvider, + cell: UITableViewCell? ) -> AnyPublisher, Error> { // prepare authentication guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { assertionFailure() return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() } - - return _toggleUserBlockRelationship( - context: provider.context, - activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, - mastodonUser: provider.mastodonUser().eraseToAnyPublisher() - ) + if let cell = cell { + return _toggleUserBlockRelationship( + context: provider.context, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, + mastodonUser: provider.mastodonUser(for: cell).eraseToAnyPublisher() + ) + } else { + return _toggleUserBlockRelationship( + context: provider.context, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, + mastodonUser: provider.mastodonUser().eraseToAnyPublisher() + ) + } } private static func _toggleUserBlockRelationship( @@ -90,25 +95,31 @@ extension UserProviderFacade { .switchToLatest() .eraseToAnyPublisher() } - } extension UserProviderFacade { - static func toggleUserMuteRelationship( - provider: UserProvider + provider: UserProvider, + cell: UITableViewCell? ) -> AnyPublisher, Error> { // prepare authentication guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { assertionFailure() return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() } - - return _toggleUserMuteRelationship( - context: provider.context, - activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, - mastodonUser: provider.mastodonUser().eraseToAnyPublisher() - ) + if let cell = cell { + return _toggleUserMuteRelationship( + context: provider.context, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, + mastodonUser: provider.mastodonUser(for: cell).eraseToAnyPublisher() + ) + } else { + return _toggleUserMuteRelationship( + context: provider.context, + activeMastodonAuthenticationBox: activeMastodonAuthenticationBox, + mastodonUser: provider.mastodonUser().eraseToAnyPublisher() + ) + } } private static func _toggleUserMuteRelationship( @@ -130,81 +141,135 @@ extension UserProviderFacade { .switchToLatest() .eraseToAnyPublisher() } - } extension UserProviderFacade { - static func createProfileActionMenu( for mastodonUser: MastodonUser, + isMyself: Bool, isMuting: Bool, isBlocking: Bool, - needsShareAction: Bool, + isInSameDomain: Bool, + isDomainBlocking: Bool, provider: UserProvider, + cell: UITableViewCell?, sourceView: UIView?, - barButtonItem: UIBarButtonItem? + barButtonItem: UIBarButtonItem?, + shareUser: MastodonUser?, + shareStatus: Status? ) -> UIMenu { var children: [UIMenuElement] = [] let name = mastodonUser.displayNameWithFallback - // mute - let muteAction = UIAction( - title: isMuting ? L10n.Common.Controls.Firendship.unmuteUser(name) : L10n.Common.Controls.Firendship.mute, - image: isMuting ? UIImage(systemName: "speaker") : UIImage(systemName: "speaker.slash"), - discoverabilityTitle: isMuting ? nil : L10n.Common.Controls.Firendship.muteUser(name), - attributes: isMuting ? [] : .destructive, - state: .off - ) { [weak provider] _ in - guard let provider = provider else { return } + if !isMyself { + // mute + let muteAction = UIAction( + title: isMuting ? L10n.Common.Controls.Firendship.unmuteUser(name) : L10n.Common.Controls.Firendship.mute, + image: isMuting ? UIImage(systemName: "speaker") : UIImage(systemName: "speaker.slash"), + discoverabilityTitle: isMuting ? nil : L10n.Common.Controls.Firendship.muteUser(name), + attributes: isMuting ? [] : .destructive, + state: .off + ) { [weak provider] _ in + guard let provider = provider else { return } - UserProviderFacade.toggleUserMuteRelationship( - provider: provider - ) - .sink { _ in - // do nothing - } receiveValue: { _ in - // do nothing + UserProviderFacade.toggleUserMuteRelationship( + provider: provider, + cell: cell + ) + .sink { _ in + // do nothing + } receiveValue: { _ in + // do nothing + } + .store(in: &provider.context.disposeBag) + } + if isMuting { + children.append(muteAction) + } else { + let muteMenu = UIMenu(title: L10n.Common.Controls.Firendship.muteUser(name), image: UIImage(systemName: "speaker.slash"), options: [], children: [muteAction]) + children.append(muteMenu) } - .store(in: &provider.context.disposeBag) - } - if isMuting { - children.append(muteAction) - } else { - let muteMenu = UIMenu(title: L10n.Common.Controls.Firendship.muteUser(name), image: UIImage(systemName: "speaker.slash"), options: [], children: [muteAction]) - children.append(muteMenu) } - // block - let blockAction = UIAction( - title: isBlocking ? L10n.Common.Controls.Firendship.unblockUser(name) : L10n.Common.Controls.Firendship.block, - image: isBlocking ? UIImage(systemName: "hand.raised.slash") : UIImage(systemName: "hand.raised"), - discoverabilityTitle: isBlocking ? nil : L10n.Common.Controls.Firendship.blockUser(name), - attributes: isBlocking ? [] : .destructive, - state: .off - ) { [weak provider] _ in - guard let provider = provider else { return } + if !isMyself { + // block + let blockAction = UIAction( + title: isBlocking ? L10n.Common.Controls.Firendship.unblockUser(name) : L10n.Common.Controls.Firendship.block, + image: isBlocking ? UIImage(systemName: "hand.raised.slash") : UIImage(systemName: "hand.raised"), + discoverabilityTitle: isBlocking ? nil : L10n.Common.Controls.Firendship.blockUser(name), + attributes: isBlocking ? [] : .destructive, + state: .off + ) { [weak provider] _ in + guard let provider = provider else { return } - UserProviderFacade.toggleUserBlockRelationship( - provider: provider - ) - .sink { _ in - // do nothing - } receiveValue: { _ in - // do nothing + UserProviderFacade.toggleUserBlockRelationship( + provider: provider, + cell: cell + ) + .sink { _ in + // do nothing + } receiveValue: { _ in + // do nothing + } + .store(in: &provider.context.disposeBag) + } + if isBlocking { + children.append(blockAction) + } else { + let blockMenu = UIMenu(title: L10n.Common.Controls.Firendship.blockUser(name), image: UIImage(systemName: "hand.raised"), options: [], children: [blockAction]) + children.append(blockMenu) } - .store(in: &provider.context.disposeBag) - } - if isBlocking { - children.append(blockAction) - } else { - let blockMenu = UIMenu(title: L10n.Common.Controls.Firendship.blockUser(name), image: UIImage(systemName: "hand.raised"), options: [], children: [blockAction]) - children.append(blockMenu) } - if needsShareAction { + if !isMyself { + let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "flag"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in + guard let provider = provider else { return } + guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + let viewModel = ReportViewModel( + context: provider.context, + domain: authenticationBox.domain, + user: mastodonUser, + status: nil + ) + provider.coordinator.present( + scene: .report(viewModel: viewModel), + from: provider, + transition: .modal(animated: true, completion: nil) + ) + } + children.append(reportAction) + } + + if !isInSameDomain { + if isDomainBlocking { + let unblockDomainAction = UIAction(title: L10n.Common.Controls.Actions.unblockDomain(mastodonUser.domainFromAcct), image: UIImage(systemName: "nosign"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in + guard let provider = provider else { return } + provider.context.blockDomainService.unblockDomain(userProvider: provider, cell: cell) + } + children.append(unblockDomainAction) + } else { + let blockDomainAction = UIAction(title: L10n.Common.Controls.Actions.blockDomain(mastodonUser.domainFromAcct), image: UIImage(systemName: "nosign"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in + guard let provider = provider else { return } + let alertController = UIAlertController(title: "", message: L10n.Common.Alerts.BlockDomain.message(mastodonUser.domainFromAcct), preferredStyle: .alert) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in + } + alertController.addAction(cancelAction) + let blockDomainAction = UIAlertAction(title: L10n.Common.Alerts.BlockDomain.blockEntireDomain, style: .destructive) { _ in + provider.context.blockDomainService.blockDomain(userProvider: provider, cell: cell) + } + alertController.addAction(blockDomainAction) + provider.present(alertController, animated: true, completion: nil) + } + children.append(blockDomainAction) + } + } + + if let shareUser = shareUser { let shareAction = UIAction(title: L10n.Common.Controls.Actions.shareUser(name), image: UIImage(systemName: "square.and.arrow.up"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in guard let provider = provider else { return } - let activityViewController = createActivityViewControllerForMastodonUser(mastodonUser: mastodonUser, dependency: provider) + let activityViewController = createActivityViewControllerForMastodonUser(mastodonUser: shareUser, dependency: provider) provider.coordinator.present( scene: .activityViewController( activityViewController: activityViewController, @@ -218,23 +283,22 @@ extension UserProviderFacade { children.append(shareAction) } - let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in - guard let provider = provider else { return } - guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { - return + if let shareStatus = shareStatus { + let shareAction = UIAction(title: L10n.Common.Controls.Actions.sharePost, image: UIImage(systemName: "square.and.arrow.up"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in + guard let provider = provider else { return } + let activityViewController = createActivityViewControllerForMastodonUser(status: shareStatus, dependency: provider) + provider.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: sourceView, + barButtonItem: barButtonItem + ), + from: provider, + transition: .activityViewControllerPresent(animated: true, completion: nil) + ) } - let viewModel = ReportViewModel( - context: provider.context, - domain: authenticationBox.domain, - user: mastodonUser, - status: nil) - provider.coordinator.present( - scene: .report(viewModel: viewModel), - from: provider, - transition: .modal(animated: true, completion: nil) - ) + children.append(shareAction) } - children.append(reportAction) return UIMenu(title: "", options: [], children: children) } @@ -246,5 +310,12 @@ extension UserProviderFacade { ) return activityViewController } - + + static func createActivityViewControllerForMastodonUser(status: Status, dependency: NeedsDependency) -> UIActivityViewController { + let activityViewController = UIActivityViewController( + activityItems: status.activityItems, + applicationActivities: [SafariActivity(sceneCoordinator: dependency.coordinator)] + ) + return activityViewController + } } diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 2249b15f8..59f83a5cd 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -1,3 +1,5 @@ +"Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain"; +"Common.Alerts.BlockDomain.Message" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed."; "Common.Alerts.Common.PleaseTryAgain" = "Please try again."; "Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; @@ -16,6 +18,7 @@ Please check your internet connection."; "Common.Alerts.VoteFailure.Title" = "Vote Failure"; "Common.Controls.Actions.Add" = "Add"; "Common.Controls.Actions.Back" = "Back"; +"Common.Controls.Actions.BlockDomain" = "Block %@"; "Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Continue" = "Continue"; @@ -34,12 +37,14 @@ Please check your internet connection."; "Common.Controls.Actions.SeeMore" = "See More"; "Common.Controls.Actions.Settings" = "Settings"; "Common.Controls.Actions.Share" = "Share"; +"Common.Controls.Actions.SharePost" = "Share post"; "Common.Controls.Actions.ShareUser" = "Share %@"; "Common.Controls.Actions.SignIn" = "Sign In"; "Common.Controls.Actions.SignUp" = "Sign Up"; "Common.Controls.Actions.Skip" = "Skip"; "Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Actions.TryAgain" = "Try Again"; +"Common.Controls.Actions.UnblockDomain" = "Unblock %@"; "Common.Controls.Firendship.Block" = "Block"; "Common.Controls.Firendship.BlockDomain" = "Block %@"; "Common.Controls.Firendship.BlockUser" = "Block %@"; diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+Provider.swift similarity index 96% rename from Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+StatusProvider.swift rename to Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+Provider.swift index 191ad374d..3bc3a36b5 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+Provider.swift @@ -1,5 +1,5 @@ // -// HashtagTimelineViewController+StatusProvider.swift +// HashtagTimelineViewController+Provider.swift // Mastodon // // Created by BradGao on 2021/3/31. @@ -86,3 +86,4 @@ extension HashtagTimelineViewController: StatusProvider { } +extension HashtagTimelineViewController: UserProvider {} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift similarity index 96% rename from Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift rename to Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift index aea931a62..d735d5843 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift @@ -1,5 +1,5 @@ // -// HomeTimelineViewController+StatusProvider.swift +// HomeTimelineViewController+Provider.swift // Mastodon // // Created by sxiaojian on 2021/2/5. @@ -85,3 +85,5 @@ extension HomeTimelineViewController: StatusProvider { } } + +extension HomeTimelineViewController: UserProvider {} diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+StatusProvider.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+Provider.swift similarity index 98% rename from Mastodon/Scene/Profile/Favorite/FavoriteViewController+StatusProvider.swift rename to Mastodon/Scene/Profile/Favorite/FavoriteViewController+Provider.swift index 68adc1e3e..88f368c15 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+StatusProvider.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+Provider.swift @@ -85,3 +85,5 @@ extension FavoriteViewController: StatusProvider { } } + +extension FavoriteViewController: UserProvider {} diff --git a/Mastodon/Scene/Profile/ProfileViewController+UserProvider.swift b/Mastodon/Scene/Profile/ProfileViewController+UserProvider.swift index 3a26db1c1..6bfa132b8 100644 --- a/Mastodon/Scene/Profile/ProfileViewController+UserProvider.swift +++ b/Mastodon/Scene/Profile/ProfileViewController+UserProvider.swift @@ -8,8 +8,15 @@ import Foundation import Combine import CoreDataStack +import UIKit extension ProfileViewController: UserProvider { + func mastodonUser(for cell: UITableViewCell?) -> Future { + return Future { promise in + promise(.success(nil)) + } + } + func mastodonUser() -> Future { return Future { promise in diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index d826cdaaa..d186d13df 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -373,20 +373,45 @@ extension ProfileViewController { .receive(on: DispatchQueue.main) .assign(to: \.text, on: profileHeaderViewController.profileHeaderView.usernameLabel) .store(in: &disposeBag) - viewModel.relationshipActionOptionSet - .receive(on: DispatchQueue.main) - .sink { [weak self] relationshipActionOptionSet in - guard let self = self else { return } - guard let mastodonUser = self.viewModel.mastodonUser.value else { - self.moreMenuBarButtonItem.menu = nil - return - } - let isMuting = relationshipActionOptionSet.contains(.muting) - let isBlocking = relationshipActionOptionSet.contains(.blocking) - let needsShareAction = self.viewModel.isMeBarButtonItemsHidden.value - self.moreMenuBarButtonItem.menu = UserProviderFacade.createProfileActionMenu(for: mastodonUser, isMuting: isMuting, isBlocking: isBlocking, needsShareAction: needsShareAction, provider: self, sourceView: nil, barButtonItem: self.moreMenuBarButtonItem) + Publishers.CombineLatest( + viewModel.relationshipActionOptionSet, + viewModel.context.blockDomainService.blockedDomains + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] relationshipActionOptionSet,domains in + guard let self = self else { return } + guard let mastodonUser = self.viewModel.mastodonUser.value else { + self.moreMenuBarButtonItem.menu = nil + return } - .store(in: &disposeBag) + guard let currentMastodonUser = self.viewModel.currentMastodonUser.value else { + self.moreMenuBarButtonItem.menu = nil + return + } + guard let currentDomain = self.viewModel.domain.value else { return } + let isMuting = relationshipActionOptionSet.contains(.muting) + let isBlocking = relationshipActionOptionSet.contains(.blocking) + let isDomainBlocking = domains.contains(mastodonUser.domainFromAcct) + let needsShareAction = self.viewModel.isMeBarButtonItemsHidden.value + let isInSameDomain = mastodonUser.domainFromAcct == currentDomain + let isMyself = currentMastodonUser.id == mastodonUser.id + + self.moreMenuBarButtonItem.menu = UserProviderFacade.createProfileActionMenu( + for: mastodonUser, + isMyself: isMyself, + isMuting: isMuting, + isBlocking: isBlocking, + isInSameDomain: isInSameDomain, + isDomainBlocking: isDomainBlocking, + provider: self, + cell: nil, + sourceView: nil, + barButtonItem: self.moreMenuBarButtonItem, + shareUser: needsShareAction ? mastodonUser : nil, + shareStatus: nil) + } + .store(in: &disposeBag) + viewModel.isRelationshipActionButtonHidden .receive(on: DispatchQueue.main) .sink { [weak self] isHidden in @@ -767,7 +792,7 @@ extension ProfileViewController: ProfileHeaderViewDelegate { ) let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unmute, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserMuteRelationship(provider: self) + UserProviderFacade.toggleUserMuteRelationship(provider: self, cell: nil) .sink { _ in // do nothing } receiveValue: { _ in @@ -789,7 +814,7 @@ extension ProfileViewController: ProfileHeaderViewDelegate { ) let unblockAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unblock, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserBlockRelationship(provider: self) + UserProviderFacade.toggleUserBlockRelationship(provider: self, cell: nil) .sink { _ in // do nothing } receiveValue: { _ in diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+Provider.swift similarity index 96% rename from Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift rename to Mastodon/Scene/Profile/Timeline/UserTimelineViewController+Provider.swift index 4fc857812..30029ae5b 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+Provider.swift @@ -1,5 +1,5 @@ // -// UserTimelineViewController+StatusProvider.swift +// UserTimelineViewController+Provider.swift // Mastodon // // Created by MainasuK Cirno on 2021-3-30. @@ -85,3 +85,5 @@ extension UserTimelineViewController: StatusProvider { } } + +extension UserTimelineViewController: UserProvider {} diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+Provider.swift similarity index 96% rename from Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift rename to Mastodon/Scene/PublicTimeline/PublicTimelineViewController+Provider.swift index 04fc526a0..96963914c 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+Provider.swift @@ -1,5 +1,5 @@ // -// PublicTimelineViewController+StatusProvider.swift +// PublicTimelineViewController+Provider.swift // Mastodon // // Created by sxiaojian on 2021/1/27. @@ -85,3 +85,5 @@ extension PublicTimelineViewController: StatusProvider { } } + +extension PublicTimelineViewController: UserProvider {} diff --git a/Mastodon/Scene/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/SearchViewController+Follow.swift index 8b0acda0a..a7f7faf90 100644 --- a/Mastodon/Scene/Search/SearchViewController+Follow.swift +++ b/Mastodon/Scene/Search/SearchViewController+Follow.swift @@ -11,6 +11,13 @@ import Foundation import UIKit extension SearchViewController: UserProvider { + + func mastodonUser(for cell: UITableViewCell?) -> Future { + return Future { promise in + promise(.success(nil)) + } + } + func mastodonUser() -> Future { Future { promise in promise(.success(self.viewModel.mastodonUser.value)) @@ -47,7 +54,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat ) let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unmute, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserMuteRelationship(provider: self) + UserProviderFacade.toggleUserMuteRelationship(provider: self, cell: nil) .sink { _ in // do nothing } receiveValue: { _ in @@ -69,7 +76,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat ) let unblockAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unblock, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserBlockRelationship(provider: self) + UserProviderFacade.toggleUserBlockRelationship(provider: self, cell: nil) .sink { _ in // do nothing } receiveValue: { _ in diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 5d27a6a93..462577a4b 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -356,8 +356,4 @@ extension StatusTableViewCell: ActionToolbarContainerDelegate { delegate?.statusTableViewCell(self, actionToolbarContainer: actionToolbarContainer, likeButtonDidPressed: sender) } - func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) { - - } - } diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift index b777207d2..a2f57dee2 100644 --- a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift +++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift @@ -12,7 +12,6 @@ protocol ActionToolbarContainerDelegate: class { func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, reblogButtonDidPressed sender: UIButton) func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) - func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) } @@ -63,7 +62,6 @@ extension ActionToolbarContainer { replyButton.addTarget(self, action: #selector(ActionToolbarContainer.replyButtonDidPressed(_:)), for: .touchUpInside) reblogButton.addTarget(self, action: #selector(ActionToolbarContainer.reblogButtonDidPressed(_:)), for: .touchUpInside) favoriteButton.addTarget(self, action: #selector(ActionToolbarContainer.favoriteButtonDidPressed(_:)), for: .touchUpInside) - moreButton.addTarget(self, action: #selector(ActionToolbarContainer.moreButtonDidPressed(_:)), for: .touchUpInside) } } @@ -194,11 +192,6 @@ extension ActionToolbarContainer { delegate?.actionToolbarContainer(self, starButtonDidPressed: sender) } - @objc private func moreButtonDidPressed(_ sender: UIButton) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.actionToolbarContainer(self, moreButtonDidPressed: sender) - } - } #if DEBUG diff --git a/Mastodon/Scene/Thread/ThreadViewController+StatusProvider.swift b/Mastodon/Scene/Thread/ThreadViewController+Provider.swift similarity index 96% rename from Mastodon/Scene/Thread/ThreadViewController+StatusProvider.swift rename to Mastodon/Scene/Thread/ThreadViewController+Provider.swift index 05cc6e4b2..a76a22d0b 100644 --- a/Mastodon/Scene/Thread/ThreadViewController+StatusProvider.swift +++ b/Mastodon/Scene/Thread/ThreadViewController+Provider.swift @@ -1,5 +1,5 @@ // -// ThreadViewController+StatusProvider.swift +// ThreadViewController+Provider.swift // Mastodon // // Created by MainasuK Cirno on 2021-4-12. @@ -86,3 +86,5 @@ extension ThreadViewController: StatusProvider { } } + +extension ThreadViewController: UserProvider {} diff --git a/Mastodon/Service/APIService/APIService+DomainBlock.swift b/Mastodon/Service/APIService/APIService+DomainBlock.swift new file mode 100644 index 000000000..887c3f074 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+DomainBlock.swift @@ -0,0 +1,139 @@ +// +// APIService+DomainBlock.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/29. +// + +import Combine +import CommonOSLog +import CoreData +import CoreDataStack +import DateToolsSwift +import Foundation +import MastodonSDK + +extension APIService { + func getDomainblocks( + domain: String, + limit: Int = onceRequestDomainBlocksMaxCount, + authorizationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = authorizationBox.userAuthorization + + let query = Mastodon.API.DomainBlock.Query( + maxID: nil, sinceID: nil, limit: limit + ) + return Mastodon.API.DomainBlock.getDomainblocks( + domain: domain, + session: session, + authorization: authorization, + query: query + ) + .flatMap { response -> AnyPublisher, Error> in + self.backgroundManagedObjectContext.performChanges { + let blockedDomains: [DomainBlock] = { + let request = DomainBlock.sortedFetchRequest + request.predicate = DomainBlock.predicate(domain: authorizationBox.domain, userID: authorizationBox.userID) + request.returnsObjectsAsFaults = false + do { + return try self.backgroundManagedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return [] + } + }() + blockedDomains.forEach { self.backgroundManagedObjectContext.delete($0) } + + response.value.forEach { domain in + // use constrain to avoid repeated save + _ = DomainBlock.insert( + into: self.backgroundManagedObjectContext, + blockedDomain: domain, + domain: authorizationBox.domain, + userID: authorizationBox.userID + ) + } + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content<[String]> in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + func blockDomain( + user: MastodonUser, + authorizationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = authorizationBox.userAuthorization + + return Mastodon.API.DomainBlock.blockDomain( + domain: authorizationBox.domain, + blockDomain: user.domainFromAcct, + session: session, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + self.backgroundManagedObjectContext.performChanges { + let requestMastodonUserRequest = MastodonUser.sortedFetchRequest + requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: authorizationBox.domain, id: authorizationBox.userID) + requestMastodonUserRequest.fetchLimit = 1 + guard let requestMastodonUser = self.backgroundManagedObjectContext.safeFetch(requestMastodonUserRequest).first else { return } + user.update(isDomainBlocking: true, by: requestMastodonUser) + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + + func unblockDomain( + user: MastodonUser, + authorizationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = authorizationBox.userAuthorization + + return Mastodon.API.DomainBlock.unblockDomain( + domain: authorizationBox.domain, + blockDomain: user.domainFromAcct, + session: session, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + self.backgroundManagedObjectContext.performChanges { + let requestMastodonUserRequest = MastodonUser.sortedFetchRequest + requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: authorizationBox.domain, id: authorizationBox.userID) + requestMastodonUserRequest.fetchLimit = 1 + guard let requestMastodonUser = self.backgroundManagedObjectContext.safeFetch(requestMastodonUserRequest).first else { return } + user.update(isDomainBlocking: false, by: requestMastodonUser) + } + .setFailureType(to: Error.self) + .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.swift b/Mastodon/Service/APIService/APIService.swift index 11e6f5cac..b38e2e059 100644 --- a/Mastodon/Service/APIService/APIService.swift +++ b/Mastodon/Service/APIService/APIService.swift @@ -46,6 +46,7 @@ final class APIService { extension APIService { public static let onceRequestStatusMaxCount = 100 public static let onceRequestUserMaxCount = 100 + public static let onceRequestDomainBlocksMaxCount = 100 } extension APIService { diff --git a/Mastodon/Service/BlockDomainService.swift b/Mastodon/Service/BlockDomainService.swift new file mode 100644 index 000000000..036083e60 --- /dev/null +++ b/Mastodon/Service/BlockDomainService.swift @@ -0,0 +1,122 @@ +// +// BlockDomainService.swift +// Mastodon +// +// Created by sxiaojian on 2021/4/29. +// + +import Combine +import CoreData +import CoreDataStack +import Foundation +import MastodonSDK +import OSLog +import UIKit + +final class BlockDomainService { + // input + weak var backgroundManagedObjectContext: NSManagedObjectContext? + weak var authenticationService: AuthenticationService? + + // output + let blockedDomains = CurrentValueSubject<[String], Never>([]) + + init( + backgroundManagedObjectContext: NSManagedObjectContext, + authenticationService: AuthenticationService + ) { + self.backgroundManagedObjectContext = backgroundManagedObjectContext + self.authenticationService = authenticationService + guard let authorizationBox = authenticationService.activeMastodonAuthenticationBox.value else { return } + backgroundManagedObjectContext.perform { + let _blockedDomains: [DomainBlock] = { + let request = DomainBlock.sortedFetchRequest + request.predicate = DomainBlock.predicate(domain: authorizationBox.domain, userID: authorizationBox.userID) + request.returnsObjectsAsFaults = false + do { + return try backgroundManagedObjectContext.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + return [] + } + }() + self.blockedDomains.value = _blockedDomains.map(\.blockedDomain) + } + } + + func blockDomain( + userProvider: UserProvider, + cell: UITableViewCell? + ) { + guard let activeMastodonAuthenticationBox = userProvider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let context = userProvider.context else { + return + } + var mastodonUser: AnyPublisher + if let cell = cell { + mastodonUser = userProvider.mastodonUser(for: cell).eraseToAnyPublisher() + } else { + mastodonUser = userProvider.mastodonUser().eraseToAnyPublisher() + } + mastodonUser + .compactMap { mastodonUser -> AnyPublisher, Error>? in + guard let mastodonUser = mastodonUser else { + return nil + } + return context.apiService.blockDomain(user: mastodonUser, authorizationBox: activeMastodonAuthenticationBox) + } + .switchToLatest() + .flatMap { _ -> AnyPublisher, Error> in + context.apiService.getDomainblocks(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox) + } + .sink { completion in + switch completion { + case .finished: + break + case .failure(let error): + print(error) + } + } receiveValue: { [weak self] response in + self?.blockedDomains.value = response.value + } + .store(in: &userProvider.disposeBag) + } + + func unblockDomain( + userProvider: UserProvider, + cell: UITableViewCell? + ) { + guard let activeMastodonAuthenticationBox = userProvider.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let context = userProvider.context else { + return + } + var mastodonUser: AnyPublisher + if let cell = cell { + mastodonUser = userProvider.mastodonUser(for: cell).eraseToAnyPublisher() + } else { + mastodonUser = userProvider.mastodonUser().eraseToAnyPublisher() + } + mastodonUser + .compactMap { mastodonUser -> AnyPublisher, Error>? in + guard let mastodonUser = mastodonUser else { + return nil + } + return context.apiService.unblockDomain(user: mastodonUser, authorizationBox: activeMastodonAuthenticationBox) + } + .switchToLatest() + .flatMap { _ -> AnyPublisher, Error> in + context.apiService.getDomainblocks(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox) + } + .sink { completion in + switch completion { + case .finished: + break + case .failure(let error): + print(error) + } + } receiveValue: { [weak self] response in + self?.blockedDomains.value = response.value + } + .store(in: &userProvider.disposeBag) + } +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 7a74de8bd..55d5841f7 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -30,6 +30,8 @@ class AppContext: ObservableObject { let statusPublishService = StatusPublishService() let notificationService: NotificationService let settingService: SettingService + + let blockDomainService: BlockDomainService let photoLibraryService = PhotoLibraryService() let documentStore: DocumentStore @@ -73,6 +75,11 @@ class AppContext: ObservableObject { notificationService: _notificationService ) + blockDomainService = BlockDomainService( + backgroundManagedObjectContext: _backgroundManagedObjectContext, + authenticationService: _authenticationService + ) + documentStore = DocumentStore() documentStoreSubscription = documentStore.objectWillChange .receive(on: DispatchQueue.main) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+DomainBlock.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+DomainBlock.swift new file mode 100644 index 000000000..04ed813ab --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+DomainBlock.swift @@ -0,0 +1,146 @@ +// +// File.swift +// +// +// Created by sxiaojian on 2021/4/29. +// + +import Foundation +import Combine + +extension Mastodon.API.DomainBlock { + static func domainBlockEndpointURL(domain: String) -> URL { + Mastodon.API.endpointURL(domain: domain).appendingPathComponent("domain_blocks") + } + + /// Fetch domain blocks + /// + /// - Since: 1.4.0 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/domain_blocks/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `String` nested in the response + public static func getDomainblocks( + domain: String, + session: URLSession, + authorization: Mastodon.API.OAuth.Authorization, + query: Mastodon.API.DomainBlock.Query + ) -> AnyPublisher, Error> { + let url = domainBlockEndpointURL(domain: domain) + let request = Mastodon.API.get(url: url, query: query, authorization: authorization) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: [String].self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + /// Block a domain + /// + /// - Since: 1.4.0 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/domain_blocks/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `String` nested in the response + public static func blockDomain( + domain: String, + blockDomain:String, + session: URLSession, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let query = Mastodon.API.DomainBlock.BlockQuery(domain: blockDomain) + let request = Mastodon.API.post( + url: domainBlockEndpointURL(domain: domain), + query: query, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Empty.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + /// Unblock a domain + /// + /// - Since: 1.4.0 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/domain_blocks/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `String` nested in the response + public static func unblockDomain( + domain: String, + blockDomain:String, + session: URLSession, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let query = Mastodon.API.DomainBlock.BlockDeleteQuery(domain: blockDomain) + let request = Mastodon.API.delete( + url: domainBlockEndpointURL(domain: domain), + query: query, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Empty.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } +} + +extension Mastodon.API.DomainBlock { + public struct Query: GetQuery { + public let maxID: Mastodon.Entity.Status.ID? + public let sinceID: Mastodon.Entity.Status.ID? + public let limit: Int? + + public init( + maxID: Mastodon.Entity.Status.ID?, + sinceID: Mastodon.Entity.Status.ID?, + limit: Int? + ) { + self.maxID = maxID + self.sinceID = sinceID + self.limit = limit + } + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) } + sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) } + limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } + guard !items.isEmpty else { return nil } + return items + } + } + + public struct BlockDeleteQuery: Codable, DeleteQuery { + + public let domain: String + + public init(domain: String) { + self.domain = domain + } + } + + public struct BlockQuery: Codable, PostQuery { + + public let domain: String + + public init(domain: String) { + self.domain = domain + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 75af54090..e202568c5 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -117,6 +117,7 @@ extension Mastodon.API { public enum Notifications { } public enum Subscriptions { } public enum Reports { } + public enum DomainBlock { } } extension Mastodon.API.V2 { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Empty.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Empty.swift new file mode 100644 index 000000000..494151178 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Empty.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by sxiaojian on 2021/4/30. +// + +import Foundation + +extension Mastodon.Entity { + public struct Empty: Codable { + + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Query/Query.swift b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift index b729129bd..7e27eb50a 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/Query.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift @@ -60,3 +60,8 @@ protocol PutQuery: RequestQuery { } // DELETE protocol DeleteQuery: RequestQuery { } + +extension DeleteQuery { + // By default a `DeleteQuery` does not has query items + var queryItems: [URLQueryItem]? { nil } +}