diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 1f05b920..90a2fc65 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 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 */; }; + 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F35DB4629506F6600B3281A /* NotificationTab.swift */; }; + 9F35DB4A29506FA100B3281A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9F35DB4929506FA100B3281A /* Notifications */; }; 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 */; }; @@ -32,6 +34,8 @@ 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 = ""; }; + 9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = ""; }; + 9F35DB4829506F7F00B3281A /* Notifications */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Notifications; path = Packages/Notifications; 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 = ""; }; @@ -59,6 +63,7 @@ 9F35DB44294F9A7D00B3281A /* Status in Frameworks */, 9F24EEBB293619210042359D /* Routeur in Frameworks */, 9F295540292B6C3400E0E81B /* Timeline in Frameworks */, + 9F35DB4A29506FA100B3281A /* Notifications in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -89,6 +94,7 @@ children = ( 9FE151A4293C90EA00E9683D /* Settings */, 9F398AB229360A4C00A889F2 /* TimelineTab.swift */, + 9F35DB4629506F6600B3281A /* NotificationTab.swift */, ); path = Tabs; sourceTree = ""; @@ -112,6 +118,7 @@ 9F35DB45294FA04C00B3281A /* DesignSystem */, 9F398AA32935F90100A889F2 /* Models */, 9F29553D292B67B600E0E81B /* Network */, + 9F35DB4829506F7F00B3281A /* Notifications */, 9F29553E292B6AF600E0E81B /* Timeline */, 9F24EEB92936185B0042359D /* Routeur */, 9F35DB42294F9A2900B3281A /* Status */, @@ -178,6 +185,7 @@ 9F24EEBA293619210042359D /* Routeur */, 9FAE4ACD29379A5A00772766 /* KeychainSwift */, 9F35DB43294F9A7D00B3281A /* Status */, + 9F35DB4929506FA100B3281A /* Notifications */, ); productName = IceCubesApp; productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */; @@ -242,6 +250,7 @@ 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, 9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, + 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */, 9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -487,6 +496,10 @@ isa = XCSwiftPackageProductDependency; productName = Status; }; + 9F35DB4929506FA100B3281A /* Notifications */ = { + isa = XCSwiftPackageProductDependency; + productName = Notifications; + }; 9F398AA82935FFDB00A889F2 /* Account */ = { isa = XCSwiftPackageProductDependency; productName = Account; diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift index ed9ed59b..d20f49d6 100644 --- a/IceCubesApp/App/IceCubesApp.swift +++ b/IceCubesApp/App/IceCubesApp.swift @@ -16,6 +16,10 @@ struct IceCubesApp: App { .tabItem { Label("Home", systemImage: "globe") } + NotificationsTab() + .tabItem { + Label("Notifications", systemImage: "bell") + } SettingsTabs() .tabItem { Label("Settings", systemImage: "gear") diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift new file mode 100644 index 00000000..027ba95c --- /dev/null +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -0,0 +1,17 @@ +import SwiftUI +import Timeline +import Routeur +import Network +import Notifications + +struct NotificationsTab: View { + @StateObject private var routeurPath = RouterPath() + + var body: some View { + NavigationStack(path: $routeurPath.path) { + NotificationsListView() + .withAppRouteur() + } + .environmentObject(routeurPath) + } +} diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 03d9c008..cc6099ba 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -6,7 +6,7 @@ import Status @MainActor class AccountDetailViewModel: ObservableObject, StatusesFetcher { let accountId: String - var client: Client = .init(server: "") + var client: Client? enum State { case loading, data(account: Account), error(error: Error) @@ -28,6 +28,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } func fetchAccount() async { + guard let client else { return } do { state = .data(account: try await client.get(endpoint: Accounts.accounts(id: accountId))) } catch { @@ -36,6 +37,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } func fetchStatuses() async { + guard let client else { return } do { statusesState = .loading statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil)) @@ -46,6 +48,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } func fetchNextPage() async { + guard let client else { return } do { guard let lastId = statuses.last?.id else { return } statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage) diff --git a/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift index 2daeb2e5..886bd242 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/DesignSystem.swift @@ -2,6 +2,7 @@ import Foundation public struct DS { public enum Constants { - public static let layoutPadding: CGFloat = 16 + public static let layoutPadding: CGFloat = 20 + public static let dividerPadding: CGFloat = 8 } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift new file mode 100644 index 00000000..fa706430 --- /dev/null +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift @@ -0,0 +1,32 @@ +import SwiftUI + +public struct AvatarView: View { + @Environment(\.redactionReasons) private var reasons + public let url: URL + + public init(url: URL) { + self.url = url + } + + public var body: some View { + if reasons == .placeholder { + RoundedRectangle(cornerRadius: 4) + .fill(.gray) + .frame(maxWidth: 40, maxHeight: 40) + } else { + AsyncImage( + url: url, + content: { image in + image.resizable() + .aspectRatio(contentMode: .fit) + .cornerRadius(4) + .frame(maxWidth: 40, maxHeight: 40) + }, + placeholder: { + ProgressView() + .frame(maxWidth: 40, maxHeight: 40) + } + ) + } + } +} diff --git a/Packages/Models/Sources/Models/Notification.swift b/Packages/Models/Sources/Models/Notification.swift new file mode 100644 index 00000000..ed950ad7 --- /dev/null +++ b/Packages/Models/Sources/Models/Notification.swift @@ -0,0 +1,30 @@ +import Foundation + +public struct Notification: Codable, Identifiable { + public enum NotificationType: String { + case follow, follow_request, mention, reblog, status, favourite, poll, update + } + + public let id: String + public let type: String + public let createdAt: ServerDate + public let account: Account + public let status: Status? + + public var supportedType: NotificationType? { + .init(rawValue: type) + } + + public static func placeholder() -> Notification { + .init(id: UUID().uuidString, + type: NotificationType.favourite.rawValue, + createdAt: "2022-12-16T10:20:54.000Z", + account: .placeholder(), + status: .placeholder()) + } + + public static func placeholders() -> [Notification] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } +} + diff --git a/Packages/Network/Sources/Network/Endpoint/Notifications.swift b/Packages/Network/Sources/Network/Endpoint/Notifications.swift new file mode 100644 index 00000000..0552ac5a --- /dev/null +++ b/Packages/Network/Sources/Network/Endpoint/Notifications.swift @@ -0,0 +1,20 @@ +import Foundation + +public enum Notifications: Endpoint { + case notifications(maxId: String?) + + public func path() -> String { + switch self { + case .notifications: + return "notifications" + } + } + + public func queryItems() -> [URLQueryItem]? { + switch self { + case .notifications(let maxId): + guard let maxId else { return nil } + return [.init(name: "max_id", value: maxId)] + } + } +} diff --git a/Packages/Notifications/.gitignore b/Packages/Notifications/.gitignore new file mode 100644 index 00000000..3b298120 --- /dev/null +++ b/Packages/Notifications/.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/Notifications/Package.swift b/Packages/Notifications/Package.swift new file mode 100644 index 00000000..384b7363 --- /dev/null +++ b/Packages/Notifications/Package.swift @@ -0,0 +1,35 @@ +// 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: "Notifications", + platforms: [ + .iOS(.v16), + ], + products: [ + .library( + name: "Notifications", + targets: ["Notifications"]), + ], + dependencies: [ + .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: [ + .target( + name: "Notifications", + dependencies: [ + .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") + ]), + ] +) + diff --git a/Packages/Notifications/README.md b/Packages/Notifications/README.md new file mode 100644 index 00000000..27d553de --- /dev/null +++ b/Packages/Notifications/README.md @@ -0,0 +1,3 @@ +# Notifications + +A description of this package. diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift new file mode 100644 index 00000000..e2534097 --- /dev/null +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -0,0 +1,108 @@ +import SwiftUI +import Models +import DesignSystem +import Status +import Routeur + +struct NotificationRowView: View { + @EnvironmentObject private var routeurPath: RouterPath + @Environment(\.redactionReasons) private var reasons + + let notification: Models.Notification + + var body: some View { + if let type = notification.supportedType { + HStack(alignment: .top, spacing: 8) { + AvatarView(url: notification.account.avatar) + .onTapGesture { + routeurPath.navigate(to: .accountDetailWithAccount(account: notification.account)) + } + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 0) { + if (type != .mention) { + Image(systemName: type.iconName()) + .resizable() + .frame(width: 16, height: 16) + .aspectRatio(contentMode: .fit) + .padding(.horizontal, 4) + if type.displayAccountName() { + Text(notification.account.displayName) + .font(.headline) + + Text(" ") + } + Text(type.label()) + .font(.body) + Spacer() + } + } + if let status = notification.status { + StatusRowView(status: status, isEmbed: true) + } else { + Text(notification.account.acct) + .font(.callout) + .foregroundColor(.gray) + } + } + } + } else { + EmptyView() + } + } +} + +extension Models.Notification.NotificationType { + func displayAccountName() -> Bool { + switch self { + case .status, .mention, .reblog, .follow, .follow_request, .favourite: + return true + case .poll, .update: + return false + } + } + + func label() -> String { + switch self { + case .status: + return "posted a status" + case .mention: + return "mentionned you" + case .reblog: + return "boosted" + case .follow: + return "followed you" + case .follow_request: + return "request to follow you" + case .favourite: + return "starred" + case .poll: + return "poll ended" + case .update: + return "has been edited" + } + } + + func iconName() -> String { + switch self { + case .status: + return "pencil" + case .mention: + return "at" + case .reblog: + return "arrow.left.arrow.right.circle.fill" + case .follow, .follow_request: + return "person.fill.badge.plus" + case .favourite: + return "star.fill" + case .poll: + return "chart.bar.fill" + case .update: + return "pencil.line" + } + } +} + +struct NotificationRowView_Previews: PreviewProvider { + static var previews: some View { + NotificationRowView(notification: .placeholder()) + } +} diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift new file mode 100644 index 00000000..76ec4cee --- /dev/null +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -0,0 +1,56 @@ +import SwiftUI +import Network +import Models +import Shimmer +import DesignSystem + +public struct NotificationsListView: View { + @EnvironmentObject private var client: Client + @StateObject private var viewModel = NotificationsViewModel() + @State private var didAppear: Bool = false + + public init() { } + + public var body: some View { + ScrollView { + LazyVStack { + if client.isAuth { + switch viewModel.state { + case .loading: + ForEach(Models.Notification.placeholders()) { notification in + NotificationRowView(notification: notification) + .redacted(reason: .placeholder) + .shimmering() + Divider() + .padding(.vertical, DS.Constants.dividerPadding) + } + + case let .display(notifications, _): + ForEach(notifications) { notification in + NotificationRowView(notification: notification) + Divider() + .padding(.vertical, DS.Constants.dividerPadding) + } + + case let .error(error): + Text(error.localizedDescription) + } + } else { + Text("Please Sign In to see your notifications") + .font(.title3) + } + } + .padding(.horizontal, DS.Constants.layoutPadding) + .padding(.top, DS.Constants.layoutPadding) + } + .task { + if !didAppear { + didAppear = true + viewModel.client = client + await viewModel.fetchNotifications() + } + } + .navigationTitle(Text("Notifications")) + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift new file mode 100644 index 00000000..7bdc0c68 --- /dev/null +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -0,0 +1,45 @@ +import Foundation +import SwiftUI +import Network +import Models + +@MainActor +class NotificationsViewModel: ObservableObject { + public enum State { + public enum PagingState { + case hasNextPage, loadingNextPage + } + case loading + case display(notifications: [Models.Notification], nextPageState: State.PagingState) + case error(error: Error) + } + + var client: Client? + @Published var state: State = .loading + + private var notifications: [Models.Notification] = [] + + func fetchNotifications() async { + guard let client else { return } + do { + state = .loading + notifications = try await client.get(endpoint: Notifications.notifications(maxId: nil)) + state = .display(notifications: notifications, nextPageState: .hasNextPage) + } catch { + state = .error(error: error) + } + } + + func fetchNextPage() async { + guard let client else { return } + do { + guard let lastId = notifications.last?.id else { return } + state = .display(notifications: notifications, nextPageState: .loadingNextPage) + let newNotifications: [Models.Notification] = try await client.get(endpoint: Notifications.notifications(maxId: lastId)) + notifications.append(contentsOf: newNotifications) + state = .display(notifications: notifications, nextPageState: .hasNextPage) + } catch { + state = .error(error: error) + } + } +} diff --git a/Packages/Status/Sources/Status/StatusDetailVIew.swift b/Packages/Status/Sources/Status/Detail/StatusDetailVIew.swift similarity index 100% rename from Packages/Status/Sources/Status/StatusDetailVIew.swift rename to Packages/Status/Sources/Status/Detail/StatusDetailVIew.swift diff --git a/Packages/Status/Sources/Status/List/StatusesFetcher.swift b/Packages/Status/Sources/Status/List/StatusesFetcher.swift new file mode 100644 index 00000000..0079f318 --- /dev/null +++ b/Packages/Status/Sources/Status/List/StatusesFetcher.swift @@ -0,0 +1,18 @@ +import SwiftUI +import Models + +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 +} diff --git a/Packages/Status/Sources/Status/StatusesListView.swift b/Packages/Status/Sources/Status/List/StatusesListView.swift similarity index 69% rename from Packages/Status/Sources/Status/StatusesListView.swift rename to Packages/Status/Sources/Status/List/StatusesListView.swift index 7da6299e..40676dfc 100644 --- a/Packages/Status/Sources/Status/StatusesListView.swift +++ b/Packages/Status/Sources/Status/List/StatusesListView.swift @@ -3,22 +3,6 @@ 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: View where Fetcher: StatusesFetcher { @ObservedObject private var fetcher: Fetcher @@ -35,7 +19,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { .redacted(reason: .placeholder) .shimmering() Divider() - .padding(.bottom, DS.Constants.layoutPadding) + .padding(.vertical, DS.Constants.dividerPadding) } case let .error(error): Text(error.localizedDescription) @@ -43,7 +27,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { ForEach(statuses) { status in StatusRowView(status: status) Divider() - .padding(.bottom, DS.Constants.layoutPadding) + .padding(.vertical, DS.Constants.dividerPadding) } switch nextPageState { @@ -59,7 +43,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { } } } - .padding(.horizontal, 16) + .padding(.horizontal, DS.Constants.layoutPadding) } private var loadingRow: some View { diff --git a/Packages/Status/Sources/Status/StatusActionsView.swift b/Packages/Status/Sources/Status/Row/StatusActionsView.swift similarity index 100% rename from Packages/Status/Sources/Status/StatusActionsView.swift rename to Packages/Status/Sources/Status/Row/StatusActionsView.swift diff --git a/Packages/Status/Sources/Status/StatusMediaPreviewView.swift b/Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift similarity index 100% rename from Packages/Status/Sources/Status/StatusMediaPreviewView.swift rename to Packages/Status/Sources/Status/Row/StatusMediaPreviewView.swift diff --git a/Packages/Status/Sources/Status/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift similarity index 69% rename from Packages/Status/Sources/Status/StatusRowView.swift rename to Packages/Status/Sources/Status/Row/StatusRowView.swift index 80588ee7..fb34a78c 100644 --- a/Packages/Status/Sources/Status/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -1,15 +1,18 @@ import SwiftUI import Models import Routeur +import DesignSystem public struct StatusRowView: View { @Environment(\.redactionReasons) private var reasons @EnvironmentObject private var routeurPath: RouterPath private let status: Status + private let isEmbed: Bool - public init(status: Status) { + public init(status: Status, isEmbed: Bool = false) { self.status = status + self.isEmbed = isEmbed } public var body: some View { @@ -37,11 +40,13 @@ public struct StatusRowView: View { @ViewBuilder private var statusView: some View { if let status: AnyStatus = status.reblog ?? status { - Button { - routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) - } label: { - makeAccountView(status: status) - }.buttonStyle(.plain) + if !isEmbed { + Button { + routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) + } label: { + makeAccountView(status: status) + }.buttonStyle(.plain) + } Text(try! AttributedString(markdown: status.content.asMarkdown)) .font(.body) @@ -58,25 +63,7 @@ public struct StatusRowView: View { @ViewBuilder private func makeAccountView(status: AnyStatus) -> some View { - if reasons == .placeholder { - RoundedRectangle(cornerRadius: 4) - .fill(.gray) - .frame(maxWidth: 40, maxHeight: 40) - } else { - AsyncImage( - url: status.account.avatar, - content: { image in - image.resizable() - .aspectRatio(contentMode: .fit) - .cornerRadius(4) - .frame(maxWidth: 40, maxHeight: 40) - }, - placeholder: { - ProgressView() - .frame(maxWidth: 40, maxHeight: 40) - } - ) - } + AvatarView(url: status.account.avatar) VStack(alignment: .leading) { Text(status.account.displayName) .font(.headline) diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 1ea0086f..03ab4701 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -3,6 +3,7 @@ import Network import Models import Shimmer import Status +import DesignSystem public struct TimelineView: View { @EnvironmentObject private var client: Client @@ -16,6 +17,7 @@ public struct TimelineView: View { LazyVStack { StatusesListView(fetcher: viewModel) } + .padding(.top, DS.Constants.layoutPadding) } .navigationTitle(viewModel.timeline.rawValue) .navigationBarTitleDisplayMode(.inline) diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 85439022..e4549d48 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -17,9 +17,9 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } } - var client: Client = .init(server: "") { + var client: Client? { didSet { - timeline = client.isAuth ? .home : .pub + timeline = client?.isAuth == true ? .home : .pub } } @@ -37,10 +37,11 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } var serverName: String { - client.server + client?.server ?? "Error" } func fetchStatuses() async { + guard let client else { return } do { statusesState = .loading statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil)) @@ -51,6 +52,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher { } func fetchNextPage() async { + guard let client else { return } do { guard let lastId = statuses.last?.id else { return } statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage)