mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 16:21:00 +00:00
Status contexts
This commit is contained in:
parent
b5017d4805
commit
7863866f68
12 changed files with 304 additions and 9 deletions
|
@ -0,0 +1,17 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ContextEndpoint: Stubbing {
|
||||
func dataString(url: URL) -> String? {
|
||||
switch self {
|
||||
case .context:
|
||||
return """
|
||||
{
|
||||
"ancestors": [],
|
||||
"descendants": []
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,6 +44,16 @@
|
|||
D019E6EE24DF7BF300697C7D /* IdentityDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D019E6EC24DF7BF300697C7D /* IdentityDatabase.swift */; };
|
||||
D019E6F024DF7C2F00697C7D /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */; };
|
||||
D019E6F124DF7C2F00697C7D /* DatabaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */; };
|
||||
D020F50B24EC9F1D005AB084 /* ContextService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F50A24EC9F1D005AB084 /* ContextService.swift */; };
|
||||
D020F50C24EC9F1D005AB084 /* ContextService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F50A24EC9F1D005AB084 /* ContextService.swift */; };
|
||||
D020F50E24ECA25F005AB084 /* ContextEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F50D24ECA25F005AB084 /* ContextEndpoint.swift */; };
|
||||
D020F50F24ECA25F005AB084 /* ContextEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F50D24ECA25F005AB084 /* ContextEndpoint.swift */; };
|
||||
D020F51124ECA309005AB084 /* MastodonContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F51024ECA309005AB084 /* MastodonContext.swift */; };
|
||||
D020F51224ECA309005AB084 /* MastodonContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F51024ECA309005AB084 /* MastodonContext.swift */; };
|
||||
D020F51424ECBA60005AB084 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F51324ECBA60005AB084 /* LazyView.swift */; };
|
||||
D020F51524ECBA60005AB084 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D020F51324ECBA60005AB084 /* LazyView.swift */; };
|
||||
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03658D024EDD80900AC17EC /* ContextEndpoint+Stubbing.swift */; };
|
||||
D03658D224EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03658D024EDD80900AC17EC /* ContextEndpoint+Stubbing.swift */; };
|
||||
D03DF45B24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */; };
|
||||
D03DF45C24E62A68007A8CD5 /* DeletionEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */; };
|
||||
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D047FA8524C3E21000AF17C5 /* MetatextApp.swift */; };
|
||||
|
@ -272,6 +282,11 @@
|
|||
D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstanceEndpoint.swift; sourceTree = "<group>"; };
|
||||
D019E6EC24DF7BF300697C7D /* IdentityDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentityDatabase.swift; sourceTree = "<group>"; };
|
||||
D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseError.swift; sourceTree = "<group>"; };
|
||||
D020F50A24EC9F1D005AB084 /* ContextService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextService.swift; sourceTree = "<group>"; };
|
||||
D020F50D24ECA25F005AB084 /* ContextEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextEndpoint.swift; sourceTree = "<group>"; };
|
||||
D020F51024ECA309005AB084 /* MastodonContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonContext.swift; sourceTree = "<group>"; };
|
||||
D020F51324ECBA60005AB084 /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||
D03658D024EDD80900AC17EC /* ContextEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||
D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletionEndpoint.swift; sourceTree = "<group>"; };
|
||||
D047FA8524C3E21000AF17C5 /* MetatextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
|
||||
D047FA8724C3E21200AF17C5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -447,6 +462,7 @@
|
|||
D019E6DF24DF72E700697C7D /* AccessTokenEndpoint.swift */,
|
||||
D019E6DE24DF72E700697C7D /* AccountEndpoint.swift */,
|
||||
D019E6DC24DF72E700697C7D /* AppAuthorizationEndpoint.swift */,
|
||||
D020F50D24ECA25F005AB084 /* ContextEndpoint.swift */,
|
||||
D03DF45A24E62A68007A8CD5 /* DeletionEndpoint.swift */,
|
||||
D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */,
|
||||
D019E6DD24DF72E700697C7D /* PreferencesEndpoint.swift */,
|
||||
|
@ -548,6 +564,7 @@
|
|||
D054951024EB101F008B00A5 /* Status List Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D020F50A24EC9F1D005AB084 /* ContextService.swift */,
|
||||
D054951124EB1041008B00A5 /* StatusListService.swift */,
|
||||
D054951424EB1053008B00A5 /* TimelineService.swift */,
|
||||
);
|
||||
|
@ -575,6 +592,7 @@
|
|||
D0ED1BD624CF94B200B4899C /* Application.swift */,
|
||||
D05494E924EA3F54008B00A5 /* Attachment.swift */,
|
||||
D05494EF24EA3FE5008B00A5 /* Card.swift */,
|
||||
D020F51024ECA309005AB084 /* MastodonContext.swift */,
|
||||
D0666A5324C6C3E500F3F04B /* Emoji.swift */,
|
||||
D0666A4A24C6C37700F3F04B /* Identity.swift */,
|
||||
D0666A4D24C6C39600F3F04B /* Instance.swift */,
|
||||
|
@ -613,6 +631,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D0DB6EF324C5228A00D965FE /* AddIdentityView.swift */,
|
||||
D020F51324ECBA60005AB084 /* LazyView.swift */,
|
||||
D075817B24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift */,
|
||||
D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */,
|
||||
D0091B6D24DD68090040E8D2 /* PreferencesView.swift */,
|
||||
|
@ -666,6 +685,7 @@
|
|||
D0DC175424D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift */,
|
||||
D04FD73824D4A7B4007D572D /* AccountEndpoint+Stubbing.swift */,
|
||||
D0DC174924CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift */,
|
||||
D03658D024EDD80900AC17EC /* ContextEndpoint+Stubbing.swift */,
|
||||
D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */,
|
||||
D0DC175124D008E300A75C65 /* MastodonTarget+Stubbing.swift */,
|
||||
D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */,
|
||||
|
@ -963,17 +983,20 @@
|
|||
D075817924E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */,
|
||||
D0ED1BD724CF94B200B4899C /* Application.swift in Sources */,
|
||||
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
||||
D03658D124EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */,
|
||||
D0BEC94724CA22C400E864C4 /* StatusesViewModel.swift in Sources */,
|
||||
D0666A4E24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||
D057426724E9FE1D00839EBA /* ContentDatabase.swift in Sources */,
|
||||
D054951524EB1053008B00A5 /* TimelineService.swift in Sources */,
|
||||
D019E6E924DF72E700697C7D /* InstanceEndpoint.swift in Sources */,
|
||||
D0ED1BE324CFA84400B4899C /* MastodonError.swift in Sources */,
|
||||
D020F50E24ECA25F005AB084 /* ContextEndpoint.swift in Sources */,
|
||||
D0EC8DE824E21FEC00A08489 /* Data+Extensions.swift in Sources */,
|
||||
D0666A6324C6DC6C00F3F04B /* AppAuthorization.swift in Sources */,
|
||||
D05494F024EA3FE5008B00A5 /* Card.swift in Sources */,
|
||||
D019E6E524DF72E700697C7D /* AccountEndpoint.swift in Sources */,
|
||||
D075817C24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
D020F51424ECBA60005AB084 /* LazyView.swift in Sources */,
|
||||
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||
D05494E424EA3EF7008B00A5 /* Tag.swift in Sources */,
|
||||
|
@ -1004,6 +1027,7 @@
|
|||
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */,
|
||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||
D0EC8DC524DF842700A08489 /* KeychainService.swift in Sources */,
|
||||
D020F50B24EC9F1D005AB084 /* ContextService.swift in Sources */,
|
||||
D0DC175824D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
||||
D0DC177724D0CF2600A75C65 /* MockKeychainService.swift in Sources */,
|
||||
D0EC8DC224DF7D9C00A08489 /* IdentityService.swift in Sources */,
|
||||
|
@ -1042,6 +1066,7 @@
|
|||
D0EC8DCE24DFB64200A08489 /* AuthenticationService.swift in Sources */,
|
||||
D04FD73C24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
|
||||
D0DC175B24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
|
||||
D020F51124ECA309005AB084 /* MastodonContext.swift in Sources */,
|
||||
D0ED1BD124CF779B00B4899C /* MastodonTarget.swift in Sources */,
|
||||
D0CD847C24DBEA9F00CF380C /* Unknowable.swift in Sources */,
|
||||
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
||||
|
@ -1108,6 +1133,7 @@
|
|||
D054951624EB1053008B00A5 /* TimelineService.swift in Sources */,
|
||||
D019E6D824DF728400697C7D /* MastodonEncoder.swift in Sources */,
|
||||
D054951C24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */,
|
||||
D03658D224EDD80900AC17EC /* ContextEndpoint+Stubbing.swift in Sources */,
|
||||
D0DC174E24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
||||
D0091B6C24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||
D0091B6F24DD68090040E8D2 /* PreferencesView.swift in Sources */,
|
||||
|
@ -1121,6 +1147,7 @@
|
|||
D019E6F124DF7C2F00697C7D /* DatabaseError.swift in Sources */,
|
||||
D074577824D29006004758DB /* MockWebAuthSession.swift in Sources */,
|
||||
D057426E24EA339300839EBA /* ListTimeline.swift in Sources */,
|
||||
D020F51224ECA309005AB084 /* MastodonContext.swift in Sources */,
|
||||
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */,
|
||||
|
@ -1133,8 +1160,11 @@
|
|||
D019E6E824DF72E700697C7D /* AccessTokenEndpoint.swift in Sources */,
|
||||
D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */,
|
||||
D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */,
|
||||
D020F51524ECBA60005AB084 /* LazyView.swift in Sources */,
|
||||
D0ED1BD224CF779B00B4899C /* MastodonTarget.swift in Sources */,
|
||||
D0EC8DC624DF842700A08489 /* KeychainService.swift in Sources */,
|
||||
D020F50C24EC9F1D005AB084 /* ContextService.swift in Sources */,
|
||||
D020F50F24ECA25F005AB084 /* ContextEndpoint.swift in Sources */,
|
||||
D0CD847D24DBEA9F00CF380C /* Unknowable.swift in Sources */,
|
||||
D0666A7024C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
||||
D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */,
|
||||
|
|
|
@ -56,6 +56,26 @@ extension ContentDatabase {
|
|||
.map { $0.map(Status.init(statusResult:)) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func statusesObservation(collection: TransientStatusCollection) -> AnyPublisher<[Status], Error> {
|
||||
ValueObservation.tracking {
|
||||
try StatusResult.fetchAll(
|
||||
$0,
|
||||
StoredStatus.filter(
|
||||
try collection
|
||||
.elements
|
||||
.fetchAll($0)
|
||||
.map(\.statusId)
|
||||
.contains(Column("id")))
|
||||
.including(required: StoredStatus.account)
|
||||
.including(optional: StoredStatus.reblogAccount)
|
||||
.including(optional: StoredStatus.reblog))
|
||||
}
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseQueue)
|
||||
.map { $0.map(Status.init(statusResult:)) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
private extension ContentDatabase {
|
||||
|
|
8
Shared/Model/MastodonContext.swift
Normal file
8
Shared/Model/MastodonContext.swift
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MastodonContext: Codable, Hashable {
|
||||
let ancestors: [Status]
|
||||
let descendants: [Status]
|
||||
}
|
|
@ -105,3 +105,69 @@ class Status: Codable, Identifiable {
|
|||
self.pinned = pinned
|
||||
}
|
||||
}
|
||||
|
||||
extension Status: Hashable {
|
||||
static func == (lhs: Status, rhs: Status) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
&& lhs.uri == rhs.uri
|
||||
&& lhs.createdAt == rhs.createdAt
|
||||
&& lhs.account == rhs.account
|
||||
&& lhs.content == rhs.content
|
||||
&& lhs.visibility == rhs.visibility
|
||||
&& lhs.sensitive == rhs.sensitive
|
||||
&& lhs.spoilerText == rhs.spoilerText
|
||||
&& lhs.mediaAttachments == rhs.mediaAttachments
|
||||
&& lhs.mentions == rhs.mentions
|
||||
&& lhs.tags == rhs.tags
|
||||
&& lhs.emojis == rhs.emojis
|
||||
&& lhs.reblogsCount == rhs.reblogsCount
|
||||
&& lhs.favouritesCount == rhs.favouritesCount
|
||||
&& lhs.repliesCount == rhs.repliesCount
|
||||
&& lhs.application == rhs.application
|
||||
&& lhs.url == rhs.url
|
||||
&& lhs.inReplyToId == rhs.inReplyToId
|
||||
&& lhs.inReplyToAccountId == rhs.inReplyToAccountId
|
||||
&& lhs.reblog == rhs.reblog
|
||||
&& lhs.poll == rhs.poll
|
||||
&& lhs.card == rhs.card
|
||||
&& lhs.language == rhs.language
|
||||
&& lhs.text == rhs.text
|
||||
&& lhs.favourited == rhs.favourited
|
||||
&& lhs.reblogged == rhs.reblogged
|
||||
&& lhs.muted == rhs.muted
|
||||
&& lhs.bookmarked == rhs.bookmarked
|
||||
&& lhs.pinned == rhs.pinned
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
hasher.combine(uri)
|
||||
hasher.combine(createdAt)
|
||||
hasher.combine(account)
|
||||
hasher.combine(content)
|
||||
hasher.combine(visibility)
|
||||
hasher.combine(sensitive)
|
||||
hasher.combine(spoilerText)
|
||||
hasher.combine(mediaAttachments)
|
||||
hasher.combine(mentions)
|
||||
hasher.combine(tags)
|
||||
hasher.combine(emojis)
|
||||
hasher.combine(reblogsCount)
|
||||
hasher.combine(favouritesCount)
|
||||
hasher.combine(repliesCount)
|
||||
hasher.combine(application)
|
||||
hasher.combine(url)
|
||||
hasher.combine(inReplyToId)
|
||||
hasher.combine(inReplyToAccountId)
|
||||
hasher.combine(reblog)
|
||||
hasher.combine(poll)
|
||||
hasher.combine(card)
|
||||
hasher.combine(language)
|
||||
hasher.combine(text)
|
||||
hasher.combine(favourited)
|
||||
hasher.combine(reblogged)
|
||||
hasher.combine(muted)
|
||||
hasher.combine(bookmarked)
|
||||
hasher.combine(pinned)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ContextEndpoint {
|
||||
case context(id: String)
|
||||
}
|
||||
|
||||
extension ContextEndpoint: MastodonEndpoint {
|
||||
typealias ResultType = MastodonContext
|
||||
|
||||
var context: [String] {
|
||||
defaultContext + ["statuses"]
|
||||
}
|
||||
|
||||
var pathComponentsInContext: [String] {
|
||||
switch self {
|
||||
case let .context(id):
|
||||
return [id, "context"]
|
||||
}
|
||||
}
|
||||
|
||||
var method: HTTPMethod { .get }
|
||||
}
|
48
Shared/Services/Status List Services/ContextService.swift
Normal file
48
Shared/Services/Status List Services/ContextService.swift
Normal file
|
@ -0,0 +1,48 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
struct ContextService {
|
||||
let statusSections: AnyPublisher<[[Status]], Error>
|
||||
|
||||
private var status: Status
|
||||
private let context = CurrentValueSubject<MastodonContext, Never>(MastodonContext(ancestors: [], descendants: []))
|
||||
private let networkClient: MastodonClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
private let collection: TransientStatusCollection
|
||||
|
||||
init(status: Status, networkClient: MastodonClient, contentDatabase: ContentDatabase) {
|
||||
self.status = status
|
||||
self.networkClient = networkClient
|
||||
self.contentDatabase = contentDatabase
|
||||
collection = TransientStatusCollection(id: "context-\(status.id)")
|
||||
statusSections = contentDatabase.statusesObservation(collection: collection)
|
||||
.combineLatest(context.setFailureType(to: Error.self))
|
||||
.map { statuses, context in
|
||||
[
|
||||
context.ancestors.map { a in statuses.first { $0.id == a.id } ?? a },
|
||||
[statuses.first { $0.id == status.id } ?? status],
|
||||
context.descendants.map { d in statuses.first { $0.id == d.id } ?? d }
|
||||
]
|
||||
}
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
extension ContextService: StatusListService {
|
||||
var contextParent: Status? { status }
|
||||
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Void, Error> {
|
||||
networkClient.request(ContextEndpoint.context(id: status.id))
|
||||
.handleEvents(receiveOutput: context.send)
|
||||
.map { ($0.ancestors + $0.descendants, collection) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func contextService(status: Status) -> ContextService {
|
||||
ContextService(status: status, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
}
|
|
@ -5,5 +5,11 @@ import Combine
|
|||
|
||||
protocol StatusListService {
|
||||
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
||||
var contextParent: Status? { get }
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Void, Error>
|
||||
func contextService(status: Status) -> ContextService
|
||||
}
|
||||
|
||||
extension StatusListService {
|
||||
var contextParent: Status? { nil }
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
|
||||
struct TimelineService: StatusListService {
|
||||
struct TimelineService {
|
||||
let statusSections: AnyPublisher<[[Status]], Error>
|
||||
|
||||
private let timeline: Timeline
|
||||
|
@ -18,11 +18,17 @@ struct TimelineService: StatusListService {
|
|||
.map { [$0] }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineService: StatusListService {
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Void, Error> {
|
||||
return networkClient.request(timeline.endpoint)
|
||||
.map { ($0, timeline) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func contextService(status: Status) -> ContextService {
|
||||
ContextService(status: status, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,25 +4,53 @@ import Foundation
|
|||
import Combine
|
||||
|
||||
class StatusesViewModel: ObservableObject {
|
||||
@Published var statusSections = [[Status]]()
|
||||
@Published private(set) var statusSections = [[Status]]()
|
||||
@Published var alertItem: AlertItem?
|
||||
@Published private(set) var loading = false
|
||||
let scrollToStatusID: AnyPublisher<String, Never>
|
||||
private let statusListService: StatusListService
|
||||
private let scrollToStatusIDInput = PassthroughSubject<String, Never>()
|
||||
private var hasScrolledToParentAfterContextLoad = false
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(statusListService: StatusListService) {
|
||||
self.statusListService = statusListService
|
||||
scrollToStatusID = scrollToStatusIDInput.eraseToAnyPublisher()
|
||||
|
||||
statusListService.statusSections
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$statusSections)
|
||||
|
||||
$statusSections
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if
|
||||
let contextParent = self.contextParent,
|
||||
!($0.first ?? []).isEmpty || !(($0.last ?? []).isEmpty),
|
||||
!self.hasScrolledToParentAfterContextLoad {
|
||||
self.hasScrolledToParentAfterContextLoad = true
|
||||
self.scrollToStatusIDInput.send(contextParent.id)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusesViewModel {
|
||||
var contextParent: Status? { statusListService.contextParent }
|
||||
|
||||
func request(maxID: String? = nil, minID: String? = nil) {
|
||||
statusListService.request(maxID: maxID, minID: minID)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.handleEvents(
|
||||
receiveSubscription: { [weak self] _ in self?.loading = true },
|
||||
receiveCompletion: { [weak self] _ in self?.loading = false })
|
||||
.sink {}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func contextViewModel(status: Status) -> StatusesViewModel {
|
||||
StatusesViewModel(statusListService: statusListService.contextService(status: status))
|
||||
}
|
||||
}
|
||||
|
|
17
Shared/Views/LazyView.swift
Normal file
17
Shared/Views/LazyView.swift
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LazyView<V: View>: View {
|
||||
typealias RenderClosure = () -> V
|
||||
|
||||
let render: RenderClosure
|
||||
|
||||
init(_ render: @autoclosure @escaping RenderClosure) {
|
||||
self.render = render
|
||||
}
|
||||
|
||||
var body: V {
|
||||
render()
|
||||
}
|
||||
}
|
|
@ -6,14 +6,33 @@ struct StatusesView: View {
|
|||
@StateObject var viewModel: StatusesViewModel
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(Array(zip(viewModel.statusSections.indices, viewModel.statusSections)),
|
||||
id: \.0) { _, statuses in
|
||||
ForEach(statuses) { status in
|
||||
Text(status.content)
|
||||
Divider()
|
||||
ScrollViewReader { scrollViewProxy in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(Array(zip(viewModel.statusSections.indices, viewModel.statusSections)),
|
||||
id: \.0) { _, statuses in
|
||||
ForEach(statuses) { status in
|
||||
if status == viewModel.contextParent {
|
||||
statusView(status: status)
|
||||
} else {
|
||||
NavigationLink(destination:
|
||||
LazyView(StatusesView(viewModel:
|
||||
viewModel.contextViewModel(status: status)))) {
|
||||
statusView(status: status)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
if viewModel.loading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(viewModel.scrollToStatusID.receive(on: DispatchQueue.main)) { id in
|
||||
withAnimation {
|
||||
scrollViewProxy.scrollTo(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +41,12 @@ struct StatusesView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private extension StatusesView {
|
||||
func statusView(status: Status) -> some View {
|
||||
Text(status.content)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct StatusesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
|
Loading…
Reference in a new issue