a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

properly render text in card blocks

+104 -60
+3 -52
components/Blocks.tsx
··· 6 6 useEditorStates, 7 7 } from "components/TextBlock"; 8 8 import { generateKeyBetween } from "fractional-indexing"; 9 - import { useEffect, useMemo, useRef, useState } from "react"; 10 - import { useSubscribe } from "replicache-react"; 9 + import { useEffect } from "react"; 11 10 import { elementId } from "src/utils/elementId"; 12 11 import { TextSelection } from "prosemirror-state"; 13 12 import { useSelectingMouse } from "components/SelectionManager"; ··· 16 15 import { CardBlock } from "./CardBlock"; 17 16 import { ExternalLinkBlock } from "./ExternalLinkBlock"; 18 17 import { BlockOptions } from "./BlockOptions"; 18 + import { useBlocks } from "src/hooks/queries/useBlocks"; 19 19 20 20 export type Block = { 21 21 parent: string; ··· 25 25 }; 26 26 export function Blocks(props: { entityID: string }) { 27 27 let rep = useReplicache(); 28 - let initialValue = useMemo( 29 - () => 30 - rep.initialFacts 31 - .filter( 32 - (f) => f.attribute === "card/block" && f.entity === props.entityID, 33 - ) 34 - .map((_f) => { 35 - let block = _f as Fact<"card/block">; 36 - let type = rep.initialFacts.find( 37 - (f) => 38 - f.entity === block.data.value && f.attribute === "block/type", 39 - ) as Fact<"block/type"> | undefined; 40 - if (!type) return null; 41 - return { ...block.data, type: type.data.value, parent: block.entity }; 42 - }), 43 - [rep.initialFacts, props.entityID], 44 - ); 45 - let data = 46 - useSubscribe(rep?.rep, async (tx) => { 47 - let initialized = await tx.get("initialized"); 48 - if (!initialized) return null; 49 - let blocks = await tx 50 - .scan< 51 - Fact<"card/block"> 52 - >({ indexName: "eav", prefix: `${props.entityID}-card/block` }) 53 - .toArray(); 54 - 55 - return Promise.all( 56 - blocks.map(async (b) => { 57 - let type = ( 58 - await tx 59 - .scan< 60 - Fact<"block/type"> 61 - >({ prefix: `${b.data.value}-block/type`, indexName: "eav" }) 62 - .toArray() 63 - )[0]; 64 - if (!type) return null; 65 - return { 66 - ...b.data, 67 - type: type.data.value, 68 - parent: b.entity, 69 - } as Block; 70 - }), 71 - ); 72 - }) || initialValue; 73 - let blocks = data 74 - .flatMap((f) => (!f ? [] : [f])) 75 - .sort((a, b) => { 76 - return a.position > b.position ? 1 : -1; 77 - }); 28 + let blocks = useBlocks(props.entityID); 78 29 79 30 let lastBlock = blocks[blocks.length - 1]; 80 31 return (
+18 -8
components/CardBlock.tsx
··· 3 3 import { useEntity, useReplicache } from "src/replicache"; 4 4 import { useUIState } from "src/useUIState"; 5 5 import { RenderedTextBlock } from "./TextBlock"; 6 + import { useDocMetadata } from "src/hooks/queries/useDocMetadata"; 6 7 7 8 export function CardBlock(props: BlockProps) { 8 9 let isSelected = useUIState( ··· 10 11 (props.type !== "text" || s.selectedBlock.length > 1) && 11 12 s.selectedBlock.find((b) => b.value === props.entityID), 12 13 ); 13 - let blocks = useEntity(props.entityID, "card/block"); 14 - let firstBlock = blocks.sort((a, b) => { 15 - return a.data.position > b.data.position ? 1 : -1; 16 - })[0]; 14 + let docMetadata = useDocMetadata(props.entityID); 17 15 18 16 let isOpen = useUIState((s) => s.openCards).includes(props.entityID); 19 17 ··· 35 33 if (rep) focusCard(props.entityID, rep, "focusFirstBlock"); 36 34 }} 37 35 > 38 - <div className={`p-2 grow`}> 36 + <div className="pt-2 pb-2 px-2 grow min-w-0"> 39 37 {/* TODO: 40 38 if the document is completely empty (no blocks, no text) show placeholder text 41 39 the placeholder should be classname= "text-tertiary italic font-bold" */} ··· 51 49 52 50 {/* TODO: 53 51 the cardBlockPreview image should be screenshot of the page it links to */} 54 - 55 - <RenderedTextBlock entityID={firstBlock?.data.value} /> 52 + {docMetadata.heading && ( 53 + <div 54 + className={`cardBlockTitle bg-transparent -mb-0.5 border-none text-base font-bold outline-none resize-none align-top border h-[24px] line-clamp-1`} 55 + > 56 + <RenderedTextBlock entityID={docMetadata.heading} /> 57 + </div> 58 + )} 59 + {docMetadata.content && ( 60 + <div 61 + className={`cardBlockDescription text-sm bg-transparent border-none outline-none resize-none align-top ${docMetadata.heading ? "line-clamp-3" : "line-clamp-4"}`} 62 + > 63 + <RenderedTextBlock entityID={docMetadata.content} /> 64 + </div> 65 + )} 56 66 </div> 57 67 <CardPreview entityID={props.entityID} /> 58 68 </div> ··· 63 73 let blocks = useEntity(props.entityID, "card/block"); 64 74 return ( 65 75 <div 66 - className={`cardBlockPreview w-[120px] p-1 m-2 -mb-2 bg-bg-card border border-border-light flex flex-col gap-1 rotate-6 origin-center`} 76 + className={`cardBlockPreview w-[120px] p-1 m-2 -mb-2 bg-bg-card border shrink-0 border-border-light flex flex-col gap-1 rotate-6 origin-center`} 67 77 > 68 78 {blocks 69 79 .sort((a, b) => (a.data.position > b.data.position ? 1 : -1))
+63
src/hooks/queries/useBlocks.ts
··· 1 + import { Block } from "components/Blocks"; 2 + import { useMemo } from "react"; 3 + import { ReadTransaction } from "replicache"; 4 + import { useSubscribe } from "replicache-react"; 5 + import { Fact, useReplicache } from "src/replicache"; 6 + 7 + export const useBlocks = (entityID: string) => { 8 + let rep = useReplicache(); 9 + let initialValue = useMemo( 10 + () => 11 + rep.initialFacts 12 + .filter((f) => f.attribute === "card/block" && f.entity === entityID) 13 + .map((_f) => { 14 + let block = _f as Fact<"card/block">; 15 + let type = rep.initialFacts.find( 16 + (f) => 17 + f.entity === block.data.value && f.attribute === "block/type", 18 + ) as Fact<"block/type"> | undefined; 19 + if (!type) return null; 20 + return { ...block.data, type: type.data.value, parent: block.entity }; 21 + }), 22 + [rep.initialFacts, entityID], 23 + ); 24 + let data = 25 + useSubscribe(rep?.rep, async (tx) => getBlocksWithType(tx, entityID)) || 26 + initialValue; 27 + return data 28 + .flatMap((f) => (!f ? [] : [f])) 29 + .sort((a, b) => { 30 + return a.position > b.position ? 1 : -1; 31 + }); 32 + }; 33 + 34 + export const getBlocksWithType = async ( 35 + tx: ReadTransaction, 36 + entityID: string, 37 + ) => { 38 + let initialized = await tx.get("initialized"); 39 + if (!initialized) return null; 40 + let blocks = await tx 41 + .scan< 42 + Fact<"card/block"> 43 + >({ indexName: "eav", prefix: `${entityID}-card/block` }) 44 + .toArray(); 45 + 46 + return await Promise.all( 47 + blocks.map(async (b) => { 48 + let type = ( 49 + await tx 50 + .scan< 51 + Fact<"block/type"> 52 + >({ prefix: `${b.data.value}-block/type`, indexName: "eav" }) 53 + .toArray() 54 + )[0]; 55 + if (!type) return null; 56 + return { 57 + ...b.data, 58 + type: type.data.value, 59 + parent: b.entity, 60 + } as Block; 61 + }), 62 + ); 63 + };
+20
src/hooks/queries/useDocMetadata.ts
··· 1 + import { useBlocks } from "./useBlocks"; 2 + 3 + type DocMetaData = { 4 + heading?: string; 5 + content?: string; 6 + }; 7 + 8 + export function useDocMetadata(entityID: string) { 9 + let docMetadata: DocMetaData = {}; 10 + let blocks = useBlocks(entityID); 11 + let firstBlock = blocks[0]; 12 + let secondBlock = blocks[1]; 13 + if (firstBlock?.type === "heading") { 14 + docMetadata.heading = blocks[0].value; 15 + if (secondBlock?.type === "text") docMetadata.content = blocks[1].value; 16 + } else { 17 + if (firstBlock?.type === "text") docMetadata.content = blocks[0].value; 18 + } 19 + return docMetadata; 20 + }