mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 08:10:59 +00:00
Basic status fetching and rendering
This commit is contained in:
parent
f6568abad9
commit
b5017d4805
30 changed files with 2544 additions and 40 deletions
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"data" : [
|
||||
{
|
||||
"filename" : "timeline.json",
|
||||
"idiom" : "universal",
|
||||
"universal-type-identifier" : "public.json"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -47,7 +47,9 @@ extension AppEnvironment {
|
|||
static let development = AppEnvironment(
|
||||
session: Session(configuration: .stubbing),
|
||||
webAuthSessionType: SuccessfulMockWebAuthSession.self,
|
||||
keychainServiceType: MockKeychainService.self)
|
||||
keychainServiceType: MockKeychainService.self,
|
||||
userDefaults: MockUserDefaults(),
|
||||
inMemoryContent: true)
|
||||
}
|
||||
|
||||
extension IdentitiesService {
|
||||
|
@ -110,4 +112,8 @@ extension NotificationTypesPreferencesViewModel {
|
|||
static let development = NotificationTypesPreferencesViewModel(identityService: .development)
|
||||
}
|
||||
|
||||
extension StatusesViewModel {
|
||||
static let development = StatusesViewModel(statusListService: IdentityService.development.service(timeline: .home))
|
||||
}
|
||||
|
||||
// swiftlint:enable force_try
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
extension TimelinesEndpoint: Stubbing {
|
||||
func data(url: URL) -> Data? {
|
||||
NSDataAsset(name: "TimelineJSON")!.data
|
||||
}
|
||||
}
|
|
@ -30,5 +30,7 @@ extension Stubbing {
|
|||
dataString(url: url)?.data(using: .utf8)
|
||||
}
|
||||
|
||||
func dataString(url: URL) -> String? { nil }
|
||||
|
||||
func statusCode(url: URL) -> Int? { 200 }
|
||||
}
|
||||
|
|
|
@ -61,6 +61,34 @@
|
|||
D052BBCB24D74C9300A80A7A /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC824D74B6400A80A7A /* MockUserDefaults.swift */; };
|
||||
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
||||
D052BBD224D750CB00A80A7A /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBCC24D750A100A80A7A /* AppEnvironment.swift */; };
|
||||
D05494E424EA3EF7008B00A5 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E324EA3EF7008B00A5 /* Tag.swift */; };
|
||||
D05494E524EA3EF7008B00A5 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E324EA3EF7008B00A5 /* Tag.swift */; };
|
||||
D05494E724EA3F1A008B00A5 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E624EA3F1A008B00A5 /* Mention.swift */; };
|
||||
D05494E824EA3F1A008B00A5 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E624EA3F1A008B00A5 /* Mention.swift */; };
|
||||
D05494EA24EA3F54008B00A5 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E924EA3F54008B00A5 /* Attachment.swift */; };
|
||||
D05494EB24EA3F54008B00A5 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494E924EA3F54008B00A5 /* Attachment.swift */; };
|
||||
D05494ED24EA3FA9008B00A5 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EC24EA3FA9008B00A5 /* Poll.swift */; };
|
||||
D05494EE24EA3FA9008B00A5 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EC24EA3FA9008B00A5 /* Poll.swift */; };
|
||||
D05494F024EA3FE5008B00A5 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EF24EA3FE5008B00A5 /* Card.swift */; };
|
||||
D05494F124EA3FE5008B00A5 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494EF24EA3FE5008B00A5 /* Card.swift */; };
|
||||
D05494F724EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */; };
|
||||
D05494F824EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */; };
|
||||
D05494FA24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */; };
|
||||
D05494FB24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */; };
|
||||
D054950124EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */; };
|
||||
D054950224EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */; };
|
||||
D054951224EB1041008B00A5 /* StatusListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951124EB1041008B00A5 /* StatusListService.swift */; };
|
||||
D054951324EB1041008B00A5 /* StatusListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951124EB1041008B00A5 /* StatusListService.swift */; };
|
||||
D054951524EB1053008B00A5 /* TimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951424EB1053008B00A5 /* TimelineService.swift */; };
|
||||
D054951624EB1053008B00A5 /* TimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951424EB1053008B00A5 /* TimelineService.swift */; };
|
||||
D054951B24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */; };
|
||||
D054951C24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */; };
|
||||
D057426724E9FE1D00839EBA /* ContentDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426624E9FE1D00839EBA /* ContentDatabase.swift */; };
|
||||
D057426824E9FE1D00839EBA /* ContentDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426624E9FE1D00839EBA /* ContentDatabase.swift */; };
|
||||
D057426A24EA32AC00839EBA /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426924EA32AC00839EBA /* Timeline.swift */; };
|
||||
D057426B24EA32AC00839EBA /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426924EA32AC00839EBA /* Timeline.swift */; };
|
||||
D057426D24EA339300839EBA /* ListTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426C24EA339300839EBA /* ListTimeline.swift */; };
|
||||
D057426E24EA339300839EBA /* ListTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D057426C24EA339300839EBA /* ListTimeline.swift */; };
|
||||
D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; };
|
||||
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D065F53A24D3B33A00741304 /* View+Extensions.swift */; };
|
||||
|
@ -102,10 +130,10 @@
|
|||
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93724C9632800E864C4 /* RootViewModel.swift */; };
|
||||
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; };
|
||||
D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC93A24C96FD500E864C4 /* RootView.swift */; };
|
||||
D0BEC94724CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
||||
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */; };
|
||||
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; };
|
||||
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* TimelineView.swift */; };
|
||||
D0BEC94724CA22C400E864C4 /* StatusesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */; };
|
||||
D0BEC94824CA22C400E864C4 /* StatusesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */; };
|
||||
D0BEC94A24CA231200E864C4 /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* StatusesView.swift */; };
|
||||
D0BEC94B24CA231200E864C4 /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEC94924CA231200E864C4 /* StatusesView.swift */; };
|
||||
D0C963FB24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; };
|
||||
D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FA24CC359D003BD330 /* AlertItem.swift */; };
|
||||
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */; };
|
||||
|
@ -258,6 +286,20 @@
|
|||
D052BBC624D749C800A80A7A /* RootViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModelTests.swift; sourceTree = "<group>"; };
|
||||
D052BBC824D74B6400A80A7A /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = "<group>"; };
|
||||
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
|
||||
D05494E324EA3EF7008B00A5 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
D05494E624EA3F1A008B00A5 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
|
||||
D05494E924EA3F54008B00A5 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||
D05494EC24EA3FA9008B00A5 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
|
||||
D05494EF24EA3FE5008B00A5 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = "<group>"; };
|
||||
D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesEndpoint.swift; sourceTree = "<group>"; };
|
||||
D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelinesEndpoint+Stubbing.swift"; sourceTree = "<group>"; };
|
||||
D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DevelopmentAssets.xcassets; sourceTree = "<group>"; };
|
||||
D054951124EB1041008B00A5 /* StatusListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListService.swift; sourceTree = "<group>"; };
|
||||
D054951424EB1053008B00A5 /* TimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineService.swift; sourceTree = "<group>"; };
|
||||
D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransientStatusCollection.swift; sourceTree = "<group>"; };
|
||||
D057426624E9FE1D00839EBA /* ContentDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentDatabase.swift; sourceTree = "<group>"; };
|
||||
D057426924EA32AC00839EBA /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
|
||||
D057426C24EA339300839EBA /* ListTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimeline.swift; sourceTree = "<group>"; };
|
||||
D065F53A24D3B33A00741304 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -278,8 +320,8 @@
|
|||
D0B23F0C24D210E90066F411 /* NSError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0BEC93724C9632800E864C4 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
|
||||
D0BEC93A24C96FD500E864C4 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = "<group>"; };
|
||||
D0BEC94924CA231200E864C4 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||
D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesViewModel.swift; sourceTree = "<group>"; };
|
||||
D0BEC94924CA231200E864C4 /* StatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesView.swift; sourceTree = "<group>"; };
|
||||
D0C963FA24CC359D003BD330 /* AlertItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = "<group>"; };
|
||||
D0C963FD24CC3812003BD330 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPreferences.swift; sourceTree = "<group>"; };
|
||||
|
@ -409,6 +451,7 @@
|
|||
D019E6E024DF72E700697C7D /* InstanceEndpoint.swift */,
|
||||
D019E6DD24DF72E700697C7D /* PreferencesEndpoint.swift */,
|
||||
D0EC8DED24E2704D00A08489 /* PushSubscriptionEndpoint.swift */,
|
||||
D05494F624EA49F7008B00A5 /* TimelinesEndpoint.swift */,
|
||||
);
|
||||
path = Endpoints;
|
||||
sourceTree = "<group>";
|
||||
|
@ -418,6 +461,7 @@
|
|||
children = (
|
||||
D019E6EF24DF7C2F00697C7D /* DatabaseError.swift */,
|
||||
D019E6EC24DF7BF300697C7D /* IdentityDatabase.swift */,
|
||||
D057426624E9FE1D00839EBA /* ContentDatabase.swift */,
|
||||
);
|
||||
path = Databases;
|
||||
sourceTree = "<group>";
|
||||
|
@ -425,12 +469,13 @@
|
|||
D019E6F224DF7C9E00697C7D /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D054951024EB101F008B00A5 /* Status List Services */,
|
||||
D0EC8DCD24DFB64200A08489 /* AuthenticationService.swift */,
|
||||
D0EC8DCA24DFA06700A08489 /* IdentitiesService.swift */,
|
||||
D0EC8DC124DF7D9C00A08489 /* IdentityService.swift */,
|
||||
D0EC8DC424DF842700A08489 /* KeychainService.swift */,
|
||||
D0EC8DD724E096C900A08489 /* UserNotificationService.swift */,
|
||||
D0EC8DC724DF8B3C00A08489 /* SecretsService.swift */,
|
||||
D0EC8DD724E096C900A08489 /* UserNotificationService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
|
@ -500,6 +545,15 @@
|
|||
path = macOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D054951024EB101F008B00A5 /* Status List Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D054951124EB1041008B00A5 /* StatusListService.swift */,
|
||||
D054951424EB1053008B00A5 /* TimelineService.swift */,
|
||||
);
|
||||
path = "Status List Services";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0666A2224C677B400F3F04B /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -519,14 +573,22 @@
|
|||
D0666A6224C6DC6C00F3F04B /* AppAuthorization.swift */,
|
||||
D052BBCC24D750A100A80A7A /* AppEnvironment.swift */,
|
||||
D0ED1BD624CF94B200B4899C /* Application.swift */,
|
||||
D05494E924EA3F54008B00A5 /* Attachment.swift */,
|
||||
D05494EF24EA3FE5008B00A5 /* Card.swift */,
|
||||
D0666A5324C6C3E500F3F04B /* Emoji.swift */,
|
||||
D0666A4A24C6C37700F3F04B /* Identity.swift */,
|
||||
D0666A4D24C6C39600F3F04B /* Instance.swift */,
|
||||
D057426C24EA339300839EBA /* ListTimeline.swift */,
|
||||
D0ED1BE224CFA84400B4899C /* MastodonError.swift */,
|
||||
D0CD847224DBDEC700CF380C /* MastodonPreferences.swift */,
|
||||
D05494E624EA3F1A008B00A5 /* Mention.swift */,
|
||||
D05494EC24EA3FA9008B00A5 /* Poll.swift */,
|
||||
D0E5362F24E5436C00FB1CE1 /* PushNotification.swift */,
|
||||
D0EC8DEA24E26F1100A08489 /* PushSubscription.swift */,
|
||||
D0CD847524DBDF3C00CF380C /* Status.swift */,
|
||||
D05494E324EA3EF7008B00A5 /* Tag.swift */,
|
||||
D057426924EA32AC00839EBA /* Timeline.swift */,
|
||||
D054951A24EB2825008B00A5 /* TransientStatusCollection.swift */,
|
||||
D0CD847B24DBEA9F00CF380C /* Unknowable.swift */,
|
||||
);
|
||||
path = Model;
|
||||
|
@ -555,7 +617,7 @@
|
|||
D0091B6724DC10B30040E8D2 /* PostingReadingPreferencesView.swift */,
|
||||
D0091B6D24DD68090040E8D2 /* PreferencesView.swift */,
|
||||
D0BEC93A24C96FD500E864C4 /* RootView.swift */,
|
||||
D0BEC94924CA231200E864C4 /* TimelineView.swift */,
|
||||
D0BEC94924CA231200E864C4 /* StatusesView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
@ -579,7 +641,7 @@
|
|||
D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */,
|
||||
D0091B7024DD68220040E8D2 /* PreferencesViewModel.swift */,
|
||||
D0BEC93724C9632800E864C4 /* RootViewModel.swift */,
|
||||
D0BEC94624CA22C400E864C4 /* TimelineViewModel.swift */,
|
||||
D0BEC94624CA22C400E864C4 /* StatusesViewModel.swift */,
|
||||
);
|
||||
path = "View Models";
|
||||
sourceTree = "<group>";
|
||||
|
@ -607,6 +669,7 @@
|
|||
D04FD73B24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift */,
|
||||
D0DC175124D008E300A75C65 /* MastodonTarget+Stubbing.swift */,
|
||||
D0A652AC24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift */,
|
||||
D05494F924EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift */,
|
||||
);
|
||||
path = "Mastodon API Stubs";
|
||||
sourceTree = "<group>";
|
||||
|
@ -641,6 +704,7 @@
|
|||
D0ED1BB224CE3A1600B4899C /* Development Assets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D054950024EA4FFE008B00A5 /* DevelopmentAssets.xcassets */,
|
||||
D04FD74124D4AA34007D572D /* DevelopmentModels.swift */,
|
||||
D0DC175724D0130800A75C65 /* HTTPStubs.swift */,
|
||||
D0DC174824CFF13700A75C65 /* Mastodon API Stubs */,
|
||||
|
@ -817,6 +881,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D054950124EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */,
|
||||
D047FAB224C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||
D06B491F24D3F7FE00642749 /* Localizable.strings in Resources */,
|
||||
);
|
||||
|
@ -826,6 +891,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D054950224EA4FFE008B00A5 /* DevelopmentAssets.xcassets in Resources */,
|
||||
D047FAB324C3E21200AF17C5 /* Assets.xcassets in Resources */,
|
||||
D06B492024D3FB8000642749 /* Localizable.strings in Resources */,
|
||||
);
|
||||
|
@ -890,21 +956,27 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D04FD73924D4A7B4007D572D /* AccountEndpoint+Stubbing.swift in Sources */,
|
||||
D05494F724EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */,
|
||||
D054951B24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */,
|
||||
D0DB6F0924C65AC000D965FE /* AddIdentityViewModel.swift in Sources */,
|
||||
D0CD847324DBDEC700CF380C /* MastodonPreferences.swift in Sources */,
|
||||
D075817924E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */,
|
||||
D0ED1BD724CF94B200B4899C /* Application.swift in Sources */,
|
||||
D047FAAE24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
||||
D0BEC94724CA22C400E864C4 /* TimelineViewModel.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 */,
|
||||
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 */,
|
||||
D065F53B24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||
D0DC174A24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||
D05494E424EA3EF7008B00A5 /* Tag.swift in Sources */,
|
||||
D0159F8A24DE742F00E78478 /* IdentitiesViewModel.swift in Sources */,
|
||||
D0666A5124C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||
D019E6E124DF72E700697C7D /* AppAuthorizationEndpoint.swift in Sources */,
|
||||
|
@ -920,13 +992,15 @@
|
|||
D019E6E324DF72E700697C7D /* PreferencesEndpoint.swift in Sources */,
|
||||
D0666A4B24C6C37700F3F04B /* Identity.swift in Sources */,
|
||||
D0666A5424C6C3E500F3F04B /* Emoji.swift in Sources */,
|
||||
D05494FA24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */,
|
||||
D0A652AD24DE3EB6002EA33F /* PreferencesEndpoint+Stubbing.swift in Sources */,
|
||||
D0DC175524D00F0A00A75C65 /* AccessTokenEndpoint+Stubbing.swift in Sources */,
|
||||
D0B23F0D24D210E90066F411 /* NSError+Extensions.swift in Sources */,
|
||||
D05494ED24EA3FA9008B00A5 /* Poll.swift in Sources */,
|
||||
D019E6D924DF728400697C7D /* MastodonDecoder.swift in Sources */,
|
||||
D052BBCA24D74C9200A80A7A /* MockUserDefaults.swift in Sources */,
|
||||
D0DC175224D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||
D0BEC94A24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||
D0BEC94A24CA231200E864C4 /* StatusesView.swift in Sources */,
|
||||
D0BEC93B24C96FD500E864C4 /* RootView.swift in Sources */,
|
||||
D04FD74224D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||
D0EC8DC524DF842700A08489 /* KeychainService.swift in Sources */,
|
||||
|
@ -941,6 +1015,7 @@
|
|||
D0EC8DE524E0B44500A08489 /* UserNotificationService.swift in Sources */,
|
||||
D0EC8DCB24DFA06700A08489 /* IdentitiesService.swift in Sources */,
|
||||
D0091B7124DD68220040E8D2 /* PreferencesViewModel.swift in Sources */,
|
||||
D057426A24EA32AC00839EBA /* Timeline.swift in Sources */,
|
||||
D0DC174D24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
||||
D0091B6B24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||
D0159F8624DE742F00E78478 /* TabNavigationViewModel.swift in Sources */,
|
||||
|
@ -952,13 +1027,16 @@
|
|||
D0DB6EF424C5228A00D965FE /* AddIdentityView.swift in Sources */,
|
||||
D074577724D29006004758DB /* MockWebAuthSession.swift in Sources */,
|
||||
D0159FA524DE989700E78478 /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||
D057426D24EA339300839EBA /* ListTimeline.swift in Sources */,
|
||||
D0ED1BCE24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||
D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||
D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */,
|
||||
D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||
D054951224EB1041008B00A5 /* StatusListService.swift in Sources */,
|
||||
D0159F9124DE743700E78478 /* TabNavigationView.swift in Sources */,
|
||||
D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */,
|
||||
D0EC8DC824DF8B3C00A08489 /* SecretsService.swift in Sources */,
|
||||
D05494E724EA3F1A008B00A5 /* Mention.swift in Sources */,
|
||||
D0159FA324DE955900E78478 /* CustomEmojiText.swift in Sources */,
|
||||
D0C963FE24CC3812003BD330 /* Publisher+Extensions.swift in Sources */,
|
||||
D0EC8DCE24DFB64200A08489 /* AuthenticationService.swift in Sources */,
|
||||
|
@ -969,6 +1047,7 @@
|
|||
D0666A6F24C6DFB300F3F04B /* AccessToken.swift in Sources */,
|
||||
D0ED1BCB24CF744200B4899C /* MastodonClient.swift in Sources */,
|
||||
D0091B6824DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */,
|
||||
D05494EA24EA3F54008B00A5 /* Attachment.swift in Sources */,
|
||||
D0CD847624DBDF3C00CF380C /* Status.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -983,7 +1062,7 @@
|
|||
D0CD847424DBDEC700CF380C /* MastodonPreferences.swift in Sources */,
|
||||
D0ED1BD824CF94B200B4899C /* Application.swift in Sources */,
|
||||
D047FAAF24C3E21200AF17C5 /* MetatextApp.swift in Sources */,
|
||||
D0BEC94824CA22C400E864C4 /* TimelineViewModel.swift in Sources */,
|
||||
D0BEC94824CA22C400E864C4 /* StatusesViewModel.swift in Sources */,
|
||||
D0159FA624DE98F600E78478 /* NSMutableAttributedString+Extensions.swift in Sources */,
|
||||
D0EC8DC324DF7D9C00A08489 /* IdentityService.swift in Sources */,
|
||||
D0666A4F24C6C39600F3F04B /* Instance.swift in Sources */,
|
||||
|
@ -992,6 +1071,8 @@
|
|||
D065F53C24D3B33A00741304 /* View+Extensions.swift in Sources */,
|
||||
D0DC174B24CFF15F00A75C65 /* AppAuthorizationEndpoint+Stubbing.swift in Sources */,
|
||||
D0666A5224C6C3BC00F3F04B /* Account.swift in Sources */,
|
||||
D05494EE24EA3FA9008B00A5 /* Poll.swift in Sources */,
|
||||
D05494FB24EA4E5E008B00A5 /* TimelinesEndpoint+Stubbing.swift in Sources */,
|
||||
D052BBD124D750CA00A80A7A /* AppEnvironment.swift in Sources */,
|
||||
D081A40624D0F1A8001B016E /* String+Extensions.swift in Sources */,
|
||||
D0BEC93924C9632800E864C4 /* RootViewModel.swift in Sources */,
|
||||
|
@ -1008,18 +1089,25 @@
|
|||
D052BBCB24D74C9300A80A7A /* MockUserDefaults.swift in Sources */,
|
||||
D0DC175324D008E300A75C65 /* MastodonTarget+Stubbing.swift in Sources */,
|
||||
D0EC8DE924E21FEC00A08489 /* Data+Extensions.swift in Sources */,
|
||||
D0BEC94B24CA231200E864C4 /* TimelineView.swift in Sources */,
|
||||
D0BEC94B24CA231200E864C4 /* StatusesView.swift in Sources */,
|
||||
D05494E524EA3EF7008B00A5 /* Tag.swift in Sources */,
|
||||
D0BEC93C24C96FD500E864C4 /* RootView.swift in Sources */,
|
||||
D0159F9B24DE748900E78478 /* SidebarNavigationViewModel.swift in Sources */,
|
||||
D04FD74324D4AA34007D572D /* DevelopmentModels.swift in Sources */,
|
||||
D0DC175924D0130800A75C65 /* HTTPStubs.swift in Sources */,
|
||||
D05494E824EA3F1A008B00A5 /* Mention.swift in Sources */,
|
||||
D057426B24EA32AC00839EBA /* Timeline.swift in Sources */,
|
||||
D019E6DA24DF728400697C7D /* MastodonDecoder.swift in Sources */,
|
||||
D05494EB24EA3F54008B00A5 /* Attachment.swift in Sources */,
|
||||
D0DC177824D0CF2600A75C65 /* MockKeychainService.swift in Sources */,
|
||||
D0C963FC24CC359D003BD330 /* AlertItem.swift in Sources */,
|
||||
D019E6E224DF72E700697C7D /* AppAuthorizationEndpoint.swift in Sources */,
|
||||
D0DC174724CFEC2000A75C65 /* StubbingURLProtocol.swift in Sources */,
|
||||
D05494F824EA49F7008B00A5 /* TimelinesEndpoint.swift in Sources */,
|
||||
D0091B7224DD68220040E8D2 /* PreferencesViewModel.swift in Sources */,
|
||||
D054951624EB1053008B00A5 /* TimelineService.swift in Sources */,
|
||||
D019E6D824DF728400697C7D /* MastodonEncoder.swift in Sources */,
|
||||
D054951C24EB2825008B00A5 /* TransientStatusCollection.swift in Sources */,
|
||||
D0DC174E24CFF1F100A75C65 /* Stubbing.swift in Sources */,
|
||||
D0091B6C24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift in Sources */,
|
||||
D0091B6F24DD68090040E8D2 /* PreferencesView.swift in Sources */,
|
||||
|
@ -1028,9 +1116,11 @@
|
|||
D0DB6EF524C5233E00D965FE /* AddIdentityView.swift in Sources */,
|
||||
D019E6EA24DF72E700697C7D /* InstanceEndpoint.swift in Sources */,
|
||||
D0EC8DCF24DFB64200A08489 /* AuthenticationService.swift in Sources */,
|
||||
D054951324EB1041008B00A5 /* StatusListService.swift in Sources */,
|
||||
D0159F9C24DE748C00E78478 /* SidebarNavigationView.swift in Sources */,
|
||||
D019E6F124DF7C2F00697C7D /* DatabaseError.swift in Sources */,
|
||||
D074577824D29006004758DB /* MockWebAuthSession.swift in Sources */,
|
||||
D057426E24EA339300839EBA /* ListTimeline.swift in Sources */,
|
||||
D0ED1BCF24CF768200B4899C /* MastodonEndpoint.swift in Sources */,
|
||||
D074577B24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */,
|
||||
D0ED1BB824CE47F400B4899C /* WebAuthSession.swift in Sources */,
|
||||
|
@ -1050,6 +1140,8 @@
|
|||
D0ED1BCC24CF744200B4899C /* MastodonClient.swift in Sources */,
|
||||
D0091B6924DC10B30040E8D2 /* PostingReadingPreferencesView.swift in Sources */,
|
||||
D075817A24E6657B0081F6A3 /* NotificationTypesPreferencesViewModel.swift in Sources */,
|
||||
D05494F124EA3FE5008B00A5 /* Card.swift in Sources */,
|
||||
D057426824E9FE1D00839EBA /* ContentDatabase.swift in Sources */,
|
||||
D019E6E624DF72E700697C7D /* AccountEndpoint.swift in Sources */,
|
||||
D0CD847724DBDF3C00CF380C /* Status.swift in Sources */,
|
||||
D0EC8DEF24E2704D00A08489 /* PushSubscriptionEndpoint.swift in Sources */,
|
||||
|
|
410
Shared/Databases/ContentDatabase.swift
Normal file
410
Shared/Databases/ContentDatabase.swift
Normal file
|
@ -0,0 +1,410 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import GRDB
|
||||
|
||||
// swiftlint:disable file_length
|
||||
struct ContentDatabase {
|
||||
private let databaseQueue: DatabaseQueue
|
||||
|
||||
init(identityID: UUID, inMemory: Bool = false) throws {
|
||||
guard
|
||||
let documentsDirectory = NSSearchPathForDirectoriesInDomains(
|
||||
.documentDirectory,
|
||||
.userDomainMask, true)
|
||||
.first
|
||||
else { throw DatabaseError.documentsDirectoryNotFound }
|
||||
|
||||
if inMemory {
|
||||
databaseQueue = DatabaseQueue()
|
||||
} else {
|
||||
databaseQueue = try DatabaseQueue(path: "\(documentsDirectory)/\(identityID.uuidString).sqlite3")
|
||||
}
|
||||
|
||||
try Self.migrate(databaseQueue)
|
||||
try Self.createTemporaryTables(databaseQueue)
|
||||
}
|
||||
}
|
||||
|
||||
extension ContentDatabase {
|
||||
func insert(statuses: [Status], collection: StatusCollection? = nil) -> AnyPublisher<Void, Error> {
|
||||
databaseQueue.writePublisher {
|
||||
try collection?.save($0)
|
||||
|
||||
for status in statuses {
|
||||
for component in status.storedComponents() {
|
||||
try component.save($0)
|
||||
}
|
||||
|
||||
try collection?.joinRecord(status: status).save($0)
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func statusesObservation(timeline: Timeline) -> AnyPublisher<[Status], Error> {
|
||||
ValueObservation
|
||||
.tracking(timeline.statuses
|
||||
.including(required: StoredStatus.account)
|
||||
.including(optional: StoredStatus.reblogAccount)
|
||||
.including(optional: StoredStatus.reblog)
|
||||
.asRequest(of: StatusResult.self)
|
||||
.fetchAll)
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseQueue)
|
||||
.map { $0.map(Status.init(statusResult:)) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
private extension ContentDatabase {
|
||||
// swiftlint:disable function_body_length
|
||||
static func migrate(_ writer: DatabaseWriter) throws {
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
migrator.registerMigration("createStatuses") { db in
|
||||
try db.create(table: "account", ifNotExists: true) { t in
|
||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||
t.column("username", .text).notNull()
|
||||
t.column("acct", .text).notNull()
|
||||
t.column("displayName", .text).notNull()
|
||||
t.column("locked", .boolean).notNull()
|
||||
t.column("createdAt", .date).notNull()
|
||||
t.column("followersCount", .integer).notNull()
|
||||
t.column("followingCount", .integer).notNull()
|
||||
t.column("statusesCount", .integer).notNull()
|
||||
t.column("note", .text).notNull()
|
||||
t.column("url", .text).notNull()
|
||||
t.column("avatar", .text).notNull()
|
||||
t.column("avatarStatic", .text).notNull()
|
||||
t.column("header", .text).notNull()
|
||||
t.column("headerStatic", .text).notNull()
|
||||
t.column("fields", .blob).notNull()
|
||||
t.column("emojis", .blob).notNull()
|
||||
t.column("bot", .boolean).notNull()
|
||||
t.column("moved", .boolean)
|
||||
t.column("discoverable", .boolean)
|
||||
}
|
||||
|
||||
try db.create(table: "storedStatus", ifNotExists: true) { t in
|
||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||
t.column("uri", .text).notNull()
|
||||
t.column("createdAt", .datetime).notNull()
|
||||
t.column("accountId", .text).indexed().notNull().references("account", column: "id")
|
||||
t.column("content", .text).notNull()
|
||||
t.column("visibility", .text).notNull()
|
||||
t.column("sensitive", .boolean).notNull()
|
||||
t.column("spoilerText", .text).notNull()
|
||||
t.column("mediaAttachments", .blob).notNull()
|
||||
t.column("mentions", .blob).notNull()
|
||||
t.column("tags", .blob).notNull()
|
||||
t.column("emojis", .blob).notNull()
|
||||
t.column("reblogsCount", .integer).notNull()
|
||||
t.column("favouritesCount", .integer).notNull()
|
||||
t.column("repliesCount", .integer).notNull()
|
||||
t.column("application", .blob)
|
||||
t.column("url", .text)
|
||||
t.column("inReplyToId", .text)
|
||||
t.column("inReplyToAccountId", .text)
|
||||
t.column("reblogId", .text).indexed().references("storedStatus", column: "id")
|
||||
t.column("poll", .blob)
|
||||
t.column("card", .blob)
|
||||
t.column("language", .text)
|
||||
t.column("text", .text)
|
||||
t.column("favourited", .boolean)
|
||||
t.column("reblogged", .boolean)
|
||||
t.column("muted", .boolean)
|
||||
t.column("bookmarked", .boolean)
|
||||
t.column("pinned", .boolean)
|
||||
}
|
||||
|
||||
try db.create(table: "timeline", ifNotExists: true) { t in
|
||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||
t.column("listTitle", .text)
|
||||
}
|
||||
|
||||
try db.create(table: "timelineStatusJoin", ifNotExists: true) { t in
|
||||
t.column("timelineId", .text)
|
||||
.indexed()
|
||||
.notNull()
|
||||
.references("timeline", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text)
|
||||
.indexed()
|
||||
.notNull()
|
||||
.references("storedStatus", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
|
||||
t.primaryKey(["timelineId", "statusId"], onConflict: .replace)
|
||||
}
|
||||
}
|
||||
|
||||
try migrator.migrate(writer)
|
||||
}
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
static func createTemporaryTables(_ writer: DatabaseWriter) throws {
|
||||
try writer.write { database in
|
||||
try database.create(table: "transientStatusCollection", temporary: true, ifNotExists: true) { t in
|
||||
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
|
||||
}
|
||||
|
||||
try database.create(table: "transientStatusCollectionElement", temporary: true, ifNotExists: true) { t in
|
||||
t.column("transientStatusCollectionId", .text)
|
||||
.notNull()
|
||||
.references("transientStatusCollection", column: "id", onDelete: .cascade, onUpdate: .cascade)
|
||||
t.column("statusId", .text).notNull()
|
||||
|
||||
t.primaryKey(["transientStatusCollectionId", "statusId"], onConflict: .replace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Account: TableRecord, FetchableRecord, PersistableRecord {
|
||||
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||
MastodonDecoder()
|
||||
}
|
||||
|
||||
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||
MastodonEncoder()
|
||||
}
|
||||
}
|
||||
|
||||
protocol StatusCollection: FetchableRecord, PersistableRecord {
|
||||
var id: String { get }
|
||||
|
||||
func joinRecord(status: Status) -> PersistableRecord
|
||||
}
|
||||
|
||||
private struct TimelineStatusJoin: Codable, TableRecord, FetchableRecord, PersistableRecord {
|
||||
let timelineId: String
|
||||
let statusId: String
|
||||
|
||||
static let status = belongsTo(StoredStatus.self)
|
||||
}
|
||||
|
||||
extension Timeline: StatusCollection {
|
||||
enum Columns: String, ColumnExpression {
|
||||
case id, listTitle
|
||||
}
|
||||
|
||||
init(row: Row) {
|
||||
switch row[Columns.id] as String {
|
||||
case Timeline.home.id:
|
||||
self = .home
|
||||
case Timeline.local.id:
|
||||
self = .local
|
||||
case Timeline.federated.id:
|
||||
self = .federated
|
||||
default:
|
||||
self = .list(MastodonList(id: row[Columns.id], title: row[Columns.listTitle]))
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container[Columns.id] = id
|
||||
|
||||
if case let .list(list) = self {
|
||||
container[Columns.listTitle] = list.title
|
||||
}
|
||||
}
|
||||
|
||||
func joinRecord(status: Status) -> PersistableRecord {
|
||||
TimelineStatusJoin(timelineId: id, statusId: status.id)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Timeline {
|
||||
static let statusJoins = hasMany(TimelineStatusJoin.self)
|
||||
|
||||
static let statuses = hasMany(StoredStatus.self,
|
||||
through: statusJoins,
|
||||
using: TimelineStatusJoin.status).order(Column("createdAt").desc)
|
||||
|
||||
var statusJoins: QueryInterfaceRequest<TimelineStatusJoin> {
|
||||
request(for: Self.statusJoins)
|
||||
}
|
||||
|
||||
var statuses: QueryInterfaceRequest<StoredStatus> {
|
||||
request(for: Self.statuses)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TransientStatusCollectionElement: Codable, TableRecord, FetchableRecord, PersistableRecord {
|
||||
let transientStatusCollectionId: String
|
||||
let statusId: String
|
||||
|
||||
static let status = belongsTo(StoredStatus.self, key: "statusId")
|
||||
}
|
||||
|
||||
extension TransientStatusCollection: StatusCollection {
|
||||
func joinRecord(status: Status) -> PersistableRecord {
|
||||
TransientStatusCollectionElement(transientStatusCollectionId: id, statusId: status.id)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TransientStatusCollection {
|
||||
static let elements = hasMany(TransientStatusCollectionElement.self)
|
||||
|
||||
var elements: QueryInterfaceRequest<TransientStatusCollectionElement> {
|
||||
request(for: Self.elements)
|
||||
}
|
||||
}
|
||||
|
||||
private struct StoredStatus: Codable, Hashable {
|
||||
let id: String
|
||||
let uri: String
|
||||
let createdAt: Date
|
||||
let accountId: String
|
||||
let content: String
|
||||
let visibility: Status.Visibility
|
||||
let sensitive: Bool
|
||||
let spoilerText: String
|
||||
let mediaAttachments: [Attachment]
|
||||
let mentions: [Mention]
|
||||
let tags: [Tag]
|
||||
let emojis: [Emoji]
|
||||
let reblogsCount: Int
|
||||
let favouritesCount: Int
|
||||
let repliesCount: Int
|
||||
let application: Application?
|
||||
let url: URL?
|
||||
let inReplyToId: String?
|
||||
let inReplyToAccountId: String?
|
||||
let reblogId: String?
|
||||
let poll: Poll?
|
||||
let card: Card?
|
||||
let language: String?
|
||||
let text: String?
|
||||
let favourited: Bool?
|
||||
let reblogged: Bool?
|
||||
let muted: Bool?
|
||||
let bookmarked: Bool?
|
||||
let pinned: Bool?
|
||||
}
|
||||
|
||||
private extension StoredStatus {
|
||||
static let account = belongsTo(Account.self, key: "account")
|
||||
static let reblogAccount = hasOne(Account.self, through: Self.reblog, using: Self.account, key: "reblogAccount")
|
||||
static let reblog = belongsTo(StoredStatus.self, key: "reblog")
|
||||
|
||||
var account: QueryInterfaceRequest<Account> {
|
||||
request(for: Self.account)
|
||||
}
|
||||
|
||||
var reblogAccount: QueryInterfaceRequest<Account> {
|
||||
request(for: Self.reblogAccount)
|
||||
}
|
||||
|
||||
var reblog: QueryInterfaceRequest<StoredStatus> {
|
||||
request(for: Self.reblog)
|
||||
}
|
||||
|
||||
init(status: Status) {
|
||||
id = status.id
|
||||
uri = status.uri
|
||||
createdAt = status.createdAt
|
||||
accountId = status.account.id
|
||||
content = status.content
|
||||
visibility = status.visibility
|
||||
sensitive = status.sensitive
|
||||
spoilerText = status.spoilerText
|
||||
mediaAttachments = status.mediaAttachments
|
||||
mentions = status.mentions
|
||||
tags = status.tags
|
||||
emojis = status.emojis
|
||||
reblogsCount = status.reblogsCount
|
||||
favouritesCount = status.favouritesCount
|
||||
repliesCount = status.repliesCount
|
||||
application = status.application
|
||||
url = status.url
|
||||
inReplyToId = status.inReplyToId
|
||||
inReplyToAccountId = status.inReplyToAccountId
|
||||
reblogId = status.reblog?.id
|
||||
poll = status.poll
|
||||
card = status.card
|
||||
language = status.language
|
||||
text = status.text
|
||||
favourited = status.favourited
|
||||
reblogged = status.reblogged
|
||||
muted = status.muted
|
||||
bookmarked = status.bookmarked
|
||||
pinned = status.pinned
|
||||
}
|
||||
}
|
||||
|
||||
extension StoredStatus: TableRecord, FetchableRecord, PersistableRecord {
|
||||
static func databaseJSONDecoder(for column: String) -> JSONDecoder {
|
||||
MastodonDecoder()
|
||||
}
|
||||
|
||||
static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||
MastodonEncoder()
|
||||
}
|
||||
}
|
||||
|
||||
private struct StatusResult: Codable, Hashable, FetchableRecord {
|
||||
let account: Account
|
||||
let status: StoredStatus
|
||||
let reblogAccount: Account?
|
||||
let reblog: StoredStatus?
|
||||
}
|
||||
|
||||
private extension Status {
|
||||
func storedComponents() -> [PersistableRecord] {
|
||||
var components: [PersistableRecord] = [account]
|
||||
|
||||
if let reblog = reblog {
|
||||
components.append(reblog.account)
|
||||
components.append(StoredStatus(status: reblog))
|
||||
}
|
||||
|
||||
components.append(StoredStatus(status: self))
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
convenience init(statusResult: StatusResult) {
|
||||
var reblog: Status?
|
||||
|
||||
if let reblogResult = statusResult.reblog, let reblogAccount = statusResult.reblogAccount {
|
||||
reblog = Status(storedStatus: reblogResult, account: reblogAccount, reblog: nil)
|
||||
}
|
||||
|
||||
self.init(storedStatus: statusResult.status, account: statusResult.account, reblog: reblog)
|
||||
}
|
||||
|
||||
convenience init(storedStatus: StoredStatus, account: Account, reblog: Status?) {
|
||||
self.init(
|
||||
id: storedStatus.id,
|
||||
uri: storedStatus.uri,
|
||||
createdAt: storedStatus.createdAt,
|
||||
account: account,
|
||||
content: storedStatus.content,
|
||||
visibility: storedStatus.visibility,
|
||||
sensitive: storedStatus.sensitive,
|
||||
spoilerText: storedStatus.spoilerText,
|
||||
mediaAttachments: storedStatus.mediaAttachments,
|
||||
mentions: storedStatus.mentions,
|
||||
tags: storedStatus.tags,
|
||||
emojis: storedStatus.emojis,
|
||||
reblogsCount: storedStatus.reblogsCount,
|
||||
favouritesCount: storedStatus.favouritesCount,
|
||||
repliesCount: storedStatus.repliesCount,
|
||||
application: storedStatus.application,
|
||||
url: storedStatus.url,
|
||||
inReplyToId: storedStatus.inReplyToId,
|
||||
inReplyToAccountId: storedStatus.inReplyToAccountId,
|
||||
reblog: reblog,
|
||||
poll: storedStatus.poll,
|
||||
card: storedStatus.card,
|
||||
language: storedStatus.language,
|
||||
text: storedStatus.text,
|
||||
favourited: storedStatus.favourited,
|
||||
reblogged: storedStatus.reblogged,
|
||||
muted: storedStatus.muted,
|
||||
bookmarked: storedStatus.bookmarked,
|
||||
pinned: storedStatus.pinned)
|
||||
}
|
||||
}
|
||||
// swiftlint:enable file_length
|
|
@ -6,12 +6,15 @@ struct AppEnvironment {
|
|||
let session: Session
|
||||
let webAuthSessionType: WebAuthSession.Type
|
||||
let keychainServiceType: KeychainService.Type
|
||||
let userDefaults: UserDefaults = .standard
|
||||
let userDefaults: UserDefaults
|
||||
let inMemoryContent: Bool
|
||||
}
|
||||
|
||||
extension AppEnvironment {
|
||||
static let live: Self = Self(
|
||||
session: Session(configuration: .default),
|
||||
webAuthSessionType: LiveWebAuthSession.self,
|
||||
keychainServiceType: LiveKeychainService.self)
|
||||
keychainServiceType: LiveKeychainService.self,
|
||||
userDefaults: .standard,
|
||||
inMemoryContent: false)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct Application: Codable {
|
||||
struct Application: Codable, Hashable {
|
||||
let name: String
|
||||
let website: String
|
||||
let website: String?
|
||||
}
|
||||
|
|
43
Shared/Model/Attachment.swift
Normal file
43
Shared/Model/Attachment.swift
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Attachment: Codable, Hashable {
|
||||
enum AttachmentType: String, Codable, Hashable, Unknowable {
|
||||
case image, video, gifv, audio, unknown
|
||||
|
||||
static var unknownCase: Self { .unknown }
|
||||
}
|
||||
|
||||
// swiftlint:disable nesting
|
||||
struct Meta: Codable, Hashable {
|
||||
struct Info: Codable, Hashable {
|
||||
let width: Int?
|
||||
let height: Int?
|
||||
let size: String?
|
||||
let aspect: Double?
|
||||
let frameRate: String?
|
||||
let duration: Double?
|
||||
let bitrate: Int?
|
||||
}
|
||||
|
||||
struct Focus: Codable, Hashable {
|
||||
let x: Double
|
||||
let y: Double
|
||||
}
|
||||
|
||||
let original: Info?
|
||||
let small: Info?
|
||||
let focus: Focus?
|
||||
}
|
||||
// swiftlint:enable nesting
|
||||
|
||||
let id: String
|
||||
let type: AttachmentType
|
||||
let url: URL
|
||||
let remoteUrl: URL?
|
||||
let previewUrl: URL
|
||||
let textUrl: URL?
|
||||
let meta: Meta?
|
||||
let description: String?
|
||||
}
|
25
Shared/Model/Card.swift
Normal file
25
Shared/Model/Card.swift
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Card: Codable, Hashable {
|
||||
enum CardType: String, Codable, Hashable, Unknowable {
|
||||
case link, photo, video, rich, unknown
|
||||
|
||||
static var unknownCase: Self { .unknown }
|
||||
}
|
||||
|
||||
let url: URL
|
||||
let title: String
|
||||
let description: String
|
||||
let type: CardType
|
||||
let authorName: String?
|
||||
let authorUrl: String?
|
||||
let providerName: String?
|
||||
let providerUrl: String?
|
||||
let html: String?
|
||||
let width: Int?
|
||||
let height: Int?
|
||||
let image: URL?
|
||||
let embedUrl: String?
|
||||
}
|
8
Shared/Model/ListTimeline.swift
Normal file
8
Shared/Model/ListTimeline.swift
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MastodonList: Codable, Hashable, Identifiable {
|
||||
let id: String
|
||||
let title: String
|
||||
}
|
10
Shared/Model/Mention.swift
Normal file
10
Shared/Model/Mention.swift
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Mention: Codable, Hashable {
|
||||
let url: URL
|
||||
let username: String
|
||||
let acct: String
|
||||
let id: String
|
||||
}
|
21
Shared/Model/Poll.swift
Normal file
21
Shared/Model/Poll.swift
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Poll: Codable, Hashable {
|
||||
struct Option: Codable, Hashable {
|
||||
var title: String
|
||||
var votesCount: Int
|
||||
}
|
||||
|
||||
let id: String
|
||||
let expiresAt: Date
|
||||
let expired: Bool
|
||||
let multiple: Bool
|
||||
let votesCount: Int
|
||||
let votersCount: Int?
|
||||
let voted: Bool?
|
||||
let ownVotes: [Int]?
|
||||
let options: [Option]
|
||||
let emojis: [Emoji]
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct Status {
|
||||
class Status: Codable, Identifiable {
|
||||
enum Visibility: String, Codable, Unknowable {
|
||||
case `public`
|
||||
case unlisted
|
||||
|
@ -12,4 +12,96 @@ struct Status {
|
|||
|
||||
static var unknownCase: Self { .unknown }
|
||||
}
|
||||
|
||||
let id: String
|
||||
let uri: String
|
||||
let createdAt: Date
|
||||
let account: Account
|
||||
let content: String
|
||||
let visibility: Visibility
|
||||
let sensitive: Bool
|
||||
let spoilerText: String
|
||||
let mediaAttachments: [Attachment]
|
||||
let mentions: [Mention]
|
||||
let tags: [Tag]
|
||||
let emojis: [Emoji]
|
||||
let reblogsCount: Int
|
||||
let favouritesCount: Int
|
||||
let repliesCount: Int
|
||||
let application: Application?
|
||||
let url: URL?
|
||||
let inReplyToId: String?
|
||||
let inReplyToAccountId: String?
|
||||
let reblog: Status?
|
||||
let poll: Poll?
|
||||
let card: Card?
|
||||
let language: String?
|
||||
let text: String?
|
||||
let favourited: Bool?
|
||||
let reblogged: Bool?
|
||||
let muted: Bool?
|
||||
let bookmarked: Bool?
|
||||
let pinned: Bool?
|
||||
|
||||
// Xcode-generated memberwise initializer
|
||||
init(
|
||||
id: String,
|
||||
uri: String,
|
||||
createdAt: Date,
|
||||
account: Account,
|
||||
content: String,
|
||||
visibility: Status.Visibility,
|
||||
sensitive: Bool,
|
||||
spoilerText: String,
|
||||
mediaAttachments: [Attachment],
|
||||
mentions: [Mention],
|
||||
tags: [Tag],
|
||||
emojis: [Emoji],
|
||||
reblogsCount: Int,
|
||||
favouritesCount: Int,
|
||||
repliesCount: Int,
|
||||
application: Application?,
|
||||
url: URL?,
|
||||
inReplyToId: String?,
|
||||
inReplyToAccountId: String?,
|
||||
reblog: Status?,
|
||||
poll: Poll?,
|
||||
card: Card?,
|
||||
language: String?,
|
||||
text: String?,
|
||||
favourited: Bool?,
|
||||
reblogged: Bool?,
|
||||
muted: Bool?,
|
||||
bookmarked: Bool?,
|
||||
pinned: Bool?) {
|
||||
self.id = id
|
||||
self.uri = uri
|
||||
self.createdAt = createdAt
|
||||
self.account = account
|
||||
self.content = content
|
||||
self.visibility = visibility
|
||||
self.sensitive = sensitive
|
||||
self.spoilerText = spoilerText
|
||||
self.mediaAttachments = mediaAttachments
|
||||
self.mentions = mentions
|
||||
self.tags = tags
|
||||
self.emojis = emojis
|
||||
self.reblogsCount = reblogsCount
|
||||
self.favouritesCount = favouritesCount
|
||||
self.repliesCount = repliesCount
|
||||
self.application = application
|
||||
self.url = url
|
||||
self.inReplyToId = inReplyToId
|
||||
self.inReplyToAccountId = inReplyToAccountId
|
||||
self.reblog = reblog
|
||||
self.poll = poll
|
||||
self.card = card
|
||||
self.language = language
|
||||
self.text = text
|
||||
self.favourited = favourited
|
||||
self.reblogged = reblogged
|
||||
self.muted = muted
|
||||
self.bookmarked = bookmarked
|
||||
self.pinned = pinned
|
||||
}
|
||||
}
|
||||
|
|
8
Shared/Model/Tag.swift
Normal file
8
Shared/Model/Tag.swift
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Tag: Codable, Hashable {
|
||||
let name: String
|
||||
let url: URL
|
||||
}
|
38
Shared/Model/Timeline.swift
Normal file
38
Shared/Model/Timeline.swift
Normal file
|
@ -0,0 +1,38 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum Timeline {
|
||||
case home
|
||||
case local
|
||||
case federated
|
||||
case list(MastodonList)
|
||||
}
|
||||
|
||||
extension Timeline {
|
||||
var id: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "home"
|
||||
case .local:
|
||||
return "local"
|
||||
case .federated:
|
||||
return "federated"
|
||||
case let .list(list):
|
||||
return list.id
|
||||
}
|
||||
}
|
||||
|
||||
var endpoint: TimelinesEndpoint {
|
||||
switch self {
|
||||
case .home:
|
||||
return .home
|
||||
case .local:
|
||||
return .public(local: true)
|
||||
case .federated:
|
||||
return .public(local: false)
|
||||
case let .list(list):
|
||||
return .list(id: list.id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
class TimelineViewModel: ObservableObject {
|
||||
|
||||
struct TransientStatusCollection: Codable {
|
||||
let id: String
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
enum TimelinesEndpoint {
|
||||
case `public`(local: Bool)
|
||||
case tag(String)
|
||||
case home
|
||||
case list(id: String)
|
||||
}
|
||||
|
||||
extension TimelinesEndpoint: MastodonEndpoint {
|
||||
typealias ResultType = [Status]
|
||||
|
||||
var context: [String] {
|
||||
defaultContext + ["timelines"]
|
||||
}
|
||||
|
||||
var pathComponentsInContext: [String] {
|
||||
switch self {
|
||||
case .public:
|
||||
return ["public"]
|
||||
case let .tag(tag):
|
||||
return ["tag", tag]
|
||||
case .home:
|
||||
return ["home"]
|
||||
case let .list(id):
|
||||
return ["list", id]
|
||||
}
|
||||
}
|
||||
|
||||
var parameters: [String: Any]? {
|
||||
switch self {
|
||||
case let .public(local):
|
||||
return ["local": local]
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var method: HTTPMethod { .get }
|
||||
}
|
|
@ -8,6 +8,7 @@ class IdentityService {
|
|||
let observationErrors: AnyPublisher<Error, Never>
|
||||
|
||||
private let identityDatabase: IdentityDatabase
|
||||
private let contentDatabase: ContentDatabase
|
||||
private let environment: AppEnvironment
|
||||
private let networkClient: MastodonClient
|
||||
private let secretsService: SecretsService
|
||||
|
@ -37,6 +38,8 @@ class IdentityService {
|
|||
networkClient.instanceURL = identity.url
|
||||
networkClient.accessToken = try? secretsService.item(.accessToken)
|
||||
|
||||
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
|
||||
|
||||
observation.catch { [weak self] error -> Empty<Identity, Never> in
|
||||
self?.observationErrorsInput.send(error)
|
||||
|
||||
|
@ -127,6 +130,10 @@ extension IdentityService {
|
|||
.flatMap(identityDatabase.updatePushSubscription(alerts:deviceToken:forIdentityID:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func service(timeline: Timeline) -> StatusListService {
|
||||
TimelineService(timeline: timeline, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
private extension IdentityService {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
protocol StatusListService {
|
||||
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Void, Error>
|
||||
}
|
28
Shared/Services/Status List Services/TimelineService.swift
Normal file
28
Shared/Services/Status List Services/TimelineService.swift
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
struct TimelineService: StatusListService {
|
||||
let statusSections: AnyPublisher<[[Status]], Error>
|
||||
|
||||
private let timeline: Timeline
|
||||
private let networkClient: MastodonClient
|
||||
private let contentDatabase: ContentDatabase
|
||||
|
||||
init(timeline: Timeline, networkClient: MastodonClient, contentDatabase: ContentDatabase) {
|
||||
self.timeline = timeline
|
||||
self.networkClient = networkClient
|
||||
self.contentDatabase = contentDatabase
|
||||
statusSections = contentDatabase.statusesObservation(timeline: timeline)
|
||||
.map { [$0] }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Void, Error> {
|
||||
return networkClient.request(timeline.endpoint)
|
||||
.map { ($0, timeline) }
|
||||
.flatMap(contentDatabase.insert(statuses:collection:))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
28
Shared/View Models/StatusesViewModel.swift
Normal file
28
Shared/View Models/StatusesViewModel.swift
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class StatusesViewModel: ObservableObject {
|
||||
@Published var statusSections = [[Status]]()
|
||||
@Published var alertItem: AlertItem?
|
||||
private let statusListService: StatusListService
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(statusListService: StatusListService) {
|
||||
self.statusListService = statusListService
|
||||
|
||||
statusListService.statusSections
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$statusSections)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusesViewModel {
|
||||
func request(maxID: String? = nil, minID: String? = nil) {
|
||||
statusListService.request(maxID: maxID, minID: minID)
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.sink {}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
31
Shared/Views/StatusesView.swift
Normal file
31
Shared/Views/StatusesView.swift
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { viewModel.request() }
|
||||
.alertItem($viewModel.alertItem)
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct StatusesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
StatusesView(viewModel: .development)
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -1,17 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineView: View {
|
||||
var body: some View {
|
||||
Text("Time of my life")
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct TimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TimelineView()
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -6,6 +6,7 @@ import Combine
|
|||
class TabNavigationViewModel: ObservableObject {
|
||||
@Published private(set) var identity: Identity
|
||||
@Published private(set) var recentIdentities = [Identity]()
|
||||
@Published private(set) var timelineViewModel: StatusesViewModel
|
||||
@Published var presentingSecondaryNavigation = false
|
||||
@Published var alertItem: AlertItem?
|
||||
var selectedTab: Tab? = .timelines
|
||||
|
@ -16,6 +17,7 @@ class TabNavigationViewModel: ObservableObject {
|
|||
init(identityService: IdentityService) {
|
||||
self.identityService = identityService
|
||||
identity = identityService.identity
|
||||
timelineViewModel = StatusesViewModel(statusListService: identityService.service(timeline: .home))
|
||||
identityService.$identity.dropFirst().assign(to: &$identity)
|
||||
|
||||
identityService.recentIdentitiesObservation()
|
||||
|
|
|
@ -40,7 +40,7 @@ private extension TabNavigationView {
|
|||
func view(tab: TabNavigationViewModel.Tab) -> some View {
|
||||
switch tab {
|
||||
case .timelines:
|
||||
TimelineView()
|
||||
StatusesView(viewModel: viewModel.timelineViewModel)
|
||||
.navigationBarTitle(viewModel.identity.handle, displayMode: .inline)
|
||||
.navigationBarItems(
|
||||
leading: Button {
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
|
||||
class SidebarNavigationViewModel: ObservableObject {
|
||||
@Published private(set) var identity: Identity
|
||||
@Published private(set) var timelineViewModel: StatusesViewModel
|
||||
@Published var alertItem: AlertItem?
|
||||
var selectedTab: Tab? = .timelines
|
||||
|
||||
|
@ -14,6 +15,7 @@ class SidebarNavigationViewModel: ObservableObject {
|
|||
init(identityService: IdentityService) {
|
||||
self.identityService = identityService
|
||||
identity = identityService.identity
|
||||
timelineViewModel = StatusesViewModel(statusListService: identityService.service(timeline: .home))
|
||||
identityService.$identity.dropFirst().assign(to: &$identity)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ private extension SidebarNavigationView {
|
|||
Group {
|
||||
switch topLevelNavigation {
|
||||
case .timelines:
|
||||
TimelineView()
|
||||
StatusesView(viewModel: viewModel.timelineViewModel)
|
||||
default: Text(topLevelNavigation.title)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue