fix(reader): grey out share/permissions and other live-only actions offline

Share, Permissions, Templatize, Duplicate, Unpublish, Archive, Move,
New Document, History, Insights (sheet + Viewer Insights toggle), and
Download all hit the server directly with no offline path — disabled
(with a tooltip on the two toolbar buttons) whenever the app isn't
effectively online, instead of failing confusingly on tap.

Left enabled: Edit, Pin/Unpin, Star/Unstar, Subscribed, Full Width
(all queue via CachingOutlineAPIClient and sync later), Present and
Search in Document (both read documents.info, which is cached), and
Copy/Print (read already-loaded text directly, no network at all).
This commit is contained in:
2026-08-15 01:22:47 +01:00
parent a9db3e1412
commit 0d9f7d752d
@@ -12,6 +12,7 @@ import OutlineKit
struct DocumentReaderView: View {
@Environment(SessionStore.self) private var session
@Environment(StarStore.self) private var starStore
@AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false
@State private var viewModel: DocumentReaderViewModel
let apiClient: OutlineAPIClient
@@ -56,6 +57,15 @@ struct DocumentReaderView: View {
self.onDocumentCreated = onDocumentCreated
}
/// Editing, pin/star/subscribe, and Full Width all queue and sync later
/// (see `CachingOutlineAPIClient`) everything else here (sharing,
/// permissions, move/archive/delete/duplicate/templatize, history,
/// insights, export) hits the server directly with no offline path, so
/// it's disabled rather than left to fail confusingly on tap.
private var isEffectivelyOnline: Bool {
session.networkMonitor.isOnline && !isOfflineModeEnabled
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
@@ -110,7 +120,8 @@ struct DocumentReaderView: View {
} label: {
Image(systemName: "square.and.arrow.up")
}
.help("Share")
.help(isEffectivelyOnline ? "Share" : "Sharing needs an internet connection")
.disabled(!isEffectivelyOnline)
.popover(isPresented: $isShowingShareSheet, arrowEdge: .bottom) {
DocumentShareSheet(apiClient: apiClient, documentId: viewModel.documentId)
}
@@ -131,7 +142,8 @@ struct DocumentReaderView: View {
} label: {
Image(systemName: "doc.badge.plus")
}
.help("New Document")
.help(isEffectivelyOnline ? "New Document" : "Creating documents needs an internet connection")
.disabled(!isEffectivelyOnline)
Menu {
menuContent
@@ -235,7 +247,8 @@ struct DocumentReaderView: View {
viewModel.isPinned,
viewModel.isInsightsEnabled ?? false,
viewModel.isFullWidth,
viewModel.isEditing
viewModel.isEditing,
isEffectivelyOnline
].map(String.init).joined(separator: "-")
}
@@ -305,27 +318,33 @@ struct DocumentReaderView: View {
Button("Permissions…") {
isShowingShareSheet = true
}
.disabled(!isEffectivelyOnline)
Divider()
Button("Templatize") {
Task { await templatize() }
}
.disabled(!isEffectivelyOnline)
Button("Duplicate") {
Task { await duplicate() }
}
.disabled(!isEffectivelyOnline)
Button("Unpublish") {
isShowingUnpublishConfirmation = true
}
.disabled(!isEffectivelyOnline)
Button("Archive…") {
isShowingArchiveConfirmation = true
}
.disabled(!isEffectivelyOnline)
Divider()
Button("Move") {
isShowingMoveSheet = true
}
.disabled(!isEffectivelyOnline)
// Multipart file upload is its own subsystem deferred rather than
// half-built here.
Button("Import Document…") {}
@@ -333,6 +352,7 @@ struct DocumentReaderView: View {
Button("New Document") {
isShowingNewDocumentSheet = true
}
.disabled(!isEffectivelyOnline)
Button(viewModel.isPinned ? "Unpin from Home" : "Pin to Home") {
Task { await togglePin() }
}
@@ -342,9 +362,13 @@ struct DocumentReaderView: View {
Button("History") {
isShowingHistorySheet = true
}
.disabled(!isEffectivelyOnline)
Button("Insights") {
isShowingInsightsSheet = true
}
.disabled(!isEffectivelyOnline)
// Present/Search in Document both read `documents.info`, which is
// read-through cached they work offline on whatever's cached.
Button("Present") {
isShowingPresentSheet = true
}
@@ -354,6 +378,7 @@ struct DocumentReaderView: View {
Button("Download") {
Task { await download() }
}
.disabled(!isEffectivelyOnline)
Button("Copy") {
Task { await copyMarkdown() }
}
@@ -370,6 +395,7 @@ struct DocumentReaderView: View {
get: { viewModel.isInsightsEnabled ?? false },
set: { _ in Task { await toggleInsights() } }
))
.disabled(!isEffectivelyOnline)
// Confirmed against a live server: there's no per-document embeds
// field. Only a workspace-level setting exists, and that's not
// reachable via the API either (no `team.update` endpoint in the
@@ -386,6 +412,7 @@ struct DocumentReaderView: View {
Button("Delete…", role: .destructive) {
isShowingDeleteConfirmation = true
}
.disabled(!isEffectivelyOnline)
}
private func star() async {