Skip to content

Commit

Permalink
feat: add useIsLivePreview & useIsPresentationTool hooks (#2050)
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan authored Oct 24, 2024
1 parent 2ab4fab commit 6aeecd4
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 24 deletions.
11 changes: 3 additions & 8 deletions apps/live-next/app/alert-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
'use client'

import {useIsPresentationTool} from '@sanity/next-loader/hooks'
import {useRouter} from 'next/navigation'
import {useSyncExternalStore, useTransition} from 'react'
import {useTransition} from 'react'
import {disableDraftMode} from './actions'

const emptySubscribe = () => () => {}

export default function AlertBanner() {
const router = useRouter()
const [pending, startTransition] = useTransition()

const shouldShow = useSyncExternalStore(
emptySubscribe,
() => window.top === window,
() => false,
)
const shouldShow = useIsPresentationTool() === false

if (!shouldShow) return null

Expand Down
10 changes: 9 additions & 1 deletion apps/live-next/app/draft-mode-status.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
'use client'

import {useDraftModeEnvironment, useDraftModePerspective} from '@sanity/next-loader/hooks'
import {
useDraftModeEnvironment,
useDraftModePerspective,
useIsLivePreview,
} from '@sanity/next-loader/hooks'

export function DraftModeStatus() {
const isLivePreview = useIsLivePreview()
const perspective = useDraftModePerspective()
const environment = useDraftModeEnvironment()

if (isLivePreview !== true) return null

return (
<div className="fixed bottom-3 right-3 block rounded bg-theme-inverse px-2 py-1 text-xs text-theme-inverse">
<p>perspective: {perspective}</p>
Expand Down
8 changes: 2 additions & 6 deletions apps/live-next/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,8 @@ export default async function RootLayout({children}: {children: React.ReactNode}
>
<body>
<section className="min-h-screen">
{(await draftMode()).isEnabled && (
<>
<AlertBanner />
<DraftModeStatus />
</>
)}
{(await draftMode()).isEnabled && <AlertBanner />}
<DraftModeStatus />
<main>{children}</main>
<Suspense>
<Footer />
Expand Down
2 changes: 1 addition & 1 deletion apps/live-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "next build",
"debug": "NEXT_PRIVATE_DEBUG_CACHE=1 next build --profile && NEXT_PRIVATE_DEBUG_CACHE=1 next start -p 3009",
"predev": "npm run typegen",
"dev": "next dev -p 3009 --turbo",
"dev": "next dev -p 3009",
"lint": "next lint",
"start": "next start",
"typegen": "sanity typegen generate"
Expand Down
41 changes: 33 additions & 8 deletions packages/next-loader/src/client-components/live/SanityLive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export interface SanityLiveProps
tag: string
}

// @TODO these should be reusable utils in visual-editing-helpers

const isMaybePreviewIframe = () => window !== window.parent
const isMaybePreviewWindow = () => !!window.opener
const isMaybePresentation = () => isMaybePreviewIframe() || isMaybePreviewWindow()

/**
* @public
*/
Expand Down Expand Up @@ -207,21 +213,35 @@ export function SanityLive(props: SanityLiveProps): React.JSX.Element | null {
* 5. Notify what environment we're in, when in Draft Mode
*/
useEffect(() => {
if (draftModeEnabled && loadComlink) {
setEnvironment(opener ? 'presentation-window' : 'presentation-iframe')
} else if (draftModeEnabled && token) {
// If we might be in Presentation Tool, then skip detecting here as it's handled later
if (isMaybePresentation()) return

// If we're definitely not in Presentation Tool, then we can set the environment as stand-alone live preview
// if we have both a browser token, and draft mode is enabled
if (draftModeEnabled && token) {
setEnvironment('live')
} else {
setEnvironment('unknown')
return
}
// If we're in draft mode, but don't have a browser token, then we're in static mode
// which means that published content is still live, but draft changes likely need manual refresh
if (draftModeEnabled) {
setEnvironment('static')
return
}
}, [draftModeEnabled, loadComlink, token])

// Fallback to `unknown` otherwise, as we simply don't know how it's setup
setEnvironment('unknown')
return
}, [draftModeEnabled, token])

/**
* 6. If Presentation Tool is detected, load up the comlink and integrate with it
*/
useEffect(() => {
if (window === parent && !opener) return
if (!isMaybePresentation()) return
const controller = new AbortController()
// Wait for a while to see if Presentation Tool is detected, before assuming the env to be stand-alone live preview
const timeout = setTimeout(() => setEnvironment('live'), 3_000)
window.addEventListener(
'message',
({data}: MessageEvent<unknown>) => {
Expand All @@ -233,13 +253,18 @@ export function SanityLive(props: SanityLiveProps): React.JSX.Element | null {
'from' in data &&
data.from === 'presentation'
) {
clearTimeout(timeout)
setEnvironment(isMaybePreviewWindow() ? 'presentation-window' : 'presentation-iframe')
setLoadComlink(true)
controller.abort()
}
},
{signal: controller.signal},
)
return () => controller.abort()
return () => {
clearTimeout(timeout)
controller.abort()
}
}, [])

/**
Expand Down
1 change: 1 addition & 0 deletions packages/next-loader/src/hooks/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type DraftEnvironment =
| 'presentation-iframe'
| 'presentation-window'
| 'live'
| 'static'
| 'unknown'

/** @internal */
Expand Down
2 changes: 2 additions & 0 deletions packages/next-loader/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
export * from './useDraftMode'
export type {DraftPerspective, DraftEnvironment} from './context'
export type {ClientPerspective} from '@sanity/client'
export * from './useIsPresentationTool'
export * from './useIsLivePreview'
25 changes: 25 additions & 0 deletions packages/next-loader/src/hooks/useIsLivePreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {useDraftModeEnvironment} from './useDraftMode'

/**
* Detects if the application is considered to be in a "Live Preview" mode.
* Live Preview means that the application is either:
* - being previewed inside Sanity Presentation Tool
* - being previewed in Draft Mode, with a `browserToken` given to `defineLive`, also known as "Standalone Live Preview'"
* When in Live Preview mode, you typically want UI to update as new content comes in, without any manual intervention.
* This is very different from Live Production mode, where you usually want to delay updates that might cause layout shifts,
* to avoid interrupting the user that is consuming your content.
* This hook lets you adapt to this difference, making sure production doesn't cause layout shifts that worsen the UX,
* while in Live Preview mode layout shift is less of an issue and it's better for the editorial experience to auto refresh in real time.
*
* The hook returns `null` initially, to signal it doesn't yet know if it's live previewing or not.
* Then `true` if it is, and `false` otherwise.
* @public
*/
export function useIsLivePreview(): boolean | null {
const environment = useDraftModeEnvironment()
return environment === 'checking'
? null
: environment === 'presentation-iframe' ||
environment === 'presentation-window' ||
environment === 'live'
}
18 changes: 18 additions & 0 deletions packages/next-loader/src/hooks/useIsPresentationTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {useDraftModeEnvironment} from './useDraftMode'

/**
* Detects if the application is being previewed inside Sanity Presentation Tool.
* Presentation Tool can open the application in an iframe, or in a new window.
* When in this context there are some UI you usually don't want to show,
* for example a Draft Mode toggle, or a "Viewing draft content" indicators, these are unnecessary and add clutter to
* the editorial experience.
* The hook returns `null` initially, when it's not yet sure if the application is running inside Presentation Tool,
* then `true` if it is, and `false` otherwise.
* @public
*/
export function useIsPresentationTool(): boolean | null {
const environment = useDraftModeEnvironment()
return environment === 'checking'
? null
: environment === 'presentation-iframe' || environment === 'presentation-window'
}

0 comments on commit 6aeecd4

Please sign in to comment.