diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
index 3c73a77bf..d655753d3 100644
--- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
+++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents
@@ -1,11 +1,11 @@
-
+
-
+
@@ -22,7 +22,7 @@
-
+
@@ -32,7 +32,7 @@
-
+
@@ -49,7 +49,7 @@
-
+
@@ -72,17 +72,38 @@
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
@@ -93,7 +114,7 @@
-
+
@@ -105,7 +126,7 @@
-
+
@@ -117,15 +138,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
-
+
@@ -145,23 +164,31 @@
-
-
+
+
-
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -170,11 +197,12 @@
-
+
-
+
+
-
\ No newline at end of file
+
diff --git a/CoreDataStack/CoreDataStack.swift b/CoreDataStack/CoreDataStack.swift
index 02aa397ff..1d13ee5ee 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: Toot.self)]
+ let bundles = [Bundle(for: Status.self)]
guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: bundles) else {
fatalError("cannot locate bundles")
}
diff --git a/CoreDataStack/Entity/Application.swift b/CoreDataStack/Entity/Application.swift
index c9aa22833..b40b2e5b2 100644
--- a/CoreDataStack/Entity/Application.swift
+++ b/CoreDataStack/Entity/Application.swift
@@ -17,8 +17,8 @@ public final class Application: NSManagedObject {
@NSManaged public private(set) var website: String?
@NSManaged public private(set) var vapidKey: String?
- // one-to-many relationship
- @NSManaged public private(set) var toots: Set
+ // one-to-one relationship
+ @NSManaged public private(set) var status: Status
}
public extension Application {
diff --git a/CoreDataStack/Entity/Attachment.swift b/CoreDataStack/Entity/Attachment.swift
index 33a0c0826..16f007bf1 100644
--- a/CoreDataStack/Entity/Attachment.swift
+++ b/CoreDataStack/Entity/Attachment.swift
@@ -28,7 +28,7 @@ public final class Attachment: NSManagedObject {
@NSManaged public private(set) var index: NSNumber
// many-to-one relastionship
- @NSManaged public private(set) var toot: Toot?
+ @NSManaged public private(set) var status: Status?
}
diff --git a/CoreDataStack/Entity/Emoji.swift b/CoreDataStack/Entity/Emoji.swift
index 933baab96..e9ee9d235 100644
--- a/CoreDataStack/Entity/Emoji.swift
+++ b/CoreDataStack/Entity/Emoji.swift
@@ -20,7 +20,7 @@ public final class Emoji: NSManagedObject {
@NSManaged public private(set) var category: String?
// many-to-one relationship
- @NSManaged public private(set) var toot: Toot?
+ @NSManaged public private(set) var status: Status?
}
public extension Emoji {
diff --git a/CoreDataStack/Entity/HomeTimelineIndex.swift b/CoreDataStack/Entity/HomeTimelineIndex.swift
index 192e06e52..a902f5ce5 100644
--- a/CoreDataStack/Entity/HomeTimelineIndex.swift
+++ b/CoreDataStack/Entity/HomeTimelineIndex.swift
@@ -22,7 +22,7 @@ final public class HomeTimelineIndex: NSManagedObject {
// many-to-one relationship
- @NSManaged public private(set) var toot: Toot
+ @NSManaged public private(set) var status: Status
}
@@ -32,16 +32,16 @@ extension HomeTimelineIndex {
public static func insert(
into context: NSManagedObjectContext,
property: Property,
- toot: Toot
+ status: Status
) -> HomeTimelineIndex {
let index: HomeTimelineIndex = context.insertObject()
index.identifier = property.identifier
index.domain = property.domain
index.userID = property.userID
- index.createdAt = toot.createdAt
+ index.createdAt = status.createdAt
- index.toot = toot
+ index.status = status
return index
}
diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift
index dc88d48a2..2228787b5 100644
--- a/CoreDataStack/Entity/MastodonUser.swift
+++ b/CoreDataStack/Entity/MastodonUser.swift
@@ -21,24 +21,44 @@ final public class MastodonUser: NSManagedObject {
@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 header: String
+ @NSManaged public private(set) var headerStatic: String?
+ @NSManaged public private(set) var note: String?
+ @NSManaged public private(set) var url: String?
+ @NSManaged public private(set) var statusesCount: NSNumber
+ @NSManaged public private(set) var followingCount: NSNumber
+ @NSManaged public private(set) var followersCount: NSNumber
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
- @NSManaged public private(set) var pinnedToot: Toot?
+ @NSManaged public private(set) var pinnedStatus: Status?
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
// one-to-many relationship
- @NSManaged public private(set) var toots: Set?
+ @NSManaged public private(set) var statuses: 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 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 votePollOptions: Set?
@NSManaged public private(set) var votePolls: Set?
+ // relationships
+ @NSManaged public private(set) var following: Set?
+ @NSManaged public private(set) var followingBy: Set?
+ @NSManaged public private(set) var followRequested: Set?
+ @NSManaged public private(set) var followRequestedBy: Set?
+ @NSManaged public private(set) var muting: Set?
+ @NSManaged public private(set) var mutingBy: Set?
+ @NSManaged public private(set) var blocking: Set?
+ @NSManaged public private(set) var blockingBy: Set?
+ @NSManaged public private(set) var endorsed: Set?
+ @NSManaged public private(set) var endorsedBy: Set?
+ @NSManaged public private(set) var domainBlocking: Set?
+ @NSManaged public private(set) var domainBlockingBy: Set?
}
@@ -60,6 +80,16 @@ extension MastodonUser {
user.displayName = property.displayName
user.avatar = property.avatar
user.avatarStatic = property.avatarStatic
+ user.header = property.header
+ user.headerStatic = property.headerStatic
+ user.note = property.note
+ user.url = property.url
+ user.statusesCount = NSNumber(value: property.statusesCount)
+ user.followingCount = NSNumber(value: property.followingCount)
+ user.followersCount = NSNumber(value: property.followersCount)
+
+ // Mastodon do not provide relationship on the `Account`
+ // Update relationship via attribute updating interface
user.createdAt = property.createdAt
user.updatedAt = property.networkDate
@@ -93,6 +123,107 @@ extension MastodonUser {
self.avatarStatic = avatarStatic
}
}
+ public func update(header: String) {
+ if self.header != header {
+ self.header = header
+ }
+ }
+ public func update(headerStatic: String?) {
+ if self.headerStatic != headerStatic {
+ self.headerStatic = headerStatic
+ }
+ }
+ public func update(note: String?) {
+ if self.note != note {
+ self.note = note
+ }
+ }
+ public func update(url: String?) {
+ if self.url != url {
+ self.url = url
+ }
+ }
+ public func update(statusesCount: Int) {
+ if self.statusesCount.intValue != statusesCount {
+ self.statusesCount = NSNumber(value: statusesCount)
+ }
+ }
+ public func update(followingCount: Int) {
+ if self.followingCount.intValue != followingCount {
+ self.followingCount = NSNumber(value: followingCount)
+ }
+ }
+ public func update(followersCount: Int) {
+ if self.followersCount.intValue != followersCount {
+ self.followersCount = NSNumber(value: followersCount)
+ }
+ }
+ public func update(isFollowing: Bool, by mastodonUser: MastodonUser) {
+ if isFollowing {
+ if !(self.followingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).add(mastodonUser)
+ }
+ } else {
+ if (self.followingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).remove(mastodonUser)
+ }
+ }
+ }
+ public func update(isFollowRequested: Bool, by mastodonUser: MastodonUser) {
+ if isFollowRequested {
+ if !(self.followRequestedBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).add(mastodonUser)
+ }
+ } else {
+ if (self.followRequestedBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).remove(mastodonUser)
+ }
+ }
+ }
+ public func update(isMuting: Bool, by mastodonUser: MastodonUser) {
+ if isMuting {
+ if !(self.mutingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).add(mastodonUser)
+ }
+ } else {
+ if (self.mutingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).remove(mastodonUser)
+ }
+ }
+ }
+ public func update(isBlocking: Bool, by mastodonUser: MastodonUser) {
+ if isBlocking {
+ if !(self.blockingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).add(mastodonUser)
+ }
+ } else {
+ if (self.blockingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).remove(mastodonUser)
+ }
+ }
+ }
+ public func update(isEndorsed: Bool, by mastodonUser: MastodonUser) {
+ if isEndorsed {
+ if !(self.endorsedBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).add(mastodonUser)
+ }
+ } else {
+ if (self.endorsedBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).remove(mastodonUser)
+ }
+ }
+ }
+ public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) {
+ if isDomainBlocking {
+ if !(self.domainBlockingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).add(mastodonUser)
+ }
+ } else {
+ if (self.domainBlockingBy ?? Set()).contains(mastodonUser) {
+ self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).remove(mastodonUser)
+ }
+ }
+ }
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
@@ -100,8 +231,8 @@ extension MastodonUser {
}
-public extension MastodonUser {
- struct Property {
+extension MastodonUser {
+ public struct Property {
public let identifier: String
public let domain: String
@@ -111,6 +242,13 @@ public extension MastodonUser {
public let displayName: String
public let avatar: String
public let avatarStatic: String?
+ public let header: String
+ public let headerStatic: String?
+ public let note: String?
+ public let url: String?
+ public let statusesCount: Int
+ public let followingCount: Int
+ public let followersCount: Int
public let createdAt: Date
public let networkDate: Date
@@ -123,6 +261,13 @@ public extension MastodonUser {
displayName: String,
avatar: String,
avatarStatic: String?,
+ header: String,
+ headerStatic: String?,
+ note: String?,
+ url: String?,
+ statusesCount: Int,
+ followingCount: Int,
+ followersCount: Int,
createdAt: Date,
networkDate: Date
) {
@@ -134,6 +279,13 @@ public extension MastodonUser {
self.displayName = displayName
self.avatar = avatar
self.avatarStatic = avatarStatic
+ self.header = header
+ self.headerStatic = headerStatic
+ self.note = note
+ self.url = url
+ self.statusesCount = statusesCount
+ self.followingCount = followingCount
+ self.followersCount = followersCount
self.createdAt = createdAt
self.networkDate = networkDate
}
diff --git a/CoreDataStack/Entity/Mention.swift b/CoreDataStack/Entity/Mention.swift
index e659cf891..9559ea5d5 100644
--- a/CoreDataStack/Entity/Mention.swift
+++ b/CoreDataStack/Entity/Mention.swift
@@ -19,7 +19,7 @@ public final class Mention: NSManagedObject {
@NSManaged public private(set) var url: String
// many-to-one relationship
- @NSManaged public private(set) var toot: Toot
+ @NSManaged public private(set) var status: Status
}
public extension Mention {
diff --git a/CoreDataStack/Entity/Poll.swift b/CoreDataStack/Entity/Poll.swift
index 356f2fc2e..3ab48b444 100644
--- a/CoreDataStack/Entity/Poll.swift
+++ b/CoreDataStack/Entity/Poll.swift
@@ -22,7 +22,7 @@ public final class Poll: NSManagedObject {
@NSManaged public private(set) var updatedAt: Date
// one-to-one relationship
- @NSManaged public private(set) var toot: Toot
+ @NSManaged public private(set) var status: Status
// one-to-many relationship
@NSManaged public private(set) var options: Set
diff --git a/CoreDataStack/Entity/PrivateNote.swift b/CoreDataStack/Entity/PrivateNote.swift
new file mode 100644
index 000000000..2e02db25c
--- /dev/null
+++ b/CoreDataStack/Entity/PrivateNote.swift
@@ -0,0 +1,56 @@
+//
+// PrivateNote.swift
+// CoreDataStack
+//
+// Created by MainasuK Cirno on 2021-4-1.
+//
+
+import CoreData
+import Foundation
+
+final public class PrivateNote: NSManagedObject {
+
+ @NSManaged public private(set) var note: String?
+
+ @NSManaged public private(set) var updatedAt: Date
+
+ // many-to-one relationship
+ @NSManaged public private(set) var to: MastodonUser?
+ @NSManaged public private(set) var from: MastodonUser
+
+}
+
+extension PrivateNote {
+ public override func awakeFromInsert() {
+ super.awakeFromInsert()
+ setPrimitiveValue(Date(), forKey: #keyPath(PrivateNote.updatedAt))
+ }
+
+ @discardableResult
+ public static func insert(
+ into context: NSManagedObjectContext,
+ property: Property
+ ) -> PrivateNote {
+ let privateNode: PrivateNote = context.insertObject()
+ privateNode.note = property.note
+ return privateNode
+ }
+}
+
+extension PrivateNote {
+ public struct Property {
+ public let note: String?
+
+ init(note: String) {
+ self.note = note
+ }
+ }
+
+}
+
+extension PrivateNote: Managed {
+ public static var defaultSortDescriptors: [NSSortDescriptor] {
+ return [NSSortDescriptor(keyPath: \PrivateNote.updatedAt, ascending: false)]
+ }
+}
+
diff --git a/CoreDataStack/Entity/Toot.swift b/CoreDataStack/Entity/Status.swift
similarity index 75%
rename from CoreDataStack/Entity/Toot.swift
rename to CoreDataStack/Entity/Status.swift
index 43e728283..f40f78639 100644
--- a/CoreDataStack/Entity/Toot.swift
+++ b/CoreDataStack/Entity/Status.swift
@@ -8,7 +8,7 @@
import CoreData
import Foundation
-public final class Toot: NSManagedObject {
+public final class Status: NSManagedObject {
public typealias ID = String
@NSManaged public private(set) var identifier: ID
@@ -30,7 +30,7 @@ public final class Toot: NSManagedObject {
@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 inReplyToID: Status.ID?
@NSManaged public private(set) var inReplyToAccountID: MastodonUser.ID?
@NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code)
@@ -38,8 +38,8 @@ public final class Toot: NSManagedObject {
// many-to-one relastionship
@NSManaged public private(set) var author: MastodonUser
- @NSManaged public private(set) var reblog: Toot?
- @NSManaged public private(set) var replyTo: Toot?
+ @NSManaged public private(set) var reblog: Status?
+ @NSManaged public private(set) var replyTo: Status?
// many-to-many relastionship
@NSManaged public private(set) var favouritedBy: Set?
@@ -52,27 +52,27 @@ public final class Toot: NSManagedObject {
@NSManaged public private(set) var poll: Poll?
// one-to-many relationship
- @NSManaged public private(set) var reblogFrom: Set?
+ @NSManaged public private(set) var reblogFrom: Set?
@NSManaged public private(set) var mentions: Set?
@NSManaged public private(set) var emojis: Set?
@NSManaged public private(set) var tags: Set?
@NSManaged public private(set) var homeTimelineIndexes: Set?
@NSManaged public private(set) var mediaAttachments: Set?
- @NSManaged public private(set) var replyFrom: Set?
+ @NSManaged public private(set) var replyFrom: Set?
@NSManaged public private(set) var updatedAt: Date
@NSManaged public private(set) var deletedAt: Date?
}
-public extension Toot {
+public extension Status {
@discardableResult
static func insert(
into context: NSManagedObjectContext,
property: Property,
author: MastodonUser,
- reblog: Toot?,
+ reblog: Status?,
application: Application?,
- replyTo: Toot?,
+ replyTo: Status?,
poll: Poll?,
mentions: [Mention]?,
emojis: [Emoji]?,
@@ -83,8 +83,8 @@ public extension Toot {
mutedBy: MastodonUser?,
bookmarkedBy: MastodonUser?,
pinnedBy: MastodonUser?
- ) -> Toot {
- let toot: Toot = context.insertObject()
+ ) -> Status {
+ let toot: Status = context.insertObject()
toot.identifier = property.identifier
toot.domain = property.domain
@@ -117,28 +117,28 @@ public extension Toot {
toot.poll = poll
if let mentions = mentions {
- toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: mentions)
+ toot.mutableSetValue(forKey: #keyPath(Status.mentions)).addObjects(from: mentions)
}
if let emojis = emojis {
- toot.mutableSetValue(forKey: #keyPath(Toot.emojis)).addObjects(from: emojis)
+ toot.mutableSetValue(forKey: #keyPath(Status.emojis)).addObjects(from: emojis)
}
if let tags = tags {
- toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags)
+ toot.mutableSetValue(forKey: #keyPath(Status.tags)).addObjects(from: tags)
}
if let mediaAttachments = mediaAttachments {
- toot.mutableSetValue(forKey: #keyPath(Toot.mediaAttachments)).addObjects(from: mediaAttachments)
+ toot.mutableSetValue(forKey: #keyPath(Status.mediaAttachments)).addObjects(from: mediaAttachments)
}
if let favouritedBy = favouritedBy {
- toot.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(favouritedBy)
+ toot.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(favouritedBy)
}
if let rebloggedBy = rebloggedBy {
- toot.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(rebloggedBy)
+ toot.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(rebloggedBy)
}
if let mutedBy = mutedBy {
- toot.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mutedBy)
+ toot.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mutedBy)
}
if let bookmarkedBy = bookmarkedBy {
- toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
+ toot.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(bookmarkedBy)
}
toot.updatedAt = property.networkDate
@@ -167,56 +167,56 @@ public extension Toot {
}
}
- func update(replyTo: Toot?) {
+ func update(replyTo: Status?) {
if self.replyTo != replyTo {
self.replyTo = replyTo
}
}
- func update(liked: Bool, mastodonUser: MastodonUser) {
+ func update(liked: Bool, by mastodonUser: MastodonUser) {
if liked {
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser)
}
} else {
if (self.favouritedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).remove(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser)
}
}
}
- func update(reblogged: Bool, mastodonUser: MastodonUser) {
+ func update(reblogged: Bool, by mastodonUser: MastodonUser) {
if reblogged {
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).add(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser)
}
} else {
if (self.rebloggedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.rebloggedBy)).remove(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser)
}
}
}
- func update(muted: Bool, mastodonUser: MastodonUser) {
+ func update(muted: Bool, by mastodonUser: MastodonUser) {
if muted {
if !(self.mutedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).add(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser)
}
} else {
if (self.mutedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.mutedBy)).remove(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser)
}
}
}
- func update(bookmarked: Bool, mastodonUser: MastodonUser) {
+ func update(bookmarked: Bool, by mastodonUser: MastodonUser) {
if bookmarked {
if !(self.bookmarkedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser)
}
} else {
if (self.bookmarkedBy ?? Set()).contains(mastodonUser) {
- self.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).remove(mastodonUser)
+ self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser)
}
}
}
@@ -227,7 +227,7 @@ public extension Toot {
}
-public extension Toot {
+public extension Status {
struct Property {
public let identifier: ID
@@ -247,7 +247,7 @@ public extension Toot {
public let repliesCount: NSNumber?
public let url: String?
- public let inReplyToID: Toot.ID?
+ public let inReplyToID: Status.ID?
public let inReplyToAccountID: MastodonUser.ID?
public let language: String? // (ISO 639 Part @1 two-letter language code)
public let text: String?
@@ -267,7 +267,7 @@ public extension Toot {
favouritesCount: NSNumber,
repliesCount: NSNumber?,
url: String?,
- inReplyToID: Toot.ID?,
+ inReplyToID: Status.ID?,
inReplyToAccountID: MastodonUser.ID?,
language: String?,
text: String?,
@@ -296,20 +296,20 @@ public extension Toot {
}
}
-extension Toot: Managed {
+extension Status: Managed {
public static var defaultSortDescriptors: [NSSortDescriptor] {
- return [NSSortDescriptor(keyPath: \Toot.createdAt, ascending: false)]
+ return [NSSortDescriptor(keyPath: \Status.createdAt, ascending: false)]
}
}
-extension Toot {
+extension Status {
static func predicate(domain: String) -> NSPredicate {
- return NSPredicate(format: "%K == %@", #keyPath(Toot.domain), domain)
+ return NSPredicate(format: "%K == %@", #keyPath(Status.domain), domain)
}
static func predicate(id: String) -> NSPredicate {
- return NSPredicate(format: "%K == %@", #keyPath(Toot.id), id)
+ return NSPredicate(format: "%K == %@", #keyPath(Status.id), id)
}
public static func predicate(domain: String, id: String) -> NSPredicate {
@@ -320,7 +320,7 @@ extension Toot {
}
static func predicate(ids: [String]) -> NSPredicate {
- return NSPredicate(format: "%K IN %@", #keyPath(Toot.id), ids)
+ return NSPredicate(format: "%K IN %@", #keyPath(Status.id), ids)
}
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
@@ -331,10 +331,10 @@ extension Toot {
}
public static func notDeleted() -> NSPredicate {
- return NSPredicate(format: "%K == nil", #keyPath(Toot.deletedAt))
+ return NSPredicate(format: "%K == nil", #keyPath(Status.deletedAt))
}
public static func deleted() -> NSPredicate {
- return NSPredicate(format: "%K != nil", #keyPath(Toot.deletedAt))
+ return NSPredicate(format: "%K != nil", #keyPath(Status.deletedAt))
}
}
diff --git a/CoreDataStack/Entity/Tag.swift b/CoreDataStack/Entity/Tag.swift
index d817c774b..2f1914c4a 100644
--- a/CoreDataStack/Entity/Tag.swift
+++ b/CoreDataStack/Entity/Tag.swift
@@ -17,7 +17,7 @@ public final class Tag: NSManagedObject {
@NSManaged public private(set) var url: String
// many-to-many relationship
- @NSManaged public private(set) var toot: Toot
+ @NSManaged public private(set) var statuses: Set?
// one-to-many relationship
@NSManaged public private(set) var histories: Set?
diff --git a/Localization/app.json b/Localization/app.json
index 433515fef..6d96fd5bd 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -32,6 +32,7 @@
"edit": "Edit",
"save": "Save",
"ok": "OK",
+ "done": "Done",
"confirm": "Confirm",
"continue": "Continue",
"cancel": "Cancel",
@@ -65,6 +66,15 @@
"closed": "Closed"
}
},
+ "firendship": {
+ "follow": "Follow",
+ "following": "Following",
+ "block": "Block",
+ "blocked": "Blocked",
+ "mute": "Mute",
+ "muted": "Muted",
+ "edit_info": "Edit info"
+ },
"timeline": {
"loader": {
"load_missing_posts": "Load missing posts",
@@ -237,6 +247,18 @@
"direct": "Only people I mention"
}
},
+ "profile": {
+ "dashboard": {
+ "posts": "posts",
+ "following": "following",
+ "followers": "followers"
+ },
+ "segmented_control": {
+ "posts": "Posts",
+ "replies": "Replies",
+ "media": "Media"
+ }
+ },
"search": {
"searchBar": {
"placeholder": "Search hashtags and users",
diff --git a/Localization/ios-infoPlist.json b/Localization/ios-infoPlist.json
index 0a260c273..f25dbcc0e 100644
--- a/Localization/ios-infoPlist.json
+++ b/Localization/ios-infoPlist.json
@@ -1,4 +1,4 @@
{
- "NSCameraUsageDescription": "Used to take photo for toot",
+ "NSCameraUsageDescription": "Used to take photo for post status",
"NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library"
-}
+}
\ No newline at end of file
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 6a7995327..0bff57bed 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -47,7 +47,7 @@
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; };
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; };
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
- 2D42FF6B25C817D2004A627A /* TootContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* TootContent.swift */; };
+ 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonStatusContent.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 */; };
@@ -122,7 +122,7 @@
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
- DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; };
+ DB084B5725CBC56C00F898ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Status.swift */; };
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; };
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
@@ -138,6 +138,9 @@
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
+ DB35FC1F2612F1D9006193C9 /* ProfileFriendshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */; };
+ DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
+ DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
@@ -168,6 +171,9 @@
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */; };
+ DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; };
+ DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */; };
+ DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; };
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; };
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
@@ -186,7 +192,7 @@
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
- DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
+ DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; };
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; };
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
@@ -203,7 +209,7 @@
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; };
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; };
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; };
- DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */; };
+ DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */; };
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; };
DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D44A2609C11900D12C0D /* PollOptionView.swift */; };
@@ -218,7 +224,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 /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA2625C110B4008580ED /* Toot.swift */; };
+ DB89BA2725C110B4008580ED /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA2625C110B4008580ED /* Status.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 */; };
@@ -257,9 +263,30 @@
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
+ DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
+ DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
+ DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
+ DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; };
+ DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */; };
+ DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */; };
+ DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */; };
+ DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */; };
+ DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; };
+ DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; };
+ DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */; };
+ DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */; };
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
+ DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; };
+ DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */; };
+ DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; };
+ DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B35261440BA0045B23D /* UINavigationController.swift */; };
+ DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B7A261443AD0045B23D /* ViewController.swift */; };
+ DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B88261454BA0045B23D /* CGImage.swift */; };
+ DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; };
+ DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */; };
+ DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B9A2615849F0045B23D /* PrivateNote.swift */; };
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */; };
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
@@ -360,7 +387,7 @@
2D38F1FD25CD481700561493 /* StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProvider.swift; sourceTree = ""; };
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = ""; };
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITapGestureRecognizer.swift; sourceTree = ""; };
- 2D42FF6A25C817D2004A627A /* TootContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TootContent.swift; sourceTree = ""; };
+ 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonStatusContent.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 = ""; };
@@ -437,7 +464,7 @@
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = ""; };
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = ""; };
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; };
- DB084B5625CBC56C00F898ED /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = ""; };
+ DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; };
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; };
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; };
@@ -454,6 +481,9 @@
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = ""; };
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; };
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = ""; };
+ DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFriendshipActionButton.swift; sourceTree = ""; };
+ DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldView.swift; sourceTree = ""; };
+ DB35FC2E26130172006193C9 /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = ""; };
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; };
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -490,6 +520,9 @@
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; };
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; };
DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; };
+ DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+State.swift"; sourceTree = ""; };
+ DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewController+StatusProvider.swift"; sourceTree = ""; };
+ DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+UserTimeline.swift"; sourceTree = ""; };
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; };
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = ""; };
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = ""; };
@@ -507,7 +540,7 @@
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = ""; };
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = ""; };
DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSKeyValueObservation.swift; sourceTree = ""; };
- DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkContentStatusBarStyleNavigationController.swift; sourceTree = ""; };
+ DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveStatusBarStyleNavigationController.swift; sourceTree = ""; };
DB68A05C25E9055900CFDF14 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; };
DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; };
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = ""; };
@@ -524,7 +557,7 @@
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; };
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; };
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = ""; };
- DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToTootContentCollectionViewCell.swift; sourceTree = ""; };
+ DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentCollectionViewCell.swift; sourceTree = ""; };
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = ""; };
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = ""; };
DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; };
@@ -541,7 +574,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 /* Toot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toot.swift; sourceTree = ""; };
+ DB89BA2625C110B4008580ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.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 = ""; };
@@ -579,9 +612,29 @@
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = ""; };
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; };
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; };
+ DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; };
+ DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; };
+ DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = ""; };
+ DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = ""; };
+ DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewController.swift; sourceTree = ""; };
+ DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = ""; };
+ DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewModel.swift; sourceTree = ""; };
+ DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; };
+ DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = ""; };
+ DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardView.swift; sourceTree = ""; };
+ DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusDashboardMeterView.swift; sourceTree = ""; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; };
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = ""; };
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPublishService.swift; sourceTree = ""; };
+ DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = ""; };
+ DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFetchedResultsController.swift; sourceTree = ""; };
+ DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = ""; };
+ DBCC3B35261440BA0045B23D /* UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = ""; };
+ DBCC3B7A261443AD0045B23D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
+ DBCC3B88261454BA0045B23D /* CGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImage.swift; sourceTree = ""; };
+ DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = ""; };
+ DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Relationship.swift"; sourceTree = ""; };
+ DBCC3B9A2615849F0045B23D /* PrivateNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateNote.swift; sourceTree = ""; };
DBCCC71D25F73297007E1AB6 /* APIService+Reblog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Reblog.swift"; sourceTree = ""; };
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = ""; };
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = ""; };
@@ -601,6 +654,7 @@
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */,
2D939AC825EE14620076FA61 /* CropViewController in Frameworks */,
+ DBB525082611EAC0002F1F29 /* Tabman in Frameworks */,
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
@@ -869,6 +923,7 @@
children = (
2D76319D25C151F600929FB9 /* Section */,
2D7631B125C159E700929FB9 /* Item */,
+ DBCBED2226132E1D00B49291 /* FetchedResultsController */,
);
path = Diffiable;
sourceTree = "";
@@ -1012,7 +1067,7 @@
isa = PBXGroup;
children = (
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
- DB084B5625CBC56C00F898ED /* Toot.swift */,
+ DB084B5625CBC56C00F898ED /* Status.swift */,
DB9D6C3725E508BE0051B173 /* Attachment.swift */,
);
path = CoreDataStack;
@@ -1093,6 +1148,7 @@
children = (
DB427DE325BAA00100D1B89D /* Info.plist */,
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
+ DBCC3B7A261443AD0045B23D /* ViewController.swift */,
2D76319C25C151DE00929FB9 /* Diffiable */,
DB8AF52A25C13561002E6C99 /* State */,
2D61335525C1886800CAE157 /* Service */,
@@ -1143,6 +1199,7 @@
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
+ DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */,
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
@@ -1151,6 +1208,7 @@
DB9A488F26035963008B817C /* APIService+Media.swift */,
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
+ DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
);
path = APIService;
sourceTree = "";
@@ -1208,7 +1266,7 @@
DB68A04F25E9028800CFDF14 /* NavigationController */ = {
isa = PBXGroup;
children = (
- DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */,
+ DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */,
);
path = NavigationController;
sourceTree = "";
@@ -1247,7 +1305,7 @@
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = {
isa = PBXGroup;
children = (
- DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift */,
+ DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */,
DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */,
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentTableViewCell.swift */,
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
@@ -1305,7 +1363,7 @@
DB89BA2C25C110B7008580ED /* Entity */ = {
isa = PBXGroup;
children = (
- DB89BA2625C110B4008580ED /* Toot.swift */,
+ DB89BA2625C110B4008580ED /* Status.swift */,
DB8AF52425C131D1002E6C99 /* MastodonUser.swift */,
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */,
2D927F0125C7E4F2004F19B8 /* Mention.swift */,
@@ -1317,6 +1375,7 @@
DB9D6C2D25E504AC0051B173 /* Attachment.swift */,
DB4481AC25EE155900BEFB67 /* Poll.swift */,
DB4481B225EE16D000BEFB67 /* PollOption.swift */,
+ DBCC3B9A2615849F0045B23D /* PrivateNote.swift */,
);
path = Entity;
sourceTree = "";
@@ -1392,6 +1451,7 @@
0FAA101B25E10E760017CCDE /* UIFont.swift */,
2D939AB425EDD8A90076FA61 /* String.swift */,
2D206B7F25F5F45E00143C56 /* UIImage.swift */,
+ DBCC3B88261454BA0045B23D /* CGImage.swift */,
2D206B8525F5FB0900143C56 /* Double.swift */,
2D206B9125F60EA700143C56 /* UIControl.swift */,
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
@@ -1402,6 +1462,8 @@
2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */,
2D84350425FF858100EECE90 /* UIScrollView.swift */,
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */,
+ DBCC3B2F261440A50045B23D /* UITabBarController.swift */,
+ DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
);
path = Extension;
sourceTree = "";
@@ -1457,7 +1519,13 @@
DB9D6C0825E4F5A60051B173 /* Profile */ = {
isa = PBXGroup;
children = (
+ DBB525132611EBB1002F1F29 /* Segmented */,
+ DBB525462611ED57002F1F29 /* Header */,
+ DBB5253B2611ECF5002F1F29 /* Timeline */,
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
+ DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
+ DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */,
+ DBB525632612C988002F1F29 /* MeProfileViewModel.swift */,
);
path = Profile;
sourceTree = "";
@@ -1487,7 +1555,8 @@
DB9E0D6925EDFFE500CFDD76 /* Helper */ = {
isa = PBXGroup;
children = (
- 2D42FF6A25C817D2004A627A /* TootContent.swift */,
+ 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
+ DB35FC2E26130172006193C9 /* MastodonField.swift */,
);
path = Helper;
sourceTree = "";
@@ -1509,6 +1578,65 @@
path = View;
sourceTree = "";
};
+ DBB525132611EBB1002F1F29 /* Segmented */ = {
+ isa = PBXGroup;
+ children = (
+ DBB525262611EBDA002F1F29 /* Paging */,
+ DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */,
+ );
+ path = Segmented;
+ sourceTree = "";
+ };
+ DBB525262611EBDA002F1F29 /* Paging */ = {
+ isa = PBXGroup;
+ children = (
+ DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */,
+ DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */,
+ );
+ path = Paging;
+ sourceTree = "";
+ };
+ DBB5253B2611ECF5002F1F29 /* Timeline */ = {
+ isa = PBXGroup;
+ children = (
+ DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */,
+ DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */,
+ DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */,
+ DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */,
+ DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */,
+ );
+ path = Timeline;
+ sourceTree = "";
+ };
+ DBB525462611ED57002F1F29 /* Header */ = {
+ isa = PBXGroup;
+ children = (
+ DBB525732612D5A5002F1F29 /* View */,
+ DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */,
+ );
+ path = Header;
+ sourceTree = "";
+ };
+ DBB525732612D5A5002F1F29 /* View */ = {
+ isa = PBXGroup;
+ children = (
+ DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */,
+ DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */,
+ DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */,
+ DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */,
+ DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */,
+ );
+ path = View;
+ sourceTree = "";
+ };
+ DBCBED2226132E1D00B49291 /* FetchedResultsController */ = {
+ isa = PBXGroup;
+ children = (
+ DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
+ );
+ path = FetchedResultsController;
+ sourceTree = "";
+ };
DBE0821A25CD382900FD6BBD /* Register */ = {
isa = PBXGroup;
children = (
@@ -1562,6 +1690,7 @@
2D939AC725EE14620076FA61 /* CropViewController */,
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
DBE64A8A260C49D200E6359A /* TwitterTextEditor */,
+ DBB525072611EAC0002F1F29 /* Tabman */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@@ -1693,6 +1822,7 @@
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
+ DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@@ -1877,6 +2007,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */,
+ DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */,
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
@@ -1884,14 +2016,17 @@
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
+ DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
+ DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */,
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
+ DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
@@ -1906,6 +2041,7 @@
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
+ DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
@@ -1919,6 +2055,7 @@
2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */,
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
+ DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */,
0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */,
@@ -1926,14 +2063,17 @@
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */,
2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */,
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
+ DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
+ DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */,
+ DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */,
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
@@ -1963,12 +2103,15 @@
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
2DE0FAC12615F04D00CDF649 /* RecomendHashTagSection.swift in Sources */,
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
+ DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */,
+ DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */,
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
+ DBCC3B7B261443AD0045B23D /* ViewController.swift in Sources */,
2D38F1DF25CD46A400561493 /* HomeTimelineViewController+StatusProvider.swift in Sources */,
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */,
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
@@ -1986,10 +2129,12 @@
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
+ DB35FC1F2612F1D9006193C9 /* ProfileFriendshipActionButton.swift in Sources */,
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */,
+ DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
@@ -2005,12 +2150,13 @@
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
+ DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */,
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
- DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */,
+ DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
@@ -2018,16 +2164,19 @@
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
+ DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */,
2D34D9CB261489930081BFC0 /* SearchViewController+RecomendView.swift in Sources */,
+ DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */,
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */,
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
+ DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
- DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
+ DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
@@ -2055,7 +2204,7 @@
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */,
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
- 2D42FF6B25C817D2004A627A /* TootContent.swift in Sources */,
+ 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */,
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
@@ -2064,7 +2213,9 @@
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */,
+ DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
+ DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */,
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */,
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */,
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
@@ -2072,19 +2223,23 @@
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
+ DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
+ DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
+ DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */,
+ DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
- DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */,
+ DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2115,11 +2270,12 @@
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */,
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
+ DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */,
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */,
DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */,
DB89BA1B25C1107F008580ED /* Collection.swift in Sources */,
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */,
- DB89BA2725C110B4008580ED /* Toot.swift in Sources */,
+ DB89BA2725C110B4008580ED /* Status.swift in Sources */,
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */,
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */,
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
@@ -2679,6 +2835,14 @@
minimumVersion = 1.4.1;
};
};
+ DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/uias/Tabman";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 2.11.0;
+ };
+ };
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/MainasuK/TwitterTextEditor";
@@ -2734,6 +2898,11 @@
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
productName = "UITextView+Placeholder";
};
+ DBB525072611EAC0002F1F29 /* Tabman */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */;
+ productName = Tabman;
+ };
DBE64A8A260C49D200E6359A /* TwitterTextEditor */ = {
isa = XCSwiftPackageProductDependency;
package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 89055cd4a..3bd82fce8 100644
--- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -55,6 +55,15 @@
"version": "6.1.0"
}
},
+ {
+ "package": "Pageboy",
+ "repositoryURL": "https://github.com/uias/Pageboy",
+ "state": {
+ "branch": null,
+ "revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6",
+ "version": "3.6.2"
+ }
+ },
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
@@ -82,6 +91,15 @@
"version": "5.0.0"
}
},
+ {
+ "package": "Tabman",
+ "repositoryURL": "https://github.com/uias/Tabman",
+ "state": {
+ "branch": null,
+ "revision": "bce2c87659c0ed868e6ef0aa1e05a330e202533f",
+ "version": "2.11.0"
+ }
+ },
{
"package": "ThirdPartyMailer",
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift
index 5590025db..d578ee528 100644
--- a/Mastodon/Coordinator/SceneCoordinator.swift
+++ b/Mastodon/Coordinator/SceneCoordinator.swift
@@ -51,6 +51,9 @@ extension SceneCoordinator {
// compose
case compose(viewModel: ComposeViewModel)
+ // profile
+ case profile(viewModel: ProfileViewModel)
+
// misc
case alertController(alertController: UIAlertController)
@@ -120,17 +123,18 @@ extension SceneCoordinator {
presentingViewController.show(viewController, sender: sender)
case .showDetail:
- let navigationController = UINavigationController(rootViewController: viewController)
+ let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
presentingViewController.showDetailViewController(navigationController, sender: sender)
case .modal(let animated, let completion):
let modalNavigationController: UINavigationController = {
if scene.isOnboarding {
- return DarkContentStatusBarStyleNavigationController(rootViewController: viewController)
+ return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
} else {
return UINavigationController(rootViewController: viewController)
}
}()
+ modalNavigationController.modalPresentationCapturesStatusBarAppearance = true
if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate {
modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate
}
@@ -147,12 +151,15 @@ extension SceneCoordinator {
sender?.navigationController?.pushViewController(viewController, animated: true)
case .safariPresent(let animated, let completion):
+ viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
case .activityViewControllerPresent(let animated, let completion):
+ viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
case .alertController(let animated, let completion):
+ viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
}
@@ -202,6 +209,10 @@ private extension SceneCoordinator {
let _viewController = ComposeViewController()
_viewController.viewModel = viewModel
viewController = _viewController
+ case .profile(let viewModel):
+ let _viewController = ProfileViewController()
+ _viewController.viewModel = viewModel
+ viewController = _viewController
case .alertController(let alertController):
if let popoverPresentationController = alertController.popoverPresentationController {
assert(
diff --git a/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift b/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift
new file mode 100644
index 000000000..a61429ab8
--- /dev/null
+++ b/Mastodon/Diffiable/FetchedResultsController/StatusFetchedResultsController.swift
@@ -0,0 +1,86 @@
+//
+// StatusFetchedResultsController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+final class StatusFetchedResultsController: NSObject {
+
+ var disposeBag = Set()
+
+ let fetchedResultsController: NSFetchedResultsController
+
+ // input
+ let domain = CurrentValueSubject(nil)
+ let statusIDs = CurrentValueSubject<[Mastodon.Entity.Status.ID], Never>([])
+
+ // output
+ let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([])
+
+ init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate) {
+ self.domain.value = domain ?? ""
+ self.fetchedResultsController = {
+ let fetchRequest = Status.sortedFetchRequest
+ fetchRequest.predicate = Status.predicate(domain: domain ?? "", ids: [])
+ fetchRequest.returnsObjectsAsFaults = false
+ fetchRequest.fetchBatchSize = 20
+ let controller = NSFetchedResultsController(
+ fetchRequest: fetchRequest,
+ managedObjectContext: managedObjectContext,
+ sectionNameKeyPath: nil,
+ cacheName: nil
+ )
+
+ return controller
+ }()
+ super.init()
+
+ fetchedResultsController.delegate = self
+
+ Publishers.CombineLatest(
+ self.domain.removeDuplicates().eraseToAnyPublisher(),
+ self.statusIDs.removeDuplicates().eraseToAnyPublisher()
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] domain, ids in
+ guard let self = self else { return }
+ self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+ Status.predicate(domain: domain ?? "", ids: ids),
+ additionalTweetPredicate
+ ])
+ do {
+ try self.fetchedResultsController.performFetch()
+ } catch {
+ assertionFailure(error.localizedDescription)
+ }
+ }
+ .store(in: &disposeBag)
+ }
+
+}
+
+// MARK: - NSFetchedResultsControllerDelegate
+extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate {
+ func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+
+ let indexes = statusIDs.value
+ let objects = fetchedResultsController.fetchedObjects ?? []
+
+ let items: [NSManagedObjectID] = objects
+ .compactMap { object in
+ indexes.firstIndex(of: object.id).map { index in (index, object) }
+ }
+ .sorted { $0.0 < $1.0 }
+ .map { $0.1.objectID }
+ self.objectIDs.value = items
+ }
+}
diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift
index 63c73d3a4..cd07c8836 100644
--- a/Mastodon/Diffiable/Item/Item.swift
+++ b/Mastodon/Diffiable/Item/Item.swift
@@ -16,40 +16,44 @@ enum Item {
case homeTimelineIndex(objectID: NSManagedObjectID, attribute: StatusAttribute)
// normal list
- case toot(objectID: NSManagedObjectID, attribute: StatusAttribute)
+ case status(objectID: NSManagedObjectID, attribute: StatusAttribute)
// loader
case homeMiddleLoader(upperTimelineIndexAnchorObjectID: NSManagedObjectID)
- case publicMiddleLoader(tootID: String)
+ case publicMiddleLoader(statusID: String)
case bottomLoader
}
protocol StatusContentWarningAttribute {
- var isStatusTextSensitive: Bool { get set }
- var isStatusSensitive: Bool { get set }
+ var isStatusTextSensitive: Bool? { get set }
+ var isStatusSensitive: Bool? { get set }
}
extension Item {
- class StatusAttribute: Equatable, Hashable, StatusContentWarningAttribute {
- var isStatusTextSensitive: Bool
- var isStatusSensitive: Bool
+ class StatusAttribute: StatusContentWarningAttribute {
+ var isStatusTextSensitive: Bool?
+ var isStatusSensitive: Bool?
- public init(
- isStatusTextSensitive: Bool,
- isStatusSensitive: Bool
+ init(
+ isStatusTextSensitive: Bool? = nil,
+ isStatusSensitive: Bool? = nil
) {
self.isStatusTextSensitive = isStatusTextSensitive
self.isStatusSensitive = isStatusSensitive
}
- static func == (lhs: Item.StatusAttribute, rhs: Item.StatusAttribute) -> Bool {
- return lhs.isStatusTextSensitive == rhs.isStatusTextSensitive &&
- lhs.isStatusSensitive == rhs.isStatusSensitive
- }
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(isStatusTextSensitive)
- hasher.combine(isStatusSensitive)
+ // delay attribute init
+ func setupForStatus(status: Status) {
+ if isStatusTextSensitive == nil {
+ isStatusTextSensitive = {
+ guard let spoilerText = status.spoilerText, !spoilerText.isEmpty else { return false }
+ return true
+ }()
+ }
+
+ if isStatusSensitive == nil {
+ isStatusSensitive = status.sensitive
+ }
}
}
}
@@ -59,7 +63,7 @@ extension Item: Equatable {
switch (lhs, rhs) {
case (.homeTimelineIndex(let objectIDLeft, _), .homeTimelineIndex(let objectIDRight, _)):
return objectIDLeft == objectIDRight
- case (.toot(let objectIDLeft, _), .toot(let objectIDRight, _)):
+ case (.status(let objectIDLeft, _), .status(let objectIDRight, _)):
return objectIDLeft == objectIDRight
case (.bottomLoader, .bottomLoader):
return true
@@ -78,7 +82,7 @@ extension Item: Hashable {
switch self {
case .homeTimelineIndex(let objectID, _):
hasher.combine(objectID)
- case .toot(let objectID, _):
+ case .status(let objectID, _):
hasher.combine(objectID)
case .publicMiddleLoader(let upper):
hasher.combine(String(describing: Item.publicMiddleLoader.self))
diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift
index 222c40246..e9785461a 100644
--- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift
+++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift
@@ -49,14 +49,14 @@ extension ComposeStatusSection {
] collectionView, indexPath, item -> UICollectionViewCell? in
switch item {
case .replyTo(let repliedToStatusObjectID):
- let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToTootContentCollectionViewCell
+ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell
return cell
- case .input(let replyToTootObjectID, let attribute):
+ case .input(let replyToStatusObjectID, let attribute):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
cell.textEditorView.text = attribute.composeContent.value ?? ""
managedObjectContext.perform {
- guard let replyToTootObjectID = replyToTootObjectID,
- let replyTo = managedObjectContext.object(with: replyToTootObjectID) as? Toot else {
+ guard let replyToStatusObjectID = replyToStatusObjectID,
+ let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
cell.statusView.headerContainerStackView.isHidden = true
return
}
diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift
index 809e3b3cc..5e891e13a 100644
--- a/Mastodon/Diffiable/Section/StatusSection.swift
+++ b/Mastodon/Diffiable/Section/StatusSection.swift
@@ -39,41 +39,41 @@ extension StatusSection {
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
- toot: timelineIndex.toot,
+ status: timelineIndex.status,
requestUserID: timelineIndex.userID,
statusItemAttribute: attribute
)
}
cell.delegate = statusTableViewCellDelegate
return cell
- case .toot(let objectID, let attribute):
+ case .status(let objectID, let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
// configure cell
managedObjectContext.performAndWait {
- let toot = managedObjectContext.object(with: objectID) as! Toot
+ let status = managedObjectContext.object(with: objectID) as! Status
StatusSection.configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
- toot: toot,
+ status: status,
requestUserID: requestUserID,
statusItemAttribute: attribute
)
}
cell.delegate = statusTableViewCellDelegate
return cell
- case .publicMiddleLoader(let upperTimelineTootID):
+ case .publicMiddleLoader(let upperTimelineStatusID):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
- timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: upperTimelineTootID, timelineIndexobjectID: nil)
+ timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: upperTimelineStatusID, timelineIndexobjectID: nil)
return cell
case .homeMiddleLoader(let upperTimelineIndexObjectID):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
- timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: nil, timelineIndexobjectID: upperTimelineIndexObjectID)
+ timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineStatusID: nil, timelineIndexobjectID: upperTimelineIndexObjectID)
return cell
case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
@@ -90,47 +90,50 @@ extension StatusSection {
dependency: NeedsDependency,
readableLayoutFrame: CGRect?,
timestampUpdatePublisher: AnyPublisher,
- toot: Toot,
+ status: Status,
requestUserID: String,
statusItemAttribute: Item.StatusAttribute
) {
+ // setup attribute
+ statusItemAttribute.setupForStatus(status: status.reblog ?? status)
+
// set header
- StatusSection.configureHeader(cell: cell, toot: toot)
- ManagedObjectObserver.observe(object: toot)
+ StatusSection.configureHeader(cell: cell, status: status)
+ ManagedObjectObserver.observe(object: status)
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
} receiveValue: { change in
guard case .update(let object) = change.changeType,
- let newToot = object as? Toot else { return }
- StatusSection.configureHeader(cell: cell, toot: newToot)
+ let newStatus = object as? Status else { return }
+ StatusSection.configureHeader(cell: cell, status: newStatus)
}
.store(in: &cell.disposeBag)
// set name username
cell.statusView.nameLabel.text = {
- let author = (toot.reblog ?? toot).author
+ let author = (status.reblog ?? status).author
return author.displayName.isEmpty ? author.username : author.displayName
}()
- cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct
+ cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set avatar
- if let reblog = toot.reblog {
+ if let reblog = status.reblog {
cell.statusView.avatarButton.isHidden = true
cell.statusView.avatarStackedContainerButton.isHidden = false
cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL()))
- cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
+ cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
} else {
cell.statusView.avatarButton.isHidden = false
cell.statusView.avatarStackedContainerButton.isHidden = true
- cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
+ cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
}
// set text
- cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
+ cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content)
// set status text content warning
- let spoilerText = (toot.reblog ?? toot).spoilerText ?? ""
- let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive
+ let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive ?? false
+ let spoilerText = (status.reblog ?? status).spoilerText ?? ""
cell.statusView.isStatusTextSensitive = isStatusTextSensitive
cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive)
cell.statusView.contentWarningTitle.text = {
@@ -142,7 +145,7 @@ extension StatusSection {
}()
// prepare media attachments
- let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
+ let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
// set image
let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
@@ -184,7 +187,7 @@ extension StatusSection {
}
}
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
- let isStatusSensitive = statusItemAttribute.isStatusSensitive
+ let isStatusSensitive = statusItemAttribute.isStatusSensitive ?? false
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
@@ -251,7 +254,7 @@ extension StatusSection {
cell.statusView.playerContainerView.playerViewController.player = nil
}
// set poll
- let poll = (toot.reblog ?? toot).poll
+ let poll = (status.reblog ?? status).poll
StatusSection.configurePoll(
cell: cell,
poll: poll,
@@ -278,10 +281,10 @@ extension StatusSection {
}
// toolbar
- StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID)
+ StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
// set date
- let createdAt = (toot.reblog ?? toot).createdAt
+ let createdAt = (status.reblog ?? status).createdAt
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
timestampUpdatePublisher
.sink { _ in
@@ -290,34 +293,34 @@ extension StatusSection {
.store(in: &cell.disposeBag)
// observe model change
- ManagedObjectObserver.observe(object: toot.reblog ?? toot)
+ ManagedObjectObserver.observe(object: status.reblog ?? status)
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
} receiveValue: { change in
guard case .update(let object) = change.changeType,
- let toot = object as? Toot else { return }
- StatusSection.configureActionToolBar(cell: cell, toot: toot, requestUserID: requestUserID)
+ let status = object as? Status else { return }
+ StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
- os_log("%{public}s[%{public}ld], %{public}s: reblog count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.reblogsCount.intValue)
- os_log("%{public}s[%{public}ld], %{public}s: like count label for toot %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, toot.id, toot.favouritesCount.intValue)
+ os_log("%{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.reblogsCount.intValue)
+ os_log("%{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.favouritesCount.intValue)
}
.store(in: &cell.disposeBag)
}
static func configureHeader(
cell: StatusTableViewCell,
- toot: Toot
+ status: Status
) {
- if toot.reblog != nil {
+ if status.reblog != nil {
cell.statusView.headerContainerStackView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
cell.statusView.headerInfoLabel.text = {
- let author = toot.author
+ let author = status.author
let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userReblogged(name)
}()
- } else if let replyTo = toot.replyTo {
+ } else if let replyTo = status.replyTo {
cell.statusView.headerContainerStackView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
cell.statusView.headerInfoLabel.text = {
@@ -332,29 +335,29 @@ extension StatusSection {
static func configureActionToolBar(
cell: StatusTableViewCell,
- toot: Toot,
+ status: Status,
requestUserID: String
) {
- let toot = toot.reblog ?? toot
+ let status = status.reblog ?? status
// set reply
let replyCountTitle: String = {
- let count = toot.repliesCount?.intValue ?? 0
+ let count = status.repliesCount?.intValue ?? 0
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.replyButton.setTitle(replyCountTitle, for: .normal)
// set reblog
- let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
+ let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let reblogCountTitle: String = {
- let count = toot.reblogsCount.intValue
+ let count = status.reblogsCount.intValue
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.reblogButton.setTitle(reblogCountTitle, for: .normal)
cell.statusView.actionToolbarContainer.isReblogButtonHighlight = isReblogged
// set like
- let isLike = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
+ let isLike = status.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
let favoriteCountTitle: String = {
- let count = toot.favouritesCount.intValue
+ let count = status.favouritesCount.intValue
return StatusSection.formattedNumberTitleForActionButton(count)
}()
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift
index 9219701f5..614735ad1 100644
--- a/Mastodon/Extension/ActiveLabel.swift
+++ b/Mastodon/Extension/ActiveLabel.swift
@@ -14,40 +14,61 @@ extension ActiveLabel {
enum Style {
case `default`
- case timelineHeaderView
+ case profileField
}
convenience init(style: Style) {
self.init()
- switch style {
- case .default:
- font = .preferredFont(forTextStyle: .body)
- textColor = Asset.Colors.Label.primary.color
- case .timelineHeaderView:
- font = .preferredFont(forTextStyle: .footnote)
- textColor = .secondaryLabel
- }
-
numberOfLines = 0
lineSpacing = 5
mentionColor = Asset.Colors.Label.highlight.color
hashtagColor = Asset.Colors.Label.highlight.color
URLColor = Asset.Colors.Label.highlight.color
+ #if DEBUG
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+ #endif
+
+ switch style {
+ case .default:
+ font = .preferredFont(forTextStyle: .body)
+ textColor = Asset.Colors.Label.primary.color
+ case .profileField:
+ font = .preferredFont(forTextStyle: .body)
+ textColor = Asset.Colors.Label.primary.color
+ numberOfLines = 1
+ }
}
}
extension ActiveLabel {
- func config(content: String) {
+ /// status content
+ func configure(content: String) {
activeEntities.removeAll()
- if let parseResult = try? TootContent.parse(toot: content) {
+ if let parseResult = try? MastodonStatusContent.parse(status: content) {
text = parseResult.trimmed
activeEntities = parseResult.activeEntities
} else {
text = ""
}
}
+
+ /// account note
+ func configure(note: String) {
+ configure(content: note)
+ }
}
+extension ActiveLabel {
+ /// account field
+ func configure(field: String) {
+ activeEntities.removeAll()
+ if let parseResult = try? MastodonField.parse(field: field) {
+ text = parseResult.value
+ activeEntities = parseResult.activeEntities
+ } else {
+ text = ""
+ }
+ }
+}
diff --git a/Mastodon/Extension/CGImage.swift b/Mastodon/Extension/CGImage.swift
new file mode 100644
index 000000000..252f1289a
--- /dev/null
+++ b/Mastodon/Extension/CGImage.swift
@@ -0,0 +1,154 @@
+//
+// CGImage.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-31.
+//
+
+import CoreImage
+
+extension CGImage {
+ // Reference
+ // https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
+ // Luma Y = 0.2126R + 0.7152G + 0.0722B
+ var brightness: CGFloat? {
+ let context = CIContext() // default with metal accelerate
+ let ciImage = CIImage(cgImage: self)
+ let rec709Image = context.createCGImage(
+ ciImage,
+ from: ciImage.extent,
+ format: .RGBA8,
+ colorSpace: CGColorSpace(name: CGColorSpace.itur_709) // BT.709 a.k.a Rec.709
+ )
+ guard let image = rec709Image,
+ image.bitsPerPixel == 32,
+ let data = rec709Image?.dataProvider?.data,
+ let pointer = CFDataGetBytePtr(data) else { return nil }
+
+ let length = CFDataGetLength(data)
+ guard length > 0 else { return nil}
+
+ var luma: CGFloat = 0.0
+ for i in stride(from: 0, to: length, by: 4) {
+ let r = pointer[i]
+ let g = pointer[i + 1]
+ let b = pointer[i + 2]
+ let Y = 0.2126 * CGFloat(r) + 0.7152 * CGFloat(g) + 0.0722 * CGFloat(b)
+ luma += Y
+ }
+ luma /= CGFloat(width * height)
+ return luma
+ }
+}
+
+#if canImport(SwiftUI) && DEBUG
+import SwiftUI
+import UIKit
+
+class BrightnessView: UIView {
+ let label = UILabel()
+ let imageView = UIImageView()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ let stackView = UIStackView()
+ stackView.axis = .horizontal
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(stackView)
+ NSLayoutConstraint.activate([
+ stackView.topAnchor.constraint(equalTo: topAnchor),
+ stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
+ stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+ stackView.distribution = .fillEqually
+ stackView.addArrangedSubview(imageView)
+ stackView.addArrangedSubview(label)
+
+ imageView.contentMode = .scaleAspectFill
+ imageView.layer.masksToBounds = true
+ label.textAlignment = .center
+ label.numberOfLines = 0
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func setImage(_ image: UIImage) {
+ imageView.image = image
+
+ guard let brightness = image.cgImage?.brightness,
+ let style = image.domainLumaCoefficientsStyle else {
+ label.text = ""
+ return
+ }
+ let styleDescription: String = {
+ switch style {
+ case .light: return "Light"
+ case .dark: return "Dark"
+ case .unspecified: fallthrough
+ @unknown default:
+ return "Unknown"
+ }
+ }()
+
+ label.text = styleDescription + "\n" + "\(brightness)"
+ }
+}
+
+struct CGImage_Brightness_Previews: PreviewProvider {
+
+ static var previews: some View {
+ Group {
+ UIViewPreview(width: 375) {
+ let view = BrightnessView()
+ view.setImage(.placeholder(color: .black))
+ return view
+ }
+ .previewLayout(.fixed(width: 375, height: 44))
+ UIViewPreview(width: 375) {
+ let view = BrightnessView()
+ view.setImage(.placeholder(color: .gray))
+ return view
+ }
+ .previewLayout(.fixed(width: 375, height: 44))
+ UIViewPreview(width: 375) {
+ let view = BrightnessView()
+ view.setImage(.placeholder(color: .separator))
+ return view
+ }
+ .previewLayout(.fixed(width: 375, height: 44))
+ UIViewPreview(width: 375) {
+ let view = BrightnessView()
+ view.setImage(.placeholder(color: .red))
+ return view
+ }
+ .previewLayout(.fixed(width: 375, height: 44))
+ UIViewPreview(width: 375) {
+ let view = BrightnessView()
+ view.setImage(.placeholder(color: .green))
+ return view
+ }
+ .previewLayout(.fixed(width: 375, height: 44))
+ UIViewPreview(width: 375) {
+ let view = BrightnessView()
+ view.setImage(.placeholder(color: .blue))
+ return view
+ }
+ .previewLayout(.fixed(width: 375, height: 44))
+ UIViewPreview(width: 375) {
+ let view = BrightnessView()
+ view.setImage(.placeholder(color: .secondarySystemGroupedBackground))
+ return view
+ }
+ .previewLayout(.fixed(width: 375, height: 44))
+ }
+ }
+
+}
+
+#endif
+
+
diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift
index 7575704ba..471adb815 100644
--- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift
+++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift
@@ -19,6 +19,13 @@ extension MastodonUser.Property {
displayName: entity.displayName,
avatar: entity.avatar,
avatarStatic: entity.avatarStatic,
+ header: entity.header,
+ headerStatic: entity.headerStatic,
+ note: entity.note,
+ url: entity.url,
+ statusesCount: entity.statusesCount,
+ followingCount: entity.followingCount,
+ followersCount: entity.followersCount,
createdAt: entity.createdAt,
networkDate: networkDate
)
@@ -26,7 +33,25 @@ extension MastodonUser.Property {
}
extension MastodonUser {
+
+ var displayNameWithFallback: String {
+ return !displayName.isEmpty ? displayName : username
+ }
+
+ var acctWithDomain: String {
+ return username + "@" + domain
+ }
+
+}
+
+extension MastodonUser {
+
+ public func headerImageURL() -> URL? {
+ return URL(string: header)
+ }
+
public func avatarImageURL() -> URL? {
return URL(string: avatar)
}
+
}
diff --git a/Mastodon/Extension/CoreDataStack/Toot.swift b/Mastodon/Extension/CoreDataStack/Status.swift
similarity index 95%
rename from Mastodon/Extension/CoreDataStack/Toot.swift
rename to Mastodon/Extension/CoreDataStack/Status.swift
index 2fab537e6..cf4f8a1bd 100644
--- a/Mastodon/Extension/CoreDataStack/Toot.swift
+++ b/Mastodon/Extension/CoreDataStack/Status.swift
@@ -1,5 +1,5 @@
//
-// Toot.swift
+// Status.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/4.
@@ -9,7 +9,7 @@ import Foundation
import CoreDataStack
import MastodonSDK
-extension Toot.Property {
+extension Status.Property {
init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) {
self.init(
domain: domain,
diff --git a/Mastodon/Extension/UIImage.swift b/Mastodon/Extension/UIImage.swift
index 3c3c43400..072a3d4d4 100644
--- a/Mastodon/Extension/UIImage.swift
+++ b/Mastodon/Extension/UIImage.swift
@@ -39,6 +39,13 @@ extension UIImage {
}
}
+extension UIImage {
+ var domainLumaCoefficientsStyle: UIUserInterfaceStyle? {
+ guard let brightness = cgImage?.brightness else { return nil }
+ return brightness > 100 ? .light : .dark // 0 ~ 255
+ }
+}
+
extension UIImage {
func blur(radius: CGFloat) -> UIImage? {
guard let inputImage = CIImage(image: self) else { return nil }
diff --git a/Mastodon/Extension/UINavigationController.swift b/Mastodon/Extension/UINavigationController.swift
new file mode 100644
index 000000000..54583e50a
--- /dev/null
+++ b/Mastodon/Extension/UINavigationController.swift
@@ -0,0 +1,17 @@
+//
+// UINavigationController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-31.
+//
+
+import UIKit
+
+// This not works!
+// SeeAlso: `AdaptiveStatusBarStyleNavigationController`
+extension UINavigationController {
+ open override var childForStatusBarStyle: UIViewController? {
+ assertionFailure("Won't enter here")
+ return visibleViewController
+ }
+}
diff --git a/Mastodon/Extension/UITabBarController.swift b/Mastodon/Extension/UITabBarController.swift
new file mode 100644
index 000000000..9b1d91292
--- /dev/null
+++ b/Mastodon/Extension/UITabBarController.swift
@@ -0,0 +1,14 @@
+//
+// UITabBarController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-31.
+//
+
+import UIKit
+
+extension UITabBarController {
+ open override var childForStatusBarStyle: UIViewController? {
+ return selectedViewController
+ }
+}
diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift
index cbb5c2940..241388199 100644
--- a/Mastodon/Generated/Assets.swift
+++ b/Mastodon/Generated/Assets.swift
@@ -38,6 +38,7 @@ internal enum Asset {
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
}
+ internal static let alertYellow = ColorAsset(name: "Colors/Background/alert.yellow")
internal static let dangerBorder = ColorAsset(name: "Colors/Background/danger.border")
internal static let danger = ColorAsset(name: "Colors/Background/danger")
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/media.type.indicotor")
diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift
index 98f08ef06..0ce6bf212 100644
--- a/Mastodon/Generated/Strings.swift
+++ b/Mastodon/Generated/Strings.swift
@@ -60,6 +60,8 @@ internal enum L10n {
internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue")
/// Discard
internal static let discard = L10n.tr("Localizable", "Common.Controls.Actions.Discard")
+ /// Done
+ internal static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done")
/// Edit
internal static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit")
/// OK
@@ -85,6 +87,22 @@ internal enum L10n {
/// Try Again
internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain")
}
+ internal enum Firendship {
+ /// Block
+ internal static let block = L10n.tr("Localizable", "Common.Controls.Firendship.Block")
+ /// Blocked
+ internal static let blocked = L10n.tr("Localizable", "Common.Controls.Firendship.Blocked")
+ /// Edit info
+ internal static let editInfo = L10n.tr("Localizable", "Common.Controls.Firendship.EditInfo")
+ /// Follow
+ internal static let follow = L10n.tr("Localizable", "Common.Controls.Firendship.Follow")
+ /// Following
+ internal static let following = L10n.tr("Localizable", "Common.Controls.Firendship.Following")
+ /// Mute
+ internal static let mute = L10n.tr("Localizable", "Common.Controls.Firendship.Mute")
+ /// Muted
+ internal static let muted = L10n.tr("Localizable", "Common.Controls.Firendship.Muted")
+ }
internal enum Status {
/// Tap to reveal that may be sensitive
internal static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning")
@@ -263,6 +281,24 @@ internal enum L10n {
internal static let publishing = L10n.tr("Localizable", "Scene.HomeTimeline.NavigationBarState.Publishing")
}
}
+ internal enum Profile {
+ internal enum Dashboard {
+ /// followers
+ internal static let followers = L10n.tr("Localizable", "Scene.Profile.Dashboard.Followers")
+ /// following
+ internal static let following = L10n.tr("Localizable", "Scene.Profile.Dashboard.Following")
+ /// posts
+ internal static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts")
+ }
+ internal enum SegmentedControl {
+ /// Media
+ internal static let media = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Media")
+ /// Posts
+ internal static let posts = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Posts")
+ /// Replies
+ internal static let replies = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Replies")
+ }
+ }
internal enum PublicTimeline {
/// Public
internal static let title = L10n.tr("Localizable", "Scene.PublicTimeline.Title")
diff --git a/Mastodon/Helper/MastodonField.swift b/Mastodon/Helper/MastodonField.swift
new file mode 100644
index 000000000..cbe87c09b
--- /dev/null
+++ b/Mastodon/Helper/MastodonField.swift
@@ -0,0 +1,48 @@
+//
+// MastodonField.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import Foundation
+import ActiveLabel
+
+enum MastodonField {
+
+ static func parse(field string: String) -> ParseResult {
+ let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+))")
+ let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
+ let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
+
+ var entities: [ActiveEntity] = []
+
+ for match in mentionMatches {
+ guard let text = string.substring(with: match, at: 0) else { continue }
+ let entity = ActiveEntity(range: match.range, type: .mention(text, userInfo: nil))
+ entities.append(entity)
+ }
+
+ for match in hashtagMatches {
+ guard let text = string.substring(with: match, at: 0) else { continue }
+ let entity = ActiveEntity(range: match.range, type: .hashtag(text, userInfo: nil))
+ entities.append(entity)
+ }
+
+ for match in urlMatches {
+ guard let text = string.substring(with: match, at: 0) else { continue }
+ let entity = ActiveEntity(range: match.range, type: .url(text, trimmed: text, url: text, userInfo: nil))
+ entities.append(entity)
+ }
+
+ return ParseResult(value: string, activeEntities: entities)
+ }
+
+}
+
+extension MastodonField {
+ struct ParseResult {
+ let value: String
+ let activeEntities: [ActiveEntity]
+ }
+}
diff --git a/Mastodon/Helper/TootContent.swift b/Mastodon/Helper/MastodonStatusContent.swift
similarity index 83%
rename from Mastodon/Helper/TootContent.swift
rename to Mastodon/Helper/MastodonStatusContent.swift
index 55f71beac..5b535b806 100755
--- a/Mastodon/Helper/TootContent.swift
+++ b/Mastodon/Helper/MastodonStatusContent.swift
@@ -1,5 +1,5 @@
//
-// TootContent.swift
+// MastodonStatusContent.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/1.
@@ -9,15 +9,15 @@ import Foundation
import Kanna
import ActiveLabel
-enum TootContent {
+enum MastodonStatusContent {
- static func parse(toot: String) throws -> TootContent.ParseResult {
- let toot = toot.replacingOccurrences(of: "
", with: "\n")
- let rootNode = try Node.parse(document: toot)
+ static func parse(status: String) throws -> MastodonStatusContent.ParseResult {
+ let status = status.replacingOccurrences(of: "
", with: "\n")
+ let rootNode = try Node.parse(document: status)
let text = String(rootNode.text)
var activeEntities: [ActiveEntity] = []
- let entities = TootContent.Node.entities(in: rootNode)
+ let entities = MastodonStatusContent.Node.entities(in: rootNode)
for entity in entities {
let range = NSRange(entity.text.startIndex.. TootContent.Node {
+ static func parse(document: String) throws -> MastodonStatusContent.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
+ let children: [MastodonStatusContent.Node] = body.flatMap { body in
return Node.parse(element: body, parentText: text[...], parentLevel: level + 1)
} ?? []
let node = Node(
@@ -253,32 +252,32 @@ extension TootContent {
}
-extension TootContent.Node {
+extension MastodonStatusContent.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 entities(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
+ return MastodonStatusContent.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 hashtags(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
+ return MastodonStatusContent.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 mentions(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
+ return MastodonStatusContent.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 }
+ static func urls(in node: MastodonStatusContent.Node) -> [MastodonStatusContent.Node] {
+ return MastodonStatusContent.Node.collect(node: node) { node in node.type == .url }
}
}
-extension TootContent.Node: CustomDebugStringConvertible {
+extension MastodonStatusContent.Node: CustomDebugStringConvertible {
var debugDescription: String {
let linkInfo: String = {
switch (href, hrefEllipsis) {
diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist
index e3d8d4a91..266cc9424 100644
--- a/Mastodon/Info.plist
+++ b/Mastodon/Info.plist
@@ -73,7 +73,5 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
- UIViewControllerBasedStatusBarAppearance
-
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift
index f3d31ff33..ffaa29b52 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider+StatusTableViewCellDelegate.swift
@@ -13,6 +13,19 @@ import CoreDataStack
import MastodonSDK
import ActiveLabel
+// MARK: - StatusViewDelegate
+extension StatusTableViewCellDelegate where Self: StatusProvider {
+
+ func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
+ StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .secondary, provider: self, cell: cell)
+ }
+
+ func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarButtonDidPressed button: UIButton) {
+ StatusProviderFacade.coordinateToStatusAuthorProfileScene(for: .primary, provider: self, cell: cell)
+ }
+
+}
+
// MARK: - ActionToolbarContainerDelegate
extension StatusTableViewCellDelegate where Self: StatusProvider {
@@ -31,7 +44,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
switch item {
case .homeTimelineIndex(_, let attribute):
attribute.isStatusTextSensitive = false
- case .toot(_, let attribute):
+ case .status(_, let attribute):
attribute.isStatusTextSensitive = false
default:
return
@@ -66,7 +79,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
switch item {
case .homeTimelineIndex(_, let attribute):
attribute.isStatusSensitive = false
- case .toot(_, let attribute):
+ case .status(_, let attribute):
attribute.isStatusSensitive = false
default:
return
@@ -89,16 +102,16 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton) {
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
- toot(for: cell, indexPath: nil)
+ status(for: cell, indexPath: nil)
.receive(on: DispatchQueue.main)
.setFailureType(to: Error.self)
- .compactMap { toot -> AnyPublisher, Error>? in
- guard let toot = (toot?.reblog ?? toot) else { return nil }
- guard let poll = toot.poll else { return nil }
+ .compactMap { status -> AnyPublisher, Error>? in
+ guard let status = (status?.reblog ?? status) else { return nil }
+ guard let poll = status.poll else { return nil }
let votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
let choices = votedOptions.map { $0.index.intValue }
- let domain = poll.toot.domain
+ let domain = poll.status.domain
button.isEnabled = false
@@ -137,7 +150,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
let poll = option.poll
let pollObjectID = option.poll.objectID
- let domain = poll.toot.domain
+ let domain = poll.status.domain
if poll.multiple {
var votedOptions = poll.options.filter { ($0.votedBy ?? Set()).contains(where: { $0.id == activeMastodonAuthenticationBox.userID }) }
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift
index 20f8e30a9..a0aaa543e 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift
@@ -11,7 +11,7 @@ import CoreDataStack
extension StatusTableViewCellDelegate where Self: StatusProvider {
func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
- // prefetch reply toot
+ // prefetch reply status
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let domain = activeMastodonAuthenticationBox.domain
@@ -20,8 +20,8 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
- statusObjectIDs.append(homeTimelineIndex.toot.objectID)
- case .toot(let objectID, _):
+ statusObjectIDs.append(homeTimelineIndex.status.objectID)
+ case .status(let objectID, _):
statusObjectIDs.append(objectID)
default:
continue
@@ -32,15 +32,15 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
backgroundManagedObjectContext.perform { [weak self] in
guard let self = self else { return }
for objectID in statusObjectIDs {
- let toot = backgroundManagedObjectContext.object(with: objectID) as! Toot
- guard let replyToID = toot.inReplyToID, toot.replyTo == nil else {
+ let status = backgroundManagedObjectContext.object(with: objectID) as! Status
+ guard let replyToID = status.inReplyToID, status.replyTo == nil else {
// skip
continue
}
self.context.statusPrefetchingService.prefetchReplyTo(
domain: domain,
- statusObjectID: toot.objectID,
- statusID: toot.id,
+ statusObjectID: status.objectID,
+ statusID: status.id,
replyToStatusID: replyToID,
authorizationBox: activeMastodonAuthenticationBox
)
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
index 89ae8e6eb..baaa708a1 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift
@@ -17,15 +17,15 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
// }
func handleTableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
- // update poll when toot appear
+ // update poll when status appear
let now = Date()
var pollID: Mastodon.Entity.Poll.ID?
- toot(for: cell, indexPath: indexPath)
- .compactMap { [weak self] toot -> AnyPublisher, Error>? in
+ status(for: cell, indexPath: indexPath)
+ .compactMap { [weak self] status -> AnyPublisher, Error>? in
guard let self = self else { return nil }
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return nil }
- guard let toot = (toot?.reblog ?? toot) else { return nil }
- guard let poll = toot.poll else { return nil }
+ guard let status = (status?.reblog ?? status) else { return nil }
+ guard let poll = status.poll else { return nil }
pollID = poll.id
// not expired AND last update > 60s
@@ -46,7 +46,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info update…", (#file as NSString).lastPathComponent, #line, #function, poll.id)
return self.context.apiService.poll(
- domain: toot.domain,
+ domain: status.domain,
pollID: poll.id,
pollObjectID: poll.objectID,
mastodonAuthenticationBox: authenticationBox
@@ -68,11 +68,11 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
})
.store(in: &disposeBag)
- toot(for: cell, indexPath: indexPath)
- .sink { [weak self] toot in
+ status(for: cell, indexPath: indexPath)
+ .sink { [weak self] status in
guard let self = self else { return }
- let toot = toot?.reblog ?? toot
- guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
+ let status = status?.reblog ?? status
+ guard let media = (status?.mediaAttachments ?? Set()).first else { return }
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
DispatchQueue.main.async {
@@ -85,17 +85,17 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
func handleTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// os_log("%{public}s[%{public}ld], %{public}s: indexPath %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
- toot(for: cell, indexPath: indexPath)
- .sink { [weak self] toot in
+ status(for: cell, indexPath: indexPath)
+ .sink { [weak self] status in
guard let self = self else { return }
- guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
+ guard let media = (status?.mediaAttachments ?? Set()).first else { return }
if let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) {
DispatchQueue.main.async {
videoPlayerViewModel.didEndDisplaying()
}
}
- if let currentAudioAttachment = self.context.audioPlaybackService.attachment, let _ = toot?.mediaAttachments?.contains(currentAudioAttachment) {
+ if let currentAudioAttachment = self.context.audioPlaybackService.attachment, let _ = status?.mediaAttachments?.contains(currentAudioAttachment) {
self.context.audioPlaybackService.pause()
}
}
diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider.swift b/Mastodon/Protocol/StatusProvider/StatusProvider.swift
index 4ec0d1e16..e16343ee6 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProvider.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProvider.swift
@@ -12,9 +12,9 @@ import CoreDataStack
protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
// async
- func toot() -> Future
- func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future
- func toot(for cell: UICollectionViewCell) -> Future
+ func status() -> Future
+ func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future
+ func status(for cell: UICollectionViewCell) -> Future
// sync
var managedObjectContext: NSManagedObjectContext { get }
diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
index cab16e68c..19b0fbf7e 100644
--- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
+++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift
@@ -13,8 +13,53 @@ import CoreDataStack
import MastodonSDK
import ActiveLabel
-enum StatusProviderFacade {
+enum StatusProviderFacade { }
+extension StatusProviderFacade {
+
+ static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider) {
+ _coordinateToStatusAuthorProfileScene(
+ for: target,
+ provider: provider,
+ status: provider.status()
+ )
+ }
+
+ static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
+ _coordinateToStatusAuthorProfileScene(
+ for: target,
+ provider: provider,
+ status: provider.status(for: cell, indexPath: nil)
+ )
+ }
+
+ private static func _coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, status: Future) {
+ status
+ .sink { [weak provider] status in
+ guard let provider = provider else { return }
+ let _status: Status? = {
+ switch target {
+ case .primary: return status?.reblog ?? status // original status
+ case .secondary: return status?.replyTo ?? status // reblog or reply to status
+ }
+ }()
+ guard let status = _status else { return }
+
+ let mastodonUser = status.author
+ let profileViewModel = CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
+ DispatchQueue.main.async {
+ if provider.navigationController == nil {
+ let from = provider.presentingViewController ?? provider
+ provider.dismiss(animated: true) {
+ provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
+ }
+ } else {
+ provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
+ }
+ }
+ }
+ .store(in: &provider.disposeBag)
+ }
}
extension StatusProviderFacade {
@@ -22,18 +67,18 @@ extension StatusProviderFacade {
static func responseToStatusLikeAction(provider: StatusProvider) {
_responseToStatusLikeAction(
provider: provider,
- toot: provider.toot()
+ status: provider.status()
)
}
static func responseToStatusLikeAction(provider: StatusProvider, cell: UITableViewCell) {
_responseToStatusLikeAction(
provider: provider,
- toot: provider.toot(for: cell, indexPath: nil)
+ status: provider.status(for: cell, indexPath: nil)
)
}
- private static func _responseToStatusLikeAction(provider: StatusProvider, toot: Future) {
+ private static func _responseToStatusLikeAction(provider: StatusProvider, status: Future) {
// prepare authentication
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
@@ -55,22 +100,22 @@ extension StatusProviderFacade {
let generator = UIImpactFeedbackGenerator(style: .light)
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
- toot
- .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
- guard let toot = toot?.reblog ?? toot else { return nil }
+ status
+ .compactMap { status -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
+ guard let status = status?.reblog ?? status else { return nil }
let favoriteKind: Mastodon.API.Favorites.FavoriteKind = {
- let isLiked = toot.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
+ let isLiked = status.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
return isLiked ? .destroy : .create
}()
- return (toot.objectID, favoriteKind)
+ return (status.objectID, favoriteKind)
}
- .map { tootObjectID, favoriteKind -> AnyPublisher<(Toot.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
+ .map { statusObjectID, favoriteKind -> AnyPublisher<(Status.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
return context.apiService.like(
- tootObjectID: tootObjectID,
+ statusObjectID: statusObjectID,
mastodonUserObjectID: mastodonUserObjectID,
favoriteKind: favoriteKind
)
- .map { tootID in (tootID, favoriteKind) }
+ .map { statusID in (statusID, favoriteKind) }
.eraseToAnyPublisher()
}
.setFailureType(to: Error.self)
@@ -82,7 +127,7 @@ extension StatusProviderFacade {
responseFeedbackGenerator.prepare()
} receiveOutput: { _, favoriteKind in
generator.impactOccurred()
- os_log("%{public}s[%{public}ld], %{public}s: [Like] update local toot like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike")
+ os_log("%{public}s[%{public}ld], %{public}s: [Like] update local status like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike")
} receiveCompletion: { completion in
switch completion {
case .failure:
@@ -92,9 +137,9 @@ extension StatusProviderFacade {
break
}
}
- .map { tootID, favoriteKind in
+ .map { statusID, favoriteKind in
return context.apiService.like(
- statusID: tootID,
+ statusID: statusID,
favoriteKind: favoriteKind,
mastodonAuthenticationBox: activeMastodonAuthenticationBox
)
@@ -126,18 +171,18 @@ extension StatusProviderFacade {
static func responseToStatusReblogAction(provider: StatusProvider) {
_responseToStatusReblogAction(
provider: provider,
- toot: provider.toot()
+ status: provider.status()
)
}
static func responseToStatusReblogAction(provider: StatusProvider, cell: UITableViewCell) {
_responseToStatusReblogAction(
provider: provider,
- toot: provider.toot(for: cell, indexPath: nil)
+ status: provider.status(for: cell, indexPath: nil)
)
}
- private static func _responseToStatusReblogAction(provider: StatusProvider, toot: Future) {
+ private static func _responseToStatusReblogAction(provider: StatusProvider, status: Future) {
// prepare authentication
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
@@ -159,22 +204,22 @@ extension StatusProviderFacade {
let generator = UIImpactFeedbackGenerator(style: .light)
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
- toot
- .compactMap { toot -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in
- guard let toot = toot?.reblog ?? toot else { return nil }
+ status
+ .compactMap { status -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in
+ guard let status = status?.reblog ?? status else { return nil }
let reblogKind: Mastodon.API.Reblog.ReblogKind = {
- let isReblogged = toot.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
+ let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
return isReblogged ? .undoReblog : .reblog(query: .init(visibility: nil))
}()
- return (toot.objectID, reblogKind)
+ return (status.objectID, reblogKind)
}
- .map { tootObjectID, reblogKind -> AnyPublisher<(Toot.ID, Mastodon.API.Reblog.ReblogKind), Error> in
+ .map { statusObjectID, reblogKind -> AnyPublisher<(Status.ID, Mastodon.API.Reblog.ReblogKind), Error> in
return context.apiService.reblog(
- tootObjectID: tootObjectID,
+ statusObjectID: statusObjectID,
mastodonUserObjectID: mastodonUserObjectID,
reblogKind: reblogKind
)
- .map { tootID in (tootID, reblogKind) }
+ .map { statusID in (statusID, reblogKind) }
.eraseToAnyPublisher()
}
.setFailureType(to: Error.self)
@@ -188,9 +233,9 @@ extension StatusProviderFacade {
generator.impactOccurred()
switch reblogKind {
case .reblog:
- os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog")
+ os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog")
case .undoReblog:
- os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local toot reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog")
+ os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog")
}
} receiveCompletion: { completion in
switch completion {
@@ -201,9 +246,9 @@ extension StatusProviderFacade {
break
}
}
- .map { tootID, reblogKind in
+ .map { statusID, reblogKind in
return context.apiService.reblog(
- statusID: tootID,
+ statusID: statusID,
reblogKind: reblogKind,
mastodonAuthenticationBox: activeMastodonAuthenticationBox
)
@@ -231,8 +276,8 @@ extension StatusProviderFacade {
extension StatusProviderFacade {
enum Target {
- case toot
- case reblog
+ case primary // original
+ case secondary // attachment reblog or reply
}
}
diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Background/alert.yellow.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Background/alert.yellow.colorset/Contents.json
new file mode 100644
index 000000000..29b7bba3d
--- /dev/null
+++ b/Mastodon/Resources/Assets.xcassets/Colors/Background/alert.yellow.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.016",
+ "green" : "0.561",
+ "red" : "0.792"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Mastodon/Resources/en.lproj/InfoPlist.strings b/Mastodon/Resources/en.lproj/InfoPlist.strings
index 972e1a7a2..48566ae36 100644
--- a/Mastodon/Resources/en.lproj/InfoPlist.strings
+++ b/Mastodon/Resources/en.lproj/InfoPlist.strings
@@ -1,2 +1,2 @@
-"NSCameraUsageDescription" = "Used to take photo for toot";
+"NSCameraUsageDescription" = "Used to take photo for post status";
"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library";
\ No newline at end of file
diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings
index db808b56d..f0ac3d44b 100644
--- a/Mastodon/Resources/en.lproj/Localizable.strings
+++ b/Mastodon/Resources/en.lproj/Localizable.strings
@@ -15,6 +15,7 @@ Please check your internet connection.";
"Common.Controls.Actions.Confirm" = "Confirm";
"Common.Controls.Actions.Continue" = "Continue";
"Common.Controls.Actions.Discard" = "Discard";
+"Common.Controls.Actions.Done" = "Done";
"Common.Controls.Actions.Edit" = "Edit";
"Common.Controls.Actions.Ok" = "OK";
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
@@ -27,6 +28,13 @@ Please check your internet connection.";
"Common.Controls.Actions.SignUp" = "Sign Up";
"Common.Controls.Actions.TakePhoto" = "Take photo";
"Common.Controls.Actions.TryAgain" = "Try Again";
+"Common.Controls.Firendship.Block" = "Block";
+"Common.Controls.Firendship.Blocked" = "Blocked";
+"Common.Controls.Firendship.EditInfo" = "Edit info";
+"Common.Controls.Firendship.Follow" = "Follow";
+"Common.Controls.Firendship.Following" = "Following";
+"Common.Controls.Firendship.Mute" = "Mute";
+"Common.Controls.Firendship.Muted" = "Muted";
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
"Common.Controls.Status.Poll.Closed" = "Closed";
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
@@ -85,6 +93,12 @@ tap the link to confirm your account.";
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
"Scene.HomeTimeline.Title" = "Home";
+"Scene.Profile.Dashboard.Followers" = "followers";
+"Scene.Profile.Dashboard.Following" = "following";
+"Scene.Profile.Dashboard.Posts" = "posts";
+"Scene.Profile.SegmentedControl.Media" = "Media";
+"Scene.Profile.SegmentedControl.Posts" = "Posts";
+"Scene.Profile.SegmentedControl.Replies" = "Replies";
"Scene.PublicTimeline.Title" = "Public";
"Scene.Register.Error.Item.Agreement" = "Agreement";
"Scene.Register.Error.Item.Email" = "Email";
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToTootContentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift
similarity index 62%
rename from Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToTootContentCollectionViewCell.swift
rename to Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift
index fe00563df..0163a54cd 100644
--- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToTootContentCollectionViewCell.swift
+++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift
@@ -1,5 +1,5 @@
//
-// ComposeRepliedToTootContentCollectionViewCell.swift
+// ComposeRepliedToStatusContentCollectionViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-11.
@@ -7,7 +7,7 @@
import UIKit
-final class ComposeRepliedToTootContentCollectionViewCell: UICollectionViewCell {
+final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
@@ -21,7 +21,7 @@ final class ComposeRepliedToTootContentCollectionViewCell: UICollectionViewCell
}
-extension ComposeRepliedToTootContentCollectionViewCell {
+extension ComposeRepliedToStatusContentCollectionViewCell {
private func _init() {
diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift
index ec46e2caf..c316e993e 100644
--- a/Mastodon/Scene/Compose/ComposeViewController.swift
+++ b/Mastodon/Scene/Compose/ComposeViewController.swift
@@ -43,7 +43,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
let collectionView: UICollectionView = {
let collectionViewLayout = ComposeViewController.createLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
- collectionView.register(ComposeRepliedToTootContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self))
+ collectionView.register(ComposeRepliedToStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self))
collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self))
collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
index 6f289b339..785d97264 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
@@ -65,7 +65,7 @@ extension HomeTimelineViewController {
guard let self = self else { return }
self.moveToFirstVideoStatus(action)
}),
- UIAction(title: "First GIF Toot", image: nil, attributes: [], handler: { [weak self] action in
+ UIAction(title: "First GIF status", image: nil, attributes: [], handler: { [weak self] action in
guard let self = self else { return }
self.moveToFirstGIFStatus(action)
}),
@@ -112,7 +112,7 @@ extension HomeTimelineViewController {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
- return homeTimelineIndex.toot.reblog != nil
+ return homeTimelineIndex.status.reblog != nil
default:
return false
}
@@ -132,7 +132,7 @@ extension HomeTimelineViewController {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
- let post = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
+ let post = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
return post.poll != nil
default:
return false
@@ -153,7 +153,7 @@ extension HomeTimelineViewController {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
- guard homeTimelineIndex.toot.inReplyToID != nil else {
+ guard homeTimelineIndex.status.inReplyToID != nil else {
return false
}
return true
@@ -176,8 +176,8 @@ extension HomeTimelineViewController {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
- let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
- return toot.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false
+ let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
+ return status.mediaAttachments?.contains(where: { $0.type == .audio }) ?? false
default:
return false
}
@@ -186,7 +186,7 @@ extension HomeTimelineViewController {
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
tableView.blinkRow(at: IndexPath(row: index, section: 0))
} else {
- print("Not found audio toot")
+ print("Not found audio status")
}
}
@@ -197,8 +197,8 @@ extension HomeTimelineViewController {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
- let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
- return toot.mediaAttachments?.contains(where: { $0.type == .video }) ?? false
+ let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
+ return status.mediaAttachments?.contains(where: { $0.type == .video }) ?? false
default:
return false
}
@@ -218,8 +218,8 @@ extension HomeTimelineViewController {
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
- let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
- return toot.mediaAttachments?.contains(where: { $0.type == .gifv }) ?? false
+ let status = homeTimelineIndex.status.reblog ?? homeTimelineIndex.status
+ return status.mediaAttachments?.contains(where: { $0.type == .gifv }) ?? false
default:
return false
}
@@ -242,12 +242,12 @@ extension HomeTimelineViewController {
default: return nil
}
}
- var droppingTootObjectIDs: [NSManagedObjectID] = []
+ var droppingStatusObjectIDs: [NSManagedObjectID] = []
context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
guard let self = self else { return }
for objectID in droppingObjectIDs {
guard let homeTimelineIndex = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue }
- droppingTootObjectIDs.append(homeTimelineIndex.toot.objectID)
+ droppingStatusObjectIDs.append(homeTimelineIndex.status.objectID)
self.context.apiService.backgroundManagedObjectContext.delete(homeTimelineIndex)
}
}
@@ -257,8 +257,8 @@ extension HomeTimelineViewController {
case .success:
self.context.apiService.backgroundManagedObjectContext.performChanges { [weak self] in
guard let self = self else { return }
- for objectID in droppingTootObjectIDs {
- guard let post = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Toot else { continue }
+ for objectID in droppingStatusObjectIDs {
+ guard let post = try? self.context.apiService.backgroundManagedObjectContext.existingObject(with: objectID) as? Status else { continue }
self.context.apiService.backgroundManagedObjectContext.delete(post)
}
}
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift
index dc8eb4803..9e1915301 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+StatusProvider.swift
@@ -14,11 +14,11 @@ import CoreDataStack
// MARK: - StatusProvider
extension HomeTimelineViewController: StatusProvider {
- func toot() -> Future {
+ func status() -> Future {
return Future { promise in promise(.success(nil)) }
}
- func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future {
+ func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future {
return Future { promise in
guard let diffableDataSource = self.viewModel.diffableDataSource else {
assertionFailure()
@@ -36,7 +36,7 @@ extension HomeTimelineViewController: StatusProvider {
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
managedObjectContext.perform {
let timelineIndex = managedObjectContext.object(with: objectID) as? HomeTimelineIndex
- promise(.success(timelineIndex?.toot))
+ promise(.success(timelineIndex?.status))
}
default:
promise(.success(nil))
@@ -44,7 +44,7 @@ extension HomeTimelineViewController: StatusProvider {
}
}
- func toot(for cell: UICollectionViewCell) -> Future {
+ func status(for cell: UICollectionViewCell) -> Future {
return Future { promise in promise(.success(nil)) }
}
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
index 078b7b445..1f3dea81a 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
@@ -175,6 +175,13 @@ extension HomeTimelineViewController {
}
.store(in: &disposeBag)
}
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ // needs trigger manually after onboarding dismiss
+ setNeedsStatusBarAppearanceUpdate()
+ }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
@@ -313,7 +320,7 @@ extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControl
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
- func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID: NSManagedObjectID?) {
+ func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID: NSManagedObjectID?) {
guard let upperTimelineIndexObjectID = timelineIndexobjectID else {
return
}
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift
index 4a34b922a..5f16a18eb 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift
@@ -31,6 +31,10 @@ extension HomeTimelineViewModel {
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
)
+
+// var snapshot = NSDiffableDataSourceSnapshot()
+// snapshot.appendSections([.main])
+// diffableDataSource?.apply(snapshot)
}
}
@@ -83,12 +87,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
var newTimelineItems: [Item] = []
for (i, timelineIndex) in timelineIndexes.enumerated() {
- let toot = timelineIndex.toot.reblog ?? timelineIndex.toot
- let isStatusTextSensitive: Bool = {
- guard let spoilerText = toot.spoilerText, !spoilerText.isEmpty else { return false }
- return true
- }()
- let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: toot.sensitive)
+ let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute()
// append new item into snapshot
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift
index 80c86a006..640d9df3b 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift
@@ -55,7 +55,7 @@ extension HomeTimelineViewModel.LoadLatestState {
managedObjectContext.perform {
let start = CACurrentMediaTime()
- let latestTootIDs: [Toot.ID]
+ let latestStatusIDs: [Status.ID]
let request = HomeTimelineIndex.sortedFetchRequest
request.returnsObjectsAsFaults = false
request.predicate = predicate
@@ -64,10 +64,10 @@ extension HomeTimelineViewModel.LoadLatestState {
let timelineIndexes = try managedObjectContext.fetch(request)
let endFetch = CACurrentMediaTime()
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect timelineIndexes cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, endFetch - start)
- latestTootIDs = timelineIndexes
- .prefix(APIService.onceRequestTootMaxCount) // avoid performance issue
+ latestStatusIDs = timelineIndexes
+ .prefix(APIService.onceRequestStatusMaxCount) // avoid performance issue
.compactMap { timelineIndex in
- timelineIndex.value(forKeyPath: #keyPath(HomeTimelineIndex.toot.id)) as? Toot.ID
+ timelineIndex.value(forKeyPath: #keyPath(HomeTimelineIndex.status.id)) as? Status.ID
}
} catch {
stateMachine.enter(Fail.self)
@@ -75,7 +75,7 @@ extension HomeTimelineViewModel.LoadLatestState {
}
let end = CACurrentMediaTime()
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect toots id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect statuses id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
// TODO: only set large count when using Wi-Fi
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
@@ -86,7 +86,7 @@ extension HomeTimelineViewModel.LoadLatestState {
case .failure(let error):
// TODO: handle error
viewModel.isFetchingLatestTimeline.value = false
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
case .finished:
// handle isFetchingLatestTimeline in fetch controller delegate
break
@@ -95,15 +95,15 @@ extension HomeTimelineViewModel.LoadLatestState {
stateMachine.enter(Idle.self)
} receiveValue: { response in
- // stop refresher if no new toots
- let toots = response.value
- let newToots = toots.filter { !latestTootIDs.contains($0.id) }
- os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new toots", ((#file as NSString).lastPathComponent), #line, #function, newToots.count)
+ // stop refresher if no new statuses
+ let statuses = response.value
+ let newStatuses = statuses.filter { !latestStatusIDs.contains($0.id) }
+ os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new statuses", ((#file as NSString).lastPathComponent), #line, #function, newStatuses.count)
- if newToots.isEmpty {
+ if newStatuses.isEmpty {
viewModel.isFetchingLatestTimeline.value = false
} else {
- if !latestTootIDs.isEmpty {
+ if !latestStatusIDs.isEmpty {
viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming()
}
}
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift
index 07b6abf17..b5b9e4ceb 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadMiddleState.swift
@@ -58,12 +58,12 @@ extension HomeTimelineViewModel.LoadMiddleState {
stateMachine.enter(Fail.self)
return
}
- let tootIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { timelineIndex in
- timelineIndex.toot.id
+ let statusIDs = (viewModel.fetchedResultsController.fetchedObjects ?? []).compactMap { timelineIndex in
+ timelineIndex.status.id
}
// TODO: only set large count when using Wi-Fi
- let maxID = timelineIndex.toot.id
+ let maxID = timelineIndex.status.id
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain,maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
@@ -72,16 +72,16 @@ extension HomeTimelineViewModel.LoadMiddleState {
switch completion {
case .failure(let error):
// TODO: handle error
- os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
+ os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
stateMachine.enter(Fail.self)
case .finished:
break
}
} receiveValue: { response in
- let toots = response.value
- let newToots = toots.filter { !tootIDs.contains($0.id) }
- os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld toots, %{public}%ld new toots", ((#file as NSString).lastPathComponent), #line, #function, toots.count, newToots.count)
- if newToots.isEmpty {
+ let statuses = response.value
+ let newStatuses = statuses.filter { !statusIDs.contains($0.id) }
+ os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld statuses, %{public}%ld new statuses", ((#file as NSString).lastPathComponent), #line, #function, statuses.count, newStatuses.count)
+ if newStatuses.isEmpty {
stateMachine.enter(Fail.self)
} else {
stateMachine.enter(Success.self)
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift
index 341183dcf..aaabd7a8b 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift
@@ -53,7 +53,7 @@ extension HomeTimelineViewModel.LoadOldestState {
}
// TODO: only set large count when using Wi-Fi
- let maxID = last.toot.id
+ let maxID = last.status.id
viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, maxID: maxID, authorizationBox: activeMastodonAuthenticationBox)
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
@@ -61,15 +61,15 @@ extension HomeTimelineViewModel.LoadOldestState {
viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion)
switch completion {
case .failure(let error):
- os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
+ os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
case .finished:
// handle isFetchingLatestTimeline in fetch controller delegate
break
}
} receiveValue: { response in
- let toots = response.value
- // enter no more state when no new toots
- if toots.isEmpty || (toots.count == 1 && toots[0].id == maxID) {
+ let statuses = response.value
+ // enter no more state when no new statuses
+ if statuses.isEmpty || (statuses.count == 1 && statuses[0].id == maxID) {
stateMachine.enter(NoMore.self)
} else {
stateMachine.enter(Idle.self)
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
index 7b9c35308..1c0ddf71b 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
@@ -74,7 +74,7 @@ final class HomeTimelineViewModel: NSObject {
let fetchRequest = HomeTimelineIndex.sortedFetchRequest
fetchRequest.fetchBatchSize = 20
fetchRequest.returnsObjectsAsFaults = false
- fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.toot)]
+ fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.status)]
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext,
diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift
index 72f62528a..9d609234f 100644
--- a/Mastodon/Scene/MainTab/MainTabBarController.swift
+++ b/Mastodon/Scene/MainTab/MainTabBarController.swift
@@ -63,10 +63,11 @@ class MainTabBarController: UITabBarController {
let _viewController = ProfileViewController()
_viewController.context = context
_viewController.coordinator = coordinator
+ _viewController.viewModel = MeProfileViewModel(context: context)
viewController = _viewController
}
viewController.title = self.title
- return UINavigationController(rootViewController: viewController)
+ return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
}
}
@@ -84,6 +85,11 @@ class MainTabBarController: UITabBarController {
extension MainTabBarController {
+
+ open override var childForStatusBarStyle: UIViewController? {
+ return selectedViewController
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
@@ -100,9 +106,9 @@ extension MainTabBarController {
selectedIndex = 0
// TODO: custom accent color
- let tabBarAppearance = UITabBarAppearance()
- tabBarAppearance.configureWithDefaultBackground()
- tabBar.standardAppearance = tabBarAppearance
+// let tabBarAppearance = UITabBarAppearance()
+// tabBarAppearance.configureWithDefaultBackground()
+// tabBar.standardAppearance = tabBarAppearance
context.apiService.error
.receive(on: DispatchQueue.main)
@@ -151,7 +157,7 @@ extension MainTabBarController {
.store(in: &disposeBag)
#if DEBUG
- // selectedIndex = 1
+ // selectedIndex = 3
#endif
}
diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift
index dee1510cd..338be6ab6 100644
--- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift
+++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift
@@ -66,6 +66,10 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc
}
extension MastodonConfirmEmailViewController {
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .darkContent
+ }
override func viewDidLoad() {
diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift
index 53f9a915a..721aae9bc 100644
--- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift
+++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift
@@ -57,6 +57,10 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
extension MastodonPickServerViewController {
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .darkContent
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
index 6079f4d0c..6439ea42b 100644
--- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
+++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
@@ -235,6 +235,10 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
extension MastodonRegisterViewController {
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .darkContent
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
diff --git a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift
index 25b9ca402..d97209317 100644
--- a/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift
+++ b/Mastodon/Scene/Onboarding/ResendEmail/MastodonResendEmailViewController.swift
@@ -40,6 +40,11 @@ final class MastodonResendEmailViewController: UIViewController, NeedsDependency
}
extension MastodonResendEmailViewController {
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .darkContent
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
@@ -59,6 +64,7 @@ extension MastodonResendEmailViewController {
webView.load(request)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: resendEmail via: %s", (#file as NSString).lastPathComponent, #line, #function, viewModel.resendEmailURL.debugDescription)
}
+
}
extension MastodonResendEmailViewController {
diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift
index edc4c59e1..b51d66b2b 100644
--- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift
+++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift
@@ -85,6 +85,10 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency
extension MastodonServerRulesViewController {
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .darkContent
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
index e415c5737..838f1327a 100644
--- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
+++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
@@ -64,6 +64,10 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
extension WelcomeViewController {
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .darkContent
+ }
+
override func viewDidLoad() {
super.viewDidLoad()
diff --git a/Mastodon/Scene/Profile/CachedProfileViewModel.swift b/Mastodon/Scene/Profile/CachedProfileViewModel.swift
new file mode 100644
index 000000000..0e5823d09
--- /dev/null
+++ b/Mastodon/Scene/Profile/CachedProfileViewModel.swift
@@ -0,0 +1,17 @@
+//
+// CachedProfileViewModel.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-31.
+//
+
+import Foundation
+import CoreDataStack
+
+final class CachedProfileViewModel: ProfileViewModel {
+
+ convenience init(context: AppContext, mastodonUser: MastodonUser) {
+ self.init(context: context, optionalMastodonUser: mastodonUser)
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
new file mode 100644
index 000000000..58a7a6110
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
@@ -0,0 +1,142 @@
+//
+// ProfileHeaderViewController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+
+protocol ProfileHeaderViewControllerDelegate: class {
+ func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
+ func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int)
+}
+
+final class ProfileHeaderViewController: UIViewController {
+
+ static let segmentedControlHeight: CGFloat = 32
+ static let segmentedControlMarginHeight: CGFloat = 20
+ static let headerMinHeight: CGFloat = segmentedControlHeight + 2 * segmentedControlMarginHeight
+
+ weak var delegate: ProfileHeaderViewControllerDelegate?
+
+ let profileBannerView = ProfileHeaderView()
+ let pageSegmentedControl: UISegmentedControl = {
+ let segmenetedControl = UISegmentedControl(items: ["A", "B"])
+ segmenetedControl.selectedSegmentIndex = 0
+ return segmenetedControl
+ }()
+
+ private var isBannerPinned = false
+ private var bottomShadowAlpha: CGFloat = 0.0
+
+ private var isAdjustBannerImageViewForSafeAreaInset = false
+ private var containerSafeAreaInset: UIEdgeInsets = .zero
+
+ deinit {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+extension ProfileHeaderViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+
+ profileBannerView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(profileBannerView)
+ NSLayoutConstraint.activate([
+ profileBannerView.topAnchor.constraint(equalTo: view.topAnchor),
+ profileBannerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ profileBannerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ ])
+ profileBannerView.preservesSuperviewLayoutMargins = true
+
+ pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(pageSegmentedControl)
+ NSLayoutConstraint.activate([
+ pageSegmentedControl.topAnchor.constraint(equalTo: profileBannerView.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
+ pageSegmentedControl.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
+ pageSegmentedControl.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
+ view.bottomAnchor.constraint(equalTo: pageSegmentedControl.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
+ pageSegmentedControl.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.segmentedControlHeight).priority(.defaultHigh),
+ ])
+
+ pageSegmentedControl.addTarget(self, action: #selector(ProfileHeaderViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ if !isAdjustBannerImageViewForSafeAreaInset {
+ isAdjustBannerImageViewForSafeAreaInset = true
+ profileBannerView.bannerImageView.frame.origin.y = -containerSafeAreaInset.top
+ profileBannerView.bannerImageView.frame.size.height += containerSafeAreaInset.top
+ }
+ }
+
+ override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
+
+ delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view)
+ view.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.12), alpha: Float(bottomShadowAlpha), x: 0, y: 2, blur: 2, spread: 0, roundedRect: view.bounds, byRoundingCorners: .allCorners, cornerRadii: .zero)
+ }
+
+}
+
+extension ProfileHeaderViewController {
+
+ @objc private func pageSegmentedControlValueChanged(_ sender: UISegmentedControl) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: selectedSegmentIndex: %ld", ((#file as NSString).lastPathComponent), #line, #function, sender.selectedSegmentIndex)
+ delegate?.profileHeaderViewController(self, pageSegmentedControlValueChanged: sender, selectedSegmentIndex: sender.selectedSegmentIndex)
+ }
+
+}
+
+extension ProfileHeaderViewController {
+
+ func updateHeaderContainerSafeAreaInset(_ inset: UIEdgeInsets) {
+ containerSafeAreaInset = inset
+ }
+
+ private func updateHeaderBottomShadow(progress: CGFloat) {
+ let alpha = min(max(0, 10 * progress - 9), 1)
+ if bottomShadowAlpha != alpha {
+ bottomShadowAlpha = alpha
+ view.setNeedsLayout()
+ }
+ }
+
+ func updateHeaderScrollProgress(_ progress: CGFloat) {
+ // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
+ updateHeaderBottomShadow(progress: progress)
+
+ let bannerImageView = profileBannerView.bannerImageView
+ guard bannerImageView.bounds != .zero else {
+ // wait layout finish
+ return
+ }
+
+ let bannerContainerInWindow = profileBannerView.convert(profileBannerView.bannerContainerView.frame, to: nil)
+ let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
+
+ if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
+ bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
+ bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
+ } else if bannerContainerBottomOffset < containerSafeAreaInset.top {
+ bannerImageView.frame.origin.y = -containerSafeAreaInset.top
+ let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
+ bannerImageView.frame.size.height = bannerImageHeight
+ } else {
+ bannerImageView.frame.origin.y = -containerSafeAreaInset.top
+ bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
+ }
+
+ // TODO: handle titleView
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift
new file mode 100644
index 000000000..e95697e5c
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift
@@ -0,0 +1,100 @@
+//
+// ProfileFieldView.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import UIKit
+import ActiveLabel
+
+final class ProfileFieldView: UIView {
+
+ let titleLabel: UILabel = {
+ let label = UILabel()
+ label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
+ label.textColor = Asset.Colors.Label.primary.color
+ label.text = "Title"
+ return label
+ }()
+
+ let valueActiveLabel: ActiveLabel = {
+ let label = ActiveLabel(style: .profileField)
+ label.configure(content: "value")
+ return label
+ }()
+
+ let topSeparatorLine = UIView.separatorLine
+ let bottomSeparatorLine = UIView.separatorLine
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ _init()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ _init()
+ }
+
+}
+
+extension ProfileFieldView {
+ private func _init() {
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(titleLabel)
+ NSLayoutConstraint.activate([
+ titleLabel.topAnchor.constraint(equalTo: topAnchor),
+ titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
+ titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
+ titleLabel.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
+ ])
+ titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+ valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(valueActiveLabel)
+ NSLayoutConstraint.activate([
+ valueActiveLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
+ valueActiveLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
+ valueActiveLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
+ ])
+ valueActiveLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+
+ topSeparatorLine.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(topSeparatorLine)
+ NSLayoutConstraint.activate([
+ topSeparatorLine.topAnchor.constraint(equalTo: topAnchor),
+ topSeparatorLine.leadingAnchor.constraint(equalTo: leadingAnchor),
+ topSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
+ topSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
+ ])
+
+ bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(bottomSeparatorLine)
+ NSLayoutConstraint.activate([
+ bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
+ bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
+ bottomSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
+ bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
+ ])
+ }
+}
+
+#if canImport(SwiftUI) && DEBUG
+import SwiftUI
+
+struct ProfileFieldView_Previews: PreviewProvider {
+
+ static var previews: some View {
+ UIViewPreview(width: 375) {
+ let filedView = ProfileFieldView()
+ filedView.valueActiveLabel.configure(field: "https://mastodon.online")
+ return filedView
+ }
+ .previewLayout(.fixed(width: 375, height: 100))
+ }
+
+}
+
+#endif
+
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFriendshipActionButton.swift b/Mastodon/Scene/Profile/Header/View/ProfileFriendshipActionButton.swift
new file mode 100644
index 000000000..286145ffd
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/View/ProfileFriendshipActionButton.swift
@@ -0,0 +1,71 @@
+//
+// ProfileFriendshipActionButton.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import UIKit
+
+final class ProfileFriendshipActionButton: RoundedEdgesButton {
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ _init()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ _init()
+ }
+
+}
+
+extension ProfileFriendshipActionButton {
+ private func _init() {
+ configure(state: .follow)
+ }
+}
+
+extension ProfileFriendshipActionButton {
+ enum State {
+ case follow
+ case following
+ case blocked
+ case muted
+ case edit
+ case editing
+
+ var title: String {
+ switch self {
+ case .follow: return L10n.Common.Controls.Firendship.follow
+ case .following: return L10n.Common.Controls.Firendship.following
+ case .blocked: return L10n.Common.Controls.Firendship.blocked
+ case .muted: return L10n.Common.Controls.Firendship.muted
+ case .edit: return L10n.Common.Controls.Firendship.editInfo
+ case .editing: return L10n.Common.Controls.Actions.done
+ }
+ }
+
+ var backgroundColor: UIColor {
+ switch self {
+ case .follow: return Asset.Colors.Button.normal.color
+ case .following: return Asset.Colors.Button.normal.color
+ case .blocked: return Asset.Colors.Background.danger.color
+ case .muted: return Asset.Colors.Background.alertYellow.color
+ case .edit: return Asset.Colors.Button.normal.color
+ case .editing: return Asset.Colors.Button.normal.color
+ }
+ }
+ }
+
+ private func configure(state: State) {
+ setTitle(state.title, for: .normal)
+ setTitleColor(.white, for: .normal)
+ setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted)
+ setBackgroundImage(.placeholder(color: state.backgroundColor), for: .normal)
+ setBackgroundImage(.placeholder(color: state.backgroundColor.withAlphaComponent(0.5)), for: .highlighted)
+ setBackgroundImage(.placeholder(color: state.backgroundColor.withAlphaComponent(0.5)), for: .disabled)
+ }
+}
+
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
new file mode 100644
index 000000000..7fac52896
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
@@ -0,0 +1,247 @@
+//
+// ProfileBannerView.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+import ActiveLabel
+
+protocol ProfileHeaderViewDelegate: class {
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity)
+
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView)
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView)
+}
+
+final class ProfileHeaderView: UIView {
+
+ static let avatarImageViewSize = CGSize(width: 56, height: 56)
+ static let avatarImageViewCornerRadius: CGFloat = 6
+ static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
+
+ weak var delegate: ProfileHeaderViewDelegate?
+
+ let bannerContainerView = UIView()
+ let bannerImageView: UIImageView = {
+ let imageView = UIImageView()
+ imageView.contentMode = .scaleAspectFill
+ imageView.image = .placeholder(color: .systemGray)
+ imageView.layer.masksToBounds = true
+ return imageView
+ }()
+
+ let avatarImageView: UIImageView = {
+ let imageView = UIImageView()
+ let placeholderImage = UIImage
+ .placeholder(size: ProfileHeaderView.avatarImageViewSize, color: Asset.Colors.Background.systemGroupedBackground.color)
+ .af.imageRounded(withCornerRadius: ProfileHeaderView.avatarImageViewCornerRadius, divideRadiusByImageScale: false)
+ imageView.image = placeholderImage
+ return imageView
+ }()
+
+ let nameLabel: UILabel = {
+ let label = UILabel()
+ label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
+ label.adjustsFontSizeToFitWidth = true
+ label.minimumScaleFactor = 0.5
+ label.textColor = .white
+ label.text = "Alice"
+ label.applyShadow(color: UIColor.black.withAlphaComponent(0.2), alpha: 0.5, x: 0, y: 2, blur: 2, spread: 0)
+ return label
+ }()
+
+ let usernameLabel: UILabel = {
+ let label = UILabel()
+ label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
+ label.adjustsFontSizeToFitWidth = true
+ label.minimumScaleFactor = 0.5
+ label.textColor = .white
+ label.text = "@alice"
+ label.applyShadow(color: UIColor.black.withAlphaComponent(0.2), alpha: 0.5, x: 0, y: 2, blur: 2, spread: 0)
+ return label
+ }()
+
+ let statusDashboardView = ProfileStatusDashboardView()
+ let friendshipActionButton = ProfileFriendshipActionButton()
+
+ let bioContainerView = UIView()
+ let fieldContainerStackView = UIStackView()
+
+ let bioActiveLabel = ActiveLabel(style: .default)
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ _init()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ _init()
+ }
+
+}
+
+extension ProfileHeaderView {
+ private func _init() {
+ backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+
+ // banner
+ bannerContainerView.translatesAutoresizingMaskIntoConstraints = false
+ bannerContainerView.preservesSuperviewLayoutMargins = true
+ addSubview(bannerContainerView)
+ NSLayoutConstraint.activate([
+ bannerContainerView.topAnchor.constraint(equalTo: topAnchor),
+ bannerContainerView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ trailingAnchor.constraint(equalTo: bannerContainerView.trailingAnchor),
+ readableContentGuide.widthAnchor.constraint(equalTo: bannerContainerView.heightAnchor, multiplier: 3), // set height to 1/3 of readable frame width
+ ])
+
+ bannerImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ bannerImageView.frame = bannerContainerView.bounds
+ bannerContainerView.addSubview(bannerImageView)
+
+ // avatar
+ avatarImageView.translatesAutoresizingMaskIntoConstraints = false
+ bannerContainerView.addSubview(avatarImageView)
+ NSLayoutConstraint.activate([
+ avatarImageView.leadingAnchor.constraint(equalTo: bannerContainerView.readableContentGuide.leadingAnchor),
+ bannerContainerView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 20),
+ avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1),
+ avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).priority(.required - 1),
+ ])
+
+ // name container: [display name | username]
+ let nameContainerStackView = UIStackView()
+ nameContainerStackView.preservesSuperviewLayoutMargins = true
+ nameContainerStackView.axis = .vertical
+ nameContainerStackView.spacing = 0
+ nameContainerStackView.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(nameContainerStackView)
+ NSLayoutConstraint.activate([
+ nameContainerStackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 12),
+ nameContainerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
+ nameContainerStackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
+ ])
+ nameContainerStackView.addArrangedSubview(nameLabel)
+ nameContainerStackView.addArrangedSubview(usernameLabel)
+
+ // meta container: [dashboard container | bio container | field container]
+ let metaContainerStackView = UIStackView()
+ metaContainerStackView.spacing = 16
+ metaContainerStackView.axis = .vertical
+ metaContainerStackView.preservesSuperviewLayoutMargins = true
+ metaContainerStackView.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(metaContainerStackView)
+ NSLayoutConstraint.activate([
+ metaContainerStackView.topAnchor.constraint(equalTo: bannerContainerView.bottomAnchor, constant: 13),
+ metaContainerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
+ metaContainerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
+ metaContainerStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
+ ])
+
+ // dashboard container: [dashboard | friendship action button]
+ let dashboardContainerView = UIView()
+ dashboardContainerView.preservesSuperviewLayoutMargins = true
+ metaContainerStackView.addArrangedSubview(dashboardContainerView)
+
+ statusDashboardView.translatesAutoresizingMaskIntoConstraints = false
+ dashboardContainerView.addSubview(statusDashboardView)
+ NSLayoutConstraint.activate([
+ statusDashboardView.topAnchor.constraint(equalTo: dashboardContainerView.topAnchor),
+ statusDashboardView.leadingAnchor.constraint(equalTo: dashboardContainerView.readableContentGuide.leadingAnchor),
+ statusDashboardView.bottomAnchor.constraint(equalTo: dashboardContainerView.bottomAnchor),
+ ])
+
+ friendshipActionButton.translatesAutoresizingMaskIntoConstraints = false
+ dashboardContainerView.addSubview(friendshipActionButton)
+ NSLayoutConstraint.activate([
+ friendshipActionButton.topAnchor.constraint(equalTo: dashboardContainerView.topAnchor),
+ friendshipActionButton.leadingAnchor.constraint(greaterThanOrEqualTo: statusDashboardView.trailingAnchor, constant: 8),
+ friendshipActionButton.trailingAnchor.constraint(equalTo: dashboardContainerView.readableContentGuide.trailingAnchor),
+ friendshipActionButton.widthAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.width).priority(.defaultHigh),
+ friendshipActionButton.heightAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.height).priority(.defaultHigh),
+ ])
+
+ bioContainerView.preservesSuperviewLayoutMargins = true
+ metaContainerStackView.addArrangedSubview(bioContainerView)
+ bioActiveLabel.translatesAutoresizingMaskIntoConstraints = false
+ bioContainerView.addSubview(bioActiveLabel)
+ NSLayoutConstraint.activate([
+ bioActiveLabel.topAnchor.constraint(equalTo: bioContainerView.topAnchor),
+ bioActiveLabel.leadingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.leadingAnchor),
+ bioActiveLabel.trailingAnchor.constraint(equalTo: bioContainerView.readableContentGuide.trailingAnchor),
+ bioActiveLabel.bottomAnchor.constraint(equalTo: bioContainerView.bottomAnchor),
+ ])
+
+ fieldContainerStackView.preservesSuperviewLayoutMargins = true
+ metaContainerStackView.addSubview(fieldContainerStackView)
+
+ bringSubviewToFront(bannerContainerView)
+ bringSubviewToFront(nameContainerStackView)
+
+ bioActiveLabel.delegate = self
+ }
+
+}
+
+// MARK: - ActiveLabelDelegate
+extension ProfileHeaderView: ActiveLabelDelegate {
+ func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select entity: %s", ((#file as NSString).lastPathComponent), #line, #function, entity.primaryText)
+ delegate?.profileHeaderView(self, activeLabel: activeLabel, entityDidPressed: entity)
+ }
+}
+
+// MARK: - ProfileStatusDashboardViewDelegate
+extension ProfileHeaderView: ProfileStatusDashboardViewDelegate {
+
+ func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
+ delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, postDashboardMeterViewDidPressed: dashboardMeterView)
+ }
+
+ func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
+ delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followingDashboardMeterViewDidPressed: dashboardMeterView)
+ }
+
+ func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
+ delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followersDashboardMeterViewDidPressed: dashboardMeterView)
+ }
+
+}
+
+// MARK: - AvatarConfigurableView
+extension ProfileHeaderView: AvatarConfigurableView {
+ static var configurableAvatarImageSize: CGSize { avatarImageViewSize }
+ static var configurableAvatarImageCornerRadius: CGFloat { avatarImageViewCornerRadius }
+ var configurableAvatarImageView: UIImageView? { return avatarImageView }
+ var configurableAvatarButton: UIButton? { return nil }
+}
+
+
+#if DEBUG
+import SwiftUI
+
+struct ProfileHeaderView_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ UIViewPreview(width: 375) {
+ let banner = ProfileHeaderView()
+ banner.bannerImageView.image = UIImage(named: "lucas-ludwig")
+ return banner
+ }
+ .previewLayout(.fixed(width: 375, height: 800))
+ UIViewPreview(width: 375) {
+ let banner = ProfileHeaderView()
+ //banner.bannerImageView.image = UIImage(named: "peter-luo")
+ return banner
+ }
+ .preferredColorScheme(.dark)
+ .previewLayout(.fixed(width: 375, height: 800))
+ }
+ }
+}
+#endif
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardMeterView.swift b/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardMeterView.swift
new file mode 100644
index 000000000..4355fdc3e
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardMeterView.swift
@@ -0,0 +1,79 @@
+//
+// ProfileStatusDashboardMeterView.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import UIKit
+
+final class ProfileStatusDashboardMeterView: UIView {
+
+ let numberLabel: UILabel = {
+ let label = UILabel()
+ label.font = {
+ let font = UIFont.systemFont(ofSize: 20, weight: .semibold)
+ return font.fontDescriptor.withDesign(.rounded).flatMap {
+ UIFont(descriptor: $0, size: 20)
+ } ?? font
+ }()
+ label.textColor = Asset.Colors.Label.primary.color
+ label.text = "999"
+ label.textAlignment = .center
+ return label
+ }()
+
+ let textLabel: UILabel = {
+ let label = UILabel()
+ label.font = .systemFont(ofSize: 13, weight: .regular)
+ label.textColor = Asset.Colors.Label.primary.color
+ label.text = L10n.Scene.Profile.Dashboard.posts
+ label.textAlignment = .center
+ return label
+ }()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ _init()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ _init()
+ }
+
+}
+
+extension ProfileStatusDashboardMeterView {
+ private func _init() {
+ numberLabel.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(numberLabel)
+ NSLayoutConstraint.activate([
+ numberLabel.topAnchor.constraint(equalTo: topAnchor),
+ numberLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
+ trailingAnchor.constraint(equalTo: numberLabel.trailingAnchor),
+ ])
+
+ textLabel.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(textLabel)
+ NSLayoutConstraint.activate([
+ textLabel.topAnchor.constraint(equalTo: numberLabel.bottomAnchor),
+ textLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
+ trailingAnchor.constraint(equalTo: textLabel.trailingAnchor),
+ bottomAnchor.constraint(equalTo: textLabel.bottomAnchor),
+ ])
+ }
+}
+
+#if DEBUG
+import SwiftUI
+
+struct ProfileStatusDashboardMeterView_Previews: PreviewProvider {
+ static var previews: some View {
+ UIViewPreview(width: 54) {
+ ProfileStatusDashboardMeterView()
+ }
+ .previewLayout(.fixed(width: 54, height: 41))
+ }
+}
+#endif
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardView.swift b/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardView.swift
new file mode 100644
index 000000000..4a95fb22f
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardView.swift
@@ -0,0 +1,103 @@
+//
+// ProfileStatusDashboardView.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import os.log
+import UIKit
+
+protocol ProfileStatusDashboardViewDelegate: class {
+ func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
+ func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
+ func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
+}
+
+final class ProfileStatusDashboardView: UIView {
+
+ let postDashboardMeterView = ProfileStatusDashboardMeterView()
+ let followingDashboardMeterView = ProfileStatusDashboardMeterView()
+ let followersDashboardMeterView = ProfileStatusDashboardMeterView()
+
+ weak var delegate: ProfileStatusDashboardViewDelegate?
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ _init()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ _init()
+ }
+
+}
+
+extension ProfileStatusDashboardView {
+ private func _init() {
+ let containerStackView = UIStackView()
+ 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),
+ containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
+ ])
+
+ let spacing: CGFloat = 16
+ containerStackView.spacing = spacing
+ containerStackView.axis = .horizontal
+ containerStackView.distribution = .fillEqually
+ containerStackView.alignment = .top
+ containerStackView.addArrangedSubview(postDashboardMeterView)
+ containerStackView.setCustomSpacing(spacing - 2, after: postDashboardMeterView)
+ containerStackView.addArrangedSubview(followingDashboardMeterView)
+ containerStackView.setCustomSpacing(spacing + 2, after: followingDashboardMeterView)
+ containerStackView.addArrangedSubview(followersDashboardMeterView)
+
+ postDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.posts
+ followingDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.following
+ followersDashboardMeterView.textLabel.text = L10n.Scene.Profile.Dashboard.followers
+
+ [postDashboardMeterView, followingDashboardMeterView, followersDashboardMeterView].forEach { meterView in
+ let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
+ tapGestureRecognizer.addTarget(self, action: #selector(ProfileStatusDashboardView.tapGestureRecognizerHandler(_:)))
+ meterView.addGestureRecognizer(tapGestureRecognizer)
+ }
+
+ }
+}
+
+extension ProfileStatusDashboardView {
+ @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ guard let sourceView = sender.view as? ProfileStatusDashboardMeterView else {
+ assertionFailure()
+ return
+ }
+ if sourceView === postDashboardMeterView {
+ delegate?.profileStatusDashboardView(self, postDashboardMeterViewDidPressed: sourceView)
+ } else if sourceView === followingDashboardMeterView {
+ delegate?.profileStatusDashboardView(self, followingDashboardMeterViewDidPressed: sourceView)
+ } else if sourceView === followersDashboardMeterView {
+ delegate?.profileStatusDashboardView(self, followersDashboardMeterViewDidPressed: sourceView)
+ }
+ }
+}
+
+
+#if DEBUG
+import SwiftUI
+
+struct ProfileBannerStatusView_Previews: PreviewProvider {
+ static var previews: some View {
+ UIViewPreview(width: 375) {
+ ProfileStatusDashboardView()
+ }
+ .previewLayout(.fixed(width: 375, height: 100))
+ }
+}
+#endif
diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift
new file mode 100644
index 000000000..36bcf0b4e
--- /dev/null
+++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift
@@ -0,0 +1,33 @@
+//
+// MeProfileViewModel.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+
+final class MeProfileViewModel: ProfileViewModel {
+
+ init(context: AppContext) {
+ super.init(
+ context: context,
+ optionalMastodonUser: context.authenticationService.activeMastodonAuthentication.value?.user
+ )
+
+ self.currentMastodonUser
+ .sink { [weak self] currentMastodonUser in
+ os_log("%{public}s[%{public}ld], %{public}s: current active twitter user: %s", ((#file as NSString).lastPathComponent), #line, #function, currentMastodonUser?.username ?? "")
+
+ guard let self = self else { return }
+ self.mastodonUser.value = currentMastodonUser
+ }
+ .store(in: &disposeBag)
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift
index b3c46a42d..4b74ad632 100644
--- a/Mastodon/Scene/Profile/ProfileViewController.swift
+++ b/Mastodon/Scene/Profile/ProfileViewController.swift
@@ -5,20 +5,672 @@
// Created by MainasuK Cirno on 2021-2-23.
//
+import os.log
import UIKit
+import Combine
+import ActiveLabel
final class ProfileViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
+ var disposeBag = Set()
+ var viewModel: ProfileViewModel!
+
+ private var preferredStatusBarStyleForBanner: UIStatusBarStyle = .lightContent {
+ didSet {
+ setNeedsStatusBarAppearanceUpdate()
+ }
+ }
+
+ let refreshControl: UIRefreshControl = {
+ let refreshControl = UIRefreshControl()
+ refreshControl.tintColor = .label
+ return refreshControl
+ }()
+
+ let containerScrollView: UIScrollView = {
+ let scrollView = UIScrollView()
+ scrollView.scrollsToTop = false
+ scrollView.showsVerticalScrollIndicator = false
+ scrollView.preservesSuperviewLayoutMargins = true
+ scrollView.delaysContentTouches = false
+ return scrollView
+ }()
+
+ let overlayScrollView: UIScrollView = {
+ let scrollView = UIScrollView()
+ scrollView.showsVerticalScrollIndicator = false
+ scrollView.backgroundColor = .clear
+ scrollView.delaysContentTouches = false
+ return scrollView
+ }()
+
+ private(set) lazy var profileSegmentedViewController = ProfileSegmentedViewController()
+ private(set) lazy var profileHeaderViewController = ProfileHeaderViewController()
+ private var profileBannerImageViewLayoutConstraint: NSLayoutConstraint!
+
+ private var contentOffsets: [Int: CGFloat] = [:]
+ var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation?
+
+
+ deinit {
+ os_log("%{public}s[%{public}ld], %{public}s: deinit", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
}
extension ProfileViewController {
- override func viewDidLoad() {
- super.viewDidLoad()
-
+ func observeTableViewContentSize(scrollView: UIScrollView) -> NSKeyValueObservation {
+ updateOverlayScrollViewContentSize(scrollView: scrollView)
+ return scrollView.observe(\.contentSize, options: .new) { scrollView, change in
+ self.updateOverlayScrollViewContentSize(scrollView: scrollView)
+ }
+ }
+
+ func updateOverlayScrollViewContentSize(scrollView: UIScrollView) {
+ let bottomPageHeight = max(scrollView.contentSize.height, self.containerScrollView.frame.height - ProfileHeaderViewController.headerMinHeight - self.containerScrollView.safeAreaInsets.bottom)
+ let headerViewHeight: CGFloat = profileHeaderViewController.view.frame.height
+ let contentSize = CGSize(
+ width: self.containerScrollView.contentSize.width,
+ height: bottomPageHeight + headerViewHeight
+ )
+ self.overlayScrollView.contentSize = contentSize
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: contentSize: %s", ((#file as NSString).lastPathComponent), #line, #function, contentSize.debugDescription)
}
}
+
+extension ProfileViewController {
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return preferredStatusBarStyleForBanner
+ }
+
+ override func viewSafeAreaInsetsDidChange() {
+ super.viewSafeAreaInsetsDidChange()
+
+ profileHeaderViewController.updateHeaderContainerSafeAreaInset(view.safeAreaInsets)
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+
+ let barAppearance = UINavigationBarAppearance()
+ barAppearance.configureWithTransparentBackground()
+ navigationItem.standardAppearance = barAppearance
+ navigationItem.compactAppearance = barAppearance
+ navigationItem.scrollEdgeAppearance = barAppearance
+ navigationItem.titleView = UIView()
+
+// if navigationController?.viewControllers.first == self {
+// navigationItem.leftBarButtonItem = avatarBarButtonItem
+// }
+// avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(ProfileViewController.avatarButtonPressed(_:)), for: .touchUpInside)
+
+// unmuteMenuBarButtonItem.target = self
+// unmuteMenuBarButtonItem.action = #selector(ProfileViewController.unmuteBarButtonItemPressed(_:))
+
+// Publishers.CombineLatest4(
+// viewModel.muted.eraseToAnyPublisher(),
+// viewModel.blocked.eraseToAnyPublisher(),
+// viewModel.twitterUser.eraseToAnyPublisher(),
+// context.authenticationService.activeTwitterAuthenticationBox.eraseToAnyPublisher()
+// )
+// .receive(on: DispatchQueue.main)
+// .sink { [weak self] muted, blocked, twitterUser, activeTwitterAuthenticationBox in
+// guard let self = self else { return }
+// guard let twitterUser = twitterUser,
+// let activeTwitterAuthenticationBox = activeTwitterAuthenticationBox,
+// twitterUser.id != activeTwitterAuthenticationBox.twitterUserID else {
+// self.navigationItem.rightBarButtonItems = []
+// return
+// }
+//
+// if #available(iOS 14.0, *) {
+// self.moreMenuBarButtonItem.target = nil
+// self.moreMenuBarButtonItem.action = nil
+// self.moreMenuBarButtonItem.menu = UserProviderFacade.createMenuForUser(
+// twitterUser: twitterUser,
+// muted: muted,
+// blocked: blocked,
+// dependency: self
+// )
+// } else {
+// // no menu supports for early version
+// self.moreMenuBarButtonItem.target = self
+// self.moreMenuBarButtonItem.action = #selector(ProfileViewController.moreMenuBarButtonItemPressed(_:))
+// }
+//
+// var rightBarButtonItems: [UIBarButtonItem] = [self.moreMenuBarButtonItem]
+// if muted {
+// rightBarButtonItems.append(self.unmuteMenuBarButtonItem)
+// }
+//
+// self.navigationItem.rightBarButtonItems = rightBarButtonItems
+// }
+// .store(in: &disposeBag)
+
+ overlayScrollView.refreshControl = refreshControl
+ refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged)
+
+// drawerSidebarTransitionController = DrawerSidebarTransitionController(drawerSidebarTransitionableViewController: self)
+
+ let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter())
+ viewModel.domain.assign(to: \.value, on: postsUserTimelineViewModel.domain).store(in: &disposeBag)
+ viewModel.userID.assign(to: \.value, on: postsUserTimelineViewModel.userID).store(in: &disposeBag)
+
+ let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
+ viewModel.domain.assign(to: \.value, on: repliesUserTimelineViewModel.domain).store(in: &disposeBag)
+ viewModel.userID.assign(to: \.value, on: repliesUserTimelineViewModel.userID).store(in: &disposeBag)
+
+ let mediaUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(onlyMedia: true))
+ viewModel.domain.assign(to: \.value, on: mediaUserTimelineViewModel.domain).store(in: &disposeBag)
+ viewModel.userID.assign(to: \.value, on: mediaUserTimelineViewModel.userID).store(in: &disposeBag)
+
+ profileSegmentedViewController.pagingViewController.viewModel = {
+ let profilePagingViewModel = ProfilePagingViewModel(
+ postsUserTimelineViewModel: postsUserTimelineViewModel,
+ repliesUserTimelineViewModel: repliesUserTimelineViewModel,
+ mediaUserTimelineViewModel: mediaUserTimelineViewModel
+ )
+ profilePagingViewModel.viewControllers.forEach { viewController in
+ if let viewController = viewController as? NeedsDependency {
+ viewController.context = context
+ viewController.coordinator = coordinator
+ }
+ }
+ return profilePagingViewModel
+ }()
+
+ profileHeaderViewController.pageSegmentedControl.removeAllSegments()
+ profileSegmentedViewController.pagingViewController.viewModel.barItems.forEach { item in
+ let index = profileHeaderViewController.pageSegmentedControl.numberOfSegments
+ profileHeaderViewController.pageSegmentedControl.insertSegment(withTitle: item.title, at: index, animated: false)
+ }
+ profileHeaderViewController.pageSegmentedControl.selectedSegmentIndex = 0
+
+ overlayScrollView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(overlayScrollView)
+ NSLayoutConstraint.activate([
+ overlayScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
+ overlayScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ view.trailingAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.trailingAnchor),
+ view.bottomAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.bottomAnchor),
+ overlayScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
+ ])
+
+ containerScrollView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(containerScrollView)
+ NSLayoutConstraint.activate([
+ containerScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
+ containerScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ view.trailingAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.trailingAnchor),
+ view.bottomAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.bottomAnchor),
+ containerScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
+ ])
+
+ // add segmented list
+ addChild(profileSegmentedViewController)
+ profileSegmentedViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ containerScrollView.addSubview(profileSegmentedViewController.view)
+ profileSegmentedViewController.didMove(toParent: self)
+ NSLayoutConstraint.activate([
+ profileSegmentedViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
+ profileSegmentedViewController.view.trailingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.trailingAnchor),
+ profileSegmentedViewController.view.bottomAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.bottomAnchor),
+ profileSegmentedViewController.view.heightAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.heightAnchor),
+ ])
+
+ // add header
+ addChild(profileHeaderViewController)
+ profileHeaderViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ containerScrollView.addSubview(profileHeaderViewController.view)
+ profileHeaderViewController.didMove(toParent: self)
+ NSLayoutConstraint.activate([
+ profileHeaderViewController.view.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
+ profileHeaderViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
+ containerScrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: profileHeaderViewController.view.trailingAnchor),
+ profileSegmentedViewController.view.topAnchor.constraint(equalTo: profileHeaderViewController.view.bottomAnchor),
+ ])
+
+ containerScrollView.addGestureRecognizer(overlayScrollView.panGestureRecognizer)
+ overlayScrollView.layer.zPosition = .greatestFiniteMagnitude // make vision top-most
+ overlayScrollView.delegate = self
+ profileHeaderViewController.delegate = self
+ profileSegmentedViewController.pagingViewController.pagingDelegate = self
+
+// // add segmented bar to header
+// profileSegmentedViewController.pagingViewController.addBar(
+// bar,
+// dataSource: profileSegmentedViewController.pagingViewController.viewModel,
+// at: .custom(view: profileHeaderViewController.view, layout: { bar in
+// bar.translatesAutoresizingMaskIntoConstraints = false
+// self.profileHeaderViewController.view.addSubview(bar)
+// NSLayoutConstraint.activate([
+// bar.leadingAnchor.constraint(equalTo: self.profileHeaderViewController.view.leadingAnchor),
+// bar.trailingAnchor.constraint(equalTo: self.profileHeaderViewController.view.trailingAnchor),
+// bar.bottomAnchor.constraint(equalTo: self.profileHeaderViewController.view.bottomAnchor),
+// bar.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.headerMinHeight).priority(.defaultHigh),
+// ])
+// })
+// )
+
+ // bind view model
+ Publishers.CombineLatest(
+ viewModel.bannerImageURL.eraseToAnyPublisher(),
+ viewModel.viewDidAppear.eraseToAnyPublisher()
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] bannerImageURL, _ in
+ guard let self = self else { return }
+ self.profileHeaderViewController.profileBannerView.bannerImageView.af.cancelImageRequest()
+ let placeholder = UIImage.placeholder(color: Asset.Colors.Background.systemGroupedBackground.color)
+ guard let bannerImageURL = bannerImageURL else {
+ self.profileHeaderViewController.profileBannerView.bannerImageView.image = placeholder
+ return
+ }
+ self.profileHeaderViewController.profileBannerView.bannerImageView.af.setImage(
+ withURL: bannerImageURL,
+ placeholderImage: placeholder,
+ imageTransition: .crossDissolve(0.3),
+ runImageTransitionIfCached: false,
+ completion: { [weak self] response in
+ guard let self = self else { return }
+ switch response.result {
+ case .success(let image):
+ self.viewModel.headerDomainLumaStyle.value = image.domainLumaCoefficientsStyle ?? .dark
+ case .failure:
+ break
+ }
+ }
+ )
+ }
+ .store(in: &disposeBag)
+ viewModel.headerDomainLumaStyle
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] style in
+ guard let self = self else { return }
+ let textColor: UIColor
+ let shadowColor: UIColor
+ switch style {
+ case .light:
+ self.preferredStatusBarStyleForBanner = .darkContent
+ textColor = .black
+ shadowColor = .white
+ case .dark:
+ self.preferredStatusBarStyleForBanner = .lightContent
+ textColor = .white
+ shadowColor = .black
+ default:
+ self.preferredStatusBarStyleForBanner = .default
+ textColor = .white
+ shadowColor = .black
+ }
+
+ self.profileHeaderViewController.profileBannerView.nameLabel.textColor = textColor
+ self.profileHeaderViewController.profileBannerView.usernameLabel.textColor = textColor
+ self.profileHeaderViewController.profileBannerView.nameLabel.applyShadow(color: shadowColor, alpha: 0.5, x: 0, y: 2, blur: 2)
+ self.profileHeaderViewController.profileBannerView.usernameLabel.applyShadow(color: shadowColor, alpha: 0.5, x: 0, y: 2, blur: 2)
+ }
+ .store(in: &disposeBag)
+ Publishers.CombineLatest(
+ viewModel.avatarImageURL.eraseToAnyPublisher(),
+ viewModel.viewDidAppear.eraseToAnyPublisher()
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] avatarImageURL, _ in
+ guard let self = self else { return }
+ self.profileHeaderViewController.profileBannerView.configure(
+ with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarImageURL)
+ )
+ }
+ .store(in: &disposeBag)
+// viewModel.protected
+// .map { $0 != true }
+// .assign(to: \.isHidden, on: profileHeaderViewController.profileBannerView.lockImageView)
+// .store(in: &disposeBag)
+ viewModel.name
+ .map { $0 ?? " " }
+ .receive(on: DispatchQueue.main)
+ .assign(to: \.text, on: profileHeaderViewController.profileBannerView.nameLabel)
+ .store(in: &disposeBag)
+ viewModel.username
+ .map { username in username.flatMap { "@" + $0 } ?? " " }
+ .receive(on: DispatchQueue.main)
+ .assign(to: \.text, on: profileHeaderViewController.profileBannerView.usernameLabel)
+ .store(in: &disposeBag)
+// viewModel.friendship
+// .sink { [weak self] friendship in
+// guard let self = self else { return }
+// let followingButton = self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.followActionButton
+// followingButton.isHidden = friendship == nil
+//
+// if let friendship = friendship {
+// switch friendship {
+// case .following: followingButton.style = .following
+// case .pending: followingButton.style = .pending
+// case .none: followingButton.style = .follow
+// }
+// }
+// }
+// .store(in: &disposeBag)
+// viewModel.followedBy
+// .sink { [weak self] followedBy in
+// guard let self = self else { return }
+// let followStatusLabel = self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.followStatusLabel
+// followStatusLabel.isHidden = followedBy != true
+// }
+// .store(in: &disposeBag)
+//
+ viewModel.bioDescription
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: { [weak self] bio in
+ guard let self = self else { return }
+ self.profileHeaderViewController.profileBannerView.bioActiveLabel.configure(note: bio ?? "")
+ })
+ .store(in: &disposeBag)
+// Publishers.CombineLatest(
+// viewModel.url.eraseToAnyPublisher(),
+// viewModel.suspended.eraseToAnyPublisher()
+// )
+// .receive(on: DispatchQueue.main)
+// .sink { [weak self] url, isSuspended in
+// guard let self = self else { return }
+// let url = url.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? " "
+// self.profileHeaderViewController.profileBannerView.linkButton.setTitle(url, for: .normal)
+// let isEmpty = url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+// self.profileHeaderViewController.profileBannerView.linkContainer.isHidden = isEmpty || isSuspended
+// }
+// .store(in: &disposeBag)
+// Publishers.CombineLatest(
+// viewModel.location.eraseToAnyPublisher(),
+// viewModel.suspended.eraseToAnyPublisher()
+// )
+// .receive(on: DispatchQueue.main)
+// .sink { [weak self] location, isSuspended in
+// guard let self = self else { return }
+// let location = location.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? " "
+// self.profileHeaderViewController.profileBannerView.geoButton.setTitle(location, for: .normal)
+// let isEmpty = location.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+// self.profileHeaderViewController.profileBannerView.geoContainer.isHidden = isEmpty || isSuspended
+// }
+// .store(in: &disposeBag)
+ viewModel.statusesCount
+ .sink { [weak self] count in
+ guard let self = self else { return }
+ let text = count.flatMap { String($0) } ?? "-"
+ self.profileHeaderViewController.profileBannerView.statusDashboardView.postDashboardMeterView.numberLabel.text = text
+ }
+ .store(in: &disposeBag)
+ viewModel.followingCount
+ .sink { [weak self] count in
+ guard let self = self else { return }
+ let text = count.flatMap { String($0) } ?? "-"
+ self.profileHeaderViewController.profileBannerView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
+ }
+ .store(in: &disposeBag)
+ viewModel.followersCount
+ .sink { [weak self] count in
+ guard let self = self else { return }
+ let text = count.flatMap { String($0) } ?? "-"
+ self.profileHeaderViewController.profileBannerView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
+ }
+ .store(in: &disposeBag)
+// viewModel.followersCount
+// .sink { [weak self] count in
+// guard let self = self else { return }
+// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.followersStatusItemView.countLabel.text = count.flatMap { "\($0)" } ?? "-"
+// }
+// .store(in: &disposeBag)
+// viewModel.listedCount
+// .sink { [weak self] count in
+// guard let self = self else { return }
+// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.listedStatusItemView.countLabel.text = count.flatMap { "\($0)" } ?? "-"
+// }
+// .store(in: &disposeBag)
+// viewModel.suspended
+// .receive(on: DispatchQueue.main)
+// .sink { [weak self] isSuspended in
+// guard let self = self else { return }
+// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.isHidden = isSuspended
+// self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.isHidden = isSuspended
+// if isSuspended {
+// self.profileSegmentedViewController
+// .pagingViewController.viewModel
+// .profileTweetPostTimelineViewController.viewModel
+// .stateMachine
+// .enter(UserTimelineViewModel.State.Suspended.self)
+// self.profileSegmentedViewController
+// .pagingViewController.viewModel
+// .profileMediaPostTimelineViewController.viewModel
+// .stateMachine
+// .enter(UserMediaTimelineViewModel.State.Suspended.self)
+// self.profileSegmentedViewController
+// .pagingViewController.viewModel
+// .profileLikesPostTimelineViewController.viewModel
+// .stateMachine
+// .enter(UserLikeTimelineViewModel.State.Suspended.self)
+// }
+// }
+// .store(in: &disposeBag)
+
+//
+ profileHeaderViewController.profileBannerView.delegate = self
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ viewModel.viewDidAppear.send()
+
+ // set overlay scroll view initial content size
+ guard let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer else { return }
+ currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: currentViewController.scrollView)
+ currentViewController.scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ currentPostTimelineTableViewContentSizeObservation = nil
+ }
+
+}
+
+extension ProfileViewController {
+
+ @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+
+ let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController
+ if let currentViewController = currentViewController as? UserTimelineViewController {
+ currentViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
+ }
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ sender.endRefreshing()
+ }
+ }
+
+// @objc private func avatarButtonPressed(_ sender: UIButton) {
+// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+// coordinator.present(scene: .drawerSidebar, from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController))
+// }
+//
+// @objc private func unmuteBarButtonItemPressed(_ sender: UIBarButtonItem) {
+// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+// guard let twitterUser = viewModel.twitterUser.value else {
+// assertionFailure()
+// return
+// }
+//
+// UserProviderFacade.toggleMuteUser(
+// context: context,
+// twitterUser: twitterUser,
+// muted: viewModel.muted.value
+// )
+// .sink { _ in
+// // do nothing
+// } receiveValue: { _ in
+// // do nothing
+// }
+// .store(in: &disposeBag)
+// }
+//
+// @objc private func moreMenuBarButtonItemPressed(_ sender: UIBarButtonItem) {
+// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+// guard let twitterUser = viewModel.twitterUser.value else {
+// assertionFailure()
+// return
+// }
+//
+// let moreMenuAlertController = UserProviderFacade.createMoreMenuAlertControllerForUser(
+// twitterUser: twitterUser,
+// muted: viewModel.muted.value,
+// blocked: viewModel.blocked.value,
+// sender: sender,
+// dependency: self
+// )
+// present(moreMenuAlertController, animated: true, completion: nil)
+// }
+
+}
+
+// MARK: - UIScrollViewDelegate
+extension ProfileViewController: UIScrollViewDelegate {
+
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y
+ let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top
+ if scrollView.contentOffset.y < topMaxContentOffsetY {
+ self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
+ for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers {
+ postTimelineView.scrollView.contentOffset.y = 0
+ }
+ contentOffsets.removeAll()
+ } else {
+ containerScrollView.contentOffset.y = topMaxContentOffsetY
+ if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer {
+ let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y
+ customScrollViewContainerController.scrollView.contentOffset.y = contentOffsetY
+ }
+ }
+
+ // elastically banner image
+ let headerScrollProgress = containerScrollView.contentOffset.y / topMaxContentOffsetY
+ profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress)
+ }
+
+}
+
+// MARK: - ProfileHeaderViewControllerDelegate
+extension ProfileViewController: ProfileHeaderViewControllerDelegate {
+
+ func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) {
+ guard let scrollView = (profileSegmentedViewController.pagingViewController.currentViewController as? UserTimelineViewController)?.scrollView else {
+ // assertionFailure()
+ return
+ }
+
+ updateOverlayScrollViewContentSize(scrollView: scrollView)
+ }
+
+ func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int) {
+ profileSegmentedViewController.pagingViewController.scrollToPage(
+ .at(index: index),
+ animated: true
+ )
+ }
+
+}
+
+// MARK: - ProfilePagingViewControllerDelegate
+extension ProfileViewController: ProfilePagingViewControllerDelegate {
+
+ func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController postTimelineViewController: ScrollViewContainer, atIndex index: Int) {
+ os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
+
+ // save content offset
+ overlayScrollView.contentOffset.y = contentOffsets[index] ?? containerScrollView.contentOffset.y
+
+ // setup observer and gesture fallback
+ currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: postTimelineViewController.scrollView)
+ postTimelineViewController.scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
+
+
+// if let userMediaTimelineViewController = postTimelineViewController as? UserMediaTimelineViewController,
+// let currentState = userMediaTimelineViewController.viewModel.stateMachine.currentState {
+// switch currentState {
+// case is UserMediaTimelineViewModel.State.NoMore,
+// is UserMediaTimelineViewModel.State.NotAuthorized,
+// is UserMediaTimelineViewModel.State.Blocked:
+// break
+// default:
+// if userMediaTimelineViewController.viewModel.items.value.isEmpty {
+// userMediaTimelineViewController.viewModel.stateMachine.enter(UserMediaTimelineViewModel.State.Reloading.self)
+// }
+// }
+// }
+//
+// if let userLikeTimelineViewController = postTimelineViewController as? UserLikeTimelineViewController,
+// let currentState = userLikeTimelineViewController.viewModel.stateMachine.currentState {
+// switch currentState {
+// case is UserLikeTimelineViewModel.State.NoMore,
+// is UserLikeTimelineViewModel.State.NotAuthorized,
+// is UserLikeTimelineViewModel.State.Blocked:
+// break
+// default:
+// if userLikeTimelineViewController.viewModel.items.value.isEmpty {
+// userLikeTimelineViewController.viewModel.stateMachine.enter(UserLikeTimelineViewModel.State.Reloading.self)
+// }
+// }
+// }
+ }
+
+}
+
+// MARK: - ProfileBannerInfoActionViewDelegate
+//extension ProfileViewController: ProfileBannerInfoActionViewDelegate {
+//
+// func profileBannerInfoActionView(_ profileBannerInfoActionView: ProfileBannerInfoActionView, followActionButtonPressed button: FollowActionButton) {
+// UserProviderFacade
+// .toggleUserFriendship(provider: self, sender: button)
+// .sink { _ in
+// // do nothing
+// } receiveValue: { _ in
+// // do nothing
+// }
+// .store(in: &disposeBag)
+// }
+//
+//}
+
+// MARK: - ProfileHeaderViewDelegate
+extension ProfileViewController: ProfileHeaderViewDelegate {
+
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) {
+
+ }
+
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
+
+ }
+
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView) {
+
+ }
+
+ func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView) {
+
+ }
+
+}
+
+// MARK: - ScrollViewContainer
+extension ProfileViewController: ScrollViewContainer {
+ var scrollView: UIScrollView { return overlayScrollView }
+}
diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift
new file mode 100644
index 000000000..f7248009d
--- /dev/null
+++ b/Mastodon/Scene/Profile/ProfileViewModel.swift
@@ -0,0 +1,197 @@
+//
+// ProfileViewModel.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreDataStack
+import MastodonSDK
+
+// please override this base class
+class ProfileViewModel: NSObject {
+
+ typealias UserID = String
+
+ var disposeBag = Set()
+ var observations = Set()
+ private var mastodonUserObserver: AnyCancellable?
+ private var currentMastodonUserObserver: AnyCancellable?
+
+ // input
+ let context: AppContext
+ let mastodonUser: CurrentValueSubject
+ let currentMastodonUser = CurrentValueSubject(nil)
+ let viewDidAppear = PassthroughSubject()
+ let headerDomainLumaStyle = CurrentValueSubject(.dark) // default dark for placeholder banner
+
+ // output
+ let domain: CurrentValueSubject
+ let userID: CurrentValueSubject
+ let bannerImageURL: CurrentValueSubject
+ let avatarImageURL: CurrentValueSubject
+// let protected: CurrentValueSubject
+ let name: CurrentValueSubject
+ let username: CurrentValueSubject
+ let bioDescription: CurrentValueSubject
+ let url: CurrentValueSubject
+ let statusesCount: CurrentValueSubject
+ let followingCount: CurrentValueSubject
+ let followersCount: CurrentValueSubject
+
+// let friendship: CurrentValueSubject
+// let followedBy: CurrentValueSubject
+// let muted: CurrentValueSubject
+// let blocked: CurrentValueSubject
+//
+// let suspended = CurrentValueSubject(false)
+//
+
+ init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
+ self.context = context
+ self.mastodonUser = CurrentValueSubject(mastodonUser)
+ self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
+ self.userID = CurrentValueSubject(mastodonUser?.id)
+ self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
+ self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
+// self.protected = CurrentValueSubject(twitterUser?.protected)
+ self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
+ self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
+ self.bioDescription = CurrentValueSubject(mastodonUser?.note)
+ self.url = CurrentValueSubject(mastodonUser?.url)
+ self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.statusesCount) })
+ self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followingCount) })
+ self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) })
+// self.friendship = CurrentValueSubject(nil)
+// self.followedBy = CurrentValueSubject(nil)
+// self.muted = CurrentValueSubject(false)
+// self.blocked = CurrentValueSubject(false)
+ super.init()
+
+ // bind active authentication
+ context.authenticationService.activeMastodonAuthentication
+ .sink { [weak self] activeMastodonAuthentication in
+ guard let self = self else { return }
+ guard let activeMastodonAuthentication = activeMastodonAuthentication else {
+ self.domain.value = nil
+ self.currentMastodonUser.value = nil
+ return
+ }
+ self.domain.value = activeMastodonAuthentication.domain
+ self.currentMastodonUser.value = activeMastodonAuthentication.user
+ }
+ .store(in: &disposeBag)
+
+ setup()
+ }
+
+}
+
+extension ProfileViewModel {
+
+ enum Friendship: CustomDebugStringConvertible {
+ case following
+ case pending
+ case none
+
+ var debugDescription: String {
+ switch self {
+ case .following: return "following"
+ case .pending: return "pending"
+ case .none: return "none"
+ }
+ }
+ }
+
+}
+
+extension ProfileViewModel {
+ private func setup() {
+ Publishers.CombineLatest(
+ mastodonUser.eraseToAnyPublisher(),
+ currentMastodonUser.eraseToAnyPublisher()
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] mastodonUser, currentMastodonUser in
+ guard let self = self else { return }
+ self.update(mastodonUser: mastodonUser)
+ self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
+
+ if let mastodonUser = mastodonUser {
+ // setup observer
+ self.mastodonUserObserver = ManagedObjectObserver.observe(object: mastodonUser)
+ .sink { completion in
+ switch completion {
+ case .failure(let error):
+ assertionFailure(error.localizedDescription)
+ case .finished:
+ assertionFailure()
+ }
+ } receiveValue: { [weak self] change in
+ guard let self = self else { return }
+ guard let changeType = change.changeType else { return }
+ switch changeType {
+ case .update:
+ self.update(mastodonUser: mastodonUser)
+ self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
+ case .delete:
+ // TODO:
+ break
+ }
+ }
+
+ } else {
+ self.mastodonUserObserver = nil
+ }
+
+ if let currentMastodonUser = currentMastodonUser {
+ // setup observer
+ self.currentMastodonUserObserver = ManagedObjectObserver.observe(object: currentMastodonUser)
+ .sink { completion in
+ switch completion {
+ case .failure(let error):
+ assertionFailure(error.localizedDescription)
+ case .finished:
+ assertionFailure()
+ }
+ } receiveValue: { [weak self] change in
+ guard let self = self else { return }
+ guard let changeType = change.changeType else { return }
+ switch changeType {
+ case .update:
+ self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
+ case .delete:
+ // TODO:
+ break
+ }
+ }
+ } else {
+ self.currentMastodonUserObserver = nil
+ }
+ }
+ .store(in: &disposeBag)
+ }
+
+ private func update(mastodonUser: MastodonUser?) {
+ self.userID.value = mastodonUser?.id
+ self.bannerImageURL.value = mastodonUser?.headerImageURL()
+ self.avatarImageURL.value = mastodonUser?.avatarImageURL()
+// self.protected.value = twitterUser?.protected
+ self.name.value = mastodonUser?.displayNameWithFallback
+ self.username.value = mastodonUser?.acctWithDomain
+ self.bioDescription.value = mastodonUser?.note
+ self.url.value = mastodonUser?.url
+ self.statusesCount.value = mastodonUser.flatMap { Int(truncating: $0.statusesCount) }
+ self.followingCount.value = mastodonUser.flatMap { Int(truncating: $0.followingCount) }
+ self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) }
+ }
+
+ private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
+ // TODO:
+ }
+
+
+}
diff --git a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift
new file mode 100644
index 000000000..568369d66
--- /dev/null
+++ b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift
@@ -0,0 +1,47 @@
+//
+// ProfilePagingViewController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+import Pageboy
+import Tabman
+
+protocol ProfilePagingViewControllerDelegate: class {
+ func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int)
+}
+
+final class ProfilePagingViewController: TabmanViewController {
+
+ weak var pagingDelegate: ProfilePagingViewControllerDelegate?
+ var viewModel: ProfilePagingViewModel!
+
+
+ // MARK: - PageboyViewControllerDelegate
+
+ override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) {
+ super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated)
+
+ let viewController = viewModel.viewControllers[index]
+ pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
+ }
+
+ deinit {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+extension ProfilePagingViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = .clear
+ dataSource = viewModel
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift
new file mode 100644
index 000000000..252d5e14f
--- /dev/null
+++ b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift
@@ -0,0 +1,68 @@
+//
+// ProfilePagingViewModel.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+import Pageboy
+import Tabman
+
+final class ProfilePagingViewModel: NSObject {
+
+ let postUserTimelineViewController = UserTimelineViewController()
+ let repliesUserTimelineViewController = UserTimelineViewController()
+ let mediaUserTimelineViewController = UserTimelineViewController()
+
+ init(
+ postsUserTimelineViewModel: UserTimelineViewModel,
+ repliesUserTimelineViewModel: UserTimelineViewModel,
+ mediaUserTimelineViewModel: UserTimelineViewModel
+ ) {
+ postUserTimelineViewController.viewModel = postsUserTimelineViewModel
+ repliesUserTimelineViewController.viewModel = repliesUserTimelineViewModel
+ mediaUserTimelineViewController.viewModel = mediaUserTimelineViewModel
+ super.init()
+ }
+
+ var viewControllers: [ScrollViewContainer] {
+ return [
+ postUserTimelineViewController,
+ repliesUserTimelineViewController,
+ mediaUserTimelineViewController,
+ ]
+ }
+
+ let barItems: [TMBarItemable] = {
+ let items = [
+ TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
+ TMBarItem(title: L10n.Scene.Profile.SegmentedControl.replies),
+ TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
+ ]
+ return items
+ }()
+
+ deinit {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+// MARK: - PageboyViewControllerDataSource
+extension ProfilePagingViewModel: PageboyViewControllerDataSource {
+
+ func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
+ return viewControllers.count
+ }
+
+ func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
+ return viewControllers[index]
+ }
+
+ func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
+ return .first
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Segmented/ProfileSegmentedViewController.swift b/Mastodon/Scene/Profile/Segmented/ProfileSegmentedViewController.swift
new file mode 100644
index 000000000..06eaab3f4
--- /dev/null
+++ b/Mastodon/Scene/Profile/Segmented/ProfileSegmentedViewController.swift
@@ -0,0 +1,38 @@
+//
+// ProfileSegmentedViewController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+
+final class ProfileSegmentedViewController: UIViewController {
+ let pagingViewController = ProfilePagingViewController()
+
+ deinit {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+}
+
+extension ProfileSegmentedViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = .clear
+
+ addChild(pagingViewController)
+ pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(pagingViewController.view)
+ pagingViewController.didMove(toParent: self)
+ NSLayoutConstraint.activate([
+ pagingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
+ pagingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ view.trailingAnchor.constraint(equalTo: pagingViewController.view.trailingAnchor),
+ view.bottomAnchor.constraint(equalTo: pagingViewController.view.bottomAnchor),
+ ])
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift
new file mode 100644
index 000000000..1ea164406
--- /dev/null
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+StatusProvider.swift
@@ -0,0 +1,87 @@
+//
+// UserTimelineViewController+StatusProvider.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+
+// MARK: - StatusProvider
+extension UserTimelineViewController: StatusProvider {
+
+ func status() -> Future {
+ return Future { promise in promise(.success(nil)) }
+ }
+
+ func status(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 .status(let objectID, _):
+ let managedObjectContext = self.viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext
+ managedObjectContext.perform {
+ let status = managedObjectContext.object(with: objectID) as? Status
+ promise(.success(status))
+ }
+ default:
+ promise(.success(nil))
+ }
+ }
+ }
+
+ func status(for cell: UICollectionViewCell) -> Future {
+ return Future { promise in promise(.success(nil)) }
+ }
+
+ var managedObjectContext: NSManagedObjectContext {
+ return viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext
+ }
+
+ var tableViewDiffableDataSource: UITableViewDiffableDataSource? {
+ return viewModel.diffableDataSource
+ }
+
+ func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
+ guard let diffableDataSource = self.viewModel.diffableDataSource else {
+ assertionFailure()
+ return nil
+ }
+
+ guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
+ let item = diffableDataSource.itemIdentifier(for: indexPath) else {
+ return nil
+ }
+
+ return item
+ }
+
+ func items(indexPaths: [IndexPath]) -> [Item] {
+ guard let diffableDataSource = self.viewModel.diffableDataSource else {
+ assertionFailure()
+ return []
+ }
+
+ var items: [Item] = []
+ for indexPath in indexPaths {
+ guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue }
+ items.append(item)
+ }
+ return items
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
new file mode 100644
index 000000000..88134f1e1
--- /dev/null
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
@@ -0,0 +1,145 @@
+//
+// UserTimelineViewController.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+import AVKit
+import Combine
+import CoreDataStack
+import GameplayKit
+
+// TODO: adopt MediaPreviewableViewController
+final class UserTimelineViewController: UIViewController, NeedsDependency {
+
+ weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
+ weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
+
+ var disposeBag = Set()
+ var viewModel: UserTimelineViewModel!
+
+ // let mediaPreviewTransitionController = MediaPreviewTransitionController()
+
+ lazy var tableView: UITableView = {
+ let tableView = UITableView()
+ tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
+ tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.separatorStyle = .none
+ tableView.backgroundColor = .clear
+ return tableView
+ }()
+
+ deinit {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+extension UserTimelineViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
+
+ tableView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(tableView)
+ 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),
+ ])
+
+ tableView.delegate = self
+ viewModel.setupDiffableDataSource(
+ for: tableView,
+ dependency: self,
+ statusTableViewCellDelegate: self
+ )
+
+ // trigger user timeline loading
+ Publishers.CombineLatest(
+ viewModel.domain.removeDuplicates().eraseToAnyPublisher(),
+ viewModel.userID.removeDuplicates().eraseToAnyPublisher()
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ self.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
+ }
+ .store(in: &disposeBag)
+
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ tableView.deselectRow(with: transitionCoordinator, animated: animated)
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+
+ context.videoPlaybackService.viewDidDisappear(from: self)
+ }
+
+}
+
+// MARK: - UIScrollViewDelegate
+extension UserTimelineViewController {
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ handleScrollViewDidScroll(scrollView)
+ }
+}
+
+// MARK: - UITableViewDelegate
+extension UserTimelineViewController: UITableViewDelegate {
+
+ // TODO: cache cell height
+ func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
+ return 200
+ }
+
+}
+
+// MARK: - AVPlayerViewControllerDelegate
+extension UserTimelineViewController: AVPlayerViewControllerDelegate {
+
+ func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+ handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
+ }
+
+ func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+ handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
+ }
+
+}
+
+// MARK: - TimelinePostTableViewCellDelegate
+extension UserTimelineViewController: StatusTableViewCellDelegate {
+ weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
+ func parent() -> UIViewController { return self }
+}
+
+//// MARK: - TimelineHeaderTableViewCellDelegate
+//extension UserTimelineViewController: TimelineHeaderTableViewCellDelegate { }
+
+
+// MARK: - CustomScrollViewContainerController
+extension UserTimelineViewController: ScrollViewContainer {
+ var scrollView: UIScrollView { return tableView }
+}
+
+// MARK: - LoadMoreConfigurableTableViewContainer
+extension UserTimelineViewController: LoadMoreConfigurableTableViewContainer {
+ typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
+ typealias LoadingState = UserTimelineViewModel.State.LoadingMore
+
+ var loadMoreConfigurableTableView: UITableView { return tableView }
+ var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
+}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
new file mode 100644
index 000000000..1a09e1b35
--- /dev/null
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
@@ -0,0 +1,37 @@
+//
+// UserTimelineViewModel+Diffable.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import UIKit
+
+extension UserTimelineViewModel {
+
+ func setupDiffableDataSource(
+ for tableView: UITableView,
+ dependency: NeedsDependency,
+ statusTableViewCellDelegate: StatusTableViewCellDelegate
+ ) {
+ let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
+ .autoconnect()
+ .share()
+ .eraseToAnyPublisher()
+
+ diffableDataSource = StatusSection.tableViewDiffableDataSource(
+ for: tableView,
+ dependency: dependency,
+ managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext,
+ timestampUpdatePublisher: timestampUpdatePublisher,
+ statusTableViewCellDelegate: statusTableViewCellDelegate,
+ timelineMiddleLoaderTableViewCellDelegate: nil
+ )
+
+ // set empty section to make update animation top-to-bottom style
+ var snapshot = NSDiffableDataSourceSnapshot()
+ snapshot.appendSections([.main])
+ diffableDataSource?.apply(snapshot)
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift
new file mode 100644
index 000000000..520fa43e5
--- /dev/null
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift
@@ -0,0 +1,262 @@
+//
+// UserTimelineViewModel+State.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import os.log
+import Foundation
+import GameplayKit
+import MastodonSDK
+
+extension UserTimelineViewModel {
+ class State: GKState {
+ weak var viewModel: UserTimelineViewModel?
+
+ init(viewModel: UserTimelineViewModel) {
+ self.viewModel = viewModel
+ }
+
+ override func didEnter(from previousState: GKState?) {
+ os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
+ }
+ }
+}
+
+extension UserTimelineViewModel.State {
+ class Initial: UserTimelineViewModel.State {
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ guard let viewModel = viewModel else { return false }
+ switch stateClass {
+ case is Reloading.Type:
+ return viewModel.userID.value != nil
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+ }
+
+ class Reloading: UserTimelineViewModel.State {
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Fail.Type:
+ return true
+ case is Idle.Type:
+ return true
+ case is NoMore.Type:
+ return true
+ case is NotAuthorized.Type, is Blocked.Type:
+ return true
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+
+ override func didEnter(from previousState: GKState?) {
+ super.didEnter(from: previousState)
+ guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
+
+ // reset
+ viewModel.statusFetchedResultsController.statusIDs.value = []
+
+ guard let userID = viewModel.userID.value, !userID.isEmpty else {
+ stateMachine.enter(Fail.self)
+ return
+ }
+
+ guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
+ stateMachine.enter(Fail.self)
+ return
+ }
+ let domain = activeMastodonAuthenticationBox.domain
+ let queryFilter = viewModel.queryFilter.value
+
+ viewModel.context.apiService.userTimeline(
+ domain: domain,
+ accountID: userID,
+ maxID: nil,
+ sinceID: nil,
+ excludeReplies: queryFilter.excludeReplies,
+ excludeReblogs: queryFilter.excludeReblogs,
+ onlyMedia: queryFilter.onlyMedia,
+ authorizationBox: activeMastodonAuthenticationBox
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { completion in
+
+ } receiveValue: { response in
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+
+ var hasNewStatusesAppend = false
+ var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value
+ for status in response.value {
+ guard !statusIDs.contains(status.id) else { continue }
+ statusIDs.append(status.id)
+ hasNewStatusesAppend = true
+ }
+
+ if hasNewStatusesAppend {
+ stateMachine.enter(Idle.self)
+ } else {
+ stateMachine.enter(NoMore.self)
+ }
+ viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
+ }
+ .store(in: &viewModel.disposeBag)
+ }
+ }
+
+ class Fail: UserTimelineViewModel.State {
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Reloading.Type, is LoadingMore.Type:
+ return true
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+ }
+
+ class Idle: UserTimelineViewModel.State {
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Reloading.Type, is LoadingMore.Type:
+ return true
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+ }
+
+ class LoadingMore: UserTimelineViewModel.State {
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Fail.Type:
+ return true
+ case is Idle.Type:
+ return true
+ case is NoMore.Type:
+ return true
+ case is NotAuthorized.Type, is Blocked.Type:
+ return true
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+
+ override func didEnter(from previousState: GKState?) {
+ super.didEnter(from: previousState)
+ guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
+
+ guard let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last else {
+ stateMachine.enter(Fail.self)
+ return
+ }
+
+ guard let userID = viewModel.userID.value, !userID.isEmpty else {
+ stateMachine.enter(Fail.self)
+ return
+ }
+
+ guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
+ stateMachine.enter(Fail.self)
+ return
+ }
+ let domain = activeMastodonAuthenticationBox.domain
+ let queryFilter = viewModel.queryFilter.value
+
+ viewModel.context.apiService.userTimeline(
+ domain: domain,
+ accountID: userID,
+ maxID: maxID,
+ sinceID: nil,
+ excludeReplies: queryFilter.excludeReplies,
+ excludeReblogs: queryFilter.excludeReblogs,
+ onlyMedia: queryFilter.onlyMedia,
+ authorizationBox: activeMastodonAuthenticationBox
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { completion in
+
+ } receiveValue: { response in
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+
+ var hasNewStatusesAppend = false
+ var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value
+ for status in response.value {
+ guard !statusIDs.contains(status.id) else { continue }
+ statusIDs.append(status.id)
+ hasNewStatusesAppend = true
+ }
+
+ if hasNewStatusesAppend {
+ stateMachine.enter(Idle.self)
+ } else {
+ stateMachine.enter(NoMore.self)
+ }
+ viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
+ }
+ .store(in: &viewModel.disposeBag)
+ }
+ }
+
+ class NotAuthorized: UserTimelineViewModel.State {
+
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Reloading.Type:
+ return true
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+
+ }
+
+ class Blocked: UserTimelineViewModel.State {
+
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Reloading.Type:
+ return true
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+
+ }
+
+ class Suspended: UserTimelineViewModel.State {
+
+ }
+
+ class NoMore: UserTimelineViewModel.State {
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Reloading.Type:
+ return true
+ case is NotAuthorized.Type, is Blocked.Type:
+ return true
+ case is Suspended.Type:
+ return true
+ default:
+ return false
+ }
+ }
+ }
+}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
new file mode 100644
index 000000000..a550dc829
--- /dev/null
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
@@ -0,0 +1,127 @@
+//
+// UserTimelineViewModel.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-29.
+//
+
+import os.log
+import UIKit
+import GameplayKit
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+import AlamofireImage
+
+class UserTimelineViewModel: NSObject {
+
+ var disposeBag = Set()
+
+ // input
+ let context: AppContext
+ let domain: CurrentValueSubject
+ let userID: CurrentValueSubject
+ let queryFilter: CurrentValueSubject
+ let statusFetchedResultsController: StatusFetchedResultsController
+
+ // output
+ var diffableDataSource: UITableViewDiffableDataSource?
+ private(set) lazy var stateMachine: GKStateMachine = {
+ let stateMachine = GKStateMachine(states: [
+ State.Initial(viewModel: self),
+ State.Reloading(viewModel: self),
+ State.Fail(viewModel: self),
+ State.Idle(viewModel: self),
+ State.LoadingMore(viewModel: self),
+ State.NotAuthorized(viewModel: self),
+ State.Blocked(viewModel: self),
+ State.Suspended(viewModel: self),
+ State.NoMore(viewModel: self),
+ ])
+ stateMachine.enter(State.Initial.self)
+ return stateMachine
+ }()
+
+ init(context: AppContext, domain: String?, userID: String?, queryFilter: QueryFilter) {
+ self.context = context
+ self.statusFetchedResultsController = StatusFetchedResultsController(
+ managedObjectContext: context.managedObjectContext,
+ domain: domain,
+ additionalTweetPredicate: Status.notDeleted()
+ )
+ self.domain = CurrentValueSubject(domain)
+ self.userID = CurrentValueSubject(userID)
+ self.queryFilter = CurrentValueSubject(queryFilter)
+ super.init()
+
+ self.domain
+ .assign(to: \.value, on: statusFetchedResultsController.domain)
+ .store(in: &disposeBag)
+
+
+ statusFetchedResultsController.objectIDs
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] objectIDs in
+ guard let self = self else { return }
+ guard let diffableDataSource = self.diffableDataSource else { return }
+
+ // var isPermissionDenied = false
+
+ var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
+ let oldSnapshot = diffableDataSource.snapshot()
+ for item in oldSnapshot.itemIdentifiers {
+ guard case let .status(objectID, attribute) = item else { continue }
+ oldSnapshotAttributeDict[objectID] = attribute
+ }
+
+ var snapshot = NSDiffableDataSourceSnapshot()
+ snapshot.appendSections([.main])
+
+ var items: [Item] = []
+ for objectID in objectIDs {
+ let attribute = oldSnapshotAttributeDict[objectID] ?? Item.StatusAttribute()
+ items.append(.status(objectID: objectID, attribute: attribute))
+ }
+ snapshot.appendItems(items, toSection: .main)
+
+ if let currentState = self.stateMachine.currentState {
+ switch currentState {
+ case is State.Reloading, is State.LoadingMore, is State.Idle, is State.Fail:
+ snapshot.appendItems([.bottomLoader], toSection: .main)
+ // TODO: handle other states
+ default:
+ break
+ }
+ }
+
+ // not animate when empty items fix loader first appear layout issue
+ diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
+ }
+ .store(in: &disposeBag)
+ }
+
+ deinit {
+ os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+extension UserTimelineViewModel {
+ struct QueryFilter {
+ let excludeReplies: Bool?
+ let excludeReblogs: Bool?
+ let onlyMedia: Bool?
+
+ init(
+ excludeReplies: Bool? = nil,
+ excludeReblogs: Bool? = nil,
+ onlyMedia: Bool? = nil
+ ) {
+ self.excludeReplies = excludeReplies
+ self.excludeReblogs = excludeReblogs
+ self.onlyMedia = onlyMedia
+ }
+ }
+
+}
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift
index 7d52a5764..a92b8f37e 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController+StatusProvider.swift
@@ -15,11 +15,11 @@ import MastodonSDK
// MARK: - StatusProvider
extension PublicTimelineViewController: StatusProvider {
- func toot() -> Future {
+ func status() -> Future {
return Future { promise in promise(.success(nil)) }
}
- func toot(for cell: UITableViewCell, indexPath: IndexPath?) -> Future {
+ func status(for cell: UITableViewCell, indexPath: IndexPath?) -> Future {
return Future { promise in
guard let diffableDataSource = self.viewModel.diffableDataSource else {
assertionFailure()
@@ -33,11 +33,11 @@ extension PublicTimelineViewController: StatusProvider {
}
switch item {
- case .toot(let objectID, _):
+ case .status(let objectID, _):
let managedObjectContext = self.viewModel.fetchedResultsController.managedObjectContext
managedObjectContext.perform {
- let toot = managedObjectContext.object(with: objectID) as? Toot
- promise(.success(toot))
+ let status = managedObjectContext.object(with: objectID) as? Status
+ promise(.success(status))
}
default:
promise(.success(nil))
@@ -45,7 +45,7 @@ extension PublicTimelineViewController: StatusProvider {
}
}
- func toot(for cell: UICollectionViewCell) -> Future {
+ func status(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
index 8e954c053..844c43cd8 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewController.swift
@@ -159,13 +159,13 @@ extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
- func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID: NSManagedObjectID?) {
- guard let upperTimelineTootID = upperTimelineTootID else {return}
+ func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID: NSManagedObjectID?) {
+ guard let upperTimelineStatusID = upperTimelineStatusID else {return}
viewModel.loadMiddleSateMachineList
.receive(on: DispatchQueue.main)
.sink { [weak self] ids in
guard let _ = self else { return }
- if let stateMachine = ids[upperTimelineTootID] {
+ if let stateMachine = ids[upperTimelineStatusID] {
guard let state = stateMachine.currentState else {
assertionFailure()
return
@@ -185,17 +185,17 @@ extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegat
.store(in: &cell.disposeBag)
var dict = viewModel.loadMiddleSateMachineList.value
- if let _ = dict[upperTimelineTootID] {
+ if let _ = dict[upperTimelineStatusID] {
// do nothing
} else {
let stateMachine = GKStateMachine(states: [
- PublicTimelineViewModel.LoadMiddleState.Initial(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
- PublicTimelineViewModel.LoadMiddleState.Loading(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
- PublicTimelineViewModel.LoadMiddleState.Fail(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
- PublicTimelineViewModel.LoadMiddleState.Success(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
+ PublicTimelineViewModel.LoadMiddleState.Initial(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
+ PublicTimelineViewModel.LoadMiddleState.Loading(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
+ PublicTimelineViewModel.LoadMiddleState.Fail(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
+ PublicTimelineViewModel.LoadMiddleState.Success(viewModel: viewModel, upperTimelineStatusID: upperTimelineStatusID),
])
stateMachine.enter(PublicTimelineViewModel.LoadMiddleState.Initial.self)
- dict[upperTimelineTootID] = stateMachine
+ dict[upperTimelineStatusID] = stateMachine
viewModel.loadMiddleSateMachineList.value = dict
}
}
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
index d69da8f65..ce0e8b19d 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift
@@ -41,32 +41,32 @@ 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 indexTootTuples: [(Int, Toot)] = toots
- .compactMap { toot -> (Int, Toot)? in
- guard toot.deletedAt == nil else { return nil }
- return indexes.firstIndex(of: toot.id).map { index in (index, toot) }
+ let indexes = statusIDs.value
+ let statuses = fetchedResultsController.fetchedObjects ?? []
+ guard statuses.count == indexes.count else { return }
+ let indexStatusTuples: [(Int, Status)] = statuses
+ .compactMap { status -> (Int, Status)? in
+ guard status.deletedAt == nil else { return nil }
+ return indexes.firstIndex(of: status.id).map { index in (index, status) }
}
.sorted { $0.0 < $1.0 }
var oldSnapshotAttributeDict: [NSManagedObjectID: Item.StatusAttribute] = [:]
for item in self.items.value {
- guard case let .toot(objectID, attribute) = item else { continue }
+ guard case let .status(objectID, attribute) = item else { continue }
oldSnapshotAttributeDict[objectID] = attribute
}
var items = [Item]()
- for (_, toot) in indexTootTuples {
- let targetToot = toot.reblog ?? toot
+ for (_, status) in indexStatusTuples {
+ let targetStatus = status.reblog ?? status
let isStatusTextSensitive: Bool = {
- guard let spoilerText = targetToot.spoilerText, !spoilerText.isEmpty else { return false }
+ guard let spoilerText = targetStatus.spoilerText, !spoilerText.isEmpty else { return false }
return true
}()
- let attribute = oldSnapshotAttributeDict[toot.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetToot.sensitive)
- items.append(Item.toot(objectID: toot.objectID, attribute: attribute))
- if tootIDsWhichHasGap.contains(toot.id) {
- items.append(Item.publicMiddleLoader(tootID: toot.id))
+ let attribute = oldSnapshotAttributeDict[status.objectID] ?? Item.StatusAttribute(isStatusTextSensitive: isStatusTextSensitive, isStatusSensitive: targetStatus.sensitive)
+ items.append(Item.status(objectID: status.objectID, attribute: attribute))
+ if statusIDsWhichHasGap.contains(status.id) {
+ items.append(Item.publicMiddleLoader(statusID: status.id))
}
}
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+LoadMiddleState.swift
index 62334c746..4727072bf 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+LoadMiddleState.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+LoadMiddleState.swift
@@ -14,18 +14,18 @@ import os.log
extension PublicTimelineViewModel {
class LoadMiddleState: GKState {
weak var viewModel: PublicTimelineViewModel?
- let upperTimelineTootID: String
+ let upperTimelineStatusID: String
- init(viewModel: PublicTimelineViewModel, upperTimelineTootID: String) {
+ init(viewModel: PublicTimelineViewModel, upperTimelineStatusID: String) {
self.viewModel = viewModel
- self.upperTimelineTootID = upperTimelineTootID
+ self.upperTimelineStatusID = upperTimelineStatusID
}
override func didEnter(from previousState: GKState?) {
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, self.debugDescription, previousState.debugDescription)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
var dict = viewModel.loadMiddleSateMachineList.value
- dict[self.upperTimelineTootID] = stateMachine
+ dict[self.upperTimelineStatusID] = stateMachine
viewModel.loadMiddleSateMachineList.value = dict // trigger value change
}
}
@@ -54,42 +54,42 @@ extension PublicTimelineViewModel.LoadMiddleState {
}
viewModel.context.apiService.publicTimeline(
domain: activeMastodonAuthenticationBox.domain,
- maxID: upperTimelineTootID
+ maxID: upperTimelineStatusID
)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
- os_log("%{public}s[%{public}ld], %{public}s: fetch toots failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
+ os_log("%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
stateMachine.enter(Fail.self)
case .finished:
break
}
} receiveValue: { response in
- let toots = response.value
- let addedToots = toots.filter { !viewModel.tootIDs.value.contains($0.id) }
+ let statuses = response.value
+ let addedStatuses = statuses.filter { !viewModel.statusIDs.value.contains($0.id) }
- guard let gapIndex = viewModel.tootIDs.value.firstIndex(of: self.upperTimelineTootID) else { return }
- let upToots = Array(viewModel.tootIDs.value[...gapIndex])
- let downToots = Array(viewModel.tootIDs.value[(gapIndex + 1)...])
+ guard let gapIndex = viewModel.statusIDs.value.firstIndex(of: self.upperTimelineStatusID) else { return }
+ let upStatuses = Array(viewModel.statusIDs.value[...gapIndex])
+ let downStatuses = Array(viewModel.statusIDs.value[(gapIndex + 1)...])
- // construct newTootIDs
- var newTootIDs = upToots
- newTootIDs.append(contentsOf: addedToots.map { $0.id })
- newTootIDs.append(contentsOf: downToots)
+ // construct newStatusIDs
+ var newStatusIDs = upStatuses
+ newStatusIDs.append(contentsOf: addedStatuses.map { $0.id })
+ newStatusIDs.append(contentsOf: downStatuses)
// remove old gap from viewmodel
- if let index = viewModel.tootIDsWhichHasGap.firstIndex(of: self.upperTimelineTootID) {
- viewModel.tootIDsWhichHasGap.remove(at: index)
+ if let index = viewModel.statusIDsWhichHasGap.firstIndex(of: self.upperTimelineStatusID) {
+ viewModel.statusIDsWhichHasGap.remove(at: index)
}
// add new gap from viewmodel if need
- let intersection = toots.filter { downToots.contains($0.id) }
+ let intersection = statuses.filter { downStatuses.contains($0.id) }
if intersection.isEmpty {
- addedToots.last.flatMap { viewModel.tootIDsWhichHasGap.append($0.id) }
+ addedStatuses.last.flatMap { viewModel.statusIDsWhichHasGap.append($0.id) }
}
- viewModel.tootIDs.value = newTootIDs
- os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld toots, %{public}%ld new toots", (#file as NSString).lastPathComponent, #line, #function, toots.count, addedToots.count)
- if addedToots.isEmpty {
+ viewModel.statusIDs.value = newStatusIDs
+ os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld statuses, %{public}%ld new statues", (#file as NSString).lastPathComponent, #line, #function, statuses.count, addedStatuses.count)
+ if addedStatuses.isEmpty {
stateMachine.enter(Fail.self)
} else {
stateMachine.enter(Success.self)
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift
index 258db0cdd..c165adb70 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+State.swift
@@ -68,21 +68,21 @@ extension PublicTimelineViewModel.State {
break
}
} receiveValue: { response in
- let resposeTootIDs = response.value.compactMap { $0.id }
- var newTootsIDs = resposeTootIDs
- let oldTootsIDs = viewModel.tootIDs.value
+ let resposeStatusIDs = response.value.compactMap { $0.id }
+ var newStatusIDs = resposeStatusIDs
+ let oldStatusIDs = viewModel.statusIDs.value
var hasGap = true
- for tootID in oldTootsIDs {
- if !newTootsIDs.contains(tootID) {
- newTootsIDs.append(tootID)
+ for statusID in oldStatusIDs {
+ if !newStatusIDs.contains(statusID) {
+ newStatusIDs.append(statusID)
} else {
hasGap = false
}
}
- if hasGap && oldTootsIDs.count > 0 {
- resposeTootIDs.last.flatMap { viewModel.tootIDsWhichHasGap.append($0) }
+ if hasGap && oldStatusIDs.count > 0 {
+ resposeStatusIDs.last.flatMap { viewModel.statusIDsWhichHasGap.append($0) }
}
- viewModel.tootIDs.value = newTootsIDs
+ viewModel.statusIDs.value = newStatusIDs
stateMachine.enter(Idle.self)
}
.store(in: &viewModel.disposeBag)
@@ -138,7 +138,7 @@ extension PublicTimelineViewModel.State {
stateMachine.enter(Fail.self)
return
}
- let maxID = viewModel.tootIDs.value.last
+ let maxID = viewModel.statusIDs.value.last
viewModel.context.apiService.publicTimeline(
domain: activeMastodonAuthenticationBox.domain,
maxID: maxID
@@ -153,14 +153,14 @@ extension PublicTimelineViewModel.State {
}
} receiveValue: { response in
stateMachine.enter(Idle.self)
- var oldTootsIDs = viewModel.tootIDs.value
- for toot in response.value {
- if !oldTootsIDs.contains(toot.id) {
- oldTootsIDs.append(toot.id)
+ var oldStatusIDs = viewModel.statusIDs.value
+ for status in response.value {
+ if !oldStatusIDs.contains(status.id) {
+ oldStatusIDs.append(status.id)
}
}
- viewModel.tootIDs.value = oldTootsIDs
+ viewModel.statusIDs.value = oldStatusIDs
}
.store(in: &viewModel.disposeBag)
}
diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift
index d7d6448a5..c3b1a3d4b 100644
--- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift
+++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift
@@ -19,7 +19,7 @@ class PublicTimelineViewModel: NSObject {
// input
let context: AppContext
- let fetchedResultsController: NSFetchedResultsController
+ let fetchedResultsController: NSFetchedResultsController
let isFetchingLatestTimeline = CurrentValueSubject(false)
@@ -31,7 +31,7 @@ class PublicTimelineViewModel: NSObject {
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
//
- var tootIDsWhichHasGap = [String]()
+ var statusIDsWhichHasGap = [String]()
// output
var diffableDataSource: UITableViewDiffableDataSource?
@@ -47,15 +47,15 @@ class PublicTimelineViewModel: NSObject {
return stateMachine
}()
- let tootIDs = CurrentValueSubject<[String], Never>([])
+ let statusIDs = 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(domain: "", ids: [])
+ let fetchRequest = Status.sortedFetchRequest
+ fetchRequest.predicate = Status.predicate(domain: "", ids: [])
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
@@ -111,12 +111,12 @@ class PublicTimelineViewModel: NSObject {
}
.store(in: &disposeBag)
- tootIDs
+ statusIDs
.receive(on: DispatchQueue.main)
.sink { [weak self] ids in
guard let self = self else { return }
let domain = self.context.authenticationService.activeMastodonAuthenticationBox.value?.domain ?? ""
- self.fetchedResultsController.fetchRequest.predicate = Toot.predicate(domain: domain, ids: ids)
+ self.fetchedResultsController.fetchRequest.predicate = Status.predicate(domain: domain, ids: ids)
do {
try self.fetchedResultsController.performFetch()
} catch {
diff --git a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift
new file mode 100644
index 000000000..8ba7c2257
--- /dev/null
+++ b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift
@@ -0,0 +1,16 @@
+//
+// AdaptiveStatusBarStyleNavigationController.swift
+//
+//
+// Created by MainasuK Cirno on 2021-2-26.
+//
+
+import UIKit
+
+// Make status bar style adptive for child view controller
+// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
+final class AdaptiveStatusBarStyleNavigationController: UINavigationController {
+ override var childForStatusBarStyle: UIViewController? {
+ return visibleViewController
+ }
+}
diff --git a/Mastodon/Scene/Share/NavigationController/DarkContentStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/DarkContentStatusBarStyleNavigationController.swift
deleted file mode 100644
index 0fa4a0e20..000000000
--- a/Mastodon/Scene/Share/NavigationController/DarkContentStatusBarStyleNavigationController.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-//
-// DarkContentStatusBarStyleNavigationController.swift
-//
-//
-// Created by MainasuK Cirno on 2021-2-26.
-//
-
-import UIKit
-
-final class DarkContentStatusBarStyleNavigationController: UINavigationController {
- override var preferredStatusBarStyle: UIStatusBarStyle {
- return .darkContent
- }
-}
diff --git a/Mastodon/Scene/Share/View/Button/RoundedEdgesButton.swift b/Mastodon/Scene/Share/View/Button/RoundedEdgesButton.swift
index a38b711dd..266fa6594 100644
--- a/Mastodon/Scene/Share/View/Button/RoundedEdgesButton.swift
+++ b/Mastodon/Scene/Share/View/Button/RoundedEdgesButton.swift
@@ -7,7 +7,7 @@
import UIKit
-final class RoundedEdgesButton: UIButton {
+class RoundedEdgesButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift
index 6d7800b04..b4105a4d2 100644
--- a/Mastodon/Scene/Share/View/Content/StatusView.swift
+++ b/Mastodon/Scene/Share/View/Content/StatusView.swift
@@ -12,6 +12,8 @@ import ActiveLabel
import AlamofireImage
protocol StatusViewDelegate: class {
+ func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
+ func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton)
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
@@ -195,8 +197,9 @@ final class StatusView: UIView {
return actionToolbarContainer
}()
-
let activeTextLabel = ActiveLabel(style: .default)
+
+ private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
override init(frame: CGRect) {
super.init(frame: frame)
@@ -226,7 +229,7 @@ final class StatusView: UIView {
extension StatusView {
func _init() {
- // container: [retoot | author | status | action toolbar]
+ // container: [reblog | author | status | action toolbar]
let containerStackView = UIStackView()
containerStackView.axis = .vertical
containerStackView.spacing = 10
@@ -401,6 +404,12 @@ extension StatusView {
playerContainerView.delegate = self
+ headerInfoLabelTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerInfoLabelTapGestureRecognizerHandler(_:)))
+ headerInfoLabel.isUserInteractionEnabled = true
+ headerInfoLabel.addGestureRecognizer(headerInfoLabelTapGestureRecognizer)
+
+ avatarButton.addTarget(self, action: #selector(StatusView.avatarButtonDidPressed(_:)), for: .touchUpInside)
+ avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside)
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
}
@@ -439,6 +448,21 @@ extension StatusView {
extension StatusView {
+ @objc private func headerInfoLabelTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ delegate?.statusView(self, headerInfoLabelDidPressed: headerInfoLabel)
+ }
+
+ @objc private func avatarButtonDidPressed(_ sender: UIButton) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ delegate?.statusView(self, avatarButtonDidPressed: sender)
+ }
+
+ @objc private func avatarStackedContainerButtonDidPressed(_ sender: UIButton) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ delegate?.statusView(self, avatarButtonDidPressed: sender)
+ }
+
@objc private func contentWarningActionButtonPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.statusView(self, contentWarningActionButtonPressed: sender)
diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
index 5f9bdf654..9c954e505 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell.swift
@@ -20,6 +20,8 @@ protocol StatusTableViewCellDelegate: class {
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
+ func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, headerInfoLabelDidPressed label: UILabel)
+ func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, avatarButtonDidPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
@@ -194,6 +196,14 @@ extension StatusTableViewCell: UITableViewDelegate {
// MARK: - StatusViewDelegate
extension StatusTableViewCell: StatusViewDelegate {
+ func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
+ delegate?.statusTableViewCell(self, statusView: statusView, headerInfoLabelDidPressed: label)
+ }
+
+ func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) {
+ delegate?.statusTableViewCell(self, statusView: statusView, avatarButtonDidPressed: button)
+ }
+
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton) {
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
}
diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift
index 597420481..75c06a339 100644
--- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift
+++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift
@@ -11,7 +11,7 @@ import os.log
import UIKit
protocol TimelineMiddleLoaderTableViewCellDelegate: class {
- func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String?, timelineIndexobjectID:NSManagedObjectID?)
+ func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineStatusID: String?, timelineIndexobjectID:NSManagedObjectID?)
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton)
}
diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift
index 2eacff573..d49a7371b 100644
--- a/Mastodon/Service/APIService/APIService+Favorite.swift
+++ b/Mastodon/Service/APIService/APIService+Favorite.swift
@@ -17,29 +17,29 @@ extension APIService {
// make local state change only
func like(
- tootObjectID: NSManagedObjectID,
+ statusObjectID: NSManagedObjectID,
mastodonUserObjectID: NSManagedObjectID,
favoriteKind: Mastodon.API.Favorites.FavoriteKind
- ) -> AnyPublisher {
- var _targetTootID: Toot.ID?
+ ) -> AnyPublisher {
+ var _targetStatusID: Status.ID?
let managedObjectContext = backgroundManagedObjectContext
return managedObjectContext.performChanges {
- let toot = managedObjectContext.object(with: tootObjectID) as! Toot
+ let status = managedObjectContext.object(with: statusObjectID) as! Status
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
- let targetToot = toot.reblog ?? toot
- let targetTootID = targetToot.id
- _targetTootID = targetTootID
+ let targetStatus = status.reblog ?? status
+ let targetStatusID = targetStatus.id
+ _targetStatusID = targetStatusID
- targetToot.update(liked: favoriteKind == .create, mastodonUser: mastodonUser)
+ targetStatus.update(liked: favoriteKind == .create, by: mastodonUser)
}
.tryMap { result in
switch result {
case .success:
- guard let targetTootID = _targetTootID else {
+ guard let targetStatusID = _targetStatusID else {
throw APIError.implicit(.badRequest)
}
- return targetTootID
+ return targetStatusID
case .failure(let error):
assertionFailure(error.localizedDescription)
@@ -76,12 +76,12 @@ extension APIService {
return nil
}
}()
- let _oldToot: Toot? = {
- let request = Toot.sortedFetchRequest
- request.predicate = Toot.predicate(domain: mastodonAuthenticationBox.domain, id: statusID)
+ let _oldStatus: Status? = {
+ let request = Status.sortedFetchRequest
+ request.predicate = Status.predicate(domain: mastodonAuthenticationBox.domain, id: statusID)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
- request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
+ request.relationshipKeyPathsForPrefetching = [#keyPath(Status.reblog)]
do {
return try managedObjectContext.fetch(request).first
} catch {
@@ -91,15 +91,15 @@ extension APIService {
}()
guard let requestMastodonUser = _requestMastodonUser,
- let oldToot = _oldToot else {
+ let oldStatus = _oldStatus else {
assertionFailure()
return
}
- APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
+ APIService.CoreData.merge(status: oldStatus, entity: entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
if favoriteKind == .destroy {
- oldToot.update(favouritesCount: NSNumber(value: max(0, oldToot.favouritesCount.intValue - 1)))
+ oldStatus.update(favouritesCount: NSNumber(value: max(0, oldStatus.favouritesCount.intValue - 1)))
}
- os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "", entity.favouritesCount )
+ os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update status %{public}s like status to: %{public}s. now %ld likes", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.favourited.flatMap { $0 ? "like" : "unlike" } ?? "", entity.favouritesCount )
}
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content in
@@ -129,7 +129,7 @@ extension APIService {
extension APIService {
func likeList(
- limit: Int = onceRequestTootMaxCount,
+ limit: Int = onceRequestStatusMaxCount,
userID: String,
maxID: String? = nil,
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
diff --git a/Mastodon/Service/APIService/APIService+HomeTimeline.swift b/Mastodon/Service/APIService/APIService+HomeTimeline.swift
index 2fbb45e0b..d4cbe69c2 100644
--- a/Mastodon/Service/APIService/APIService+HomeTimeline.swift
+++ b/Mastodon/Service/APIService/APIService+HomeTimeline.swift
@@ -19,7 +19,7 @@ extension APIService {
domain: String,
sinceID: Mastodon.Entity.Status.ID? = nil,
maxID: Mastodon.Entity.Status.ID? = nil,
- limit: Int = onceRequestTootMaxCount,
+ limit: Int = onceRequestStatusMaxCount,
local: Bool? = nil,
authorizationBox: AuthenticationService.MastodonAuthenticationBox
) -> AnyPublisher, Error> {
diff --git a/Mastodon/Service/APIService/APIService+PublicTimeline.swift b/Mastodon/Service/APIService/APIService+PublicTimeline.swift
index cd02526d6..bd176f311 100644
--- a/Mastodon/Service/APIService/APIService+PublicTimeline.swift
+++ b/Mastodon/Service/APIService/APIService+PublicTimeline.swift
@@ -21,7 +21,7 @@ extension APIService {
domain: String,
sinceID: Mastodon.Entity.Status.ID? = nil,
maxID: Mastodon.Entity.Status.ID? = nil,
- limit: Int = onceRequestTootMaxCount
+ limit: Int = onceRequestStatusMaxCount
) -> AnyPublisher, Error> {
let query = Mastodon.API.Timeline.PublicTimelineQuery(
local: nil,
diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift
index 92ff85c10..6dc9f189b 100644
--- a/Mastodon/Service/APIService/APIService+Reblog.swift
+++ b/Mastodon/Service/APIService/APIService+Reblog.swift
@@ -16,34 +16,34 @@ extension APIService {
// make local state change only
func reblog(
- tootObjectID: NSManagedObjectID,
+ statusObjectID: NSManagedObjectID,
mastodonUserObjectID: NSManagedObjectID,
reblogKind: Mastodon.API.Reblog.ReblogKind
- ) -> AnyPublisher {
- var _targetTootID: Toot.ID?
+ ) -> AnyPublisher {
+ var _targetStatusID: Status.ID?
let managedObjectContext = backgroundManagedObjectContext
return managedObjectContext.performChanges {
- let toot = managedObjectContext.object(with: tootObjectID) as! Toot
+ let status = managedObjectContext.object(with: statusObjectID) as! Status
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
- let targetToot = toot.reblog ?? toot
- let targetTootID = targetToot.id
- _targetTootID = targetTootID
+ let targetStatus = status.reblog ?? status
+ let targetStatusID = targetStatus.id
+ _targetStatusID = targetStatusID
switch reblogKind {
case .reblog:
- targetToot.update(reblogged: true, mastodonUser: mastodonUser)
+ targetStatus.update(reblogged: true, by: mastodonUser)
case .undoReblog:
- targetToot.update(reblogged: false, mastodonUser: mastodonUser)
+ targetStatus.update(reblogged: false, by: mastodonUser)
}
}
.tryMap { result in
switch result {
case .success:
- guard let targetTootID = _targetTootID else {
+ guard let targetStatusID = _targetStatusID else {
throw APIError.implicit(.badRequest)
}
- return targetTootID
+ return targetStatusID
case .failure(let error):
assertionFailure(error.localizedDescription)
@@ -85,25 +85,25 @@ extension APIService {
return
}
- guard let oldToot: Toot = {
- let request = Toot.sortedFetchRequest
- request.predicate = Toot.predicate(domain: domain, id: statusID)
+ guard let oldStatus: Status = {
+ let request = Status.sortedFetchRequest
+ request.predicate = Status.predicate(domain: domain, id: statusID)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
- request.relationshipKeyPathsForPrefetching = [#keyPath(Toot.reblog)]
+ request.relationshipKeyPathsForPrefetching = [#keyPath(Status.reblog)]
return managedObjectContext.safeFetch(request).first
}() else {
return
}
- APIService.CoreData.merge(toot: oldToot, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
+ APIService.CoreData.merge(status: oldStatus, entity: entity.reblog ?? entity, requestMastodonUser: requestMastodonUser, domain: mastodonAuthenticationBox.domain, networkDate: response.networkDate)
switch reblogKind {
case .undoReblog:
- oldToot.update(reblogsCount: NSNumber(value: max(0, oldToot.reblogsCount.intValue - 1)))
+ oldStatus.update(reblogsCount: NSNumber(value: max(0, oldStatus.reblogsCount.intValue - 1)))
default:
break
}
- os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update toot %{public}s reblog status to: %{public}s. now %ld reblog", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "reblog" : "unreblog" } ?? "", entity.reblogsCount )
+ os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: did update status %{public}s reblog status to: %{public}s. now %ld reblog", ((#file as NSString).lastPathComponent), #line, #function, entity.id, entity.reblogged.flatMap { $0 ? "reblog" : "unreblog" } ?? "", entity.reblogsCount )
}
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content in
diff --git a/Mastodon/Service/APIService/APIService+Relationship.swift b/Mastodon/Service/APIService/APIService+Relationship.swift
new file mode 100644
index 000000000..7ad5b4745
--- /dev/null
+++ b/Mastodon/Service/APIService/APIService+Relationship.swift
@@ -0,0 +1,65 @@
+//
+// APIService+Relationship.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-4-1.
+//
+
+import Foundation
+import Combine
+import CoreData
+import CoreDataStack
+import CommonOSLog
+import MastodonSDK
+
+extension APIService {
+
+ func relationship(
+ domain: String,
+ accountIDs: [Mastodon.Entity.Account.ID],
+ authorizationBox: AuthenticationService.MastodonAuthenticationBox
+ ) -> AnyPublisher, Error> {
+ fatalError()
+// let authorization = authorizationBox.userAuthorization
+// let requestMastodonUserID = authorizationBox.userID
+// let query = Mastodon.API.Account.AccountStatuseseQuery(
+// maxID: maxID,
+// sinceID: sinceID,
+// excludeReplies: excludeReplies,
+// excludeReblogs: excludeReblogs,
+// onlyMedia: onlyMedia,
+// limit: limit
+// )
+//
+// return Mastodon.API.Account.statuses(
+// session: session,
+// domain: domain,
+// accountID: accountID,
+// query: query,
+// authorization: authorization
+// )
+// .flatMap { response -> AnyPublisher, Error> in
+// return APIService.Persist.persistStatus(
+// managedObjectContext: self.backgroundManagedObjectContext,
+// domain: domain,
+// query: nil,
+// response: response,
+// persistType: .user,
+// requestMastodonUserID: requestMastodonUserID,
+// log: OSLog.api
+// )
+// .setFailureType(to: Error.self)
+// .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
+// switch result {
+// case .success:
+// return response
+// case .failure(let error):
+// throw error
+// }
+// }
+// .eraseToAnyPublisher()
+// }
+// .eraseToAnyPublisher()
+ }
+
+}
diff --git a/Mastodon/Service/APIService/APIService+UserTimeline.swift b/Mastodon/Service/APIService/APIService+UserTimeline.swift
new file mode 100644
index 000000000..cb20c85ef
--- /dev/null
+++ b/Mastodon/Service/APIService/APIService+UserTimeline.swift
@@ -0,0 +1,70 @@
+//
+// APIService+UserTimeline.swift
+// Mastodon
+//
+// Created by MainasuK Cirno on 2021-3-30.
+//
+
+import Foundation
+import Combine
+import CoreData
+import CoreDataStack
+import CommonOSLog
+import MastodonSDK
+
+extension APIService {
+
+ func userTimeline(
+ domain: String,
+ accountID: String,
+ maxID: Mastodon.Entity.Status.ID? = nil,
+ sinceID: Mastodon.Entity.Status.ID? = nil,
+ limit: Int = onceRequestStatusMaxCount,
+ excludeReplies: Bool? = nil,
+ excludeReblogs: Bool? = nil,
+ onlyMedia: Bool? = nil,
+ authorizationBox: AuthenticationService.MastodonAuthenticationBox
+ ) -> AnyPublisher, Error> {
+ let authorization = authorizationBox.userAuthorization
+ let requestMastodonUserID = authorizationBox.userID
+ let query = Mastodon.API.Account.AccountStatuseseQuery(
+ maxID: maxID,
+ sinceID: sinceID,
+ excludeReplies: excludeReplies,
+ excludeReblogs: excludeReblogs,
+ onlyMedia: onlyMedia,
+ limit: limit
+ )
+
+ return Mastodon.API.Account.statuses(
+ session: session,
+ domain: domain,
+ accountID: accountID,
+ query: query,
+ authorization: authorization
+ )
+ .flatMap { response -> AnyPublisher, Error> in
+ return APIService.Persist.persistStatus(
+ managedObjectContext: self.backgroundManagedObjectContext,
+ domain: domain,
+ query: nil,
+ response: response,
+ persistType: .user,
+ requestMastodonUserID: requestMastodonUserID,
+ log: OSLog.api
+ )
+ .setFailureType(to: Error.self)
+ .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
+ switch result {
+ case .success:
+ return response
+ case .failure(let error):
+ throw error
+ }
+ }
+ .eraseToAnyPublisher()
+ }
+ .eraseToAnyPublisher()
+ }
+
+}
diff --git a/Mastodon/Service/APIService/APIService.swift b/Mastodon/Service/APIService/APIService.swift
index 655684cc5..11e6f5cac 100644
--- a/Mastodon/Service/APIService/APIService.swift
+++ b/Mastodon/Service/APIService/APIService.swift
@@ -44,7 +44,7 @@ final class APIService {
}
extension APIService {
- public static let onceRequestTootMaxCount = 100
+ public static let onceRequestStatusMaxCount = 100
public static let onceRequestUserMaxCount = 100
}
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift
index 512e224d2..745f47999 100644
--- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift
@@ -48,11 +48,11 @@ extension APIService.CoreData {
if let oldMastodonUser = oldMastodonUser {
// merge old mastodon usre
- APIService.CoreData.mergeMastodonUser(
- for: requestMastodonUser,
- old: oldMastodonUser,
- in: domain,
+ APIService.CoreData.merge(
+ user: oldMastodonUser,
entity: entity,
+ requestMastodonUser: requestMastodonUser,
+ domain: domain,
networkDate: networkDate
)
return (oldMastodonUser, false)
@@ -68,11 +68,15 @@ extension APIService.CoreData {
}
}
- static func mergeMastodonUser(
- for requestMastodonUser: MastodonUser?,
- old user: MastodonUser,
- in domain: String,
+}
+
+extension APIService.CoreData {
+
+ static func merge(
+ user: MastodonUser,
entity: Mastodon.Entity.Account,
+ requestMastodonUser: MastodonUser?,
+ domain: String,
networkDate: Date
) {
guard networkDate > user.updatedAt else { return }
@@ -84,6 +88,38 @@ extension APIService.CoreData {
user.update(displayName: property.displayName)
user.update(avatar: property.avatar)
user.update(avatarStatic: property.avatarStatic)
+ user.update(header: property.header)
+ user.update(headerStatic: property.headerStatic)
+ user.update(note: property.note)
+ user.update(url: property.url)
+ user.update(statusesCount: property.statusesCount)
+ user.update(followingCount: property.followingCount)
+ user.update(followersCount: property.followersCount)
+
+ user.didUpdate(at: networkDate)
+ }
+
+}
+
+extension APIService.CoreData {
+
+ static func update(
+ user: MastodonUser,
+ entity: Mastodon.Entity.Relationship,
+ requestMastodonUser: MastodonUser,
+ domain: String,
+ networkDate: Date
+ ) {
+ guard networkDate > user.updatedAt else { return }
+
+ user.update(isFollowing: entity.following, by: requestMastodonUser)
+ entity.requested.flatMap { user.update(isFollowRequested: $0, by: requestMastodonUser) }
+ entity.endorsed.flatMap { user.update(isEndorsed: $0, by: requestMastodonUser) }
+ requestMastodonUser.update(isFollowing: entity.followedBy, by: user)
+ entity.muting.flatMap { user.update(isMuting: $0, by: requestMastodonUser) }
+ user.update(isBlocking: entity.blocking, by: requestMastodonUser)
+ entity.domainBlocking.flatMap { user.update(isDomainBlocking: $0, by: requestMastodonUser) }
+ entity.blockedBy.flatMap { requestMastodonUser.update(isBlocking: $0, by: user) }
user.didUpdate(at: networkDate)
}
diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift
index 28bfd9727..a05574b6b 100644
--- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift
+++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Status.swift
@@ -18,39 +18,39 @@ extension APIService.CoreData {
for requestMastodonUser: MastodonUser?,
domain: String,
entity: Mastodon.Entity.Status,
- tootCache: APIService.Persist.PersistCache?,
+ statusCache: APIService.Persist.PersistCache?,
userCache: APIService.Persist.PersistCache?,
networkDate: Date,
log: OSLog
- ) -> (Toot: Toot, isTootCreated: Bool, isMastodonUserCreated: Bool) {
+ ) -> (status: Status, isStatusCreated: Bool, isMastodonUserCreated: Bool) {
let processEntityTaskSignpostID = OSSignpostID(log: log)
- os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id)
+ os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "process status %{public}s", entity.id)
defer {
- os_signpost(.end, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id)
+ os_signpost(.end, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "process status %{public}s", entity.id)
}
// build tree
- let reblog = entity.reblog.flatMap { entity -> Toot in
- let (toot, _, _) = createOrMergeStatus(
+ let reblog = entity.reblog.flatMap { entity -> Status in
+ let (status, _, _) = createOrMergeStatus(
into: managedObjectContext,
for: requestMastodonUser,
domain: domain,
entity: entity,
- tootCache: tootCache,
+ statusCache: statusCache,
userCache: userCache,
networkDate: networkDate,
log: log
)
- return toot
+ return status
}
- // fetch old Toot
- let oldToot: Toot? = {
- if let tootCache = tootCache {
- return tootCache.dictionary[entity.id]
+ // fetch old Status
+ let oldStatus: Status? = {
+ if let statusCache = statusCache {
+ return statusCache.dictionary[entity.id]
} else {
- let request = Toot.sortedFetchRequest
- request.predicate = Toot.predicate(domain: domain, id: entity.id)
+ let request = Status.sortedFetchRequest
+ request.predicate = Status.predicate(domain: domain, id: entity.id)
request.fetchLimit = 1
request.returnsObjectsAsFaults = false
do {
@@ -62,19 +62,19 @@ extension APIService.CoreData {
}
}()
- if let oldToot = oldToot {
- // merge old Toot
- APIService.CoreData.merge(toot: oldToot, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
- return (oldToot, false, false)
+ if let oldStatus = oldStatus {
+ // merge old Status
+ APIService.CoreData.merge(status: oldStatus, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
+ return (oldStatus, false, false)
} else {
let (mastodonUser, isMastodonUserCreated) = createOrMergeMastodonUser(into: managedObjectContext, for: requestMastodonUser,in: domain, entity: entity.account, userCache: userCache, networkDate: networkDate, log: log)
let application = entity.application.flatMap { app -> Application? in
Application.insert(into: managedObjectContext, property: Application.Property(name: app.name, website: app.website, vapidKey: app.vapidKey))
}
- let replyTo: Toot? = {
- // could be nil if target replyTo toot's persist task in the queue
+ let replyTo: Status? = {
+ // could be nil if target replyTo status's persist task in the queue
guard let inReplyToID = entity.inReplyToID,
- let replyTo = tootCache?.dictionary[inReplyToID] else { return nil }
+ let replyTo = statusCache?.dictionary[inReplyToID] else { return nil }
return replyTo
}()
let poll = entity.poll.flatMap { poll -> Poll in
@@ -111,10 +111,10 @@ extension APIService.CoreData {
guard !attachments.isEmpty else { return nil }
return attachments
}()
- let tootProperty = Toot.Property(entity: entity, domain: domain, networkDate: networkDate)
- let toot = Toot.insert(
+ let statusProperty = Status.Property(entity: entity, domain: domain, networkDate: networkDate)
+ let status = Status.insert(
into: managedObjectContext,
- property: tootProperty,
+ property: statusProperty,
author: mastodonUser,
reblog: reblog,
application: application,
@@ -130,67 +130,81 @@ extension APIService.CoreData {
bookmarkedBy: (entity.bookmarked ?? false) ? requestMastodonUser : nil,
pinnedBy: (entity.pinned ?? false) ? requestMastodonUser : nil
)
- tootCache?.dictionary[entity.id] = toot
- os_signpost(.event, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "did insert new tweet %{public}s: %s", mastodonUser.identifier, entity.id)
- return (toot, true, isMastodonUserCreated)
+ statusCache?.dictionary[entity.id] = status
+ os_signpost(.event, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "did insert new tweet %{public}s: %s", mastodonUser.identifier, entity.id)
+ return (status, true, isMastodonUserCreated)
}
}
+}
+
+extension APIService.CoreData {
static func merge(
- toot: Toot,
+ status: Status,
entity: Mastodon.Entity.Status,
requestMastodonUser: MastodonUser?,
domain: String,
networkDate: Date
) {
- guard networkDate > toot.updatedAt else { return }
+ guard networkDate > status.updatedAt else { return }
// merge poll
- if let poll = toot.poll, let entity = entity.poll {
+ if let poll = status.poll, let entity = entity.poll {
merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
}
// merge metrics
- if entity.favouritesCount != toot.favouritesCount.intValue {
- toot.update(favouritesCount:NSNumber(value: entity.favouritesCount))
+ if entity.favouritesCount != status.favouritesCount.intValue {
+ status.update(favouritesCount:NSNumber(value: entity.favouritesCount))
}
if let repliesCount = entity.repliesCount {
- if (repliesCount != toot.repliesCount?.intValue) {
- toot.update(repliesCount:NSNumber(value: repliesCount))
+ if (repliesCount != status.repliesCount?.intValue) {
+ status.update(repliesCount:NSNumber(value: repliesCount))
}
}
- if entity.reblogsCount != toot.reblogsCount.intValue {
- toot.update(reblogsCount:NSNumber(value: entity.reblogsCount))
+ if entity.reblogsCount != status.reblogsCount.intValue {
+ status.update(reblogsCount:NSNumber(value: entity.reblogsCount))
}
// merge relationship
if let mastodonUser = requestMastodonUser {
if let favourited = entity.favourited {
- toot.update(liked: favourited, mastodonUser: mastodonUser)
+ status.update(liked: favourited, by: mastodonUser)
}
if let reblogged = entity.reblogged {
- toot.update(reblogged: reblogged, mastodonUser: mastodonUser)
+ status.update(reblogged: reblogged, by: mastodonUser)
}
if let muted = entity.muted {
- toot.update(muted: muted, mastodonUser: mastodonUser)
+ status.update(muted: muted, by: mastodonUser)
}
if let bookmarked = entity.bookmarked {
- toot.update(bookmarked: bookmarked, mastodonUser: mastodonUser)
+ status.update(bookmarked: bookmarked, by: mastodonUser)
}
}
// set updateAt
- toot.didUpdate(at: networkDate)
+ status.didUpdate(at: networkDate)
// merge user
- mergeMastodonUser(for: requestMastodonUser, old: toot.author, in: domain, entity: entity.account, networkDate: networkDate)
+ merge(
+ user: status.author,
+ entity: entity.account,
+ requestMastodonUser: requestMastodonUser,
+ domain: domain,
+ networkDate: networkDate
+ )
// merge indirect reblog
- if let reblog = toot.reblog, let reblogEntity = entity.reblog {
- merge(toot: reblog, entity: reblogEntity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: networkDate)
+ if let reblog = status.reblog, let reblogEntity = entity.reblog {
+ merge(
+ status: reblog,
+ entity: reblogEntity,
+ requestMastodonUser: requestMastodonUser,
+ domain: domain,
+ networkDate: networkDate
+ )
}
}
-
}
extension APIService.CoreData {
diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift
index 16461494a..eb354035f 100644
--- a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift
+++ b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistCache.swift
@@ -17,23 +17,23 @@ extension APIService.Persist {
}
-extension APIService.Persist.PersistCache where T == Toot {
+extension APIService.Persist.PersistCache where T == Status {
- static func ids(for toots: [Mastodon.Entity.Status]) -> Set {
+ static func ids(for statuses: [Mastodon.Entity.Status]) -> Set {
var value = Set()
- for toot in toots {
- value = value.union(ids(for: toot))
+ for status in statuses {
+ value = value.union(ids(for: status))
}
return value
}
- static func ids(for toot: Mastodon.Entity.Status) -> Set {
+ static func ids(for status: Mastodon.Entity.Status) -> Set {
var value = Set()
- value.insert(toot.id)
- if let inReplyToID = toot.inReplyToID {
+ value.insert(status.id)
+ if let inReplyToID = status.inReplyToID {
value.insert(inReplyToID)
}
- if let reblog = toot.reblog {
+ if let reblog = status.reblog {
value = value.union(ids(for: reblog))
}
return value
@@ -43,21 +43,21 @@ extension APIService.Persist.PersistCache where T == Toot {
extension APIService.Persist.PersistCache where T == MastodonUser {
- static func ids(for toots: [Mastodon.Entity.Status]) -> Set {
+ static func ids(for statuses: [Mastodon.Entity.Status]) -> Set {
var value = Set()
- for toot in toots {
- value = value.union(ids(for: toot))
+ for status in statuses {
+ value = value.union(ids(for: status))
}
return value
}
- static func ids(for toot: Mastodon.Entity.Status) -> Set {
+ static func ids(for status: Mastodon.Entity.Status) -> Set {
var value = Set()
- value.insert(toot.account.id)
- if let inReplyToAccountID = toot.inReplyToAccountID {
+ value.insert(status.account.id)
+ if let inReplyToAccountID = status.inReplyToAccountID {
value.insert(inReplyToAccountID)
}
- if let reblog = toot.reblog {
+ if let reblog = status.reblog {
value = value.union(ids(for: reblog))
}
return value
diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift
index b74bb7771..dab4ba6ad 100644
--- a/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift
+++ b/Mastodon/Service/APIService/Persist/APIService+Persist+PersistMemo.swift
@@ -125,36 +125,36 @@ extension APIService.Persist.PersistMemo {
}
-extension APIService.Persist.PersistMemo where T == Toot, U == MastodonUser {
+extension APIService.Persist.PersistMemo where T == Status, U == MastodonUser {
- static func createOrMergeToot(
+ static func createOrMergeStatus(
into managedObjectContext: NSManagedObjectContext,
for requestMastodonUser: MastodonUser?,
requestMastodonUserID: MastodonUser.ID?,
domain: String,
entity: Mastodon.Entity.Status,
memoType: MemoType,
- tootCache: APIService.Persist.PersistCache?,
+ statusCache: APIService.Persist.PersistCache?,
userCache: APIService.Persist.PersistCache?,
networkDate: Date,
log: OSLog
) -> APIService.Persist.PersistMemo {
let processEntityTaskSignpostID = OSSignpostID(log: log)
- os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "process toot %{public}s", entity.id)
+ os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "process status %{public}s", entity.id)
defer {
- os_signpost(.end, log: log, name: "update database - process entity: createOrMergeToot", signpostID: processEntityTaskSignpostID, "finish process toot %{public}s", entity.id)
+ os_signpost(.end, log: log, name: "update database - process entity: createOrMergeStatus", signpostID: processEntityTaskSignpostID, "finish process status %{public}s", entity.id)
}
// build tree
let reblogMemo = entity.reblog.flatMap { entity -> APIService.Persist.PersistMemo in
- createOrMergeToot(
+ createOrMergeStatus(
into: managedObjectContext,
for: requestMastodonUser,
requestMastodonUserID: requestMastodonUserID,
domain: domain,
entity: entity,
memoType: .reblog,
- tootCache: tootCache,
+ statusCache: statusCache,
userCache: userCache,
networkDate: networkDate,
log: log
@@ -163,27 +163,27 @@ extension APIService.Persist.PersistMemo where T == Toot, U == MastodonUser {
let children = [reblogMemo].compactMap { $0 }
- let (toot, isTootCreated, isMastodonUserCreated) = APIService.CoreData.createOrMergeStatus(
+ let (status, isStatusCreated, isMastodonUserCreated) = APIService.CoreData.createOrMergeStatus(
into: managedObjectContext,
for: requestMastodonUser,
domain: domain,
entity: entity,
- tootCache: tootCache,
+ statusCache: statusCache,
userCache: userCache,
networkDate: networkDate,
log: log
)
let memo = APIService.Persist.PersistMemo(
- status: toot,
+ status: status,
children: children,
memoType: memoType,
- statusProcessType: isTootCreated ? .create : .merge,
+ statusProcessType: isStatusCreated ? .create : .merge,
authorProcessType: isMastodonUserCreated ? .create : .merge
)
switch (memo.statusProcessType, memoType) {
case (.create, .homeTimeline), (.merge, .homeTimeline):
- let timelineIndex = toot.homeTimelineIndexes?
+ let timelineIndex = status.homeTimelineIndexes?
.first { $0.userID == requestMastodonUserID }
guard let requestMastodonUserID = requestMastodonUserID else {
assertionFailure()
@@ -192,7 +192,7 @@ extension APIService.Persist.PersistMemo where T == Toot, U == MastodonUser {
if timelineIndex == nil {
// make it indexed
let timelineIndexProperty = HomeTimelineIndex.Property(domain: domain, userID: requestMastodonUserID)
- let _ = HomeTimelineIndex.insert(into: managedObjectContext, property: timelineIndexProperty, toot: toot)
+ let _ = HomeTimelineIndex.insert(into: managedObjectContext, property: timelineIndexProperty, status: status)
} else {
// enity already in home timeline
}
diff --git a/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift b/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift
index 2944e66a5..dfd41094a 100644
--- a/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift
+++ b/Mastodon/Service/APIService/Persist/APIService+Persist+Status.swift
@@ -18,6 +18,7 @@ extension APIService.Persist {
enum PersistTimelineType {
case `public`
case home
+ case user
case likeList
case lookUp
}
@@ -32,8 +33,8 @@ extension APIService.Persist {
log: OSLog
) -> AnyPublisher, Never> {
return managedObjectContext.performChanges {
- let toots = response.value
- os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: persist %{public}ld toots…", ((#file as NSString).lastPathComponent), #line, #function, toots.count)
+ let statuses = response.value
+ os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: persist %{public}ld statuses…", ((#file as NSString).lastPathComponent), #line, #function, statuses.count)
let contextTaskSignpostID = OSSignpostID(log: log)
let start = CACurrentMediaTime()
@@ -61,18 +62,18 @@ extension APIService.Persist {
// load working set into context to avoid cache miss
let cacheTaskSignpostID = OSSignpostID(log: log)
- os_signpost(.begin, log: log, name: "load toots & users into cache", signpostID: cacheTaskSignpostID)
+ os_signpost(.begin, log: log, name: "load statuses & users into cache", signpostID: cacheTaskSignpostID)
// contains reblog
- let tootCache: PersistCache