mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-09-21 19:20:02 +00:00
Paginated tiemeline and refactoring
This commit is contained in:
parent
0608996bb8
commit
ec8de5fb83
9 changed files with 113 additions and 26 deletions
|
@ -13,8 +13,7 @@ struct IceCubesAppApp: App {
|
||||||
TabView {
|
TabView {
|
||||||
ForEach(tabs, id: \.self) { tab in
|
ForEach(tabs, id: \.self) { tab in
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
TimelineView(kind: .pub)
|
TimelineView(client: .init(server: tab))
|
||||||
.environmentObject(Client(server: tab))
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
|
|
|
@ -23,6 +23,7 @@ public class Client: ObservableObject {
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
components.host = server
|
components.host = server
|
||||||
components.path += "/api/\(version.rawValue)/\(endpoint.path())"
|
components.path += "/api/\(version.rawValue)/\(endpoint.path())"
|
||||||
|
components.queryItems = endpoint.queryItems()
|
||||||
return components.url!
|
return components.url!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,5 @@ import Foundation
|
||||||
|
|
||||||
public protocol Endpoint {
|
public protocol Endpoint {
|
||||||
func path() -> String
|
func path() -> String
|
||||||
|
func queryItems() -> [URLQueryItem]?
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Timeline: Endpoint {
|
public enum Timeline: Endpoint {
|
||||||
case pub
|
case pub(sinceId: String?)
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -9,4 +9,11 @@ public enum Timeline: Endpoint {
|
||||||
return "timelines/public"
|
return "timelines/public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
|
switch self {
|
||||||
|
case .pub(let sinceId):
|
||||||
|
return [.init(name: "max_id", value: sinceId)]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,5 @@ public struct Status: Codable, Identifiable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let content: String
|
public let content: String
|
||||||
public let account: Account
|
public let account: Account
|
||||||
|
public let createdAt: String
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SwiftSoup
|
|
||||||
import HTML2Markdown
|
import HTML2Markdown
|
||||||
|
import Foundation
|
||||||
|
|
||||||
extension Status {
|
extension Status {
|
||||||
public var contentAsMarkdown: String {
|
public var contentAsMarkdown: String {
|
||||||
|
@ -10,4 +10,18 @@ extension Status {
|
||||||
return content
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ struct StatusRowView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack(alignment: .top) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
url: status.account.avatar,
|
url: status.account.avatar,
|
||||||
content: { image in
|
content: { image in
|
||||||
|
@ -27,6 +27,10 @@ struct StatusRowView: View {
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
Spacer()
|
||||||
|
Text(status.createdAtFormatted)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
Text(try! AttributedString(markdown: status.contentAsMarkdown))
|
Text(try! AttributedString(markdown: status.contentAsMarkdown))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,40 +2,52 @@ import SwiftUI
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
public struct TimelineView: View {
|
public struct TimelineView: View {
|
||||||
public enum Kind {
|
@StateObject private var viewModel: TimelineViewModel
|
||||||
case pub, hastah, home, list
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnvironmentObject private var client: Client
|
public init(client: Client) {
|
||||||
|
_viewModel = StateObject(wrappedValue: TimelineViewModel(client: client))
|
||||||
@State private var statuses: [Status] = []
|
|
||||||
|
|
||||||
private let kind: Kind
|
|
||||||
|
|
||||||
public init(kind: Kind) {
|
|
||||||
self.kind = kind
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List(statuses) { status in
|
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)
|
StatusRowView(status: status)
|
||||||
}
|
}
|
||||||
|
switch nextPageState {
|
||||||
|
case .hasNextPage:
|
||||||
|
loadingRow
|
||||||
|
.onAppear {
|
||||||
|
Task {
|
||||||
|
await viewModel.loadNextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .loadingNextPage:
|
||||||
|
loadingRow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.navigationTitle("Public Timeline: \(client.server)")
|
.navigationTitle("Public Timeline: \(viewModel.serverName)")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.task {
|
.task {
|
||||||
await refreshTimeline()
|
await viewModel.refreshTimeline()
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
await refreshTimeline()
|
await viewModel.refreshTimeline()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshTimeline() async {
|
private var loadingRow: some View {
|
||||||
do {
|
HStack {
|
||||||
self.statuses = try await client.fetchArray(endpoint: Timeline.pub)
|
Spacer()
|
||||||
} catch {
|
ProgressView()
|
||||||
print(error.localizedDescription)
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
48
Packages/Timeline/Sources/Timeline/TimelineViewModel.swift
Normal file
48
Packages/Timeline/Sources/Timeline/TimelineViewModel.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue