diff --git a/src/atoms/hooks/viewport.ts b/src/atoms/hooks/viewport.ts index 9406dfe3d0..f4c919aeea 100644 --- a/src/atoms/hooks/viewport.ts +++ b/src/atoms/hooks/viewport.ts @@ -3,6 +3,8 @@ import { useAtomValue } from 'jotai' import { selectAtom } from 'jotai/utils' import type { ExtractAtomValue } from 'jotai' +import { jotaiStore } from '~/lib/store' + import { viewportAtom } from '../viewport' export const useViewport = ( @@ -17,9 +19,12 @@ export const useViewport = ( export const useIsMobile = () => useViewport( - useCallback( - (v: ExtractAtomValue) => - (v.sm || v.md || !v.sm) && !v.lg, - [], - ), + useCallback((v: ExtractAtomValue) => isMobile(v), []), ) + +const isMobile = (v: ExtractAtomValue) => + (v.sm || v.md || !v.sm) && !v.lg +export const currentIsMobile = () => { + const v = jotaiStore.get(viewportAtom) + return isMobile(v) +} diff --git a/src/components/layout/footer/GatewayInfo.tsx b/src/components/layout/footer/GatewayInfo.tsx index b493ec1497..df29ceccce 100644 --- a/src/components/layout/footer/GatewayInfo.tsx +++ b/src/components/layout/footer/GatewayInfo.tsx @@ -95,6 +95,7 @@ export const GatewayInfo = () => { diff --git a/src/components/ui/image/ZoomedImage.tsx b/src/components/ui/image/ZoomedImage.tsx index 10baaac0d3..3f77b7f434 100644 --- a/src/components/ui/image/ZoomedImage.tsx +++ b/src/components/ui/image/ZoomedImage.tsx @@ -1,14 +1,21 @@ 'use client' -import { forwardRef, useCallback, useMemo, useRef, useState } from 'react' +import { forwardRef, memo, useCallback, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { useIsomorphicLayoutEffect } from 'foxact/use-isomorphic-layout-effect' import mediumZoom from 'medium-zoom' import Image from 'next/image' import { tv } from 'tailwind-variants' import type { Zoom } from 'medium-zoom' -import type { FC, ReactNode } from 'react' +import type { + AnimationEventHandler, + DetailedHTMLProps, + FC, + ImgHTMLAttributes, + ReactNode, +} from 'react' +import { useIsMobile } from '~/atoms/hooks' import { LazyLoad } from '~/components/common/Lazyload' import { useIsUnMounted } from '~/hooks/common/use-is-unmounted' import { isDev, isServerSide } from '~/lib/env' @@ -67,7 +74,7 @@ export const ImageLazy: Component = ({ const [zoomer_] = useState(() => { if (isServerSide) return null! if (zoomer) return zoomer - const zoom = mediumZoom(undefined) + const zoom = mediumZoom(undefined, {}) zoomer = zoom return zoom }) @@ -86,6 +93,7 @@ export const ImageLazy: Component = ({ [isUnmount], ) const imageRef = useRef(null) + const isMobile = useIsMobile() useIsomorphicLayoutEffect(() => { if (imageLoadStatus !== ImageLoadStatus.Loaded) { return @@ -95,6 +103,18 @@ export const ImageLazy: Component = ({ } const $image = imageRef.current + if (!$image) return + if (isMobile) { + $image.onclick = () => { + // NOTE: document 上的 click 可以用 stopImmediatePropagation 阻止 + // e.stopImmediatePropagation() + window.open(src) + } + return () => { + $image.onclick = null + } + } + if ($image) { zoomer_.attach($image) @@ -102,8 +122,31 @@ export const ImageLazy: Component = ({ zoomer_.detach($image) } } - }, [zoom, zoomer_, imageLoadStatus]) + }, [zoom, zoomer_, imageLoadStatus, isMobile]) + const handleOnLoad = useCallback(() => { + setImageLoadStatusSafe(ImageLoadStatus.Loaded) + }, [setImageLoadStatusSafe]) + const handleError = useCallback( + () => setImageLoadStatusSafe(ImageLoadStatus.Error), + [setImageLoadStatusSafe], + ) + const handleOnAnimationEnd: AnimationEventHandler = + useCallback((e) => { + if (ImageLoadStatus.Loaded) { + ;(e.target as HTMLElement).classList.remove( + imageStyles[ImageLoadStatus.Loaded], + ) + } + }, []) + const imageClassName = useMemo( + () => + styles({ + status: imageLoadStatus, + className: clsx(imageStyles[ImageLoadStatus.Loaded], className), + }), + [className, imageLoadStatus], + ) return (
@@ -130,21 +173,10 @@ export const ImageLazy: Component = ({ title={title} alt={alt || title || ''} ref={imageRef} - onLoad={() => { - setImageLoadStatusSafe(ImageLoadStatus.Loaded) - }} - onError={() => setImageLoadStatusSafe(ImageLoadStatus.Error)} - className={styles({ - status: imageLoadStatus, - className: clsx(imageStyles[ImageLoadStatus.Loaded], className), - })} - onAnimationEnd={(e: Event) => { - if (ImageLoadStatus.Loaded) { - ;(e.target as HTMLElement).classList.remove( - imageStyles[ImageLoadStatus.Loaded], - ) - } - }} + onLoad={handleOnLoad} + onError={handleError} + className={imageClassName} + onAnimationEnd={handleOnAnimationEnd} /> @@ -256,21 +288,41 @@ const NoFixedPlaceholder = ({ accent }: { accent?: string }) => { ) } -const OptimizedImage: FC = forwardRef(({ src, alt, ...rest }, ref) => { - const { height, width } = useMarkdownImageRecord(src!) || rest - if (!height || !width) return {alt} - return ( - {alt - ) -}) +const OptimizedImage = memo( + forwardRef< + HTMLImageElement, + DetailedHTMLProps, HTMLImageElement> + >(({ src, alt, ...rest }, ref) => { + const { height, width } = useMarkdownImageRecord(src!) || rest + const hasDim = !!(height && width) + + const ImageEl = ( + {alt} + ) + return ( + <> + {hasDim ? ( + <> + {/* @ts-expect-error */} + {alt +
+ {ImageEl} +
+ + ) : ( + ImageEl + )} + + ) + }), +) OptimizedImage.displayName = 'OptimizedImage' diff --git a/src/components/ui/markdown/renderers/heading.tsx b/src/components/ui/markdown/renderers/heading.tsx index 9fa3feb110..0fb49fadbe 100644 --- a/src/components/ui/markdown/renderers/heading.tsx +++ b/src/components/ui/markdown/renderers/heading.tsx @@ -1,6 +1,7 @@ import { createElement, useId } from 'react' import type { DOMAttributes } from 'react' +import { useIsClient } from '~/hooks/common/use-is-client' import { springScrollToElement } from '~/lib/scroller' interface HeadingProps { @@ -15,6 +16,8 @@ export const MHeader = (props: HeadingProps) => { const rid = useId() + const isClient = useIsClient() + const nextId = `${rid}${id}` return createElement, HTMLHeadingElement>( `h${level}`, @@ -27,19 +30,21 @@ export const MHeader = (props: HeadingProps) => { null, <> {children} - { - const state = history.state - history.replaceState(state, '', `#${nextId}`) - springScrollToElement(document.getElementById(nextId)!, -100) - }} - > - - + {isClient && ( + { + const state = history.state + history.replaceState(state, '', `#${nextId}`) + springScrollToElement(document.getElementById(nextId)!, -100) + }} + > + + + )} , ) } diff --git a/src/components/ui/rich-link/Favicon.tsx b/src/components/ui/rich-link/Favicon.tsx index 678b3b062d..67bb517c13 100644 --- a/src/components/ui/rich-link/Favicon.tsx +++ b/src/components/ui/rich-link/Favicon.tsx @@ -112,7 +112,7 @@ export const Favicon: Component = (props) => { return (