diff --git a/src/components/RecordPreview.tsx b/src/components/RecordPreview.tsx index edd8e74..7db4299 100644 --- a/src/components/RecordPreview.tsx +++ b/src/components/RecordPreview.tsx @@ -50,12 +50,12 @@ export default function RecordPreview({ const recordType = value.$type || collection; const displayType = recordType.replace('app.bsky.', '').replace('com.atproto.', '').replace('net.anisota.', ''); - // Get a few key interesting fields to preview (limit to 5-6 on universal - // link pages, no cap inside the explorer where the page is the canonical - // record view). - const allFields = Object.entries(value).filter( - ([key]) => !key.startsWith('$') && key !== 'createdAt' && key !== 'updatedAt' - ); + // Show every field in the record value — including $type, createdAt, and + // updatedAt. The preview is meant to be a faithful, readable rendering of + // the record (and, inside the explorer, the rich counterpart to the raw + // JSON view), so dropping fields would make the two disagree and hide real + // data. Universal-link pages still cap the count for a compact teaser. + const allFields = Object.entries(value); const previewFields = hideExplorerCtas ? allFields : allFields.slice(0, 6); const hasMoreFields = !hideExplorerCtas && allFields.length > 6; diff --git a/src/components/account/tabs/GeneralTab.tsx b/src/components/account/tabs/GeneralTab.tsx index 4c37a2b..307118c 100644 --- a/src/components/account/tabs/GeneralTab.tsx +++ b/src/components/account/tabs/GeneralTab.tsx @@ -122,6 +122,22 @@ function AppearanceCard() { checked={prefs.minimalPostPreview} onChange={(next) => update((p) => ({ ...p, minimalPostPreview: next }))} /> + + update((p) => ({ ...p, hideRichPreview: next }))} + /> + + update((p) => ({ ...p, showRawRecordJson: next }))} + /> ); } diff --git a/src/components/explore/RecordExplorer.tsx b/src/components/explore/RecordExplorer.tsx index 64f5372..35dd791 100644 --- a/src/components/explore/RecordExplorer.tsx +++ b/src/components/explore/RecordExplorer.tsx @@ -111,6 +111,12 @@ export default function RecordExplorer({ repo, collection, rkey }: Props) { const isPost = collection === 'app.bsky.feed.post'; const showPostViewSwitch = !prefsLoading && isPost; const minimalPost = showPostViewSwitch && prefs.minimalPostPreview; + // Rich-preview vs raw-JSON view state. Both gated on prefs having settled so + // first paint matches the defaults (rich shown, raw hidden) and doesn't + // flicker. The two are independent: a visitor can collapse the rich card, + // surface the raw JSON, do both, or neither. + const hideRich = !prefsLoading && prefs.hideRichPreview; + const showRawJson = !prefsLoading && prefs.showRawRecordJson; // Universal link uses the canonical `/profile/` path; bare-form // `///` still works as a fallback route but // shareable copies should point at the canonical one. @@ -177,44 +183,80 @@ export default function RecordExplorer({ repo, collection, rkey }: Props) { {!record && !editing && (

Loading record…

)} - - {editing && canEdit && agent ? ( - { - setRecord((prev) => (prev ? { ...prev, value: next } : prev)); - }} - onDeleted={() => { - setEditing(false); - router.push(`/explore/${repoSeg}/${collection}`); - }} - onCancel={() => setEditing(false)} + {(editing || !hideRich) && ( + + {editing && canEdit && agent ? ( + { + setRecord((prev) => (prev ? { ...prev, value: next } : prev)); + }} + onDeleted={() => { + setEditing(false); + router.push(`/explore/${repoSeg}/${collection}`); + }} + onCancel={() => setEditing(false)} + /> + ) : ( + + )} + + )} + {/* View switches beneath the preview. "Hide rich preview" collapses + the rich card; "Show raw JSON" swaps in the linkified record JSON + (replacing the old standalone disclosure at the page foot). The + minimal/full post switch only applies to the rich post card, so + it's hidden once the rich preview is collapsed. All flip persisted + prefs so the choice sticks and syncs across devices. */} + {!editing && record && ( +
+ + updatePrefs((p) => ({ ...p, hideRichPreview: !p.hideRichPreview })) + } /> - ) : ( - + updatePrefs((p) => ({ ...p, minimalPostPreview: !p.minimalPostPreview })) + } + /> + )} + + updatePrefs((p) => ({ ...p, showRawRecordJson: !p.showRawRecordJson })) + } /> - )} - - {/* Inline switch between the rich post card and the minimal - record-only view — mirrors the repo page's profile switch so - users can flip without round-tripping through settings. */} - {!editing && showPostViewSwitch && ( - - updatePrefs((p) => ({ ...p, minimalPostPreview: !p.minimalPostPreview })) - } - /> +
+ )} + {/* Raw record JSON, surfaced right under the preview area so it + reads as the rich preview's counterpart rather than a footnote. */} + {!editing && record && showRawJson && ( + + + )} )} @@ -273,11 +315,11 @@ export default function RecordExplorer({ repo, collection, rkey }: Props) {
)} - {/* Standalone Edit chip — only for post / margin previews that - don't have a place to slot the button into their layout, and - only in read mode. Sits with the sign-in prompt below as the - page's editing affordances. */} - {!editsInPreviewFooter && !editing && editButton && ( + {/* Standalone Edit chip — for post / margin previews that don't have a + place to slot the button into their layout, and as the fallback when + the rich preview (which would host the generic record's footer Edit + button) is collapsed. Read mode only. */} + {(!editsInPreviewFooter || hideRich) && !editing && editButton && (
)} - {record && ( -
- Raw record JSON - -
- )}
); } /** - * Quiet text toggle between the rich post card and the minimal - * record-only view, mirroring the repo page's ProfileViewSwitch. Flips the - * persisted `minimalPostPreview` pref so the choice sticks and syncs. + * Quiet text toggle used for the record page's view switches (hide/show the + * rich preview, minimal/full post, show/hide raw JSON). Mirrors the repo + * page's ProfileViewSwitch styling; callers own the persisted pref each one + * flips and supply a state-dependent label. */ -function PostViewSwitch({ - minimal, +function ViewSwitchButton({ + label, onToggle, }: { - minimal: boolean; + label: string; onToggle: () => void; }) { return ( @@ -337,8 +374,6 @@ function PostViewSwitch({ type="button" onClick={onToggle} style={{ - alignSelf: 'flex-start', - marginTop: '-0.5rem', padding: 0, background: 'transparent', border: 0, @@ -356,7 +391,7 @@ function PostViewSwitch({ e.currentTarget.style.color = 'var(--text-tertiary)'; }} > - {minimal ? 'Show full post preview' : 'Use minimal view'} + {label} ); } diff --git a/src/utils/atproto/preferencesPds.ts b/src/utils/atproto/preferencesPds.ts index 1514a53..1adbcdf 100644 --- a/src/utils/atproto/preferencesPds.ts +++ b/src/utils/atproto/preferencesPds.ts @@ -25,6 +25,8 @@ * hideRepoGlance?: boolean // hide the "Repo at a glance" section * repoGlanceCollapsedByDefault?: boolean // start that section collapsed * minimalProfile?: boolean // minimal profile on repo pages + * hideRichPreview?: boolean // collapse the rich preview on record pages + * showRawRecordJson?: boolean // show raw record JSON on record pages * updatedAt: datetime * * // Legacy — still written for back-compat with older Aturi clients @@ -101,6 +103,8 @@ export async function writePreferencesToPds( hideRepoGlance: prefs.hideRepoGlance, repoGlanceCollapsedByDefault: prefs.repoGlanceCollapsedByDefault, minimalProfile: prefs.minimalProfile, + hideRichPreview: prefs.hideRichPreview, + showRawRecordJson: prefs.showRawRecordJson, // Legacy fields kept for back-compat with older clients. hiddenWaypoints: prefs.hiddenWaypoints, waypointOrder: prefs.waypointOrder, diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index 7cd140a..4f591ce 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -140,6 +140,19 @@ export type Preferences = { * offers an inline switch to flip back to the full preview. */ minimalPostPreview: boolean; + /** + * Collapse the rich preview card on explorer record pages. Paired with + * `showRawRecordJson` this lets a visitor read the record as raw JSON + * instead of (or alongside) the rich rendering. Flipped by the inline + * "Hide rich preview" switch beneath the preview. + */ + hideRichPreview: boolean; + /** + * Show the raw (linkified) record JSON on explorer record pages. Replaces + * the old always-present "Raw record JSON" disclosure with a persisted + * toggle, surfaced by the inline "Show raw JSON" switch. + */ + showRawRecordJson: boolean; /** * ISO timestamp of last local change. Used to break ties when local and * PDS prefs both exist on sign-in. @@ -165,6 +178,8 @@ export const DEFAULT_PREFERENCES: Preferences = { repoGlanceCollapsedByDefault: false, minimalProfile: false, minimalPostPreview: false, + hideRichPreview: false, + showRawRecordJson: false, updatedAt: new Date(0).toISOString(), }; @@ -266,6 +281,10 @@ export function mergeWithDefaults(input: Partial | null | undefined typeof input.minimalProfile === 'boolean' ? input.minimalProfile : false; const minimalPostPreview = typeof input.minimalPostPreview === 'boolean' ? input.minimalPostPreview : false; + const hideRichPreview = + typeof input.hideRichPreview === 'boolean' ? input.hideRichPreview : false; + const showRawRecordJson = + typeof input.showRawRecordJson === 'boolean' ? input.showRawRecordJson : false; return { waypointGroups, hiddenWaypoints, @@ -281,6 +300,8 @@ export function mergeWithDefaults(input: Partial | null | undefined repoGlanceCollapsedByDefault, minimalProfile, minimalPostPreview, + hideRichPreview, + showRawRecordJson, updatedAt: typeof input.updatedAt === 'string' ? input.updatedAt : new Date(0).toISOString(), }; @@ -350,6 +371,8 @@ export function preferencesAreEqual(a: Preferences, b: Preferences): boolean { a.repoGlanceCollapsedByDefault === b.repoGlanceCollapsedByDefault && a.minimalProfile === b.minimalProfile && a.minimalPostPreview === b.minimalPostPreview && + a.hideRichPreview === b.hideRichPreview && + a.showRawRecordJson === b.showRawRecordJson && JSON.stringify(a.waypointGroups) === JSON.stringify(b.waypointGroups) && JSON.stringify(a.customWaypoints) === JSON.stringify(b.customWaypoints) && JSON.stringify(a.knownWaypointIds) === JSON.stringify(b.knownWaypointIds) &&