Skip to content

Commit

Permalink
fix: browser error handling (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
bouassaba authored Nov 23, 2024
1 parent 82e41c9 commit 6a101f5
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 92 deletions.
File renamed without changes.
File renamed without changes.
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)
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
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

0 comments on commit 6a101f5

Please sign in to comment.