"use client"; import { Fact, useEntity, useReplicache } from "src/replicache"; import { memo, useEffect, useState } from "react"; import { useUIState } from "src/useUIState"; import { useBlockMouseHandlers } from "./useBlockMouseHandlers"; import { useBlockKeyboardHandlers } from "./useBlockKeyboardHandlers"; import { useLongPress } from "src/hooks/useLongPress"; import { focusBlock } from "src/utils/focusBlock"; import { useHandleDrop } from "./useHandleDrop"; import { useEntitySetContext } from "components/EntitySetProvider"; import { TextBlock } from "components/Blocks/TextBlock"; import { ImageBlock } from "./ImageBlock"; import { PageLinkBlock } from "./PageLinkBlock"; import { ExternalLinkBlock } from "./ExternalLinkBlock"; import { EmbedBlock } from "./EmbedBlock"; import { MailboxBlock } from "./MailboxBlock"; import { AreYouSure } from "./DeleteBlock"; import { useIsMobile } from "src/hooks/isMobile"; import { DateTimeBlock } from "./DateTimeBlock"; import { RSVPBlock } from "./RSVPBlock"; import { elementId } from "src/utils/elementId"; import { ButtonBlock } from "./ButtonBlock"; import { PollBlock } from "./PollBlock"; import { BlueskyPostBlock } from "./BlueskyPostBlock"; import { CheckboxChecked } from "components/Icons/CheckboxChecked"; import { CheckboxEmpty } from "components/Icons/CheckboxEmpty"; import { LockTiny } from "components/Icons/LockTiny"; import { MathBlock } from "./MathBlock"; import { CodeBlock } from "./CodeBlock"; import { HorizontalRule } from "./HorizontalRule"; import { deepEquals } from "src/utils/deepEquals"; import { isTextBlock } from "src/utils/isTextBlock"; export type Block = { factID: string; parent: string; position: string; value: string; type: Fact<"block/type">["data"]["value"]; listData?: { checklist?: boolean; path: { depth: number; entity: string }[]; parent: string; depth: number; }; }; export type BlockProps = { pageType: Fact<"page/type">["data"]["value"]; entityID: string; parent: string; position: string; nextBlock: Block | null; previousBlock: Block | null; nextPosition: string | null; } & Block; export const Block = memo(function Block( props: BlockProps & { preview?: boolean }, ) { // Block handles all block level events like // mouse events, keyboard events and longPress, and setting AreYouSure state // and shared styling like padding and flex for list layouting let mouseHandlers = useBlockMouseHandlers(props); let handleDrop = useHandleDrop({ parent: props.parent, position: props.position, nextPosition: props.nextPosition, }); let entity_set = useEntitySetContext(); let { isLongPress, handlers } = useLongPress(() => { if (isTextBlock[props.type]) return; if (isLongPress.current) { focusBlock( { type: props.type, value: props.entityID, parent: props.parent }, { type: "start" }, ); } }); let selected = useUIState( (s) => !!s.selectedBlocks.find((b) => b.value === props.entityID), ); let [areYouSure, setAreYouSure] = useState(false); useEffect(() => { if (!selected) { setAreYouSure(false); } }, [selected]); // THIS IS WHERE YOU SET WHETHER OR NOT AREYOUSURE IS TRIGGERED ON THE DELETE KEY useBlockKeyboardHandlers(props, areYouSure, setAreYouSure); return (
{ e.preventDefault(); e.stopPropagation(); } : undefined } onDrop={ !props.preview && entity_set.permissions.write ? handleDrop : undefined } className={` blockWrapper relative flex flex-row gap-2 px-3 sm:px-4 ${ !props.nextBlock ? "pb-3 sm:pb-4" : props.type === "heading" || (props.listData && props.nextBlock?.listData) ? "pb-0" : "pb-2" } ${props.type === "blockquote" && props.previousBlock?.type === "blockquote" ? (!props.listData ? "-mt-3" : "-mt-1") : ""} ${ !props.previousBlock ? props.type === "heading" || props.type === "text" ? "pt-2 sm:pt-3" : "pt-3 sm:pt-4" : "pt-1" }`} > {!props.preview && }
); }, deepEqualsBlockProps); function deepEqualsBlockProps( prevProps: BlockProps & { preview?: boolean }, nextProps: BlockProps & { preview?: boolean }, ): boolean { // Compare primitive fields if ( prevProps.pageType !== nextProps.pageType || prevProps.entityID !== nextProps.entityID || prevProps.parent !== nextProps.parent || prevProps.position !== nextProps.position || prevProps.factID !== nextProps.factID || prevProps.value !== nextProps.value || prevProps.type !== nextProps.type || prevProps.nextPosition !== nextProps.nextPosition || prevProps.preview !== nextProps.preview ) { return false; } // Compare listData if present if (prevProps.listData !== nextProps.listData) { if (!prevProps.listData || !nextProps.listData) { return false; // One is undefined, the other isn't } if ( prevProps.listData.checklist !== nextProps.listData.checklist || prevProps.listData.parent !== nextProps.listData.parent || prevProps.listData.depth !== nextProps.listData.depth ) { return false; } // Compare path array if (prevProps.listData.path.length !== nextProps.listData.path.length) { return false; } for (let i = 0; i < prevProps.listData.path.length; i++) { if ( prevProps.listData.path[i].depth !== nextProps.listData.path[i].depth || prevProps.listData.path[i].entity !== nextProps.listData.path[i].entity ) { return false; } } } // Compare nextBlock if (prevProps.nextBlock !== nextProps.nextBlock) { if (!prevProps.nextBlock || !nextProps.nextBlock) { return false; // One is null, the other isn't } if ( prevProps.nextBlock.factID !== nextProps.nextBlock.factID || prevProps.nextBlock.parent !== nextProps.nextBlock.parent || prevProps.nextBlock.position !== nextProps.nextBlock.position || prevProps.nextBlock.value !== nextProps.nextBlock.value || prevProps.nextBlock.type !== nextProps.nextBlock.type ) { return false; } // Compare nextBlock's listData (using deepEquals for simplicity) if ( !deepEquals(prevProps.nextBlock.listData, nextProps.nextBlock.listData) ) { return false; } } // Compare previousBlock if (prevProps.previousBlock !== nextProps.previousBlock) { if (!prevProps.previousBlock || !nextProps.previousBlock) { return false; // One is null, the other isn't } if ( prevProps.previousBlock.factID !== nextProps.previousBlock.factID || prevProps.previousBlock.parent !== nextProps.previousBlock.parent || prevProps.previousBlock.position !== nextProps.previousBlock.position || prevProps.previousBlock.value !== nextProps.previousBlock.value || prevProps.previousBlock.type !== nextProps.previousBlock.type ) { return false; } // Compare previousBlock's listData (using deepEquals for simplicity) if ( !deepEquals( prevProps.previousBlock.listData, nextProps.previousBlock.listData, ) ) { return false; } } return true; } export const BaseBlock = ( props: BlockProps & { preview?: boolean; areYouSure?: boolean; setAreYouSure?: (value: boolean) => void; }, ) => { // BaseBlock renders the actual block content, delete states, controls spacing between block and list markers let BlockTypeComponent = BlockTypeComponents[props.type]; let alignment = useEntity(props.value, "block/text-alignment")?.data.value; let alignmentStyle = props.type === "button" || props.type === "image" ? "justify-center" : "justify-start"; if (alignment) alignmentStyle = { left: "justify-start", right: "justify-end", center: "justify-center", justify: "justify-start", }[alignment]; if (!BlockTypeComponent) return
unknown block
; return (
{props.listData && } {props.areYouSure ? ( props.setAreYouSure && props.setAreYouSure(false) } type={props.type} entityID={props.entityID} /> ) : ( )}
); }; const BlockTypeComponents: { [K in Fact<"block/type">["data"]["value"]]: React.ComponentType< BlockProps & { preview?: boolean } >; } = { code: CodeBlock, math: MathBlock, card: PageLinkBlock, text: TextBlock, blockquote: TextBlock, heading: TextBlock, image: ImageBlock, link: ExternalLinkBlock, embed: EmbedBlock, mailbox: MailboxBlock, datetime: DateTimeBlock, rsvp: RSVPBlock, button: ButtonBlock, poll: PollBlock, "bluesky-post": BlueskyPostBlock, "horizontal-rule": HorizontalRule, }; export const BlockMultiselectIndicator = (props: BlockProps) => { let { rep } = useReplicache(); let isMobile = useIsMobile(); let first = props.previousBlock === null; let isMultiselected = useUIState( (s) => !!s.selectedBlocks.find((b) => b.value === props.entityID) && s.selectedBlocks.length > 1, ); let isSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.entityID), ); let isLocked = useEntity(props.value, "block/is-locked"); let nextBlockSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.nextBlock?.value), ); let prevBlockSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.previousBlock?.value), ); if (isMultiselected || (isLocked?.data.value && isSelected)) // not sure what multiselected and selected classes are doing (?) // use a hashed pattern for locked things. show this pattern if the block is selected, even if it isn't multiselected return ( <>
{isLocked?.data.value && (
)} ); }; export const ListMarker = ( props: Block & { previousBlock?: Block | null; nextBlock?: Block | null; } & { className?: string; }, ) => { let isMobile = useIsMobile(); let checklist = useEntity(props.value, "block/check-list"); let headingLevel = useEntity(props.value, "block/heading-level")?.data.value; let children = useEntity(props.value, "card/block"); let folded = useUIState((s) => s.foldedBlocks.includes(props.value)) && children.length > 0; let depth = props.listData?.depth; let { permissions } = useEntitySetContext(); let { rep } = useReplicache(); return (
{checklist && ( )}
); };