diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 379f4631..1f05b920 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F24EEB729360C330042359D /* Preview Assets.xcassets */; }; 9F24EEBB293619210042359D /* Routeur in Frameworks */ = {isa = PBXBuildFile; productRef = 9F24EEBA293619210042359D /* Routeur */; }; 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 */; }; 9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; }; 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 = ""; }; 9F29553D292B67B600E0E81B /* Network */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Network; path = Packages/Network; sourceTree = ""; }; 9F29553E292B6AF600E0E81B /* Timeline */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Timeline; path = Packages/Timeline; sourceTree = ""; }; + 9F35DB42294F9A2900B3281A /* Status */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Status; path = Packages/Status; sourceTree = ""; }; + 9F35DB45294FA04C00B3281A /* DesignSystem */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DesignSystem; path = Packages/DesignSystem; sourceTree = ""; }; 9F398AA32935F90100A889F2 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = ""; }; 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = ""; }; 9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = ""; }; @@ -53,6 +56,7 @@ 9F398AA92935FFDB00A889F2 /* Account in Frameworks */, 9FBFE64E292A72BD00C250E9 /* Network in Frameworks */, 9F398AAB2935FFDB00A889F2 /* Models in Frameworks */, + 9F35DB44294F9A7D00B3281A /* Status in Frameworks */, 9F24EEBB293619210042359D /* Routeur in Frameworks */, 9F295540292B6C3400E0E81B /* Timeline in Frameworks */, ); @@ -105,10 +109,12 @@ 9FBFE63A292A715500C250E9 /* Products */, 9FBFE64C292A72BD00C250E9 /* Frameworks */, 9F398AAC2936005300A889F2 /* Account */, + 9F35DB45294FA04C00B3281A /* DesignSystem */, 9F398AA32935F90100A889F2 /* Models */, + 9F29553D292B67B600E0E81B /* Network */, 9F29553E292B6AF600E0E81B /* Timeline */, 9F24EEB92936185B0042359D /* Routeur */, - 9F29553D292B67B600E0E81B /* Network */, + 9F35DB42294F9A2900B3281A /* Status */, ); sourceTree = ""; }; @@ -171,6 +177,7 @@ 9F398AAA2935FFDB00A889F2 /* Models */, 9F24EEBA293619210042359D /* Routeur */, 9FAE4ACD29379A5A00772766 /* KeychainSwift */, + 9F35DB43294F9A7D00B3281A /* Status */, ); productName = IceCubesApp; productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */; @@ -360,7 +367,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_TEAM = Z6P74P6T99; ENABLE_HARDENED_RUNTIME = YES; @@ -381,7 +388,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.0.1; + MARKETING_VERSION = 0.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -401,7 +408,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 150; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_TEAM = Z6P74P6T99; ENABLE_HARDENED_RUNTIME = YES; @@ -422,7 +429,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.0.1; + MARKETING_VERSION = 0.0.2; PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -476,6 +483,10 @@ isa = XCSwiftPackageProductDependency; productName = Timeline; }; + 9F35DB43294F9A7D00B3281A /* Status */ = { + isa = XCSwiftPackageProductDependency; + productName = Status; + }; 9F398AA82935FFDB00A889F2 /* Account */ = { isa = XCSwiftPackageProductDependency; productName = Account; diff --git a/IceCubesApp/App/AppRouteur.swift b/IceCubesApp/App/AppRouteur.swift index e2762b0a..6aad6032 100644 --- a/IceCubesApp/App/AppRouteur.swift +++ b/IceCubesApp/App/AppRouteur.swift @@ -2,6 +2,7 @@ import SwiftUI import Timeline import Account import Routeur +import Status extension View { func withAppRouteur() -> some View { diff --git a/Packages/Account/Package.swift b/Packages/Account/Package.swift index a83636c9..d24d3893 100644 --- a/Packages/Account/Package.swift +++ b/Packages/Account/Package.swift @@ -16,13 +16,15 @@ let package = Package( dependencies: [ .package(name: "Network", path: "../Network"), .package(name: "Models", path: "../Models"), + .package(name: "Status", path: "../Status"), ], targets: [ .target( name: "Account", dependencies: [ .product(name: "Network", package: "Network"), - .product(name: "Models", package: "Models") + .product(name: "Models", package: "Models"), + .product(name: "Status", package: "Status"), ]), .testTarget( name: "AccountTests", diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index f9060eef..5f9b6885 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -1,5 +1,6 @@ import SwiftUI import Models +import DesignSystem struct AccountDetailHeaderView: View { @Environment(\.redactionReasons) private var reasons @@ -70,17 +71,17 @@ struct AccountDetailHeaderView: View { .font(.body) .padding(.top, 8) } - .padding(.horizontal, 16) + .padding(.horizontal, DS.Constants.layoutPadding) .offset(y: -40) } private func makeCustomInfoLabel(title: String, count: Int) -> some View { VStack { + Text("\(count)") + .font(.headline) Text(title) .font(.footnote) .foregroundColor(.gray) - Text("\(count)") - .font(.headline) } } } diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 3a97ab3e..10c5abe6 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -1,6 +1,9 @@ import SwiftUI import Models import Network +import Status +import Shimmer +import DesignSystem public struct AccountDetailView: View { @EnvironmentObject private var client: Client @@ -17,21 +20,71 @@ public struct AccountDetailView: View { public var body: some View { ScrollView { LazyVStack { - 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)") - } + headerView + statusesView + .padding(.horizontal, 16) } } .edgesIgnoringSafeArea(.top) .task { viewModel.client = client 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() } } } diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index d664a47e..9ae11e2a 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -11,7 +11,19 @@ class AccountDetailViewModel: ObservableObject { 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 statusesState: StatusesState = .loading + + private var statuses: [Status] = [] init(accountId: String) { self.accountId = accountId @@ -29,4 +41,26 @@ class AccountDetailViewModel: ObservableObject { 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) + } + } } diff --git a/Packages/DesignSystem/.gitignore b/Packages/DesignSystem/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/Packages/DesignSystem/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Packages/DesignSystem/Package.swift b/Packages/DesignSystem/Package.swift new file mode 100644 index 00000000..185bd760 --- /dev/null +++ b/Packages/DesignSystem/Package.swift @@ -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: []), + ] +) + diff --git a/Packages/DesignSystem/README.md b/Packages/DesignSystem/README.md new file mode 100644 index 00000000..53ac668e --- /dev/null +++ b/Packages/DesignSystem/README.md @@ -0,0 +1,3 @@ +# DesignSystem + +A description of this package. diff --git a/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift new file mode 100644 index 00000000..2daeb2e5 --- /dev/null +++ b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct DS { + public enum Constants { + public static let layoutPadding: CGFloat = 16 + } +} diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index 4a3b2da7..f48462af 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -3,6 +3,7 @@ import Foundation public enum Accounts: Endpoint { case accounts(id: String) case verifyCredentials + case statuses(id: String, sinceId: String?) public func path() -> String { switch self { @@ -10,10 +11,18 @@ public enum Accounts: Endpoint { return "accounts/\(id)" case .verifyCredentials: return "accounts/verify_credentials" + case .statuses(let id, _): + return "accounts/\(id)/statuses" } } 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 + } } } diff --git a/Packages/Network/Sources/Network/Endpoint/Timelines.swift b/Packages/Network/Sources/Network/Endpoint/Timelines.swift index 91058e39..e324b5f6 100644 --- a/Packages/Network/Sources/Network/Endpoint/Timelines.swift +++ b/Packages/Network/Sources/Network/Endpoint/Timelines.swift @@ -16,8 +16,10 @@ public enum Timelines: Endpoint { public func queryItems() -> [URLQueryItem]? { switch self { case .pub(let sinceId): + guard let sinceId else { return nil } return [.init(name: "max_id", value: sinceId)] case .home(let sinceId): + guard let sinceId else { return nil } return [.init(name: "max_id", value: sinceId)] } } diff --git a/Packages/Status/.gitignore b/Packages/Status/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/Packages/Status/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Packages/Status/Package.swift b/Packages/Status/Package.swift new file mode 100644 index 00000000..0f1d4830 --- /dev/null +++ b/Packages/Status/Package.swift @@ -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"), + ]), + ] +) + diff --git a/Packages/Status/README.md b/Packages/Status/README.md new file mode 100644 index 00000000..95b2b014 --- /dev/null +++ b/Packages/Status/README.md @@ -0,0 +1,3 @@ +# Status + +A description of this package. diff --git a/Packages/Timeline/Sources/Timeline/Status/StatusActionsView.swift b/Packages/Status/Sources/Status/StatusActionsView.swift similarity index 96% rename from Packages/Timeline/Sources/Timeline/Status/StatusActionsView.swift rename to Packages/Status/Sources/Status/StatusActionsView.swift index 975f24ae..da434abb 100644 --- a/Packages/Timeline/Sources/Timeline/Status/StatusActionsView.swift +++ b/Packages/Status/Sources/Status/StatusActionsView.swift @@ -30,6 +30,6 @@ struct StatusActionsView: View { } label: { Image(systemName: "square.and.arrow.up") } - } + }.tint(.black) } } diff --git a/Packages/Timeline/Sources/Timeline/Status/StatusDetailVIew.swift b/Packages/Status/Sources/Status/StatusDetailVIew.swift similarity index 100% rename from Packages/Timeline/Sources/Timeline/Status/StatusDetailVIew.swift rename to Packages/Status/Sources/Status/StatusDetailVIew.swift diff --git a/Packages/Timeline/Sources/Timeline/Status/StatusMediaPreviewView.swift b/Packages/Status/Sources/Status/StatusMediaPreviewView.swift similarity index 94% rename from Packages/Timeline/Sources/Timeline/Status/StatusMediaPreviewView.swift rename to Packages/Status/Sources/Status/StatusMediaPreviewView.swift index 4a53f8aa..f6b43225 100644 --- a/Packages/Timeline/Sources/Timeline/Status/StatusMediaPreviewView.swift +++ b/Packages/Status/Sources/Status/StatusMediaPreviewView.swift @@ -31,7 +31,7 @@ public struct StatusMediaPreviewView: View { content: { image in image.resizable() .aspectRatio(contentMode: .fill) - .frame(maxHeight: 200) + .frame(maxHeight: attachements.count > 2 ? 100 : 200) .clipped() .cornerRadius(4) }, diff --git a/Packages/Timeline/Sources/Timeline/Status/StatusRowView.swift b/Packages/Status/Sources/Status/StatusRowView.swift similarity index 93% rename from Packages/Timeline/Sources/Timeline/Status/StatusRowView.swift rename to Packages/Status/Sources/Status/StatusRowView.swift index 2af12ad5..80588ee7 100644 --- a/Packages/Timeline/Sources/Timeline/Status/StatusRowView.swift +++ b/Packages/Status/Sources/Status/StatusRowView.swift @@ -2,13 +2,17 @@ import SwiftUI import Models import Routeur -struct StatusRowView: View { +public struct StatusRowView: View { @Environment(\.redactionReasons) private var reasons @EnvironmentObject private var routeurPath: RouterPath - let status: Status + private let status: Status + + public init(status: Status) { + self.status = status + } - var body: some View { + public var body: some View { VStack(alignment: .leading) { reblogView statusView diff --git a/Packages/Timeline/Package.swift b/Packages/Timeline/Package.swift index c7328e42..97aef48d 100644 --- a/Packages/Timeline/Package.swift +++ b/Packages/Timeline/Package.swift @@ -17,6 +17,7 @@ let package = Package( .package(name: "Network", path: "../Network"), .package(name: "Models", path: "../Models"), .package(name: "Routeur", path: "../Routeur"), + .package(name: "Status", path: "../Status"), .package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0") ], targets: [ @@ -26,6 +27,7 @@ let package = Package( .product(name: "Network", package: "Network"), .product(name: "Models", package: "Models"), .product(name: "Routeur", package: "Routeur"), + .product(name: "Status", package: "Status"), .product(name: "Shimmer", package: "SwiftUI-Shimmer") ]), .testTarget( @@ -33,3 +35,4 @@ let package = Package( dependencies: ["Timeline"]), ] ) + diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 455a487f..bc8fe37d 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -2,6 +2,7 @@ import SwiftUI import Network import Models import Shimmer +import Status public struct TimelineView: View { @EnvironmentObject private var client: Client diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 1ec1708d..a02103e1 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -5,11 +5,11 @@ import Models @MainActor class TimelineViewModel: ObservableObject { enum State { - enum PadingState { + enum PagingState { case hasNextPage, loadingNextPage } case loading - case display(statuses: [Status], nextPageState: State.PadingState) + case display(statuses: [Status], nextPageState: State.PagingState) case error(error: Error) }