diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 1ef9f929d..96ec6971d 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -150,7 +150,7 @@ - + @@ -168,9 +168,9 @@ - - + + \ No newline at end of file diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e1283e374..7509264bb 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -153,7 +153,7 @@ DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; }; DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; }; DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; }; - DB92CF7225E7BB98002C1017 /* PollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */; }; + DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.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 */; }; DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; @@ -379,7 +379,7 @@ DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = ""; }; - DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableViewCell.swift; sourceTree = ""; }; + DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionTableViewCell.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 = ""; }; DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; @@ -684,7 +684,7 @@ 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */, 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */, 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */, - DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */, + DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */, ); path = TableviewCell; sourceTree = ""; @@ -1469,7 +1469,7 @@ 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */, 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, - DB92CF7225E7BB98002C1017 /* PollTableViewCell.swift in Sources */, + DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, diff --git a/Mastodon/Diffiable/Item/PollItem.swift b/Mastodon/Diffiable/Item/PollItem.swift index 7a13df413..1ae8f34e3 100644 --- a/Mastodon/Diffiable/Item/PollItem.swift +++ b/Mastodon/Diffiable/Item/PollItem.swift @@ -9,7 +9,7 @@ import Foundation import CoreData enum PollItem { - case pollOpion(objectID: NSManagedObjectID, attribute: Attribute) + case opion(objectID: NSManagedObjectID, attribute: Attribute) } @@ -17,6 +17,10 @@ extension PollItem { class Attribute: Hashable { var voted: Bool = false + init(voted: Bool = false) { + self.voted = voted + } + static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool { return lhs.voted == rhs.voted } @@ -30,10 +34,8 @@ extension PollItem { extension PollItem: Equatable { static func == (lhs: PollItem, rhs: PollItem) -> Bool { switch (lhs, rhs) { - case (.pollOpion(let objectIDLeft, _), .pollOpion(let objectIDRight, _)): + case (.opion(let objectIDLeft, _), .opion(let objectIDRight, _)): return objectIDLeft == objectIDRight - default: - return false } } } @@ -42,7 +44,7 @@ extension PollItem: Equatable { extension PollItem: Hashable { func hash(into hasher: inout Hasher) { switch self { - case .pollOpion(let objectID, _): + case .opion(let objectID, _): hasher.combine(objectID) } } diff --git a/Mastodon/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/PollSection.swift index 9b175c3f9..b48231dbf 100644 --- a/Mastodon/Diffiable/Section/PollSection.swift +++ b/Mastodon/Diffiable/Section/PollSection.swift @@ -9,7 +9,7 @@ import UIKit import CoreData import CoreDataStack -enum PollSection { +enum PollSection: Equatable, Hashable { case main } @@ -19,7 +19,25 @@ extension PollSection { managedObjectContext: NSManagedObjectContext ) -> UITableViewDiffableDataSource { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in - return nil + switch item { + case .opion(let objectID, let attribute): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self), for: indexPath) as! PollOptionTableViewCell + managedObjectContext.performAndWait { + let option = managedObjectContext.object(with: objectID) as! PollOption + PollSection.configure(cell: cell, pollOption: option, itemAttribute: attribute) + } + return cell + } } } } + +extension PollSection { + static func configure( + cell: PollOptionTableViewCell, + pollOption: PollOption, + itemAttribute: PollItem.Attribute + ) { + cell.optionLabel.text = pollOption.title + } +} diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 89bf1c6e1..b8838869c 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -155,7 +155,31 @@ extension StatusSection { cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0 // set poll - + if let poll = (toot.reblog ?? toot).poll { + cell.statusView.statusPollTableView.isHidden = false + + let managedObjectContext = toot.managedObjectContext! + cell.statusView.statusPollTableViewDataSource = PollSection.tableViewDiffableDataSource( + for: cell.statusView.statusPollTableView, + managedObjectContext: managedObjectContext + ) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + let pollItems = poll.options + .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 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.statusPollTableView.layoutIfNeeded() + } else { + cell.statusView.statusPollTableView.isHidden = true + } // toolbar let replyCountTitle: String = { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index bb6d6dae4..0937e1fb4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -152,6 +152,10 @@ extension HomeTimelineViewController { self.context.apiService.backgroundManagedObjectContext.delete(toot) } } + .sink { _ in + // do nothing + } + .store(in: &self.disposeBag) case .failure(let error): assertionFailure(error.localizedDescription) } diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index abaa38f33..77cc851c1 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -17,6 +17,8 @@ protocol StatusViewDelegate: class { final class StatusView: UIView { + var statusPollTableViewHeightObservation: NSKeyValueObservation? + static let avatarImageSize = CGSize(width: 42, height: 42) static let avatarImageCornerRadius: CGFloat = 4 static let contentWarningBlurRadius: CGFloat = 12 @@ -24,6 +26,7 @@ final class StatusView: UIView { weak var delegate: StatusViewDelegate? var isStatusTextSensitive = false var statusPollTableViewDataSource: UITableViewDiffableDataSource? + var statusPollTableViewHeightLaoutConstraint: NSLayoutConstraint! let headerContainerStackView = UIStackView() @@ -103,9 +106,11 @@ final class StatusView: UIView { let statusMosaicImageView = MosaicImageViewContainer() let statusPollTableView: UITableView = { - let tableView = UITableView() - tableView.register(PollTableViewCell.self, forCellReuseIdentifier: String(describing: PollTableViewCell.self)) + let tableView = UITableView(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 + tableView.backgroundColor = .clear return tableView }() @@ -144,6 +149,10 @@ final class StatusView: UIView { drawContentWarningImageView() } } + + deinit { + statusPollTableViewHeightObservation = nil + } } @@ -265,8 +274,23 @@ extension StatusView { ]) statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle) statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton) + statusContainerStackView.addArrangedSubview(statusMosaicImageView) + statusPollTableView.translatesAutoresizingMaskIntoConstraints = false statusContainerStackView.addArrangedSubview(statusPollTableView) + statusPollTableViewHeightLaoutConstraint = statusPollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1) + NSLayoutConstraint.activate([ + statusPollTableViewHeightLaoutConstraint, + ]) + + statusPollTableViewHeightObservation = statusPollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in + guard let self = self else { return } + guard self.statusPollTableView.contentSize.height != .zero else { + self.statusPollTableViewHeightLaoutConstraint.constant = 44 + return + } + self.statusPollTableViewHeightLaoutConstraint.constant = self.statusPollTableView.contentSize.height + }) // action toolbar container containerStackView.addArrangedSubview(actionToolbarContainer) @@ -322,14 +346,13 @@ extension StatusView { } } +// MARK: - AvatarConfigurableView extension StatusView: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize } static var configurableAvatarImageCornerRadius: CGFloat { return 4 } var configurableAvatarImageView: UIImageView? { return nil } var configurableAvatarButton: UIButton? { return avatarButton } var configurableVerifiedBadgeImageView: UIImageView? { nil } - - } #if canImport(SwiftUI) && DEBUG diff --git a/Mastodon/Scene/Share/View/TableviewCell/PollTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift similarity index 86% rename from Mastodon/Scene/Share/View/TableviewCell/PollTableViewCell.swift rename to Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift index d41fd7428..5372380bd 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/PollTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/PollOptionTableViewCell.swift @@ -1,5 +1,5 @@ // -// PollTableViewCell.swift +// PollOptionTableViewCell.swift // Mastodon // // Created by MainasuK Cirno on 2021-2-25. @@ -7,8 +7,11 @@ import UIKit -final class PollTableViewCell: UITableViewCell { +final class PollOptionTableViewCell: UITableViewCell { + static let height: CGFloat = optionHeight + 2 * verticalMargin + static let optionHeight: CGFloat = 44 + static let verticalMargin: CGFloat = 5 static let checkmarkImageSize = CGSize(width: 26, height: 26) let roundedBackgroundView = UIView() @@ -57,9 +60,11 @@ final class PollTableViewCell: UITableViewCell { } -extension PollTableViewCell { +extension PollOptionTableViewCell { private func _init() { + selectionStyle = .none + backgroundColor = .clear roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false @@ -69,6 +74,7 @@ extension PollTableViewCell { roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5), + roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh), ]) checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false @@ -77,8 +83,8 @@ extension PollTableViewCell { checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9), checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9), roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9), - checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollTableViewCell.checkmarkImageSize.width).priority(.defaultHigh), - checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollTableViewCell.checkmarkImageSize.height).priority(.defaultHigh), + checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.width).priority(.defaultHigh), + checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.height).priority(.defaultHigh), ]) checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false @@ -104,6 +110,8 @@ extension PollTableViewCell { roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18), optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor), ]) + optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal) + optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal) configureCheckmark(state: .none) } @@ -111,8 +119,12 @@ extension PollTableViewCell { override func layoutSubviews() { super.layoutSubviews() + updateCornerRadius() + } + + private func updateCornerRadius() { roundedBackgroundView.layer.masksToBounds = true - roundedBackgroundView.layer.cornerRadius = roundedBackgroundView.bounds.height * 0.5 + roundedBackgroundView.layer.cornerRadius = PollOptionTableViewCell.optionHeight * 0.5 roundedBackgroundView.layer.cornerCurve = .circular checkmarkBackgroundView.layer.masksToBounds = true @@ -122,7 +134,7 @@ extension PollTableViewCell { } -extension PollTableViewCell { +extension PollOptionTableViewCell { enum CheckmarkState { case none @@ -168,17 +180,17 @@ struct PollTableViewCell_Previews: PreviewProvider { static var controls: some View { Group { UIViewPreview() { - PollTableViewCell() + PollOptionTableViewCell() } .previewLayout(.fixed(width: 375, height: 44 + 10)) UIViewPreview() { - let cell = PollTableViewCell() + let cell = PollOptionTableViewCell() cell.configureCheckmark(state: .off) return cell } .previewLayout(.fixed(width: 375, height: 44 + 10)) UIViewPreview() { - let cell = PollTableViewCell() + let cell = PollOptionTableViewCell() cell.configureCheckmark(state: .on) return cell } diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift index eb1a015b4..900094c57 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift @@ -10,7 +10,6 @@ import UIKit import AVKit import Combine - protocol StatusTableViewCellDelegate: class { func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)