diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index cd08985c..e1283e37 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -109,6 +109,9 @@ DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.swift */; }; DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481AC25EE155900BEFB67 /* Poll.swift */; }; DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B225EE16D000BEFB67 /* PollOption.swift */; }; + DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; }; + DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481C525EE2ADA00BEFB67 /* PollSection.swift */; }; + DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */; }; DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */; }; DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; }; DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; }; @@ -331,6 +334,9 @@ DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = ""; }; DB4481AC25EE155900BEFB67 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = ""; }; DB4481B225EE16D000BEFB67 /* PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOption.swift; sourceTree = ""; }; + DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = ""; }; + DB4481C525EE2ADA00BEFB67 /* PollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollSection.swift; sourceTree = ""; }; + DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollItem.swift; sourceTree = ""; }; DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardResponderService.swift; sourceTree = ""; }; DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; }; @@ -634,8 +640,8 @@ 2D76319C25C151DE00929FB9 /* Diffiable */ = { isa = PBXGroup; children = ( - 2D7631B125C159E700929FB9 /* Item */, 2D76319D25C151F600929FB9 /* Section */, + 2D7631B125C159E700929FB9 /* Item */, ); path = Diffiable; sourceTree = ""; @@ -644,6 +650,7 @@ isa = PBXGroup; children = ( 2D76319E25C1521200929FB9 /* StatusSection.swift */, + DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, ); path = Section; sourceTree = ""; @@ -686,6 +693,7 @@ isa = PBXGroup; children = ( 2D7631B225C159F700929FB9 /* Item.swift */, + DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */, ); path = Item; sourceTree = ""; @@ -1021,6 +1029,7 @@ DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */, 2D42FF8E25C8228A004A627A /* UIButton.swift */, DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */, + DB4481B825EE289600BEFB67 /* UITableView.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 0FAA101B25E10E760017CCDE /* UIFont.swift */, 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift */, @@ -1476,7 +1485,9 @@ DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, + DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, + DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, @@ -1501,6 +1512,7 @@ 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, + DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */, DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, 2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/PollItem.swift b/Mastodon/Diffiable/Item/PollItem.swift new file mode 100644 index 00000000..7a13df41 --- /dev/null +++ b/Mastodon/Diffiable/Item/PollItem.swift @@ -0,0 +1,49 @@ +// +// PollItem.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-2. +// + +import Foundation +import CoreData + +enum PollItem { + case pollOpion(objectID: NSManagedObjectID, attribute: Attribute) +} + + +extension PollItem { + class Attribute: Hashable { + var voted: Bool = false + + static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool { + return lhs.voted == rhs.voted + } + + func hash(into hasher: inout Hasher) { + hasher.combine(voted) + } + } +} + +extension PollItem: Equatable { + static func == (lhs: PollItem, rhs: PollItem) -> Bool { + switch (lhs, rhs) { + case (.pollOpion(let objectIDLeft, _), .pollOpion(let objectIDRight, _)): + return objectIDLeft == objectIDRight + default: + return false + } + } +} + + +extension PollItem: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case .pollOpion(let objectID, _): + hasher.combine(objectID) + } + } +} diff --git a/Mastodon/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/PollSection.swift new file mode 100644 index 00000000..9b175c3f --- /dev/null +++ b/Mastodon/Diffiable/Section/PollSection.swift @@ -0,0 +1,25 @@ +// +// PollSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-2. +// + +import UIKit +import CoreData +import CoreDataStack + +enum PollSection { + case main +} + +extension PollSection { + static func tableViewDiffableDataSource( + for tableView: UITableView, + managedObjectContext: NSManagedObjectContext + ) -> UITableViewDiffableDataSource { + return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in + return nil + } + } +} diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 4fac88b4..89bf1c6e 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -153,6 +153,9 @@ extension StatusSection { let isStatusSensitive = statusContentWarningAttribute?.isStatusSensitive ?? (toot.reblog ?? toot).sensitive cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 + + // set poll + // toolbar let replyCountTitle: String = { diff --git a/Mastodon/Extension/UITableView.swift b/Mastodon/Extension/UITableView.swift new file mode 100644 index 00000000..22ae6c0b --- /dev/null +++ b/Mastodon/Extension/UITableView.swift @@ -0,0 +1,55 @@ +// +// UITableView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-3-2. +// + +import UIKit + +extension UITableView { + + // static let groupedTableViewPaddingHeaderViewHeight: CGFloat = 16 + // static var groupedTableViewPaddingHeaderView: UIView { + // return UIView(frame: CGRect(x: 0, y: 0, width: 100, height: groupedTableViewPaddingHeaderViewHeight)) + // } + +} + +extension UITableView { + + func deselectRow(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) { + guard let indexPathForSelectedRow = indexPathForSelectedRow else { return } + + guard let transitionCoordinator = transitionCoordinator else { + deselectRow(at: indexPathForSelectedRow, animated: animated) + return + } + + transitionCoordinator.animate(alongsideTransition: { _ in + self.deselectRow(at: indexPathForSelectedRow, animated: animated) + }, completion: { context in + if context.isCancelled { + self.selectRow(at: indexPathForSelectedRow, animated: animated, scrollPosition: .none) + } + }) + } + + func blinkRow(at indexPath: IndexPath) { + DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) { [weak self] in + guard let self = self else { return } + guard let cell = self.cellForRow(at: indexPath) else { return } + let backgroundColor = cell.backgroundColor + + UIView.animate(withDuration: 0.3) { + cell.backgroundColor = Asset.Colors.Label.highlight.color.withAlphaComponent(0.5) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + UIView.animate(withDuration: 0.3) { + cell.backgroundColor = backgroundColor + } + } + } + } + } + +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 9c3af1f7..bb6d6dae 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -19,6 +19,7 @@ extension HomeTimelineViewController { identifier: nil, options: .displayInline, children: [ + moveMenu, dropMenu, UIAction(title: "Show Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in guard let self = self else { return } @@ -33,6 +34,41 @@ extension HomeTimelineViewController { return menu } + var moveMenu: UIMenu { + return UIMenu( + title: "Move to…", + image: UIImage(systemName: "arrow.forward.circle"), + identifier: nil, + options: [], + children: [ + UIAction(title: "First Gap", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToTopGapAction(action) + }), + UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToFirstPollToot(action) + }), +// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in +// guard let self = self else { return } +// self.moveToFirstReplyToot(action) +// }), +// UIAction(title: "First Reply Reblog", image: nil, attributes: [], handler: { [weak self] action in +// guard let self = self else { return } +// self.moveToFirstReplyReblog(action) +// }), +// UIAction(title: "First Video Toot", image: nil, attributes: [], handler: { [weak self] action in +// guard let self = self else { return } +// self.moveToFirstVideoToot(action) +// }), +// UIAction(title: "First GIF Toot", image: nil, attributes: [], handler: { [weak self] action in +// guard let self = self else { return } +// self.moveToFirstGIFToot(action) +// }), + ] + ) + } + var dropMenu: UIMenu { return UIMenu( title: "Drop…", @@ -40,9 +76,9 @@ extension HomeTimelineViewController { identifier: nil, options: [], children: [50, 100, 150, 200, 250, 300].map { count in - UIAction(title: "Drop Recent \(count) Tweets", image: nil, attributes: [], handler: { [weak self] action in + UIAction(title: "Drop Recent \(count) Toots", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } - self.dropRecentTweetsAction(action, count: count) + self.dropRecentTootsAction(action, count: count) }) } ) @@ -51,7 +87,42 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { - @objc private func dropRecentTweetsAction(_ sender: UIAction, count: Int) { + @objc private func moveToTopGapAction(_ sender: UIAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + let snapshotTransitioning = diffableDataSource.snapshot() + let item = snapshotTransitioning.itemIdentifiers.first(where: { item in + switch item { + case .homeMiddleLoader: return true + default: return false + } + }) + if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) + } + } + + @objc private func moveToFirstPollToot(_ sender: UIAction) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + let snapshotTransitioning = diffableDataSource.snapshot() + let item = snapshotTransitioning.itemIdentifiers.first(where: { item in + switch item { + case .homeTimelineIndex(let objectID, _): + let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex + let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot + return toot.poll != nil + default: + return false + } + }) + if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) + tableView.blinkRow(at: IndexPath(row: index, section: 0)) + } else { + print("Not found poll toot") + } + } + + @objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index dd5ee97b..44457839 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -110,10 +110,10 @@ final class HomeTimelineViewModel: NSObject { context.authenticationService.activeMastodonAuthentication .sink { [weak self] activeMastodonAuthentication in guard let self = self else { return } - guard let twitterAuthentication = activeMastodonAuthentication else { return } - let activeTwitterUserID = twitterAuthentication.userID + guard let mastodonAuthentication = activeMastodonAuthentication else { return } + let activeMastodonUserID = mastodonAuthentication.userID let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - HomeTimelineIndex.predicate(userID: activeTwitterUserID), + HomeTimelineIndex.predicate(userID: activeMastodonUserID), HomeTimelineIndex.notDeleted() ]) self.timelinePredicate.value = predicate diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index be754ed8..abaa38f3 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -23,6 +23,7 @@ final class StatusView: UIView { weak var delegate: StatusViewDelegate? var isStatusTextSensitive = false + var statusPollTableViewDataSource: UITableViewDiffableDataSource? let headerContainerStackView = UIStackView() @@ -101,6 +102,13 @@ final class StatusView: UIView { }() let statusMosaicImageView = MosaicImageViewContainer() + let statusPollTableView: UITableView = { + let tableView = UITableView() + tableView.register(PollTableViewCell.self, forCellReuseIdentifier: String(describing: PollTableViewCell.self)) + tableView.isScrollEnabled = false + return tableView + }() + // do not use visual effect view due to we blur text only without background let contentWarningBlurContentImageView: UIImageView = { let imageView = UIImageView() @@ -222,7 +230,7 @@ extension StatusView { subtitleContainerStackView.axis = .horizontal subtitleContainerStackView.addArrangedSubview(usernameLabel) - // status container: [status | image / video | audio] + // status container: [status | image / video | audio | poll] containerStackView.addArrangedSubview(statusContainerStackView) statusContainerStackView.axis = .vertical statusContainerStackView.spacing = 10 @@ -258,7 +266,7 @@ extension StatusView { statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle) statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton) statusContainerStackView.addArrangedSubview(statusMosaicImageView) - + statusContainerStackView.addArrangedSubview(statusPollTableView) // action toolbar container containerStackView.addArrangedSubview(actionToolbarContainer) @@ -266,6 +274,8 @@ extension StatusView { headerContainerStackView.isHidden = true statusMosaicImageView.isHidden = true + statusPollTableView.isHidden = true + contentWarningBlurContentImageView.isHidden = true statusContentWarningContainerStackView.isHidden = true statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index 572f23e0..eb1a015b 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -33,6 +33,7 @@ final class StatusTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() statusView.isStatusTextSensitive = false + statusView.statusPollTableView.dataSource = nil statusView.cleanUpContentWarning() disposeBag.removeAll() observations.removeAll()