import { useEntity, useReplicache } from "src/replicache"; import { useEntitySetContext } from "./EntitySetProvider"; import { v7 } from "uuid"; import { BaseBlock } from "./Blocks/Block"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useDrag } from "src/hooks/useDrag"; import { useLongPress } from "src/hooks/useLongPress"; import { focusBlock } from "src/utils/focusBlock"; import { elementId } from "src/utils/elementId"; import { useUIState } from "src/useUIState"; import useMeasure from "react-use-measure"; import { useIsMobile } from "src/hooks/isMobile"; import { Media } from "./Media"; import { TooltipButton } from "./Buttons"; import { useBlockKeyboardHandlers } from "./Blocks/useBlockKeyboardHandlers"; import { AddSmall } from "./Icons/AddSmall"; import { InfoSmall } from "./Icons/InfoSmall"; import { Popover } from "./Popover"; import { Separator } from "./Layout"; import { CommentTiny } from "./Icons/CommentTiny"; import { QuoteTiny } from "./Icons/QuoteTiny"; import { AddTags, PublicationMetadata } from "./Pages/PublicationMetadata"; import { useLeafletPublicationData } from "./PageSWRDataProvider"; import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; import { useBlockMouseHandlers } from "./Blocks/useBlockMouseHandlers"; import { RecommendTinyEmpty } from "./Icons/RecommendTiny"; import { useSubscribe } from "src/replicache/useSubscribe"; import { mergePreferences } from "src/utils/mergePreferences"; export function Canvas(props: { entityID: string; preview?: boolean; first?: boolean; }) { let entity_set = useEntitySetContext(); let ref = useRef(null); useEffect(() => { let abort = new AbortController(); let isTouch = false; let startX: number, startY: number, scrollLeft: number, scrollTop: number; let el = ref.current; ref.current?.addEventListener( "wheel", (e) => { if (!el) return; if ( (e.deltaX > 0 && el.scrollLeft >= el.scrollWidth - el.clientWidth) || (e.deltaX < 0 && el.scrollLeft <= 0) || (e.deltaY > 0 && el.scrollTop >= el.scrollHeight - el.clientHeight) || (e.deltaY < 0 && el.scrollTop <= 0) ) { return; } e.preventDefault(); el.scrollLeft += e.deltaX; el.scrollTop += e.deltaY; }, { passive: false, signal: abort.signal }, ); return () => abort.abort(); }); return (
); } export function CanvasContent(props: { entityID: string; preview?: boolean }) { let blocks = useEntity(props.entityID, "canvas/block"); let { rep } = useReplicache(); let entity_set = useEntitySetContext(); let height = Math.max(...blocks.map((f) => f.data.position.y), 0); let handleDrop = useHandleCanvasDrop(props.entityID); return (
{ if (e.currentTarget !== e.target) return; useUIState.setState(() => ({ selectedBlocks: [], focusedEntity: { entityType: "page", entityID: props.entityID }, })); useUIState.setState({ focusedEntity: { entityType: "page", entityID: props.entityID }, }); document .getElementById(elementId.page(props.entityID).container) ?.scrollIntoView({ behavior: "smooth", inline: "nearest", }); if (e.detail === 2 || e.ctrlKey || e.metaKey) { let parentRect = e.currentTarget.getBoundingClientRect(); let newEntityID = v7(); await rep?.mutate.addCanvasBlock({ newEntityID, parent: props.entityID, position: { x: Math.max(e.clientX - parentRect.left, 0), y: Math.max(e.clientY - parentRect.top - 12, 0), }, factID: v7(), type: "text", permission_set: entity_set.set, }); focusBlock( { type: "text", parent: props.entityID, value: newEntityID }, { type: "start" }, ); } }} onDragOver={ !props.preview && entity_set.permissions.write ? (e) => { e.preventDefault(); e.stopPropagation(); } : undefined } onDrop={ !props.preview && entity_set.permissions.write ? handleDrop : undefined } style={{ minHeight: height + 512, contain: "size layout paint", }} className="relative h-full w-[1272px]" > {blocks .sort((a, b) => { if (a.data.position.y === b.data.position.y) { return a.data.position.x - b.data.position.x; } return a.data.position.y - b.data.position.y; }) .map((b) => { return ( ); })}
); } const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { let { data: pub, normalizedPublication } = useLeafletPublicationData(); let { rep } = useReplicache(); let postPreferences = useSubscribe(rep, (tx) => tx.get<{ showComments?: boolean; showMentions?: boolean; showRecommends?: boolean; } | null>("post_preferences"), ); if (!pub || !pub.publications) return null; if (!normalizedPublication) return null; let merged = mergePreferences( postPreferences || undefined, normalizedPublication.preferences, ); let showComments = merged.showComments !== false; let showMentions = merged.showMentions !== false; let showRecommends = merged.showRecommends !== false; return (
{showRecommends && (
)} {showComments && (
)} {showMentions && (
)} {showMentions !== false || showComments !== false || showRecommends === false ? ( ) : null} {!props.isSubpage && ( <> } > )}
); }; const AddCanvasBlockButton = (props: { entityID: string; entity_set: { set: string }; }) => { let { rep } = useReplicache(); let { permissions } = useEntitySetContext(); let blocks = useEntity(props.entityID, "canvas/block"); if (!permissions.write) return null; return (
Add a Block!
or double click anywhere
} className="w-fit p-2 rounded-full bg-accent-1 border-2 outline-solid outline-transparent hover:outline-1 hover:outline-accent-1 border-accent-1 text-accent-2" onMouseDown={() => { let page = document.getElementById( elementId.page(props.entityID).canvasScrollArea, ); if (!page) return; let newEntityID = v7(); rep?.mutate.addCanvasBlock({ newEntityID, parent: props.entityID, position: { x: page?.clientWidth + page?.scrollLeft - 468, y: 32 + page.scrollTop, }, factID: v7(), type: "text", permission_set: props.entity_set.set, }); setTimeout(() => { focusBlock( { type: "text", value: newEntityID, parent: props.entityID }, { type: "start" }, ); }, 20); }} > ); }; function CanvasBlock(props: { preview?: boolean; entityID: string; parent: string; position: { x: number; y: number }; factID: string; }) { let width = useEntity(props.entityID, "canvas/block/width")?.data.value || 360; let rotation = useEntity(props.entityID, "canvas/block/rotation")?.data.value || 0; let [ref, rect] = useMeasure(); let type = useEntity(props.entityID, "block/type"); let { rep } = useReplicache(); let isMobile = useIsMobile(); let { permissions } = useEntitySetContext(); let onDragEnd = useCallback( (dragPosition: { x: number; y: number }) => { if (!permissions.write) return; rep?.mutate.assertFact({ id: props.factID, entity: props.parent, attribute: "canvas/block", data: { type: "spatial-reference", value: props.entityID, position: { x: props.position.x + dragPosition.x, y: props.position.y + dragPosition.y, }, }, }); }, [props, rep, permissions], ); let { dragDelta, handlers: dragHandlers } = useDrag({ onDragEnd, }); let widthOnDragEnd = useCallback( (dragPosition: { x: number; y: number }) => { rep?.mutate.assertFact({ entity: props.entityID, attribute: "canvas/block/width", data: { type: "number", value: width + dragPosition.x, }, }); }, [props, rep, width], ); let widthHandle = useDrag({ onDragEnd: widthOnDragEnd }); let RotateOnDragEnd = useCallback( (dragDelta: { x: number; y: number }) => { let originX = rect.x + rect.width / 2; let originY = rect.y + rect.height / 2; let angle = find_angle( { x: rect.x + rect.width, y: rect.y + rect.height }, { x: originX, y: originY }, { x: rect.x + rect.width + dragDelta.x, y: rect.y + rect.height + dragDelta.y, }, ) * (180 / Math.PI); rep?.mutate.assertFact({ entity: props.entityID, attribute: "canvas/block/rotation", data: { type: "number", value: (rotation + angle) % 360, }, }); }, [props, rep, rect, rotation], ); let rotateHandle = useDrag({ onDragEnd: RotateOnDragEnd }); let { isLongPress, longPressHandlers: longPressHandlers } = useLongPress( () => { if (isLongPress.current && permissions.write) { focusBlock( { type: type?.data.value || "text", value: props.entityID, parent: props.parent, }, { type: "start" }, ); } }, ); let angle = 0; if (rotateHandle.dragDelta) { let originX = rect.x + rect.width / 2; let originY = rect.y + rect.height / 2; angle = find_angle( { x: rect.x + rect.width, y: rect.y + rect.height }, { x: originX, y: originY }, { x: rect.x + rect.width + rotateHandle.dragDelta.x, y: rect.y + rect.height + rotateHandle.dragDelta.y, }, ) * (180 / Math.PI); } let x = props.position.x + (dragDelta?.x || 0); let y = props.position.y + (dragDelta?.y || 0); let transform = `translate(${x}px, ${y}px) rotate(${rotation + angle}deg) scale(${!dragDelta ? "1.0" : "1.02"})`; let [areYouSure, setAreYouSure] = useState(false); let blockProps = useMemo(() => { return { pageType: "canvas" as const, preview: props.preview, type: type?.data.value || "text", value: props.entityID, factID: props.factID, position: "", nextPosition: "", entityID: props.entityID, parent: props.parent, nextBlock: null, previousBlock: null, }; }, [props, type?.data.value]); useBlockKeyboardHandlers(blockProps, areYouSure, setAreYouSure); let mouseHandlers = useBlockMouseHandlers(blockProps); let isList = useEntity(props.entityID, "block/is-list"); let isFocused = useUIState( (s) => s.focusedEntity?.entityID === props.entityID, ); return (
{!props.preview && permissions.write && ( )}
{!props.preview && permissions.write && (
)} {!props.preview && permissions.write && (
)}
); } export const CanvasBackground = (props: { entityID: string }) => { let cardBackgroundImage = useEntity( props.entityID, "theme/card-background-image", ); let cardBackgroundImageRepeat = useEntity( props.entityID, "theme/card-background-image-repeat", ); let cardBackgroundImageOpacity = useEntity(props.entityID, "theme/card-background-image-opacity")?.data .value || 1; let canvasPattern = useEntity(props.entityID, "canvas/background-pattern")?.data.value || "grid"; return (
); }; export const CanvasBackgroundPattern = (props: { pattern: "grid" | "dot" | "plain"; scale?: number; }) => { if (props.pattern === "plain") return null; let patternID = `canvasPattern-${props.pattern}-${props.scale}`; if (props.pattern === "grid") return ( ); if (props.pattern === "dot") { return ( ); } }; const Gripper = (props: { onMouseDown: (e: React.MouseEvent) => void; isFocused: boolean; }) => { return (
{/* the gripper is two svg's stacked on top of each other. One for the actual gripper, the other is an outline to endure the gripper stays visible on image backgrounds */}
); }; type P = { x: number; y: number }; function find_angle(P2: P, P1: P, P3: P) { if (P1.x === P3.x && P1.y === P3.y) return 0; let a = Math.atan2(P3.y - P1.y, P3.x - P1.x); let b = Math.atan2(P2.y - P1.y, P2.x - P1.x); return a - b; }