diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index 3fe5fe16e..1ef9f929d 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
@@ -83,6 +83,7 @@
+
@@ -93,6 +94,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -131,6 +153,7 @@
+
@@ -138,14 +161,16 @@
+
-
+
-
-
+
+
+
\ No newline at end of file
diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift
index bcbfe5d26..8ecf66282 100644
--- a/CoreDataStack/Entity/MastodonUser.swift
+++ b/CoreDataStack/Entity/MastodonUser.swift
@@ -37,6 +37,7 @@ final public class MastodonUser: NSManagedObject {
@NSManaged public private(set) var reblogged: Set?
@NSManaged public private(set) var muted: Set?
@NSManaged public private(set) var bookmarked: Set?
+ @NSManaged public private(set) var votePollOptions: Set?
}
diff --git a/CoreDataStack/Entity/Poll.swift b/CoreDataStack/Entity/Poll.swift
new file mode 100644
index 000000000..1e8b2528f
--- /dev/null
+++ b/CoreDataStack/Entity/Poll.swift
@@ -0,0 +1,96 @@
+//
+// 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
+}
+
+extension Poll {
+
+ public override func awakeFromInsert() {
+ super.awakeFromInsert()
+ createdAt = Date()
+ }
+
+ @discardableResult
+ public static func insert(
+ into context: NSManagedObjectContext,
+ property: Property,
+ 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
+ poll.mutableSetValue(forKey: #keyPath(Poll.options)).addObjects(from: options)
+
+ return poll
+ }
+
+}
+
+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)]
+ }
+}
diff --git a/CoreDataStack/Entity/PollOption.swift b/CoreDataStack/Entity/PollOption.swift
new file mode 100644
index 000000000..f0d3219d8
--- /dev/null
+++ b/CoreDataStack/Entity/PollOption.swift
@@ -0,0 +1,76 @@
+//
+// 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?
+}
+
+extension PollOption {
+
+ public override func awakeFromInsert() {
+ super.awakeFromInsert()
+ createdAt = Date()
+ }
+
+ @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
+ }
+
+}
+
+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)]
+ }
+}
diff --git a/CoreDataStack/Entity/Tag.swift b/CoreDataStack/Entity/Tag.swift
index b5d8be688..3f5d2bcac 100644
--- a/CoreDataStack/Entity/Tag.swift
+++ b/CoreDataStack/Entity/Tag.swift
@@ -23,13 +23,14 @@ public final class Tag: NSManagedObject {
@NSManaged public private(set) var histories: Set?
}
-public extension Tag {
- override func awakeFromInsert() {
+extension Tag {
+ public override func awakeFromInsert() {
super.awakeFromInsert()
identifier = UUID()
}
+
@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]?
diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift
index b37609a21..c5fcf4869 100644
--- a/CoreDataStack/Entity/Toot.swift
+++ b/CoreDataStack/Entity/Toot.swift
@@ -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?
@@ -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)
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 0415385bb..cd08985c5 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -107,6 +107,8 @@
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 */; };
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 */; };
@@ -149,7 +151,6 @@
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
DB92CF7225E7BB98002C1017 /* PollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */; };
- DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98334625C8056600AD9700 /* AuthenticationViewModel.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 */; };
@@ -328,6 +329,8 @@
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = ""; };
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = ""; };
+ DB4481AC25EE155900BEFB67 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = ""; };
+ DB4481B225EE16D000BEFB67 /* PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOption.swift; sourceTree = ""; };
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardResponderService.swift; sourceTree = ""; };
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; };
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; };
@@ -371,7 +374,6 @@
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; };
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = ""; };
DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableViewCell.swift; sourceTree = ""; };
- DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; };
DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; };
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; };
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; };
@@ -945,6 +947,8 @@
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
2DA7D05625CA693F00804E11 /* Application.swift */,
DB9D6C2D25E504AC0051B173 /* Attachment.swift */,
+ DB4481AC25EE155900BEFB67 /* Poll.swift */,
+ DB4481B225EE16D000BEFB67 /* PollOption.swift */,
);
path = Entity;
sourceTree = "";
@@ -1590,8 +1594,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 */,
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index b1a7a744c..bc78dfa4b 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -17,12 +17,12 @@
Mastodon - Release.xcscheme_^#shared#^_
orderHint
- 1
+ 2
Mastodon.xcscheme_^#shared#^_
orderHint
- 0
+ 1
SuppressBuildableAutocreation
diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift
index a4f4e0803..08507ed9d 100644
--- a/Mastodon/Generated/Assets.swift
+++ b/Mastodon/Generated/Assets.swift
@@ -71,7 +71,6 @@ internal enum Asset {
internal enum Welcome {
internal static let mastodonLogo = ImageAsset(name: "Welcome/mastodon.logo")
internal static let mastodonLogoLarge = ImageAsset(name: "Welcome/mastodon.logo.large")
- internal static let welcomeLogo = ImageAsset(name: "Welcome/welcome.logo")
}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
index 69f0347e0..9c3af1f73 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
@@ -7,6 +7,8 @@
import os.log
import UIKit
+import CoreData
+import CoreDataStack
#if DEBUG
extension HomeTimelineViewController {
@@ -17,6 +19,7 @@ extension HomeTimelineViewController {
identifier: nil,
options: .displayInline,
children: [
+ 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 +32,62 @@ extension HomeTimelineViewController {
)
return menu
}
+
+ 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) Tweets", image: nil, attributes: [], handler: { [weak self] action in
+ guard let self = self else { return }
+ self.dropRecentTweetsAction(action, count: count)
+ })
+ }
+ )
+ }
}
extension HomeTimelineViewController {
+ @objc private func dropRecentTweetsAction(_ 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)
+ }
+ }
+ case .failure(let error):
+ assertionFailure(error.localizedDescription)
+ }
+ }
+ .store(in: &disposeBag)
+ }
+
@objc private func showPublicTimelineAction(_ sender: UIAction) {
coordinator.present(scene: .publicTimeline, from: self, transition: .show)
}
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift
index bbf814e66..eeb2afa2a 100644
--- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Toot.swift
@@ -51,6 +51,14 @@ extension APIService.CoreData {
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 object = Poll.insert(into: managedObjectContext, property: Poll.Property(id: poll.id, expiresAt: poll.expiresAt, expired: poll.expired, multiple: poll.multiple, votesCount: poll.votesCount, votersCount: poll.votersCount, networkDate: networkDate), options: options)
+ 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 +91,7 @@ extension APIService.CoreData {
author: mastodonUser,
reblog: reblog,
application: application,
+ poll: poll,
mentions: metions,
emojis: emojis,
tags: tags,
@@ -128,9 +137,6 @@ extension APIService.CoreData {
}
}
-
-
-
// set updateAt
toot.didUpdate(at: networkDate)