diff --git a/src/app/notes/[id]/page.tsx b/src/app/notes/[id]/page.tsx index 4a4ebb1857..6fa8d24d20 100644 --- a/src/app/notes/[id]/page.tsx +++ b/src/app/notes/[id]/page.tsx @@ -5,6 +5,7 @@ import { Balancer } from 'react-wrap-balancer' import clsx from 'clsx' import dayjs from 'dayjs' import { useParams } from 'next/navigation' +import type { Image } from '@mx-space/api-client' import type { MarkdownToJSX } from '~/components/ui/markdown' import { PageDataHolder } from '~/components/common/PageHolder' @@ -15,11 +16,14 @@ import { Toc, TocAutoScroll } from '~/components/widgets/toc' import { useBeforeMounted } from '~/hooks/common/use-before-mounted' import { useNoteByNidQuery } from '~/hooks/data/use-note' import { ArticleElementProvider } from '~/providers/article/article-element-provider' +import { MarkdownImageRecordProvider } from '~/providers/article/markdown-image-record-provider' import { useSetCurrentNoteId } from '~/providers/note/current-note-id-provider' import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider' import styles from './page.module.css' +const noopArr = [] as Image[] + const PageImpl = () => { const { id } = useParams() as { id: string } const { data } = useNoteByNidQuery(id) @@ -72,7 +76,9 @@ const PageImpl = () => { - + + + diff --git a/src/components/common/ProviderComposer.tsx b/src/components/common/ProviderComposer.tsx new file mode 100644 index 0000000000..43d1845627 --- /dev/null +++ b/src/components/common/ProviderComposer.tsx @@ -0,0 +1,11 @@ +'use client' + +import React from 'react' + +export const ProviderComposer: Component<{ + contexts: JSX.Element[] +}> = ({ contexts, children }) => { + return contexts.reduceRight((kids: any, parent: any) => { + return React.cloneElement(parent, { children: kids }) + }, children) +} diff --git a/src/components/ui/float-popover/FloatPopover.tsx b/src/components/ui/float-popover/FloatPopover.tsx index 85716bb4f9..63c2ca8ba8 100644 --- a/src/components/ui/float-popover/FloatPopover.tsx +++ b/src/components/ui/float-popover/FloatPopover.tsx @@ -235,7 +235,7 @@ export const FloatPopover: FC< aria-modal="true" className={clsxm( '!shadow-out-sm focus:!shadow-out-sm focus-visible:!shadow-out-sm', - 'rounded-xl border border-zinc-400/20 bg-base-100/80 p-4 shadow-lg outline-none backdrop-blur-lg dark:border-zinc-500/30', + 'bg-base-100/98 rounded-xl border border-zinc-400/20 p-4 shadow-lg outline-none backdrop-blur-lg dark:border-zinc-500/30', headless && styles['headless'], animate && styles['animate'], diff --git a/src/components/ui/image/ZoomedImage.tsx b/src/components/ui/image/ZoomedImage.tsx index 95b6dd99ec..76382b336e 100644 --- a/src/components/ui/image/ZoomedImage.tsx +++ b/src/components/ui/image/ZoomedImage.tsx @@ -1,11 +1,16 @@ 'use client' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' +import { motion, useAnimationControls } from 'framer-motion' import { tv } from 'tailwind-variants' -import type { ReactNode } from 'react' +import type { FC, ReactNode } from 'react' import { LazyLoad } from '~/components/common/Lazyload' import { useIsUnMounted } from '~/hooks/common/use-is-unmounted' +import { calculateDimensions } from '~/lib/calc-image' +import { useArticleElementSize } from '~/providers/article/article-element-provider' +import { useMarkdownImageRecord } from '~/providers/article/markdown-image-record-provider' +import { clsxm } from '~/utils/helper' import { Divider } from '../divider' @@ -28,7 +33,7 @@ export enum ImageLoadStatus { } const styles = tv({ - base: '', + base: 'rounded-xl overflow-hidden text-center inline-flex items-center justify-center duration-200', variants: { status: { loading: 'hidden opacity-0', @@ -37,11 +42,12 @@ const styles = tv({ }, }, }) + export const ImageLazy: Component = ({ alt, src, title, - accent, + placeholder, }) => { const figcaption = title || alt @@ -57,33 +63,117 @@ export const ImageLazy: Component = ({ }, [isUnmount], ) + + const controls = useAnimationControls() + return (
- {alt} setImageLoadStatusSafe(ImageLoadStatus.Loaded)} - onError={() => setImageLoadStatusSafe(ImageLoadStatus.Error)} - className={styles({ - status: imageLoadStatus, - })} - /> -
- - {figcaption} -
+ + + {imageLoadStatus !== ImageLoadStatus.Loaded && placeholder} + + { + setImageLoadStatusSafe(ImageLoadStatus.Loaded) + requestAnimationFrame(() => { + controls.start('loaded') + }) + }} + onError={() => setImageLoadStatusSafe(ImageLoadStatus.Error)} + className={styles({ + status: imageLoadStatus, + })} + /> + + + {!!figcaption && ( +
+ + {figcaption} +
+ )}
) } export const ZoomedImage: Component = (props) => { - console.log(props) return ( ) } + +export const FixedZoomedImage: Component = (props) => { + const placeholder = useMemo(() => { + return + }, [props.src]) + return +} + +const Placeholder: FC<{ + src: string +}> = ({ src }) => { + const { h, w } = useArticleElementSize() + const imageMeta = useMarkdownImageRecord(src) + + const scaledSize = useMemo(() => { + if (!h || !w) return + if (!imageMeta) return + const { height, width } = imageMeta + const { height: scaleHeight, width: scaleWidth } = calculateDimensions( + width, + height, + { + width: w, + height: Infinity, + }, + ) + + return { + scaleHeight, + scaleWidth, + } + }, [h, w, imageMeta]) + + if (!scaledSize) return + if (h === 0 || w === 0) return + return ( + + ) +} + +const NoFixedPlaceholder = () => { + return ( + + ) +} diff --git a/src/components/ui/markdown/Markdown.tsx b/src/components/ui/markdown/Markdown.tsx index 9167eabdfd..bb8c6a06ab 100644 --- a/src/components/ui/markdown/Markdown.tsx +++ b/src/components/ui/markdown/Markdown.tsx @@ -5,6 +5,7 @@ import { compiler } from 'markdown-to-jsx' import type { MarkdownToJSX } from 'markdown-to-jsx' import type { FC, PropsWithChildren } from 'react' +import { FixedZoomedImage } from '../image' import styles from './index.module.css' import { CommentAtRule } from './parsers/comment-at' import { ContainerRule } from './parsers/container' @@ -16,7 +17,6 @@ import { SpoilderRule } from './parsers/spoiler' import { MParagraph, MTableBody, MTableHead, MTableRow } from './renderers' import { MDetails } from './renderers/collapse' import { MFootNote } from './renderers/footnotes' -import { ZoomedImage } from '../image' export interface MdProps { value?: string @@ -66,7 +66,7 @@ export const Markdown: FC = // FIXME: footer tag in raw html will renders not as expected, but footer tag in this markdown lib will wrapper as linkReferer footnotes footer: MFootNote, details: MDetails, - img: ZoomedImage, + img: FixedZoomedImage, // for custom react component // LinkCard, diff --git a/src/lib/fonts.ts b/src/lib/fonts.ts index e00690eaa4..633390dfb3 100644 --- a/src/lib/fonts.ts +++ b/src/lib/fonts.ts @@ -6,6 +6,7 @@ const sansFont = Manrope({ variable: '--font-sans', display: 'swap', }) + const serifFont = Noto_Serif_SC({ subsets: ['latin'], weight: ['400'], diff --git a/src/providers/article/article-element-provider.tsx b/src/providers/article/article-element-provider.tsx index a0e5a3f588..567d1c50c7 100644 --- a/src/providers/article/article-element-provider.tsx +++ b/src/providers/article/article-element-provider.tsx @@ -2,6 +2,7 @@ import { memo, useEffect, useRef } from 'react' import { createContextState } from 'foxact/create-context-state' import { useIsomorphicLayoutEffect } from 'foxact/use-isomorphic-layout-effect' +import { ProviderComposer } from '~/components/common/ProviderComposer' import { clsxm } from '~/utils/helper' const [ @@ -25,16 +26,17 @@ const [ useSetIsEOArticleElement, ] = createContextState(false) +const Providers = [ + , + , + , +] const ArticleElementProvider: Component = ({ children, className }) => { return ( - - - - - {children} - - - + + + {children} + ) } const ArticleElementResizeObserver = () => { diff --git a/src/providers/article/markdown-image-record-provider.tsx b/src/providers/article/markdown-image-record-provider.tsx new file mode 100644 index 0000000000..7593bd4564 --- /dev/null +++ b/src/providers/article/markdown-image-record-provider.tsx @@ -0,0 +1,44 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, +} from 'react' +import { atom, useAtomValue } from 'jotai' +import { selectAtom } from 'jotai/utils' +import type { Image } from '@mx-space/api-client' + +import { jotaiStore } from '~/lib/store' + +const MarkdownImageRecordProviderInternal = createContext(atom([] as Image[])) + +export const MarkdownImageRecordProvider: Component<{ + images: Image[] +}> = ({ children, images }) => { + const atomRef = useRef(atom([...images] as Image[])).current + + useEffect(() => { + jotaiStore.set(atomRef, [...images]) + }, [images]) + + return ( + + {children} + + ) +} + +export const useMarkdownImageRecord = (src: string) => { + return useAtomValue( + selectAtom( + useContext(MarkdownImageRecordProviderInternal), + useCallback( + (value: Image[]) => { + return value.find((image) => image.src === src) + }, + [src], + ), + ), + ) +} diff --git a/src/providers/root/index.tsx b/src/providers/root/index.tsx index 98341e94f5..a2d7e6dc12 100644 --- a/src/providers/root/index.tsx +++ b/src/providers/root/index.tsx @@ -1,10 +1,10 @@ 'use client' import { ReactQueryProvider } from './react-query-provider' -import React from 'react' import { ThemeProvider } from 'next-themes' import type { PropsWithChildren } from 'react' +import { ProviderComposer } from '../../components/common/ProviderComposer' import { AggregationProvider } from './aggregation-data-provider' import { DebugProvider } from './debug-provider' import { JotaiStoreProvider } from './jotai-provider' @@ -12,14 +12,6 @@ import { PageScrollInfoProvider } from './page-scroll-info-provider' import { SocketProvider } from './socket-provider' import { ViewportProvider } from './viewport-provider' -const ProviderComposer: Component<{ - contexts: JSX.Element[] -}> = ({ contexts, children }) => { - return contexts.reduceRight((kids: any, parent: any) => { - return React.cloneElement(parent, { children: kids }) - }, children) -} - const contexts: JSX.Element[] = [ , ,