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 append: Bool?
|
||||||
public let fullWidth: Bool?
|
public let fullWidth: Bool?
|
||||||
public let insightsEnabled: 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(
|
public init(
|
||||||
id: String,
|
id: String,
|
||||||
@@ -20,8 +14,7 @@ public struct UpdateDocumentRequest: Encodable, Sendable {
|
|||||||
text: String? = nil,
|
text: String? = nil,
|
||||||
append: Bool? = nil,
|
append: Bool? = nil,
|
||||||
fullWidth: Bool? = nil,
|
fullWidth: Bool? = nil,
|
||||||
insightsEnabled: Bool? = nil,
|
insightsEnabled: Bool? = nil
|
||||||
documentEmbeds: Bool? = nil
|
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.title = title
|
self.title = title
|
||||||
@@ -29,6 +22,5 @@ public struct UpdateDocumentRequest: Encodable, Sendable {
|
|||||||
self.append = append
|
self.append = append
|
||||||
self.fullWidth = fullWidth
|
self.fullWidth = fullWidth
|
||||||
self.insightsEnabled = insightsEnabled
|
self.insightsEnabled = insightsEnabled
|
||||||
self.documentEmbeds = documentEmbeds
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,22 @@ struct DocumentReaderView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis.circle")
|
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.loadFullContent() }
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadPinAndSubscriptionState()
|
await viewModel.loadPinAndSubscriptionState()
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
await viewModel.loadInsightsEnabledState()
|
||||||
|
}
|
||||||
.task {
|
.task {
|
||||||
while !Task.isCancelled {
|
while !Task.isCancelled {
|
||||||
await viewModel.loadViewers()
|
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
|
@ViewBuilder
|
||||||
private var viewerAvatars: some View {
|
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) {
|
HStack(spacing: -6) {
|
||||||
ForEach(viewModel.viewers.prefix(5)) { viewer in
|
ForEach(viewModel.viewers.prefix(5)) { viewer in
|
||||||
AvatarBadge(
|
AvatarBadge(
|
||||||
@@ -255,9 +281,10 @@ struct DocumentReaderView: View {
|
|||||||
Button(starStore.isStarred(documentId: viewModel.documentId) ? "Unstar" : "Star") {
|
Button(starStore.isStarred(documentId: viewModel.documentId) ? "Unstar" : "Star") {
|
||||||
Task { await star() }
|
Task { await star() }
|
||||||
}
|
}
|
||||||
Button(viewModel.isSubscribed ? "Unsubscribe" : "Subscribe") {
|
Toggle("Subscribed", isOn: Binding(
|
||||||
Task { await toggleSubscription() }
|
get: { viewModel.isSubscribed },
|
||||||
}
|
set: { _ in Task { await toggleSubscription() } }
|
||||||
|
))
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
@@ -329,15 +356,20 @@ struct DocumentReaderView: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
Button("Enable Viewer Insights") {
|
Toggle("Viewer Insights", isOn: Binding(
|
||||||
Task { await enableInsights() }
|
get: { viewModel.isInsightsEnabled ?? false },
|
||||||
}
|
set: { _ in Task { await toggleInsights() } }
|
||||||
Button("Enable Embeds") {
|
))
|
||||||
Task { await enableEmbeds() }
|
// Confirmed against a live server: there's no per-document embeds
|
||||||
}
|
// field. Only a workspace-level setting exists, and that's not
|
||||||
Button(viewModel.isFullWidth ? "Default Width" : "Full Width") {
|
// reachable via the API either (no `team.update` endpoint in the
|
||||||
Task { await toggleFullWidth() }
|
// 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()
|
Divider()
|
||||||
|
|
||||||
@@ -378,19 +410,11 @@ struct DocumentReaderView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func enableInsights() async {
|
private func toggleInsights() async {
|
||||||
do {
|
do {
|
||||||
try await viewModel.enableViewerInsights()
|
try await viewModel.toggleViewerInsights()
|
||||||
} catch {
|
} catch {
|
||||||
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't enable viewer insights.")
|
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't update viewer insights.")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func enableEmbeds() async {
|
|
||||||
do {
|
|
||||||
try await viewModel.enableEmbeds()
|
|
||||||
} catch {
|
|
||||||
actionErrorMessage = outlineErrorMessage(error, fallback: "Couldn't enable embeds.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ final class DocumentReaderViewModel {
|
|||||||
private var subscriptionId: String?
|
private var subscriptionId: String?
|
||||||
private(set) var share: OutlineShare?
|
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
|
let documentId: String
|
||||||
private let apiClient: OutlineAPIClient
|
private let apiClient: OutlineAPIClient
|
||||||
|
|
||||||
@@ -100,6 +106,15 @@ final class DocumentReaderViewModel {
|
|||||||
share = try? await apiClient.shareInfo(documentId: documentId)
|
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 {
|
func togglePin() async throws {
|
||||||
if let pinId {
|
if let pinId {
|
||||||
self.pinId = nil
|
self.pinId = nil
|
||||||
@@ -170,14 +185,14 @@ final class DocumentReaderViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fire-and-forget: `insightsEnabled` isn't readable back off `Document`
|
func toggleViewerInsights() async throws {
|
||||||
/// in the vendored spec, so there's no state to reflect as a checkmark.
|
let newValue = !(isInsightsEnabled ?? false)
|
||||||
func enableViewerInsights() async throws {
|
isInsightsEnabled = newValue
|
||||||
_ = try await apiClient.updateDocument(UpdateDocumentRequest(id: documentId, insightsEnabled: true))
|
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