Paginated tiemeline and refactoring

This commit is contained in:
Thomas Ricouard 2022-11-25 12:00:01 +01:00
parent 0608996bb8
commit ec8de5fb83
9 changed files with 113 additions and 26 deletions

View file

@ -13,8 +13,7 @@ struct IceCubesAppApp: App {
TabView {
ForEach(tabs, id: \.self) { tab in
NavigationStack {
TimelineView(kind: .pub)
.environmentObject(Client(server: tab))
TimelineView(client: .init(server: tab))
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {

View file

@ -23,6 +23,7 @@ public class Client: ObservableObject {
components.scheme = "https"
components.host = server
components.path += "/api/\(version.rawValue)/\(endpoint.path())"
components.queryItems = endpoint.queryItems()
return components.url!
}

View file

@ -2,4 +2,5 @@ import Foundation
public protocol Endpoint {
func path() -> String
func queryItems() -> [URLQueryItem]?
}

View file

@ -1,7 +1,7 @@
import Foundation
public enum Timeline: Endpoint {
case pub
case pub(sinceId: String?)
public func path() -> String {
switch self {
@ -9,4 +9,11 @@ public enum Timeline: Endpoint {
return "timelines/public"
}
}
public func queryItems() -> [URLQueryItem]? {
switch self {
case .pub(let sinceId):
return [.init(name: "max_id", value: sinceId)]
}
}
}

View file

@ -4,4 +4,5 @@ public struct Status: Codable, Identifiable {
public let id: String
public let content: String
public let account: Account
public let createdAt: String
}

View file

@ -1,5 +1,5 @@
import SwiftSoup
import HTML2Markdown
import Foundation
extension Status {
public var contentAsMarkdown: String {
@ -10,4 +10,18 @@ extension Status {
return content
}
}
public var createdAtDate: Date {
let dateFormatter = DateFormatter()
dateFormatter.calendar = .init(identifier: .iso8601)
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
dateFormatter.timeZone = .init(abbreviation: "UTC")
return dateFormatter.date(from: createdAt)!
}
public var createdAtFormatted: String {
let dateFormatter = RelativeDateTimeFormatter()
dateFormatter.unitsStyle = .abbreviated
return dateFormatter.localizedString(for: createdAtDate, relativeTo: Date())
}
}

View file

@ -6,7 +6,7 @@ struct StatusRowView: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
HStack(alignment: .top) {
AsyncImage(
url: status.account.avatar,
content: { image in
@ -27,6 +27,10 @@ struct StatusRowView: View {
.font(.footnote)
.foregroundColor(.gray)
}
Spacer()
Text(status.createdAtFormatted)
.font(.footnote)
.foregroundColor(.gray)
}
Text(try! AttributedString(markdown: status.contentAsMarkdown))
}

View file

@ -2,40 +2,52 @@ import SwiftUI
import Network
public struct TimelineView: View {
public enum Kind {
case pub, hastah, home, list
}
@StateObject private var viewModel: TimelineViewModel
@EnvironmentObject private var client: Client
@State private var statuses: [Status] = []
private let kind: Kind
public init(kind: Kind) {
self.kind = kind
public init(client: Client) {
_viewModel = StateObject(wrappedValue: TimelineViewModel(client: client))
}
public var body: some View {
List(statuses) { status in
StatusRowView(status: status)
List {
switch viewModel.state {
case .loading:
loadingRow
case .error:
Text("An error occurred, please try to refresh")
case let .display(statuses, nextPageState):
ForEach(statuses) { status in
StatusRowView(status: status)
}
switch nextPageState {
case .hasNextPage:
loadingRow
.onAppear {
Task {
await viewModel.loadNextPage()
}
}
case .loadingNextPage:
loadingRow
}
}
}
.listStyle(.plain)
.navigationTitle("Public Timeline: \(client.server)")
.navigationTitle("Public Timeline: \(viewModel.serverName)")
.navigationBarTitleDisplayMode(.inline)
.task {
await refreshTimeline()
await viewModel.refreshTimeline()
}
.refreshable {
await refreshTimeline()
await viewModel.refreshTimeline()
}
}
private func refreshTimeline() async {
do {
self.statuses = try await client.fetchArray(endpoint: Timeline.pub)
} catch {
print(error.localizedDescription)
private var loadingRow: some View {
HStack {
Spacer()
ProgressView()
Spacer()
}
}
}

View file

@ -0,0 +1,48 @@
import SwiftUI
import Network
@MainActor
class TimelineViewModel: ObservableObject {
enum State {
enum PadingState {
case hasNextPage, loadingNextPage
}
case loading
case display(statuses: [Status], nextPageState: State.PadingState)
case error
}
private let client: Client
private var statuses: [Status] = []
@Published var state: State = .loading
var serverName: String {
client.server
}
init(client: Client) {
self.client = client
}
func refreshTimeline() async {
do {
statuses = try await client.fetchArray(endpoint: Timeline.pub(sinceId: nil))
state = .display(statuses: statuses, nextPageState: .hasNextPage)
} catch {
print(error.localizedDescription)
}
}
func loadNextPage() async {
do {
guard let lastId = statuses.last?.id else { return }
state = .display(statuses: statuses, nextPageState: .loadingNextPage)
let newStatuses: [Status] = try await client.fetch(endpoint: Timeline.pub(sinceId: lastId))
statuses.append(contentsOf: newStatuses)
state = .display(statuses: statuses, nextPageState: .hasNextPage)
} catch {
print(error.localizedDescription)
}
}
}