Files
Outpost/Outpost/Features/Home/HomeView.swift
T
Puranjay Savar Mattas c2b41ca960 fix(sidebar): auto-refresh after creating a doc from Home or the reader toolbar
The reader's inline "New Document" button and Home's New Document sheet
only navigated to the new document — neither had a handle on the
sidebar row it landed under, so the tree stayed stale until the
periodic remote-changes poll (45s) surfaced the "reload" banner.

New Document flows that already have a direct handle on their own
sidebar row (right-click a collection, right-click a document) already
refreshed correctly and are untouched.

Threads a documentsChangedToken from ContentView_macOS down through
CollectionsTreeView -> CollectionTreeRow -> CollectionDocumentsOutline
as externalRefreshToken; every expanded row reloads itself when it
bumps, since neither Home nor the reader knows which row (if any)
corresponds to where the new document landed.
2026-08-14 16:46:40 +01:00

170 lines
6.5 KiB
Swift

#if os(macOS)
import SwiftUI
import OutlineKit
struct HomeView: View {
let apiClient: OutlineAPIClient
let onOpenDocument: (OutlineDocument) -> Void
/// Home has no sidebar row of its own to reload directly — this tells
/// the sidebar a document exists now so it can pick it up. See
/// `CollectionDocumentsOutline.externalRefreshToken`.
let onDocumentCreated: () -> Void
@State private var viewModel: HomeViewModel
@State private var selectedTab: HomeTab = .recentlyViewed
@State private var isShowingNewDocumentSheet = false
private let pinnedGridColumns = [GridItem(.adaptive(minimum: 260), spacing: 8)]
private let tabGridColumns = [GridItem(.adaptive(minimum: 220), spacing: 12)]
init(apiClient: OutlineAPIClient, onOpenDocument: @escaping (OutlineDocument) -> Void, onDocumentCreated: @escaping () -> Void) {
self.apiClient = apiClient
self.onOpenDocument = onOpenDocument
self.onDocumentCreated = onDocumentCreated
_viewModel = State(initialValue: HomeViewModel(apiClient: apiClient))
}
private var isShowingPinnedSection: Bool {
viewModel.isLoadingPinned || !viewModel.pinnedDocuments.isEmpty
}
var body: some View {
// The pinned section claims roughly the top half when it has
// anything to show (scrolling within itself if there are enough
// pinned documents to overflow that), and collapses away entirely
// when there's nothing pinned so the tabs get the full height.
GeometryReader { proxy in
VStack(spacing: 0) {
if isShowingPinnedSection {
pinnedSection
.frame(height: max(proxy.size.height / 2, 180))
Divider()
}
tabSection
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
isShowingNewDocumentSheet = true
} label: {
Image(systemName: "doc.badge.plus")
}
.help("New Document")
}
}
.sheet(isPresented: $isShowingNewDocumentSheet) {
NewDocumentSheet(apiClient: apiClient) { document in
onDocumentCreated()
onOpenDocument(document)
}
}
.task { await viewModel.loadPinned() }
.task(id: selectedTab) { await viewModel.load(tab: selectedTab) }
}
private var pinnedSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Pinned")
.font(.title3.weight(.semibold))
.padding(.horizontal, 24)
.padding(.top, 20)
if viewModel.isLoadingPinned {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollView {
LazyVGrid(columns: pinnedGridColumns, spacing: 8) {
ForEach(viewModel.pinnedDocuments) { document in
Button {
onOpenDocument(document)
} label: {
PinnedDocumentCard(document: document)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 24)
.padding(.bottom, 16)
}
}
}
}
private var tabSection: some View {
VStack(alignment: .leading, spacing: 0) {
tabBar
Divider()
let documents = viewModel.documents(for: selectedTab)
Group {
if viewModel.isLoadingTab && documents.isEmpty {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let errorMessage = viewModel.errorMessage, documents.isEmpty {
ContentUnavailableView {
Label("Couldn't Load Documents", systemImage: "exclamationmark.triangle")
} description: {
Text(errorMessage)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if documents.isEmpty {
ContentUnavailableView(
"No Documents",
systemImage: "doc.text",
description: Text("Nothing to show here yet.")
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollView {
LazyVGrid(columns: tabGridColumns, spacing: 12) {
ForEach(documents) { document in
Button {
onOpenDocument(document)
} label: {
DocumentCardView(document: document)
}
.buttonStyle(.plain)
}
}
.padding(24)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// Mirrors `CollectionOverviewView.tabBar`'s exact style, rather than the
// native `.pickerStyle(.segmented)` this started with.
private var tabBar: some View {
HStack(spacing: 4) {
Spacer(minLength: 0)
ForEach(HomeTab.allCases) { tab in
Button {
selectedTab = tab
} label: {
Text(tab.rawValue)
.font(.callout.weight(selectedTab == tab ? .semibold : .regular))
.foregroundStyle(selectedTab == tab ? Color.primary : Color.secondary)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
selectedTab == tab ? Color.accentColor.opacity(0.15) : Color.clear,
in: RoundedRectangle(cornerRadius: 6)
)
}
.buttonStyle(.plain)
}
Spacer(minLength: 0)
}
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
}
}
#endif