Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: browser error handling #15

Merged
merged 2 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Sources/Protocols/ListItemScrollable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2024 Anass Bouassaba.
//
// Use of this software is governed by the Business Source License
// included in the file LICENSE in the root of this repository.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the GNU Affero General Public License v3.0 only, included in the file
// AGPL-3.0-only in the root of this repository.

import Foundation

protocol ListItemScrollable {
func onListItemAppear(_ id: String)
}
131 changes: 72 additions & 59 deletions Sources/Screens/Browser/BrowserList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,24 @@ import Combine
import SwiftUI
import VoltaserveCore

struct BrowserList: View {
struct BrowserList: View, LoadStateProvider, ViewDataProvider, TimerLifecycle, TokenDistributing, ListItemScrollable {
@EnvironmentObject private var tokenStore: TokenStore
@ObservedObject private var workspaceStore: WorkspaceStore
@StateObject private var browserStore = BrowserStore()
@State private var tappedItem: VOFile.Entity?
@State private var showError = false
@State private var searchText = ""
private let fileID: String
private let folderID: String
private let confirmLabelText: String?
private let onCompletion: ((String) -> Void)?
private let onDismiss: (() -> Void)?

init(
_ fileID: String,
_ folderID: String,
workspaceStore: WorkspaceStore,
confirmLabelText: String?,
onCompletion: ((String) -> Void)? = nil,
onDismiss: (() -> Void)? = nil
) {
self.fileID = fileID
self.folderID = folderID
self.workspaceStore = workspaceStore
self.confirmLabelText = confirmLabelText
self.onCompletion = onCompletion
Expand All @@ -40,74 +38,73 @@ struct BrowserList: View {

var body: some View {
VStack {
if let entities = browserStore.entities {
Group {
if entities.count == 0 {
Text("There are no items.")
} else {
List {
ForEach(entities, id: \.id) { file in
NavigationLink {
BrowserList(
file.id,
workspaceStore: workspaceStore,
confirmLabelText: confirmLabelText,
onCompletion: onCompletion
)
.navigationTitle(file.name)
} label: {
FileRow(file)
}
.onAppear {
onListItemAppear(file.id)
if isLoading {
ProgressView()
} else if let error {
VOErrorMessage(error)
} else {
if let entities = browserStore.entities {
Group {
if entities.count == 0 {
Text("There are no items.")
} else {
List {
ForEach(entities, id: \.id) { file in
NavigationLink {
BrowserList(
file.id,
workspaceStore: workspaceStore,
confirmLabelText: confirmLabelText,
onCompletion: onCompletion
)
.navigationTitle(file.name)
} label: {
FileRow(file)
}
.onAppear {
onListItemAppear(file.id)
}
}
}
.listStyle(.inset)
.searchable(text: $browserStore.searchText)
.onChange(of: browserStore.searchText) {
browserStore.searchPublisher.send($1)
}
.navigationDestination(item: $tappedItem) {
Viewer($0)
}
}
.listStyle(.inset)
.searchable(text: $searchText)
.onChange(of: browserStore.searchText) {
browserStore.searchPublisher.send($1)
}
.navigationDestination(item: $tappedItem) {
Viewer($0)
}
.voErrorAlert(
isPresented: $showError,
title: browserStore.errorTitle,
message: browserStore.errorMessage
)
}
.refreshable {
browserStore.fetchNextPage(replace: true)
}
}
.refreshable {
browserStore.fetchNextPage(replace: true)
}
} else {
ProgressView()
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(confirmLabelText ?? "Done") {
onCompletion?(fileID)
onCompletion?(folderID)
}
}
if let workspace = workspaceStore.current, fileID == workspace.rootID {
if let workspace = workspaceStore.current, folderID == workspace.rootID {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel") {
onDismiss?()
}
}
}
ToolbarItem(placement: .topBarLeading) {
if browserStore.isLoading, browserStore.entities != nil {
if browserStore.entitiesIsLoading {
ProgressView()
}
}
}
.onAppear {
browserStore.fileID = fileID
browserStore.folderID = folderID
if let token = tokenStore.token {
assignTokensToStores(token)
assignTokenToStores(token)
startTimers()
onAppearOrChange()
}
Expand All @@ -117,40 +114,56 @@ struct BrowserList: View {
}
.onChange(of: tokenStore.token) { _, newToken in
if let newToken {
assignTokensToStores(newToken)
assignTokenToStores(newToken)
onAppearOrChange()
}
}
.onChange(of: browserStore.query) {
browserStore.clear()
browserStore.fetchNextPage()
}
.sync($browserStore.searchText, with: $searchText)
.sync($browserStore.showError, with: $showError)
}

private func onAppearOrChange() {
// MARK: - LoadStateProvider

var isLoading: Bool {
workspaceStore.entities == nil && browserStore.folderIsLoading
}

var error: String? {
workspaceStore.entitiesError ?? browserStore.folderError
}

// MARK: - ViewDataProvider

func onAppearOrChange() {
fetchData()
}

private func fetchData() {
browserStore.fetch()
func fetchData() {
browserStore.fetchFolder()
browserStore.fetchNextPage(replace: true)
}

private func startTimers() {
// MARK: - TimerLifecycle

func startTimers() {
browserStore.startTimer()
}

private func stopTimers() {
func stopTimers() {
browserStore.stopTimer()
}

private func assignTokensToStores(_ token: VOToken.Value) {
// MARK: - TokenDistributing

func assignTokenToStores(_ token: VOToken.Value) {
browserStore.token = token
}

private func onListItemAppear(_ id: String) {
// MARK: - ListItemScrollable

func onListItemAppear(_ id: String) {
if browserStore.isEntityThreshold(id) {
browserStore.fetchNextPage()
}
Expand Down
59 changes: 29 additions & 30 deletions Sources/Screens/Browser/BrowserStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ import VoltaserveCore

class BrowserStore: ObservableObject {
@Published var entities: [VOFile.Entity]?
@Published var current: VOFile.Entity?
@Published var entitiesIsLoading: Bool = false
@Published var entitiesError: String?
@Published var folder: VOFile.Entity?
@Published var folderIsLoading: Bool = false
@Published var folderError: String?
@Published var query: VOFile.Query?
@Published var showError = false
@Published var errorTitle: String?
@Published var errorMessage: String?
@Published var searchText = ""
@Published var isLoading = false
private var list: VOFile.List?
private var cancellables = Set<AnyCancellable>()
private var timer: Timer?
private var fileClient: VOFile?
var fileID: String?
var folderID: String?
let searchPublisher = PassthroughSubject<String, Never>()

var token: VOToken.Value? {
Expand All @@ -51,23 +51,24 @@ class BrowserStore: ObservableObject {

// MARK: - Fetch

private func fetch(_ id: String) async throws -> VOFile.Entity? {
try await fileClient?.fetch(id)
private func fetchFolder() async throws -> VOFile.Entity? {
guard let folderID else { return nil }
return try await fileClient?.fetch(folderID)
}

func fetch() {
guard let fileID else { return }
var file: VOFile.Entity?

func fetchFolder() {
var folder: VOFile.Entity?
withErrorHandling {
file = try await self.fetch(fileID)
folder = try await self.fetchFolder()
return true
} before: {
self.folderIsLoading = true
} success: {
self.current = file
self.folder = folder
} failure: { message in
self.errorTitle = "Error: Fetching File"
self.errorMessage = message
self.showError = true
self.folderError = message
} anyways: {
self.folderIsLoading = false
}
}

Expand All @@ -80,15 +81,15 @@ class BrowserStore: ObservableObject {
}

func fetchNextPage(replace: Bool = false) {
guard let fileID else { return }
guard !isLoading else { return }
guard let folderID else { return }
guard !entitiesIsLoading else { return }

var nextPage = -1
var list: VOFile.List?

withErrorHandling {
if let list = self.list {
let probe = try await self.fetchProbe(fileID, size: Constants.pageSize)
let probe = try await self.fetchProbe(folderID, size: Constants.pageSize)
if let probe {
self.list = .init(
data: list.data,
Expand All @@ -102,10 +103,10 @@ class BrowserStore: ObservableObject {
}
if !self.hasNextPage() { return false }
nextPage = self.nextPage()
list = try await self.fetchList(fileID, page: nextPage)
list = try await self.fetchList(folderID, page: nextPage)
return true
} before: {
self.isLoading = true
self.entitiesIsLoading = true
} success: {
self.list = list
if let list {
Expand All @@ -116,11 +117,9 @@ class BrowserStore: ObservableObject {
}
}
} failure: { message in
self.errorTitle = "Error: Fetching Files"
self.errorMessage = message
self.showError = true
self.entitiesError = message
} anyways: {
self.isLoading = false
self.entitiesIsLoading = false
}
}

Expand Down Expand Up @@ -176,7 +175,7 @@ class BrowserStore: ObservableObject {
func startTimer() {
guard timer == nil else { return }
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
if let current = self.current {
if let current = self.folder {
Task {
var size = Constants.pageSize
if let list = self.list {
Expand All @@ -190,12 +189,12 @@ class BrowserStore: ObservableObject {
}
}
}
if let current = self.current {
if let current = self.folder {
Task {
let file = try await self.fetch(current.id)
let file = try await self.fetchFolder()
if let file {
DispatchQueue.main.async {
self.current = file
self.folder = file
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions Voltaserve.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
644C38B42C8F3744008E8F0F /* Helpers */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Helpers; sourceTree = "<group>"; };
644C38B72C8F3744008E8F0F /* Infrastructure */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (644C38DA2C8F3744008E8F0F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Infrastructure; sourceTree = "<group>"; };
64C0F65F2CF26AE8007049E7 /* Protocol */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Protocol; sourceTree = "<group>"; };
64C0F65F2CF26AE8007049E7 /* Protocols */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Protocols; sourceTree = "<group>"; };
64DA01D12C98766E0063A4CB /* Design */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Design; sourceTree = "<group>"; };
64DA46312CF1A7AA00D57366 /* Library */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Library; sourceTree = "<group>"; };
64E4C9B52C92495000395C9D /* Screens */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (64E4CA1B2C92496100395C9D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Screens; sourceTree = "<group>"; };
Expand Down Expand Up @@ -257,7 +257,7 @@
649066E02C5E35AD00BA3CCD /* Sources */ = {
isa = PBXGroup;
children = (
64C0F65F2CF26AE8007049E7 /* Protocol */,
64C0F65F2CF26AE8007049E7 /* Protocols */,
64DA46312CF1A7AA00D57366 /* Library */,
64E4C9B52C92495000395C9D /* Screens */,
64DA01D12C98766E0063A4CB /* Design */,
Expand Down Expand Up @@ -306,7 +306,7 @@
);
fileSystemSynchronizedGroups = (
644C38B42C8F3744008E8F0F /* Helpers */,
64C0F65F2CF26AE8007049E7 /* Protocol */,
64C0F65F2CF26AE8007049E7 /* Protocols */,
64DA01D12C98766E0063A4CB /* Design */,
64DA46312CF1A7AA00D57366 /* Library */,
);
Expand Down
Loading