diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index fd2b557b8..ca67b1820 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,36 +1,105 @@ - + + + + + + + + + + + + + + + + + + + - + - + + + - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + - - - + + + + + + + \ No newline at end of file diff --git a/CoreDataStack/CoreDataStack.swift b/CoreDataStack/CoreDataStack.swift index d9046eafa..07b24a843 100644 --- a/CoreDataStack/CoreDataStack.swift +++ b/CoreDataStack/CoreDataStack.swift @@ -38,7 +38,7 @@ public final class CoreDataStack { }() static func persistentContainer() -> NSPersistentContainer { - let bundles = [Bundle(for: Toots.self)] + let bundles = [Bundle(for: Toot.self)] guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: bundles) else { fatalError("cannot locate bundles") } diff --git a/CoreDataStack/Entity/Emoji.swift b/CoreDataStack/Entity/Emoji.swift new file mode 100644 index 000000000..f43dcbf4a --- /dev/null +++ b/CoreDataStack/Entity/Emoji.swift @@ -0,0 +1,70 @@ +// +// Emoji.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/2/1. +// + +import CoreData +import Foundation + +public final class Emoji: NSManagedObject { + public typealias ID = UUID + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var createAt: Date + + @NSManaged public private(set) var shortcode: String + @NSManaged public private(set) var url: String + @NSManaged public private(set) var staticURL: String + @NSManaged public private(set) var visibleInPicker: Bool + @NSManaged public private(set) var category: String? + + // many-to-one relationship + @NSManaged public private(set) var toot: Toot? +} + +public extension Emoji { + override func awakeFromInsert() { + super.awakeFromInsert() + identifier = UUID() + } + + @discardableResult + static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> Emoji { + let emoji: Emoji = context.insertObject() + emoji.shortcode = property.shortcode + emoji.url = property.url + emoji.staticURL = property.staticURL + emoji.visibleInPicker = property.visibleInPicker + return emoji + } +} + +public extension Emoji { + struct Property { + + public let shortcode: String + public let url: String + public let staticURL: String + public let visibleInPicker: Bool + public let category: String? + + public init(shortcode: String, url: String, staticURL: String, visibleInPicker: Bool, category: String?) { + self.shortcode = shortcode + self.url = url + self.staticURL = staticURL + self.visibleInPicker = visibleInPicker + self.category = category + } + + } +} + +extension Emoji: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \Emoji.createAt, ascending: false)] + } +} diff --git a/CoreDataStack/Entity/History.swift b/CoreDataStack/Entity/History.swift new file mode 100644 index 000000000..664933687 --- /dev/null +++ b/CoreDataStack/Entity/History.swift @@ -0,0 +1,61 @@ +// +// History.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/2/1. +// + +import CoreData +import Foundation + +public final class History: NSManagedObject { + public typealias ID = UUID + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var createAt: Date + + @NSManaged public private(set) var day: Date + @NSManaged public private(set) var uses: Int + @NSManaged public private(set) var accounts: Int + + // many-to-one relationship + @NSManaged public private(set) var tag: Tag +} + +public extension History { + override func awakeFromInsert() { + super.awakeFromInsert() + identifier = UUID() + } + + @discardableResult + static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> History { + let history: History = context.insertObject() + history.day = property.day + history.uses = property.uses + history.accounts = property.accounts + return history + } +} + +public extension History { + struct Property { + public let day: Date + public let uses: Int + public let accounts: Int + + public init(day: Date, uses: Int, accounts: Int) { + self.day = day + self.uses = uses + self.accounts = accounts + } + } +} + +extension History: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \History.createAt, ascending: false)] + } +} diff --git a/CoreDataStack/Entity/HomeTimelineIndex.swift b/CoreDataStack/Entity/HomeTimelineIndex.swift index 3eb0a28df..7c4e4eb77 100644 --- a/CoreDataStack/Entity/HomeTimelineIndex.swift +++ b/CoreDataStack/Entity/HomeTimelineIndex.swift @@ -8,7 +8,7 @@ import Foundation import CoreData -final class HomeTimelineIndex: NSManagedObject { +final public class HomeTimelineIndex: NSManagedObject { public typealias ID = String @NSManaged public private(set) var identifier: ID @@ -18,7 +18,7 @@ final class HomeTimelineIndex: NSManagedObject { @NSManaged public private(set) var createdAt: Date // many-to-one relationship - @NSManaged public private(set) var toots: Toots + @NSManaged public private(set) var toot: Toot } @@ -28,16 +28,16 @@ extension HomeTimelineIndex { public static func insert( into context: NSManagedObjectContext, property: Property, - toots: Toots + toot: Toot ) -> HomeTimelineIndex { let index: HomeTimelineIndex = context.insertObject() index.identifier = property.identifier index.domain = property.domain - index.userIdentifier = toots.author.identifier - index.createdAt = toots.createdAt + index.userIdentifier = toot.author.identifier + index.createdAt = toot.createdAt - index.toots = toots + index.toot = toot return index } diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift index c649ca014..338acd514 100644 --- a/CoreDataStack/Entity/MastodonUser.swift +++ b/CoreDataStack/Entity/MastodonUser.swift @@ -5,11 +5,10 @@ // Created by MainasuK Cirno on 2021/1/27. // -import Foundation import CoreData +import Foundation -final class MastodonUser: NSManagedObject { - +public final class MastodonUser: NSManagedObject { public typealias ID = String @NSManaged public private(set) var identifier: ID @NSManaged public private(set) var domain: String @@ -17,19 +16,31 @@ final class MastodonUser: NSManagedObject { @NSManaged public private(set) var id: String @NSManaged public private(set) var acct: String @NSManaged public private(set) var username: String - @NSManaged public private(set) var displayName: String? + @NSManaged public private(set) var displayName: String + @NSManaged public private(set) var avatar: String + @NSManaged public private(set) var avatarStatic: String? @NSManaged public private(set) var createdAt: Date @NSManaged public private(set) var updatedAt: Date - @NSManaged public private(set) var toots: Set? - + // one-to-one relationship + @NSManaged public private(set) var pinnedToot: Toot? + + // one-to-many relationship + @NSManaged public private(set) var toots: Set? + + // many-to-many relationship + @NSManaged public private(set) var favourite: Set? + @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 retweets: Set? } -extension MastodonUser { - +public extension MastodonUser { @discardableResult - public static func insert( + static func insert( into context: NSManagedObjectContext, property: Property ) -> MastodonUser { @@ -42,24 +53,27 @@ extension MastodonUser { user.acct = property.acct user.username = property.username user.displayName = property.displayName + user.avatar = property.avatar + user.avatarStatic = property.avatarStatic user.createdAt = property.createdAt user.updatedAt = property.networkDate return user } - } -extension MastodonUser { - public struct Property { +public extension MastodonUser { + struct Property { public let identifier: String public let domain: String public let id: String public let acct: String public let username: String - public let displayName: String? + public let displayName: String + public let avatar: String + public let avatarStatic: String? public let createdAt: Date public let networkDate: Date @@ -69,8 +83,9 @@ extension MastodonUser { domain: String, acct: String, username: String, - displayName: String?, - content: String, + displayName: String, + avatar: String, + avatarStatic: String?, createdAt: Date, networkDate: Date ) { @@ -79,9 +94,9 @@ extension MastodonUser { self.id = id self.acct = acct self.username = username - self.displayName = displayName.flatMap { displayName in - return displayName.isEmpty ? nil : displayName - } + self.displayName = displayName + self.avatar = avatar + self.avatarStatic = avatarStatic self.createdAt = createdAt self.networkDate = networkDate } @@ -93,4 +108,3 @@ extension MastodonUser: Managed { return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)] } } - diff --git a/CoreDataStack/Entity/Mention.swift b/CoreDataStack/Entity/Mention.swift new file mode 100644 index 000000000..caec10d32 --- /dev/null +++ b/CoreDataStack/Entity/Mention.swift @@ -0,0 +1,65 @@ +// +// Mention.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/2/1. +// + +import CoreData +import Foundation + +public final class Mention: NSManagedObject { + public typealias ID = UUID + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var id: String + @NSManaged public private(set) var createAt: Date + + @NSManaged public private(set) var username: String + @NSManaged public private(set) var acct: String + @NSManaged public private(set) var url: String + + // many-to-one relationship + @NSManaged public private(set) var toot: Toot +} + +public extension Mention { + override func awakeFromInsert() { + super.awakeFromInsert() + identifier = UUID() + } + + @discardableResult + static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> Mention { + let mention: Mention = context.insertObject() + mention.id = property.id + mention.username = property.username + mention.acct = property.acct + mention.url = property.url + return mention + } +} + +public extension Mention { + struct Property { + public let id: String + public let username: String + public let acct: String + public let url: String + + public init(id: String, username: String, acct: String, url: String) { + self.id = id + self.username = username + self.acct = acct + self.url = url + } + } +} + +extension Mention: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \Mention.createAt, ascending: false)] + } +} diff --git a/CoreDataStack/Entity/Tag.swift b/CoreDataStack/Entity/Tag.swift new file mode 100644 index 000000000..b5d8be688 --- /dev/null +++ b/CoreDataStack/Entity/Tag.swift @@ -0,0 +1,64 @@ +// +// Tag.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/2/1. +// + +import CoreData +import Foundation + +public final class Tag: NSManagedObject { + public typealias ID = UUID + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var createAt: Date + + @NSManaged public private(set) var name: String + @NSManaged public private(set) var url: String + + // many-to-many relationship + @NSManaged public private(set) var toot: Toot + + // one-to-many relationship + @NSManaged public private(set) var histories: Set? +} + +public extension Tag { + override func awakeFromInsert() { + super.awakeFromInsert() + identifier = UUID() + } + @discardableResult + static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> Tag { + let tag: Tag = context.insertObject() + tag.name = property.name + tag.url = property.url + if let histories = property.histories { + tag.mutableSetValue(forKey: #keyPath(Tag.histories)).addObjects(from: histories) + } + return tag + } +} + +public extension Tag { + struct Property { + public let name: String + public let url: String + public let histories: [History]? + + public init(name: String, url: String, histories: [History]?) { + self.name = name + self.url = url + self.histories = histories + } + } +} + +extension Tag: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \Tag.createAt, ascending: false)] + } +} diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Toot.swift new file mode 100644 index 000000000..59ef034dd --- /dev/null +++ b/CoreDataStack/Entity/Toot.swift @@ -0,0 +1,269 @@ +// +// Toot.swift +// CoreDataStack +// +// Created by MainasuK Cirno on 2021/1/27. +// + +import CoreData +import Foundation + +public final class Toot: NSManagedObject { + public typealias ID = String + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var domain: String + + @NSManaged public private(set) var id: String + @NSManaged public private(set) var uri: String + @NSManaged public private(set) var createdAt: Date + @NSManaged public private(set) var content: String + + @NSManaged public private(set) var visibility: String? + @NSManaged public private(set) var sensitive: Bool + @NSManaged public private(set) var spoilerText: String? + + // Informational + @NSManaged public private(set) var reblogsCount: NSNumber + @NSManaged public private(set) var favouritesCount: NSNumber + @NSManaged public private(set) var repliesCount: NSNumber? + + @NSManaged public private(set) var url: String? + @NSManaged public private(set) var inReplyToID: Toot.ID? + @NSManaged public private(set) var inReplyToAccountID: MastodonUser.ID? + + @NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code) + @NSManaged public private(set) var text: String? + + // many-to-one relastionship + @NSManaged public private(set) var favouritedBy: MastodonUser? + @NSManaged public private(set) var rebloggedBy: MastodonUser? + @NSManaged public private(set) var mutedBy: MastodonUser? + @NSManaged public private(set) var bookmarkedBy: MastodonUser? + + // one-to-one relastionship + @NSManaged public private(set) var pinnedBy: MastodonUser? + + @NSManaged public private(set) var updatedAt: Date + @NSManaged public private(set) var deletedAt: Date? + + // one-to-many relationship + @NSManaged public private(set) var reblogFrom: Set? + + // one-to-many relationship + @NSManaged public private(set) var mentions: Set? + // one-to-many relationship + @NSManaged public private(set) var emojis: Set? + + // one-to-many relationship + @NSManaged public private(set) var tags: Set? + + // many-to-one relastionship + @NSManaged public private(set) var reblog: Toot? + + // many-to-one relationship + @NSManaged public private(set) var author: MastodonUser + + // one-to-many relationship + @NSManaged public private(set) var homeTimelineIndexes: Set? +} + +public extension Toot { + @discardableResult + static func insert( + into context: NSManagedObjectContext, + property: Property, + author: MastodonUser + ) -> Toot { + let toot: Toot = context.insertObject() + + toot.identifier = property.identifier + toot.domain = property.domain + + toot.id = property.id + toot.uri = property.uri + toot.createdAt = property.createdAt + toot.content = property.content + + toot.visibility = property.visibility + toot.sensitive = property.sensitive + toot.spoilerText = property.spoilerText + + if let mentions = property.mentions { + toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions) + } + + if let emojis = property.emojis { + toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: emojis) + } + + if let tags = property.tags { + toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags) + } + + toot.reblogsCount = property.reblogsCount + toot.favouritesCount = property.favouritesCount + toot.repliesCount = property.repliesCount + + toot.url = property.url + toot.inReplyToID = property.inReplyToID + toot.inReplyToAccountID = property.inReplyToAccountID + toot.reblog = property.reblog + toot.language = property.language + toot.text = property.text + + if let favouritedBy = property.favouritedBy { + toot.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(favouritedBy) + } + if let rebloggedBy = property.rebloggedBy { + toot.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(rebloggedBy) + } + if let mutedBy = property.mutedBy { + toot.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mutedBy) + } + if let bookmarkedBy = property.bookmarkedBy { + toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy) + } + if let pinnedBy = property.pinnedBy { + toot.mutableSetValue(forKey: #keyPath(Toot.pinnedBy)) + } + + + toot.updatedAt = property.updatedAt + toot.deletedAt = property.deletedAt + toot.author = property.author + toot.content = property.content + toot.homeTimelineIndexes = property.homeTimelineIndexes + + return toot + } +} + +public extension Toot { + struct Property { + public init( + domain: String, + id: String, + uri: String, + createdAt: Date, + content: String, + visibility: String?, + sensitive: Bool, + spoilerText: String?, + mentions: [Mention]?, + emojis: [Emoji]?, + tags: [Tag]?, + reblogsCount: NSNumber, + favouritesCount: NSNumber, + repliesCount: NSNumber?, + url: String?, + inReplyToID: Toot.ID?, + inReplyToAccountID: MastodonUser.ID?, + reblog: Toot?, + language: String?, + text: String?, + favouritedBy: MastodonUser?, + rebloggedBy: MastodonUser?, + mutedBy: MastodonUser?, + bookmarkedBy: MastodonUser?, + pinnedBy: MastodonUser?, + updatedAt: Date, + deletedAt: Date?, + author: MastodonUser, + homeTimelineIndexes: Set?) + { + self.identifier = id + "@" + domain + self.domain = domain + self.id = id + self.uri = uri + self.createdAt = createdAt + self.content = content + self.visibility = visibility + self.sensitive = sensitive + self.spoilerText = spoilerText + self.mentions = mentions + self.emojis = emojis + self.tags = tags + self.reblogsCount = reblogsCount + self.favouritesCount = favouritesCount + self.repliesCount = repliesCount + self.url = url + self.inReplyToID = inReplyToID + self.inReplyToAccountID = inReplyToAccountID + self.reblog = reblog + self.language = language + self.text = text + self.favouritedBy = favouritedBy + self.rebloggedBy = rebloggedBy + self.mutedBy = mutedBy + self.bookmarkedBy = bookmarkedBy + self.pinnedBy = pinnedBy + self.updatedAt = updatedAt + self.deletedAt = deletedAt + self.author = author + self.homeTimelineIndexes = homeTimelineIndexes + } + + public let identifier: ID + public let domain: String + + public let id: String + public let uri: String + public let createdAt: Date + public let content: String + + public let visibility: String? + public let sensitive: Bool + public let spoilerText: String? + + public let mentions: [Mention]? + public let emojis: [Emoji]? + public let tags: [Tag]? + public let reblogsCount: NSNumber + public let favouritesCount: NSNumber + public let repliesCount: NSNumber? + + public let url: String? + public let inReplyToID: Toot.ID? + public let inReplyToAccountID: MastodonUser.ID? + public let reblog: Toot? + public let language: String? // (ISO 639 Part @1 two-letter language code) + public let text: String? + + public let favouritedBy: MastodonUser? + public let rebloggedBy: MastodonUser? + public let mutedBy: MastodonUser? + public let bookmarkedBy: MastodonUser? + public let pinnedBy: MastodonUser? + + public let updatedAt: Date + public let deletedAt: Date? + + public let author: MastodonUser + + public let homeTimelineIndexes: Set? + } +} + +extension Toot: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \Toot.createdAt, ascending: false)] + } +} + +public extension Toot { + static func predicate(idStr: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(Toot.id), idStr) + } + + static func predicate(idStrs: [String]) -> NSPredicate { + return NSPredicate(format: "%K IN %@", #keyPath(Toot.id), idStrs) + } + + static func notDeleted() -> NSPredicate { + return NSPredicate(format: "%K == nil", #keyPath(Toot.deletedAt)) + } + + static func deleted() -> NSPredicate { + return NSPredicate(format: "%K != nil", #keyPath(Toot.deletedAt)) + } +} diff --git a/CoreDataStack/Entity/Toots.swift b/CoreDataStack/Entity/Toots.swift deleted file mode 100644 index ad5e64caa..000000000 --- a/CoreDataStack/Entity/Toots.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// Toots.swift -// CoreDataStack -// -// Created by MainasuK Cirno on 2021/1/27. -// - -import Foundation -import CoreData - -final class Toots: NSManagedObject { - - public typealias ID = String - @NSManaged public private(set) var identifier: ID - @NSManaged public private(set) var domain: String - - @NSManaged public private(set) var id: String - @NSManaged public private(set) var content: String - - @NSManaged public private(set) var createdAt: Date - @NSManaged public private(set) var updatedAt: Date - - // many-to-one relationship - @NSManaged public private(set) var author: MastodonUser - - // one-to-many relationship - @NSManaged public private(set) var homeTimelineIndexes: Set? - -} - -extension Toots { - - @discardableResult - public static func insert( - into context: NSManagedObjectContext, - property: Property, - author: MastodonUser - ) -> Toots { - let toots: Toots = context.insertObject() - - toots.identifier = property.identifier - toots.domain = property.domain - - toots.id = property.id - toots.content = property.content - toots.createdAt = property.createdAt - toots.updatedAt = property.networkDate - - toots.author = author - - return toots - } - -} - -extension Toots { - public struct Property { - public let identifier: String - public let domain: String - - public let id: String - public let content: String - public let createdAt: Date - public let networkDate: Date - - public init( - id: String, - domain: String, - content: String, - createdAt: Date, - networkDate: Date - ) { - self.identifier = id + "@" + domain - self.domain = domain - self.id = id - self.content = content - self.createdAt = createdAt - self.networkDate = networkDate - } - } -} - -extension Toots: Managed { - public static var defaultSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \Toots.createdAt, ascending: false)] - } -} - diff --git a/CoreDataStack/Extension/UIFont.swift b/CoreDataStack/Extension/UIFont.swift new file mode 100644 index 000000000..a1a97a112 --- /dev/null +++ b/CoreDataStack/Extension/UIFont.swift @@ -0,0 +1,35 @@ +// +// UIFont.swift +// CoreDataStack +// +// Created by sxiaojian on 2021/1/28. +// + +import UIKit + +extension UIFont { + + // refs: https://stackoverflow.com/questions/26371024/limit-supported-dynamic-type-font-sizes + static func preferredFont(withTextStyle textStyle: UIFont.TextStyle, maxSize: CGFloat) -> UIFont { + // Get the descriptor + let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) + + // Return a font with the minimum size + return UIFont(descriptor: fontDescriptor, size: min(fontDescriptor.pointSize, maxSize)) + } + + public static func preferredMonospacedFont(withTextStyle textStyle: UIFont.TextStyle, compatibleWith traitCollection: UITraitCollection? = nil) -> UIFont { + let fontDescription = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle).addingAttributes([ + UIFontDescriptor.AttributeName.featureSettings: [ + [ + UIFontDescriptor.FeatureKey.featureIdentifier: + kNumberSpacingType, + UIFontDescriptor.FeatureKey.typeIdentifier: + kMonospacedNumbersSelector + ] + ] + ]) + return UIFontMetrics(forTextStyle: textStyle).scaledFont(for: UIFont(descriptor: fontDescription, size: 0), compatibleWith: traitCollection) + } + +} diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 94f6e02ef..f3328cbf0 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -7,17 +7,45 @@ objects = { /* Begin PBXBuildFile section */ + 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; + 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; }; + 2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */; }; + 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; }; + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; + 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; }; + 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; + 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; + 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; }; + 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; }; + 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; }; + 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; }; + 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; + 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; + 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; + 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; }; + 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */; }; + 2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* TimelineSection.swift */; }; + 2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */; }; + 2D7631B325C159F700929FB9 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7631B225C159F700929FB9 /* Item.swift */; }; + 2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0125C7E4F2004F19B8 /* Mention.swift */; }; + 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; }; + 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; + 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; + 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; 3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; }; - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; + 45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; + 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */; }; DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102225BAA7B400EAA174 /* Assets.swift */; }; DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102325BAA7B400EAA174 /* Strings.swift */; }; @@ -37,7 +65,7 @@ DB89BA1B25C1107F008580ED /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1825C1107F008580ED /* Collection.swift */; }; DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */; }; DB89BA1D25C1107F008580ED /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1A25C1107F008580ED /* URL.swift */; }; - DB89BA2725C110B4008580ED /* Toots.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA2625C110B4008580ED /* Toots.swift */; }; + DB89BA2725C110B4008580ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA2625C110B4008580ED /* Toot.swift */; }; DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */; }; DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */; }; DB89BA4425C1165F008580ED /* Managed.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4225C1165F008580ED /* Managed.swift */; }; @@ -107,12 +135,38 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = ""; }; + 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePostView.swift; sourceTree = ""; }; + 2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; + 2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = ""; }; + 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = ""; }; + 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = ""; }; + 2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = ""; }; + 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = ""; }; + 2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = ""; }; + 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; + 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; + 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; + 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = ""; }; + 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; + 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+Diffable.swift"; sourceTree = ""; }; + 2D76319E25C1521200929FB9 /* TimelineSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSection.swift; sourceTree = ""; }; + 2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePostTableViewCell.swift; sourceTree = ""; }; + 2D7631B225C159F700929FB9 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + 2D927F0125C7E4F2004F19B8 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; + 2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; + 2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; + 2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; + 2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; + 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; }; 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; }; A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; @@ -148,7 +202,7 @@ DB89BA1825C1107F008580ED /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; }; DB89BA1A25C1107F008580ED /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; - DB89BA2625C110B4008580ED /* Toots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toots.swift; sourceTree = ""; }; + DB89BA2625C110B4008580ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = ""; }; DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreData.xcdatamodel; sourceTree = ""; }; DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkUpdatable.swift; sourceTree = ""; }; DB89BA4225C1165F008580ED /* Managed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Managed.swift; sourceTree = ""; }; @@ -173,11 +227,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */, - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, + 45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -186,6 +243,7 @@ buildActionMask = 2147483647; files = ( 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */, + 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -194,6 +252,7 @@ buildActionMask = 2147483647; files = ( 3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */, + 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -228,12 +287,119 @@ path = Pods; sourceTree = ""; }; - 4E8E8B18DB8471A676012CF9 /* Frameworks */ = { + 2D152A8A25C295B8009AA50C /* Content */ = { + isa = PBXGroup; + children = ( + 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */, + ); + path = Content; + sourceTree = ""; + }; + 2D42FF7C25C82207004A627A /* ToolBar */ = { + isa = PBXGroup; + children = ( + 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */, + ); + path = ToolBar; + sourceTree = ""; + }; + 2D42FF8325C82245004A627A /* Button */ = { + isa = PBXGroup; + children = ( + 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */, + ); + path = Button; + sourceTree = ""; + }; + 2D61335525C1886800CAE157 /* Service */ = { + isa = PBXGroup; + children = ( + 2D61335D25C1894B00CAE157 /* APIService.swift */, + 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */, + 2D61335625C1887F00CAE157 /* Persist */, + ); + path = Service; + sourceTree = ""; + }; + 2D61335625C1887F00CAE157 /* Persist */ = { + isa = PBXGroup; + children = ( + 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */, + ); + path = Persist; + sourceTree = ""; + }; + 2D76316325C14BAC00929FB9 /* PublicTimeline */ = { + isa = PBXGroup; + children = ( + 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */, + 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */, + 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */, + 2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */, + ); + path = PublicTimeline; + sourceTree = ""; + }; + 2D76319C25C151DE00929FB9 /* Diffiable */ = { + isa = PBXGroup; + children = ( + 2D7631B125C159E700929FB9 /* Item */, + 2D76319D25C151F600929FB9 /* Section */, + ); + path = Diffiable; + sourceTree = ""; + }; + 2D76319D25C151F600929FB9 /* Section */ = { + isa = PBXGroup; + children = ( + 2D76319E25C1521200929FB9 /* TimelineSection.swift */, + ); + path = Section; + sourceTree = ""; + }; + 2D7631A425C1532200929FB9 /* Share */ = { + isa = PBXGroup; + children = ( + 2D7631A525C1532D00929FB9 /* View */, + ); + path = Share; + sourceTree = ""; + }; + 2D7631A525C1532D00929FB9 /* View */ = { + isa = PBXGroup; + children = ( + 2D42FF8325C82245004A627A /* Button */, + 2D42FF7C25C82207004A627A /* ToolBar */, + 2D152A8A25C295B8009AA50C /* Content */, + 2D7631A625C1533800929FB9 /* TableviewCell */, + ); + path = View; + sourceTree = ""; + }; + 2D7631A625C1533800929FB9 /* TableviewCell */ = { isa = PBXGroup; children = ( 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */, A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */, - CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */, + 2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */, + ); + path = TableviewCell; + sourceTree = ""; + }; + 2D7631B125C159E700929FB9 /* Item */ = { + isa = PBXGroup; + children = ( + 2D7631B225C159F700929FB9 /* Item.swift */, + ); + path = Item; + sourceTree = ""; + }; + 3FE14AD363ED19AE7FF210A6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */, + 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */, + 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -300,7 +466,7 @@ DB89B9FC25C10FD0008580ED /* CoreDataStackTests */, DB427DD325BAA00100D1B89D /* Products */, 1EBA4F56E920856A3FC84ACB /* Pods */, - 4E8E8B18DB8471A676012CF9 /* Frameworks */, + 3FE14AD363ED19AE7FF210A6 /* Frameworks */, ); sourceTree = ""; }; @@ -321,7 +487,9 @@ children = ( DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB427DE325BAA00100D1B89D /* Info.plist */, + 2D76319C25C151DE00929FB9 /* Diffiable */, DB8AF52A25C13561002E6C99 /* State */, + 2D61335525C1886800CAE157 /* Service */, DB8AF56225C138BC002E6C99 /* Extension */, DB8AF55525C1379F002E6C99 /* Scene */, DB8AF54125C13647002E6C99 /* Coordinator */, @@ -379,6 +547,7 @@ DB89BA1825C1107F008580ED /* Collection.swift */, DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */, DB89BA1A25C1107F008580ED /* URL.swift */, + 2D152A9125C2980C009AA50C /* UIFont.swift */, ); path = Extension; sourceTree = ""; @@ -386,9 +555,13 @@ DB89BA2C25C110B7008580ED /* Entity */ = { isa = PBXGroup; children = ( - DB89BA2625C110B4008580ED /* Toots.swift */, + DB89BA2625C110B4008580ED /* Toot.swift */, DB8AF52425C131D1002E6C99 /* MastodonUser.swift */, DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */, + 2D927F0125C7E4F2004F19B8 /* Mention.swift */, + 2D927F0725C7E9A8004F19B8 /* Tag.swift */, + 2D927F0D25C7E9C9004F19B8 /* History.swift */, + 2D927F1325C7EDD9004F19B8 /* Emoji.swift */, ); path = Entity; sourceTree = ""; @@ -432,6 +605,8 @@ DB8AF55525C1379F002E6C99 /* Scene */ = { isa = PBXGroup; children = ( + 2D7631A425C1532200929FB9 /* Share */, + 2D76316325C14BAC00929FB9 /* PublicTimeline */, DB8AF54E25C13703002E6C99 /* MainTab */, DB8AF55625C137A8002E6C99 /* HomeViewController.swift */, DB01409B25C40BB600F9F3CF /* Authentication */, @@ -444,6 +619,11 @@ children = ( DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, + 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, + 2D46976325C2A71500CF4AA9 /* UIIamge.swift */, + 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, + 2D42FF6A25C817D2004A627A /* MastodonContent.swift */, + 2D42FF8E25C8228A004A627A /* UIButton.swift */, ); path = Extension; sourceTree = ""; @@ -481,9 +661,11 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* AlamofireImage */, - 5D526FE125BE9AC400460CB9 /* MastodonSDK */, - DB0140BC25C40D7500F9F3CF /* CommonOSLog */, + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -576,6 +758,7 @@ TargetAttributes = { DB427DD125BAA00100D1B89D = { CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1220; }; DB427DE725BAA00100D1B89D = { CreatedOnToolsVersion = 12.4; @@ -605,8 +788,10 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -788,20 +973,38 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2D7631B325C159F700929FB9 /* Item.swift in Sources */, + 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, + 2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */, + 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, + 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, + 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, + 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, + 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, + 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */, + 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, + 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, + 2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, + 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */, + 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */, + 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */, + 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, + 2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */, DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */, + 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */, DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */, DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, @@ -829,16 +1032,21 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */, DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */, DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */, + 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */, DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */, DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */, DB89BA1B25C1107F008580ED /* Collection.swift in Sources */, - DB89BA2725C110B4008580ED /* Toots.swift in Sources */, + DB89BA2725C110B4008580ED /* Toot.swift in Sources */, + 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */, DB89BA4425C1165F008580ED /* Managed.swift in Sources */, DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */, DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */, + 2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */, DB89BA1D25C1107F008580ED /* URL.swift in Sources */, + 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1030,6 +1238,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7LFDZ96332; @@ -1041,6 +1250,7 @@ MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1052,6 +1262,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7LFDZ96332; @@ -1307,7 +1518,23 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.1.0; + }; + }; + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -1315,7 +1542,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -1326,18 +1553,28 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { + isa = XCSwiftPackageProductDependency; + package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; + productName = ActiveLabel; + }; + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { + isa = XCSwiftPackageProductDependency; + package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; + productName = AlamofireNetworkActivityIndicator; + }; + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; productName = AlamofireImage; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 12419edb6..997b42f49 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 6 + 7 Mastodon.xcscheme_^#shared#^_ orderHint - 5 + 0 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 296f9dd2a..c1cd92950 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "ActiveLabel", + "repositoryURL": "https://github.com/TwidereProject/ActiveLabel.swift", + "state": { + "branch": null, + "revision": "3d8115c992c44358eabbb21ffc4616f4d56028b1", + "version": "3.0.0" + } + }, { "package": "Alamofire", "repositoryURL": "https://github.com/Alamofire/Alamofire.git", @@ -19,6 +28,15 @@ "version": "4.1.0" } }, + { + "package": "AlamofireNetworkActivityIndicator", + "repositoryURL": "https://github.com/Alamofire/AlamofireNetworkActivityIndicator", + "state": { + "branch": null, + "revision": "392bed083e8d193aca16bfa684ee24e4bcff0510", + "version": "3.1.0" + } + }, { "package": "CommonOSLog", "repositoryURL": "https://github.com/MainasuK/CommonOSLog", diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift new file mode 100644 index 000000000..d04261a3f --- /dev/null +++ b/Mastodon/Diffiable/Item/Item.swift @@ -0,0 +1,37 @@ +// +// Item.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import Foundation +import CoreData +import MastodonSDK +import CoreDataStack + +/// Note: update Equatable when change case +enum Item { + + // normal list + case toot(objectID: NSManagedObjectID) +} + +extension Item: Equatable { + static func == (lhs: Item, rhs: Item) -> Bool { + switch (lhs, rhs) { + case (.toot(let objectIDLeft), .toot(let objectIDRight)): + return objectIDLeft == objectIDRight + } + } +} + +extension Item: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case .toot(let objectID): + hasher.combine(objectID) + } + } +} + diff --git a/Mastodon/Diffiable/Section/TimelineSection.swift b/Mastodon/Diffiable/Section/TimelineSection.swift new file mode 100644 index 000000000..881b51544 --- /dev/null +++ b/Mastodon/Diffiable/Section/TimelineSection.swift @@ -0,0 +1,74 @@ +// +// TimelineSection.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import Combine +import CoreData +import CoreDataStack +import os.log +import UIKit + +enum TimelineSection: Equatable, Hashable { + case main +} + +extension TimelineSection { + static func tableViewDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency, + managedObjectContext: NSManagedObjectContext, + timestampUpdatePublisher: AnyPublisher, + timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in + guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() } + + switch item { + case .toot(let objectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelinePostTableViewCell.self), for: indexPath) as! TimelinePostTableViewCell + + // configure cell + managedObjectContext.performAndWait { + let toot = managedObjectContext.object(with: objectID) as! Toot + TimelineSection.configure(cell: cell,timestampUpdatePublisher: timestampUpdatePublisher, toot: toot) + } + cell.delegate = timelinePostTableViewCellDelegate + return cell + } + } + } + + static func configure( + cell: TimelinePostTableViewCell, + timestampUpdatePublisher: AnyPublisher, + toot: Toot + ) { + // set name username avatar + cell.timelinePostView.nameLabel.text = toot.author.displayName + cell.timelinePostView.usernameLabel.text = "@" + toot.author.username + cell.timelinePostView.avatarImageView.af.setImage( + withURL: URL(string: toot.author.avatar)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + // set text + cell.timelinePostView.activeTextLabel.config(content: toot.content) + // set date + let createdAt = (toot.reblog ?? toot).createdAt + timestampUpdatePublisher + .sink { _ in + cell.timelinePostView.dateLabel.text = createdAt.shortTimeAgoSinceNow + } + .store(in: &cell.disposeBag) + } +} + +extension TimelineSection { + private static func formattedNumberTitleForActionButton(_ number: Int?) -> String { + guard let number = number, number > 0 else { return "" } + return String(number) + } +} diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift new file mode 100644 index 000000000..6e08d7268 --- /dev/null +++ b/Mastodon/Extension/ActiveLabel.swift @@ -0,0 +1,57 @@ +// +// ActiveLabel.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/29. +// + +import UIKit +import Foundation +import ActiveLabel +import os.log + +extension ActiveLabel { + + enum Style { + case `default` + case timelineHeaderView + } + + convenience init(style: Style) { + self.init() + + switch style { + case .default: +// urlMaximumLength = 30 + font = .preferredFont(forTextStyle: .body) + textColor = .white + case .timelineHeaderView: + font = .preferredFont(forTextStyle: .footnote) + textColor = .secondaryLabel + } + + numberOfLines = 0 + mentionColor = UIColor.yellow + hashtagColor = UIColor.blue + URLColor = UIColor.red + text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + +} + +extension ActiveLabel { + func config(content: String) { + if let parseResult = try? TootContent.parse(toot: content) { + activeEntities.removeAll() + numberOfLines = 0 + font = UIFont(name: "SFProText-Regular", size: 16) + textColor = .white + URLColor = .systemRed + mentionColor = .systemGreen + hashtagColor = .systemBlue + text = parseResult.trimmed + activeEntities = parseResult.activeEntities + } + } +} + diff --git a/Mastodon/Extension/MastodonContent.swift b/Mastodon/Extension/MastodonContent.swift new file mode 100755 index 000000000..3e6b072d6 --- /dev/null +++ b/Mastodon/Extension/MastodonContent.swift @@ -0,0 +1,302 @@ +// +// MastodonContent.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/2/1. +// + +import Foundation +import Kanna +import ActiveLabel + +enum TootContent { + + static func parse(toot: String) throws -> TootContent.ParseResult { + let toot = toot.replacingOccurrences(of: "
", with: "\n") + let rootNode = try Node.parse(document: toot) + let text = String(rootNode.text) + + var activeEntities: [ActiveEntity] = [] + let entities = TootContent.Node.entities(in: rootNode) + for entity in entities { + let range = NSRange(entity.text.startIndex.. String { + guard self.hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) + } +} + +extension TootContent { + struct ParseResult { + let document: String + let original: String + let trimmed: String + let activeEntities: [ActiveEntity] + } +} + + +extension TootContent { + + class Node { + + let level: Int + let type: Type? + + // substring text + let text: Substring + + // range in parent String + var range: Range { + return text.startIndex.. + let href: String? + let hrefEllipsis: String? + + let children: [Node] + + init( + level: Int, + text: Substring, + tagName: String?, + className: String?, + href: String?, + hrefEllipsis: String?, + children: [Node] + ) { + let _classNames: Set = { + guard let className = className else { return Set() } + return Set(className.components(separatedBy: " ")) + }() + let _type: Type? = { + if tagName == "a" && !_classNames.contains("mention") { + return .url + } + + if _classNames.contains("mention") { + if _classNames.contains("u-url") { + return .mention + } else if _classNames.contains("hashtag") { + return .hashtag + } + } + + return nil + }() + self.level = level + self.type = _type + self.text = text + self.tagName = tagName + self.classNames = _classNames + self.href = href + self.hrefEllipsis = hrefEllipsis + self.children = children + } + + static func parse(document: String) throws -> TootContent.Node { + let html = try HTML(html: document, encoding: .utf8) + let body = html.body ?? nil + let text = body?.text ?? "" + let level = 0 + let children: [TootContent.Node] = body.flatMap { body in + return Node.parse(element: body, parentText: text[...], parentLevel: level + 1) + } ?? [] + let node = Node( + level: level, + text: text[...], + tagName: body?.tagName, + className: body?.className, + href: nil, + hrefEllipsis: nil, + children: children + ) + + return node + } + + static func parse(element: XMLElement, parentText: Substring, parentLevel: Int) -> [Node] { + let parent = element + let scanner = Scanner(string: String(parentText)) + scanner.charactersToBeSkipped = .none + + var element = parent.at_css(":first-child") + var children: [Node] = [] + + while let _element = element { + let _text = _element.text ?? "" + + // scan element text + _ = scanner.scanUpToString(_text) + let startIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string) + guard scanner.scanString(_text) != nil else { + assertionFailure() + continue + } + let endIndexOffset = scanner.currentIndex.utf16Offset(in: scanner.string) + + // locate substring + let startIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: startIndexOffset) + let endIndex = parentText.utf16.index(parentText.utf16.startIndex, offsetBy: endIndexOffset) + let text = Substring(parentText.utf16[startIndex.. Bool + ) -> [Node] { + var nodes: [Node] = [] + + if predicate(node) { + nodes.append(node) + } + + for child in node.children { + nodes.append(contentsOf: Node.collect(node: child, where: predicate)) + } + return nodes + } + + } + +} + +extension TootContent.Node { + enum `Type` { + case url + case mention + case hashtag + } + + static func entities(in node: TootContent.Node) -> [TootContent.Node] { + return TootContent.Node.collect(node: node) { node in node.type != nil } + } + + static func hashtags(in node: TootContent.Node) -> [TootContent.Node] { + return TootContent.Node.collect(node: node) { node in node.type == .hashtag } + } + + static func mentions(in node: TootContent.Node) -> [TootContent.Node] { + return TootContent.Node.collect(node: node) { node in node.type == .mention } + } + + static func urls(in node: TootContent.Node) -> [TootContent.Node] { + return TootContent.Node.collect(node: node) { node in node.type == .url } + } + +} + +extension TootContent.Node: CustomDebugStringConvertible { + var debugDescription: String { + let linkInfo: String = { + switch (href, hrefEllipsis) { + case (nil, nil): + return "" + case (let href, let hrefEllipsis): + return "(\(href ?? "nil") - \(hrefEllipsis ?? "nil"))" + } + }() + let classNamesInfo: String = { + guard !classNames.isEmpty else { return "" } + let names = Array(classNames) + .sorted() + .joined(separator: ", ") + return "@[\(names)]" + }() + let nodeDescription = String( + format: "<%@>%@%@: %@", + tagName ?? "", + classNamesInfo, + linkInfo, + String(text) + ) + guard !children.isEmpty else { + return nodeDescription + } + + let indent = Array(repeating: " ", count: level).joined() + let childrenDescription = children + .map { indent + $0.debugDescription } + .joined(separator: "\n") + + return nodeDescription + "\n" + childrenDescription + } +} diff --git a/Mastodon/Extension/NSLayoutConstraint.swift b/Mastodon/Extension/NSLayoutConstraint.swift new file mode 100644 index 000000000..cae353187 --- /dev/null +++ b/Mastodon/Extension/NSLayoutConstraint.swift @@ -0,0 +1,15 @@ +// +// NSLayoutConstraint.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/28. +// + +import UIKit + +extension NSLayoutConstraint { + func priority(_ priority: UILayoutPriority) -> Self { + self.priority = priority + return self + } +} diff --git a/Mastodon/Extension/UIButton.swift b/Mastodon/Extension/UIButton.swift new file mode 100644 index 000000000..916ad222d --- /dev/null +++ b/Mastodon/Extension/UIButton.swift @@ -0,0 +1,45 @@ +// +// UIButton.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/1. +// + +import UIKit + +extension UIButton { + func setInsets( + forContentPadding contentPadding: UIEdgeInsets, + imageTitlePadding: CGFloat + ) { + switch UIApplication.shared.userInterfaceLayoutDirection { + case .rightToLeft: + self.contentEdgeInsets = UIEdgeInsets( + top: contentPadding.top, + left: contentPadding.left + imageTitlePadding, + bottom: contentPadding.bottom, + right: contentPadding.right + ) + self.titleEdgeInsets = UIEdgeInsets( + top: 0, + left: -imageTitlePadding, + bottom: 0, + right: imageTitlePadding + ) + default: + self.contentEdgeInsets = UIEdgeInsets( + top: contentPadding.top, + left: contentPadding.left, + bottom: contentPadding.bottom, + right: contentPadding.right + imageTitlePadding + ) + self.titleEdgeInsets = UIEdgeInsets( + top: 0, + left: imageTitlePadding, + bottom: 0, + right: -imageTitlePadding + ) + } + } +} + diff --git a/Mastodon/Extension/UIIamge.swift b/Mastodon/Extension/UIIamge.swift new file mode 100644 index 000000000..20069f7c7 --- /dev/null +++ b/Mastodon/Extension/UIIamge.swift @@ -0,0 +1,42 @@ +// +// UIIamge.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/28. +// + +import UIKit +import CoreImage +import CoreImage.CIFilterBuiltins + +extension UIImage { + + static func placeholder(size: CGSize = CGSize(width: 1, height: 1), color: UIColor) -> UIImage { + let render = UIGraphicsImageRenderer(size: size) + + return render.image { (context: UIGraphicsImageRendererContext) in + context.cgContext.setFillColor(color.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + } + } + +} + +// refs: https://www.hackingwithswift.com/example-code/media/how-to-read-the-average-color-of-a-uiimage-using-ciareaaverage +extension UIImage { + @available(iOS 14.0, *) + var dominantColor: UIColor? { + guard let inputImage = CIImage(image: self) else { return nil } + + let filter = CIFilter.areaAverage() + filter.inputImage = inputImage + filter.extent = inputImage.extent + guard let outputImage = filter.outputImage else { return nil } + + var bitmap = [UInt8](repeating: 0, count: 4) + let context = CIContext(options: [.workingColorSpace: kCFNull]) + context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil) + + return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) + } +} diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 62a06accc..c5b25beb6 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -12,6 +12,8 @@ // Deprecated typealiases @available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") internal typealias AssetColorTypeAlias = ColorAsset.Color +@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") +internal typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable superfluous_disable_command file_length implicit_return @@ -20,6 +22,27 @@ internal typealias AssetColorTypeAlias = ColorAsset.Color // swiftlint:disable identifier_name line_length nesting type_body_length type_name internal enum Asset { internal static let accentColor = ColorAsset(name: "AccentColor") + internal enum Colors { + internal static let tootDark = ColorAsset(name: "Colors/Toot.Dark") + internal static let tootGray = ColorAsset(name: "Colors/Toot.Gray") + internal static let tootWhite = ColorAsset(name: "Colors/Toot.White") + internal static let likeOrange = ColorAsset(name: "Colors/like.orange") + } + internal enum ToolBar { + internal static let bookmark = ImageAsset(name: "ToolBar/bookmark") + internal static let lock = ImageAsset(name: "ToolBar/lock") + internal static let more = ImageAsset(name: "ToolBar/more") + internal static let reply = ImageAsset(name: "ToolBar/reply") + internal static let retoot = ImageAsset(name: "ToolBar/retoot") + internal static let star = ImageAsset(name: "ToolBar/star") + } + internal enum TootTimeline { + internal static let global = ImageAsset(name: "TootTimeline/Global") + internal static let textlock = ImageAsset(name: "TootTimeline/Textlock") + internal static let email = ImageAsset(name: "TootTimeline/email") + internal static let lock = ImageAsset(name: "TootTimeline/lock") + internal static let unlock = ImageAsset(name: "TootTimeline/unlock") + } } // swiftlint:enable identifier_name line_length nesting type_body_length type_name @@ -61,6 +84,47 @@ internal extension ColorAsset.Color { } } +internal struct ImageAsset { + internal fileprivate(set) var name: String + + #if os(macOS) + internal typealias Image = NSImage + #elseif os(iOS) || os(tvOS) || os(watchOS) + internal typealias Image = UIImage + #endif + + internal var image: Image { + let bundle = BundleToken.bundle + #if os(iOS) || os(tvOS) + let image = Image(named: name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + let name = NSImage.Name(self.name) + let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) + #elseif os(watchOS) + let image = Image(named: name) + #endif + guard let result = image else { + fatalError("Unable to load image asset named \(name).") + } + return result + } +} + +internal extension ImageAsset.Image { + @available(macOS, deprecated, + message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") + convenience init?(asset: ImageAsset) { + #if os(iOS) || os(tvOS) + let bundle = BundleToken.bundle + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + self.init(named: NSImage.Name(asset.name)) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + // swiftlint:disable convenience_type private final class BundleToken { static let bundle: Bundle = { diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Toot.Dark.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Toot.Dark.colorset/Contents.json new file mode 100644 index 000000000..ca15c25d7 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Toot.Dark.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "extended-srgb", + "components" : { + "alpha" : "1.000", + "blue" : "55", + "green" : "45", + "red" : "41" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "55", + "green" : "45", + "red" : "41" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Toot.Gray.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Toot.Gray.colorset/Contents.json new file mode 100644 index 000000000..d06ba8456 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Toot.Gray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "132", + "green" : "105", + "red" : "96" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "132", + "green" : "105", + "red" : "96" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Toot.White.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Toot.White.colorset/Contents.json new file mode 100644 index 000000000..43e2bc587 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Toot.White.colorset/Contents.json @@ -0,0 +1,41 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/like.orange.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/like.orange.colorset/Contents.json new file mode 100644 index 000000000..b50f8fa6c --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/like.orange.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "10", + "green" : "159", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "10", + "green" : "159", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/Contents.json b/Mastodon/Resources/Assets.xcassets/ToolBar/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/bookmark.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/ToolBar/bookmark.imageset/Contents.json new file mode 100644 index 000000000..4a79584fa --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/bookmark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "bookmark.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/bookmark.imageset/bookmark.pdf b/Mastodon/Resources/Assets.xcassets/ToolBar/bookmark.imageset/bookmark.pdf new file mode 100644 index 000000000..846a6e572 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/bookmark.imageset/bookmark.pdf @@ -0,0 +1,170 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.063477 0 0.180664 -0.195801 0.882812 1.271484 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.063477 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -6.382812 0.679688 cm +0.376471 0.411765 0.517647 scn +3.492188 2.453125 m +h +7.554688 -0.679688 m +8.007812 -0.679688 8.312500 -0.453125 8.945312 0.171875 c +11.937500 3.140625 l +11.968750 3.171875 12.031250 3.171875 12.070312 3.140625 c +15.054688 0.164062 l +15.695312 -0.453125 15.992188 -0.679688 16.453125 -0.679688 c +17.164062 -0.679688 17.617188 -0.179688 17.617188 0.601562 c +17.617188 14.289062 l +17.617188 15.898438 16.750000 16.773438 15.156250 16.773438 c +8.843750 16.773438 l +7.250000 16.773438 6.382812 15.898438 6.382812 14.289062 c +6.382812 0.601562 l +6.382812 -0.179688 6.835938 -0.679688 7.554688 -0.679688 c +h +8.382812 2.257812 m +8.281250 2.156250 8.164062 2.187500 8.164062 2.335938 c +8.164062 14.140625 l +8.164062 14.718750 8.437500 14.992188 9.023438 14.992188 c +14.976562 14.992188 l +15.562500 14.992188 15.843750 14.718750 15.843750 14.140625 c +15.843750 2.335938 l +15.843750 2.187500 15.726562 2.156250 15.617188 2.257812 c +12.601562 5.179688 l +12.203125 5.562500 11.796875 5.562500 11.398438 5.179688 c +8.382812 2.257812 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -6.382812 0.679688 cm +BT +16.000000 0.000000 0.000000 16.000000 3.492188 2.453125 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 1276 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 11.234375 17.453125 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Type /Catalog + /Pages 11 0 R + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000002373 00000 n +0000002396 00000 n +0000002571 00000 n +0000002647 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +2708 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/lock.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/ToolBar/lock.imageset/Contents.json new file mode 100644 index 000000000..fe86d2855 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/lock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "lock.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/lock.imageset/lock.pdf b/Mastodon/Resources/Assets.xcassets/ToolBar/lock.imageset/lock.pdf new file mode 100644 index 000000000..3aabb0bd0 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/lock.imageset/lock.pdf @@ -0,0 +1,174 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.093750 0 0.197266 -0.131348 0.896484 1.184082 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.093750 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -6.406250 1.312500 cm +0.266667 0.294118 0.364706 scn +3.250000 0.789062 m +h +8.257812 -1.312500 m +15.742188 -1.312500 l +16.984375 -1.312500 17.593750 -0.695312 17.593750 0.648438 c +17.593750 6.343750 l +17.593750 7.531250 17.101562 8.156250 16.117188 8.273438 c +16.117188 10.039062 l +16.117188 13.023438 14.101562 14.476562 12.000000 14.476562 c +9.898438 14.476562 7.882812 13.023438 7.882812 10.039062 c +7.882812 8.273438 l +6.890625 8.156250 6.406250 7.531250 6.406250 6.343750 c +6.406250 0.648438 l +6.406250 -0.695312 7.015625 -1.312500 8.257812 -1.312500 c +h +9.570312 10.171875 m +9.570312 11.882812 10.656250 12.843750 12.000000 12.843750 c +13.343750 12.843750 14.429688 11.882812 14.429688 10.171875 c +14.429688 8.296875 l +9.570312 8.296875 l +9.570312 10.171875 l +h +8.656250 0.289062 m +8.328125 0.289062 8.164062 0.445312 8.164062 0.843750 c +8.164062 6.148438 l +8.164062 6.546875 8.328125 6.687500 8.656250 6.687500 c +15.343750 6.687500 l +15.679688 6.687500 15.835938 6.546875 15.835938 6.148438 c +15.835938 0.843750 l +15.835938 0.445312 15.679688 0.289062 15.343750 0.289062 c +8.656250 0.289062 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -6.406250 1.312500 cm +BT +16.000000 0.000000 0.000000 16.000000 3.250000 0.789062 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 1332 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 11.187500 15.789062 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Type /Catalog + /Pages 11 0 R + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000002429 00000 n +0000002452 00000 n +0000002627 00000 n +0000002703 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +2764 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/more.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/ToolBar/more.imageset/Contents.json new file mode 100644 index 000000000..d6d5bc04d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/more.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "more.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/more.imageset/more.pdf b/Mastodon/Resources/Assets.xcassets/ToolBar/more.imageset/more.pdf new file mode 100644 index 000000000..93abdd80a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/more.imageset/more.pdf @@ -0,0 +1,162 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.124512 0 0.087402 0.243652 1.037109 0.304199 d1 + +endstream +endobj + +2 0 obj + 50 +endobj + +3 0 obj + [ 1.124512 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -4.398438 7.632812 cm +0.376471 0.411765 0.517647 scn +3.000000 -11.531250 m +h +7.875000 -5.898438 m +7.875000 -4.921875 7.117188 -4.164062 6.132812 -4.164062 c +5.179688 -4.164062 4.398438 -4.937500 4.398438 -5.898438 c +4.398438 -6.835938 5.179688 -7.632812 6.132812 -7.632812 c +7.078125 -7.632812 7.875000 -6.835938 7.875000 -5.898438 c +h +13.726562 -5.898438 m +13.726562 -4.921875 12.968750 -4.164062 11.992188 -4.164062 c +11.039062 -4.164062 10.265625 -4.937500 10.265625 -5.898438 c +10.265625 -6.835938 11.039062 -7.632812 11.992188 -7.632812 c +12.937500 -7.632812 13.726562 -6.835938 13.726562 -5.898438 c +h +19.593750 -5.898438 m +19.593750 -4.921875 18.835938 -4.164062 17.859375 -4.164062 c +16.898438 -4.164062 16.117188 -4.937500 16.117188 -5.898438 c +16.117188 -6.835938 16.898438 -7.632812 17.859375 -7.632812 c +18.796875 -7.632812 19.593750 -6.835938 19.593750 -5.898438 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -4.398438 7.632812 cm +BT +16.000000 0.000000 0.000000 16.000000 3.000000 -11.531250 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 1113 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 15.195312 3.468750 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Type /Catalog + /Pages 11 0 R + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000116 00000 n +0000000137 00000 n +0000000168 00000 n +0000000560 00000 n +0000000582 00000 n +0000000994 00000 n +0000001040 00000 n +0000002209 00000 n +0000002232 00000 n +0000002406 00000 n +0000002482 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +2543 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/reply.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/ToolBar/reply.imageset/Contents.json new file mode 100644 index 000000000..37b4fcb41 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/reply.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reply all.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/reply.imageset/reply all.pdf b/Mastodon/Resources/Assets.xcassets/ToolBar/reply.imageset/reply all.pdf new file mode 100644 index 000000000..a23ce8334 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/reply.imageset/reply all.pdf @@ -0,0 +1,206 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.523438 0 0.076172 -0.107910 1.409668 0.996582 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.523438 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -1.031250 2.000000 cm +0.376471 0.411765 0.517647 scn +-0.187500 -0.273438 m +h +8.937500 -2.000000 m +9.601562 -2.000000 10.101562 -1.492188 10.101562 -0.828125 c +10.101562 0.390625 l +12.023438 -1.406250 l +12.460938 -1.812500 12.820312 -2.000000 13.281250 -2.000000 c +13.945312 -2.000000 14.445312 -1.492188 14.445312 -0.828125 c +14.445312 1.828125 l +14.617188 1.828125 l +17.335938 1.828125 18.953125 0.929688 20.062500 -1.140625 c +20.406250 -1.757812 20.804688 -1.929688 21.273438 -1.929688 c +21.921875 -1.929688 22.367188 -1.304688 22.367188 -0.078125 c +22.367188 5.515625 19.828125 8.867188 14.617188 8.867188 c +14.445312 8.867188 l +14.445312 11.531250 l +14.445312 12.195312 13.945312 12.726562 13.265625 12.726562 c +12.828125 12.726562 12.507812 12.546875 12.023438 12.101562 c +10.101562 10.320312 l +10.101562 11.531250 l +10.101562 12.195312 9.601562 12.726562 8.921875 12.726562 c +8.476562 12.726562 8.164062 12.546875 7.679688 12.101562 c +1.468750 6.335938 l +1.156250 6.039062 1.031250 5.687500 1.031250 5.367188 c +1.031250 5.054688 1.164062 4.687500 1.476562 4.390625 c +7.679688 -1.406250 l +8.109375 -1.812500 8.476562 -2.000000 8.937500 -2.000000 c +h +8.273438 0.343750 m +3.109375 5.218750 l +3.046875 5.281250 3.031250 5.320312 3.031250 5.367188 c +3.031250 5.414062 3.046875 5.453125 3.109375 5.507812 c +8.273438 10.429688 l +8.312500 10.460938 8.351562 10.484375 8.406250 10.484375 c +8.476562 10.484375 8.523438 10.437500 8.523438 10.359375 c +8.523438 8.851562 l +5.820312 6.335938 l +5.507812 6.039062 5.375000 5.687500 5.375000 5.367188 c +5.375000 5.054688 5.507812 4.687500 5.820312 4.390625 c +8.523438 1.867188 l +8.523438 0.414062 l +8.523438 0.335938 8.476562 0.281250 8.406250 0.281250 c +8.359375 0.281250 8.320312 0.296875 8.273438 0.343750 c +h +12.750000 0.281250 m +12.703125 0.281250 12.664062 0.296875 12.617188 0.343750 c +7.453125 5.218750 l +7.390625 5.281250 7.375000 5.320312 7.375000 5.367188 c +7.375000 5.414062 7.398438 5.453125 7.453125 5.507812 c +12.617188 10.429688 l +12.656250 10.460938 12.703125 10.484375 12.750000 10.484375 c +12.820312 10.484375 12.867188 10.437500 12.867188 10.359375 c +12.867188 7.523438 l +12.867188 7.351562 12.945312 7.273438 13.125000 7.273438 c +14.078125 7.273438 l +18.867188 7.273438 20.757812 4.257812 20.859375 0.492188 c +20.859375 0.445312 20.835938 0.421875 20.804688 0.421875 c +20.773438 0.421875 20.757812 0.445312 20.734375 0.492188 c +19.796875 2.437500 17.570312 3.453125 14.078125 3.453125 c +13.125000 3.453125 l +12.945312 3.453125 12.867188 3.375000 12.867188 3.195312 c +12.867188 0.414062 l +12.867188 0.335938 12.820312 0.281250 12.750000 0.281250 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -1.031250 2.000000 cm +BT +16.000000 0.000000 0.000000 16.000000 -0.187500 -0.273438 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 2842 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 21.335938 14.726562 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Type /Catalog + /Pages 11 0 R + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000003939 00000 n +0000003962 00000 n +0000004137 00000 n +0000004213 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +4274 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/retoot.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/ToolBar/retoot.imageset/Contents.json new file mode 100644 index 000000000..04488ee0a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/retoot.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "retoot.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/retoot.imageset/retoot.pdf b/Mastodon/Resources/Assets.xcassets/ToolBar/retoot.imageset/retoot.pdf new file mode 100644 index 000000000..7cce18192 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/retoot.imageset/retoot.pdf @@ -0,0 +1,186 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.503418 0 0.119141 -0.109863 1.384277 1.042480 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.503418 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -1.967758 1.984375 cm +0.376471 0.411765 0.517647 scn +-0.031250 -0.226562 m +h +16.234375 12.789062 m +10.367188 12.789062 l +9.789062 12.789062 9.414062 12.437500 9.414062 11.898438 c +9.421875 11.351562 9.789062 11.000000 10.367188 11.000000 c +16.070312 11.000000 l +16.750000 11.000000 17.109375 10.664062 17.109375 9.953125 c +17.109375 2.109375 l +16.062500 3.281250 l +15.531250 3.804688 l +15.156250 4.179688 14.625000 4.195312 14.257812 3.820312 c +13.882812 3.445312 13.890625 2.914062 14.265625 2.539062 c +16.968750 -0.156250 l +17.625000 -0.804688 18.382812 -0.804688 19.039062 -0.156250 c +21.742188 2.539062 l +22.117188 2.914062 22.117188 3.445312 21.750000 3.820312 c +21.382812 4.195312 20.851562 4.179688 20.476562 3.804688 c +19.945312 3.281250 l +18.898438 2.117188 l +18.898438 10.148438 l +18.898438 11.867188 17.968750 12.789062 16.234375 12.789062 c +h +2.242188 6.984375 m +2.609375 6.617188 3.140625 6.625000 3.515625 7.000000 c +4.046875 7.523438 l +5.093750 8.687500 l +5.093750 0.664062 l +5.093750 -1.062500 6.023438 -1.984375 7.757812 -1.984375 c +13.625000 -1.984375 l +14.203125 -1.984375 14.578125 -1.625000 14.578125 -1.085938 c +14.570312 -0.546875 14.203125 -0.195312 13.625000 -0.195312 c +7.921875 -0.195312 l +7.242188 -0.195312 6.882812 0.148438 6.882812 0.859375 c +6.882812 8.695312 l +7.929688 7.523438 l +8.460938 7.000000 l +8.835938 6.632812 9.367188 6.609375 9.734375 6.984375 c +10.109375 7.359375 10.101562 7.890625 9.726562 8.265625 c +7.023438 10.960938 l +6.367188 11.617188 5.609375 11.609375 4.953125 10.960938 c +2.250000 8.265625 l +1.875000 7.890625 1.875000 7.359375 2.242188 6.984375 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -1.967758 1.984375 cm +BT +16.000000 0.000000 0.000000 16.000000 -0.031250 -0.226562 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 1839 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.056656 14.773438 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Type /Catalog + /Pages 11 0 R + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000002936 00000 n +0000002959 00000 n +0000003134 00000 n +0000003210 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +3271 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/star.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/ToolBar/star.imageset/Contents.json new file mode 100644 index 000000000..80fb0f24a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/star.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "star.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/ToolBar/star.imageset/star.pdf b/Mastodon/Resources/Assets.xcassets/ToolBar/star.imageset/star.pdf new file mode 100644 index 000000000..b95a8f64e --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/ToolBar/star.imageset/star.pdf @@ -0,0 +1,193 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.311523 0 0.092285 -0.149414 1.218750 1.164551 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.311523 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -3.102768 0.104736 cm +0.376471 0.411765 0.517647 scn +1.507812 2.156250 m +h +6.515625 0.078125 m +6.929688 -0.234375 7.429688 -0.132812 8.000000 0.281250 c +12.000000 3.218750 l +16.000000 0.281250 l +16.570312 -0.132812 17.070312 -0.234375 17.484375 0.078125 c +17.890625 0.382812 17.968750 0.890625 17.750000 1.546875 c +16.164062 6.242188 l +20.203125 9.140625 l +20.765625 9.539062 21.007812 10.000000 20.843750 10.484375 c +20.679688 10.968750 20.226562 11.203125 19.531250 11.195312 c +14.585938 11.156250 l +13.078125 15.882812 l +12.867188 16.554688 12.507812 16.921875 12.000000 16.921875 c +11.492188 16.921875 11.140625 16.554688 10.921875 15.882812 c +9.414062 11.156250 l +4.468750 11.195312 l +3.773438 11.203125 3.320312 10.968750 3.156250 10.492188 c +2.984375 10.000000 3.234375 9.539062 3.796875 9.140625 c +7.835938 6.242188 l +6.250000 1.546875 l +6.031250 0.890625 6.109375 0.382812 6.515625 0.078125 c +h +8.117188 2.281250 m +8.109375 2.296875 8.109375 2.304688 8.117188 2.343750 c +9.531250 6.281250 l +9.695312 6.726562 9.664062 6.968750 9.234375 7.250000 c +5.773438 9.601562 l +5.742188 9.617188 5.726562 9.632812 5.734375 9.656250 c +5.742188 9.671875 5.757812 9.671875 5.796875 9.671875 c +9.976562 9.554688 l +10.453125 9.539062 10.671875 9.664062 10.804688 10.132812 c +11.960938 14.148438 l +11.968750 14.187500 11.984375 14.203125 12.000000 14.203125 c +12.015625 14.203125 12.031250 14.187500 12.039062 14.148438 c +13.203125 10.132812 l +13.328125 9.664062 13.546875 9.539062 14.023438 9.554688 c +18.203125 9.671875 l +18.242188 9.671875 18.265625 9.671875 18.273438 9.656250 c +18.273438 9.632812 18.265625 9.625000 18.234375 9.601562 c +14.765625 7.242188 l +14.343750 6.960938 14.304688 6.726562 14.468750 6.281250 c +15.882812 2.343750 l +15.890625 2.304688 15.890625 2.296875 15.882812 2.281250 c +15.867188 2.257812 15.851562 2.273438 15.820312 2.289062 c +12.515625 4.859375 l +12.132812 5.164062 11.867188 5.164062 11.484375 4.859375 c +8.179688 2.289062 l +8.148438 2.273438 8.132812 2.257812 8.117188 2.281250 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -3.102768 0.104736 cm +BT +16.000000 0.000000 0.000000 16.000000 1.507812 2.156250 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 2242 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 17.791397 17.026611 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Type /Catalog + /Pages 11 0 R + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000003339 00000 n +0000003362 00000 n +0000003537 00000 n +0000003613 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +3674 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/Contents.json b/Mastodon/Resources/Assets.xcassets/TootTimeline/Contents.json new file mode 100644 index 000000000..6e965652d --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/Global.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/TootTimeline/Global.imageset/Contents.json new file mode 100644 index 000000000..cc2565e2a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/Global.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "globe-americas.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/Global.imageset/globe-americas.pdf b/Mastodon/Resources/Assets.xcassets/TootTimeline/Global.imageset/globe-americas.pdf new file mode 100644 index 000000000..623ec9691 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/Global.imageset/globe-americas.pdf @@ -0,0 +1,140 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.333252 1.333252 cm +0.376471 0.411765 0.517647 scn +6.666667 13.333374 m +2.984678 13.333374 0.000000 10.348697 0.000000 6.666707 c +0.000000 2.984718 2.984678 0.000040 6.666667 0.000040 c +10.348656 0.000040 13.333334 2.984718 13.333334 6.666707 c +13.333334 10.348697 10.348656 13.333374 6.666667 13.333374 c +h +8.878764 3.720470 m +8.773925 3.616169 8.663979 3.506761 8.574732 3.417245 c +8.494355 3.336599 8.437634 3.237138 8.408871 3.129342 c +8.368279 2.977191 8.335485 2.823428 8.280645 2.675847 c +7.813172 1.416438 l +7.443280 1.335793 7.060484 1.290363 6.666667 1.290363 c +6.666667 2.026384 l +6.712097 2.365632 6.461290 3.001116 6.058333 3.404073 c +5.897043 3.565363 5.806452 3.784181 5.806452 4.012406 c +5.806452 4.872890 l +5.806452 5.185793 5.637903 5.473427 5.363978 5.624772 c +4.977688 5.838481 4.428226 6.137137 4.051882 6.326653 c +3.743279 6.482030 3.457796 6.679880 3.201075 6.911331 c +3.179570 6.930686 l +2.995985 7.096402 2.832988 7.283587 2.694086 7.488213 c +2.441936 7.858374 2.031183 8.467245 1.764247 8.862944 c +2.314516 10.086061 3.306183 11.068320 4.538441 11.601922 c +5.183871 11.279073 l +5.469893 11.136063 5.806452 11.343858 5.806452 11.663751 c +5.806452 11.967514 l +6.021236 12.002192 6.239785 12.024234 6.462097 12.032568 c +7.222850 11.271814 l +7.390861 11.103804 7.390861 10.831492 7.222850 10.663482 c +7.096774 10.537675 l +6.818818 10.259718 l +6.734946 10.175847 6.734946 10.039557 6.818818 9.955686 c +6.944893 9.829611 l +7.028764 9.745740 7.028764 9.609449 6.944893 9.525578 c +6.729839 9.310524 l +6.689461 9.270225 6.634737 9.247601 6.577688 9.247622 c +6.336021 9.247622 l +6.280107 9.247622 6.226344 9.225847 6.186021 9.186600 c +5.919355 8.927191 l +5.886667 8.895360 5.864938 8.853966 5.857304 8.808983 c +5.849670 8.764001 5.856525 8.717755 5.876882 8.676922 c +6.295968 7.838481 l +6.367474 7.695471 6.263441 7.527191 6.103764 7.527191 c +5.952151 7.527191 l +5.900269 7.527191 5.850269 7.546009 5.811290 7.579879 c +5.561828 7.796546 l +5.505376 7.845519 5.437150 7.878955 5.363858 7.893567 c +5.290566 7.908178 5.214734 7.903461 5.143817 7.879879 c +4.305914 7.600578 l +4.241943 7.579248 4.186307 7.538327 4.146889 7.483615 c +4.107471 7.428902 4.086270 7.363173 4.086290 7.295739 c +4.086290 7.173965 4.155107 7.062944 4.263978 7.008374 c +4.561828 6.859449 l +4.814785 6.732836 5.093817 6.666976 5.376613 6.666976 c +5.659409 6.666976 5.983871 5.933374 6.236828 5.806761 c +8.031183 5.806761 l +8.259409 5.806761 8.477958 5.716170 8.639517 5.554880 c +9.007526 5.186869 l +9.161268 5.033069 9.247619 4.824495 9.247581 4.607030 c +9.247526 4.442246 9.214915 4.279098 9.151622 4.126954 c +9.088329 3.974811 8.995601 3.836672 8.878764 3.720470 c +8.878764 3.720470 l +h +11.209678 6.176116 m +11.054032 6.215095 10.918280 6.310524 10.829302 6.444127 c +10.345968 7.169127 l +10.275246 7.275051 10.237501 7.399558 10.237501 7.526922 c +10.237501 7.654286 10.275246 7.778794 10.345968 7.884718 c +10.872581 8.674503 l +10.934946 8.767782 11.020431 8.843589 11.120968 8.893589 c +11.469893 9.068051 l +11.833333 8.344396 12.043011 7.530417 12.043011 6.666707 c +12.043011 6.433643 12.023118 6.205417 11.994086 5.980148 c +11.209678 6.176116 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 3208 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003298 00000 n +0000003321 00000 n +0000003494 00000 n +0000003568 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3627 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/Textlock.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/TootTimeline/Textlock.imageset/Contents.json new file mode 100644 index 000000000..05f0e9c97 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/Textlock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Textlock.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/Textlock.imageset/Textlock.pdf b/Mastodon/Resources/Assets.xcassets/TootTimeline/Textlock.imageset/Textlock.pdf new file mode 100644 index 000000000..0aba1b650 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/Textlock.imageset/Textlock.pdf @@ -0,0 +1,174 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.093750 0 0.197266 -0.131348 0.896484 1.184082 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.093750 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -3.755859 1.167969 cm +0.376471 0.411765 0.517647 scn +0.796875 0.802246 m +h +5.491699 -1.167969 m +12.508301 -1.167969 l +13.672852 -1.167969 14.244141 -0.589355 14.244141 0.670410 c +14.244141 6.009766 l +14.244141 7.123047 13.782715 7.708984 12.859863 7.818848 c +12.859863 9.474121 l +12.859863 12.271973 10.970215 13.634277 9.000000 13.634277 c +7.029785 13.634277 5.140137 12.271973 5.140137 9.474121 c +5.140137 7.818848 l +4.209961 7.708984 3.755859 7.123047 3.755859 6.009766 c +3.755859 0.670410 l +3.755859 -0.589355 4.327148 -1.167969 5.491699 -1.167969 c +h +6.722168 9.598633 m +6.722168 11.202637 7.740234 12.103516 9.000000 12.103516 c +10.259766 12.103516 11.277832 11.202637 11.277832 9.598633 c +11.277832 7.840820 l +6.722168 7.840820 l +6.722168 9.598633 l +h +5.865234 0.333496 m +5.557617 0.333496 5.403809 0.479980 5.403809 0.853516 c +5.403809 5.826660 l +5.403809 6.200195 5.557617 6.332031 5.865234 6.332031 c +12.134766 6.332031 l +12.449707 6.332031 12.596191 6.200195 12.596191 5.826660 c +12.596191 0.853516 l +12.596191 0.479980 12.449707 0.333496 12.134766 0.333496 c +5.865234 0.333496 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -3.755859 1.167969 cm +BT +15.000000 0.000000 0.000000 15.000000 0.796875 0.802246 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 1324 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 10.488281 14.802246 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Type /Catalog + /Pages 11 0 R + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000002421 00000 n +0000002444 00000 n +0000002619 00000 n +0000002695 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +2756 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/email.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/TootTimeline/email.imageset/Contents.json new file mode 100644 index 000000000..0604f1eff --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/email.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_email.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/email.imageset/icon_email.pdf b/Mastodon/Resources/Assets.xcassets/TootTimeline/email.imageset/icon_email.pdf new file mode 100644 index 000000000..4114c2dcc --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/email.imageset/icon_email.pdf @@ -0,0 +1,83 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.333252 2.666626 cm +0.376471 0.411765 0.517647 scn +12.000000 10.666687 m +1.333333 10.666687 l +0.600000 10.666687 0.006667 10.066687 0.006667 9.333354 c +0.000000 1.333354 l +0.000000 0.600021 0.600000 0.000021 1.333333 0.000021 c +12.000000 0.000021 l +12.733334 0.000021 13.333334 0.600021 13.333334 1.333354 c +13.333334 9.333354 l +13.333334 10.066687 12.733334 10.666687 12.000000 10.666687 c +h +12.000000 8.000021 m +6.666667 4.666687 l +1.333333 8.000021 l +1.333333 9.333354 l +6.666667 6.000021 l +12.000000 9.333354 l +12.000000 8.000021 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 612 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000702 00000 n +0000000724 00000 n +0000000897 00000 n +0000000971 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1030 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/lock.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/TootTimeline/lock.imageset/Contents.json new file mode 100644 index 000000000..c83be324a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/lock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Iconlock.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/lock.imageset/Iconlock.pdf b/Mastodon/Resources/Assets.xcassets/TootTimeline/lock.imageset/Iconlock.pdf new file mode 100644 index 000000000..235e9242a --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/lock.imageset/Iconlock.pdf @@ -0,0 +1,87 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.000000 1.333252 cm +0.376471 0.411765 0.517647 scn +10.119047 7.500041 m +9.511904 7.500041 l +9.511904 9.375040 l +9.511904 11.557332 7.786607 13.333374 5.666667 13.333374 c +3.546726 13.333374 1.821428 11.557332 1.821428 9.375040 c +1.821428 7.500041 l +1.214286 7.500041 l +0.543899 7.500041 0.000000 6.940145 0.000000 6.250041 c +0.000000 1.250040 l +0.000000 0.559936 0.543899 0.000040 1.214286 0.000040 c +10.119047 0.000040 l +10.789433 0.000040 11.333333 0.559936 11.333333 1.250040 c +11.333333 6.250041 l +11.333333 6.940145 10.789433 7.500041 10.119047 7.500041 c +h +7.488095 7.500041 m +3.845238 7.500041 l +3.845238 9.375040 l +3.845238 10.408895 4.662351 11.250040 5.666667 11.250040 c +6.670982 11.250040 7.488095 10.408895 7.488095 9.375040 c +7.488095 7.500041 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 836 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000926 00000 n +0000000948 00000 n +0000001121 00000 n +0000001195 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1254 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/unlock.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/TootTimeline/unlock.imageset/Contents.json new file mode 100644 index 000000000..372e28767 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/unlock.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Iconunlock.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/TootTimeline/unlock.imageset/Iconunlock.pdf b/Mastodon/Resources/Assets.xcassets/TootTimeline/unlock.imageset/Iconunlock.pdf new file mode 100644 index 000000000..09d2143d5 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/TootTimeline/unlock.imageset/Iconunlock.pdf @@ -0,0 +1,87 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.000000 1.325439 cm +0.376471 0.411765 0.517647 scn +10.416220 6.674232 m +3.958164 6.674232 l +3.958164 9.359012 l +3.958164 10.390217 4.783649 11.246952 5.814855 11.257368 c +6.856477 11.267784 7.708003 10.421466 7.708003 9.382448 c +7.708003 8.965799 l +7.708003 8.619460 7.986637 8.340826 8.332976 8.340826 c +9.166274 8.340826 l +9.512613 8.340826 9.791247 8.619460 9.791247 8.965799 c +9.791247 9.382448 l +9.791247 11.569854 8.007469 13.348424 5.820063 13.340611 c +3.632657 13.332799 1.874920 11.530793 1.874920 9.343388 c +1.874920 6.674232 l +1.249946 6.674232 l +0.559872 6.674232 0.000000 6.114359 0.000000 5.424285 c +0.000000 1.257797 l +0.000000 0.567722 0.559872 0.007851 1.249946 0.007851 c +10.416220 0.007851 l +11.106295 0.007851 11.666166 0.567722 11.666166 1.257797 c +11.666166 5.424285 l +11.666166 6.114359 11.106295 6.674232 10.416220 6.674232 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 926 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 15.999268 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001016 00000 n +0000001038 00000 n +0000001211 00000 n +0000001285 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1344 +%%EOF \ No newline at end of file diff --git a/Mastodon/Scene/HomeViewController.swift b/Mastodon/Scene/HomeViewController.swift index 0d0f8cc96..0fbcfd0e6 100644 --- a/Mastodon/Scene/HomeViewController.swift +++ b/Mastodon/Scene/HomeViewController.swift @@ -21,6 +21,6 @@ extension HomeViewController { title = "Home" view.backgroundColor = .systemBackground + } - } diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index f15db318b..2d6c43136 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -19,17 +19,19 @@ class MainTabBarController: UITabBarController { enum Tab: Int, CaseIterable { case home - + case publicTimeline var title: String { switch self { case .home: return "Home" + case .publicTimeline : return "Public" } } var image: UIImage { switch self { case .home: return UIImage(systemName: "house")! + case .publicTimeline: return UIImage(systemName: "flame")! } } @@ -41,6 +43,12 @@ class MainTabBarController: UITabBarController { _viewController.context = context _viewController.coordinator = coordinator viewController = _viewController + case .publicTimeline: + let _viewController = PublicTimelineViewController() + _viewController.viewModel = PublicTimelineViewModel(context: context) + _viewController.context = context + _viewController.coordinator = coordinator + viewController = _viewController } viewController.title = self.title return UINavigationController(rootViewController: viewController) diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift new file mode 100644 index 000000000..d27531cc7 --- /dev/null +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift @@ -0,0 +1,49 @@ +// +// PublicTimelineViewController+StatusProvider.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import MastodonSDK + +// MARK: - StatusProvider +extension PublicTimelineViewController { + + func toot() -> Future { + return Future { promise in promise(.success(nil)) } + } + + func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future { + 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 + } + + switch item { + case .toot(let objectID): + let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext + managedObjectContext.perform { + let toot = managedObjectContext.object(with: objectID) as? Toot + promise(.success(toot)) + } + } + } + } + + func toot(for cell: UICollectionViewCell) -> Future { + return Future { promise in promise(.success(nil)) } + } + +} diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift new file mode 100644 index 000000000..ce3bcd298 --- /dev/null +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift @@ -0,0 +1,110 @@ +// +// PublicTimelineViewController.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import UIKit +import AVKit +import Combine +import CoreDataStack +import GameplayKit + +final class PublicTimelineViewController: UIViewController, NeedsDependency, TimelinePostTableViewCellDelegate { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + var viewModel: PublicTimelineViewModel! + + + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self)) + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + return tableView + }() + + deinit { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension PublicTimelineViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = Asset.Colors.tootDark.color + view.addSubview(tableView) + view.backgroundColor = Asset.Colors.tootDark.color + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + viewModel.tableView = tableView + tableView.delegate = self + viewModel.setupDiffableDataSource( + for: tableView, + dependency: self, + timelinePostTableViewCellDelegate: self + ) + } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewModel.fetchLatest() + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + os_log("%{public}s[%{public}ld], %{public}s: fetch user timeline latest response error: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + break + } + } receiveValue: { response in + let tootsIDs = response.value.map { $0.id } + self.viewModel.tootIDs.value = tootsIDs + } + .store(in: &viewModel.disposeBag) + } + +} + +// MARK: - UITableViewDelegate +extension PublicTimelineViewController: 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 + } + + return ceil(frame.height) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + } + + func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + + let key = item.hashValue + let frame = cell.frame + viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) + } +} diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift new file mode 100644 index 000000000..c2ee8e5db --- /dev/null +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift @@ -0,0 +1,53 @@ +// +// PublicTimelineViewModel+Diffable.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import UIKit +import CoreData +import CoreDataStack + +extension PublicTimelineViewModel { + func setupDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency, + timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate + ) { + let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() + + diffableDataSource = TimelineSection.tableViewDiffableDataSource( + for: tableView, + dependency: dependency, + managedObjectContext: fetchedResultsController.managedObjectContext, + timestampUpdatePublisher: timestampUpdatePublisher, + timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate) + items.value = [] + } +} + +// MARK: - NSFetchedResultsControllerDelegate +extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate { + func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + let indexes = tootIDs.value + let toots = fetchedResultsController.fetchedObjects ?? [] + guard toots.count == indexes.count else { return } + + let items: [Item] = toots + .compactMap { toot -> (Int, Toot)? in + guard toot.deletedAt == nil else { return nil } + return indexes.firstIndex(of: toot.id).map { index in (index, toot) } + } + .sorted { $0.0 < $1.0 } + .map { Item.toot(objectID: $0.1.objectID) } + self.items.value = items + } + +} diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift new file mode 100644 index 000000000..2150bb25b --- /dev/null +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift @@ -0,0 +1,95 @@ +// +// PublicTimelineViewModel.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import UIKit +import GameplayKit +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import AlamofireImage + + +class PublicTimelineViewModel: NSObject { + + var disposeBag = Set() + + // input + let context: AppContext + let fetchedResultsController: NSFetchedResultsController + weak var tableView: UITableView? + + // output + var diffableDataSource: UITableViewDiffableDataSource? + + let tootIDs = CurrentValueSubject<[String], Never>([]) + let items = CurrentValueSubject<[Item], Never>([]) + var cellFrameCache = NSCache() + + init(context: AppContext) { + self.context = context + self.fetchedResultsController = { + let fetchRequest = Toot.sortedFetchRequest + fetchRequest.predicate = Toot.predicate(idStrs: []) + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 20 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: context.managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + + return controller + }() + super.init() + + self.fetchedResultsController.delegate = self + + items + .receive(on: DispatchQueue.main) + .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) + .sink { [weak self] items in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + os_log("%{public}s[%{public}ld], %{public}s: items did change", ((#file as NSString).lastPathComponent), #line, #function) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(items) + + diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty) + } + .store(in: &disposeBag) + + tootIDs + .receive(on: DispatchQueue.main) + .sink { [weak self] ids in + guard let self = self else { return } + self.fetchedResultsController.fetchRequest.predicate = Toot.predicate(idStrs: ids) + do { + try self.fetchedResultsController.performFetch() + } catch { + assertionFailure(error.localizedDescription) + } + } + .store(in: &disposeBag) + } + + deinit { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension PublicTimelineViewModel { + + func fetchLatest() -> AnyPublisher, Error> { + return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp") + } +} diff --git a/Mastodon/Scene/Share/View/Button/HitTestExpandedButton.swift b/Mastodon/Scene/Share/View/Button/HitTestExpandedButton.swift new file mode 100644 index 000000000..f56e7e7ee --- /dev/null +++ b/Mastodon/Scene/Share/View/Button/HitTestExpandedButton.swift @@ -0,0 +1,18 @@ +// +// HitTestExpandedButton.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/1. +// + +import UIKit + +final class HitTestExpandedButton: UIButton { + + var expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return bounds.inset(by: expandEdgeInsets).contains(point) + } + +} diff --git a/Mastodon/Scene/Share/View/Content/TimelinePostView.swift b/Mastodon/Scene/Share/View/Content/TimelinePostView.swift new file mode 100644 index 000000000..e75371a40 --- /dev/null +++ b/Mastodon/Scene/Share/View/Content/TimelinePostView.swift @@ -0,0 +1,163 @@ +// +// TimelinePostView.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/28. +// + +import UIKit +import AVKit +import ActiveLabel + +final class TimelinePostView: UIView { + + static let avatarImageViewSize = CGSize(width: 44, height: 44) + + let avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.layer.masksToBounds = true + imageView.layer.cornerRadius = avatarImageViewSize.width/2 + imageView.layer.cornerCurve = .continuous + imageView.contentMode = .scaleAspectFill + return imageView + }() + + let visibilityImageView: UIImageView = { + let imageView = UIImageView(image: Asset.TootTimeline.global.image.withRenderingMode(.alwaysTemplate)) + imageView.tintColor = Asset.Colors.tootGray.color + return imageView + }() + + let lockImageView: UIImageView = { + let imageview = UIImageView(image: Asset.TootTimeline.textlock.image.withRenderingMode(.alwaysTemplate)) + imageview.tintColor = Asset.Colors.tootGray.color + imageview.isHidden = true + return imageview + }() + + let nameLabel: UILabel = { + let label = UILabel() + label.font = UIFont(name: "Roboto-Medium", size: 14) + label.textColor = Asset.Colors.tootWhite.color + + label.text = "Alice" + return label + }() + + let usernameLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.tootGray.color + label.font = UIFont(name: "Roboto-Regular", size: 14) + label.text = "@alice" + return label + }() + + let dateLabel: UILabel = { + let label = UILabel() + label.font = UIFont(name: "Roboto-Regular", size: 14) + label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? .left : .right + label.textColor = Asset.Colors.tootGray.color + label.text = "1d" + return label + }() + + let actionToolbarContainer: ActionToolbarContainer = { + let actionToolbarContainer = ActionToolbarContainer() + actionToolbarContainer.configure(for: .inline) + return actionToolbarContainer + }() + + let mainContainerStackView = UIStackView() + + let activeTextLabel = ActiveLabel(style: .default) + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension TimelinePostView { + + func _init() { + // container: [retoot | post] + let containerStackView = UIStackView() + containerStackView.axis = .vertical + containerStackView.spacing = 8 + //containerStackView.alignment = .top + containerStackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor), + bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor), + ]) + + // post container: [user avatar | toot container] + let postContainerStackView = UIStackView() + containerStackView.addArrangedSubview(postContainerStackView) + postContainerStackView.axis = .horizontal + postContainerStackView.spacing = 10 + postContainerStackView.alignment = .top + + // user avatar + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + postContainerStackView.addArrangedSubview(avatarImageView) + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: TimelinePostView.avatarImageViewSize.width).priority(.required - 1), + avatarImageView.heightAnchor.constraint(equalToConstant: TimelinePostView.avatarImageViewSize.height).priority(.required - 1), + ]) + + // toot container: [user meta container | main container | action toolbar] + let tootContainerStackView = UIStackView() + postContainerStackView.addArrangedSubview(tootContainerStackView) + tootContainerStackView.axis = .vertical + tootContainerStackView.spacing = 2 + + // user meta container: [name | lock | username | visiablity | date ] + let userMetaContainerStackView = UIStackView() + tootContainerStackView.addArrangedSubview(userMetaContainerStackView) + userMetaContainerStackView.axis = .horizontal + userMetaContainerStackView.alignment = .center + userMetaContainerStackView.spacing = 6 + userMetaContainerStackView.addArrangedSubview(nameLabel) + userMetaContainerStackView.addArrangedSubview(lockImageView) + userMetaContainerStackView.addArrangedSubview(usernameLabel) + userMetaContainerStackView.addArrangedSubview(visibilityImageView) + userMetaContainerStackView.addArrangedSubview(dateLabel) + nameLabel.setContentHuggingPriority(.defaultHigh + 10, for: .horizontal) + nameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + lockImageView.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal) + lockImageView.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal) + usernameLabel.setContentHuggingPriority(.defaultHigh - 3, for: .horizontal) + usernameLabel.setContentCompressionResistancePriority(.defaultHigh - 1, for: .horizontal) + visibilityImageView.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal) + visibilityImageView.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal) + dateLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) + dateLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal) + + // main container: [text | image / video | quote | geo] + tootContainerStackView.addArrangedSubview(mainContainerStackView) + mainContainerStackView.axis = .vertical + mainContainerStackView.spacing = 8 + activeTextLabel.translatesAutoresizingMaskIntoConstraints = false + mainContainerStackView.addArrangedSubview(activeTextLabel) + + activeTextLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical) + + // action toolbar + actionToolbarContainer.translatesAutoresizingMaskIntoConstraints = false + tootContainerStackView.addArrangedSubview(actionToolbarContainer) + actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + + } + +} + diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelinePostTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelinePostTableViewCell.swift new file mode 100644 index 000000000..487ba68c6 --- /dev/null +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelinePostTableViewCell.swift @@ -0,0 +1,66 @@ +// +// TimelinePostTableViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import UIKit +import AVKit +import Combine + + +protocol TimelinePostTableViewCellDelegate: class { + +} + +final class TimelinePostTableViewCell: UITableViewCell { + + static let verticalMargin: CGFloat = 16 // without retoot indicator + static let verticalMarginAlt: CGFloat = 8 // with retoot indicator + + weak var delegate: TimelinePostTableViewCellDelegate? + + var disposeBag = Set() + var observations = Set() + + let timelinePostView = TimelinePostView() + + var timelinePostViewTopLayoutConstraint: NSLayoutConstraint! + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag.removeAll() + observations.removeAll() + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension TimelinePostTableViewCell { + + private func _init() { + self.backgroundColor = Asset.Colors.tootDark.color + self.selectionStyle = .none + timelinePostView.translatesAutoresizingMaskIntoConstraints = false + timelinePostViewTopLayoutConstraint = timelinePostView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TimelinePostTableViewCell.verticalMargin) + contentView.addSubview(timelinePostView) + NSLayoutConstraint.activate([ + timelinePostViewTopLayoutConstraint, + timelinePostView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + contentView.readableContentGuide.trailingAnchor.constraint(equalTo: timelinePostView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: timelinePostView.bottomAnchor), // use action toolbar margin + ]) + } + +} diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift new file mode 100644 index 000000000..4403e4b83 --- /dev/null +++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift @@ -0,0 +1,201 @@ +// +// ActionToolBarContainer.swift +// Mastodon +// +// Created by sxiaojian on 2021/2/1. +// + +import os.log +import UIKit + +protocol ActionToolbarContainerDelegate: class { + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, replayButtonDidPressed sender: UIButton) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, retootButtonDidPressed sender: UIButton) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, starButtonDidPressed sender: UIButton) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, bookmarkButtonDidPressed sender: UIButton) + func actionToolbarContainer(_ actionToolbarContainer: ActionToolbarContainer, moreButtonDidPressed sender: UIButton) + +} + + +final class ActionToolbarContainer: UIView { + + let replyButton = HitTestExpandedButton() + let retootButton = HitTestExpandedButton() + let starButton = HitTestExpandedButton() + let bookmartButton = HitTestExpandedButton() + let moreButton = HitTestExpandedButton() + + var isstarButtonHighlight: Bool = false { + didSet { isstarButtonHighlightStateDidChange(to: isstarButtonHighlight) } + } + + weak var delegate: ActionToolbarContainerDelegate? + + private let container = UIStackView() + private var style: Style? + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ActionToolbarContainer { + + private func _init() { + container.translatesAutoresizingMaskIntoConstraints = false + addSubview(container) + NSLayoutConstraint.activate([ + container.topAnchor.constraint(equalTo: topAnchor), + container.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingAnchor.constraint(equalTo: container.trailingAnchor), + bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + + replyButton.addTarget(self, action: #selector(ActionToolbarContainer.replyButtonDidPressed(_:)), for: .touchUpInside) + retootButton.addTarget(self, action: #selector(ActionToolbarContainer.retootButtonDidPressed(_:)), for: .touchUpInside) + starButton.addTarget(self, action: #selector(ActionToolbarContainer.starButtonDidPressed(_:)), for: .touchUpInside) + bookmartButton.addTarget(self, action: #selector(ActionToolbarContainer.bookmarkButtonDidPressed(_:)), for: .touchUpInside) + moreButton.addTarget(self, action: #selector(ActionToolbarContainer.moreButtonDidPressed(_:)), for: .touchUpInside) + } + +} + +extension ActionToolbarContainer { + + enum Style { + case inline + case plain + + var buttonTitleImagePadding: CGFloat { + switch self { + case .inline: return 4.0 + case .plain: return 0 + } + } + } + + func configure(for style: Style) { + guard needsConfigure(for: style) else { + return + } + + self.style = style + container.arrangedSubviews.forEach { subview in + container.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + + let buttons = [replyButton, retootButton, starButton,bookmartButton, moreButton] + buttons.forEach { button in + button.tintColor = Asset.Colors.tootGray.color + button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) + button.setTitle("", for: .normal) + button.setTitleColor(.secondaryLabel, for: .normal) + button.setInsets(forContentPadding: .zero, imageTitlePadding: style.buttonTitleImagePadding) + } + + switch style { + case .inline: + buttons.forEach { button in + button.contentHorizontalAlignment = .leading + } + replyButton.setImage(Asset.ToolBar.reply.image.withRenderingMode(.alwaysTemplate), for: .normal) + retootButton.setImage(Asset.ToolBar.retoot.image.withRenderingMode(.alwaysTemplate), for: .normal) + starButton.setImage(Asset.ToolBar.star.image.withRenderingMode(.alwaysTemplate), for: .normal) + bookmartButton.setImage(Asset.ToolBar.bookmark.image.withRenderingMode(.alwaysTemplate), for: .normal) + moreButton.setImage(Asset.ToolBar.more.image.withRenderingMode(.alwaysTemplate), for: .normal) + + container.axis = .horizontal + container.distribution = .fill + + replyButton.translatesAutoresizingMaskIntoConstraints = false + retootButton.translatesAutoresizingMaskIntoConstraints = false + starButton.translatesAutoresizingMaskIntoConstraints = false + bookmartButton.translatesAutoresizingMaskIntoConstraints = false + moreButton.translatesAutoresizingMaskIntoConstraints = false + container.addArrangedSubview(replyButton) + container.addArrangedSubview(retootButton) + container.addArrangedSubview(starButton) + container.addArrangedSubview(bookmartButton) + container.addArrangedSubview(moreButton) + NSLayoutConstraint.activate([ + replyButton.heightAnchor.constraint(equalToConstant: 40).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: retootButton.heightAnchor).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: starButton.heightAnchor).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: moreButton.heightAnchor).priority(.defaultHigh), + replyButton.heightAnchor.constraint(equalTo: bookmartButton.heightAnchor).priority(.defaultHigh), + replyButton.widthAnchor.constraint(equalTo: retootButton.widthAnchor).priority(.defaultHigh), + replyButton.widthAnchor.constraint(equalTo: starButton.widthAnchor).priority(.defaultHigh), + replyButton.widthAnchor.constraint(equalTo: bookmartButton.widthAnchor).priority(.defaultHigh), + ]) + moreButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) + moreButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + case .plain: + buttons.forEach { button in + button.contentHorizontalAlignment = .center + } + replyButton.setImage(Asset.ToolBar.reply.image.withRenderingMode(.alwaysTemplate), for: .normal) + retootButton.setImage(Asset.ToolBar.retoot.image.withRenderingMode(.alwaysTemplate), for: .normal) + starButton.setImage(Asset.ToolBar.bookmark.image.withRenderingMode(.alwaysTemplate), for: .normal) + bookmartButton.setImage(Asset.ToolBar.bookmark.image.withRenderingMode(.alwaysTemplate), for: .normal) + + container.axis = .horizontal + container.spacing = 8 + container.distribution = .fillEqually + + container.addArrangedSubview(replyButton) + container.addArrangedSubview(retootButton) + container.addArrangedSubview(starButton) + container.addArrangedSubview(bookmartButton) + } + } + + private func needsConfigure(for style: Style) -> Bool { + guard let oldStyle = self.style else { return true } + return oldStyle != style + } + + private func isstarButtonHighlightStateDidChange(to isHighlight: Bool) { + let tintColor = isHighlight ? Asset.Colors.likeOrange.color : Asset.Colors.tootGray.color + starButton.tintColor = tintColor + starButton.setTitleColor(tintColor, for: .normal) + starButton.setTitleColor(tintColor, for: .highlighted) + } +} + +extension ActionToolbarContainer { + + @objc private func replyButtonDidPressed(_ sender: UIButton) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.actionToolbarContainer(self, replayButtonDidPressed: sender) + } + + @objc private func retootButtonDidPressed(_ sender: UIButton) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.actionToolbarContainer(self, retootButtonDidPressed: sender) + } + + @objc private func starButtonDidPressed(_ sender: UIButton) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.actionToolbarContainer(self, starButtonDidPressed: sender) + } + + @objc private func moreButtonDidPressed(_ sender: UIButton) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.actionToolbarContainer(self, moreButtonDidPressed: sender) + } + @objc private func bookmarkButtonDidPressed(_ sender: UIButton) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.actionToolbarContainer(self, bookmarkButtonDidPressed: sender) + } + +} diff --git a/Mastodon/Service/APIService+PublicTimeline.swift b/Mastodon/Service/APIService+PublicTimeline.swift new file mode 100644 index 000000000..6b73409cf --- /dev/null +++ b/Mastodon/Service/APIService+PublicTimeline.swift @@ -0,0 +1,49 @@ +// +// APIService+PublicTimeline.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/28. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import DateToolsSwift +import MastodonSDK + +extension APIService { + + static let publicTimelineRequestWindowInSec: TimeInterval = 15 * 60 + + func publicTimeline( + count: Int = 20, + domain: String + ) -> AnyPublisher, Error> { + + return Mastodon.API.Timeline.public( + session: session, + domain: domain, + query: Mastodon.API.Timeline.PublicTimelineQuery() + ) + .flatMap { response -> AnyPublisher,Error> in + return APIService.Persist.persistTimeline( + domain: domain, + managedObjectContext: self.backgroundManagedObjectContext, + response: response, + persistType: Persist.PersistTimelineType.publicHomeTimeline + ) + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +} diff --git a/Mastodon/Service/APIService.swift b/Mastodon/Service/APIService.swift new file mode 100644 index 000000000..e79fb1176 --- /dev/null +++ b/Mastodon/Service/APIService.swift @@ -0,0 +1,47 @@ +// +// APIService.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import AlamofireImage +import AlamofireNetworkActivityIndicator + +final class APIService { + + var disposeBag = Set() + + // internal + let session: URLSession + + + // input + let backgroundManagedObjectContext: NSManagedObjectContext + + + init(backgroundManagedObjectContext: NSManagedObjectContext) { + self.backgroundManagedObjectContext = backgroundManagedObjectContext + self.session = URLSession(configuration: .default) + + // setup cache. 10MB RAM + 50MB Disk + URLCache.shared = URLCache(memoryCapacity: 10 * 1024 * 1024, diskCapacity: 50 * 1024 * 1024, diskPath: nil) + + // enable network activity manager for AlamofireImage + NetworkActivityIndicatorManager.shared.isEnabled = true + NetworkActivityIndicatorManager.shared.startDelay = 0.2 + NetworkActivityIndicatorManager.shared.completionDelay = 0.5 + } + +} + +extension APIService { + public enum Persist { } + public enum CoreData { } +} diff --git a/Mastodon/Service/Persist/APIService+Persist+Timeline.swift b/Mastodon/Service/Persist/APIService+Persist+Timeline.swift new file mode 100644 index 000000000..2fc645a88 --- /dev/null +++ b/Mastodon/Service/Persist/APIService+Persist+Timeline.swift @@ -0,0 +1,77 @@ +// +// APIService+Persist+Timeline.swift +// Mastodon +// +// Created by sxiaojian on 2021/1/27. +// + +import os.log +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +extension APIService.Persist { + enum PersistTimelineType { + case publicHomeTimeline + } + static func persistTimeline( + domain: String, + managedObjectContext: NSManagedObjectContext, + response: Mastodon.Response.Content<[Mastodon.Entity.Toot]>, + persistType: PersistTimelineType + ) -> AnyPublisher, Never> { + return managedObjectContext.performChanges { + let toots = response.value + let _ = toots.map { + let userProperty = MastodonUser.Property(id: $0.account.id, domain: domain, acct: $0.account.acct, username: $0.account.username, displayName: $0.account.displayName,avatar: $0.account.avatar,avatarStatic: $0.account.avatarStatic, createdAt: $0.createdAt, networkDate: $0.createdAt) + let author = MastodonUser.insert(into: managedObjectContext, property: userProperty) + let metions = $0.mentions?.compactMap({ (mention) -> Mention in + Mention.insert(into: managedObjectContext, property: Mention.Property(id: mention.id, username: mention.username, acct: mention.acct, url: mention.url)) + }) + let emojis = $0.emojis?.compactMap({ (emoji) -> Emoji in + Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category)) + }) + + let tags = $0.tags?.compactMap({ (tag) -> Tag in + let histories = tag.history?.compactMap({ (history) -> History in + History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts)) + }) + return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories)) + }) + let tootProperty = Toot.Property( + domain: domain, + id: $0.id, + uri: $0.uri, + createdAt: $0.createdAt, + content: $0.content, + visibility: $0.visibility?.rawValue, + sensitive: $0.sensitive ?? false, + spoilerText: $0.spoilerText, + mentions: metions, + emojis: emojis, + tags: tags, + reblogsCount: NSNumber(value: $0.reblogsCount), + favouritesCount: NSNumber(value: $0.favouritesCount), + repliesCount: ($0.repliesCount != nil) ? NSNumber(value: $0.repliesCount!) : nil, + url: $0.uri, + inReplyToID: $0.inReplyToID, + inReplyToAccountID: $0.inReplyToAccountID, + reblog: nil, //TODO need fix + language: $0.language, + text: $0.text, + favouritedBy: ($0.favourited ?? false) ? author : nil, + rebloggedBy: ($0.reblogged ?? false) ? author : nil, + mutedBy: ($0.muted ?? false) ? author : nil, + bookmarkedBy: ($0.bookmarked ?? false) ? author : nil, + pinnedBy: ($0.pinned ?? false) ? author : nil, + updatedAt: response.networkDate, + deletedAt: nil, + author: author, + homeTimelineIndexes: nil) + Toot.insert(into: managedObjectContext, property: tootProperty, author: author) + } + }.eraseToAnyPublisher() + } +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 0742597c4..19cb4757d 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -21,6 +21,7 @@ class AppContext: ObservableObject { let managedObjectContext: NSManagedObjectContext let backgroundManagedObjectContext: NSManagedObjectContext + let apiService: APIService let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable! @@ -35,6 +36,10 @@ class AppContext: ObservableObject { managedObjectContext = _managedObjectContext backgroundManagedObjectContext = _backgroundManagedObjectContext + let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext) + apiService = _apiService + + documentStore = DocumentStore() documentStoreSubscription = documentStore.objectWillChange .receive(on: DispatchQueue.main) diff --git a/Podfile b/Podfile index 32659fa54..e9aaee930 100644 --- a/Podfile +++ b/Podfile @@ -9,7 +9,7 @@ target 'Mastodon' do # misc pod 'SwiftGen', '~> 6.4.0' pod 'DateToolsSwift', '~> 5.0.0' - + pod 'Kanna', '~> 5.2.2' target 'MastodonTests' do inherit! :search_paths # Pods for testing diff --git a/Podfile.lock b/Podfile.lock index da5a99e0e..7da6a1e24 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,20 +1,24 @@ PODS: - DateToolsSwift (5.0.0) + - Kanna (5.2.4) - SwiftGen (6.4.0) DEPENDENCIES: - DateToolsSwift (~> 5.0.0) + - Kanna (~> 5.2.2) - SwiftGen (~> 6.4.0) SPEC REPOS: trunk: - DateToolsSwift + - Kanna - SwiftGen SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 + Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 -PODFILE CHECKSUM: 5a58ccfd113912468e008313e1c91ed51b7cba20 +PODFILE CHECKSUM: 8b24099ae9ac02698d464cc508af9550352c85cb COCOAPODS: 1.10.1