Add displaying and voting on polls in statuses
This commit is contained in:
parent
b0bd27db31
commit
1c36dfcc5f
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// Poll.swift
|
||||
// Pachyderm
|
||||
//
|
||||
// Created by Shadowfacts on 4/25/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class Poll: Codable {
|
||||
public let id: String
|
||||
public let expiresAt: Date?
|
||||
public let expired: Bool
|
||||
public let multiple: Bool
|
||||
public let votesCount: Int
|
||||
public let votersCount: Int?
|
||||
public let voted: Bool?
|
||||
public let ownVotes: [Int]?
|
||||
public let options: [Option]
|
||||
public let emojis: [Emoji]
|
||||
|
||||
public var effectiveExpired: Bool {
|
||||
expired || (expiresAt != nil && expiresAt! < Date())
|
||||
}
|
||||
|
||||
public static func vote(_ pollID: String, choices: [Int]) -> Request<Poll> {
|
||||
return Request<Poll>(method: .post, path: "/api/v1/polls/\(pollID)/votes", body: FormDataBody("choices" => choices, nil))
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case expiresAt = "expires_at"
|
||||
case expired
|
||||
case multiple
|
||||
case votesCount = "votes_count"
|
||||
case votersCount = "voters_count"
|
||||
case voted
|
||||
case ownVotes = "own_votes"
|
||||
case options
|
||||
case emojis
|
||||
}
|
||||
}
|
||||
|
||||
extension Poll {
|
||||
public final class Option: Codable {
|
||||
public let title: String
|
||||
public let votesCount: Int?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case title
|
||||
case votesCount = "votes_count"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ public final class Status: /*StatusProtocol,*/ Decodable {
|
|||
public let pinned: Bool?
|
||||
public let bookmarked: Bool?
|
||||
public let card: Card?
|
||||
public let poll: Poll?
|
||||
|
||||
public var applicationName: String? { application?.name }
|
||||
|
||||
|
@ -132,6 +133,7 @@ public final class Status: /*StatusProtocol,*/ Decodable {
|
|||
case pinned
|
||||
case bookmarked
|
||||
case card
|
||||
case poll
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,10 @@ extension String {
|
|||
let name = "\(name)[]"
|
||||
return values.map { Parameter(name: name, value: $0) }
|
||||
}
|
||||
|
||||
static func =>(name: String, values: [Int]) -> [Parameter] {
|
||||
return name => values.map { $0.description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Parameter: CustomStringConvertible {
|
||||
|
|
|
@ -85,6 +85,10 @@
|
|||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A524F1C81800B82A16 /* ComposeReplyView.swift */; };
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A924F1E01C00B82A16 /* ComposeTextView.swift */; };
|
||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
|
||||
D623A53F2635F6910095BD04 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53E2635F6910095BD04 /* Poll.swift */; };
|
||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
|
||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; };
|
||||
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; };
|
||||
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
||||
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
|
||||
|
@ -215,6 +219,7 @@
|
|||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
|
||||
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */; };
|
||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = D69CCBBE249E6EFD000AF167 /* CrashReporter */; };
|
||||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
||||
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
|
||||
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; };
|
||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
||||
|
@ -454,6 +459,10 @@
|
|||
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyView.swift; sourceTree = "<group>"; };
|
||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyContentView.swift; sourceTree = "<group>"; };
|
||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTextView.swift; sourceTree = "<group>"; };
|
||||
D623A53C2635F5590095BD04 /* StatusPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPollView.swift; sourceTree = "<group>"; };
|
||||
D623A53E2635F6910095BD04 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
|
||||
D623A5402635FB3C0095BD04 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
||||
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionCheckboxView.swift; sourceTree = "<group>"; };
|
||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; };
|
||||
|
@ -586,6 +595,7 @@
|
|||
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
|
||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
|
||||
D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UnknownNotificationTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
||||
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; };
|
||||
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationGroup.swift; sourceTree = "<group>"; };
|
||||
D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -837,6 +847,7 @@
|
|||
D6109A062145756700432DC2 /* LoginSettings.swift */,
|
||||
D61099F22145688600432DC2 /* Mention.swift */,
|
||||
D61099F4214568C300432DC2 /* Notification.swift */,
|
||||
D623A53E2635F6910095BD04 /* Poll.swift */,
|
||||
D61099F62145693500432DC2 /* PushSubscription.swift */,
|
||||
D6109A022145722C00432DC2 /* RegisteredApplication.swift */,
|
||||
D61099F82145698900432DC2 /* Relationship.swift */,
|
||||
|
@ -892,6 +903,17 @@
|
|||
path = "Instance Cell";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D623A53B2635F4E20095BD04 /* Poll */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D623A53C2635F5590095BD04 /* StatusPollView.swift */,
|
||||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */,
|
||||
D623A5402635FB3C0095BD04 /* PollOptionView.swift */,
|
||||
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */,
|
||||
);
|
||||
path = Poll;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D626494023C122C800612E6E /* Asset Picker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1390,6 +1412,7 @@
|
|||
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||
D641C78A213DD926004B4513 /* Status */,
|
||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||
D623A53B2635F4E20095BD04 /* Poll */,
|
||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||
D641C78C213DD937004B4513 /* Notifications */,
|
||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
|
@ -1870,6 +1893,7 @@
|
|||
D61099F5214568C300432DC2 /* Notification.swift in Sources */,
|
||||
D61099EF214566C000432DC2 /* Instance.swift in Sources */,
|
||||
D61099D22144B2E600432DC2 /* Body.swift in Sources */,
|
||||
D623A53F2635F6910095BD04 /* Poll.swift in Sources */,
|
||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */,
|
||||
D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */,
|
||||
D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */,
|
||||
|
@ -1982,7 +2006,9 @@
|
|||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
||||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */,
|
||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||
|
@ -1993,6 +2019,7 @@
|
|||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||
|
@ -2105,6 +2132,7 @@
|
|||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -42,6 +42,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||
@NSManaged public var uri: String // todo: are both uri and url necessary?
|
||||
@NSManaged public var url: URL?
|
||||
@NSManaged private var visibilityString: String
|
||||
@NSManaged private var pollData: Data?
|
||||
@NSManaged public var account: AccountMO
|
||||
@NSManaged public var reblog: StatusMO?
|
||||
|
||||
|
@ -60,6 +61,9 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
|
||||
public var card: Card?
|
||||
|
||||
@LazilyDecoding(from: \StatusMO.pollData, fallback: nil)
|
||||
public var poll: Poll?
|
||||
|
||||
public var pinned: Bool? { pinnedInternal }
|
||||
public var bookmarked: Bool? { bookmarkedInternal }
|
||||
|
||||
|
@ -129,6 +133,8 @@ extension StatusMO {
|
|||
self.uri = status.uri
|
||||
self.url = status.url
|
||||
self.visibility = status.visibility
|
||||
self.poll = status.poll
|
||||
|
||||
if let existing = container.account(for: status.account.id, in: context) {
|
||||
existing.updateFrom(apiAccount: status.account, container: container)
|
||||
self.account = existing
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17510.1" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="URI"/>
|
||||
|
@ -57,6 +57,7 @@
|
|||
<attribute name="mentionsData" attributeType="Binary"/>
|
||||
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="pollData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="reblogged" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="referenceCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
|
@ -74,8 +75,8 @@
|
|||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="329"/>
|
||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="433"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="434"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// PollOptionCheckboxView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/25/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class PollOptionCheckboxView: UIView {
|
||||
|
||||
var isChecked: Bool = false {
|
||||
didSet {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
var readOnly: Bool = true {
|
||||
didSet {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
var voted: Bool = false {
|
||||
didSet {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
|
||||
private let imageView: UIImageView
|
||||
|
||||
init(multiple: Bool) {
|
||||
imageView = UIImageView(image: UIImage(systemName: "checkmark")!)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
let size: CGFloat = 20
|
||||
layer.cornerRadius = (multiple ? 0.1 : 0.5) * size
|
||||
layer.borderWidth = 2
|
||||
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.isHidden = true
|
||||
addSubview(imageView)
|
||||
|
||||
updateStyle()
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
widthAnchor.constraint(equalTo: heightAnchor),
|
||||
widthAnchor.constraint(equalToConstant: size),
|
||||
|
||||
imageView.widthAnchor.constraint(equalTo: widthAnchor, constant: -3),
|
||||
imageView.heightAnchor.constraint(equalTo: heightAnchor, constant: -3),
|
||||
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateStyle() {
|
||||
imageView.isHidden = !isChecked
|
||||
if voted || readOnly {
|
||||
layer.borderColor = UIColor.clear.cgColor
|
||||
} else if isChecked {
|
||||
layer.borderColor = tintColor.cgColor
|
||||
} else {
|
||||
layer.borderColor = UIColor.gray.cgColor
|
||||
}
|
||||
backgroundColor = isChecked && !voted ? tintColor : .clear
|
||||
imageView.tintColor = voted ? .black : .white
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// PollOptionView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/25/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class PollOptionView: UIView {
|
||||
|
||||
private let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
||||
|
||||
let checkbox: PollOptionCheckboxView
|
||||
|
||||
init(poll: Poll, option: Poll.Option) {
|
||||
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
let minHeight: CGFloat = 35
|
||||
layer.cornerRadius = 0.1 * minHeight
|
||||
backgroundColor = unselectedBackgroundColor
|
||||
|
||||
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(checkbox)
|
||||
|
||||
let label = EmojiLabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.numberOfLines = 0
|
||||
label.text = option.title
|
||||
label.setEmojis(poll.emojis, identifier: poll.id)
|
||||
addSubview(label)
|
||||
|
||||
if (poll.voted ?? false) || poll.effectiveExpired,
|
||||
let optionVotes = option.votesCount {
|
||||
let fillView = UIView()
|
||||
fillView.translatesAutoresizingMaskIntoConstraints = false
|
||||
fillView.backgroundColor = tintColor.withAlphaComponent(0.6)
|
||||
fillView.layer.zPosition = -1
|
||||
fillView.layer.cornerRadius = layer.cornerRadius
|
||||
addSubview(fillView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
fillView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
fillView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: CGFloat(optionVotes) / CGFloat(poll.votesCount)),
|
||||
fillView.topAnchor.constraint(equalTo: topAnchor),
|
||||
fillView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
||||
// on the first layout, something is weird and this becomes ambiguous even though it's fine on subsequent layouts
|
||||
// this keeps autolayout from complaining
|
||||
minHeightConstraint.priority = .required - 1
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
minHeightConstraint,
|
||||
|
||||
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||
|
||||
label.topAnchor.constraint(equalTo: topAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
||||
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// PollOptionsView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/26/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class PollOptionsView: UIControl {
|
||||
|
||||
var checkedOptionIndices: [Int] {
|
||||
options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset)
|
||||
}
|
||||
var checkedOptionsChanged: (() -> Void)?
|
||||
|
||||
private let stack: UIStackView
|
||||
private var options: [PollOptionView] = []
|
||||
|
||||
private var poll: Poll!
|
||||
private var animator: UIViewPropertyAnimator!
|
||||
private var currentSelectedOptionIndex: Int?
|
||||
|
||||
private let animationDuration: TimeInterval = 0.1
|
||||
private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
||||
|
||||
override var isEnabled: Bool {
|
||||
didSet {
|
||||
options.forEach { $0.checkbox.readOnly = !isEnabled }
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
stack = UIStackView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 4
|
||||
addSubview(stack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
stack.topAnchor.constraint(equalTo: topAnchor),
|
||||
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateUI(poll: Poll) {
|
||||
self.poll = poll
|
||||
|
||||
options.forEach { $0.removeFromSuperview() }
|
||||
|
||||
options = poll.options.enumerated().map { (index, opt) in
|
||||
let optionView = PollOptionView(poll: poll, option: opt)
|
||||
optionView.checkbox.readOnly = !isEnabled
|
||||
optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
||||
optionView.checkbox.voted = poll.voted ?? false
|
||||
stack.addArrangedSubview(optionView)
|
||||
return optionView
|
||||
}
|
||||
}
|
||||
|
||||
private func selectOption(_ option: PollOptionView) {
|
||||
if poll.multiple {
|
||||
option.checkbox.isChecked.toggle()
|
||||
} else {
|
||||
for opt in options {
|
||||
if opt === option {
|
||||
opt.checkbox.isChecked = true
|
||||
} else {
|
||||
opt.checkbox.isChecked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkedOptionsChanged?()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
// don't let subviews receive touch events
|
||||
return self
|
||||
}
|
||||
|
||||
// MARK: - UIControl
|
||||
|
||||
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
guard isEnabled else {
|
||||
return false
|
||||
}
|
||||
|
||||
for (index, view) in options.enumerated() {
|
||||
if view.point(inside: touch.location(in: view), with: event) {
|
||||
currentSelectedOptionIndex = index
|
||||
|
||||
animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) {
|
||||
view.transform = self.scaledTransform
|
||||
}
|
||||
animator.startAnimation()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
var newIndex: Int? = nil
|
||||
for (index, view) in options.enumerated() {
|
||||
if view.point(inside: touch.location(in: view), with: event) {
|
||||
newIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if newIndex != currentSelectedOptionIndex {
|
||||
currentSelectedOptionIndex = newIndex
|
||||
|
||||
UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) {
|
||||
for (index, view) in self.options.enumerated() {
|
||||
view.transform = index == newIndex ? self.scaledTransform : .identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
super.endTracking(touch, with: event)
|
||||
|
||||
func selectOption() {
|
||||
guard let index = currentSelectedOptionIndex else { return }
|
||||
let option = options[index]
|
||||
animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) {
|
||||
option.transform = .identity
|
||||
self.selectOption(option)
|
||||
}
|
||||
animator.startAnimation()
|
||||
}
|
||||
|
||||
if animator.isRunning {
|
||||
animator.addCompletion { (_) in
|
||||
selectOption()
|
||||
}
|
||||
} else {
|
||||
selectOption()
|
||||
}
|
||||
}
|
||||
|
||||
override func cancelTracking(with event: UIEvent?) {
|
||||
super.cancelTracking(with: event)
|
||||
UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) {
|
||||
for view in self.options {
|
||||
view.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
//
|
||||
// StatusPollView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 4/25/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class StatusPollView: UIView {
|
||||
|
||||
private static let formatter: DateComponentsFormatter = {
|
||||
let f = DateComponentsFormatter()
|
||||
f.includesTimeRemainingPhrase = true
|
||||
f.maximumUnitCount = 1
|
||||
f.unitsStyle = .full
|
||||
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
|
||||
return f
|
||||
}()
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var statusID: String!
|
||||
private var poll: Poll!
|
||||
|
||||
private var optionsView: PollOptionsView!
|
||||
private var voteButton: UIButton!
|
||||
private var infoLabel: UILabel!
|
||||
private var options: [PollOptionView] = []
|
||||
|
||||
private var canVote = true
|
||||
private var animator: UIViewPropertyAnimator!
|
||||
private var currentSelectedOptionIndex: Int!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
backgroundColor = .clear
|
||||
|
||||
optionsView = PollOptionsView(frame: .zero)
|
||||
optionsView.translatesAutoresizingMaskIntoConstraints = false
|
||||
optionsView.checkedOptionsChanged = self.checkedOptionsChanged
|
||||
addSubview(optionsView)
|
||||
|
||||
infoLabel = UILabel()
|
||||
infoLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
infoLabel.textColor = .secondaryLabel
|
||||
infoLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .callout), size: 0)
|
||||
infoLabel.adjustsFontSizeToFitWidth = true
|
||||
addSubview(infoLabel)
|
||||
|
||||
voteButton = UIButton(type: .system)
|
||||
voteButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside)
|
||||
voteButton.setTitle("Vote", for: .normal)
|
||||
voteButton.setTitleColor(.secondaryLabel, for: .disabled)
|
||||
voteButton.titleLabel!.font = infoLabel.font
|
||||
addSubview(voteButton)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
optionsView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
optionsView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
optionsView.topAnchor.constraint(equalTo: topAnchor),
|
||||
|
||||
infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
infoLabel.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
||||
infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
infoLabel.trailingAnchor.constraint(lessThanOrEqualTo: voteButton.leadingAnchor, constant: -8),
|
||||
|
||||
voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
|
||||
voteButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
||||
voteButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func updateUI(status: StatusMO, poll: Poll) {
|
||||
self.statusID = status.id
|
||||
self.poll = poll
|
||||
|
||||
options.forEach { $0.removeFromSuperview() }
|
||||
|
||||
// poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon
|
||||
if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account.id {
|
||||
canVote = false
|
||||
} else {
|
||||
canVote = true
|
||||
}
|
||||
|
||||
optionsView.isEnabled = canVote
|
||||
optionsView.updateUI(poll: poll)
|
||||
|
||||
var expired = false
|
||||
let expiryText: String?
|
||||
if let expiresAt = poll.expiresAt {
|
||||
if expiresAt > Date() {
|
||||
expiryText = StatusPollView.formatter.string(from: Date(), to: expiresAt)
|
||||
} else {
|
||||
expired = true
|
||||
expiryText = nil
|
||||
}
|
||||
} else {
|
||||
expiryText = "Does not expire"
|
||||
}
|
||||
|
||||
let format = NSLocalizedString("poll votes count", comment: "poll total votes count")
|
||||
infoLabel.text = String.localizedStringWithFormat(format, poll.votesCount)
|
||||
if let expiryText = expiryText {
|
||||
infoLabel.text! += ", \(expiryText)"
|
||||
}
|
||||
|
||||
if expired {
|
||||
voteButton.setTitle("Expired", for: .disabled)
|
||||
} else if poll.voted ?? false {
|
||||
voteButton.setTitle("Voted", for: .disabled)
|
||||
} else if poll.multiple {
|
||||
voteButton.setTitle("Select multiple", for: .disabled)
|
||||
} else {
|
||||
voteButton.setTitle("Select one", for: .disabled)
|
||||
}
|
||||
voteButton.isEnabled = false
|
||||
}
|
||||
|
||||
private func checkedOptionsChanged() {
|
||||
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
||||
}
|
||||
|
||||
@objc private func votePressed() {
|
||||
optionsView.isEnabled = false
|
||||
voteButton.isEnabled = false
|
||||
voteButton.setTitle("Voted", for: .disabled)
|
||||
|
||||
let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices)
|
||||
mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
fatalError("error voting in poll: \(error)")
|
||||
|
||||
case let .success(poll, _):
|
||||
let container = self.mastodonController.persistentContainer
|
||||
DispatchQueue.main.async {
|
||||
guard let status = container.status(for: self.statusID) else {
|
||||
return
|
||||
}
|
||||
status.poll = poll
|
||||
if container.viewContext.hasChanges {
|
||||
try! container.viewContext.save()
|
||||
}
|
||||
container.statusSubject.send(status.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -34,6 +34,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var contentTextView: StatusContentTextView!
|
||||
@IBOutlet weak var cardView: StatusCardView!
|
||||
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
||||
@IBOutlet weak var pollView: StatusPollView!
|
||||
@IBOutlet weak var replyButton: UIButton!
|
||||
@IBOutlet weak var favoriteButton: UIButton!
|
||||
@IBOutlet weak var reblogButton: UIButton!
|
||||
|
@ -213,6 +214,14 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
|
||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton))
|
||||
|
||||
if let poll = status.poll {
|
||||
pollView.isHidden = false
|
||||
pollView.mastodonController = mastodonController
|
||||
pollView.updateUI(status: status, poll: poll)
|
||||
} else {
|
||||
pollView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateUI(account: AccountMO) {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
|
@ -90,26 +89,30 @@
|
|||
</connections>
|
||||
</button>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z0g-HN-gS0" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="124.5" width="343" height="47.5"/>
|
||||
<rect key="frame" x="0.0" y="124.5" width="343" height="0.0"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QqC-GR-TLC" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="128.5" width="343" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="65" id="Tdo-Hv-ITE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="128.5" width="343" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" secondItem="IF9-9U-Gk0" secondAttribute="width" multiplier="9:16" priority="999" id="5oh-eK-J5d"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TLv-Xu-tT1" customClass="StatusPollView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="132.5" width="343" height="39.5"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ejU-sO-Og5">
|
||||
<rect key="frame" x="0.0" y="180" width="343" height="0.5"/>
|
||||
<color key="backgroundColor" systemColor="opaqueSeparatorColor"/>
|
||||
|
@ -221,6 +224,7 @@
|
|||
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
|
||||
<constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/>
|
||||
<constraint firstItem="3Fp-Nj-sVj" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="kfI-WN-ouW"/>
|
||||
<constraint firstItem="TLv-Xu-tT1" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="v87-hd-fd4"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
|
@ -244,6 +248,7 @@
|
|||
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
|
||||
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/>
|
||||
<outlet property="moreButton" destination="Ujo-Ap-dmK" id="2ba-5w-HDx"/>
|
||||
<outlet property="pollView" destination="TLv-Xu-tT1" id="hJX-YD-lNr"/>
|
||||
<outlet property="profileDetailContainerView" destination="Cnd-Fj-B7l" id="wco-VB-VQx"/>
|
||||
<outlet property="reblogButton" destination="GUG-f7-Hdy" id="WtT-Ph-DQm"/>
|
||||
<outlet property="replyButton" destination="2cc-lE-AdG" id="My8-JV-Nho"/>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -109,26 +108,30 @@
|
|||
</connections>
|
||||
</button>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="83" width="277" height="86.5"/>
|
||||
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LKo-VB-XWl" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="167.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="65" id="khY-jm-CPn"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="167.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" secondItem="nbq-yr-2mA" secondAttribute="width" multiplier="9:16" priority="999" id="Rvt-zs-fkd"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="x3b-Zl-9F0" customClass="StatusPollView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
||||
|
@ -254,6 +257,7 @@
|
|||
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
|
||||
<outlet property="moreButton" destination="982-J4-NGl" id="Pux-tL-aWe"/>
|
||||
<outlet property="pinImageView" destination="wtt-8G-Ua1" id="mE8-oe-m1l"/>
|
||||
<outlet property="pollView" destination="x3b-Zl-9F0" id="WIF-Oz-cnm"/>
|
||||
<outlet property="reblogButton" destination="6tW-z8-Qh9" id="u2t-8D-kOn"/>
|
||||
<outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
|
||||
<outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
|
||||
|
|
|
@ -2,10 +2,26 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>poll votes count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%2$#@votes@</string>
|
||||
<key>votes</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>u</string>
|
||||
<key>one</key>
|
||||
<string>1 vote</string>
|
||||
<key>other</key>
|
||||
<string>%u votes</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>trending hashtag info</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@accounts@, %#@posts@ recently</string>
|
||||
<string>%1$#@accounts@, %2$#@posts@ recently</string>
|
||||
<key>posts</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
|
|
Loading…
Reference in New Issue