a tool for shared writing and social publishing
298
fork

Configure Feed

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

fix rendering client component in rss feed

+212 -204
+20 -197
app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx
··· 1 - import { UnicodeString } from "@atproto/api"; 2 - import { PubLeafletRichtextFacet } from "lexicons/api"; 3 - import { didToBlueskyUrl } from "src/utils/mentionUtils"; 4 - import { AtMentionLink } from "components/AtMentionLink"; 5 1 import { ProfilePopover } from "components/ProfilePopover"; 2 + import { TextBlockCore, TextBlockCoreProps, RichText } from "./TextBlockCore"; 3 + import { ReactNode } from "react"; 6 4 7 - type Facet = PubLeafletRichtextFacet.Main; 8 - export function BaseTextBlock(props: { 9 - plaintext: string; 10 - facets?: Facet[]; 11 - index: number[]; 12 - preview?: boolean; 13 - }) { 14 - let children = []; 15 - let richText = new RichText({ 16 - text: props.plaintext, 17 - facets: props.facets || [], 18 - }); 19 - let counter = 0; 20 - for (const segment of richText.segments()) { 21 - let id = segment.facet?.find(PubLeafletRichtextFacet.isId); 22 - let link = segment.facet?.find(PubLeafletRichtextFacet.isLink); 23 - let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold); 24 - let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode); 25 - let isStrikethrough = segment.facet?.find( 26 - PubLeafletRichtextFacet.isStrikethrough, 27 - ); 28 - let isDidMention = segment.facet?.find( 29 - PubLeafletRichtextFacet.isDidMention, 30 - ); 31 - let isAtMention = segment.facet?.find(PubLeafletRichtextFacet.isAtMention); 32 - let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); 33 - let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); 34 - let isHighlighted = segment.facet?.find( 35 - PubLeafletRichtextFacet.isHighlight, 36 - ); 37 - let className = ` 38 - ${isCode ? "inline-code" : ""} 39 - ${id ? "scroll-mt-12 scroll-mb-10" : ""} 40 - ${isBold ? "font-bold" : ""} 41 - ${isItalic ? "italic" : ""} 42 - ${isUnderline ? "underline" : ""} 43 - ${isStrikethrough ? "line-through decoration-tertiary" : ""} 44 - ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); 5 + // Re-export RichText for backwards compatibility 6 + export { RichText }; 45 7 46 - // Split text by newlines and insert <br> tags 47 - const textParts = segment.text.split("\n"); 48 - const renderedText = textParts.flatMap((part, i) => 49 - i < textParts.length - 1 50 - ? [part, <br key={`br-${counter}-${i}`} />] 51 - : [part], 52 - ); 53 - 54 - if (isCode) { 55 - children.push( 56 - <code key={counter} className={className} id={id?.id}> 57 - {renderedText} 58 - </code>, 59 - ); 60 - } else if (isDidMention) { 61 - children.push( 62 - <ProfilePopover 63 - key={counter} 64 - didOrHandle={isDidMention.did} 65 - trigger={<span className="mention">{renderedText}</span>} 66 - />, 67 - ); 68 - } else if (isAtMention) { 69 - children.push( 70 - <AtMentionLink 71 - key={counter} 72 - atURI={isAtMention.atURI} 73 - className={className} 74 - > 75 - {renderedText} 76 - </AtMentionLink>, 77 - ); 78 - } else if (link) { 79 - children.push( 80 - <a 81 - key={counter} 82 - href={link.uri} 83 - className={`text-accent-contrast hover:underline ${className}`} 84 - target="_blank" 85 - > 86 - {renderedText} 87 - </a>, 88 - ); 89 - } else { 90 - children.push( 91 - <span key={counter} className={className} id={id?.id}> 92 - {renderedText} 93 - </span>, 94 - ); 95 - } 96 - 97 - counter++; 98 - } 99 - return <>{children}</>; 8 + function DidMentionWithPopover(props: { did: string; children: ReactNode }) { 9 + return ( 10 + <ProfilePopover 11 + didOrHandle={props.did} 12 + trigger={props.children} 13 + /> 14 + ); 100 15 } 101 16 102 - type RichTextSegment = { 103 - text: string; 104 - facet?: Exclude<Facet["features"], { $type: string }>; 105 - }; 106 - 107 - export class RichText { 108 - unicodeText: UnicodeString; 109 - facets?: Facet[]; 110 - 111 - constructor(props: { text: string; facets: Facet[] }) { 112 - this.unicodeText = new UnicodeString(props.text); 113 - this.facets = props.facets; 114 - if (this.facets) { 115 - this.facets = this.facets 116 - .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) 117 - .sort((a, b) => a.index.byteStart - b.index.byteStart); 118 - } 119 - } 120 - 121 - *segments(): Generator<RichTextSegment, void, void> { 122 - const facets = this.facets || []; 123 - if (!facets.length) { 124 - yield { text: this.unicodeText.utf16 }; 125 - return; 126 - } 127 - 128 - let textCursor = 0; 129 - let facetCursor = 0; 130 - do { 131 - const currFacet = facets[facetCursor]; 132 - if (textCursor < currFacet.index.byteStart) { 133 - yield { 134 - text: this.unicodeText.slice(textCursor, currFacet.index.byteStart), 135 - }; 136 - } else if (textCursor > currFacet.index.byteStart) { 137 - facetCursor++; 138 - continue; 139 - } 140 - if (currFacet.index.byteStart < currFacet.index.byteEnd) { 141 - const subtext = this.unicodeText.slice( 142 - currFacet.index.byteStart, 143 - currFacet.index.byteEnd, 144 - ); 145 - if (!subtext.trim()) { 146 - // dont empty string entities 147 - yield { text: subtext }; 148 - } else { 149 - yield { text: subtext, facet: currFacet.features }; 150 - } 151 - } 152 - textCursor = currFacet.index.byteEnd; 153 - facetCursor++; 154 - } while (facetCursor < facets.length); 155 - if (textCursor < this.unicodeText.length) { 156 - yield { 157 - text: this.unicodeText.slice(textCursor, this.unicodeText.length), 158 - }; 159 - } 160 - } 161 - } 162 - function addFacet(facets: Facet[], newFacet: Facet, length: number) { 163 - if (facets.length === 0) { 164 - return [newFacet]; 165 - } 166 - 167 - const allFacets = [...facets, newFacet]; 168 - 169 - // Collect all boundary positions 170 - const boundaries = new Set<number>(); 171 - boundaries.add(0); 172 - boundaries.add(length); 173 - 174 - for (const facet of allFacets) { 175 - boundaries.add(facet.index.byteStart); 176 - boundaries.add(facet.index.byteEnd); 177 - } 178 - 179 - const sortedBoundaries = Array.from(boundaries).sort((a, b) => a - b); 180 - const result: Facet[] = []; 181 - 182 - // Process segments between consecutive boundaries 183 - for (let i = 0; i < sortedBoundaries.length - 1; i++) { 184 - const start = sortedBoundaries[i]; 185 - const end = sortedBoundaries[i + 1]; 186 - 187 - // Find facets that are active at the start position 188 - const activeFacets = allFacets.filter( 189 - (facet) => facet.index.byteStart <= start && facet.index.byteEnd > start, 190 - ); 191 - 192 - // Only create facet if there are active facets (features present) 193 - if (activeFacets.length > 0) { 194 - const features = activeFacets.flatMap((f) => f.features); 195 - result.push({ 196 - index: { byteStart: start, byteEnd: end }, 197 - features, 198 - }); 199 - } 200 - } 201 - 202 - return result; 17 + export function BaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) { 18 + return ( 19 + <TextBlockCore 20 + {...props} 21 + renderers={{ 22 + DidMention: DidMentionWithPopover, 23 + }} 24 + /> 25 + ); 203 26 }
+11 -7
app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx
··· 12 12 PubLeafletPagesLinearDocument, 13 13 } from "lexicons/api"; 14 14 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 15 - import { BaseTextBlock } from "./BaseTextBlock"; 15 + import { TextBlockCore, TextBlockCoreProps } from "./TextBlockCore"; 16 16 import { StaticMathBlock } from "./StaticMathBlock"; 17 17 import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki"; 18 + 19 + function StaticBaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) { 20 + return <TextBlockCore {...props} />; 21 + } 18 22 19 23 export function StaticPostContent({ 20 24 blocks, ··· 47 51 case PubLeafletBlocksBlockquote.isMain(b.block): { 48 52 return ( 49 53 <blockquote className={` blockquote `}> 50 - <BaseTextBlock 54 + <StaticBaseTextBlock 51 55 facets={b.block.facets} 52 56 plaintext={b.block.plaintext} 53 57 index={[]} ··· 116 120 case PubLeafletBlocksText.isMain(b.block): 117 121 return ( 118 122 <p> 119 - <BaseTextBlock 123 + <StaticBaseTextBlock 120 124 facets={b.block.facets} 121 125 plaintext={b.block.plaintext} 122 126 index={[]} ··· 127 131 if (b.block.level === 1) 128 132 return ( 129 133 <h1> 130 - <BaseTextBlock {...b.block} index={[]} /> 134 + <StaticBaseTextBlock {...b.block} index={[]} /> 131 135 </h1> 132 136 ); 133 137 if (b.block.level === 2) 134 138 return ( 135 139 <h2> 136 - <BaseTextBlock {...b.block} index={[]} /> 140 + <StaticBaseTextBlock {...b.block} index={[]} /> 137 141 </h2> 138 142 ); 139 143 if (b.block.level === 3) 140 144 return ( 141 145 <h3> 142 - <BaseTextBlock {...b.block} index={[]} /> 146 + <StaticBaseTextBlock {...b.block} index={[]} /> 143 147 </h3> 144 148 ); 145 149 // if (b.block.level === 4) return <h4>{b.block.plaintext}</h4>; 146 150 // if (b.block.level === 5) return <h5>{b.block.plaintext}</h5>; 147 151 return ( 148 152 <h6> 149 - <BaseTextBlock {...b.block} index={[]} /> 153 + <StaticBaseTextBlock {...b.block} index={[]} /> 150 154 </h6> 151 155 ); 152 156 }
+181
app/lish/[did]/[publication]/[rkey]/TextBlockCore.tsx
··· 1 + import { UnicodeString } from "@atproto/api"; 2 + import { PubLeafletRichtextFacet } from "lexicons/api"; 3 + import { AtMentionLink } from "components/AtMentionLink"; 4 + import { ReactNode } from "react"; 5 + 6 + type Facet = PubLeafletRichtextFacet.Main; 7 + 8 + export type FacetRenderers = { 9 + DidMention?: (props: { did: string; children: ReactNode }) => ReactNode; 10 + }; 11 + 12 + export type TextBlockCoreProps = { 13 + plaintext: string; 14 + facets?: Facet[]; 15 + index: number[]; 16 + preview?: boolean; 17 + renderers?: FacetRenderers; 18 + }; 19 + 20 + export function TextBlockCore(props: TextBlockCoreProps) { 21 + let children = []; 22 + let richText = new RichText({ 23 + text: props.plaintext, 24 + facets: props.facets || [], 25 + }); 26 + let counter = 0; 27 + for (const segment of richText.segments()) { 28 + let id = segment.facet?.find(PubLeafletRichtextFacet.isId); 29 + let link = segment.facet?.find(PubLeafletRichtextFacet.isLink); 30 + let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold); 31 + let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode); 32 + let isStrikethrough = segment.facet?.find( 33 + PubLeafletRichtextFacet.isStrikethrough, 34 + ); 35 + let isDidMention = segment.facet?.find( 36 + PubLeafletRichtextFacet.isDidMention, 37 + ); 38 + let isAtMention = segment.facet?.find(PubLeafletRichtextFacet.isAtMention); 39 + let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); 40 + let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); 41 + let isHighlighted = segment.facet?.find( 42 + PubLeafletRichtextFacet.isHighlight, 43 + ); 44 + let className = ` 45 + ${isCode ? "inline-code" : ""} 46 + ${id ? "scroll-mt-12 scroll-mb-10" : ""} 47 + ${isBold ? "font-bold" : ""} 48 + ${isItalic ? "italic" : ""} 49 + ${isUnderline ? "underline" : ""} 50 + ${isStrikethrough ? "line-through decoration-tertiary" : ""} 51 + ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); 52 + 53 + // Split text by newlines and insert <br> tags 54 + const textParts = segment.text.split("\n"); 55 + const renderedText = textParts.flatMap((part, i) => 56 + i < textParts.length - 1 57 + ? [part, <br key={`br-${counter}-${i}`} />] 58 + : [part], 59 + ); 60 + 61 + if (isCode) { 62 + children.push( 63 + <code key={counter} className={className} id={id?.id}> 64 + {renderedText} 65 + </code>, 66 + ); 67 + } else if (isDidMention) { 68 + const DidMentionRenderer = props.renderers?.DidMention; 69 + if (DidMentionRenderer) { 70 + children.push( 71 + <DidMentionRenderer key={counter} did={isDidMention.did}> 72 + <span className="mention">{renderedText}</span> 73 + </DidMentionRenderer>, 74 + ); 75 + } else { 76 + // Default: render as a simple link 77 + children.push( 78 + <a 79 + key={counter} 80 + href={`https://leaflet.pub/p/${isDidMention.did}`} 81 + target="_blank" 82 + className="no-underline" 83 + > 84 + <span className="mention">{renderedText}</span> 85 + </a>, 86 + ); 87 + } 88 + } else if (isAtMention) { 89 + children.push( 90 + <AtMentionLink 91 + key={counter} 92 + atURI={isAtMention.atURI} 93 + className={className} 94 + > 95 + {renderedText} 96 + </AtMentionLink>, 97 + ); 98 + } else if (link) { 99 + children.push( 100 + <a 101 + key={counter} 102 + href={link.uri} 103 + className={`text-accent-contrast hover:underline ${className}`} 104 + target="_blank" 105 + > 106 + {renderedText} 107 + </a>, 108 + ); 109 + } else { 110 + children.push( 111 + <span key={counter} className={className} id={id?.id}> 112 + {renderedText} 113 + </span>, 114 + ); 115 + } 116 + 117 + counter++; 118 + } 119 + return <>{children}</>; 120 + } 121 + 122 + type RichTextSegment = { 123 + text: string; 124 + facet?: Exclude<Facet["features"], { $type: string }>; 125 + }; 126 + 127 + export class RichText { 128 + unicodeText: UnicodeString; 129 + facets?: Facet[]; 130 + 131 + constructor(props: { text: string; facets: Facet[] }) { 132 + this.unicodeText = new UnicodeString(props.text); 133 + this.facets = props.facets; 134 + if (this.facets) { 135 + this.facets = this.facets 136 + .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) 137 + .sort((a, b) => a.index.byteStart - b.index.byteStart); 138 + } 139 + } 140 + 141 + *segments(): Generator<RichTextSegment, void, void> { 142 + const facets = this.facets || []; 143 + if (!facets.length) { 144 + yield { text: this.unicodeText.utf16 }; 145 + return; 146 + } 147 + 148 + let textCursor = 0; 149 + let facetCursor = 0; 150 + do { 151 + const currFacet = facets[facetCursor]; 152 + if (textCursor < currFacet.index.byteStart) { 153 + yield { 154 + text: this.unicodeText.slice(textCursor, currFacet.index.byteStart), 155 + }; 156 + } else if (textCursor > currFacet.index.byteStart) { 157 + facetCursor++; 158 + continue; 159 + } 160 + if (currFacet.index.byteStart < currFacet.index.byteEnd) { 161 + const subtext = this.unicodeText.slice( 162 + currFacet.index.byteStart, 163 + currFacet.index.byteEnd, 164 + ); 165 + if (!subtext.trim()) { 166 + // dont empty string entities 167 + yield { text: subtext }; 168 + } else { 169 + yield { text: subtext, facet: currFacet.features }; 170 + } 171 + } 172 + textCursor = currFacet.index.byteEnd; 173 + facetCursor++; 174 + } while (facetCursor < facets.length); 175 + if (textCursor < this.unicodeText.length) { 176 + yield { 177 + text: this.unicodeText.slice(textCursor, this.unicodeText.length), 178 + }; 179 + } 180 + } 181 + }