mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 08:20:59 +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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
||||||
|
@ -32,6 +34,8 @@
|
||||||
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
||||||
|
@ -59,6 +63,7 @@
|
||||||
9F35DB44294F9A7D00B3281A /* Status in Frameworks */,
|
9F35DB44294F9A7D00B3281A /* Status in Frameworks */,
|
||||||
9F24EEBB293619210042359D /* Routeur in Frameworks */,
|
9F24EEBB293619210042359D /* Routeur in Frameworks */,
|
||||||
9F295540292B6C3400E0E81B /* Timeline in Frameworks */,
|
9F295540292B6C3400E0E81B /* Timeline in Frameworks */,
|
||||||
|
9F35DB4A29506FA100B3281A /* Notifications in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -89,6 +94,7 @@
|
||||||
children = (
|
children = (
|
||||||
9FE151A4293C90EA00E9683D /* Settings */,
|
9FE151A4293C90EA00E9683D /* Settings */,
|
||||||
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
||||||
|
9F35DB4629506F6600B3281A /* NotificationTab.swift */,
|
||||||
);
|
);
|
||||||
path = Tabs;
|
path = Tabs;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -112,6 +118,7 @@
|
||||||
9F35DB45294FA04C00B3281A /* DesignSystem */,
|
9F35DB45294FA04C00B3281A /* DesignSystem */,
|
||||||
9F398AA32935F90100A889F2 /* Models */,
|
9F398AA32935F90100A889F2 /* Models */,
|
||||||
9F29553D292B67B600E0E81B /* Network */,
|
9F29553D292B67B600E0E81B /* Network */,
|
||||||
|
9F35DB4829506F7F00B3281A /* Notifications */,
|
||||||
9F29553E292B6AF600E0E81B /* Timeline */,
|
9F29553E292B6AF600E0E81B /* Timeline */,
|
||||||
9F24EEB92936185B0042359D /* Routeur */,
|
9F24EEB92936185B0042359D /* Routeur */,
|
||||||
9F35DB42294F9A2900B3281A /* Status */,
|
9F35DB42294F9A2900B3281A /* Status */,
|
||||||
|
@ -178,6 +185,7 @@
|
||||||
9F24EEBA293619210042359D /* Routeur */,
|
9F24EEBA293619210042359D /* Routeur */,
|
||||||
9FAE4ACD29379A5A00772766 /* KeychainSwift */,
|
9FAE4ACD29379A5A00772766 /* KeychainSwift */,
|
||||||
9F35DB43294F9A7D00B3281A /* Status */,
|
9F35DB43294F9A7D00B3281A /* Status */,
|
||||||
|
9F35DB4929506FA100B3281A /* Notifications */,
|
||||||
);
|
);
|
||||||
productName = IceCubesApp;
|
productName = IceCubesApp;
|
||||||
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
||||||
|
@ -242,6 +250,7 @@
|
||||||
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
|
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
|
||||||
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */,
|
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */,
|
||||||
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
||||||
|
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
|
||||||
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -487,6 +496,10 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Status;
|
productName = Status;
|
||||||
};
|
};
|
||||||
|
9F35DB4929506FA100B3281A /* Notifications */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Notifications;
|
||||||
|
};
|
||||||
9F398AA82935FFDB00A889F2 /* Account */ = {
|
9F398AA82935FFDB00A889F2 /* Account */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Account;
|
productName = Account;
|
||||||
|
|
|
@ -16,6 +16,10 @@ struct IceCubesApp: App {
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Home", systemImage: "globe")
|
Label("Home", systemImage: "globe")
|
||||||
}
|
}
|
||||||
|
NotificationsTab()
|
||||||
|
.tabItem {
|
||||||
|
Label("Notifications", systemImage: "bell")
|
||||||
|
}
|
||||||
SettingsTabs()
|
SettingsTabs()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Settings", systemImage: "gear")
|
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
|
@MainActor
|
||||||
class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
let accountId: String
|
let accountId: String
|
||||||
var client: Client = .init(server: "")
|
var client: Client?
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
case loading, data(account: Account), error(error: Error)
|
case loading, data(account: Account), error(error: Error)
|
||||||
|
@ -28,6 +28,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAccount() async {
|
func fetchAccount() async {
|
||||||
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
state = .data(account: try await client.get(endpoint: Accounts.accounts(id: accountId)))
|
state = .data(account: try await client.get(endpoint: Accounts.accounts(id: accountId)))
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -36,6 +37,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStatuses() async {
|
func fetchStatuses() async {
|
||||||
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
statusesState = .loading
|
statusesState = .loading
|
||||||
statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil))
|
statuses = try await client.get(endpoint: Accounts.statuses(id: accountId, sinceId: nil))
|
||||||
|
@ -46,6 +48,7 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async {
|
||||||
|
guard let client else { return }
|
||||||
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)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Foundation
|
||||||
|
|
||||||
public struct DS {
|
public struct DS {
|
||||||
public enum Constants {
|
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 Shimmer
|
||||||
import DesignSystem
|
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 {
|
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
@ObservedObject private var fetcher: Fetcher
|
@ObservedObject private var fetcher: Fetcher
|
||||||
|
|
||||||
|
@ -35,7 +19,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.shimmering()
|
.shimmering()
|
||||||
Divider()
|
Divider()
|
||||||
.padding(.bottom, DS.Constants.layoutPadding)
|
.padding(.vertical, DS.Constants.dividerPadding)
|
||||||
}
|
}
|
||||||
case let .error(error):
|
case let .error(error):
|
||||||
Text(error.localizedDescription)
|
Text(error.localizedDescription)
|
||||||
|
@ -43,7 +27,7 @@ public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
ForEach(statuses) { status in
|
ForEach(statuses) { status in
|
||||||
StatusRowView(status: status)
|
StatusRowView(status: status)
|
||||||
Divider()
|
Divider()
|
||||||
.padding(.bottom, DS.Constants.layoutPadding)
|
.padding(.vertical, DS.Constants.dividerPadding)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch nextPageState {
|
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 {
|
private var loadingRow: some View {
|
|
@ -1,15 +1,18 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Models
|
import Models
|
||||||
import Routeur
|
import Routeur
|
||||||
|
import DesignSystem
|
||||||
|
|
||||||
public 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
|
||||||
|
|
||||||
private let status: Status
|
private let status: Status
|
||||||
|
private let isEmbed: Bool
|
||||||
|
|
||||||
public init(status: Status) {
|
public init(status: Status, isEmbed: Bool = false) {
|
||||||
self.status = status
|
self.status = status
|
||||||
|
self.isEmbed = isEmbed
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
|
@ -37,11 +40,13 @@ public struct StatusRowView: View {
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var statusView: some View {
|
private var statusView: some View {
|
||||||
if let status: AnyStatus = status.reblog ?? status {
|
if let status: AnyStatus = status.reblog ?? status {
|
||||||
|
if !isEmbed {
|
||||||
Button {
|
Button {
|
||||||
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
|
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
|
||||||
} label: {
|
} label: {
|
||||||
makeAccountView(status: status)
|
makeAccountView(status: status)
|
||||||
}.buttonStyle(.plain)
|
}.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
Text(try! AttributedString(markdown: status.content.asMarkdown))
|
Text(try! AttributedString(markdown: status.content.asMarkdown))
|
||||||
.font(.body)
|
.font(.body)
|
||||||
|
@ -58,25 +63,7 @@ public struct StatusRowView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func makeAccountView(status: AnyStatus) -> some View {
|
private func makeAccountView(status: AnyStatus) -> some View {
|
||||||
if reasons == .placeholder {
|
AvatarView(url: status.account.avatar)
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(status.account.displayName)
|
Text(status.account.displayName)
|
||||||
.font(.headline)
|
.font(.headline)
|
|
@ -3,6 +3,7 @@ import Network
|
||||||
import Models
|
import Models
|
||||||
import Shimmer
|
import Shimmer
|
||||||
import Status
|
import Status
|
||||||
|
import DesignSystem
|
||||||
|
|
||||||
public struct TimelineView: View {
|
public struct TimelineView: View {
|
||||||
@EnvironmentObject private var client: Client
|
@EnvironmentObject private var client: Client
|
||||||
|
@ -16,6 +17,7 @@ public struct TimelineView: View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
StatusesListView(fetcher: viewModel)
|
StatusesListView(fetcher: viewModel)
|
||||||
}
|
}
|
||||||
|
.padding(.top, DS.Constants.layoutPadding)
|
||||||
}
|
}
|
||||||
.navigationTitle(viewModel.timeline.rawValue)
|
.navigationTitle(viewModel.timeline.rawValue)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|
|
@ -17,9 +17,9 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var client: Client = .init(server: "") {
|
var client: Client? {
|
||||||
didSet {
|
didSet {
|
||||||
timeline = client.isAuth ? .home : .pub
|
timeline = client?.isAuth == true ? .home : .pub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,10 +37,11 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverName: String {
|
var serverName: String {
|
||||||
client.server
|
client?.server ?? "Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStatuses() async {
|
func fetchStatuses() async {
|
||||||
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
statusesState = .loading
|
statusesState = .loading
|
||||||
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil))
|
statuses = try await client.get(endpoint: timeline.endpoint(sinceId: nil))
|
||||||
|
@ -51,6 +52,7 @@ class TimelineViewModel: ObservableObject, StatusesFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async {
|
||||||
|
guard let client else { return }
|
||||||
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)
|
||||||
|
|
Loading…
Reference in a new issue