223 lines
8.0 KiB
Swift
223 lines
8.0 KiB
Swift
//
|
|
// https://mczachurski.dev
|
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
|
// Licensed under the Apache License 2.0.
|
|
//
|
|
|
|
import SwiftUI
|
|
import ServicesKit
|
|
import EnvironmentKit
|
|
import WidgetsKit
|
|
import OSLog
|
|
import Semaphore
|
|
|
|
struct HomeFeedView: View {
|
|
@Environment(\.managedObjectContext) private var viewContext
|
|
|
|
@EnvironmentObject var applicationState: ApplicationState
|
|
@EnvironmentObject var routerPath: RouterPath
|
|
|
|
@State private var allItemsLoaded = false
|
|
@State private var state: ViewState = .loading
|
|
|
|
@State private var opacity = 0.0
|
|
@State private var offset = -50.0
|
|
|
|
@FetchRequest var dbStatuses: FetchedResults<StatusData>
|
|
|
|
init(accountId: String) {
|
|
_dbStatuses = FetchRequest<StatusData>(
|
|
sortDescriptors: [SortDescriptor(\.id, order: .reverse)],
|
|
predicate: NSPredicate(format: "pixelfedAccount.id = %@", accountId))
|
|
}
|
|
|
|
var body: some View {
|
|
switch state {
|
|
case .loading:
|
|
LoadingIndicator()
|
|
.task {
|
|
await self.loadData()
|
|
}
|
|
case .loaded:
|
|
if self.dbStatuses.isEmpty {
|
|
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "home.title.noPhotos")
|
|
} else {
|
|
self.timeline()
|
|
}
|
|
case .error(let error):
|
|
ErrorView(error: error) {
|
|
self.state = .loading
|
|
await self.loadData()
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func timeline() -> some View {
|
|
ZStack {
|
|
ScrollView {
|
|
LazyVStack {
|
|
ForEach(dbStatuses, id: \.self) { item in
|
|
if self.shouldUpToDateBeVisible(statusId: item.id) {
|
|
self.upToDatePlaceholder()
|
|
}
|
|
|
|
ImageRow(statusData: item)
|
|
}
|
|
|
|
if allItemsLoaded == false {
|
|
LoadingIndicator()
|
|
.task {
|
|
do {
|
|
if let account = self.applicationState.account {
|
|
let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(
|
|
for: account,
|
|
includeReblogs: self.applicationState.showReboostedStatuses,
|
|
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt
|
|
)
|
|
|
|
if newStatusesCount == 0 {
|
|
allItemsLoaded = true
|
|
}
|
|
}
|
|
} catch {
|
|
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.newPhotosView()
|
|
.offset(y: self.offset)
|
|
.opacity(self.opacity)
|
|
}
|
|
.refreshable {
|
|
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
|
|
await self.refreshData()
|
|
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
|
|
}
|
|
.onChange(of: self.applicationState.amountOfNewStatuses) { _ in
|
|
self.calculateOffset()
|
|
}.onAppear {
|
|
self.calculateOffset()
|
|
}
|
|
}
|
|
|
|
private func refreshData() async {
|
|
do {
|
|
if let account = self.applicationState.account {
|
|
let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account,
|
|
includeReblogs: self.applicationState.showReboostedStatuses,
|
|
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt,
|
|
updateLastSeenStatus: true)
|
|
asyncAfter(0.75) {
|
|
self.applicationState.lastSeenStatusId = lastSeenStatusId
|
|
self.applicationState.amountOfNewStatuses = 0
|
|
}
|
|
}
|
|
} catch {
|
|
ErrorService.shared.handle(error, message: "global.error.errorDuringDownloadStatuses", showToastr: !Task.isCancelled)
|
|
}
|
|
}
|
|
|
|
private func loadData() async {
|
|
do {
|
|
// We have to load data automatically only when the database is empty.
|
|
guard self.dbStatuses.isEmpty else {
|
|
withAnimation {
|
|
self.state = .loaded
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if let account = self.applicationState.account {
|
|
_ = try await HomeTimelineService.shared.refreshTimeline(for: account,
|
|
includeReblogs: self.applicationState.showReboostedStatuses,
|
|
hideStatusesWithoutAlt: self.applicationState.hideStatusesWithoutAlt)
|
|
}
|
|
|
|
self.applicationState.amountOfNewStatuses = 0
|
|
self.state = .loaded
|
|
} catch {
|
|
if !Task.isCancelled {
|
|
ErrorService.shared.handle(error, message: "global.error.statusesNotRetrieved", showToastr: true)
|
|
self.state = .error(error)
|
|
} else {
|
|
ErrorService.shared.handle(error, message: "global.error.statusesNotRetrieved", showToastr: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func calculateOffset() {
|
|
if self.applicationState.amountOfNewStatuses > 0 {
|
|
withAnimation(.easeIn) {
|
|
self.showNewStatusesView()
|
|
}
|
|
} else {
|
|
withAnimation(.easeOut) {
|
|
self.hideNewStatusesView()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func showNewStatusesView() {
|
|
self.offset = 0.0
|
|
self.opacity = 1.0
|
|
}
|
|
|
|
private func hideNewStatusesView() {
|
|
self.offset = -50.0
|
|
self.opacity = 0.0
|
|
}
|
|
|
|
private func shouldUpToDateBeVisible(statusId: String) -> Bool {
|
|
return self.applicationState.lastSeenStatusId != dbStatuses.first?.id && self.applicationState.lastSeenStatusId == statusId
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func upToDatePlaceholder() -> some View {
|
|
VStack(alignment: .center) {
|
|
Image(systemName: "checkmark.seal")
|
|
.resizable()
|
|
.frame(width: 64, height: 64)
|
|
.fontWeight(.ultraLight)
|
|
.foregroundColor(self.applicationState.tintColor.color().opacity(0.6))
|
|
Text("home.title.allCaughtUp", comment: "You're all caught up")
|
|
.font(.title2)
|
|
.fontWeight(.thin)
|
|
.foregroundColor(Color.mainTextColor.opacity(0.6))
|
|
}
|
|
.padding(.vertical, 8)
|
|
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width * 0.75)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func newPhotosView() -> some View {
|
|
VStack(alignment: .trailing, spacing: 4) {
|
|
HStack {
|
|
Spacer()
|
|
|
|
HStack {
|
|
Image(systemName: "arrow.up")
|
|
.fontWeight(.light)
|
|
Text("\(self.applicationState.amountOfNewStatuses)")
|
|
.fontWeight(.semibold)
|
|
}
|
|
.padding(.vertical, 12)
|
|
.padding(.horizontal, 18)
|
|
.font(.callout)
|
|
.foregroundColor(Color.mainTextColor)
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding(.top, 10)
|
|
.padding(.trailing, 6)
|
|
}
|
|
}
|