import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Platform } from 'react-native'; import { useForm, FormProvider } from 'react-hook-form'; import { View, Pressable } from 'app/design/view'; import Image, { POST_ENTRY_COVER_SIZES, POST_ENTRY_COVER_WIDTH_CAP } from 'app/ui/atoms/image'; import { getCoverAspectClass } from 'app/lib/image/upload-settings'; import Html from 'app/ui/atoms/html'; import { splitInlineMedia } from 'app/lib/editor/inline-video'; import { Text, H1C } from 'app/design/typography'; import { Input } from 'app/design/controls'; import { appSetting, clearLinks, getYouTubeVideoId, cn } from 'app/lib/util' import { useEditableRequest } from 'app/lib/form/form-helpers' import { ContentMore } from 'app/ui/molecules/content/content-more'; import EntityAttachments from './entity-attachments'; import TextMore from 'app/ui/molecules/content/text-more'; import Video from 'app/ui/atoms/video'; import Youtube from 'app/ui/molecules/content/youtube' import InlineVideo from 'app/ui/molecules/content/inline-video' import InlineEmbed from 'app/ui/molecules/content/inline-embed' import InlineTable from 'app/ui/molecules/content/inline-table' import { BlockWrapper } from 'app/components/block-wrapper' import RftText from 'app/components/form-fields/editor-rft-text' const isWeb = Platform.OS === 'web' function useClickOutside(enabled, ref, onOutside) { const onOutsideRef = useRef(onOutside) onOutsideRef.current = onOutside useEffect(() => { if (!enabled || !isWeb || typeof document === 'undefined') return const handle = (event) => { const node = ref.current if (!node) return const target = event.target if (node.contains?.(target)) return onOutsideRef.current?.() } // Skip the same click that opened edit mode const timer = setTimeout(() => { document.addEventListener('mousedown', handle) document.addEventListener('touchstart', handle) }, 0) return () => { clearTimeout(timer) document.removeEventListener('mousedown', handle) document.removeEventListener('touchstart', handle) } }, [enabled, ref]) } function EditableTitleField({ editable, requestUrl, fieldName = 'title', initialValue, inputClassName, children, }) { const { value, commit } = useEditableRequest({ initialValue: initialValue ?? '', requestUrl, fieldName, toParam: (v) => encodeURIComponent(String(v ?? '')), }) const [editing, setEditing] = useState(false) const draftRef = useRef(value ?? '') const wrapRef = useRef(null) const saveAndClose = useCallback(async () => { await commit(draftRef.current) setEditing(false) }, [commit]) useClickOutside(editing, wrapRef, saveAndClose) if (!editable) { return typeof children === 'function' ? children(initialValue) : children } if (editing) { return ( { draftRef.current = text }} onBlur={saveAndClose} onSubmitEditing={saveAndClose} className={cn( 'w-full border border-border/60 bg-input/40 web:focus:bg-input/70 rounded-lg px-3 py-2', inputClassName, )} /> ) } return ( { draftRef.current = value ?? '' setEditing(true) }} accessibilityRole="button" accessibilityLabel="Edit title" className="w-full web:cursor-pointer rounded-lg" > {typeof children === 'function' ? children(value) : children} ) } function EditableTextField({ editable, requestUrl, fieldName = 'text', initialValue, children, }) { const { value, commit } = useEditableRequest({ initialValue: initialValue ?? '', requestUrl, fieldName, toParam: (v) => encodeURIComponent(String(v ?? '')), }) const [editing, setEditing] = useState(false) const wrapRef = useRef(null) const methods = useForm({ defaultValues: { [fieldName]: value ?? '' }, }) const { isDirty } = methods.formState const saveAndClose = useCallback(async () => { if (isDirty) { await commit(methods.getValues(fieldName) ?? '') } setEditing(false) }, [commit, fieldName, isDirty, methods]) useClickOutside(editing, wrapRef, saveAndClose) useEffect(() => { if (editing) { methods.reset({ [fieldName]: value ?? '' }) } }, [editing, fieldName, methods, value]) if (!editable) { return typeof children === 'function' ? children(initialValue) : children } if (editing) { return ( ) } return ( setEditing(true)} accessibilityRole="button" accessibilityLabel="Edit text" className="w-full web:cursor-pointer rounded-lg" > {typeof children === 'function' ? children(value) : children} ) } export default function EntityTextBlock ({blockWrapperProps, data, block, showPad, sidebar, editable, afterTitle}) { const isEditable = !!(editable || data?.editable) const view = appSetting('entry', 'default_view'); switch (view) { case 'small': return ; default: return ; } } function Small({ data, editable, afterTitle }) { const requestUrl = data?.params?.request_url const text = clearLinks(data.entry_text); return ( {(title) => ( {title} )} {afterTitle} {(body) => ( )} ); } const getImagesData = (data) => { if (!data["bx_if:show_screenshots"] || !data["bx_if:show_screenshots"].condition) { return []; } const screenshots = data["bx_if:show_screenshots"].content.screenshots; const att = screenshots.map(screenshot => ({ type: 'image', data: { 'src': screenshot.url_bg } })); return att; }; /** Post body with editor videos, embeds (standalone link paragraphs) and tables rendered inline. */ function HtmlWithMedia({ html }) { // Split the raw HTML: clearLinks would turn UNA hrefs into relative ones. const segments = useMemo(() => splitInlineMedia(html), [html]) if (!segments.some((s) => s.type !== 'html')) return return segments.map((s, i) => { if (s.type === 'video') return if (s.type === 'embed') return if (s.type === 'table') return return }) } function Default({ data, showPad, sidebar, block, blockWrapperProps, editable, afterTitle }) { const att = getImagesData(data); const isSmall = block?.module == "bx_market"; const checked = _checkEmpty(data); const text = checked?.text ?? clearLinks(data.entry_text); const videoId = checked?.videoId ?? null; const requestUrl = data?.params?.request_url; if (!editable && !text && !data.video && !data.image && !videoId) return null return ( {(!!data.video?.src_mp4) && } {(videoId) && } {(!!data.image && !data.video) && ( {data.title} )} {(title) => ( isSmall ? : {title} )} {afterTitle} {(body) => { return isSmall ? : }} ); } const _checkEmpty = (data) => { if (!data) return false const text = clearLinks(data.entry_text); const videoId = data.video_embed && getYouTubeVideoId(data.video_embed) || null; if (!text && !data.video && !data.image && !videoId) return false return {text: text, videoId: videoId} } EntityTextBlock.checkEmpty = (item) => { if (item?.editable || item?.data?.editable) { return _checkEmpty(item.data) || true } return _checkEmpty(item.data) };