From 80954b04925f67a936a43fb988e83f542df7d0c1 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 2 Mar 2021 15:51:16 +0800 Subject: [PATCH] feat: add Poll and PollOption entity to CoreDataStack --- .../CoreData.xcdatamodel/contents | 31 +++++- CoreDataStack/Entity/MastodonUser.swift | 1 + CoreDataStack/Entity/Poll.swift | 96 +++++++++++++++++++ CoreDataStack/Entity/PollOption.swift | 76 +++++++++++++++ CoreDataStack/Entity/Tag.swift | 11 ++- CoreDataStack/Entity/Toot.swift | 3 + Mastodon.xcodeproj/project.pbxproj | 10 +- .../xcschemes/xcschememanagement.plist | 4 +- Mastodon/Generated/Assets.swift | 1 - ...meTimelineViewController+DebugAction.swift | 55 +++++++++++ .../CoreData/APIService+CoreData+Toot.swift | 12 ++- 11 files changed, 284 insertions(+), 16 deletions(-) create mode 100644 CoreDataStack/Entity/Poll.swift create mode 100644 CoreDataStack/Entity/PollOption.swift 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)