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:
@@ -12,6 +12,7 @@ import OutlineKit
|
|||||||
struct DocumentReaderView: View {
|
struct DocumentReaderView: View {
|
||||||
@Environment(SessionStore.self) private var session
|
@Environment(SessionStore.self) private var session
|
||||||
@Environment(StarStore.self) private var starStore
|
@Environment(StarStore.self) private var starStore
|
||||||
|
@AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false
|
||||||
|
|
||||||
@State private var viewModel: DocumentReaderViewModel
|
@State private var viewModel: DocumentReaderViewModel
|
||||||
let apiClient: OutlineAPIClient
|
let apiClient: OutlineAPIClient
|
||||||
@@ -56,6 +57,15 @@ struct DocumentReaderView: View {
|
|||||||
self.onDocumentCreated = onDocumentCreated
|
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 {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
@@ -110,7 +120,8 @@ struct DocumentReaderView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "square.and.arrow.up")
|
Image(systemName: "square.and.arrow.up")
|
||||||
}
|
}
|
||||||
.help("Share")
|
.help(isEffectivelyOnline ? "Share" : "Sharing needs an internet connection")
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
.popover(isPresented: $isShowingShareSheet, arrowEdge: .bottom) {
|
.popover(isPresented: $isShowingShareSheet, arrowEdge: .bottom) {
|
||||||
DocumentShareSheet(apiClient: apiClient, documentId: viewModel.documentId)
|
DocumentShareSheet(apiClient: apiClient, documentId: viewModel.documentId)
|
||||||
}
|
}
|
||||||
@@ -131,7 +142,8 @@ struct DocumentReaderView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "doc.badge.plus")
|
Image(systemName: "doc.badge.plus")
|
||||||
}
|
}
|
||||||
.help("New Document")
|
.help(isEffectivelyOnline ? "New Document" : "Creating documents needs an internet connection")
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
menuContent
|
menuContent
|
||||||
@@ -235,7 +247,8 @@ struct DocumentReaderView: View {
|
|||||||
viewModel.isPinned,
|
viewModel.isPinned,
|
||||||
viewModel.isInsightsEnabled ?? false,
|
viewModel.isInsightsEnabled ?? false,
|
||||||
viewModel.isFullWidth,
|
viewModel.isFullWidth,
|
||||||
viewModel.isEditing
|
viewModel.isEditing,
|
||||||
|
isEffectivelyOnline
|
||||||
].map(String.init).joined(separator: "-")
|
].map(String.init).joined(separator: "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,27 +318,33 @@ struct DocumentReaderView: View {
|
|||||||
Button("Permissions…") {
|
Button("Permissions…") {
|
||||||
isShowingShareSheet = true
|
isShowingShareSheet = true
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button("Templatize") {
|
Button("Templatize") {
|
||||||
Task { await templatize() }
|
Task { await templatize() }
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Button("Duplicate") {
|
Button("Duplicate") {
|
||||||
Task { await duplicate() }
|
Task { await duplicate() }
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Button("Unpublish") {
|
Button("Unpublish") {
|
||||||
isShowingUnpublishConfirmation = true
|
isShowingUnpublishConfirmation = true
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Button("Archive…") {
|
Button("Archive…") {
|
||||||
isShowingArchiveConfirmation = true
|
isShowingArchiveConfirmation = true
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button("Move") {
|
Button("Move") {
|
||||||
isShowingMoveSheet = true
|
isShowingMoveSheet = true
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
// Multipart file upload is its own subsystem — deferred rather than
|
// Multipart file upload is its own subsystem — deferred rather than
|
||||||
// half-built here.
|
// half-built here.
|
||||||
Button("Import Document…") {}
|
Button("Import Document…") {}
|
||||||
@@ -333,6 +352,7 @@ struct DocumentReaderView: View {
|
|||||||
Button("New Document") {
|
Button("New Document") {
|
||||||
isShowingNewDocumentSheet = true
|
isShowingNewDocumentSheet = true
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Button(viewModel.isPinned ? "Unpin from Home" : "Pin to Home") {
|
Button(viewModel.isPinned ? "Unpin from Home" : "Pin to Home") {
|
||||||
Task { await togglePin() }
|
Task { await togglePin() }
|
||||||
}
|
}
|
||||||
@@ -342,9 +362,13 @@ struct DocumentReaderView: View {
|
|||||||
Button("History") {
|
Button("History") {
|
||||||
isShowingHistorySheet = true
|
isShowingHistorySheet = true
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Button("Insights") {
|
Button("Insights") {
|
||||||
isShowingInsightsSheet = true
|
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") {
|
Button("Present") {
|
||||||
isShowingPresentSheet = true
|
isShowingPresentSheet = true
|
||||||
}
|
}
|
||||||
@@ -354,6 +378,7 @@ struct DocumentReaderView: View {
|
|||||||
Button("Download") {
|
Button("Download") {
|
||||||
Task { await download() }
|
Task { await download() }
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Button("Copy") {
|
Button("Copy") {
|
||||||
Task { await copyMarkdown() }
|
Task { await copyMarkdown() }
|
||||||
}
|
}
|
||||||
@@ -370,6 +395,7 @@ struct DocumentReaderView: View {
|
|||||||
get: { viewModel.isInsightsEnabled ?? false },
|
get: { viewModel.isInsightsEnabled ?? false },
|
||||||
set: { _ in Task { await toggleInsights() } }
|
set: { _ in Task { await toggleInsights() } }
|
||||||
))
|
))
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
// Confirmed against a live server: there's no per-document embeds
|
// Confirmed against a live server: there's no per-document embeds
|
||||||
// field. Only a workspace-level setting exists, and that's not
|
// field. Only a workspace-level setting exists, and that's not
|
||||||
// reachable via the API either (no `team.update` endpoint in the
|
// reachable via the API either (no `team.update` endpoint in the
|
||||||
@@ -386,6 +412,7 @@ struct DocumentReaderView: View {
|
|||||||
Button("Delete…", role: .destructive) {
|
Button("Delete…", role: .destructive) {
|
||||||
isShowingDeleteConfirmation = true
|
isShowingDeleteConfirmation = true
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func star() async {
|
private func star() async {
|
||||||
|
|||||||
Reference in New Issue
Block a user