feat: make poll cell label appearance update according to the underneath background
This commit is contained in:
parent
30c035e09a
commit
028f3a9404
|
@ -84,6 +84,7 @@
|
||||||
<relationship name="reblogged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="rebloggedBy" inverseEntity="Toot"/>
|
<relationship name="reblogged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="rebloggedBy" inverseEntity="Toot"/>
|
||||||
<relationship name="toots" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="author" inverseEntity="Toot"/>
|
<relationship name="toots" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="author" inverseEntity="Toot"/>
|
||||||
<relationship name="votePollOptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
|
<relationship name="votePollOptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
|
||||||
|
<relationship name="votePolls" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Mention" representedClassName=".Mention" syncable="YES">
|
<entity name="Mention" representedClassName=".Mention" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
|
@ -105,6 +106,7 @@
|
||||||
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<relationship name="options" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
|
<relationship name="options" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
|
||||||
<relationship name="toot" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="poll" inverseEntity="Toot"/>
|
<relationship name="toot" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="poll" inverseEntity="Toot"/>
|
||||||
|
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="PollOption" representedClassName=".PollOption" syncable="YES">
|
<entity name="PollOption" representedClassName=".PollOption" syncable="YES">
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
@ -166,9 +168,9 @@
|
||||||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="299"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="314"/>
|
||||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||||
<element name="Poll" positionX="72" positionY="162" width="128" height="179"/>
|
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||||
<element name="Toot" positionX="0" positionY="0" width="128" height="539"/>
|
<element name="Toot" positionX="0" positionY="0" width="128" height="539"/>
|
||||||
|
|
|
@ -38,6 +38,7 @@ final public class MastodonUser: NSManagedObject {
|
||||||
@NSManaged public private(set) var muted: Set<Toot>?
|
@NSManaged public private(set) var muted: Set<Toot>?
|
||||||
@NSManaged public private(set) var bookmarked: Set<Toot>?
|
@NSManaged public private(set) var bookmarked: Set<Toot>?
|
||||||
@NSManaged public private(set) var votePollOptions: Set<PollOption>?
|
@NSManaged public private(set) var votePollOptions: Set<PollOption>?
|
||||||
|
@NSManaged public private(set) var votePolls: Set<Poll>?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,9 @@ public final class Poll: NSManagedObject {
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var options: Set<PollOption>
|
@NSManaged public private(set) var options: Set<PollOption>
|
||||||
|
|
||||||
|
// many-to-many relationship
|
||||||
|
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Poll {
|
extension Poll {
|
||||||
|
@ -39,6 +42,7 @@ extension Poll {
|
||||||
public static func insert(
|
public static func insert(
|
||||||
into context: NSManagedObjectContext,
|
into context: NSManagedObjectContext,
|
||||||
property: Property,
|
property: Property,
|
||||||
|
votedBy: MastodonUser?,
|
||||||
options: [PollOption]
|
options: [PollOption]
|
||||||
) -> Poll {
|
) -> Poll {
|
||||||
let poll: Poll = context.insertObject()
|
let poll: Poll = context.insertObject()
|
||||||
|
@ -50,7 +54,12 @@ extension Poll {
|
||||||
poll.votesCount = property.votesCount
|
poll.votesCount = property.votesCount
|
||||||
poll.votersCount = property.votersCount
|
poll.votersCount = property.votersCount
|
||||||
|
|
||||||
|
|
||||||
poll.updatedAt = property.networkDate
|
poll.updatedAt = property.networkDate
|
||||||
|
|
||||||
|
if let votedBy = votedBy {
|
||||||
|
poll.mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(votedBy)
|
||||||
|
}
|
||||||
poll.mutableSetValue(forKey: #keyPath(Poll.options)).addObjects(from: options)
|
poll.mutableSetValue(forKey: #keyPath(Poll.options)).addObjects(from: options)
|
||||||
|
|
||||||
return poll
|
return poll
|
||||||
|
@ -80,6 +89,18 @@ extension Poll {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func update(voted: Bool, by: MastodonUser) {
|
||||||
|
if voted {
|
||||||
|
if !(votedBy ?? Set()).contains(by) {
|
||||||
|
mutableSetValue(forKey: #keyPath(Poll.votedBy)).add(by)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (votedBy ?? Set()).contains(by) {
|
||||||
|
mutableSetValue(forKey: #keyPath(Poll.votedBy)).remove(by)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func didUpdate(at networkDate: Date) {
|
public func didUpdate(at networkDate: Date) {
|
||||||
self.updatedAt = networkDate
|
self.updatedAt = networkDate
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,9 +56,15 @@ extension PollOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(votedBy: MastodonUser) {
|
public func update(voted: Bool, by: MastodonUser) {
|
||||||
if !(self.votedBy ?? Set()).contains(votedBy) {
|
if voted {
|
||||||
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(votedBy)
|
if !(self.votedBy ?? Set()).contains(by) {
|
||||||
|
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(by)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !(self.votedBy ?? Set()).contains(by) {
|
||||||
|
self.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).remove(by)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,8 @@
|
||||||
"single": "%d voter",
|
"single": "%d voter",
|
||||||
"multiple": "%d voters",
|
"multiple": "%d voters",
|
||||||
},
|
},
|
||||||
"time_left": "%s left"
|
"time_left": "%s left",
|
||||||
|
"closed": "Closed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
|
|
|
@ -129,6 +129,7 @@
|
||||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.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 */; };
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
|
||||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
||||||
|
DB59F11825EFA35B001F1DAB /* VoteProgressStripView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */; };
|
||||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
||||||
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
||||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||||
|
@ -357,6 +358,7 @@
|
||||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
||||||
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
|
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
|
||||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
||||||
|
DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoteProgressStripView.swift; sourceTree = "<group>"; };
|
||||||
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
|
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = "<group>"; };
|
||||||
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = "<group>"; };
|
||||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||||
|
@ -525,6 +527,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D152A8B25C295CC009AA50C /* StatusView.swift */,
|
2D152A8B25C295CC009AA50C /* StatusView.swift */,
|
||||||
|
DB59F11725EFA35B001F1DAB /* VoteProgressStripView.swift */,
|
||||||
);
|
);
|
||||||
path = Content;
|
path = Content;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1520,6 +1523,7 @@
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||||
|
DB59F11825EFA35B001F1DAB /* VoteProgressStripView.swift in Sources */,
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||||
|
|
|
@ -15,20 +15,34 @@ enum PollItem {
|
||||||
|
|
||||||
extension PollItem {
|
extension PollItem {
|
||||||
class Attribute: Hashable {
|
class Attribute: Hashable {
|
||||||
// var pollVotable: Bool
|
|
||||||
var isOptionVoted: Bool
|
|
||||||
|
|
||||||
init(isOptionVoted: Bool) {
|
enum SelectState: Equatable, Hashable {
|
||||||
// self.pollVotable = pollVotable
|
case none
|
||||||
self.isOptionVoted = isOptionVoted
|
case off
|
||||||
|
case on
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VoteState: Equatable, Hashable {
|
||||||
|
case hidden
|
||||||
|
case reveal(voted: Bool, percentage: Double)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectState: SelectState
|
||||||
|
var voteState: VoteState
|
||||||
|
|
||||||
|
init(selectState: SelectState, voteState: VoteState) {
|
||||||
|
self.selectState = selectState
|
||||||
|
self.voteState = voteState
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool {
|
static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool {
|
||||||
return lhs.isOptionVoted == rhs.isOptionVoted
|
return lhs.selectState == rhs.selectState &&
|
||||||
|
lhs.voteState == rhs.voteState
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(isOptionVoted)
|
hasher.combine(selectState)
|
||||||
|
hasher.combine(voteState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,49 @@ extension PollSection {
|
||||||
itemAttribute: PollItem.Attribute
|
itemAttribute: PollItem.Attribute
|
||||||
) {
|
) {
|
||||||
cell.optionLabel.text = pollOption.title
|
cell.optionLabel.text = pollOption.title
|
||||||
cell.configure(state: itemAttribute.isOptionVoted ? .on : .off)
|
configure(cell: cell, selectState: itemAttribute.selectState)
|
||||||
|
configure(cell: cell, voteState: itemAttribute.voteState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PollSection {
|
||||||
|
|
||||||
|
static func configure(cell: PollOptionTableViewCell, selectState state: PollItem.Attribute.SelectState) {
|
||||||
|
switch state {
|
||||||
|
case .none:
|
||||||
|
cell.checkmarkBackgroundView.isHidden = true
|
||||||
|
cell.checkmarkImageView.isHidden = true
|
||||||
|
case .off:
|
||||||
|
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||||
|
cell.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
||||||
|
cell.checkmarkBackgroundView.layer.borderWidth = 1
|
||||||
|
cell.checkmarkBackgroundView.isHidden = false
|
||||||
|
cell.checkmarkImageView.isHidden = true
|
||||||
|
case .on:
|
||||||
|
cell.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||||
|
cell.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
||||||
|
cell.checkmarkBackgroundView.layer.borderWidth = 0
|
||||||
|
cell.checkmarkBackgroundView.isHidden = false
|
||||||
|
cell.checkmarkImageView.isHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.selectState = state
|
||||||
|
}
|
||||||
|
|
||||||
|
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
|
||||||
|
switch state {
|
||||||
|
case .hidden:
|
||||||
|
cell.optionPercentageLabel.isHidden = true
|
||||||
|
case .reveal(let voted, let percentage):
|
||||||
|
cell.optionPercentageLabel.isHidden = false
|
||||||
|
cell.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
|
||||||
|
cell.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
|
||||||
|
cell.voteProgressStripView.progress.send(CGFloat(percentage))
|
||||||
|
}
|
||||||
|
cell.voteState = state
|
||||||
|
|
||||||
|
cell.layoutIfNeeded()
|
||||||
|
cell.updateTextAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -66,6 +66,9 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StatusSection {
|
||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
|
@ -155,52 +158,20 @@ extension StatusSection {
|
||||||
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
||||||
|
|
||||||
// set poll
|
// set poll
|
||||||
if let poll = (toot.reblog ?? toot).poll {
|
let poll = (toot.reblog ?? toot).poll
|
||||||
cell.statusView.pollTableView.isHidden = false
|
configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, poll: poll, requestUserID: requestUserID)
|
||||||
cell.statusView.pollStatusStackView.isHidden = false
|
if let poll = poll {
|
||||||
cell.statusView.pollVoteButton.isHidden = !poll.multiple
|
ManagedObjectObserver.observe(object: poll)
|
||||||
cell.statusView.pollVoteCountLabel.text = {
|
.sink { _ in
|
||||||
if poll.multiple {
|
// do nothing
|
||||||
let count = poll.votersCount?.intValue ?? 0
|
} receiveValue: { change in
|
||||||
if count > 1 {
|
guard case let .update(object) = change.changeType,
|
||||||
return L10n.Common.Controls.Status.Poll.VoterCount.single(count)
|
let newPoll = object as? Poll else { return }
|
||||||
} else {
|
StatusSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, poll: newPoll, requestUserID: requestUserID)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
.store(in: &cell.disposeBag)
|
||||||
|
|
||||||
let managedObjectContext = toot.managedObjectContext!
|
|
||||||
cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource(
|
|
||||||
for: cell.statusView.pollTableView,
|
|
||||||
managedObjectContext: managedObjectContext
|
|
||||||
)
|
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
|
|
||||||
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(isOptionVoted: isVoted)
|
|
||||||
let option = PollItem.opion(objectID: option.objectID, attribute: attribute)
|
|
||||||
return option
|
|
||||||
}
|
|
||||||
snapshot.appendItems(pollItems, toSection: .main)
|
|
||||||
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
|
// toolbar
|
||||||
let replyCountTitle: String = {
|
let replyCountTitle: String = {
|
||||||
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0
|
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0
|
||||||
|
@ -244,6 +215,104 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func configure(
|
||||||
|
cell: StatusTableViewCell,
|
||||||
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
|
poll: Poll?,
|
||||||
|
requestUserID: String
|
||||||
|
) {
|
||||||
|
guard let poll = poll,
|
||||||
|
let managedObjectContext = poll.managedObjectContext else {
|
||||||
|
cell.statusView.pollTableView.isHidden = true
|
||||||
|
cell.statusView.pollStatusStackView.isHidden = true
|
||||||
|
cell.statusView.pollVoteButton.isHidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if poll.expired {
|
||||||
|
cell.pollCountdownSubscription = nil
|
||||||
|
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed
|
||||||
|
} else if let expiresAt = poll.expiresAt {
|
||||||
|
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
|
||||||
|
cell.pollCountdownSubscription = timestampUpdatePublisher
|
||||||
|
.sink { _ in
|
||||||
|
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assertionFailure()
|
||||||
|
cell.pollCountdownSubscription = nil
|
||||||
|
cell.statusView.pollCountdownLabel.text = "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.statusView.pollTableView.allowsSelection = !poll.expired
|
||||||
|
cell.statusView.pollTableView.allowsMultipleSelection = poll.multiple
|
||||||
|
|
||||||
|
cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource(
|
||||||
|
for: cell.statusView.pollTableView,
|
||||||
|
managedObjectContext: managedObjectContext
|
||||||
|
)
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
let votedOptions = poll.options.filter { option in
|
||||||
|
(option.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
|
||||||
|
}
|
||||||
|
let isPollVoted = (poll.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
|
||||||
|
let pollItems = poll.options
|
||||||
|
.sorted(by: { $0.index.intValue < $1.index.intValue })
|
||||||
|
.map { option -> PollItem in
|
||||||
|
let attribute: PollItem.Attribute = {
|
||||||
|
let selectState: PollItem.Attribute.SelectState = {
|
||||||
|
if isPollVoted {
|
||||||
|
guard !votedOptions.isEmpty else {
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
return votedOptions.contains(option) ? .on : .off
|
||||||
|
} else if poll.expired {
|
||||||
|
return .none
|
||||||
|
} else {
|
||||||
|
return .off
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
let voteState: PollItem.Attribute.VoteState = {
|
||||||
|
guard isPollVoted else { return .hidden }
|
||||||
|
let percentage: Double = {
|
||||||
|
guard poll.votesCount.intValue > 0 else { return 0.0 }
|
||||||
|
return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue)
|
||||||
|
}()
|
||||||
|
let voted = votedOptions.isEmpty ? true : votedOptions.contains(option)
|
||||||
|
return .reveal(voted: voted, percentage: percentage)
|
||||||
|
}()
|
||||||
|
return PollItem.Attribute(selectState: selectState, voteState: voteState)
|
||||||
|
}()
|
||||||
|
let option = PollItem.opion(objectID: option.objectID, attribute: attribute)
|
||||||
|
return option
|
||||||
|
}
|
||||||
|
snapshot.appendItems(pollItems, toSection: .main)
|
||||||
|
cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusSection {
|
extension StatusSection {
|
||||||
|
|
|
@ -30,6 +30,10 @@ internal enum Asset {
|
||||||
}
|
}
|
||||||
internal enum Colors {
|
internal enum Colors {
|
||||||
internal enum Background {
|
internal enum Background {
|
||||||
|
internal enum Poll {
|
||||||
|
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
||||||
|
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
|
||||||
|
}
|
||||||
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
|
||||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||||
|
|
|
@ -69,6 +69,8 @@ internal enum L10n {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1))
|
return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1))
|
||||||
}
|
}
|
||||||
internal enum Poll {
|
internal enum Poll {
|
||||||
|
/// Closed
|
||||||
|
internal static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed")
|
||||||
/// %@ left
|
/// %@ left
|
||||||
internal static func timeLeft(_ p1: Any) -> String {
|
internal static func timeLeft(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "Common.Controls.Status.Poll.TimeLeft", String(describing: p1))
|
return L10n.tr("Localizable", "Common.Controls.Status.Poll.TimeLeft", String(describing: p1))
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.784",
|
||||||
|
"green" : "0.682",
|
||||||
|
"red" : "0.608"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.851",
|
||||||
|
"green" : "0.565",
|
||||||
|
"red" : "0.169"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
"Common.Controls.Actions.SignUp" = "Sign Up";
|
"Common.Controls.Actions.SignUp" = "Sign Up";
|
||||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||||
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
||||||
|
"Common.Controls.Status.Poll.Closed" = "Closed";
|
||||||
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
||||||
"Common.Controls.Status.Poll.Vote" = "Vote";
|
"Common.Controls.Status.Poll.Vote" = "Vote";
|
||||||
"Common.Controls.Status.Poll.VoteCount.Multiple" = "%d votes";
|
"Common.Controls.Status.Poll.VoteCount.Multiple" = "%d votes";
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
//
|
||||||
|
// VoteProgressStripView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-3-3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class VoteProgressStripView: UIView {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private lazy var stripLayer: CAShapeLayer = {
|
||||||
|
let shapeLayer = CAShapeLayer()
|
||||||
|
shapeLayer.lineCap = .round
|
||||||
|
shapeLayer.fillColor = tintColor.cgColor
|
||||||
|
shapeLayer.strokeColor = UIColor.clear.cgColor
|
||||||
|
return shapeLayer
|
||||||
|
}()
|
||||||
|
|
||||||
|
let progressMaskLayer: CAShapeLayer = {
|
||||||
|
let shapeLayer = CAShapeLayer()
|
||||||
|
shapeLayer.fillColor = UIColor.red.cgColor
|
||||||
|
return shapeLayer
|
||||||
|
}()
|
||||||
|
|
||||||
|
let progress = CurrentValueSubject<CGFloat, Never>(0.0)
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VoteProgressStripView {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
updateLayerPath()
|
||||||
|
|
||||||
|
layer.addSublayer(stripLayer)
|
||||||
|
|
||||||
|
progress
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] progress in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.updateLayerPath()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
updateLayerPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VoteProgressStripView {
|
||||||
|
private func updateLayerPath() {
|
||||||
|
guard bounds != .zero else { return }
|
||||||
|
|
||||||
|
stripLayer.frame = bounds
|
||||||
|
stripLayer.fillColor = tintColor.cgColor
|
||||||
|
|
||||||
|
stripLayer.path = {
|
||||||
|
let path = UIBezierPath(roundedRect: bounds, cornerRadius: 0)
|
||||||
|
return path.cgPath
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
progressMaskLayer.path = {
|
||||||
|
var rect = bounds
|
||||||
|
let newWidth = progress.value * rect.width
|
||||||
|
let widthChanged = rect.width - newWidth
|
||||||
|
rect.size.width = newWidth
|
||||||
|
switch UIApplication.shared.userInterfaceLayoutDirection {
|
||||||
|
case .rightToLeft:
|
||||||
|
rect.origin.x += widthChanged
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let path = UIBezierPath(rect: rect)
|
||||||
|
return path.cgPath
|
||||||
|
}()
|
||||||
|
stripLayer.mask = progressMaskLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct VoteProgressStripView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview() {
|
||||||
|
VoteProgressStripView()
|
||||||
|
}
|
||||||
|
.frame(width: 100, height: 44)
|
||||||
|
.padding()
|
||||||
|
.background(Color.black)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
UIViewPreview() {
|
||||||
|
let bar = VoteProgressStripView()
|
||||||
|
bar.tintColor = .white
|
||||||
|
bar.progress.value = 0.5
|
||||||
|
return bar
|
||||||
|
}
|
||||||
|
.frame(width: 100, height: 44)
|
||||||
|
.padding()
|
||||||
|
.background(Color.black)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
UIViewPreview() {
|
||||||
|
let bar = VoteProgressStripView()
|
||||||
|
bar.tintColor = .white
|
||||||
|
bar.progress.value = 1.0
|
||||||
|
return bar
|
||||||
|
}
|
||||||
|
.frame(width: 100, height: 44)
|
||||||
|
.padding()
|
||||||
|
.background(Color.black)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -16,9 +16,15 @@ final class PollOptionTableViewCell: UITableViewCell {
|
||||||
static let checkmarkImageSize = CGSize(width: 26, height: 26)
|
static let checkmarkImageSize = CGSize(width: 26, height: 26)
|
||||||
|
|
||||||
private var viewStateDisposeBag = Set<AnyCancellable>()
|
private var viewStateDisposeBag = Set<AnyCancellable>()
|
||||||
private(set) var pollState: PollState = .off
|
var selectState: PollItem.Attribute.SelectState = .off
|
||||||
|
var voteState: PollItem.Attribute.VoteState?
|
||||||
|
|
||||||
let roundedBackgroundView = UIView()
|
let roundedBackgroundView = UIView()
|
||||||
|
let voteProgressStripView: VoteProgressStripView = {
|
||||||
|
let view = VoteProgressStripView()
|
||||||
|
view.tintColor = Asset.Colors.Background.Poll.highlight.color
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
let checkmarkBackgroundView: UIView = {
|
let checkmarkBackgroundView: UIView = {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
|
@ -43,6 +49,8 @@ final class PollOptionTableViewCell: UITableViewCell {
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let optionLabelMiddlePaddingView = UIView()
|
||||||
|
|
||||||
let optionPercentageLabel: UILabel = {
|
let optionPercentageLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
|
@ -64,16 +72,26 @@ final class PollOptionTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||||
super.setSelected(selected, animated: animated)
|
super.setSelected(selected, animated: animated)
|
||||||
|
|
||||||
|
guard let voteState = voteState else { return }
|
||||||
|
switch voteState {
|
||||||
|
case .hidden:
|
||||||
|
let color = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
|
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||||
|
case .reveal:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||||
super.setHighlighted(highlighted, animated: animated)
|
super.setHighlighted(highlighted, animated: animated)
|
||||||
|
|
||||||
switch pollState {
|
guard let voteState = voteState else { return }
|
||||||
case .off, .none:
|
switch voteState {
|
||||||
|
case .hidden:
|
||||||
let color = Asset.Colors.Background.systemGroupedBackground.color
|
let color = Asset.Colors.Background.systemGroupedBackground.color
|
||||||
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
self.roundedBackgroundView.backgroundColor = isHighlighted ? color.withAlphaComponent(0.8) : color
|
||||||
case .on:
|
case .reveal:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +115,15 @@ extension PollOptionTableViewCell {
|
||||||
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh),
|
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
voteProgressStripView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
roundedBackgroundView.addSubview(voteProgressStripView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
voteProgressStripView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor),
|
||||||
|
voteProgressStripView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor),
|
||||||
|
voteProgressStripView.trailingAnchor.constraint(equalTo: roundedBackgroundView.trailingAnchor),
|
||||||
|
voteProgressStripView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
roundedBackgroundView.addSubview(checkmarkBackgroundView)
|
roundedBackgroundView.addSubview(checkmarkBackgroundView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -123,23 +150,32 @@ extension PollOptionTableViewCell {
|
||||||
optionLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
optionLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
optionLabelMiddlePaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
roundedBackgroundView.addSubview(optionLabelMiddlePaddingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
optionLabelMiddlePaddingView.leadingAnchor.constraint(equalTo: optionLabel.trailingAnchor),
|
||||||
|
optionLabelMiddlePaddingView.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||||
|
optionLabelMiddlePaddingView.heightAnchor.constraint(equalToConstant: 4).priority(.defaultHigh),
|
||||||
|
optionLabelMiddlePaddingView.widthAnchor.constraint(greaterThanOrEqualToConstant: 8).priority(.defaultLow),
|
||||||
|
])
|
||||||
|
optionLabelMiddlePaddingView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
|
optionPercentageLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
roundedBackgroundView.addSubview(optionPercentageLabel)
|
roundedBackgroundView.addSubview(optionPercentageLabel)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabel.trailingAnchor, constant: 8),
|
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
|
||||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
|
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
|
||||||
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||||
])
|
])
|
||||||
optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||||
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||||
|
|
||||||
configure(state: .none)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
updateCornerRadius()
|
updateCornerRadius()
|
||||||
|
updateTextAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCornerRadius() {
|
private func updateCornerRadius() {
|
||||||
|
@ -152,41 +188,35 @@ extension PollOptionTableViewCell {
|
||||||
checkmarkBackgroundView.layer.cornerCurve = .circular
|
checkmarkBackgroundView.layer.cornerCurve = .circular
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func updateTextAppearance() {
|
||||||
|
guard let voteState = voteState else {
|
||||||
extension PollOptionTableViewCell {
|
|
||||||
|
|
||||||
enum PollState {
|
|
||||||
case none
|
|
||||||
case off
|
|
||||||
case on
|
|
||||||
}
|
|
||||||
|
|
||||||
func configure(state: PollState) {
|
|
||||||
switch state {
|
|
||||||
case .none:
|
|
||||||
checkmarkBackgroundView.backgroundColor = .clear
|
|
||||||
checkmarkImageView.isHidden = true
|
|
||||||
optionPercentageLabel.isHidden = true
|
|
||||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
optionLabel.textColor = Asset.Colors.Label.primary.color
|
||||||
optionLabel.layer.removeShadow()
|
optionLabel.layer.removeShadow()
|
||||||
case .off:
|
return
|
||||||
checkmarkBackgroundView.backgroundColor = .systemBackground
|
|
||||||
checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
|
||||||
checkmarkBackgroundView.layer.borderWidth = 1
|
|
||||||
checkmarkImageView.isHidden = true
|
|
||||||
optionPercentageLabel.isHidden = true
|
|
||||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
|
||||||
optionLabel.layer.removeShadow()
|
|
||||||
case .on:
|
|
||||||
checkmarkBackgroundView.backgroundColor = .systemBackground
|
|
||||||
checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
|
||||||
checkmarkBackgroundView.layer.borderWidth = 0
|
|
||||||
checkmarkImageView.isHidden = false
|
|
||||||
optionPercentageLabel.isHidden = false
|
|
||||||
optionLabel.textColor = .white
|
|
||||||
optionLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch voteState {
|
||||||
|
case .hidden:
|
||||||
|
optionLabel.textColor = Asset.Colors.Label.primary.color
|
||||||
|
optionLabel.layer.removeShadow()
|
||||||
|
case .reveal(_, let percentage):
|
||||||
|
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.minX {
|
||||||
|
optionLabel.textColor = .white
|
||||||
|
optionLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||||
|
} else {
|
||||||
|
optionLabel.textColor = Asset.Colors.Label.primary.color
|
||||||
|
optionLabel.layer.removeShadow()
|
||||||
|
}
|
||||||
|
|
||||||
|
if CGFloat(percentage) * voteProgressStripView.frame.width > optionLabelMiddlePaddingView.frame.maxX {
|
||||||
|
optionPercentageLabel.textColor = .white
|
||||||
|
optionPercentageLabel.layer.setupShadow(x: 0, y: 0, blur: 4, spread: 0)
|
||||||
|
} else {
|
||||||
|
optionPercentageLabel.textColor = Asset.Colors.Label.primary.color
|
||||||
|
optionPercentageLabel.layer.removeShadow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -205,13 +235,13 @@ struct PollTableViewCell_Previews: PreviewProvider {
|
||||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||||
UIViewPreview() {
|
UIViewPreview() {
|
||||||
let cell = PollOptionTableViewCell()
|
let cell = PollOptionTableViewCell()
|
||||||
cell.configure(state: .off)
|
PollSection.configure(cell: cell, selectState: .off)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||||
UIViewPreview() {
|
UIViewPreview() {
|
||||||
let cell = PollOptionTableViewCell()
|
let cell = PollOptionTableViewCell()
|
||||||
cell.configure(state: .on)
|
PollSection.configure(cell: cell, selectState: .on)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||||
|
|
|
@ -27,6 +27,7 @@ final class StatusTableViewCell: UITableViewCell {
|
||||||
weak var delegate: StatusTableViewCellDelegate?
|
weak var delegate: StatusTableViewCellDelegate?
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var pollCountdownSubscription: AnyCancellable?
|
||||||
var observations = Set<NSKeyValueObservation>()
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
let statusView = StatusView()
|
let statusView = StatusView()
|
||||||
|
|
|
@ -56,7 +56,8 @@ extension APIService.CoreData {
|
||||||
let votedBy: MastodonUser? = (poll.ownVotes ?? []).contains(i) ? requestMastodonUser : nil
|
let votedBy: MastodonUser? = (poll.ownVotes ?? []).contains(i) ? requestMastodonUser : nil
|
||||||
return PollOption.insert(into: managedObjectContext, property: PollOption.Property(index: i, title: option.title, votesCount: option.votesCount, networkDate: networkDate), votedBy: votedBy)
|
return PollOption.insert(into: managedObjectContext, property: PollOption.Property(index: i, title: option.title, votesCount: option.votesCount, networkDate: networkDate), votedBy: votedBy)
|
||||||
}
|
}
|
||||||
let object = Poll.insert(into: managedObjectContext, property: Poll.Property(id: poll.id, expiresAt: poll.expiresAt, expired: poll.expired, multiple: poll.multiple, votesCount: poll.votesCount, votersCount: poll.votersCount, networkDate: networkDate), options: options)
|
let votedBy: MastodonUser? = (poll.voted ?? false) ? requestMastodonUser : nil
|
||||||
|
let object = Poll.insert(into: managedObjectContext, property: Poll.Property(id: poll.id, expiresAt: poll.expiresAt, expired: poll.expired, multiple: poll.multiple, votesCount: poll.votesCount, votersCount: poll.votersCount, networkDate: networkDate), votedBy: votedBy, options: options)
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
let metions = entity.mentions?.compactMap { mention -> Mention in
|
let metions = entity.mentions?.compactMap { mention -> Mention in
|
||||||
|
@ -116,21 +117,8 @@ extension APIService.CoreData {
|
||||||
guard networkDate > toot.updatedAt else { return }
|
guard networkDate > toot.updatedAt else { return }
|
||||||
|
|
||||||
// merge poll
|
// merge poll
|
||||||
if let poll = entity.poll, let oldPoll = toot.poll, poll.options.count == oldPoll.options.count {
|
if let poll = toot.poll, let entity = entity.poll {
|
||||||
oldPoll.update(expiresAt: poll.expiresAt)
|
merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
||||||
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
|
// merge metrics
|
||||||
|
@ -188,12 +176,15 @@ extension APIService.CoreData {
|
||||||
poll.update(expired: entity.expired)
|
poll.update(expired: entity.expired)
|
||||||
poll.update(votesCount: entity.votesCount)
|
poll.update(votesCount: entity.votesCount)
|
||||||
poll.update(votersCount: entity.votersCount)
|
poll.update(votersCount: entity.votersCount)
|
||||||
|
requestMastodonUser.flatMap {
|
||||||
|
poll.update(voted: entity.voted ?? false, by: $0)
|
||||||
|
}
|
||||||
|
|
||||||
let oldOptions = poll.options.sorted(by: { $0.index.intValue < $1.index.intValue })
|
let oldOptions = poll.options.sorted(by: { $0.index.intValue < $1.index.intValue })
|
||||||
for (i, (optionEntity, option)) in zip(entity.options, oldOptions).enumerated() {
|
for (i, (optionEntity, option)) in zip(entity.options, oldOptions).enumerated() {
|
||||||
let votedBy: MastodonUser? = (entity.ownVotes ?? []).contains(i) ? requestMastodonUser : nil
|
let voted: Bool = (entity.ownVotes ?? []).contains(i)
|
||||||
option.update(votesCount: optionEntity.votesCount)
|
option.update(votesCount: optionEntity.votesCount)
|
||||||
votedBy.flatMap { option.update(votedBy: $0) }
|
requestMastodonUser.flatMap { option.update(voted: voted, by: $0) }
|
||||||
option.didUpdate(at: networkDate)
|
option.didUpdate(at: networkDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue