From 30c035e09a25d68539f0cb63510212e4613a2803 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 3 Mar 2021 16:12:48 +0800 Subject: [PATCH] feat: implement auto refresh logic for Poll --- CoreDataStack/Entity/Poll.swift | 28 +++++++ CoreDataStack/Entity/PollOption.swift | 16 ++++ Localization/app.json | 5 ++ Mastodon.xcodeproj/project.pbxproj | 36 +++++++-- Mastodon/Diffiable/Item/PollItem.swift | 12 +-- Mastodon/Diffiable/Section/PollSection.swift | 3 +- .../Diffiable/Section/StatusSection.swift | 35 ++++++-- Mastodon/Generated/Strings.swift | 12 +++ ...Provider+StatusTableViewCellDelegate.swift | 71 ++++++++++++++++ ...er+TimelinePostTableViewCellDelegate.swift | 81 ------------------- .../StatusProvider+UITableViewDelegate.swift | 71 ++++++++++++++++ .../StatusProvider/StatusProvider.swift | 6 +- ...ableViewCellHeightCacheableContainer.swift | 12 +++ .../Resources/en.lproj/Localizable.strings | 3 + ...imelineViewController+StatusProvider.swift | 30 +++---- .../HomeTimelineViewController.swift | 27 ++++--- .../HomeTimelineViewModel+Diffable.swift | 4 +- ...imelineViewController+StatusProvider.swift | 31 +++---- .../PublicTimelineViewController.swift | 2 +- .../PublicTimelineViewModel+Diffable.swift | 4 +- .../Scene/Share/View/Content/StatusView.swift | 27 +++++-- .../Share/View/TableView/PollTableView.swift | 10 +++ .../PollOptionTableViewCell.swift | 30 +++++-- .../TableviewCell/StatusTableViewCell.swift | 24 +++++- .../APIService/APIService+Favorite.swift | 4 +- .../Service/APIService/APIService+Poll.swift | 71 ++++++++++++++++ .../CoreData/APIService+CoreData+Toot.swift | 61 ++++++++++++-- .../API/Mastodon+API+Favorites.swift | 45 ++++++++++- .../MastodonSDK/API/Mastodon+API+Polls.swift | 51 ++++++++++++ .../API/Mastodon+API+Timeline.swift | 3 +- .../MastodonSDK/API/Mastodon+API.swift | 3 + 31 files changed, 645 insertions(+), 173 deletions(-) create mode 100644 Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift delete mode 100644 Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift create mode 100644 Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift create mode 100644 Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift create mode 100644 Mastodon/Scene/Share/View/TableView/PollTableView.swift create mode 100644 Mastodon/Service/APIService/APIService+Poll.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Polls.swift diff --git a/CoreDataStack/Entity/Poll.swift b/CoreDataStack/Entity/Poll.swift index 1e8b2528f..a7f6d431a 100644 --- a/CoreDataStack/Entity/Poll.swift +++ b/CoreDataStack/Entity/Poll.swift @@ -56,6 +56,34 @@ extension Poll { return poll } + public func update(expiresAt: Date?) { + if self.expiresAt != expiresAt { + self.expiresAt = expiresAt + } + } + + public func update(expired: Bool) { + if self.expired != expired { + self.expired = expired + } + } + + public func update(votesCount: Int) { + if self.votesCount.intValue != votesCount { + self.votesCount = NSNumber(value: votesCount) + } + } + + public func update(votersCount: Int?) { + if self.votersCount?.intValue != votersCount { + self.votersCount = votersCount.flatMap { NSNumber(value: $0) } + } + } + + public func didUpdate(at networkDate: Date) { + self.updatedAt = networkDate + } + } extension Poll { diff --git a/CoreDataStack/Entity/PollOption.swift b/CoreDataStack/Entity/PollOption.swift index f0d3219d8..6c88fe609 100644 --- a/CoreDataStack/Entity/PollOption.swift +++ b/CoreDataStack/Entity/PollOption.swift @@ -50,6 +50,22 @@ extension PollOption { return option } + public func update(votesCount: Int?) { + if self.votesCount?.intValue != votesCount { + self.votesCount = votesCount.flatMap { NSNumber(value: $0) } + } + } + + public func update(votedBy: MastodonUser) { + if !(self.votedBy ?? Set()).contains(votedBy) { + self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(votedBy) + } + } + + public func didUpdate(at networkDate: Date) { + self.updatedAt = networkDate + } + } extension PollOption { diff --git a/Localization/app.json b/Localization/app.json index 7b118b34d..d35443b74 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -60,10 +60,15 @@ "status_content_warning": "content warning", "media_content_warning": "Tap to reveal that may be sensitive", "poll": { + "vote": "Vote", "vote_count": { "single": "%d vote", "multiple": "%d votes", }, + "voter_count": { + "single": "%d voter", + "multiple": "%d voters", + }, "time_left": "%s left" } }, diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 7509264bb..ed78ec478 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -77,7 +77,7 @@ 2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; }; 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; 2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */; }; - 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */; }; + 2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */; }; 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; }; 2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; }; 2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; }; @@ -94,6 +94,7 @@ DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; }; DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; + DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; }; DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; }; DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; @@ -125,6 +126,9 @@ DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; + DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; }; + DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; }; + DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; }; DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; }; @@ -292,7 +296,7 @@ 2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProviderFacade.swift; sourceTree = ""; }; - 2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+TimelinePostTableViewCellDelegate.swift"; sourceTree = ""; }; + 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusTableViewCellDelegate.swift"; sourceTree = ""; }; 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = ""; }; 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectObserver.swift; sourceTree = ""; }; 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = ""; }; @@ -313,6 +317,7 @@ DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; + DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = ""; }; DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; }; DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; @@ -349,6 +354,9 @@ DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; + DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = ""; }; + DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = ""; }; + DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = ""; }; DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = ""; }; DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = ""; }; DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -551,7 +559,8 @@ children = ( 2D38F1FD25CD481700561493 /* StatusProvider.swift */, 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */, - 2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */, + 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */, + DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */, ); path = StatusProvider; sourceTree = ""; @@ -617,9 +626,10 @@ 2D38F1FC25CD47D900561493 /* StatusProvider */, DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */, 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */, + 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */, + DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */, 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */, 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */, - 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */, ); path = Protocol; sourceTree = ""; @@ -672,6 +682,7 @@ 2D42FF7C25C82207004A627A /* ToolBar */, DB9D6C1325E4F97A0051B173 /* Container */, 2D152A8A25C295B8009AA50C /* Content */, + DB1D187125EF5BBD003F1F23 /* TableView */, 2D7631A625C1533800929FB9 /* TableviewCell */, ); path = View; @@ -752,6 +763,14 @@ path = CoreDataStack; sourceTree = ""; }; + DB1D187125EF5BBD003F1F23 /* TableView */ = { + isa = PBXGroup; + children = ( + DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */, + ); + path = TableView; + sourceTree = ""; + }; DB3D0FF725BAA68500EAA174 /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -850,15 +869,16 @@ DB45FB0925CA87BC005A8AC7 /* CoreData */, 2D61335625C1887F00CAE157 /* Persist */, 2D61335D25C1894B00CAE157 /* APIService.swift */, + DB98337E25C9452D00AD9700 /* APIService+APIError.swift */, DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */, 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */, - DB98337E25C9452D00AD9700 /* APIService+APIError.swift */, DB98336A25C9420100AD9700 /* APIService+App.swift */, DB98337025C9443200AD9700 /* APIService+Authentication.swift */, DB98339B25C96DE600AD9700 /* APIService+Account.swift */, 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */, DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */, DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */, + DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */, ); path = APIService; sourceTree = ""; @@ -1480,6 +1500,7 @@ DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */, 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */, + DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, @@ -1499,6 +1520,7 @@ DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, + DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */, DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */, 2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */, DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */, @@ -1515,9 +1537,11 @@ DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, - 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, + 2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */, + DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, + DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/PollItem.swift b/Mastodon/Diffiable/Item/PollItem.swift index 1ae8f34e3..ca1bbc364 100644 --- a/Mastodon/Diffiable/Item/PollItem.swift +++ b/Mastodon/Diffiable/Item/PollItem.swift @@ -15,18 +15,20 @@ enum PollItem { extension PollItem { class Attribute: Hashable { - var voted: Bool = false + // var pollVotable: Bool + var isOptionVoted: Bool - init(voted: Bool = false) { - self.voted = voted + init(isOptionVoted: Bool) { + // self.pollVotable = pollVotable + self.isOptionVoted = isOptionVoted } static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool { - return lhs.voted == rhs.voted + return lhs.isOptionVoted == rhs.isOptionVoted } func hash(into hasher: inout Hasher) { - hasher.combine(voted) + hasher.combine(isOptionVoted) } } } diff --git a/Mastodon/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/PollSection.swift index 08f1f8710..de303d4a0 100644 --- a/Mastodon/Diffiable/Section/PollSection.swift +++ b/Mastodon/Diffiable/Section/PollSection.swift @@ -39,7 +39,6 @@ extension PollSection { itemAttribute: PollItem.Attribute ) { cell.optionLabel.text = pollOption.title - cell.configureCheckmark(state: itemAttribute.voted ? .on : .off) - + cell.configure(state: itemAttribute.isOptionVoted ? .on : .off) } } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 2fd87f4d1..2c88f7f76 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -21,11 +21,11 @@ extension StatusSection { dependency: NeedsDependency, managedObjectContext: NSManagedObjectContext, timestampUpdatePublisher: AnyPublisher, - timelinePostTableViewCellDelegate: StatusTableViewCellDelegate, + statusTableViewCellDelegate: StatusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? ) -> UITableViewDiffableDataSource { - UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in - guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() } + UITableViewDiffableDataSource(tableView: tableView) { [weak statusTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in + guard let statusTableViewCellDelegate = statusTableViewCellDelegate else { return UITableViewCell() } switch item { case .homeTimelineIndex(objectID: let objectID, let attribute): @@ -36,7 +36,7 @@ extension StatusSection { let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusContentWarningAttribute: attribute) } - cell.delegate = timelinePostTableViewCellDelegate + cell.delegate = statusTableViewCellDelegate return cell case .toot(let objectID, let attribute): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell @@ -47,7 +47,7 @@ extension StatusSection { let toot = managedObjectContext.object(with: objectID) as! Toot StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusContentWarningAttribute: attribute) } - cell.delegate = timelinePostTableViewCellDelegate + cell.delegate = statusTableViewCellDelegate return cell case .publicMiddleLoader(let upperTimelineTootID): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell @@ -158,9 +158,27 @@ extension StatusSection { if let poll = (toot.reblog ?? toot).poll { cell.statusView.pollTableView.isHidden = false cell.statusView.pollStatusStackView.isHidden = false + cell.statusView.pollVoteButton.isHidden = !poll.multiple + cell.statusView.pollVoteCountLabel.text = { + if poll.multiple { + let count = poll.votersCount?.intValue ?? 0 + if count > 1 { + return L10n.Common.Controls.Status.Poll.VoterCount.single(count) + } else { + return L10n.Common.Controls.Status.Poll.VoterCount.multiple(count) + } + } else { + let count = poll.votesCount.intValue + if count > 1 { + return L10n.Common.Controls.Status.Poll.VoteCount.single(count) + } else { + return L10n.Common.Controls.Status.Poll.VoteCount.multiple(count) + } + } + }() let managedObjectContext = toot.managedObjectContext! - cell.statusView.statusPollTableViewDataSource = PollSection.tableViewDiffableDataSource( + cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource( for: cell.statusView.pollTableView, managedObjectContext: managedObjectContext ) @@ -171,15 +189,16 @@ extension StatusSection { .sorted(by: { $0.index.intValue < $1.index.intValue }) .map { option -> PollItem in let isVoted = (option.votedBy ?? Set()).map { $0.id }.contains(requestUserID) - let attribute = PollItem.Attribute(voted: isVoted) + let attribute = PollItem.Attribute(isOptionVoted: isVoted) let option = PollItem.opion(objectID: option.objectID, attribute: attribute) return option } snapshot.appendItems(pollItems, toSection: .main) - cell.statusView.statusPollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) + cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) } else { cell.statusView.pollTableView.isHidden = true cell.statusView.pollStatusStackView.isHidden = true + cell.statusView.pollVoteButton.isHidden = true } // toolbar diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index c049f4fca..190657e94 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -73,6 +73,8 @@ internal enum L10n { internal static func timeLeft(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Status.Poll.TimeLeft", String(describing: p1)) } + /// Vote + internal static let vote = L10n.tr("Localizable", "Common.Controls.Status.Poll.Vote") internal enum VoteCount { /// %d votes internal static func multiple(_ p1: Int) -> String { @@ -83,6 +85,16 @@ internal enum L10n { return L10n.tr("Localizable", "Common.Controls.Status.Poll.VoteCount.Single", p1) } } + internal enum VoterCount { + /// %d voters + internal static func multiple(_ p1: Int) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.Poll.VoterCount.Multiple", p1) + } + /// %d voter + internal static func single(_ p1: Int) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.Poll.VoterCount.Single", p1) + } + } } } internal enum Timeline { diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift new file mode 100644 index 000000000..6ef4b5f94 --- /dev/null +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift @@ -0,0 +1,71 @@ +// +// StatusProvider+StatusTableViewCellDelegate.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/8. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import ActiveLabel + +// MARK: - ActionToolbarContainerDelegate +extension StatusTableViewCellDelegate where Self: StatusProvider { + + func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) { + StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell) + } + + func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) { + guard let diffableDataSource = self.tableViewDiffableDataSource else { return } + guard let item = item(for: cell, indexPath: nil) else { return } + + switch item { + case .homeTimelineIndex(_, let attribute): + attribute.isStatusTextSensitive = false + case .toot(_, let attribute): + attribute.isStatusTextSensitive = false + default: + return + } + var snapshot = diffableDataSource.snapshot() + snapshot.reloadItems([item]) + diffableDataSource.apply(snapshot) + } + +} + +extension StatusTableViewCellDelegate where Self: StatusProvider { + + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { + + } + + func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { + guard let diffableDataSource = self.tableViewDiffableDataSource else { return } + guard let item = item(for: cell, indexPath: nil) else { return } + + switch item { + case .homeTimelineIndex(_, let attribute): + attribute.isStatusSensitive = false + case .toot(_, let attribute): + attribute.isStatusSensitive = false + default: + return + } + + var snapshot = diffableDataSource.snapshot() + snapshot.reloadItems([item]) + UIView.animate(withDuration: 0.33) { + cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = nil + cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = 0.0 + } completion: { _ in + diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + } + +} diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift deleted file mode 100644 index 4679969e2..000000000 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+TimelinePostTableViewCellDelegate.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// StatusProvider+TimelinePostTableViewCellDelegate.swift -// Mastodon -// -// Created by sxiaojian on 2021/2/8. -// - -import os.log -import UIKit -import Combine -import CoreData -import CoreDataStack -import MastodonSDK -import ActiveLabel - -// MARK: - ActionToolbarContainerDelegate -extension StatusTableViewCellDelegate where Self: StatusProvider { - - func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) { - StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell) - } - - func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) { - guard let diffableDataSource = self.tableViewDiffableDataSource else { return } - item(for: cell, indexPath: nil) - .receive(on: DispatchQueue.main) - .sink { [weak self] item in - guard let _ = self else { return } - guard let item = item else { return } - switch item { - case .homeTimelineIndex(_, let attribute): - attribute.isStatusTextSensitive = false - case .toot(_, let attribute): - attribute.isStatusTextSensitive = false - default: - return - } - var snapshot = diffableDataSource.snapshot() - snapshot.reloadItems([item]) - diffableDataSource.apply(snapshot) - } - .store(in: &cell.disposeBag) - } - -} - -extension StatusTableViewCellDelegate where Self: StatusProvider { - - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) { - - } - - func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) { - guard let diffableDataSource = self.tableViewDiffableDataSource else { return } - item(for: cell, indexPath: nil) - .receive(on: DispatchQueue.main) - .sink { [weak self] item in - guard let _ = self else { return } - guard let item = item else { return } - switch item { - case .homeTimelineIndex(_, let attribute): - attribute.isStatusSensitive = false - case .toot(_, let attribute): - attribute.isStatusSensitive = false - default: - return - } - - var snapshot = diffableDataSource.snapshot() - snapshot.reloadItems([item]) - UIView.animate(withDuration: 0.33) { - cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = nil - cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = 0.0 - } completion: { _ in - diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) - } - } - .store(in: &cell.disposeBag) - } - -} diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift new file mode 100644 index 000000000..ea222c763 --- /dev/null +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift @@ -0,0 +1,71 @@ +// +// StatusProvider+UITableViewDelegate.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-3. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import MastodonSDK + +extension StatusTableViewCellDelegate where Self: StatusProvider { + // TODO: + // func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + // } + + func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + let now = Date() + var pollID: Mastodon.Entity.Poll.ID? + toot(for: cell, indexPath: indexPath) + .compactMap { [weak self] toot -> AnyPublisher, Error>? in + guard let self = self else { return nil } + guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return nil } + guard let toot = (toot?.reblog ?? toot) else { return nil } + guard let poll = toot.poll else { return nil } + pollID = poll.id + + // not expired AND last update > 60s + guard !poll.expired else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s expired. Skip for update", ((#file as NSString).lastPathComponent), #line, #function, poll.id) + return nil + } + let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt) + guard timeIntervalSinceUpdate > 60 else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s updated in the %.2fs. Skip for update", ((#file as NSString).lastPathComponent), #line, #function, poll.id, timeIntervalSinceUpdate) + return nil + } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info update…", ((#file as NSString).lastPathComponent), #line, #function, poll.id) + + return self.context.apiService.poll( + domain: toot.domain, + pollID: poll.id, + pollObjectID: poll.objectID, + mastodonAuthenticationBox: authenticationBox + ) + } + .setFailureType(to: Error.self) + .switchToLatest() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info fail to update: %s", ((#file as NSString).lastPathComponent), #line, #function, pollID ?? "?", error.localizedDescription) + case .finished: + break + } + }, receiveValue: { response in + let poll = response.value + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info updated", ((#file as NSString).lastPathComponent), #line, #function, poll.id) + }) + .store(in: &disposeBag) + } + +} + +extension StatusTableViewCellDelegate where Self: StatusProvider { + + +} diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider.swift b/Mastodon/Protocol/StatusProvider/StatusProvider.swift index 781ccc9f3..a0a7116fc 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider.swift @@ -7,13 +7,17 @@ import UIKit import Combine +import CoreData import CoreDataStack protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController { + // async func toot() -> Future func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future func toot(for cell: UICollectionViewCell) -> Future + // sync + var managedObjectContext: NSManagedObjectContext { get } var tableViewDiffableDataSource: UITableViewDiffableDataSource? { get } - func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future + func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? } diff --git a/Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift b/Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift new file mode 100644 index 000000000..1b0350086 --- /dev/null +++ b/Mastodon/Protocol/TableViewCellHeightCacheableContainer.swift @@ -0,0 +1,12 @@ +// +// TableViewCellHeightCacheableContainer.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-3. +// + +import UIKit + +protocol TableViewCellHeightCacheableContainer: UIViewController { + // TODO: +} diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 9b1dfdf7a..a9480500f 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -18,8 +18,11 @@ "Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive"; "Common.Controls.Status.Poll.TimeLeft" = "%@ left"; +"Common.Controls.Status.Poll.Vote" = "Vote"; "Common.Controls.Status.Poll.VoteCount.Multiple" = "%d votes"; "Common.Controls.Status.Poll.VoteCount.Single" = "%d vote"; +"Common.Controls.Status.Poll.VoterCount.Multiple" = "%d voters"; +"Common.Controls.Status.Poll.VoterCount.Single" = "%d voter"; "Common.Controls.Status.ShowPost" = "Show Post"; "Common.Controls.Status.StatusContentWarning" = "content warning"; "Common.Controls.Status.UserBoosted" = "%@ boosted"; diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift index 697820072..a0d9204ba 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import CoreData import CoreDataStack // MARK: - StatusProvider @@ -47,25 +48,26 @@ extension HomeTimelineViewController: StatusProvider { return Future { promise in promise(.success(nil)) } } + var managedObjectContext: NSManagedObjectContext { + return viewModel.fetchedResultsController.managedObjectContext + } + var tableViewDiffableDataSource: UITableViewDiffableDataSource? { return viewModel.diffableDataSource } - func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future { - return Future { promise in - guard let diffableDataSource = self.viewModel.diffableDataSource else { - assertionFailure() - promise(.success(nil)) - return - } - guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell), - let item = diffableDataSource.itemIdentifier(for: indexPath) else { - promise(.success(nil)) - return - } - - promise(.success(item)) + func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? { + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + return nil } + + guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { + return nil + } + + return item } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index d3906fd90..b9d0f94e1 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -106,7 +106,7 @@ extension HomeTimelineViewController { viewModel.setupDiffableDataSource( for: tableView, dependency: self, - timelinePostTableViewCellDelegate: self, + statusTableViewCellDelegate: self, timelineMiddleLoaderTableViewCellDelegate: self ) @@ -220,16 +220,21 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer { // MARK: - UITableViewDelegate extension HomeTimelineViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - guard let diffableDataSource = viewModel.diffableDataSource else { return 100 } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 } - - guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else { - return 200 - } - // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription) - - return ceil(frame.height) + // TODO: + // func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + // guard let diffableDataSource = viewModel.diffableDataSource else { return 100 } + // guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 } + // + // guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else { + // return 200 + // } + // // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription) + // + // return ceil(frame.height) + // } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + handleTableView(tableView, willDisplay: cell, forRowAt: indexPath) } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index d5345de4f..fffa4b7f7 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -15,7 +15,7 @@ extension HomeTimelineViewModel { func setupDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, - timelinePostTableViewCellDelegate: StatusTableViewCellDelegate, + statusTableViewCellDelegate: StatusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate ) { let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) @@ -28,7 +28,7 @@ extension HomeTimelineViewModel { dependency: dependency, managedObjectContext: fetchedResultsController.managedObjectContext, timestampUpdatePublisher: timestampUpdatePublisher, - timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate, + statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate ) } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift index 6d83e79af..aceb83718 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift @@ -8,12 +8,13 @@ import os.log import UIKit import Combine +import CoreData import CoreDataStack import MastodonSDK // MARK: - StatusProvider extension PublicTimelineViewController: StatusProvider { - + func toot() -> Future { return Future { promise in promise(.success(nil)) } } @@ -48,25 +49,25 @@ extension PublicTimelineViewController: StatusProvider { return Future { promise in promise(.success(nil)) } } + var managedObjectContext: NSManagedObjectContext { + return viewModel.fetchedResultsController.managedObjectContext + } + var tableViewDiffableDataSource: UITableViewDiffableDataSource? { return viewModel.diffableDataSource } - func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future { - return Future { promise in - guard let diffableDataSource = self.viewModel.diffableDataSource else { - assertionFailure() - promise(.success(nil)) - return - } - guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell), - let item = diffableDataSource.itemIdentifier(for: indexPath) else { - promise(.success(nil)) - return - } - - promise(.success(item)) + func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? { + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + return nil } + guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { + return nil + } + + return item } } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift index dd5ffc84e..98d2dbd94 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -76,7 +76,7 @@ extension PublicTimelineViewController { viewModel.setupDiffableDataSource( for: tableView, dependency: self, - timelinePostTableViewCellDelegate: self, + statusTableViewCellDelegate: self, timelineMiddleLoaderTableViewCellDelegate: self ) } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift index f9c92fa0f..fa17319b4 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift @@ -14,7 +14,7 @@ extension PublicTimelineViewModel { func setupDiffableDataSource( for tableView: UITableView, dependency: NeedsDependency, - timelinePostTableViewCellDelegate: StatusTableViewCellDelegate, + statusTableViewCellDelegate: StatusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate ) { let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) @@ -27,7 +27,7 @@ extension PublicTimelineViewModel { dependency: dependency, managedObjectContext: fetchedResultsController.managedObjectContext, timestampUpdatePublisher: timestampUpdatePublisher, - timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate, + statusTableViewCellDelegate: statusTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate ) items.value = [] diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 55f785ca3..f6095db07 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -25,8 +25,8 @@ final class StatusView: UIView { weak var delegate: StatusViewDelegate? var isStatusTextSensitive = false - var statusPollTableViewDataSource: UITableViewDiffableDataSource? - var statusPollTableViewHeightLaoutConstraint: NSLayoutConstraint! + var pollTableViewDataSource: UITableViewDiffableDataSource? + var pollTableViewHeightLaoutConstraint: NSLayoutConstraint! let headerContainerStackView = UIStackView() @@ -105,8 +105,8 @@ final class StatusView: UIView { }() let statusMosaicImageViewContainer = MosaicImageViewContainer() - let pollTableView: UITableView = { - let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let pollTableView: PollTableView = { + let tableView = PollTableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) tableView.register(PollOptionTableViewCell.self, forCellReuseIdentifier: String(describing: PollOptionTableViewCell.self)) tableView.isScrollEnabled = false tableView.separatorStyle = .none @@ -136,6 +136,15 @@ final class StatusView: UIView { label.text = L10n.Common.Controls.Status.Poll.timeLeft("6 hours") return label }() + let pollVoteButton: UIButton = { + let button = HitTestExpandedButton() + button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .regular)) + button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal) + button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal) + button.setTitleColor(Asset.Colors.Button.highlight.color.withAlphaComponent(0.8), for: .highlighted) + button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled) + return button + }() // do not use visual effect view due to we blur text only without background let contentWarningBlurContentImageView: UIImageView = { @@ -302,18 +311,18 @@ extension StatusView { statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer) pollTableView.translatesAutoresizingMaskIntoConstraints = false statusContainerStackView.addArrangedSubview(pollTableView) - statusPollTableViewHeightLaoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1) + pollTableViewHeightLaoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1) NSLayoutConstraint.activate([ - statusPollTableViewHeightLaoutConstraint, + pollTableViewHeightLaoutConstraint, ]) statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in guard let self = self else { return } guard self.pollTableView.contentSize.height != .zero else { - self.statusPollTableViewHeightLaoutConstraint.constant = 44 + self.pollTableViewHeightLaoutConstraint.constant = 44 return } - self.statusPollTableViewHeightLaoutConstraint.constant = self.pollTableView.contentSize.height + self.pollTableViewHeightLaoutConstraint.constant = self.pollTableView.contentSize.height }) statusContainerStackView.addArrangedSubview(pollStatusStackView) @@ -321,9 +330,11 @@ extension StatusView { pollStatusStackView.addArrangedSubview(pollVoteCountLabel) pollStatusStackView.addArrangedSubview(pollStatusDotLabel) pollStatusStackView.addArrangedSubview(pollCountdownLabel) + pollStatusStackView.addArrangedSubview(pollVoteButton) pollVoteCountLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal) pollStatusDotLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal) pollCountdownLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + pollVoteButton.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal) // action toolbar container containerStackView.addArrangedSubview(actionToolbarContainer) diff --git a/Mastodon/Scene/Share/View/TableView/PollTableView.swift b/Mastodon/Scene/Share/View/TableView/PollTableView.swift new file mode 100644 index 000000000..d90be2b09 --- /dev/null +++ b/Mastodon/Scene/Share/View/TableView/PollTableView.swift @@ -0,0 +1,10 @@ +// +// PollTableView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-3. +// + +import UIKit + +final class PollTableView: UITableView { } diff --git a/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift index eb427bc31..1da1246b1 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift @@ -6,6 +6,7 @@ // import UIKit +import Combine final class PollOptionTableViewCell: UITableViewCell { @@ -14,6 +15,9 @@ final class PollOptionTableViewCell: UITableViewCell { static let verticalMargin: CGFloat = 5 static let checkmarkImageSize = CGSize(width: 26, height: 26) + private var viewStateDisposeBag = Set() + private(set) var pollState: PollState = .off + let roundedBackgroundView = UIView() let checkmarkBackgroundView: UIView = { @@ -57,6 +61,22 @@ final class PollOptionTableViewCell: UITableViewCell { super.init(coder: coder) _init() } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + switch pollState { + case .off, .none: + let color = Asset.Colors.Background.systemGroupedBackground.color + self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color + case .on: + break + } + } } @@ -113,7 +133,7 @@ extension PollOptionTableViewCell { optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal) optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - configureCheckmark(state: .none) + configure(state: .none) } override func layoutSubviews() { @@ -136,13 +156,13 @@ extension PollOptionTableViewCell { extension PollOptionTableViewCell { - enum CheckmarkState { + enum PollState { case none case off case on } - func configureCheckmark(state: CheckmarkState) { + func configure(state: PollState) { switch state { case .none: checkmarkBackgroundView.backgroundColor = .clear @@ -185,13 +205,13 @@ struct PollTableViewCell_Previews: PreviewProvider { .previewLayout(.fixed(width: 375, height: 44 + 10)) UIViewPreview() { let cell = PollOptionTableViewCell() - cell.configureCheckmark(state: .off) + cell.configure(state: .off) return cell } .previewLayout(.fixed(width: 375, height: 44 + 10)) UIViewPreview() { let cell = PollOptionTableViewCell() - cell.configureCheckmark(state: .on) + cell.configure(state: .on) return cell } .previewLayout(.fixed(width: 375, height: 44 + 10)) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 326687b10..1c45bfe2d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -9,13 +9,15 @@ import os.log import UIKit import AVKit import Combine +import CoreData +import CoreDataStack protocol StatusTableViewCellDelegate: class { + var managedObjectContext: NSManagedObjectContext { get } func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) - } final class StatusTableViewCell: UITableViewCell { @@ -32,8 +34,8 @@ final class StatusTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() statusView.isStatusTextSensitive = false - statusView.pollTableView.dataSource = nil statusView.cleanUpContentWarning() + statusView.pollTableView.dataSource = nil disposeBag.removeAll() observations.removeAll() } @@ -85,12 +87,30 @@ extension StatusTableViewCell { bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color statusView.delegate = self + statusView.pollTableView.delegate = self statusView.statusMosaicImageViewContainer.delegate = self statusView.actionToolbarContainer.delegate = self } } +// MARK: - UITableViewDelegate +extension StatusTableViewCell: UITableViewDelegate { + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + if tableView === statusView.pollTableView, let diffableDataSource = statusView.pollTableViewDataSource { + guard let item = diffableDataSource.itemIdentifier(for: indexPath), + case let .opion(objectID, _) = item, + let option = delegate?.managedObjectContext.object(with: objectID) as? PollOption else { + return false + } + + return !option.poll.expired + } else { + return true + } + } +} + // MARK: - StatusViewDelegate extension StatusTableViewCell: StatusViewDelegate { func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) { diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index 34bd3f0e4..e1d5febe7 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -94,7 +94,7 @@ extension APIService { assertionFailure() return } - APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldToot, in: mastodonAuthenticationBox.domain, entity: entity, networkDate: response.networkDate) + APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate) os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "", entity.favouritesCount ) } .setFailureType(to: Error.self) @@ -132,7 +132,7 @@ extension APIService { let requestMastodonUserID = mastodonAuthenticationBox.userID let query = Mastodon.API.Favorites.ListQuery(limit: limit, minID: nil, maxID: maxID) - return Mastodon.API.Favorites.getFavoriteStatus(domain: mastodonAuthenticationBox.domain, session: session, authorization: mastodonAuthenticationBox.userAuthorization, query: query) + return Mastodon.API.Favorites.favoritedStatus(domain: mastodonAuthenticationBox.domain, session: session, authorization: mastodonAuthenticationBox.userAuthorization, query: query) .map { response -> AnyPublisher, Error> in let log = OSLog.api diff --git a/Mastodon/Service/APIService/APIService+Poll.swift b/Mastodon/Service/APIService/APIService+Poll.swift new file mode 100644 index 000000000..33944c227 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Poll.swift @@ -0,0 +1,71 @@ +// +// APIService+Poll.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-3. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import CommonOSLog +import DateToolsSwift +import MastodonSDK + +extension APIService { + + func poll( + domain: String, + pollID: Mastodon.Entity.Poll.ID, + pollObjectID: NSManagedObjectID, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = mastodonAuthenticationBox.userAuthorization + let requestMastodonUserID = mastodonAuthenticationBox.userID + + return Mastodon.API.Polls.poll( + session: session, + domain: domain, + pollID: pollID, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + let entity = response.value + let managedObjectContext = self.backgroundManagedObjectContext + + return managedObjectContext.performChanges { + let _requestMastodonUser: MastodonUser? = { + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + guard let requestMastodonUser = _requestMastodonUser else { + assertionFailure() + return + } + guard let poll = managedObjectContext.object(with: pollObjectID) as? Poll else { return } + APIService.CoreData.merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) + } + .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/CoreData/APIService+CoreData+Toot.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift index eeb2afa2a..6868c668f 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift @@ -44,7 +44,7 @@ extension APIService.CoreData { if let oldToot = oldToot { // merge old Toot - APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldToot,in: domain, entity: entity, networkDate: networkDate) + APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate) return (oldToot, false, false) } else { let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, networkDate: networkDate, log: log) @@ -106,10 +106,34 @@ extension APIService.CoreData { } } - static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Status, networkDate: Date) { + static func merge( + toot: Toot, + entity: Mastodon.Entity.Status, + requestMastodonUser: MastodonUser?, + domain: String, + networkDate: Date + ) { guard networkDate > toot.updatedAt else { return } - // merge + // merge poll + if let poll = entity.poll, let oldPoll = toot.poll, poll.options.count == oldPoll.options.count { + oldPoll.update(expiresAt: poll.expiresAt) + oldPoll.update(expired: poll.expired) + oldPoll.update(votesCount: poll.votesCount) + oldPoll.update(votersCount: poll.votersCount) + + let oldOptions = oldPoll.options.sorted(by: { $0.index.intValue < $1.index.intValue }) + for (i, (option, oldOption)) in zip(poll.options, oldOptions).enumerated() { + let votedBy: MastodonUser? = (poll.ownVotes ?? []).contains(i) ? requestMastodonUser : nil + oldOption.update(votesCount: option.votesCount) + votedBy.flatMap { oldOption.update(votedBy: $0) } + oldOption.didUpdate(at: networkDate) + } + + oldPoll.didUpdate(at: networkDate) + } + + // merge metrics if entity.favouritesCount != toot.favouritesCount.intValue { toot.update(favouritesCount:NSNumber(value: entity.favouritesCount)) } @@ -122,6 +146,7 @@ extension APIService.CoreData { toot.update(reblogsCount:NSNumber(value: entity.reblogsCount)) } + // merge relationship if let mastodonUser = requestMastodonUser { if let favourited = entity.favourited { toot.update(liked: favourited, mastodonUser: mastodonUser) @@ -142,10 +167,36 @@ extension APIService.CoreData { // merge user mergeMastodonUser(for: requestMastodonUser, old: toot.author, in: domain, entity: entity.account, networkDate: networkDate) - // merge indirect reblog & quote + + // merge indirect reblog if let reblog = toot.reblog, let reblogEntity = entity.reblog { - mergeToot(for: requestMastodonUser, old: reblog,in: domain, entity: reblogEntity, networkDate: networkDate) + merge(toot: reblog, entity: reblogEntity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate) } } } + +extension APIService.CoreData { + static func merge( + poll: Poll, + entity: Mastodon.Entity.Poll, + requestMastodonUser: MastodonUser?, + domain: String, + networkDate: Date + ) { + poll.update(expiresAt: entity.expiresAt) + poll.update(expired: entity.expired) + poll.update(votesCount: entity.votesCount) + poll.update(votersCount: entity.votersCount) + + let oldOptions = poll.options.sorted(by: { $0.index.intValue < $1.index.intValue }) + for (i, (optionEntity, option)) in zip(entity.options, oldOptions).enumerated() { + let votedBy: MastodonUser? = (entity.ownVotes ?? []).contains(i) ? requestMastodonUser : nil + option.update(votesCount: optionEntity.votesCount) + votedBy.flatMap { option.update(votedBy: $0) } + option.didUpdate(at: networkDate) + } + + poll.didUpdate(at: networkDate) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift index 6942fa2f1..54a6c7f82 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Favorites.swift @@ -30,6 +30,20 @@ extension Mastodon.API.Favorites { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) } + /// Favourite / Undo Favourite + /// + /// Add a status to your favourites list / Remove a status from your favourites list + /// + /// # Last Update + /// 2021/3/3 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - statusID: Mastodon status id + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Server` nested in the response public static func favorites(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, favoriteKind: FavoriteKind) -> AnyPublisher, Error> { let url: URL = favoriteActionEndpointURL(domain: domain, statusID: statusID, favoriteKind: favoriteKind) var request = Mastodon.API.post(url: url, query: nil, authorization: authorization) @@ -42,7 +56,21 @@ extension Mastodon.API.Favorites { .eraseToAnyPublisher() } - public static func getFavoriteByUserLists(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization) -> AnyPublisher, Error> { + /// Favourited by + /// + /// View who favourited a given status. + /// + /// # Last Update + /// 2021/3/3 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - statusID: Mastodon status id + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Server` nested in the response + public static func favoriteBy(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization) -> AnyPublisher, Error> { let url = favoriteByUserListsEndpointURL(domain: domain, statusID: statusID) let request = Mastodon.API.get(url: url, query: nil, authorization: authorization) return session.dataTaskPublisher(for: request) @@ -53,7 +81,20 @@ extension Mastodon.API.Favorites { .eraseToAnyPublisher() } - public static func getFavoriteStatus(domain: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, query: Mastodon.API.Favorites.ListQuery) -> AnyPublisher, Error> { + /// Favourited statuses + /// + /// Using this endpoint to view the favourited list for user + /// + /// # Last Update + /// 2021/3/3 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/favourites/) + /// - Parameters: + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - session: `URLSession` + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Server` nested in the response + public static func favoritedStatus(domain: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, query: Mastodon.API.Favorites.ListQuery) -> AnyPublisher, Error> { let url = favoritesStatusesEndpointURL(domain: domain) let request = Mastodon.API.get(url: url, query: query, authorization: authorization) return session.dataTaskPublisher(for: request) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Polls.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Polls.swift new file mode 100644 index 000000000..6329a4403 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Polls.swift @@ -0,0 +1,51 @@ +// +// Mastodon+API+Polls.swift +// +// +// Created by MainasuK Cirno on 2021-3-3. +// + +import Foundation +import Combine + +extension Mastodon.API.Polls { + + static func viewPollEndpointURL(domain: String, pollID: Mastodon.Entity.Poll.ID) -> URL { + let pathComponent = "polls/" + pollID + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent) + } + + /// View a poll + /// + /// Using this endpoint to view the poll of status + /// + /// # Last Update + /// 2021/3/3 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/polls/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - pollID: id for poll + /// - authorization: User token. Could be nil if status is public + /// - Returns: `AnyPublisher` contains `Server` nested in the response + public static func poll( + session: URLSession, + domain: String, + pollID: Mastodon.Entity.Poll.ID, + authorization: Mastodon.API.OAuth.Authorization? + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: viewPollEndpointURL(domain: domain, pollID: pollID), + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Poll.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift index d4ec364bf..03a718b5b 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Timeline.swift @@ -53,13 +53,14 @@ extension Mastodon.API.Timeline { /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update - /// 2021/2/19 + /// 2021/3/3 /// # Reference /// [Document](https://https://docs.joinmastodon.org/methods/timelines/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `PublicTimelineQuery` with query parameters + /// - authorization: User token /// - Returns: `AnyPublisher` contains `Token` nested in the response public static func home( session: URLSession, diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 92897090c..5a55ee103 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -5,6 +5,7 @@ // Created by xiaojian sun on 2021/1/25. // +import os.log import Foundation import enum NIOHTTP1.HTTPResponseStatus @@ -93,6 +94,7 @@ extension Mastodon.API { public enum Instance { } public enum OAuth { } public enum Onboarding { } + public enum Polls { } public enum Timeline { } public enum Favorites { } } @@ -155,6 +157,7 @@ extension Mastodon.API { return try Mastodon.API.decoder.decode(type, from: data) } catch let decodeError { #if DEBUG + os_log(.info, "%{public}s[%{public}ld], %{public}s: decode fail. content %s", ((#file as NSString).lastPathComponent), #line, #function, String(data: data, encoding: .utf8) ?? "") debugPrint(decodeError) #endif