diff --git a/src/components/EmojiPicker/index.tsx b/src/components/EmojiPicker/index.tsx index 613e562b..43df4b15 100644 --- a/src/components/EmojiPicker/index.tsx +++ b/src/components/EmojiPicker/index.tsx @@ -1,4 +1,5 @@ import { EMOJI_PICKER_DATA_SOURCE } from '@/lib/emoji-picker-data-source' +import { preloadEmojiPickerModule } from '@/lib/emoji-picker-preload' import { DEFAULT_LIKE_REACTION_CONTENT, DEFAULT_LIKE_REACTION_DISPLAY_EMOJI, DEFAULT_SUGGESTED_EMOJIS } from '@/lib/like-reaction-emojis' import { recordEmojiUsed } from '@/lib/recently-used-emojis' import { useNostr } from '@/providers/NostrProvider' @@ -25,6 +26,7 @@ export default function EmojiPicker({ reactionsDefaultOpen ? 'reactions' : 'full' ) const [customEmojiTick, setCustomEmojiTick] = useState(0) + const [pickerReady, setPickerReady] = useState(false) const containerRef = useRef(null) const pickerRef = useRef<(HTMLElement & { customEmoji: unknown[] }) | null>(null) @@ -44,8 +46,9 @@ export default function EmojiPicker({ if (mode !== 'full') return let cancelled = false + setPickerReady(false) - import('emoji-picker-element').then(({ Picker }) => { + preloadEmojiPickerModule().then(({ Picker }) => { if (cancelled || !containerRef.current) return const picker = new Picker({ @@ -110,10 +113,12 @@ export default function EmojiPicker({ picker.addEventListener('emoji-click', handleClick) containerRef.current.appendChild(picker) + if (!cancelled) setPickerReady(true) }) return () => { cancelled = true + setPickerReady(false) if (pickerRef.current) { pickerRef.current.remove() pickerRef.current = null @@ -196,8 +201,14 @@ export default function EmojiPicker({ {ownEmojisRow}
+ className="relative h-[min(320px,45dvh)] min-h-[240px] w-full min-w-[280px] max-w-[350px] shrink-0" + > + {!pickerReady ? ( +
+ … +
+ ) : null} +
) } diff --git a/src/components/EmojiPickerDialog/index.tsx b/src/components/EmojiPickerDialog/index.tsx index 184b2e4b..d95d4d3a 100644 --- a/src/components/EmojiPickerDialog/index.tsx +++ b/src/components/EmojiPickerDialog/index.tsx @@ -4,9 +4,10 @@ import { DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { preloadEmojiPicker } from '@/lib/emoji-picker-preload' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { TEmoji } from '@/types' -import { useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import EmojiPicker from '../EmojiPicker' export default function EmojiPickerDialog({ @@ -21,15 +22,35 @@ export default function EmojiPickerDialog({ }) { const { isSmallScreen } = useScreenSize() const [open, setOpen] = useState(false) + /** Keep picker mounted after first open so emoji-picker-element is not cold-started every time. */ + const [pickerMounted, setPickerMounted] = useState(false) + + useEffect(() => { + if (open) setPickerMounted(true) + }, [open]) + + useEffect(() => { + void preloadEmojiPicker() + }, []) + + const handleOpenChange = useCallback((next: boolean) => { + setOpen(next) + }, []) if (isSmallScreen) { return ( - + {children} { const t = e.target as HTMLElement | null if (t?.closest?.('[data-vaul-overlay]')) return @@ -39,8 +60,8 @@ export default function EmojiPickerDialog({ Emoji Picker -
- {open ? ( +
+ {pickerMounted ? ( { e.stopPropagation() @@ -56,7 +77,7 @@ export default function EmojiPickerDialog({ } return ( - + {children} {children} + return ( +
+ {children} +
+ ) +} + export default function PostContent({ open, defaultContent = '', @@ -2539,11 +2555,10 @@ export default function PostContent({
+ {/* Dynamic Title based on mode */}
{(() => { @@ -3378,8 +3393,13 @@ export default function PostContent({
)} - -
+ + +
diff --git a/src/components/PostEditor/PostTextarea/index.tsx b/src/components/PostEditor/PostTextarea/index.tsx index e2082505..6674000c 100644 --- a/src/components/PostEditor/PostTextarea/index.tsx +++ b/src/components/PostEditor/PostTextarea/index.tsx @@ -180,7 +180,7 @@ const PostTextarea = forwardRef< () => cn( 'border rounded-lg p-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring', - isSmallScreen && 'min-h-0 flex-1 overflow-y-auto overscroll-y-contain', + isSmallScreen && 'h-full min-h-0 flex-1 overflow-y-auto overscroll-y-contain', className ), [className, isSmallScreen] diff --git a/src/components/PostEditor/index.tsx b/src/components/PostEditor/index.tsx index 42e9650f..1de38d29 100644 --- a/src/components/PostEditor/index.tsx +++ b/src/components/PostEditor/index.tsx @@ -15,6 +15,7 @@ import { } from '@/components/ui/sheet' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { pubkeyToNpub } from '@/lib/pubkey' +import { preloadEmojiPicker } from '@/lib/emoji-picker-preload' import postEditor from '@/services/post-editor.service' import { Event } from 'nostr-tools' import postEditorService from '@/services/post-editor.service' @@ -67,6 +68,7 @@ export default function PostEditor({ useEffect(() => { if (!open) return postEditorService.setComposerShellOpen(true) + void preloadEmojiPicker() return () => postEditorService.setComposerShellOpen(false) }, [open]) diff --git a/src/index.css b/src/index.css index b6978a9e..03bfd908 100644 --- a/src/index.css +++ b/src/index.css @@ -103,11 +103,17 @@ } @media (max-width: 768px) { - /* Mobile composer: scroll inside the bordered editor instead of painting past it. */ + /* Mobile composer: fill the editor slot and scroll inside the bordered surface. */ .tiptap.flex-col { + display: flex; + flex-direction: column; + flex: 1 1 0%; + height: 100%; min-height: 0; } .tiptap.flex-col .ProseMirror { + flex: 1 1 0%; + height: 100%; min-height: 0; max-height: 100%; overflow-y: auto; diff --git a/src/lib/emoji-picker-preload.ts b/src/lib/emoji-picker-preload.ts new file mode 100644 index 00000000..cc6270d9 --- /dev/null +++ b/src/lib/emoji-picker-preload.ts @@ -0,0 +1,24 @@ +import { EMOJI_PICKER_DATA_SOURCE } from '@/lib/emoji-picker-data-source' + +let modulePromise: Promise | null = null +let dataPromise: Promise | null = null + +/** Warm the emoji-picker-element chunk while the composer is open. */ +export function preloadEmojiPickerModule() { + if (!modulePromise) { + modulePromise = import('emoji-picker-element') + } + return modulePromise +} + +/** Prime the bundled emoji database so the web component's fetch hits cache. */ +export function preloadEmojiPickerData() { + if (!dataPromise) { + dataPromise = fetch(EMOJI_PICKER_DATA_SOURCE).then((r) => r.json()) + } + return dataPromise +} + +export function preloadEmojiPicker() { + return Promise.all([preloadEmojiPickerModule(), preloadEmojiPickerData()]) +}