mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 09:41:02 +00:00
Notification tab
This commit is contained in:
parent
e2455a472e
commit
cab21c137b
22 changed files with 418 additions and 49 deletions
|
@ -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 = "<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>"; };
|
||||
9F35DB4629506F6600B3281A /* NotificationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTab.swift; sourceTree = "<group>"; };
|
||||
9F35DB4829506F7F00B3281A /* Notifications */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Notifications; path = Packages/Notifications; 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>"; };
|
||||
9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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;
|
||||
|
|
|
@ -16,6 +16,10 @@ struct IceCubesApp: App {
|
|||
.tabItem {
|
||||
Label("Home", systemImage: "globe")
|
||||
}
|
||||
NotificationsTab()
|
||||
.tabItem {
|
||||
Label("Notifications", systemImage: "bell")
|
||||
}
|
||||
SettingsTabs()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
|
|
17
IceCubesApp/App/Tabs/NotificationTab.swift
Normal file
17
IceCubesApp/App/Tabs/NotificationTab.swift
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
30
Packages/Models/Sources/Models/Notification.swift
Normal file
30
Packages/Models/Sources/Models/Notification.swift
Normal file
|
@ -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()]
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
}
|
||||
}
|
||||
}
|
9
Packages/Notifications/.gitignore
vendored
Normal file
9
Packages/Notifications/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
35
Packages/Notifications/Package.swift
Normal file
35
Packages/Notifications/Package.swift
Normal file
|
@ -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")
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
3
Packages/Notifications/README.md
Normal file
3
Packages/Notifications/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Notifications
|
||||
|
||||
A description of this package.
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
18
Packages/Status/Sources/Status/List/StatusesFetcher.swift
Normal file
18
Packages/Status/Sources/Status/List/StatusesFetcher.swift
Normal file
|
@ -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
|
||||
}
|
|
@ -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<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||
@ObservedObject private var fetcher: Fetcher
|
||||
|
||||
|
@ -35,7 +19,7 @@ public struct StatusesListView<Fetcher>: 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<Fetcher>: 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<Fetcher>: View where Fetcher: StatusesFetcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||
}
|
||||
|
||||
private var loadingRow: some View {
|
|
@ -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 {
|
||||
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)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue