Account statuses and more packages

This commit is contained in:
Thomas Ricouard 2022-12-18 20:30:19 +01:00
parent 70d28e697c
commit 4c3809a95b
22 changed files with 232 additions and 26 deletions

View file

@ -10,6 +10,7 @@
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; }; 9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; };
9F24EEBB293619210042359D /* Routeur in Frameworks */ = {isa = PBXBuildFile; productRef = 9F24EEBA293619210042359D /* Routeur */; }; 9F24EEBB293619210042359D /* Routeur in Frameworks */ = {isa = PBXBuildFile; productRef = 9F24EEBA293619210042359D /* Routeur */; };
9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; }; 9F295540292B6C3400E0E81B /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F29553F292B6C3400E0E81B /* Timeline */; };
9F35DB44294F9A7D00B3281A /* Status in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB43294F9A7D00B3281A /* Status */; };
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */; }; 9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */; };
9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; }; 9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; };
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; }; 9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; };
@ -29,6 +30,8 @@
9F24EEB92936185B0042359D /* Routeur */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Routeur; path = Packages/Routeur; sourceTree = "<group>"; }; 9F24EEB92936185B0042359D /* Routeur */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Routeur; path = Packages/Routeur; sourceTree = "<group>"; };
9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = "<group>"; }; 9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = "<group>"; };
9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = "<group>"; }; 9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = "<group>"; };
9F35DB42294F9A2900B3281A /* Status */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Status; path = Packages/Status; sourceTree = "<group>"; };
9F35DB45294FA04C00B3281A /* DesignSystem */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignSystem; path = Packages/DesignSystem; sourceTree = "<group>"; };
9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = "<group>"; }; 9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = "<group>"; };
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; }; 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; }; 9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; };
@ -53,6 +56,7 @@
9F398AA92935FFDB00A889F2 /* Account in Frameworks */, 9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */, 9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */, 9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
9F35DB44294F9A7D00B3281A /* Status in Frameworks */,
9F24EEBB293619210042359D /* Routeur in Frameworks */, 9F24EEBB293619210042359D /* Routeur in Frameworks */,
9F295540292B6C3400E0E81B /* Timeline in Frameworks */, 9F295540292B6C3400E0E81B /* Timeline in Frameworks */,
); );
@ -105,10 +109,12 @@
9FBFE63A292A715500C250E9 /* Products */, 9FBFE63A292A715500C250E9 /* Products */,
9FBFE64C292A72BD00C250E9 /* Frameworks */, 9FBFE64C292A72BD00C250E9 /* Frameworks */,
9F398AAC2936005300A889F2 /* Account */, 9F398AAC2936005300A889F2 /* Account */,
9F35DB45294FA04C00B3281A /* DesignSystem */,
9F398AA32935F90100A889F2 /* Models */, 9F398AA32935F90100A889F2 /* Models */,
9F29553D292B67B600E0E81B /* Network */,
9F29553E292B6AF600E0E81B /* Timeline */, 9F29553E292B6AF600E0E81B /* Timeline */,
9F24EEB92936185B0042359D /* Routeur */, 9F24EEB92936185B0042359D /* Routeur */,
9F29553D292B67B600E0E81B /* Network */, 9F35DB42294F9A2900B3281A /* Status */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -171,6 +177,7 @@
9F398AAA2935FFDB00A889F2 /* Models */, 9F398AAA2935FFDB00A889F2 /* Models */,
9F24EEBA293619210042359D /* Routeur */, 9F24EEBA293619210042359D /* Routeur */,
9FAE4ACD29379A5A00772766 /* KeychainSwift */, 9FAE4ACD29379A5A00772766 /* KeychainSwift */,
9F35DB43294F9A7D00B3281A /* Status */,
); );
productName = IceCubesApp; productName = IceCubesApp;
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */; productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
@ -360,7 +367,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\"";
DEVELOPMENT_TEAM = Z6P74P6T99; DEVELOPMENT_TEAM = Z6P74P6T99;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@ -381,7 +388,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.0.1; MARKETING_VERSION = 0.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp; PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
@ -401,7 +408,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 150;
DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\"";
DEVELOPMENT_TEAM = Z6P74P6T99; DEVELOPMENT_TEAM = Z6P74P6T99;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@ -422,7 +429,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 0.0.1; MARKETING_VERSION = 0.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp; PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
@ -476,6 +483,10 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Timeline; productName = Timeline;
}; };
9F35DB43294F9A7D00B3281A /* Status */ = {
isa = XCSwiftPackageProductDependency;
productName = Status;
};
9F398AA82935FFDB00A889F2 /* Account */ = { 9F398AA82935FFDB00A889F2 /* Account */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Account; productName = Account;

View file

@ -2,6 +2,7 @@ import SwiftUI
import Timeline import Timeline
import Account import Account
import Routeur import Routeur
import Status
extension View { extension View {
func withAppRouteur() -> some View { func withAppRouteur() -> some View {

View file

@ -16,13 +16,15 @@ let package = Package(
dependencies: [ dependencies: [
.package(name: "Network", path: "../Network"), .package(name: "Network", path: "../Network"),
.package(name: "Models", path: "../Models"), .package(name: "Models", path: "../Models"),
.package(name: "Status", path: "../Status"),
], ],
targets: [ targets: [
.target( .target(
name: "Account", name: "Account",
dependencies: [ dependencies: [
.product(name: "Network", package: "Network"), .product(name: "Network", package: "Network"),
.product(name: "Models", package: "Models") .product(name: "Models", package: "Models"),
.product(name: "Status", package: "Status"),
]), ]),
.testTarget( .testTarget(
name: "AccountTests", name: "AccountTests",

View file

@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import Models import Models
import DesignSystem
struct AccountDetailHeaderView: View { struct AccountDetailHeaderView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@ -70,17 +71,17 @@ struct AccountDetailHeaderView: View {
.font(.body) .font(.body)
.padding(.top, 8) .padding(.top, 8)
} }
.padding(.horizontal, 16) .padding(.horizontal, DS.Constants.layoutPadding)
.offset(y: -40) .offset(y: -40)
} }
private func makeCustomInfoLabel(title: String, count: Int) -> some View { private func makeCustomInfoLabel(title: String, count: Int) -> some View {
VStack { VStack {
Text("\(count)")
.font(.headline)
Text(title) Text(title)
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
Text("\(count)")
.font(.headline)
} }
} }
} }

View file

@ -1,6 +1,9 @@
import SwiftUI import SwiftUI
import Models import Models
import Network import Network
import Status
import Shimmer
import DesignSystem
public struct AccountDetailView: View { public struct AccountDetailView: View {
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@ -17,21 +20,71 @@ public struct AccountDetailView: View {
public var body: some View { public var body: some View {
ScrollView { ScrollView {
LazyVStack { LazyVStack {
switch viewModel.state { headerView
case .loading: statusesView
AccountDetailHeaderView(account: .placeholder()) .padding(.horizontal, 16)
.redacted(reason: .placeholder)
case let .data(account):
AccountDetailHeaderView(account: account)
case let .error(error):
Text("Error: \(error.localizedDescription)")
}
} }
} }
.edgesIgnoringSafeArea(.top) .edgesIgnoringSafeArea(.top)
.task { .task {
viewModel.client = client viewModel.client = client
await viewModel.fetchAccount() await viewModel.fetchAccount()
await viewModel.fetchStatuses()
}
}
@ViewBuilder
private var headerView: some View {
switch viewModel.state {
case .loading:
AccountDetailHeaderView(account: .placeholder())
.redacted(reason: .placeholder)
case let .data(account):
AccountDetailHeaderView(account: account)
case let .error(error):
Text("Error: \(error.localizedDescription)")
}
}
@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()
} }
} }
} }

View file

@ -11,7 +11,19 @@ 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
private var statuses: [Status] = []
init(accountId: String) { init(accountId: String) {
self.accountId = accountId self.accountId = accountId
@ -29,4 +41,26 @@ class AccountDetailViewModel: ObservableObject {
state = .error(error: error) state = .error(error: error)
} }
} }
func fetchStatuses() async {
do {
statusesState = .loading
statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil))
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} catch {
statusesState = .error(error: error)
}
}
func loadNextPage() async {
do {
guard let lastId = statuses.last?.id else { return }
statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)
let newStatuses: [Status] = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: lastId))
statuses.append(contentsOf: newStatuses)
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
} catch {
statusesState = .error(error: error)
}
}
} }

9
Packages/DesignSystem/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View file

@ -0,0 +1,23 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "DesignSystem",
platforms: [
.iOS(.v16),
],
products: [
.library(
name: "DesignSystem",
targets: ["DesignSystem"]),
],
dependencies: [],
targets: [
.target(
name: "DesignSystem",
dependencies: []),
]
)

View file

@ -0,0 +1,3 @@
# DesignSystem
A description of this package.

View file

@ -0,0 +1,7 @@
import Foundation
public struct DS {
public enum Constants {
public static let layoutPadding: CGFloat = 16
}
}

View file

@ -3,6 +3,7 @@ import Foundation
public enum Accounts: Endpoint { public enum Accounts: Endpoint {
case accounts(id: String) case accounts(id: String)
case verifyCredentials case verifyCredentials
case statuses(id: String, sinceId: String?)
public func path() -> String { public func path() -> String {
switch self { switch self {
@ -10,10 +11,18 @@ public enum Accounts: Endpoint {
return "accounts/\(id)" return "accounts/\(id)"
case .verifyCredentials: case .verifyCredentials:
return "accounts/verify_credentials" return "accounts/verify_credentials"
case .statuses(let id, _):
return "accounts/\(id)/statuses"
} }
} }
public func queryItems() -> [URLQueryItem]? { public func queryItems() -> [URLQueryItem]? {
nil switch self {
case .statuses(_, let sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)]
default:
return nil
}
} }
} }

View file

@ -16,8 +16,10 @@ public enum Timelines: Endpoint {
public func queryItems() -> [URLQueryItem]? { public func queryItems() -> [URLQueryItem]? {
switch self { switch self {
case .pub(let sinceId): case .pub(let sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)] return [.init(name: "max_id", value: sinceId)]
case .home(let sinceId): case .home(let sinceId):
guard let sinceId else { return nil }
return [.init(name: "max_id", value: sinceId)] return [.init(name: "max_id", value: sinceId)]
} }
} }

9
Packages/Status/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View file

@ -0,0 +1,31 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Status",
platforms: [
.iOS(.v16),
],
products: [
.library(
name: "Status",
targets: ["Status"]),
],
dependencies: [
.package(name: "Models", path: "../Models"),
.package(name: "Routeur", path: "../Routeur"),
.package(name: "DesignSystem", path: "../DesignSystem"),
],
targets: [
.target(
name: "Status",
dependencies: [
.product(name: "Models", package: "Models"),
.product(name: "Routeur", package: "Routeur"),
.product(name: "DesignSystem", package: "DesignSystem"),
]),
]
)

View file

@ -0,0 +1,3 @@
# Status
A description of this package.

View file

@ -30,6 +30,6 @@ struct StatusActionsView: View {
} label: { } label: {
Image(systemName: "square.and.arrow.up") Image(systemName: "square.and.arrow.up")
} }
} }.tint(.black)
} }
} }

View file

@ -31,7 +31,7 @@ public struct StatusMediaPreviewView: View {
content: { image in content: { image in
image.resizable() image.resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(maxHeight: 200) .frame(maxHeight: attachements.count > 2 ? 100 : 200)
.clipped() .clipped()
.cornerRadius(4) .cornerRadius(4)
}, },

View file

@ -2,13 +2,17 @@ import SwiftUI
import Models import Models
import Routeur import Routeur
struct StatusRowView: View { public struct StatusRowView: View {
@Environment(\.redactionReasons) private var reasons @Environment(\.redactionReasons) private var reasons
@EnvironmentObject private var routeurPath: RouterPath @EnvironmentObject private var routeurPath: RouterPath
let status: Status private let status: Status
var body: some View { public init(status: Status) {
self.status = status
}
public var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
reblogView reblogView
statusView statusView

View file

@ -17,6 +17,7 @@ let package = Package(
.package(name: "Network", path: "../Network"), .package(name: "Network", path: "../Network"),
.package(name: "Models", path: "../Models"), .package(name: "Models", path: "../Models"),
.package(name: "Routeur", path: "../Routeur"), .package(name: "Routeur", path: "../Routeur"),
.package(name: "Status", path: "../Status"),
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0") .package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0")
], ],
targets: [ targets: [
@ -26,6 +27,7 @@ let package = Package(
.product(name: "Network", package: "Network"), .product(name: "Network", package: "Network"),
.product(name: "Models", package: "Models"), .product(name: "Models", package: "Models"),
.product(name: "Routeur", package: "Routeur"), .product(name: "Routeur", package: "Routeur"),
.product(name: "Status", package: "Status"),
.product(name: "Shimmer", package: "SwiftUI-Shimmer") .product(name: "Shimmer", package: "SwiftUI-Shimmer")
]), ]),
.testTarget( .testTarget(
@ -33,3 +35,4 @@ let package = Package(
dependencies: ["Timeline"]), dependencies: ["Timeline"]),
] ]
) )

View file

@ -2,6 +2,7 @@ import SwiftUI
import Network import Network
import Models import Models
import Shimmer import Shimmer
import Status
public struct TimelineView: View { public struct TimelineView: View {
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client

View file

@ -5,11 +5,11 @@ import Models
@MainActor @MainActor
class TimelineViewModel: ObservableObject { class TimelineViewModel: ObservableObject {
enum State { enum State {
enum PadingState { enum PagingState {
case hasNextPage, loadingNextPage case hasNextPage, loadingNextPage
} }
case loading case loading
case display(statuses: [Status], nextPageState: State.PadingState) case display(statuses: [Status], nextPageState: State.PagingState)
case error(error: Error) case error(error: Error)
} }