fix(reader): real toggle checkmarks for Subscribed/Viewer Insights/Full Width
This branch never got the toggle-menu fixes that landed on chore/verify-pins-subscriptions-api — Subscribed/Viewer Insights/Full Width were plain Buttons with no on/off indicator, and the menu didn't force a rebuild on state change, so even a real Toggle would've shown a stale checkmark until the view was torn down and rebuilt. Ported that branch's fixes: Subscribed/Viewer Insights/Full Width are now real Toggle views bound to observable state; the overflow Menu is keyed to a .id() built from every toggle-backed state so SwiftUI actually re-evaluates the checkmarks; viewer avatars are gated on isInsightsEnabled so they hide immediately when insights are turned off. Also dropped the dead documentEmbeds field (confirmed no per-document embeds endpoint exists) — Enable Embeds is a disabled button with an explanation, matching the other branch.
This commit is contained in:
@@ -7,12 +7,6 @@ public struct UpdateDocumentRequest: Encodable, Sendable {
|
||||
public let append: Bool?
|
||||
public let fullWidth: Bool?
|
||||
public let insightsEnabled: Bool?
|
||||
/// Not in the vendored spec's `Document`/`documents.update` shape at all
|
||||
/// (only a workspace-level `documentEmbeds` flag exists there) — included
|
||||
/// speculatively since the field may exist on newer self-hosted servers.
|
||||
/// Unrecognized fields are typically ignored server-side rather than
|
||||
/// rejected, so this is low-risk even if unsupported.
|
||||
public let documentEmbeds: Bool?
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
@@ -20,8 +14,7 @@ public struct UpdateDocumentRequest: Encodable, Sendable {
|
||||
text: String? = nil,
|
||||
append: Bool? = nil,
|
||||
fullWidth: Bool? = nil,
|
||||
insightsEnabled: Bool? = nil,
|
||||
documentEmbeds: Bool? = nil
|
||||
insightsEnabled: Bool? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
@@ -29,6 +22,5 @@ public struct UpdateDocumentRequest: Encodable, Sendable {
|
||||
self.append = append
|
||||
self.fullWidth = fullWidth
|
||||
self.insightsEnabled = insightsEnabled
|
||||
self.documentEmbeds = documentEmbeds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +128,22 @@ struct DocumentReaderView: View {
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
// SwiftUI's macOS `Menu` doesn't reliably re-evaluate a
|
||||
// `Toggle`'s checkmark against updated @Observable state on
|
||||
// its own — without a fresh `.id()` per state combination,
|
||||
// toggling Subscribed/Viewer Insights/Full Width kept
|
||||
// showing the pre-toggle checkmark until the whole view was
|
||||
// torn down and rebuilt (e.g. navigating away and back).
|
||||
.id(menuIdentity)
|
||||
}
|
||||
}
|
||||
.task { await viewModel.loadFullContent() }
|
||||
.task {
|
||||
await viewModel.loadPinAndSubscriptionState()
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadInsightsEnabledState()
|
||||
}
|
||||
.task {
|
||||
while !Task.isCancelled {
|
||||
await viewModel.loadViewers()
|
||||
@@ -208,9 +218,25 @@ struct DocumentReaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every toggle-backed piece of state shown as a checkmark inside
|
||||
/// `menuContent` — see the `.id()` comment on the `Menu` above.
|
||||
private var menuIdentity: String {
|
||||
[
|
||||
starStore.isStarred(documentId: viewModel.documentId),
|
||||
viewModel.isSubscribed,
|
||||
viewModel.isPinned,
|
||||
viewModel.isInsightsEnabled ?? false,
|
||||
viewModel.isFullWidth,
|
||||
viewModel.isEditing
|
||||
].map(String.init).joined(separator: "-")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var viewerAvatars: some View {
|
||||
if !viewModel.viewers.isEmpty {
|
||||
// Tied to the Viewer Insights toggle — that's the feature this data
|
||||
// belongs to, so turning it off should hide the avatars immediately
|
||||
// rather than leaving them showing until the view reloads.
|
||||
if viewModel.isInsightsEnabled == true, !viewModel.viewers.isEmpty {
|
||||
HStack(spacing: -6) {
|
||||
ForEach(viewModel.viewers.prefix(5)) { viewer in
|
||||
AvatarBadge(
|
||||
@@ -255,9 +281,10 @@ struct DocumentReaderView: View {
|
||||
Button(starStore.isStarred(documentId: viewModel.documentId) ? "Unstar" : "Star") {
|
||||
Task { await star() }
|
||||
}
|
||||
Button(viewModel.isSubscribed ? "Unsubscribe" : "Subscribe") {
|
||||
Task { await toggleSubscription() }
|
||||
}
|
||||
Toggle("Subscribed", isOn: Binding(
|
||||
get: { viewModel.isSubscribed },
|
||||
set: { _ in Task { await toggleSubscription() } }
|
||||
))
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -329,15 +356,20 @@ struct DocumentReaderView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Enable Viewer Insights") {
|
||||
Task { await enableInsights() }
|
||||
}
|
||||
Button("Enable Embeds") {
|
||||
Task { await enableEmbeds() }
|
||||
}
|
||||
Button(viewModel.isFullWidth ? "Default Width" : "Full Width") {
|
||||
Task { await toggleFullWidth() }
|
||||
}
|
||||
Toggle("Viewer Insights", isOn: Binding(
|
||||
get: { viewModel.isInsightsEnabled ?? false },
|
||||
set: { _ in Task { await toggleInsights() } }
|
||||
))
|
||||
// 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
|
||||
// vendored spec) — disabled rather than kept as a broken action.
|
||||
Button("Enable Embeds") {}
|
||||
.disabled(true)
|
||||
Toggle("Full Width", isOn: Binding(
|
||||
get: { viewModel.isFullWidth },
|
||||
set: { _ in Task { await toggleFullWidth() } }
|
||||
))
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -378,19 +410,11 @@ struct DocumentReaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func enableInsights() async {
|
||||
private func toggleInsights() async {
|
||||
do {
|
||||
try await viewModel.enableViewerInsights()
|
||||
try await viewModel.toggleViewerInsights()
|
||||
} catch {
|
||||
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't enable viewer insights.")
|
||||
}
|
||||
}
|
||||
|
||||
private func enableEmbeds() async {
|
||||
do {
|
||||
try await viewModel.enableEmbeds()
|
||||
} catch {
|
||||
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't enable embeds.")
|
||||
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't update viewer insights.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ final class DocumentReaderViewModel {
|
||||
private var subscriptionId: String?
|
||||
private(set) var share: OutlineShare?
|
||||
|
||||
/// `nil` until checked. Inferred from whether `documents.insights`
|
||||
/// succeeds or fails — `insightsEnabled` isn't readable back off
|
||||
/// `Document` in the vendored spec, so there's no direct field to read.
|
||||
/// This is a heuristic, not confirmed server behavior.
|
||||
private(set) var isInsightsEnabled: Bool?
|
||||
|
||||
let documentId: String
|
||||
private let apiClient: OutlineAPIClient
|
||||
|
||||
@@ -100,6 +106,15 @@ final class DocumentReaderViewModel {
|
||||
share = try? await apiClient.shareInfo(documentId: documentId)
|
||||
}
|
||||
|
||||
func loadInsightsEnabledState() async {
|
||||
do {
|
||||
_ = try await apiClient.documentInsights(DocumentInsightsRequest(id: documentId))
|
||||
isInsightsEnabled = true
|
||||
} catch {
|
||||
isInsightsEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
func togglePin() async throws {
|
||||
if let pinId {
|
||||
self.pinId = nil
|
||||
@@ -170,14 +185,14 @@ final class DocumentReaderViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire-and-forget: `insightsEnabled` isn't readable back off `Document`
|
||||
/// in the vendored spec, so there's no state to reflect as a checkmark.
|
||||
func enableViewerInsights() async throws {
|
||||
_ = try await apiClient.updateDocument(UpdateDocumentRequest(id: documentId, insightsEnabled: true))
|
||||
func toggleViewerInsights() async throws {
|
||||
let newValue = !(isInsightsEnabled ?? false)
|
||||
isInsightsEnabled = newValue
|
||||
do {
|
||||
_ = try await apiClient.updateDocument(UpdateDocumentRequest(id: documentId, insightsEnabled: newValue))
|
||||
} catch {
|
||||
isInsightsEnabled = !newValue
|
||||
throw error
|
||||
}
|
||||
|
||||
/// Fire-and-forget, speculative field — see `UpdateDocumentRequest.documentEmbeds`.
|
||||
func enableEmbeds() async throws {
|
||||
_ = try await apiClient.updateDocument(UpdateDocumentRequest(id: documentId, documentEmbeds: true))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user