feat: add home timeline api

This commit is contained in:
CMK 2021-02-03 18:52:47 +08:00
parent 3e1d2bcc16
commit 6daccf5170
9 changed files with 212 additions and 46 deletions

View File

@ -33,12 +33,10 @@
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; };
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; };
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; };
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */; };
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */; };
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; };
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; };
@ -61,6 +59,7 @@
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */; };
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; };
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; };
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -173,10 +172,8 @@
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = "<group>"; };
A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = "<group>"; };
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -207,6 +204,7 @@
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthentication.swift; sourceTree = "<group>"; };
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = "<group>"; };
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = "<group>"; };
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -254,7 +252,6 @@
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */,
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */,
);
@ -273,7 +270,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */,
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -400,8 +396,6 @@
2D7631A625C1533800929FB9 /* TableviewCell */ = {
isa = PBXGroup;
children = (
602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */,
A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */,
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
);
path = TableviewCell;
@ -542,6 +536,7 @@
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
);
path = APIService;
sourceTree = "<group>";
@ -1032,6 +1027,7 @@
buildActionMask = 2147483647;
files = (
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,

View File

@ -12,7 +12,7 @@
<key>Mastodon.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -90,6 +90,6 @@ class PublicTimelineViewModel: NSObject {
extension PublicTimelineViewModel {
func fetchLatest() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
return context.apiService.publicTimeline(count: 20, domain: "mstdn.jp")
return context.apiService.publicTimeline(domain: "mstdn.jp")
}
}

View File

@ -0,0 +1,60 @@
//
// APIService+HomeTimeline.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021/2/3.
//
import Foundation
import Combine
import CoreData
import CoreDataStack
import DateToolsSwift
import MastodonSDK
extension APIService {
func homeTimeline(
domain: String,
sinceID: Mastodon.Entity.Status.ID? = nil,
maxID: Mastodon.Entity.Status.ID? = nil,
limit: Int = 100,
authorizationBox: AuthenticationService.MastodonAuthenticationBox
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
let authorization = authorizationBox.userAuthorization
let query = Mastodon.API.Timeline.HomeTimelineQuery(
maxID: maxID,
sinceID: sinceID,
minID: nil, // prefer sinceID
limit: limit,
local: nil // TODO:
)
return Mastodon.API.Timeline.home(
session: session,
domain: domain,
query: query,
authorization: authorization
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> in
return APIService.Persist.persistTimeline(
domain: domain,
managedObjectContext: self.backgroundManagedObjectContext,
response: response,
persistType: .homeTimeline
)
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in
switch result {
case .success:
return response
case .failure(let error):
throw error
}
}
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}

View File

@ -17,21 +17,32 @@ extension APIService {
static let publicTimelineRequestWindowInSec: TimeInterval = 15 * 60
func publicTimeline(
count: Int = 20,
domain: String
domain: String,
sinceID: Mastodon.Entity.Status.ID? = nil,
maxID: Mastodon.Entity.Status.ID? = nil,
limit: Int = 100
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
let query = Mastodon.API.Timeline.PublicTimelineQuery(
local: nil,
remote: nil,
onlyMedia: nil,
maxID: maxID,
sinceID: sinceID,
minID: nil, // prefer sinceID
limit: limit
)
return Mastodon.API.Timeline.public(
session: session,
domain: domain,
query: Mastodon.API.Timeline.PublicTimelineQuery()
query: query
)
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> in
return APIService.Persist.persistTimeline(
domain: domain,
managedObjectContext: self.backgroundManagedObjectContext,
response: response,
persistType: Persist.PersistTimelineType.publicHomeTimeline
persistType: Persist.PersistTimelineType.publicTimeline
)
.setFailureType(to: Error.self)
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Toot]> in

View File

@ -14,7 +14,8 @@ import MastodonSDK
extension APIService.Persist {
enum PersistTimelineType {
case publicHomeTimeline
case publicTimeline
case homeTimeline
}
static func persistTimeline(
domain: String,

View File

@ -13,6 +13,9 @@ extension Mastodon.API.Timeline {
static func publicTimelineEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("timelines/public")
}
static func homeTimelineEndpointURL(domain: String) -> URL {
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("timelines/home")
}
public static func `public`(
session: URLSession,
@ -32,9 +35,29 @@ extension Mastodon.API.Timeline {
.eraseToAnyPublisher()
}
public static func home(
session: URLSession,
domain: String,
query: HomeTimelineQuery,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
let request = Mastodon.API.get(
url: homeTimelineEndpointURL(domain: domain),
query: query,
authorization: authorization
)
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Toot].self, from: data, response: response)
return Mastodon.Response.Content(value: value, response: response)
}
.eraseToAnyPublisher()
}
}
extension Mastodon.API.Timeline {
public struct PublicTimelineQuery: Codable, GetQuery {
public let local: Bool?
@ -76,4 +99,38 @@ extension Mastodon.API.Timeline {
return items
}
}
public struct HomeTimelineQuery: Codable, GetQuery {
public let maxID: Mastodon.Entity.Toot.ID?
public let sinceID: Mastodon.Entity.Toot.ID?
public let minID: Mastodon.Entity.Toot.ID?
public let limit: Int?
public let local: Bool?
public init(
maxID: Mastodon.Entity.Toot.ID? = nil,
sinceID: Mastodon.Entity.Toot.ID? = nil,
minID: Mastodon.Entity.Toot.ID? = nil,
limit: Int? = nil,
local: Bool? = nil
) {
self.maxID = maxID
self.sinceID = sinceID
self.minID = minID
self.limit = limit
self.local = local
}
var queryItems: [URLQueryItem]? {
var items: [URLQueryItem] = []
maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) }
sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) }
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
local.flatMap { items.append(URLQueryItem(name: "local", value: $0.queryItemValue)) }
guard !items.isEmpty else { return nil }
return items
}
}
}

View File

@ -0,0 +1,71 @@
//
// MastodonSDK+API+TimelineTests.swift
//
//
// Created by MainasuK Cirno on 2021/2/3.
//
import os.log
import XCTest
import Combine
@testable import MastodonSDK
extension MastodonSDKTests {
func testPublicTimeline() throws {
try _testPublicTimeline(domain: domain)
}
private func _testPublicTimeline(domain: String) throws {
let theExpectation = expectation(description: "Fetch Public Timeline")
let query = Mastodon.API.Timeline.PublicTimelineQuery()
Mastodon.API.Timeline.public(session: session, domain: domain, query: query)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
XCTFail(error.localizedDescription)
case .finished:
break
}
} receiveValue: { response in
XCTAssert(!response.value.isEmpty)
theExpectation.fulfill()
}
.store(in: &disposeBag)
wait(for: [theExpectation], timeout: 10.0)
}
}
extension MastodonSDKTests {
func testHomeTimeline() {
let domain = ""
let accessToken = ""
guard !domain.isEmpty, !accessToken.isEmpty else { return }
let query = Mastodon.API.Timeline.HomeTimelineQuery()
let authorization = Mastodon.API.OAuth.Authorization(accessToken: accessToken)
let theExpectation = expectation(description: "Fetch Home Timeline")
Mastodon.API.Timeline.home(session: session, domain: domain, query: query, authorization: authorization)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
XCTFail(error.localizedDescription)
case .finished:
break
}
} receiveValue: { response in
XCTAssert(!response.value.isEmpty)
theExpectation.fulfill()
}
.store(in: &disposeBag)
wait(for: [theExpectation], timeout: 10.0)
}
}

View File

@ -14,33 +14,3 @@ final class MastodonSDKTests: XCTestCase {
}
}
extension MastodonSDKTests {
func testPublicTimeline() throws {
try _testPublicTimeline(domain: domain)
}
private func _testPublicTimeline(domain: String) throws {
let theExpectation = expectation(description: "Fetch Public Timeline")
let query = Mastodon.API.Timeline.PublicTimelineQuery()
Mastodon.API.Timeline.public(session: session, domain: domain, query: query)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error):
XCTFail(error.localizedDescription)
case .finished:
break
}
} receiveValue: { response in
XCTAssert(!response.value.isEmpty)
theExpectation.fulfill()
}
.store(in: &disposeBag)
wait(for: [theExpectation], timeout: 10.0)
}
}