mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-18 02:35:15 +00:00
Generic Statuses list
This commit is contained in:
parent
4c3809a95b
commit
e2455a472e
7 changed files with 173 additions and 109 deletions
|
@ -21,8 +21,7 @@ public struct AccountDetailView: View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
headerView
|
headerView
|
||||||
statusesView
|
StatusesListView(fetcher: viewModel)
|
||||||
.padding(.horizontal, 16)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.top)
|
.edgesIgnoringSafeArea(.top)
|
||||||
|
@ -46,47 +45,6 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var statusesView: some View {
|
|
||||||
switch viewModel.statusesState {
|
|
||||||
case .loading:
|
|
||||||
ForEach(Status.placeholders()) { status in
|
|
||||||
StatusRowView(status: status)
|
|
||||||
.redacted(reason: .placeholder)
|
|
||||||
.shimmering()
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
case let .error(error):
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
case let .display(statuses, nextPageState):
|
|
||||||
ForEach(statuses) { status in
|
|
||||||
StatusRowView(status: status)
|
|
||||||
Divider()
|
|
||||||
.padding(.bottom, DS.Constants.layoutPadding)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch nextPageState {
|
|
||||||
case .hasNextPage:
|
|
||||||
loadingRow
|
|
||||||
.onAppear {
|
|
||||||
Task {
|
|
||||||
await viewModel.loadNextPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .loadingNextPage:
|
|
||||||
loadingRow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var loadingRow: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ProgressView()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AccountDetailView_Previews: PreviewProvider {
|
struct AccountDetailView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Network
|
import Network
|
||||||
import Models
|
import Models
|
||||||
|
import Status
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class AccountDetailViewModel: ObservableObject {
|
class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
let accountId: String
|
let accountId: String
|
||||||
var client: Client = .init(server: "")
|
var client: Client = .init(server: "")
|
||||||
|
|
||||||
|
@ -11,14 +12,6 @@ class AccountDetailViewModel: ObservableObject {
|
||||||
case loading, data(account: Account), error(error: Error)
|
case loading, data(account: Account), error(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StatusesState {
|
|
||||||
enum PagingState {
|
|
||||||
case hasNextPage, loadingNextPage
|
|
||||||
}
|
|
||||||
case loading
|
|
||||||
case display(statuses: [Status], nextPageState: StatusesState.PagingState)
|
|
||||||
case error(error: Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Published var state: State = .loading
|
@Published var state: State = .loading
|
||||||
@Published var statusesState: StatusesState = .loading
|
@Published var statusesState: StatusesState = .loading
|
||||||
|
@ -52,7 +45,7 @@ class AccountDetailViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNextPage() async {
|
func fetchNextPage() async {
|
||||||
do {
|
do {
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1420"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "Status"
|
||||||
|
BuildableName = "Status"
|
||||||
|
BlueprintName = "Status"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "StatusTests"
|
||||||
|
BuildableName = "StatusTests"
|
||||||
|
BlueprintName = "StatusTests"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "Status"
|
||||||
|
BuildableName = "Status"
|
||||||
|
BlueprintName = "Status"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -17,6 +17,7 @@ let package = Package(
|
||||||
.package(name: "Models", path: "../Models"),
|
.package(name: "Models", path: "../Models"),
|
||||||
.package(name: "Routeur", path: "../Routeur"),
|
.package(name: "Routeur", path: "../Routeur"),
|
||||||
.package(name: "DesignSystem", path: "../DesignSystem"),
|
.package(name: "DesignSystem", path: "../DesignSystem"),
|
||||||
|
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
|
@ -25,6 +26,7 @@ let package = Package(
|
||||||
.product(name: "Models", package: "Models"),
|
.product(name: "Models", package: "Models"),
|
||||||
.product(name: "Routeur", package: "Routeur"),
|
.product(name: "Routeur", package: "Routeur"),
|
||||||
.product(name: "DesignSystem", package: "DesignSystem"),
|
.product(name: "DesignSystem", package: "DesignSystem"),
|
||||||
|
.product(name: "Shimmer", package: "SwiftUI-Shimmer")
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
72
Packages/Status/Sources/Status/StatusesListView.swift
Normal file
72
Packages/Status/Sources/Status/StatusesListView.swift
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Shimmer
|
||||||
|
import DesignSystem
|
||||||
|
|
||||||
|
public enum StatusesState {
|
||||||
|
public enum PagingState {
|
||||||
|
case hasNextPage, loadingNextPage
|
||||||
|
}
|
||||||
|
case loading
|
||||||
|
case display(statuses: [Status], nextPageState: StatusesState.PagingState)
|
||||||
|
case error(error: Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public protocol StatusesFetcher: ObservableObject {
|
||||||
|
var statusesState: StatusesState { get }
|
||||||
|
func fetchStatuses() async
|
||||||
|
func fetchNextPage() async
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
|
@ObservedObject private var fetcher: Fetcher
|
||||||
|
|
||||||
|
public init(fetcher: Fetcher) {
|
||||||
|
self.fetcher = fetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Group {
|
||||||
|
switch fetcher.statusesState {
|
||||||
|
case .loading:
|
||||||
|
ForEach(Status.placeholders()) { status in
|
||||||
|
StatusRowView(status: status)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
.shimmering()
|
||||||
|
Divider()
|
||||||
|
.padding(.bottom, DS.Constants.layoutPadding)
|
||||||
|
}
|
||||||
|
case let .error(error):
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
case let .display(statuses, nextPageState):
|
||||||
|
ForEach(statuses) { status in
|
||||||
|
StatusRowView(status: status)
|
||||||
|
Divider()
|
||||||
|
.padding(.bottom, DS.Constants.layoutPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch nextPageState {
|
||||||
|
case .hasNextPage:
|
||||||
|
loadingRow
|
||||||
|
.onAppear {
|
||||||
|
Task {
|
||||||
|
await fetcher.fetchNextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .loadingNextPage:
|
||||||
|
loadingRow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingRow: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,34 +12,11 @@ public struct TimelineView: View {
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
ScrollView {
|
||||||
switch viewModel.state {
|
LazyVStack {
|
||||||
case .loading:
|
StatusesListView(fetcher: viewModel)
|
||||||
ForEach(Status.placeholders()) { placeholder in
|
|
||||||
StatusRowView(status: placeholder)
|
|
||||||
.redacted(reason: .placeholder)
|
|
||||||
.shimmering()
|
|
||||||
}
|
|
||||||
case let .error(error):
|
|
||||||
Text(error.localizedDescription)
|
|
||||||
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(viewModel.timeline.rawValue)
|
.navigationTitle(viewModel.timeline.rawValue)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
@ -50,22 +27,15 @@ public struct TimelineView: View {
|
||||||
.task {
|
.task {
|
||||||
viewModel.client = client
|
viewModel.client = client
|
||||||
if !didAppear {
|
if !didAppear {
|
||||||
await viewModel.refreshTimeline()
|
await viewModel.fetchStatuses()
|
||||||
didAppear = true
|
didAppear = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
await viewModel.refreshTimeline()
|
await viewModel.fetchStatuses()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingRow: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ProgressView()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var timelineFilterButton: some View {
|
private var timelineFilterButton: some View {
|
||||||
Menu {
|
Menu {
|
||||||
|
|
|
@ -1,18 +1,10 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Network
|
import Network
|
||||||
import Models
|
import Models
|
||||||
|
import Status
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class TimelineViewModel: ObservableObject {
|
class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
enum State {
|
|
||||||
enum PagingState {
|
|
||||||
case hasNextPage, loadingNextPage
|
|
||||||
}
|
|
||||||
case loading
|
|
||||||
case display(statuses: [Status], nextPageState: State.PagingState)
|
|
||||||
case error(error: Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TimelineFilter: String, CaseIterable {
|
enum TimelineFilter: String, CaseIterable {
|
||||||
case pub = "Public"
|
case pub = "Public"
|
||||||
case home = "Home"
|
case home = "Home"
|
||||||
|
@ -33,12 +25,12 @@ class TimelineViewModel: ObservableObject {
|
||||||
|
|
||||||
private var statuses: [Status] = []
|
private var statuses: [Status] = []
|
||||||
|
|
||||||
@Published var state: State = .loading
|
@Published var statusesState: StatusesState = .loading
|
||||||
@Published var timeline: TimelineFilter = .pub {
|
@Published var timeline: TimelineFilter = .pub {
|
||||||
didSet {
|
didSet {
|
||||||
if oldValue != timeline {
|
if oldValue != timeline {
|
||||||
Task {
|
Task {
|
||||||
await refreshTimeline()
|
await fetchStatuses()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,25 +40,25 @@ class TimelineViewModel: ObservableObject {
|
||||||
client.server
|
client.server
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshTimeline() async {
|
func fetchStatuses() async {
|
||||||
do {
|
do {
|
||||||
state = .loading
|
statusesState = .loading
|
||||||
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil))
|
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil))
|
||||||
state = .display(statuses: statuses, nextPageState: .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
statusesState = .error(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNextPage() async {
|
func fetchNextPage() async {
|
||||||
do {
|
do {
|
||||||
guard let lastId = statuses.last?.id else { return }
|
guard let lastId = statuses.last?.id else { return }
|
||||||
state = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
|
||||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: lastId))
|
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: lastId))
|
||||||
statuses.append(contentsOf: newStatuses)
|
statuses.append(contentsOf: newStatuses)
|
||||||
state = .display(statuses: statuses, nextPageState: .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
statusesState = .error(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue