commit
ce0fc56cd7
|
@ -83,6 +83,8 @@
|
|||
<relationship name="pinnedToot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="pinnedBy" 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="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 name="Mention" representedClassName=".Mention" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
|
@ -93,6 +95,28 @@
|
|||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="toot" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="mentions" inverseEntity="Toot"/>
|
||||
</entity>
|
||||
<entity name="Poll" representedClassName=".Poll" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="votersCount" optional="YES" 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="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 name="PollOption" representedClassName=".PollOption" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="votesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="poll" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
|
||||
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
|
||||
</entity>
|
||||
<entity name="Tag" representedClassName=".Tag" syncable="YES">
|
||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
|
@ -128,9 +152,10 @@
|
|||
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
|
||||
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="toot" inverseEntity="HomeTimelineIndex"/>
|
||||
<relationship name="mediaAttachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="toot" inverseEntity="Attachment"/>
|
||||
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
|
||||
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
|
||||
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
|
||||
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedToot" inverseEntity="MastodonUser"/>
|
||||
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="toot" inverseEntity="Poll"/>
|
||||
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="reblogFrom" inverseEntity="Toot"/>
|
||||
<relationship name="reblogFrom" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="reblog" inverseEntity="Toot"/>
|
||||
<relationship name="rebloggedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
|
||||
|
@ -138,14 +163,16 @@
|
|||
</entity>
|
||||
<elements>
|
||||
<element name="Application" positionX="160" positionY="192" width="128" height="104"/>
|
||||
<element name="Attachment" positionX="72" positionY="162" width="128" height="14"/>
|
||||
<element name="Emoji" positionX="45" positionY="135" width="128" height="149"/>
|
||||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="314"/>
|
||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
<element name="Toot" positionX="0" positionY="0" width="128" height="524"/>
|
||||
<element name="Attachment" positionX="72" positionY="162" width="128" height="14"/>
|
||||
<element name="Toot" positionX="0" positionY="0" width="128" height="539"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -24,7 +24,7 @@ public final class Application: NSManagedObject {
|
|||
public extension Application {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
identifier = UUID()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(Application.identifier))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
|
@ -36,7 +36,7 @@ public extension Attachment {
|
|||
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
createdAt = Date()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Attachment.createdAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
|
@ -26,7 +26,7 @@ public final class Emoji: NSManagedObject {
|
|||
public extension Emoji {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
identifier = UUID()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(Emoji.identifier))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
|
@ -24,7 +24,7 @@ public final class History: NSManagedObject {
|
|||
public extension History {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
identifier = UUID()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(History.identifier))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
|
@ -36,12 +36,12 @@ extension MastodonAuthentication {
|
|||
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
identifier = UUID()
|
||||
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(MastodonAuthentication.identifier))
|
||||
let now = Date()
|
||||
createdAt = now
|
||||
updatedAt = now
|
||||
activedAt = now
|
||||
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.createdAt))
|
||||
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.updatedAt))
|
||||
setPrimitiveValue(now, forKey: #keyPath(MastodonAuthentication.activedAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
|
@ -37,6 +37,8 @@ final public class MastodonUser: NSManagedObject {
|
|||
@NSManaged public private(set) var reblogged: Set<Toot>?
|
||||
@NSManaged public private(set) var muted: Set<Toot>?
|
||||
@NSManaged public private(set) var bookmarked: Set<Toot>?
|
||||
@NSManaged public private(set) var votePollOptions: Set<PollOption>?
|
||||
@NSManaged public private(set) var votePolls: Set<Poll>?
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@ public final class Mention: NSManagedObject {
|
|||
public extension Mention {
|
||||
override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
identifier = UUID()
|
||||
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(Mention.identifier))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
//
|
||||
// Poll.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class Poll: NSManagedObject {
|
||||
public typealias ID = String
|
||||
|
||||
@NSManaged public private(set) var id: ID
|
||||
@NSManaged public private(set) var expiresAt: Date?
|
||||
@NSManaged public private(set) var expired: Bool
|
||||
@NSManaged public private(set) var multiple: Bool
|
||||
@NSManaged public private(set) var votesCount: NSNumber
|
||||
@NSManaged public private(set) var votersCount: NSNumber?
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// one-to-one relationship
|
||||
@NSManaged public private(set) var toot: Toot
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var options: Set<PollOption>
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||
}
|
||||
|
||||
extension Poll {
|
||||
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(Poll.createdAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
votedBy: MastodonUser?,
|
||||
options: [PollOption]
|
||||
) -> Poll {
|
||||
let poll: Poll = context.insertObject()
|
||||
|
||||
poll.id = property.id
|
||||
poll.expiresAt = property.expiresAt
|
||||
poll.expired = property.expired
|
||||
poll.multiple = property.multiple
|
||||
poll.votesCount = property.votesCount
|
||||
poll.votersCount = property.votersCount
|
||||
|
||||
|
||||
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)
|
||||
|
||||
return poll
|
||||
}
|
||||
|
||||
public func update(expiresAt: Date?) {
|
||||
if self.expiresAt != expiresAt {
|
||||
self.expiresAt = expiresAt
|
||||
}
|
||||
}
|
||||
|
||||
public func update(expired: Bool) {
|
||||
if self.expired != expired {
|
||||
self.expired = expired
|
||||
}
|
||||
}
|
||||
|
||||
public func update(votesCount: Int) {
|
||||
if self.votesCount.intValue != votesCount {
|
||||
self.votesCount = NSNumber(value: votesCount)
|
||||
}
|
||||
}
|
||||
|
||||
public func update(votersCount: Int?) {
|
||||
if self.votersCount?.intValue != votersCount {
|
||||
self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public func 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) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Poll {
|
||||
public struct Property {
|
||||
public let id: ID
|
||||
public let expiresAt: Date?
|
||||
public let expired: Bool
|
||||
public let multiple: Bool
|
||||
public let votesCount: NSNumber
|
||||
public let votersCount: NSNumber?
|
||||
|
||||
public let networkDate: Date
|
||||
|
||||
public init(
|
||||
id: Poll.ID,
|
||||
expiresAt: Date?,
|
||||
expired: Bool,
|
||||
multiple: Bool,
|
||||
votesCount: Int,
|
||||
votersCount: Int?,
|
||||
networkDate: Date
|
||||
) {
|
||||
self.id = id
|
||||
self.expiresAt = expiresAt
|
||||
self.expired = expired
|
||||
self.multiple = multiple
|
||||
self.votesCount = NSNumber(value: votesCount)
|
||||
self.votersCount = votersCount.flatMap { NSNumber(value: $0) }
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Poll: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \Poll.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// PollOption.swift
|
||||
// CoreDataStack
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public final class PollOption: NSManagedObject {
|
||||
@NSManaged public private(set) var index: NSNumber
|
||||
@NSManaged public private(set) var title: String
|
||||
@NSManaged public private(set) var votesCount: NSNumber?
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
// many-to-one relationship
|
||||
@NSManaged public private(set) var poll: Poll
|
||||
|
||||
// many-to-many relationship
|
||||
@NSManaged public private(set) var votedBy: Set<MastodonUser>?
|
||||
}
|
||||
|
||||
extension PollOption {
|
||||
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
setPrimitiveValue(Date(), forKey: #keyPath(PollOption.createdAt))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property,
|
||||
votedBy: MastodonUser?
|
||||
) -> PollOption {
|
||||
let option: PollOption = context.insertObject()
|
||||
|
||||
option.index = property.index
|
||||
option.title = property.title
|
||||
option.votesCount = property.votesCount
|
||||
option.updatedAt = property.networkDate
|
||||
|
||||
if let votedBy = votedBy {
|
||||
option.mutableSetValue(forKey: #keyPath(PollOption.votedBy)).add(votedBy)
|
||||
}
|
||||
|
||||
return option
|
||||
}
|
||||
|
||||
public func update(votesCount: Int?) {
|
||||
if self.votesCount?.intValue != votesCount {
|
||||
self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public func update(voted: Bool, by: MastodonUser) {
|
||||
if voted {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func didUpdate(at networkDate: Date) {
|
||||
self.updatedAt = networkDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PollOption {
|
||||
public struct Property {
|
||||
public let index: NSNumber
|
||||
public let title: String
|
||||
public let votesCount: NSNumber?
|
||||
|
||||
public let networkDate: Date
|
||||
|
||||
public init(index: Int, title: String, votesCount: Int?, networkDate: Date) {
|
||||
self.index = NSNumber(value: index)
|
||||
self.title = title
|
||||
self.votesCount = votesCount.flatMap { NSNumber(value: $0) }
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PollOption: Managed {
|
||||
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||
return [NSSortDescriptor(keyPath: \PollOption.createdAt, ascending: false)]
|
||||
}
|
||||
}
|
|
@ -23,13 +23,14 @@ public final class Tag: NSManagedObject {
|
|||
@NSManaged public private(set) var histories: Set<History>?
|
||||
}
|
||||
|
||||
public extension Tag {
|
||||
override func awakeFromInsert() {
|
||||
extension Tag {
|
||||
public override func awakeFromInsert() {
|
||||
super.awakeFromInsert()
|
||||
identifier = UUID()
|
||||
setPrimitiveValue(UUID(), forKey: #keyPath(Tag.identifier))
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> Tag {
|
||||
|
@ -43,8 +44,8 @@ public extension Tag {
|
|||
}
|
||||
}
|
||||
|
||||
public extension Tag {
|
||||
struct Property {
|
||||
extension Tag {
|
||||
public struct Property {
|
||||
public let name: String
|
||||
public let url: String
|
||||
public let histories: [History]?
|
||||
|
|
|
@ -48,6 +48,7 @@ public final class Toot: NSManagedObject {
|
|||
|
||||
// one-to-one relastionship
|
||||
@NSManaged public private(set) var pinnedBy: MastodonUser?
|
||||
@NSManaged public private(set) var poll: Poll?
|
||||
|
||||
// one-to-many relationship
|
||||
@NSManaged public private(set) var reblogFrom: Set<Toot>?
|
||||
|
@ -69,6 +70,7 @@ public extension Toot {
|
|||
author: MastodonUser,
|
||||
reblog: Toot?,
|
||||
application: Application?,
|
||||
poll: Poll?,
|
||||
mentions: [Mention]?,
|
||||
emojis: [Emoji]?,
|
||||
tags: [Tag]?,
|
||||
|
@ -109,6 +111,7 @@ public extension Toot {
|
|||
toot.reblog = reblog
|
||||
|
||||
toot.pinnedBy = pinnedBy
|
||||
toot.poll = poll
|
||||
|
||||
if let mentions = mentions {
|
||||
toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
{
|
||||
"common": {
|
||||
"alerts": {
|
||||
"common": {
|
||||
"please_try_again": "Please try again.",
|
||||
"please_try_again_later": "Please try again later."
|
||||
},
|
||||
"sign_up_failure": {
|
||||
"title": "Sign Up Failure"
|
||||
},
|
||||
"server_error": {
|
||||
"title": "Server Error"
|
||||
},
|
||||
"vote_failure": {
|
||||
"title": "Vote Failure",
|
||||
"poll_expired": "The poll has expired"
|
||||
}
|
||||
},
|
||||
"controls": {
|
||||
|
@ -31,7 +39,20 @@
|
|||
"user_boosted": "%s boosted",
|
||||
"show_post": "Show Post",
|
||||
"status_content_warning": "content warning",
|
||||
"media_content_warning": "Tap to reveal that may be sensitive"
|
||||
"media_content_warning": "Tap to reveal that may be sensitive",
|
||||
"poll": {
|
||||
"vote": "Vote",
|
||||
"vote_count": {
|
||||
"single": "%d vote",
|
||||
"multiple": "%d votes",
|
||||
},
|
||||
"voter_count": {
|
||||
"single": "%d voter",
|
||||
"multiple": "%d voters",
|
||||
},
|
||||
"time_left": "%s left",
|
||||
"closed": "Closed"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"load_more": "Load More"
|
||||
|
@ -154,4 +175,4 @@
|
|||
"title": "Public"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@
|
|||
2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; };
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */; };
|
||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */; };
|
||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */; };
|
||||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; };
|
||||
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; };
|
||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
||||
|
@ -96,12 +96,13 @@
|
|||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
|
||||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; };
|
||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; };
|
||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; };
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; };
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; };
|
||||
DB1FD44A25F26CD7004CFCFC /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44925F26CD7004CFCFC /* PickServerItem.swift */; };
|
||||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; };
|
||||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; };
|
||||
DB1FD46025F278AF004CFCFC /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */; };
|
||||
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
|
||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||
|
@ -114,6 +115,12 @@
|
|||
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; };
|
||||
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
|
||||
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
|
||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.swift */; };
|
||||
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481AC25EE155900BEFB67 /* Poll.swift */; };
|
||||
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B225EE16D000BEFB67 /* PollOption.swift */; };
|
||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; };
|
||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481C525EE2ADA00BEFB67 /* PollSection.swift */; };
|
||||
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */; };
|
||||
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */; };
|
||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; };
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; };
|
||||
|
@ -127,6 +134,10 @@
|
|||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
|
||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* StripProgressView.swift */; };
|
||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
|
||||
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
|
||||
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
|
||||
|
@ -157,6 +168,7 @@
|
|||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
|
||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
|
||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */; };
|
||||
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */; };
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; };
|
||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
|
||||
|
@ -297,7 +309,7 @@
|
|||
2DA7D05625CA693F00804E11 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
||||
2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProviderFacade.swift; sourceTree = "<group>"; };
|
||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+TimelinePostTableViewCellDelegate.swift"; sourceTree = "<group>"; };
|
||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+StatusTableViewCellDelegate.swift"; sourceTree = "<group>"; };
|
||||
2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = "<group>"; };
|
||||
2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectObserver.swift; sourceTree = "<group>"; };
|
||||
2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = "<group>"; };
|
||||
|
@ -318,9 +330,11 @@
|
|||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
|
||||
DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = "<group>"; };
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||
DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = "<group>"; };
|
||||
DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = "<group>"; };
|
||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerSection.swift; sourceTree = "<group>"; };
|
||||
DB1FD44925F26CD7004CFCFC /* PickServerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PickServerItem.swift; path = Mastodon/Diffiable/Section/PickServerItem.swift; sourceTree = SOURCE_ROOT; };
|
||||
DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
|
||||
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
|
||||
|
@ -342,6 +356,12 @@
|
|||
DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
|
||||
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = "<group>"; };
|
||||
DB4481AC25EE155900BEFB67 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
|
||||
DB4481B225EE16D000BEFB67 /* PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOption.swift; sourceTree = "<group>"; };
|
||||
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
|
||||
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollSection.swift; sourceTree = "<group>"; };
|
||||
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollItem.swift; sourceTree = "<group>"; };
|
||||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardResponderService.swift; sourceTree = "<group>"; };
|
||||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
|
||||
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -354,6 +374,10 @@
|
|||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = "<group>"; };
|
||||
DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.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>"; };
|
||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
||||
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.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>"; };
|
||||
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
|
||||
|
@ -386,6 +410,7 @@
|
|||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
|
||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
||||
DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerEmptyStateView.swift; sourceTree = "<group>"; };
|
||||
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = "<group>"; };
|
||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
|
||||
|
@ -563,7 +588,8 @@
|
|||
children = (
|
||||
2D38F1FD25CD481700561493 /* StatusProvider.swift */,
|
||||
2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */,
|
||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift */,
|
||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
||||
);
|
||||
path = StatusProvider;
|
||||
sourceTree = "<group>";
|
||||
|
@ -629,9 +655,10 @@
|
|||
2D38F1FC25CD47D900561493 /* StatusProvider */,
|
||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
|
||||
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */,
|
||||
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */,
|
||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
||||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */,
|
||||
);
|
||||
path = Protocol;
|
||||
sourceTree = "<group>";
|
||||
|
@ -652,8 +679,8 @@
|
|||
2D76319C25C151DE00929FB9 /* Diffiable */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7631B125C159E700929FB9 /* Item */,
|
||||
2D76319D25C151F600929FB9 /* Section */,
|
||||
2D7631B125C159E700929FB9 /* Item */,
|
||||
);
|
||||
path = Diffiable;
|
||||
sourceTree = "<group>";
|
||||
|
@ -662,8 +689,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D76319E25C1521200929FB9 /* StatusSection.swift */,
|
||||
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */,
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
@ -684,7 +712,9 @@
|
|||
2D42FF8325C82245004A627A /* Button */,
|
||||
2D42FF7C25C82207004A627A /* ToolBar */,
|
||||
DB9D6C1325E4F97A0051B173 /* Container */,
|
||||
DBA9B90325F1D4420012E7B6 /* Control */,
|
||||
2D152A8A25C295B8009AA50C /* Content */,
|
||||
DB1D187125EF5BBD003F1F23 /* TableView */,
|
||||
2D7631A625C1533800929FB9 /* TableviewCell */,
|
||||
);
|
||||
path = View;
|
||||
|
@ -697,6 +727,7 @@
|
|||
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
|
||||
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */,
|
||||
);
|
||||
path = TableviewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -705,7 +736,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7631B225C159F700929FB9 /* Item.swift */,
|
||||
DB1FD44925F26CD7004CFCFC /* PickServerItem.swift */,
|
||||
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
|
||||
DB1E347725F519300079D7DF /* PickServerItem.swift */,
|
||||
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
|
||||
);
|
||||
path = Item;
|
||||
|
@ -765,6 +797,14 @@
|
|||
path = CoreDataStack;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB1D187125EF5BBD003F1F23 /* TableView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */,
|
||||
);
|
||||
path = TableView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB3D0FF725BAA68500EAA174 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -863,15 +903,16 @@
|
|||
DB45FB0925CA87BC005A8AC7 /* CoreData */,
|
||||
2D61335625C1887F00CAE157 /* Persist */,
|
||||
2D61335D25C1894B00CAE157 /* APIService.swift */,
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */,
|
||||
2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */,
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
||||
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||
);
|
||||
path = APIService;
|
||||
sourceTree = "<group>";
|
||||
|
@ -977,6 +1018,8 @@
|
|||
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
||||
2DA7D05625CA693F00804E11 /* Application.swift */,
|
||||
DB9D6C2D25E504AC0051B173 /* Attachment.swift */,
|
||||
DB4481AC25EE155900BEFB67 /* Poll.swift */,
|
||||
DB4481B225EE16D000BEFB67 /* PollOption.swift */,
|
||||
);
|
||||
path = Entity;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1037,6 +1080,7 @@
|
|||
children = (
|
||||
DB084B5125CBC56300F898ED /* CoreDataStack */,
|
||||
DB6C8C0525F0921200AAA452 /* MastodonSDK */,
|
||||
DB44384E25E8C1FA008912A2 /* CALayer.swift */,
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
|
||||
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
|
||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
|
||||
|
@ -1049,6 +1093,7 @@
|
|||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
||||
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
||||
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
|
||||
DB4481B825EE289600BEFB67 /* UITableView.swift */,
|
||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
|
||||
0FAA101B25E10E760017CCDE /* UIFont.swift */,
|
||||
2D939AB425EDD8A90076FA61 /* String.swift */,
|
||||
|
@ -1061,6 +1106,7 @@
|
|||
children = (
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */,
|
||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
|
@ -1114,6 +1160,14 @@
|
|||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBA9B90325F1D4420012E7B6 /* Control */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */,
|
||||
);
|
||||
path = Control;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBE0821A25CD382900FD6BBD /* Register */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1488,10 +1542,11 @@
|
|||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
|
||||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
|
||||
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
|
||||
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
||||
DB1FD44A25F26CD7004CFCFC /* PickServerItem.swift in Sources */,
|
||||
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
|
||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
|
||||
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
|
||||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
|
@ -1502,12 +1557,15 @@
|
|||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
|
||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */,
|
||||
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
|
||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||
|
@ -1519,6 +1577,8 @@
|
|||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||
|
@ -1532,18 +1592,22 @@
|
|||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
||||
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
||||
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */,
|
||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
|
||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||
|
@ -1556,6 +1620,7 @@
|
|||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
|
||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||
|
@ -1568,7 +1633,6 @@
|
|||
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */,
|
||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||
DB1FD46025F278AF004CFCFC /* CategoryPickerSection.swift in Sources */,
|
||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||
2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */,
|
||||
|
@ -1632,8 +1696,10 @@
|
|||
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */,
|
||||
DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */,
|
||||
DB89BA1B25C1107F008580ED /* Collection.swift in Sources */,
|
||||
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */,
|
||||
DB89BA2725C110B4008580ED /* Toot.swift in Sources */,
|
||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
|
||||
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
|
||||
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
||||
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>8</integer>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>7</integer>
|
||||
<integer>8</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -13,10 +13,10 @@ import MastodonSDK
|
|||
/// Note: update Equatable when change case
|
||||
enum Item {
|
||||
// timeline
|
||||
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusTimelineAttribute)
|
||||
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
|
||||
// normal list
|
||||
case toot(objectID: NSManagedObjectID, attribute: StatusTimelineAttribute)
|
||||
case toot(objectID: NSManagedObjectID, attribute: StatusAttribute)
|
||||
|
||||
// loader
|
||||
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
|
||||
|
@ -30,7 +30,7 @@ protocol StatusContentWarningAttribute {
|
|||
}
|
||||
|
||||
extension Item {
|
||||
class StatusTimelineAttribute: Equatable, Hashable, StatusContentWarningAttribute {
|
||||
class StatusAttribute: Equatable, Hashable, StatusContentWarningAttribute {
|
||||
var isStatusTextSensitive: Bool
|
||||
var isStatusSensitive: Bool
|
||||
|
||||
|
@ -42,7 +42,7 @@ extension Item {
|
|||
self.isStatusSensitive = isStatusSensitive
|
||||
}
|
||||
|
||||
static func == (lhs: Item.StatusTimelineAttribute, rhs: Item.StatusTimelineAttribute) -> Bool {
|
||||
static func == (lhs: Item.StatusAttribute, rhs: Item.StatusAttribute) -> Bool {
|
||||
return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive &&
|
||||
lhs.isStatusSensitive == rhs.isStatusSensitive
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// PollItem.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
enum PollItem {
|
||||
case opion(objectID: NSManagedObjectID, attribute: Attribute)
|
||||
}
|
||||
|
||||
|
||||
extension PollItem {
|
||||
class Attribute: Hashable {
|
||||
|
||||
enum SelectState: Equatable, Hashable {
|
||||
case none
|
||||
case off
|
||||
case on
|
||||
}
|
||||
|
||||
enum VoteState: Equatable, Hashable {
|
||||
case hidden
|
||||
case reveal(voted: Bool, percentage: Double, animated: Bool)
|
||||
}
|
||||
|
||||
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 {
|
||||
return lhs.selectState == rhs.selectState &&
|
||||
lhs.voteState == rhs.voteState
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(selectState)
|
||||
hasher.combine(voteState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PollItem: Equatable {
|
||||
static func == (lhs: PollItem, rhs: PollItem) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.opion(let objectIDLeft, _), .opion(let objectIDRight, _)):
|
||||
return objectIDLeft == objectIDRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PollItem: Hashable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .opion(let objectID, _):
|
||||
hasher.combine(objectID)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
//
|
||||
// PollSection.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
enum PollSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
extension PollSection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
) -> UITableViewDiffableDataSource<PollSection, PollItem> {
|
||||
return UITableViewDiffableDataSource<PollSection, PollItem>(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .opion(let objectID, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self), for: indexPath) as! PollOptionTableViewCell
|
||||
managedObjectContext.performAndWait {
|
||||
let option = managedObjectContext.object(with: objectID) as! PollOption
|
||||
PollSection.configure(cell: cell, pollOption: option, pollItemAttribute: attribute)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PollSection {
|
||||
static func configure(
|
||||
cell: PollOptionTableViewCell,
|
||||
pollOption option: PollOption,
|
||||
pollItemAttribute attribute: PollItem.Attribute
|
||||
) {
|
||||
cell.optionLabel.text = option.title
|
||||
configure(cell: cell, selectState: attribute.selectState)
|
||||
configure(cell: cell, voteState: attribute.voteState)
|
||||
cell.attribute = attribute
|
||||
cell.layoutIfNeeded()
|
||||
cell.updateTextAppearance()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
static func configure(cell: PollOptionTableViewCell, voteState state: PollItem.Attribute.VoteState) {
|
||||
switch state {
|
||||
case .hidden:
|
||||
cell.optionPercentageLabel.isHidden = true
|
||||
cell.voteProgressStripView.isHidden = true
|
||||
cell.voteProgressStripView.setProgress(0.0, animated: false)
|
||||
case .reveal(let voted, let percentage, let animated):
|
||||
cell.optionPercentageLabel.isHidden = false
|
||||
cell.optionPercentageLabel.text = String(Int(100 * percentage)) + "%"
|
||||
cell.voteProgressStripView.isHidden = false
|
||||
cell.voteProgressStripView.tintColor = voted ? Asset.Colors.Background.Poll.highlight.color : Asset.Colors.Background.Poll.disabled.color
|
||||
cell.voteProgressStripView.setProgress(CGFloat(percentage), animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -21,11 +21,11 @@ extension StatusSection {
|
|||
dependency: NeedsDependency,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||
timelinePostTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
statusTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
) -> UITableViewDiffableDataSource<StatusSection, Item> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() }
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak statusTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let statusTableViewCellDelegate = statusTableViewCellDelegate else { return UITableViewCell() }
|
||||
|
||||
switch item {
|
||||
case .homeTimelineIndex(objectID: let objectID, let attribute):
|
||||
|
@ -34,9 +34,9 @@ extension StatusSection {
|
|||
// configure cell
|
||||
managedObjectContext.performAndWait {
|
||||
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||
StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusContentWarningAttribute: attribute)
|
||||
StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusItemAttribute: attribute)
|
||||
}
|
||||
cell.delegate = timelinePostTableViewCellDelegate
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
return cell
|
||||
case .toot(let objectID, let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
|
||||
|
@ -45,9 +45,9 @@ extension StatusSection {
|
|||
// configure cell
|
||||
managedObjectContext.performAndWait {
|
||||
let toot = managedObjectContext.object(with: objectID) as! Toot
|
||||
StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusContentWarningAttribute: attribute)
|
||||
StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusItemAttribute: attribute)
|
||||
}
|
||||
cell.delegate = timelinePostTableViewCellDelegate
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
return cell
|
||||
case .publicMiddleLoader(let upperTimelineTootID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
||||
|
@ -66,6 +66,9 @@ extension StatusSection {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusSection {
|
||||
|
||||
static func configure(
|
||||
cell: StatusTableViewCell,
|
||||
|
@ -73,7 +76,7 @@ extension StatusSection {
|
|||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||
toot: Toot,
|
||||
requestUserID: String,
|
||||
statusContentWarningAttribute: StatusContentWarningAttribute?
|
||||
statusItemAttribute: Item.StatusAttribute
|
||||
) {
|
||||
// set header
|
||||
cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil
|
||||
|
@ -96,7 +99,7 @@ extension StatusSection {
|
|||
|
||||
// set status text content warning
|
||||
let spoilerText = (toot.reblog ?? toot).spoilerText ?? ""
|
||||
let isStatusTextSensitive = statusContentWarningAttribute?.isStatusTextSensitive ?? !spoilerText.isEmpty
|
||||
let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive
|
||||
cell.statusView.isStatusTextSensitive = isStatusTextSensitive
|
||||
cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive)
|
||||
cell.statusView.contentWarningTitle.text = {
|
||||
|
@ -132,14 +135,14 @@ extension StatusSection {
|
|||
}()
|
||||
if mosiacImageViewModel.metas.count == 1 {
|
||||
let meta = mosiacImageViewModel.metas[0]
|
||||
let imageView = cell.statusView.statusMosaicImageView.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
|
||||
let imageView = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
|
||||
imageView.af.setImage(
|
||||
withURL: meta.url,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
)
|
||||
} else {
|
||||
let imageViews = cell.statusView.statusMosaicImageView.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height)
|
||||
let imageViews = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height)
|
||||
for (i, imageView) in imageViews.enumerated() {
|
||||
let meta = mosiacImageViewModel.metas[i]
|
||||
imageView.af.setImage(
|
||||
|
@ -149,11 +152,38 @@ extension StatusSection {
|
|||
)
|
||||
}
|
||||
}
|
||||
cell.statusView.statusMosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty
|
||||
let isStatusSensitive = statusContentWarningAttribute?.isStatusSensitive ?? (toot.reblog ?? toot).sensitive
|
||||
cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil
|
||||
cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
||||
|
||||
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
|
||||
let isStatusSensitive = statusItemAttribute.isStatusSensitive
|
||||
cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil
|
||||
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
|
||||
|
||||
// set poll
|
||||
let poll = (toot.reblog ?? toot).poll
|
||||
StatusSection.configure(
|
||||
cell: cell,
|
||||
poll: poll,
|
||||
requestUserID: requestUserID,
|
||||
updateProgressAnimated: false,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher
|
||||
)
|
||||
if let poll = poll {
|
||||
ManagedObjectObserver.observe(object: poll)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { change in
|
||||
guard case let .update(object) = change.changeType,
|
||||
let newPoll = object as? Poll else { return }
|
||||
StatusSection.configure(
|
||||
cell: cell,
|
||||
poll: newPoll,
|
||||
requestUserID: requestUserID,
|
||||
updateProgressAnimated: true,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher
|
||||
)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
// toolbar
|
||||
let replyCountTitle: String = {
|
||||
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0
|
||||
|
@ -197,6 +227,116 @@ extension StatusSection {
|
|||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
static func configure(
|
||||
cell: StatusTableViewCell,
|
||||
poll: Poll?,
|
||||
requestUserID: String,
|
||||
updateProgressAnimated: Bool,
|
||||
timestampUpdatePublisher: AnyPublisher<Date, Never>
|
||||
) {
|
||||
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.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
|
||||
|
||||
let votedOptions = poll.options.filter { option in
|
||||
(option.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
|
||||
}
|
||||
let didVotedLocal = !votedOptions.isEmpty
|
||||
let didVotedRemote = (poll.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
|
||||
cell.statusView.pollVoteButton.isEnabled = didVotedLocal
|
||||
cell.statusView.pollVoteButton.isHidden = !poll.multiple ? true : (didVotedRemote || poll.expired)
|
||||
|
||||
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 attribute: PollItem.Attribute = {
|
||||
let selectState: PollItem.Attribute.SelectState = {
|
||||
// check didVotedRemote later to make the local change possible
|
||||
if !votedOptions.isEmpty {
|
||||
return votedOptions.contains(option) ? .on : .off
|
||||
} else if poll.expired {
|
||||
return .none
|
||||
} else if didVotedRemote, votedOptions.isEmpty {
|
||||
return .none
|
||||
} else {
|
||||
return .off
|
||||
}
|
||||
}()
|
||||
let voteState: PollItem.Attribute.VoteState = {
|
||||
var needsReveal: Bool
|
||||
if poll.expired {
|
||||
needsReveal = true
|
||||
} else if didVotedRemote {
|
||||
needsReveal = true
|
||||
} else {
|
||||
needsReveal = false
|
||||
}
|
||||
guard needsReveal 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, animated: updateProgressAnimated)
|
||||
}()
|
||||
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 {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// CALayer.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-2-26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension CALayer {
|
||||
|
||||
func setupShadow(
|
||||
color: UIColor = .black,
|
||||
alpha: Float = 0.5,
|
||||
x: CGFloat = 0,
|
||||
y: CGFloat = 2,
|
||||
blur: CGFloat = 4,
|
||||
spread: CGFloat = 0,
|
||||
roundedRect: CGRect? = nil,
|
||||
byRoundingCorners corners: UIRectCorner? = nil,
|
||||
cornerRadii: CGSize? = nil
|
||||
) {
|
||||
// assert(roundedRect != .zero)
|
||||
shadowColor = color.cgColor
|
||||
shadowOpacity = alpha
|
||||
shadowOffset = CGSize(width: x, height: y)
|
||||
shadowRadius = blur / 2
|
||||
rasterizationScale = UIScreen.main.scale
|
||||
shouldRasterize = true
|
||||
masksToBounds = false
|
||||
|
||||
guard let roundedRect = roundedRect,
|
||||
let corners = corners,
|
||||
let cornerRadii = cornerRadii else {
|
||||
return
|
||||
}
|
||||
|
||||
if spread == 0 {
|
||||
shadowPath = UIBezierPath(roundedRect: roundedRect, byRoundingCorners: corners, cornerRadii: cornerRadii).cgPath
|
||||
} else {
|
||||
let rect = roundedRect.insetBy(dx: -spread, dy: -spread)
|
||||
shadowPath = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: cornerRadii).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
func removeShadow() {
|
||||
shadowRadius = 0
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// UITableView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-3-2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UITableView {
|
||||
|
||||
// static let groupedTableViewPaddingHeaderViewHeight: CGFloat = 16
|
||||
// static var groupedTableViewPaddingHeaderView: UIView {
|
||||
// return UIView(frame: CGRect(x: 0, y: 0, width: 100, height: groupedTableViewPaddingHeaderViewHeight))
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
extension UITableView {
|
||||
|
||||
func deselectRow(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) {
|
||||
guard let indexPathForSelectedRow = indexPathForSelectedRow else { return }
|
||||
|
||||
guard let transitionCoordinator = transitionCoordinator else {
|
||||
deselectRow(at: indexPathForSelectedRow, animated: animated)
|
||||
return
|
||||
}
|
||||
|
||||
transitionCoordinator.animate(alongsideTransition: { _ in
|
||||
self.deselectRow(at: indexPathForSelectedRow, animated: animated)
|
||||
}, completion: { context in
|
||||
if context.isCancelled {
|
||||
self.selectRow(at: indexPathForSelectedRow, animated: animated, scrollPosition: .none)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func blinkRow(at indexPath: IndexPath) {
|
||||
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let cell = self.cellForRow(at: indexPath) else { return }
|
||||
let backgroundColor = cell.backgroundColor
|
||||
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
cell.backgroundColor = Asset.Colors.Label.highlight.color.withAlphaComponent(0.5)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
cell.backgroundColor = backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -33,6 +33,10 @@ internal enum Asset {
|
|||
}
|
||||
internal enum Colors {
|
||||
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 secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||
|
|
|
@ -13,6 +13,12 @@ internal enum L10n {
|
|||
|
||||
internal enum Common {
|
||||
internal enum Alerts {
|
||||
internal enum Common {
|
||||
/// Please try again.
|
||||
internal static let pleaseTryAgain = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgain")
|
||||
/// Please try again later.
|
||||
internal static let pleaseTryAgainLater = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgainLater")
|
||||
}
|
||||
internal enum ServerError {
|
||||
/// Server Error
|
||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.ServerError.Title")
|
||||
|
@ -21,6 +27,12 @@ internal enum L10n {
|
|||
/// Sign Up Failure
|
||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.SignUpFailure.Title")
|
||||
}
|
||||
internal enum VoteFailure {
|
||||
/// The poll has expired
|
||||
internal static let pollExpired = L10n.tr("Localizable", "Common.Alerts.VoteFailure.PollExpired")
|
||||
/// Vote Failure
|
||||
internal static let title = L10n.tr("Localizable", "Common.Alerts.VoteFailure.Title")
|
||||
}
|
||||
}
|
||||
internal enum Controls {
|
||||
internal enum Actions {
|
||||
|
@ -68,6 +80,36 @@ internal enum L10n {
|
|||
internal static func userBoosted(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1))
|
||||
}
|
||||
internal enum Poll {
|
||||
/// Closed
|
||||
internal static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed")
|
||||
/// %@ left
|
||||
internal static func timeLeft(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.Poll.TimeLeft", String(describing: p1))
|
||||
}
|
||||
/// Vote
|
||||
internal static let vote = L10n.tr("Localizable", "Common.Controls.Status.Poll.Vote")
|
||||
internal enum VoteCount {
|
||||
/// %d votes
|
||||
internal static func multiple(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.Poll.VoteCount.Multiple", p1)
|
||||
}
|
||||
/// %d vote
|
||||
internal static func single(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.Poll.VoteCount.Single", p1)
|
||||
}
|
||||
}
|
||||
internal enum VoterCount {
|
||||
/// %d voters
|
||||
internal static func multiple(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.Poll.VoterCount.Multiple", p1)
|
||||
}
|
||||
/// %d voter
|
||||
internal static func single(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.Poll.VoterCount.Single", p1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum Timeline {
|
||||
/// Load More
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
//
|
||||
// StatusProvider+StatusTableViewCellDelegate.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/2/8.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import ActiveLabel
|
||||
|
||||
// MARK: - ActionToolbarContainerDelegate
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) {
|
||||
StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) {
|
||||
guard let diffableDataSource = self.tableViewDiffableDataSource else { return }
|
||||
guard let item = item(for: cell, indexPath: nil) else { return }
|
||||
|
||||
switch item {
|
||||
case .homeTimelineIndex(_, let attribute):
|
||||
attribute.isStatusTextSensitive = false
|
||||
case .toot(_, let attribute):
|
||||
attribute.isStatusTextSensitive = false
|
||||
default:
|
||||
return
|
||||
}
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
snapshot.reloadItems([item])
|
||||
diffableDataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MosciaImageViewContainerDelegate
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
|
||||
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) {
|
||||
guard let diffableDataSource = self.tableViewDiffableDataSource else { return }
|
||||
guard let item = item(for: cell, indexPath: nil) else { return }
|
||||
|
||||
switch item {
|
||||
case .homeTimelineIndex(_, let attribute):
|
||||
attribute.isStatusSensitive = false
|
||||
case .toot(_, let attribute):
|
||||
attribute.isStatusSensitive = false
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
snapshot.reloadItems([item])
|
||||
UIView.animate(withDuration: 0.33) {
|
||||
cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = nil
|
||||
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = 0.0
|
||||
} completion: { _ in
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - PollTableView
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
toot(for: cell, indexPath: nil)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.setFailureType(to: Error.self)
|
||||
.compactMap { toot -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error>? in
|
||||
guard let toot = (toot?.reblog ?? toot) else { return nil }
|
||||
guard let poll = toot.poll else { return nil }
|
||||
|
||||
let votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
|
||||
let choices = votedOptions.map { $0.index.intValue }
|
||||
let domain = poll.toot.domain
|
||||
|
||||
button.isEnabled = false
|
||||
|
||||
return self.context.apiService.vote(
|
||||
domain: domain,
|
||||
pollID: poll.id,
|
||||
pollObjectID: poll.objectID,
|
||||
choices: choices,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.switchToLatest()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: multiple vote fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
button.isEnabled = true
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
// do nothing
|
||||
})
|
||||
.store(in: &context.disposeBag)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
guard let activeMastodonAuthentication = context.authenticationService.activeMastodonAuthentication.value else { return }
|
||||
|
||||
guard let diffableDataSource = cell.statusView.pollTableViewDataSource else { return }
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||
guard case let .opion(objectID, attribute) = item else { return }
|
||||
guard let option = managedObjectContext.object(with: objectID) as? PollOption else { return }
|
||||
|
||||
let poll = option.poll
|
||||
let pollObjectID = option.poll.objectID
|
||||
let domain = poll.toot.domain
|
||||
|
||||
if poll.multiple {
|
||||
var votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
|
||||
if votedOptions.contains(option) {
|
||||
votedOptions.remove(option)
|
||||
} else {
|
||||
votedOptions.insert(option)
|
||||
}
|
||||
let choices = votedOptions.map { $0.index.intValue }
|
||||
context.apiService.vote(
|
||||
pollObjectID: option.poll.objectID,
|
||||
mastodonUserObjectID: activeMastodonAuthentication.user.objectID,
|
||||
choices: choices
|
||||
)
|
||||
.handleEvents(receiveOutput: { _ in
|
||||
// TODO: add haptic
|
||||
})
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
// Do nothing
|
||||
} receiveValue: { _ in
|
||||
// Do nothing
|
||||
}
|
||||
.store(in: &context.disposeBag)
|
||||
} else {
|
||||
let choices = [option.index.intValue]
|
||||
context.apiService.vote(
|
||||
pollObjectID: pollObjectID,
|
||||
mastodonUserObjectID: activeMastodonAuthentication.user.objectID,
|
||||
choices: [option.index.intValue]
|
||||
)
|
||||
.handleEvents(receiveOutput: { _ in
|
||||
// TODO: add haptic
|
||||
})
|
||||
.flatMap { pollID -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> in
|
||||
return self.context.apiService.vote(
|
||||
domain: domain,
|
||||
pollID: pollID,
|
||||
pollObjectID: pollObjectID,
|
||||
choices: choices,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
|
||||
} receiveValue: { response in
|
||||
print(response.value)
|
||||
}
|
||||
.store(in: &context.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
//
|
||||
// StatusProvider+TimelinePostTableViewCellDelegate.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/2/8.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import ActiveLabel
|
||||
|
||||
// MARK: - ActionToolbarContainerDelegate
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton) {
|
||||
StatusProviderFacade.responseToStatusLikeAction(provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton) {
|
||||
guard let diffableDataSource = self.tableViewDiffableDataSource else { return }
|
||||
item(for: cell, indexPath: nil)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] item in
|
||||
guard let _ = self else { return }
|
||||
guard let item = item else { return }
|
||||
switch item {
|
||||
case .homeTimelineIndex(_, let attribute):
|
||||
attribute.isStatusTextSensitive = false
|
||||
case .toot(_, let attribute):
|
||||
attribute.isStatusTextSensitive = false
|
||||
default:
|
||||
return
|
||||
}
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
snapshot.reloadItems([item])
|
||||
diffableDataSource.apply(snapshot)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
|
||||
|
||||
}
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) {
|
||||
guard let diffableDataSource = self.tableViewDiffableDataSource else { return }
|
||||
item(for: cell, indexPath: nil)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] item in
|
||||
guard let _ = self else { return }
|
||||
guard let item = item else { return }
|
||||
switch item {
|
||||
case .homeTimelineIndex(_, let attribute):
|
||||
attribute.isStatusSensitive = false
|
||||
case .toot(_, let attribute):
|
||||
attribute.isStatusSensitive = false
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
snapshot.reloadItems([item])
|
||||
UIView.animate(withDuration: 0.33) {
|
||||
cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = nil
|
||||
cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = 0.0
|
||||
} completion: { _ in
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// StatusProvider+UITableViewDelegate.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-3.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
// TODO:
|
||||
// func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
// }
|
||||
|
||||
func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
let now = Date()
|
||||
var pollID: Mastodon.Entity.Poll.ID?
|
||||
toot(for: cell, indexPath: indexPath)
|
||||
.compactMap { [weak self] toot -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
|
||||
guard let toot = (toot?.reblog ?? toot) else { return nil }
|
||||
guard let poll = toot.poll else { return nil }
|
||||
pollID = poll.id
|
||||
|
||||
// not expired AND last update > 60s
|
||||
guard !poll.expired else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s expired. Skip for update", ((#file as NSString).lastPathComponent), #line, #function, poll.id)
|
||||
return nil
|
||||
}
|
||||
let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt)
|
||||
#if DEBUG
|
||||
let autoRefreshTimeInterval: TimeInterval = 3 // speedup testing
|
||||
#else
|
||||
let autoRefreshTimeInterval: TimeInterval = 60
|
||||
#endif
|
||||
guard timeIntervalSinceUpdate > autoRefreshTimeInterval else {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s updated in the %.2fs. Skip for update", ((#file as NSString).lastPathComponent), #line, #function, poll.id, timeIntervalSinceUpdate)
|
||||
return nil
|
||||
}
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info update…", ((#file as NSString).lastPathComponent), #line, #function, poll.id)
|
||||
|
||||
return self.context.apiService.poll(
|
||||
domain: toot.domain,
|
||||
pollID: poll.id,
|
||||
pollObjectID: poll.objectID,
|
||||
mastodonAuthenticationBox: authenticationBox
|
||||
)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.switchToLatest()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info fail to update: %s", ((#file as NSString).lastPathComponent), #line, #function, pollID ?? "?", error.localizedDescription)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
let poll = response.value
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info updated", ((#file as NSString).lastPathComponent), #line, #function, poll.id)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
|
||||
|
||||
}
|
|
@ -7,13 +7,17 @@
|
|||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
|
||||
// async
|
||||
func toot() -> Future<Toot?, Never>
|
||||
func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Toot?, Never>
|
||||
func toot(for cell: UICollectionViewCell) -> Future<Toot?, Never>
|
||||
|
||||
// sync
|
||||
var managedObjectContext: NSManagedObjectContext { get }
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? { get }
|
||||
func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Item?, Never>
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item?
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// TableViewCellHeightCacheableContainer.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-3.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol TableViewCellHeightCacheableContainer: UIViewController {
|
||||
// TODO:
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -5,9 +5,27 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x37",
|
||||
"green" : "0x2D",
|
||||
"red" : "0x29"
|
||||
"blue" : "0xE8",
|
||||
"green" : "0xE1",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.216",
|
||||
"green" : "0.176",
|
||||
"red" : "0.161"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "232",
|
||||
"green" : "225",
|
||||
"red" : "217"
|
||||
"blue" : "0xE8",
|
||||
"green" : "0xE1",
|
||||
"red" : "0xD9"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.600",
|
||||
"blue" : "0x43",
|
||||
"green" : "0x3C",
|
||||
"red" : "0x3C"
|
||||
"blue" : "67",
|
||||
"green" : "60",
|
||||
"red" : "60"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.600",
|
||||
"blue" : "0.263",
|
||||
"green" : "0.235",
|
||||
"red" : "0.235"
|
||||
"blue" : "67",
|
||||
"green" : "60",
|
||||
"red" : "60"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
|
||||
"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
|
||||
"Common.Alerts.ServerError.Title" = "Server Error";
|
||||
"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
|
||||
"Common.Alerts.VoteFailure.PollExpired" = "The poll has expired";
|
||||
"Common.Alerts.VoteFailure.Title" = "Vote Failure";
|
||||
"Common.Controls.Actions.Add" = "Add";
|
||||
"Common.Controls.Actions.Back" = "Back";
|
||||
"Common.Controls.Actions.Cancel" = "Cancel";
|
||||
|
@ -17,6 +21,13 @@
|
|||
"Common.Controls.Actions.SignUp" = "Sign Up";
|
||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||
"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.Vote" = "Vote";
|
||||
"Common.Controls.Status.Poll.VoteCount.Multiple" = "%d votes";
|
||||
"Common.Controls.Status.Poll.VoteCount.Single" = "%d vote";
|
||||
"Common.Controls.Status.Poll.VoterCount.Multiple" = "%d voters";
|
||||
"Common.Controls.Status.Poll.VoterCount.Single" = "%d voter";
|
||||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.StatusContentWarning" = "content warning";
|
||||
"Common.Controls.Status.UserBoosted" = "%@ boosted";
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import os.log
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
#if DEBUG
|
||||
extension HomeTimelineViewController {
|
||||
|
@ -17,6 +19,8 @@ extension HomeTimelineViewController {
|
|||
identifier: nil,
|
||||
options: .displayInline,
|
||||
children: [
|
||||
moveMenu,
|
||||
dropMenu,
|
||||
UIAction(title: "Show Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.showPublicTimelineAction(action)
|
||||
|
@ -29,10 +33,136 @@ extension HomeTimelineViewController {
|
|||
)
|
||||
return menu
|
||||
}
|
||||
|
||||
var moveMenu: UIMenu {
|
||||
return UIMenu(
|
||||
title: "Move to…",
|
||||
image: UIImage(systemName: "arrow.forward.circle"),
|
||||
identifier: nil,
|
||||
options: [],
|
||||
children: [
|
||||
UIAction(title: "First Gap", image: nil, attributes: [], handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.moveToTopGapAction(action)
|
||||
}),
|
||||
UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.moveToFirstPollToot(action)
|
||||
}),
|
||||
// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||
// guard let self = self else { return }
|
||||
// self.moveToFirstReplyToot(action)
|
||||
// }),
|
||||
// UIAction(title: "First Reply Reblog", image: nil, attributes: [], handler: { [weak self] action in
|
||||
// guard let self = self else { return }
|
||||
// self.moveToFirstReplyReblog(action)
|
||||
// }),
|
||||
// UIAction(title: "First Video Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||
// guard let self = self else { return }
|
||||
// self.moveToFirstVideoToot(action)
|
||||
// }),
|
||||
// UIAction(title: "First GIF Toot", image: nil, attributes: [], handler: { [weak self] action in
|
||||
// guard let self = self else { return }
|
||||
// self.moveToFirstGIFToot(action)
|
||||
// }),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
var dropMenu: UIMenu {
|
||||
return UIMenu(
|
||||
title: "Drop…",
|
||||
image: UIImage(systemName: "minus.circle"),
|
||||
identifier: nil,
|
||||
options: [],
|
||||
children: [50, 100, 150, 200, 250, 300].map { count in
|
||||
UIAction(title: "Drop Recent \(count) Toots", image: nil, attributes: [], handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.dropRecentTootsAction(action, count: count)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineViewController {
|
||||
|
||||
@objc private func moveToTopGapAction(_ sender: UIAction) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
|
||||
switch item {
|
||||
case .homeMiddleLoader: return true
|
||||
default: return false
|
||||
}
|
||||
})
|
||||
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
|
||||
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func moveToFirstPollToot(_ sender: UIAction) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
|
||||
return toot.poll != nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
|
||||
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
|
||||
tableView.blinkRow(at: IndexPath(row: index, section: 0))
|
||||
} else {
|
||||
print("Not found poll toot")
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||
|
||||
let droppingObjectIDs = snapshotTransitioning.itemIdentifiers.prefix(count).compactMap { item -> NSManagedObjectID? in
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, _): return objectID
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
var droppingTootObjectIDs: [NSManagedObjectID] = []
|
||||
context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
||||
guard let self = self else { return }
|
||||
for objectID in droppingObjectIDs {
|
||||
guard let homeTimelineIndex = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue }
|
||||
droppingTootObjectIDs.append(homeTimelineIndex.toot.objectID)
|
||||
self.context.apiService.backgroundManagedObjectContext.delete(homeTimelineIndex)
|
||||
}
|
||||
}
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .success:
|
||||
self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
|
||||
guard let self = self else { return }
|
||||
for objectID in droppingTootObjectIDs {
|
||||
guard let toot = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Toot else { continue }
|
||||
self.context.apiService.backgroundManagedObjectContext.delete(toot)
|
||||
}
|
||||
}
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
@objc private func showPublicTimelineAction(_ sender: UIAction) {
|
||||
coordinator.present(scene: .publicTimeline, from: self, transition: .show)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
// MARK: - StatusProvider
|
||||
|
@ -47,25 +48,26 @@ extension HomeTimelineViewController: StatusProvider {
|
|||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
var managedObjectContext: NSManagedObjectContext {
|
||||
return viewModel.fetchedResultsController.managedObjectContext
|
||||
}
|
||||
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
|
||||
return viewModel.diffableDataSource
|
||||
}
|
||||
|
||||
func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Item?, Never> {
|
||||
return Future { promise in
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
|
||||
promise(.success(item))
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ extension HomeTimelineViewController {
|
|||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self,
|
||||
timelinePostTableViewCellDelegate: self,
|
||||
statusTableViewCellDelegate: self,
|
||||
timelineMiddleLoaderTableViewCellDelegate: self
|
||||
)
|
||||
|
||||
|
@ -220,16 +220,21 @@ extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
|||
// MARK: - UITableViewDelegate
|
||||
extension HomeTimelineViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
||||
|
||||
guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
|
||||
return 200
|
||||
}
|
||||
// os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription)
|
||||
|
||||
return ceil(frame.height)
|
||||
// TODO:
|
||||
// func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return 100 }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 }
|
||||
//
|
||||
// guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
|
||||
// return 200
|
||||
// }
|
||||
// // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription)
|
||||
//
|
||||
// return ceil(frame.height)
|
||||
// }
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ extension HomeTimelineViewModel {
|
|||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
timelinePostTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
statusTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate
|
||||
) {
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
|
@ -28,7 +28,7 @@ extension HomeTimelineViewModel {
|
|||
dependency: dependency,
|
||||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||
)
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
|
||||
// that's will be the most fastest fetch because of upstream just update and no modify needs consider
|
||||
|
||||
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusTimelineAttribute] = [:]
|
||||
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
||||
|
||||
for item in oldSnapshot.itemIdentifiers {
|
||||
guard case let .homeTimelineIndex(objectID, attribute) = item else { continue }
|
||||
|
@ -88,7 +88,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
guard let spoilerText = toot.spoilerText, !spoilerText.isEmpty else { return false }
|
||||
return true
|
||||
}()
|
||||
let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: toot.sensitive)
|
||||
let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: toot.sensitive)
|
||||
|
||||
// append new item into snapshot
|
||||
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
||||
|
|
|
@ -110,10 +110,10 @@ final class HomeTimelineViewModel: NSObject {
|
|||
context.authenticationService.activeMastodonAuthentication
|
||||
.sink { [weak self] activeMastodonAuthentication in
|
||||
guard let self = self else { return }
|
||||
guard let twitterAuthentication = activeMastodonAuthentication else { return }
|
||||
let activeTwitterUserID = twitterAuthentication.userID
|
||||
guard let mastodonAuthentication = activeMastodonAuthentication else { return }
|
||||
let activeMastodonUserID = mastodonAuthentication.userID
|
||||
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||
HomeTimelineIndex.predicate(userID: activeTwitterUserID),
|
||||
HomeTimelineIndex.predicate(userID: activeMastodonUserID),
|
||||
HomeTimelineIndex.notDeleted()
|
||||
])
|
||||
self.timelinePredicate.value = predicate
|
||||
|
|
|
@ -8,12 +8,13 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
// MARK: - StatusProvider
|
||||
extension PublicTimelineViewController: StatusProvider {
|
||||
|
||||
|
||||
func toot() -> Future<Toot?, Never> {
|
||||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
@ -48,25 +49,25 @@ extension PublicTimelineViewController: StatusProvider {
|
|||
return Future { promise in promise(.success(nil)) }
|
||||
}
|
||||
|
||||
var managedObjectContext: NSManagedObjectContext {
|
||||
return viewModel.fetchedResultsController.managedObjectContext
|
||||
}
|
||||
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
|
||||
return viewModel.diffableDataSource
|
||||
}
|
||||
|
||||
func item(for cell: UITableViewCell, indexPath: IndexPath?) -> Future<Item?, Never> {
|
||||
return Future { promise in
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
guard let indexPath = indexPath ?? self.tableView.indexPath(for: cell),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
|
||||
promise(.success(item))
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ extension PublicTimelineViewController {
|
|||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self,
|
||||
timelinePostTableViewCellDelegate: self,
|
||||
statusTableViewCellDelegate: self,
|
||||
timelineMiddleLoaderTableViewCellDelegate: self
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ extension PublicTimelineViewModel {
|
|||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
timelinePostTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
statusTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate
|
||||
) {
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
|
@ -27,7 +27,7 @@ extension PublicTimelineViewModel {
|
|||
dependency: dependency,
|
||||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||
)
|
||||
items.value = []
|
||||
|
@ -50,7 +50,7 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
return indexes.firstIndex(of: toot.id).map { index in (index, toot) }
|
||||
}
|
||||
.sorted { $0.0 < $1.0 }
|
||||
var oldSnapshotAttributeDict: [NSManagedObjectID: Item.StatusTimelineAttribute] = [:]
|
||||
var oldSnapshotAttributeDict: [NSManagedObjectID: Item.StatusAttribute] = [:]
|
||||
for item in self.items.value {
|
||||
guard case let .toot(objectID, attribute) = item else { continue }
|
||||
oldSnapshotAttributeDict[objectID] = attribute
|
||||
|
@ -63,7 +63,7 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
guard let spoilerText = targetToot.spoilerText, !spoilerText.isEmpty else { return false }
|
||||
return true
|
||||
}()
|
||||
let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusTimelineAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetToot.sensitive)
|
||||
let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetToot.sensitive)
|
||||
items.append(Item.toot(objectID: toot.objectID, attribute: attribute))
|
||||
if tootIDsWhichHasGap.contains(toot.id) {
|
||||
items.append(Item.publicMiddleLoader(tootID: toot.id))
|
||||
|
|
|
@ -13,16 +13,21 @@ import AlamofireImage
|
|||
|
||||
protocol StatusViewDelegate: class {
|
||||
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
}
|
||||
|
||||
final class StatusView: UIView {
|
||||
|
||||
var statusPollTableViewHeightObservation: NSKeyValueObservation?
|
||||
|
||||
static let avatarImageSize = CGSize(width: 42, height: 42)
|
||||
static let avatarImageCornerRadius: CGFloat = 4
|
||||
static let contentWarningBlurRadius: CGFloat = 12
|
||||
|
||||
weak var delegate: StatusViewDelegate?
|
||||
var isStatusTextSensitive = false
|
||||
var pollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
|
||||
var pollTableViewHeightLaoutConstraint: NSLayoutConstraint!
|
||||
|
||||
let headerContainerStackView = UIStackView()
|
||||
|
||||
|
@ -99,7 +104,49 @@ final class StatusView: UIView {
|
|||
button.setTitle(L10n.Common.Controls.Status.showPost, for: .normal)
|
||||
return button
|
||||
}()
|
||||
let statusMosaicImageView = MosaicImageViewContainer()
|
||||
let statusMosaicImageViewContainer = MosaicImageViewContainer()
|
||||
|
||||
let pollTableView: PollTableView = {
|
||||
let tableView = PollTableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
tableView.register(PollOptionTableViewCell.self, forCellReuseIdentifier: String(describing: PollOptionTableViewCell.self))
|
||||
tableView.isScrollEnabled = false
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let pollStatusStackView = UIStackView()
|
||||
let pollVoteCountLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = L10n.Common.Controls.Status.Poll.VoteCount.single(0)
|
||||
return label
|
||||
}()
|
||||
let pollStatusDotLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = " · "
|
||||
return label
|
||||
}()
|
||||
let pollCountdownLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = L10n.Common.Controls.Status.Poll.timeLeft("6 hours")
|
||||
return label
|
||||
}()
|
||||
let pollVoteButton: UIButton = {
|
||||
let button = HitTestExpandedButton()
|
||||
button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14, weight: .semibold))
|
||||
button.setTitle(L10n.Common.Controls.Status.Poll.vote, for: .normal)
|
||||
button.setTitleColor(Asset.Colors.Button.highlight.color, for: .normal)
|
||||
button.setTitleColor(Asset.Colors.Button.highlight.color.withAlphaComponent(0.8), for: .highlighted)
|
||||
button.setTitleColor(Asset.Colors.Button.disabled.color, for: .disabled)
|
||||
button.isEnabled = false
|
||||
return button
|
||||
}()
|
||||
|
||||
// do not use visual effect view due to we blur text only without background
|
||||
let contentWarningBlurContentImageView: UIImageView = {
|
||||
|
@ -136,6 +183,10 @@ final class StatusView: UIView {
|
|||
drawContentWarningImageView()
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
statusPollTableViewHeightObservation = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -222,7 +273,7 @@ extension StatusView {
|
|||
subtitleContainerStackView.axis = .horizontal
|
||||
subtitleContainerStackView.addArrangedSubview(usernameLabel)
|
||||
|
||||
// status container: [status | image / video | audio]
|
||||
// status container: [status | image / video | audio | poll | poll status]
|
||||
containerStackView.addArrangedSubview(statusContainerStackView)
|
||||
statusContainerStackView.axis = .vertical
|
||||
statusContainerStackView.spacing = 10
|
||||
|
@ -236,6 +287,7 @@ extension StatusView {
|
|||
activeTextLabel.trailingAnchor.constraint(equalTo: statusTextContainerView.trailingAnchor),
|
||||
statusTextContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: activeTextLabel.bottomAnchor),
|
||||
])
|
||||
activeTextLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
contentWarningBlurContentImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusTextContainerView.addSubview(contentWarningBlurContentImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -257,20 +309,50 @@ extension StatusView {
|
|||
])
|
||||
statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle)
|
||||
statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton)
|
||||
statusContainerStackView.addArrangedSubview(statusMosaicImageView)
|
||||
|
||||
statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer)
|
||||
pollTableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusContainerStackView.addArrangedSubview(pollTableView)
|
||||
pollTableViewHeightLaoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1)
|
||||
NSLayoutConstraint.activate([
|
||||
pollTableViewHeightLaoutConstraint,
|
||||
])
|
||||
|
||||
statusPollTableViewHeightObservation = pollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in
|
||||
guard let self = self else { return }
|
||||
guard self.pollTableView.contentSize.height != .zero else {
|
||||
self.pollTableViewHeightLaoutConstraint.constant = 44
|
||||
return
|
||||
}
|
||||
self.pollTableViewHeightLaoutConstraint.constant = self.pollTableView.contentSize.height
|
||||
})
|
||||
|
||||
statusContainerStackView.addArrangedSubview(pollStatusStackView)
|
||||
pollStatusStackView.axis = .horizontal
|
||||
pollStatusStackView.addArrangedSubview(pollVoteCountLabel)
|
||||
pollStatusStackView.addArrangedSubview(pollStatusDotLabel)
|
||||
pollStatusStackView.addArrangedSubview(pollCountdownLabel)
|
||||
pollStatusStackView.addArrangedSubview(pollVoteButton)
|
||||
pollVoteCountLabel.setContentHuggingPriority(.defaultHigh + 2, for: .horizontal)
|
||||
pollStatusDotLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||
pollCountdownLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
pollVoteButton.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal)
|
||||
|
||||
// action toolbar container
|
||||
containerStackView.addArrangedSubview(actionToolbarContainer)
|
||||
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
|
||||
headerContainerStackView.isHidden = true
|
||||
statusMosaicImageView.isHidden = true
|
||||
statusMosaicImageViewContainer.isHidden = true
|
||||
pollTableView.isHidden = true
|
||||
pollStatusStackView.isHidden = true
|
||||
|
||||
contentWarningBlurContentImageView.isHidden = true
|
||||
statusContentWarningContainerStackView.isHidden = true
|
||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
||||
|
||||
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
|
||||
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -306,20 +388,26 @@ extension StatusView {
|
|||
}
|
||||
|
||||
extension StatusView {
|
||||
|
||||
@objc private func contentWarningActionButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.statusView(self, contentWarningActionButtonPressed: sender)
|
||||
}
|
||||
|
||||
@objc private func pollVoteButtonPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
delegate?.statusView(self, pollVoteButtonPressed: sender)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AvatarConfigurableView
|
||||
extension StatusView: AvatarConfigurableView {
|
||||
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
|
||||
static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
|
||||
var configurableAvatarImageView: UIImageView? { return nil }
|
||||
var configurableAvatarButton: UIButton? { return avatarButton }
|
||||
var configurableVerifiedBadgeImageView: UIImageView? { nil }
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
|
@ -357,11 +445,11 @@ struct StatusView_Previews: PreviewProvider {
|
|||
statusView.drawContentWarningImageView()
|
||||
statusView.updateContentWarningDisplay(isHidden: false)
|
||||
let images = MosaicImageView_Previews.images
|
||||
let imageViews = statusView.statusMosaicImageView.setupImageViews(count: 4, maxHeight: 162)
|
||||
let imageViews = statusView.statusMosaicImageViewContainer.setupImageViews(count: 4, maxHeight: 162)
|
||||
for (i, imageView) in imageViews.enumerated() {
|
||||
imageView.image = images[i]
|
||||
}
|
||||
statusView.statusMosaicImageView.isHidden = false
|
||||
statusView.statusMosaicImageViewContainer.isHidden = false
|
||||
return statusView
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 380))
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
//
|
||||
// StripProgressView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-3.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
private final class StripProgressLayer: CALayer {
|
||||
|
||||
static let progressAnimationKey = "progressAnimationKey"
|
||||
static let progressKey = "progress"
|
||||
|
||||
var tintColor: UIColor = .black
|
||||
@NSManaged var progress: CGFloat
|
||||
|
||||
override class func needsDisplay(forKey key: String) -> Bool {
|
||||
switch key {
|
||||
case StripProgressLayer.progressKey:
|
||||
return true
|
||||
default:
|
||||
return super.needsDisplay(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
override func display() {
|
||||
let progress: CGFloat = {
|
||||
guard animation(forKey: StripProgressLayer.progressAnimationKey) != nil else {
|
||||
return self.progress
|
||||
}
|
||||
|
||||
return presentation()?.progress ?? self.progress
|
||||
}()
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
context.clear(bounds)
|
||||
|
||||
var rect = bounds
|
||||
let newWidth = CGFloat(progress) * 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)
|
||||
context.setFillColor(tintColor.cgColor)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
|
||||
contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class StripProgressView: UIView {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
private let stripProgressLayer: StripProgressLayer = {
|
||||
let layer = StripProgressLayer()
|
||||
return layer
|
||||
}()
|
||||
|
||||
override var tintColor: UIColor! {
|
||||
didSet {
|
||||
stripProgressLayer.tintColor = tintColor
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
func setProgress(_ progress: CGFloat, animated: Bool) {
|
||||
stripProgressLayer.removeAnimation(forKey: StripProgressLayer.progressAnimationKey)
|
||||
if animated {
|
||||
let animation = CABasicAnimation(keyPath: StripProgressLayer.progressKey)
|
||||
animation.fromValue = stripProgressLayer.presentation()?.progress ?? stripProgressLayer.progress
|
||||
animation.toValue = progress
|
||||
animation.duration = 0.33
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.isRemovedOnCompletion = true
|
||||
stripProgressLayer.add(animation, forKey: StripProgressLayer.progressAnimationKey)
|
||||
stripProgressLayer.progress = progress
|
||||
} else {
|
||||
stripProgressLayer.progress = progress
|
||||
stripProgressLayer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StripProgressView {
|
||||
|
||||
private func _init() {
|
||||
layer.addSublayer(stripProgressLayer)
|
||||
updateLayerPath()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateLayerPath()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StripProgressView {
|
||||
private func updateLayerPath() {
|
||||
guard bounds != .zero else { return }
|
||||
|
||||
stripProgressLayer.frame = bounds
|
||||
stripProgressLayer.tintColor = tintColor
|
||||
stripProgressLayer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct VoteProgressStripView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview() {
|
||||
StripProgressView()
|
||||
}
|
||||
.frame(width: 100, height: 44)
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
UIViewPreview() {
|
||||
let bar = StripProgressView()
|
||||
bar.tintColor = .white
|
||||
bar.setProgress(0.5, animated: false)
|
||||
return bar
|
||||
}
|
||||
.frame(width: 100, height: 44)
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
UIViewPreview() {
|
||||
let bar = StripProgressView()
|
||||
bar.tintColor = .white
|
||||
bar.setProgress(1.0, animated: false)
|
||||
return bar
|
||||
}
|
||||
.frame(width: 100, height: 44)
|
||||
.padding()
|
||||
.background(Color.black)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,10 @@
|
|||
//
|
||||
// PollTableView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-3.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class PollTableView: UITableView { }
|
|
@ -0,0 +1,261 @@
|
|||
//
|
||||
// PollOptionTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-2-25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class PollOptionTableViewCell: UITableViewCell {
|
||||
|
||||
static let height: CGFloat = optionHeight + 2 * verticalMargin
|
||||
static let optionHeight: CGFloat = 44
|
||||
static let verticalMargin: CGFloat = 5
|
||||
static let checkmarkImageSize = CGSize(width: 26, height: 26)
|
||||
|
||||
private var viewStateDisposeBag = Set<AnyCancellable>()
|
||||
var attribute: PollItem.Attribute?
|
||||
|
||||
let roundedBackgroundView = UIView()
|
||||
let voteProgressStripView: StripProgressView = {
|
||||
let view = StripProgressView()
|
||||
view.tintColor = Asset.Colors.Background.Poll.highlight.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let checkmarkBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .systemBackground
|
||||
return view
|
||||
}()
|
||||
|
||||
let checkmarkImageView: UIView = {
|
||||
let imageView = UIImageView()
|
||||
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .bold))!
|
||||
imageView.image = image.withRenderingMode(.alwaysTemplate)
|
||||
imageView.tintColor = Asset.Colors.Button.highlight.color
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let optionLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 15, weight: .medium)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "Option"
|
||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right
|
||||
return label
|
||||
}()
|
||||
|
||||
let optionLabelMiddlePaddingView = UIView()
|
||||
|
||||
let optionPercentageLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = "50%"
|
||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
guard let voteState = attribute?.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) {
|
||||
super.setHighlighted(highlighted, animated: animated)
|
||||
|
||||
guard let voteState = attribute?.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PollOptionTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(roundedBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
roundedBackgroundView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
|
||||
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
|
||||
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
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
|
||||
roundedBackgroundView.addSubview(checkmarkBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9),
|
||||
checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9),
|
||||
roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9),
|
||||
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.width).priority(.defaultHigh),
|
||||
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.height).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
checkmarkBackgroundView.addSubview(checkmarkImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
checkmarkImageView.topAnchor.constraint(equalTo: checkmarkBackgroundView.topAnchor, constant: 5),
|
||||
checkmarkImageView.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.leadingAnchor, constant: 5),
|
||||
checkmarkBackgroundView.trailingAnchor.constraint(equalTo: checkmarkImageView.trailingAnchor, constant: 5),
|
||||
checkmarkBackgroundView.bottomAnchor.constraint(equalTo: checkmarkImageView.bottomAnchor, constant: 5),
|
||||
])
|
||||
|
||||
optionLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
roundedBackgroundView.addSubview(optionLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
optionLabel.leadingAnchor.constraint(equalTo: checkmarkBackgroundView.trailingAnchor, constant: 14),
|
||||
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
|
||||
roundedBackgroundView.addSubview(optionPercentageLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
optionPercentageLabel.leadingAnchor.constraint(equalTo: optionLabelMiddlePaddingView.trailingAnchor),
|
||||
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
|
||||
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
|
||||
])
|
||||
optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
updateCornerRadius()
|
||||
updateTextAppearance()
|
||||
}
|
||||
|
||||
private func updateCornerRadius() {
|
||||
roundedBackgroundView.layer.masksToBounds = true
|
||||
roundedBackgroundView.layer.cornerRadius = PollOptionTableViewCell.optionHeight * 0.5
|
||||
roundedBackgroundView.layer.cornerCurve = .circular
|
||||
|
||||
checkmarkBackgroundView.layer.masksToBounds = true
|
||||
checkmarkBackgroundView.layer.cornerRadius = PollOptionTableViewCell.checkmarkImageSize.width * 0.5
|
||||
checkmarkBackgroundView.layer.cornerCurve = .circular
|
||||
}
|
||||
|
||||
func updateTextAppearance() {
|
||||
guard let voteState = attribute?.voteState else {
|
||||
optionLabel.textColor = Asset.Colors.Label.primary.color
|
||||
optionLabel.layer.removeShadow()
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct PollTableViewCell_Previews: PreviewProvider {
|
||||
|
||||
static var controls: some View {
|
||||
Group {
|
||||
UIViewPreview() {
|
||||
PollOptionTableViewCell()
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||
UIViewPreview() {
|
||||
let cell = PollOptionTableViewCell()
|
||||
PollSection.configure(cell: cell, selectState: .off)
|
||||
return cell
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||
UIViewPreview() {
|
||||
let cell = PollOptionTableViewCell()
|
||||
PollSection.configure(cell: cell, selectState: .on)
|
||||
return cell
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 44 + 10))
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
controls.colorScheme(.light)
|
||||
controls.colorScheme(.dark)
|
||||
}
|
||||
.background(Color.gray)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -9,14 +9,20 @@ import os.log
|
|||
import UIKit
|
||||
import AVKit
|
||||
import Combine
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
protocol StatusTableViewCellDelegate: class {
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||
var context: AppContext! { get }
|
||||
var managedObjectContext: NSManagedObjectContext { get }
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath)
|
||||
}
|
||||
|
||||
final class StatusTableViewCell: UITableViewCell {
|
||||
|
@ -26,6 +32,7 @@ final class StatusTableViewCell: UITableViewCell {
|
|||
weak var delegate: StatusTableViewCellDelegate?
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var pollCountdownSubscription: AnyCancellable?
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
let statusView = StatusView()
|
||||
|
@ -34,6 +41,7 @@ final class StatusTableViewCell: UITableViewCell {
|
|||
super.prepareForReuse()
|
||||
statusView.isStatusTextSensitive = false
|
||||
statusView.cleanUpContentWarning()
|
||||
statusView.pollTableView.dataSource = nil
|
||||
disposeBag.removeAll()
|
||||
observations.removeAll()
|
||||
}
|
||||
|
@ -85,17 +93,100 @@ extension StatusTableViewCell {
|
|||
bottomPaddingView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
statusView.delegate = self
|
||||
statusView.statusMosaicImageView.delegate = self
|
||||
statusView.pollTableView.delegate = self
|
||||
statusView.statusMosaicImageViewContainer.delegate = self
|
||||
statusView.actionToolbarContainer.delegate = self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension StatusTableViewCell: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
if tableView === statusView.pollTableView, let diffableDataSource = statusView.pollTableViewDataSource {
|
||||
var pollID: String?
|
||||
defer {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s. PollID: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription, pollID ?? "<nil>")
|
||||
}
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath),
|
||||
case let .opion(objectID, _) = item,
|
||||
let option = delegate?.managedObjectContext.object(with: objectID) as? PollOption else {
|
||||
return false
|
||||
}
|
||||
pollID = option.poll.id
|
||||
return !option.poll.expired
|
||||
} else {
|
||||
assertionFailure()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
if tableView === statusView.pollTableView, let diffableDataSource = statusView.pollTableViewDataSource {
|
||||
var pollID: String?
|
||||
defer {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s. PollID: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription, pollID ?? "<nil>")
|
||||
}
|
||||
|
||||
guard let context = delegate?.context else { return nil }
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath),
|
||||
case let .opion(objectID, _) = item,
|
||||
let option = delegate?.managedObjectContext.object(with: objectID) as? PollOption else {
|
||||
return nil
|
||||
}
|
||||
let poll = option.poll
|
||||
pollID = poll.id
|
||||
|
||||
// disallow select when: poll expired OR user voted remote OR user voted local
|
||||
let userID = activeMastodonAuthenticationBox.userID
|
||||
let didVotedRemote = (option.poll.votedBy ?? Set()).contains(where: { $0.id == userID })
|
||||
let votedOptions = poll.options.filter { option in
|
||||
(option.votedBy ?? Set()).map { $0.id }.contains(userID)
|
||||
}
|
||||
let didVotedLocal = !votedOptions.isEmpty
|
||||
|
||||
if poll.multiple {
|
||||
guard !option.poll.expired, !didVotedRemote else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
guard !option.poll.expired, !didVotedRemote, !didVotedLocal else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return indexPath
|
||||
} else {
|
||||
assertionFailure()
|
||||
return indexPath
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if tableView === statusView.pollTableView {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: indexPath: %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
|
||||
delegate?.statusTableViewCell(self, pollTableView: statusView.pollTableView, didSelectRowAt: indexPath)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StatusViewDelegate
|
||||
extension StatusTableViewCell: StatusViewDelegate {
|
||||
|
||||
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) {
|
||||
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
|
||||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MosaicImageViewDelegate
|
||||
|
|
|
@ -21,6 +21,8 @@ extension APIService {
|
|||
case badResponse
|
||||
case requestThrottle
|
||||
|
||||
case voteExpiredPoll
|
||||
|
||||
// Server API error
|
||||
case mastodonAPIError(Mastodon.API.Error)
|
||||
}
|
||||
|
@ -44,6 +46,7 @@ extension APIService.APIError: LocalizedError {
|
|||
case .badRequest: return "Bad Request"
|
||||
case .badResponse: return "Bad Response"
|
||||
case .requestThrottle: return "Request Throttled"
|
||||
case .voteExpiredPoll: return L10n.Common.Alerts.VoteFailure.title
|
||||
case .mastodonAPIError(let error):
|
||||
guard let responseError = error.mastodonError else {
|
||||
guard error.httpResponseStatus != .ok else {
|
||||
|
@ -62,6 +65,7 @@ extension APIService.APIError: LocalizedError {
|
|||
case .badRequest: return "Request invalid."
|
||||
case .badResponse: return "Response invalid."
|
||||
case .requestThrottle: return "Request too frequency."
|
||||
case .voteExpiredPoll: return L10n.Common.Alerts.VoteFailure.pollExpired
|
||||
case .mastodonAPIError(let error):
|
||||
guard let responseError = error.mastodonError else {
|
||||
return nil
|
||||
|
@ -73,9 +77,10 @@ extension APIService.APIError: LocalizedError {
|
|||
var helpAnchor: String? {
|
||||
switch errorReason {
|
||||
case .authenticationMissing: return "Please request after authenticated."
|
||||
case .badRequest: return "Please try again."
|
||||
case .badResponse: return "Please try again."
|
||||
case .requestThrottle: return "Please try again later."
|
||||
case .badRequest: return L10n.Common.Alerts.Common.pleaseTryAgain
|
||||
case .badResponse: return L10n.Common.Alerts.Common.pleaseTryAgain
|
||||
case .requestThrottle: return L10n.Common.Alerts.Common.pleaseTryAgainLater
|
||||
case .voteExpiredPoll: return nil
|
||||
case .mastodonAPIError(let error):
|
||||
guard let responseError = error.mastodonError else {
|
||||
return nil
|
||||
|
|
|
@ -94,7 +94,7 @@ extension APIService {
|
|||
assertionFailure()
|
||||
return
|
||||
}
|
||||
APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldToot, in: mastodonAuthenticationBox.domain, entity: entity, networkDate: response.networkDate)
|
||||
APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
|
||||
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "<nil>", entity.favouritesCount )
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
|
@ -132,7 +132,7 @@ extension APIService {
|
|||
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
let query = Mastodon.API.Favorites.ListQuery(limit: limit, minID: nil, maxID: maxID)
|
||||
return Mastodon.API.Favorites.getFavoriteStatus(domain: mastodonAuthenticationBox.domain, session: session, authorization: mastodonAuthenticationBox.userAuthorization, query: query)
|
||||
return Mastodon.API.Favorites.favoritedStatus(domain: mastodonAuthenticationBox.domain, session: session, authorization: mastodonAuthenticationBox.userAuthorization, query: query)
|
||||
.map { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
|
||||
let log = OSLog.api
|
||||
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
//
|
||||
// APIService+Poll.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import DateToolsSwift
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func poll(
|
||||
domain: String,
|
||||
pollID: Mastodon.Entity.Poll.ID,
|
||||
pollObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
||||
return Mastodon.API.Polls.poll(
|
||||
session: session,
|
||||
domain: domain,
|
||||
pollID: pollID,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> in
|
||||
let entity = response.value
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
|
||||
return managedObjectContext.performChanges {
|
||||
let _requestMastodonUser: MastodonUser? = {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
do {
|
||||
return try managedObjectContext.fetch(request).first
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
guard let requestMastodonUser = _requestMastodonUser else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard let poll = managedObjectContext.object(with: pollObjectID) as? Poll else { return }
|
||||
APIService.CoreData.merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Poll> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension APIService {
|
||||
|
||||
/// vote local
|
||||
/// # Note
|
||||
/// Not mark the poll voted so that view model could know when to reveal the results
|
||||
func vote(
|
||||
pollObjectID: NSManagedObjectID,
|
||||
mastodonUserObjectID: NSManagedObjectID,
|
||||
choices: [Int]
|
||||
) -> AnyPublisher<Mastodon.Entity.Poll.ID, Error> {
|
||||
var _targetPollID: Mastodon.Entity.Poll.ID?
|
||||
var isPollExpired = false
|
||||
var didVotedLocal = false
|
||||
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
return managedObjectContext.performChanges {
|
||||
let poll = managedObjectContext.object(with: pollObjectID) as! Poll
|
||||
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||
|
||||
_targetPollID = poll.id
|
||||
|
||||
if let expiresAt = poll.expiresAt, Date().timeIntervalSince(expiresAt) > 0 {
|
||||
isPollExpired = true
|
||||
poll.update(expired: true)
|
||||
return
|
||||
}
|
||||
|
||||
let options = poll.options.sorted(by: { $0.index.intValue < $1.index.intValue })
|
||||
let votedOptions = poll.options.filter { option in
|
||||
(option.votedBy ?? Set()).map { $0.id }.contains(mastodonUser.id)
|
||||
}
|
||||
|
||||
if !poll.multiple, !votedOptions.isEmpty {
|
||||
// if did voted for single poll. Do not allow vote again
|
||||
didVotedLocal = true
|
||||
return
|
||||
}
|
||||
|
||||
for option in options {
|
||||
let voted = choices.contains(option.index.intValue)
|
||||
option.update(voted: voted, by: mastodonUser)
|
||||
option.didUpdate(at: option.updatedAt) // trigger update without change anything
|
||||
}
|
||||
poll.didUpdate(at: poll.updatedAt) // trigger update without change anything
|
||||
}
|
||||
.tryMap { result in
|
||||
guard !isPollExpired else {
|
||||
throw APIError.explicit(APIError.ErrorReason.voteExpiredPoll)
|
||||
}
|
||||
guard !didVotedLocal else {
|
||||
throw APIError.implicit(APIError.ErrorReason.badRequest)
|
||||
}
|
||||
switch result {
|
||||
case .success:
|
||||
guard let targetPollID = _targetPollID else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
return targetPollID
|
||||
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// send vote request to remote
|
||||
func vote(
|
||||
domain: String,
|
||||
pollID: Mastodon.Entity.Poll.ID,
|
||||
pollObjectID: NSManagedObjectID,
|
||||
choices: [Int],
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
||||
let query = Mastodon.API.Polls.VoteQuery(choices: choices)
|
||||
return Mastodon.API.Polls.vote(
|
||||
session: session,
|
||||
domain: domain,
|
||||
pollID: pollID,
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> in
|
||||
let entity = response.value
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
|
||||
return managedObjectContext.performChanges {
|
||||
let _requestMastodonUser: MastodonUser? = {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
do {
|
||||
return try managedObjectContext.fetch(request).first
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
guard let requestMastodonUser = _requestMastodonUser else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
guard let poll = managedObjectContext.object(with: pollObjectID) as? Poll else { return }
|
||||
APIService.CoreData.merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate)
|
||||
}
|
||||
.setFailureType(to: Error.self)
|
||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Poll> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
|
@ -44,13 +44,22 @@ extension APIService.CoreData {
|
|||
|
||||
if let oldToot = oldToot {
|
||||
// merge old Toot
|
||||
APIService.CoreData.mergeToot(for: requestMastodonUser, old: oldToot,in: domain, entity: entity, networkDate: networkDate)
|
||||
APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
||||
return (oldToot, false, false)
|
||||
} else {
|
||||
let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, networkDate: networkDate, log: log)
|
||||
let application = entity.application.flatMap { app -> Application? in
|
||||
Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey))
|
||||
}
|
||||
let poll = entity.poll.flatMap { poll -> Poll in
|
||||
let options = poll.options.enumerated().map { i, option -> PollOption in
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
let metions = entity.mentions?.compactMap { mention -> Mention in
|
||||
Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url))
|
||||
}
|
||||
|
@ -83,6 +92,7 @@ extension APIService.CoreData {
|
|||
author: mastodonUser,
|
||||
reblog: reblog,
|
||||
application: application,
|
||||
poll: poll,
|
||||
mentions: metions,
|
||||
emojis: emojis,
|
||||
tags: tags,
|
||||
|
@ -97,10 +107,21 @@ extension APIService.CoreData {
|
|||
}
|
||||
}
|
||||
|
||||
static func mergeToot(for requestMastodonUser: MastodonUser?, old toot: Toot,in domain: String, entity: Mastodon.Entity.Status, networkDate: Date) {
|
||||
static func merge(
|
||||
toot: Toot,
|
||||
entity: Mastodon.Entity.Status,
|
||||
requestMastodonUser: MastodonUser?,
|
||||
domain: String,
|
||||
networkDate: Date
|
||||
) {
|
||||
guard networkDate > toot.updatedAt else { return }
|
||||
|
||||
// merge
|
||||
// merge poll
|
||||
if let poll = toot.poll, let entity = entity.poll {
|
||||
merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
||||
}
|
||||
|
||||
// merge metrics
|
||||
if entity.favouritesCount != toot.favouritesCount.intValue {
|
||||
toot.update(favouritesCount:NSNumber(value: entity.favouritesCount))
|
||||
}
|
||||
|
@ -113,6 +134,7 @@ extension APIService.CoreData {
|
|||
toot.update(reblogsCount:NSNumber(value: entity.reblogsCount))
|
||||
}
|
||||
|
||||
// merge relationship
|
||||
if let mastodonUser = requestMastodonUser {
|
||||
if let favourited = entity.favourited {
|
||||
toot.update(liked: favourited, mastodonUser: mastodonUser)
|
||||
|
@ -128,18 +150,44 @@ extension APIService.CoreData {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// set updateAt
|
||||
toot.didUpdate(at: networkDate)
|
||||
|
||||
// merge user
|
||||
mergeMastodonUser(for: requestMastodonUser, old: toot.author, in: domain, entity: entity.account, networkDate: networkDate)
|
||||
// merge indirect reblog & quote
|
||||
|
||||
// merge indirect reblog
|
||||
if let reblog = toot.reblog, let reblogEntity = entity.reblog {
|
||||
mergeToot(for: requestMastodonUser, old: reblog,in: domain, entity: reblogEntity, networkDate: networkDate)
|
||||
merge(toot: reblog, entity: reblogEntity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension APIService.CoreData {
|
||||
static func merge(
|
||||
poll: Poll,
|
||||
entity: Mastodon.Entity.Poll,
|
||||
requestMastodonUser: MastodonUser?,
|
||||
domain: String,
|
||||
networkDate: Date
|
||||
) {
|
||||
poll.update(expiresAt: entity.expiresAt)
|
||||
poll.update(expired: entity.expired)
|
||||
poll.update(votesCount: entity.votesCount)
|
||||
poll.update(votersCount: entity.votersCount)
|
||||
requestMastodonUser.flatMap {
|
||||
poll.update(voted: entity.voted ?? false, by: $0)
|
||||
}
|
||||
|
||||
let oldOptions = poll.options.sorted(by: { $0.index.intValue < $1.index.intValue })
|
||||
for (i, (optionEntity, option)) in zip(entity.options, oldOptions).enumerated() {
|
||||
let voted: Bool = (entity.ownVotes ?? []).contains(i)
|
||||
option.update(votesCount: optionEntity.votesCount)
|
||||
requestMastodonUser.flatMap { option.update(voted: voted, by: $0) }
|
||||
option.didUpdate(at: networkDate)
|
||||
}
|
||||
|
||||
poll.didUpdate(at: networkDate)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import Combine
|
|||
import Foundation
|
||||
|
||||
extension Mastodon.API.Favorites {
|
||||
|
||||
static func favoritesStatusesEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("favourites")
|
||||
}
|
||||
|
@ -30,6 +31,22 @@ extension Mastodon.API.Favorites {
|
|||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// Favourite / Undo Favourite
|
||||
///
|
||||
/// Add a status to your favourites list / Remove a status from your favourites list
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/3/3
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||
/// - Parameters:
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - statusID: Mastodon status id
|
||||
/// - session: `URLSession`
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Server` nested in the response
|
||||
public static func favorites(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, favoriteKind: FavoriteKind) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let url: URL = favoriteActionEndpointURL(domain: domain, statusID: statusID, favoriteKind: favoriteKind)
|
||||
var request = Mastodon.API.post(url: url, query: nil, authorization: authorization)
|
||||
|
@ -42,7 +59,23 @@ extension Mastodon.API.Favorites {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func getFavoriteByUserLists(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||
/// Favourited by
|
||||
///
|
||||
/// View who favourited a given status.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/3/3
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||
/// - Parameters:
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - statusID: Mastodon status id
|
||||
/// - session: `URLSession`
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Server` nested in the response
|
||||
public static func favoriteBy(domain: String, statusID: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
||||
let url = favoriteByUserListsEndpointURL(domain: domain, statusID: statusID)
|
||||
let request = Mastodon.API.get(url: url, query: nil, authorization: authorization)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
|
@ -53,7 +86,22 @@ extension Mastodon.API.Favorites {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func getFavoriteStatus(domain: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, query: Mastodon.API.Favorites.ListQuery) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
/// Favourited statuses
|
||||
///
|
||||
/// Using this endpoint to view the favourited list for user
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/3/3
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/favourites/)
|
||||
/// - Parameters:
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - session: `URLSession`
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Server` nested in the response
|
||||
public static func favoritedStatus(domain: String, session: URLSession, authorization: Mastodon.API.OAuth.Authorization, query: Mastodon.API.Favorites.ListQuery) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||
let url = favoritesStatusesEndpointURL(domain: domain)
|
||||
let request = Mastodon.API.get(url: url, query: query, authorization: authorization)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
|
@ -63,9 +111,11 @@ extension Mastodon.API.Favorites {
|
|||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension Mastodon.API.Favorites {
|
||||
|
||||
enum FavoriteKind {
|
||||
case create
|
||||
case destroy
|
||||
|
@ -103,4 +153,5 @@ public extension Mastodon.API.Favorites {
|
|||
return items
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
// Mastodon+API+Polls.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Polls {
|
||||
|
||||
static func viewPollEndpointURL(domain: String, pollID: Mastodon.Entity.Poll.ID) -> URL {
|
||||
let pathComponent = "polls/" + pollID
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
static func votePollEndpointURL(domain: String, pollID: Mastodon.Entity.Poll.ID) -> URL {
|
||||
let pathComponent = "polls/" + pollID + "/votes"
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// View a poll
|
||||
///
|
||||
/// Using this endpoint to view the poll of status
|
||||
///
|
||||
/// - Since: 2.8.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/3/3
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/statuses/polls/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - pollID: id for poll
|
||||
/// - authorization: User token. Could be nil if status is public
|
||||
/// - Returns: `AnyPublisher` contains `Poll` nested in the response
|
||||
public static func poll(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
pollID: Mastodon.Entity.Poll.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: viewPollEndpointURL(domain: domain, pollID: pollID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Poll.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Vote on a poll
|
||||
///
|
||||
/// Using this endpoint to vote an option of poll
|
||||
///
|
||||
/// - Since: 2.8.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/3/4
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/statuses/polls/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - pollID: id for poll
|
||||
/// - query: `VoteQuery`
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Poll` nested in the response
|
||||
public static func vote(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
pollID: Mastodon.Entity.Poll.ID,
|
||||
query: VoteQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Poll>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: votePollEndpointURL(domain: domain, pollID: pollID),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Poll.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Polls {
|
||||
public struct VoteQuery: Codable, PostQuery {
|
||||
public let choices: [Int]
|
||||
|
||||
public init(choices: [Int]) {
|
||||
self.choices = choices
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,13 +53,14 @@ extension Mastodon.API.Timeline {
|
|||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/2/19
|
||||
/// 2021/3/3
|
||||
/// # Reference
|
||||
/// [Document](https://https://docs.joinmastodon.org/methods/timelines/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `PublicTimelineQuery` with query parameters
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||
public static func home(
|
||||
session: URLSession,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by xiaojian sun on 2021/1/25.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import enum NIOHTTP1.HTTPResponseStatus
|
||||
|
||||
|
@ -93,6 +94,7 @@ extension Mastodon.API {
|
|||
public enum Instance { }
|
||||
public enum OAuth { }
|
||||
public enum Onboarding { }
|
||||
public enum Polls { }
|
||||
public enum Timeline { }
|
||||
public enum Favorites { }
|
||||
}
|
||||
|
@ -155,6 +157,7 @@ extension Mastodon.API {
|
|||
return try Mastodon.API.decoder.decode(type, from: data)
|
||||
} catch let decodeError {
|
||||
#if DEBUG
|
||||
os_log(.info, "%{public}s[%{public}ld], %{public}s: decode fail. content %s", ((#file as NSString).lastPathComponent), #line, #function, String(data: data, encoding: .utf8) ?? "<nil>")
|
||||
debugPrint(decodeError)
|
||||
#endif
|
||||
|
||||
|
|
Loading…
Reference in New Issue