Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Fix svg color, fix misplaced line breaks

uwx 0cba2a15 c89ca6f7

+215 -115
+214 -113
src/components/Post/MastodonHtmlContent.tsx
··· 14 14 15 15 import {MathJaxSvgText} from '#/lib/mathjax' 16 16 import {useRenderMastodonHtml} from '#/state/preferences/render-mastodon-html' 17 - import {atoms as a} from '#/alf' 17 + import {atoms as a, useAlf, web} from '#/alf' 18 18 import {Button, ButtonText} from '#/components/Button' 19 19 import {InlineLinkText} from '#/components/Link' 20 20 import {P, Text} from '#/components/Typography' ··· 72 72 const [isExpanded, setIsExpanded] = useState(false) 73 73 const [contentHeight, setContentHeight] = useState<number | null>(null) 74 74 const [isTall, setIsTall] = useState(false) 75 + const {theme: t} = useAlf() 75 76 76 77 const renderedContent = useMemo(() => { 77 78 if (!renderMastodonHtml) return null ··· 84 85 if (!rawHtml) return null 85 86 86 87 // Parse HTML once and sanitize/render in a single pass 87 - return sanitizeAndRenderHtml(rawHtml, numberOfLines, textStyle) 88 - }, [record, renderMastodonHtml, numberOfLines, textStyle]) 88 + return sanitizeAndRenderHtml(rawHtml, numberOfLines, textStyle, [ 89 + t.atoms.text, 90 + textStyle, 91 + ]) 92 + }, [record, renderMastodonHtml, numberOfLines, textStyle, t.atoms.text]) 89 93 90 94 const handleLayout = (event: LayoutChangeEvent) => { 91 95 const height = event.nativeEvent.layout.height ··· 125 129 </View> 126 130 ) 127 131 } 132 + const BLOCK_ELEMENTS = new Set([ 133 + 'p', 134 + 'blockquote', 135 + 'pre', 136 + 'div', 137 + 'ul', 138 + 'ol', 139 + 'li', 140 + 'h1', 141 + 'h2', 142 + 'h3', 143 + 'h4', 144 + 'h5', 145 + 'h6', 146 + 'html', 147 + ]) 148 + 149 + function isInlineNode(node: XMLNode): boolean { 150 + const name = node.nodeName 151 + if (name === '#text' || name === '#cdata-section' || name === '#comment') { 152 + return true 153 + } 154 + if (name === '#document') return false 155 + const el = node as XMLElement 156 + return !BLOCK_ELEMENTS.has(el.tagName.toLowerCase()) 157 + } 128 158 129 159 const PROTOCOL_REGEX = 130 160 /^(https?|dat|dweb|ipfs|ipns|ssb|gopher|xmpp|magnet|gemini):\/\//i 131 161 162 + function resolveColorFromStyle( 163 + style: StyleProp<TextStyle>, 164 + ): string | undefined { 165 + if (Array.isArray(style)) { 166 + for (const s of style) { 167 + const color = resolveColorFromStyle(s as StyleProp<TextStyle>) 168 + if (color) return color 169 + } 170 + return undefined 171 + } 172 + if (!style || typeof style === 'number') return undefined 173 + if (typeof style.color === 'symbol') return undefined 174 + return style?.color ?? undefined 175 + } 176 + 177 + function resolveFontSizeFromStyle( 178 + style: StyleProp<TextStyle>, 179 + ): number | undefined { 180 + if (Array.isArray(style)) { 181 + for (const s of style) { 182 + const fontSize = resolveFontSizeFromStyle(s as StyleProp<TextStyle>) 183 + if (fontSize) return fontSize 184 + } 185 + return undefined 186 + } 187 + if (!style || typeof style === 'number') return undefined 188 + return style?.fontSize ?? undefined 189 + } 190 + 132 191 function sanitizeAndRenderHtml( 133 192 html: string, 134 193 _numberOfLines?: number, 135 194 inputTextStyle?: StyleProp<TextStyle>, 195 + plainTextStyle?: StyleProp<TextStyle>, 136 196 ): React.ReactNode { 137 197 const doc = new DOMParser().parseFromString( 138 198 `<html>${html}</html>`, ··· 143 203 144 204 const base = null // TODO: find instance URL, allow relative URLs. mastodon allows this 145 205 146 - const textStyle: StyleProp<TextStyle> = [ 147 - a.leading_snug, 148 - a.text_md, 149 - inputTextStyle, 150 - ] 206 + inputTextStyle = [a.leading_snug, a.text_md, inputTextStyle] 151 207 152 - console.log(textStyle) 208 + plainTextStyle = [a.leading_snug, a.text_md, plainTextStyle] 209 + 210 + console.log(inputTextStyle) 153 211 154 212 // Sanitize and render in a single pass 155 213 const renderNode = ( ··· 157 215 key: string, 158 216 insideLink = false, 159 217 listItemIndex?: number, 218 + insidePre = false, 219 + trimStart = false, 220 + trimEnd = false, 160 221 ): React.ReactNode => { 161 222 key = `${key}-${node.nodeName.toLowerCase()}` 162 223 if (node.nodeName === '#text') { 163 - // Don't wrap text in styled Text component if inside a link 224 + let text = node.nodeValue ?? '' 225 + 226 + if (!insidePre) { 227 + text = text.replace(/[\t\n\r ]+/g, ' ') 228 + if (trimStart) text = text.replace(/^ /, '') 229 + if (trimEnd) text = text.replace(/ $/, '') 230 + if (text === '') return null 231 + } 232 + 164 233 if (insideLink) { 165 - return node.nodeValue 234 + return text 166 235 } 167 236 return ( 168 - <Text key={key} style={textStyle} emoji> 169 - {node.nodeValue} 237 + <Text key={key} style={inputTextStyle} emoji> 238 + {text} 170 239 </Text> 171 240 ) 172 241 } ··· 177 246 178 247 if (node.nodeName === '#cdata-section') { 179 248 return ( 180 - <Text key={key} style={textStyle} emoji> 249 + <Text key={key} style={inputTextStyle} emoji> 181 250 {node.nodeValue} 182 251 </Text> 183 252 ) ··· 187 256 return ( 188 257 <> 189 258 {map(node.childNodes, (child, i) => 190 - renderNode(child, `${key}-${i}`, insideLink), 259 + renderNode( 260 + child, 261 + `${key}-${i}`, 262 + insideLink, 263 + listItemIndex, 264 + insidePre, 265 + trimStart, 266 + trimEnd, 267 + ), 191 268 )} 192 269 </> 193 270 ) ··· 198 275 199 276 console.log(tagName) 200 277 278 + const isBlock = BLOCK_ELEMENTS.has(tagName) 279 + const childPre = insidePre || tagName === 'pre' 280 + 281 + // Find first/last non-comment child indices for whitespace trimming 282 + let firstContentIndex = -1 283 + let lastContentIndex = -1 284 + if (element.childNodes) { 285 + for (let ci = 0; ci < element.childNodes.length; ci++) { 286 + if (element.childNodes[ci].nodeName !== '#comment') { 287 + if (firstContentIndex === -1) firstContentIndex = ci 288 + lastContentIndex = ci 289 + } 290 + } 291 + } 292 + 293 + const renderChildren = (subInsideLink?: boolean, groupInline = false) => { 294 + if (!element.childNodes) return [] 295 + 296 + const childRenderer = (child: XMLNode, i: number) => 297 + renderNode( 298 + child, 299 + String(i), 300 + subInsideLink || insideLink, 301 + listItemIndex, 302 + childPre, 303 + !childPre && 304 + ((isBlock && i === firstContentIndex) || 305 + (trimStart && i === firstContentIndex)), 306 + !childPre && 307 + ((isBlock && i === lastContentIndex) || 308 + (trimEnd && i === lastContentIndex)), 309 + ) 310 + 311 + if (!groupInline) { 312 + return map(element.childNodes, childRenderer) ?? [] 313 + } 314 + 315 + // Group consecutive inline nodes into <Text> wrappers so they 316 + // render as <span> on web instead of becoming separate <div>s 317 + const result: React.ReactNode[] = [] 318 + let inlineGroup: React.ReactNode[] = [] 319 + let groupKey = 0 320 + 321 + const flushInline = () => { 322 + if (inlineGroup.length === 0) return 323 + result.push( 324 + <Text key={`ig-${groupKey}`} style={inputTextStyle} emoji> 325 + {inlineGroup} 326 + </Text>, 327 + ) 328 + inlineGroup = [] 329 + } 330 + 331 + for (let i = 0; i < element.childNodes.length; i++) { 332 + const child = element.childNodes[i] 333 + const rendered = childRenderer(child, i) 334 + if (isInlineNode(child)) { 335 + if (inlineGroup.length === 0) groupKey = i 336 + inlineGroup.push(rendered) 337 + } else { 338 + flushInline() 339 + result.push(rendered) 340 + } 341 + } 342 + flushInline() 343 + 344 + return result 345 + } 346 + 201 347 switch (tagName) { 202 348 case 'math': 203 349 const mathText = extractMathAnnotation(element) 204 350 if (mathText) { 205 351 return ( 206 - <MathJaxSvgText key={key} fontCache={true}> 352 + <MathJaxSvgText 353 + key={key} 354 + fontCache={true} 355 + color={resolveColorFromStyle(plainTextStyle)} 356 + fontSize={resolveFontSizeFromStyle(plainTextStyle)}> 207 357 {`$${mathText}$`} 208 358 </MathJaxSvgText> 209 359 ) 210 360 } 211 361 return null 212 362 case 'p': { 213 - const children = 214 - map(element.childNodes, (child, i) => 215 - renderNode(child, String(i), insideLink), 216 - ) ?? [] 363 + const children = renderChildren() 217 364 218 365 return ( 219 - <P key={key} style={textStyle} emoji> 366 + <P key={key} style={inputTextStyle} emoji> 220 367 {children} 221 368 </P> 222 369 ) 223 370 } 224 371 case 'blockquote': { 225 - const children = 226 - map(element.childNodes, (child, i) => 227 - renderNode(child, String(i), insideLink), 228 - ) ?? [] 372 + const children = renderChildren() 229 373 230 374 return ( 231 375 <View 376 + {...(web({ 377 + role: 'blockquote', 378 + }) || {})} 232 379 key={key} 233 380 style={{ 234 381 borderLeftWidth: 3, ··· 236 383 paddingLeft: 12, 237 384 marginVertical: 4, 238 385 }}> 239 - <P style={textStyle} emoji> 240 - {children} 241 - </P> 386 + {children} 242 387 </View> 243 388 ) 244 389 } 245 390 case 'pre': { 246 - const children = 247 - map(element.childNodes, (child, i) => 248 - renderNode(child, String(i), insideLink), 249 - ) ?? [] 391 + const children = renderChildren() 250 392 251 393 return ( 252 394 <View ··· 256 398 borderRadius: 4, 257 399 marginVertical: 4, 258 400 }}> 259 - <P style={[textStyle, {fontFamily: 'monospace'}]} emoji> 401 + <P style={[inputTextStyle, {fontFamily: 'monospace'}]} emoji> 260 402 {children} 261 403 </P> 262 404 </View> 263 405 ) 264 406 } 265 407 case 'code': { 266 - const children = 267 - map(element.childNodes, (child, i) => 268 - renderNode(child, String(i), insideLink), 269 - ) ?? [] 408 + const children = renderChildren() 270 409 271 410 return ( 272 411 <Text 273 412 key={key} 274 413 style={[ 275 - textStyle, 414 + inputTextStyle, 276 415 { 277 416 fontFamily: 'monospace', 278 417 paddingHorizontal: 4, ··· 286 425 } 287 426 case 'strong': 288 427 case 'b': { 289 - const children = 290 - map(element.childNodes, (child, i) => 291 - renderNode(child, String(i), insideLink), 292 - ) ?? [] 428 + const children = renderChildren() 293 429 294 430 return ( 295 - <Text key={key} style={[textStyle, a.font_bold]} emoji> 431 + <Text key={key} style={[inputTextStyle, a.font_bold]} emoji> 296 432 {children} 297 433 </Text> 298 434 ) 299 435 } 300 436 case 'em': 301 437 case 'i': { 302 - const children = 303 - map(element.childNodes, (child, i) => 304 - renderNode(child, String(i), insideLink), 305 - ) ?? [] 438 + const children = renderChildren() 306 439 307 440 return ( 308 - <Text key={key} style={[textStyle, a.italic]} emoji> 441 + <Text key={key} style={[inputTextStyle, a.italic]} emoji> 309 442 {children} 310 443 </Text> 311 444 ) 312 445 } 313 446 case 'u': { 314 - const children = 315 - map(element.childNodes, (child, i) => 316 - renderNode(child, String(i), insideLink), 317 - ) ?? [] 447 + const children = renderChildren() 318 448 319 449 return ( 320 - <Text key={key} style={[textStyle, a.underline]} emoji> 450 + <Text key={key} style={[inputTextStyle, a.underline]} emoji> 321 451 {children} 322 452 </Text> 323 453 ) 324 454 } 325 455 case 'del': 326 456 case 's': { 327 - const children = 328 - map(element.childNodes, (child, i) => 329 - renderNode(child, String(i), insideLink), 330 - ) ?? [] 457 + const children = renderChildren() 331 458 332 459 return ( 333 - <Text key={key} style={[textStyle, a.strike_through]} emoji> 460 + <Text key={key} style={[inputTextStyle, a.strike_through]} emoji> 334 461 {children} 335 462 </Text> 336 463 ) 337 464 } 338 465 case 'ul': { 339 - const children = 340 - map(element.childNodes, (child, i) => 341 - renderNode(child, String(i), insideLink), 342 - ) ?? [] 466 + const children = renderChildren() 343 467 344 468 return ( 345 469 <View key={key} style={{marginVertical: 4}}> ··· 355 479 {toArray(element.childNodes) 356 480 ?.filter(child => child.nodeName === 'LI') 357 481 .map((child, i) => 358 - renderNode(child, `${key}-${i}`, insideLink, startNum + i), 482 + renderNode( 483 + child, 484 + `${key}-${i}`, 485 + insideLink, 486 + startNum + i, 487 + childPre, 488 + true, 489 + true, 490 + ), 359 491 ) ?? []} 360 492 </View> 361 493 ) 362 494 } 363 495 case 'li': { 364 - const children = 365 - map(element.childNodes, (child, i) => 366 - renderNode(child, String(i), insideLink), 367 - ) ?? [] 496 + const children = renderChildren() 368 497 369 498 const marker = 370 499 listItemIndex !== undefined ? `${listItemIndex}.` : '\u2022' 371 500 return ( 372 501 <View key={key} style={{flexDirection: 'row', marginVertical: 2}}> 373 - <Text style={[textStyle, {marginRight: 8}]} emoji> 502 + <Text style={[inputTextStyle, {marginRight: 8}]} emoji> 374 503 {marker} 375 504 </Text> 376 - <Text style={[textStyle, {flex: 1}]} emoji> 377 - {children} 378 - </Text> 505 + <View style={[inputTextStyle, {flex: 1}]}>{children}</View> 379 506 </View> 380 507 ) 381 508 } 382 509 case 'ruby': { 383 - const children = 384 - map(element.childNodes, (child, i) => 385 - renderNode(child, String(i), insideLink), 386 - ) ?? [] 510 + const children = renderChildren() 387 511 388 - return ( 389 - <Text key={key} style={textStyle} emoji> 390 - {children} 391 - </Text> 392 - ) 512 + return <>{children}</> 393 513 } 394 514 case 'rt': 395 515 case 'rp': 396 516 return null // TODO support ruby text rendering 397 517 case 'a': { 398 - const children = 399 - map(element.childNodes, (child, i) => 400 - renderNode(child, String(i), true), 401 - ) ?? [] 518 + const children = renderChildren(true) 402 519 403 520 const href = element.getAttribute('href') 404 521 if (href) { ··· 416 533 to={url.toString()} 417 534 label={linkText} 418 535 shouldProxy 419 - style={isInvisible ? {display: 'none'} : textStyle}> 536 + style={isInvisible ? {display: 'none'} : inputTextStyle}> 420 537 {children} 421 538 </InlineLinkText> 422 539 ) 423 540 } 424 541 } 425 - return ( 426 - <Text key={key} style={textStyle} emoji> 427 - {children} 428 - </Text> 429 - ) 542 + return <>{children}</> 430 543 } 431 544 case 'br': 432 545 return ( 433 - <Text key={key} style={textStyle} emoji> 546 + <Text key={key} style={inputTextStyle} emoji> 434 547 {'\n'} 435 548 </Text> 436 549 ) 437 550 case 'span': { 438 - const children = 439 - map(element.childNodes, (child, i) => 440 - renderNode(child, String(i), insideLink), 441 - ) ?? [] 551 + const children = renderChildren() 442 552 443 553 // Handle invisible/ellipsis classes for link formatting 444 554 if (hasClass(element, 'invisible')) { ··· 454 564 return '\u2026' 455 565 } 456 566 return ( 457 - <Text key={key} style={textStyle} emoji> 567 + <Text key={key} style={inputTextStyle} emoji> 458 568 {'\u2026'} 459 569 </Text> 460 570 ) ··· 476 586 key={key} 477 587 display={tagText} 478 588 tag={tagText} 479 - textStyle={[textStyle, a.underline]} 589 + textStyle={[inputTextStyle, a.underline]} 480 590 /> 481 591 ) 482 592 } ··· 487 597 return children 488 598 } 489 599 return ( 490 - <Text key={key} style={textStyle} emoji> 600 + <Text key={key} style={inputTextStyle} emoji> 491 601 {children} 492 602 </Text> 493 603 ) ··· 497 607 return children 498 608 } 499 609 return ( 500 - <Text key={key} style={textStyle} emoji> 610 + <Text key={key} style={inputTextStyle} emoji> 501 611 {children} 502 612 </Text> 503 613 ) ··· 508 618 case 'h4': 509 619 case 'h5': 510 620 case 'h6': { 511 - const children = 512 - map(element.childNodes, (child, i) => 513 - renderNode(child, String(i), insideLink), 514 - ) ?? [] 621 + const children = renderChildren() 515 622 516 623 return ( 517 - <P key={key} style={textStyle} emoji> 518 - <Text style={[textStyle, {fontWeight: 'bold'}]} emoji> 624 + <P key={key} style={inputTextStyle} emoji> 625 + <Text style={[inputTextStyle, {fontWeight: 'bold'}]} emoji> 519 626 {children} 520 627 </Text> 521 628 </P> 522 629 ) 523 630 } 524 631 default: 525 - // Render node contents 526 - return ( 527 - <> 528 - {map(element.childNodes, (child, i) => 529 - renderNode(child, `${key}-${i}`, insideLink), 530 - ) ?? []} 531 - </> 532 - ) 632 + // Render node contents, grouping inline children when inside a block 633 + return <>{renderChildren(undefined, isBlock)}</> 533 634 } 534 635 } 535 636
+1 -2
src/lib/mathjax/index.tsx
··· 165 165 return ( 166 166 <> 167 167 {text ? ( 168 - <Text 169 - style={[{fontSize: fontSize * 2, color}, rnStyle ?? {}, textStyle]}> 168 + <Text style={[{fontSize, color}, rnStyle ?? {}, textStyle]}> 170 169 {text} 171 170 </Text> 172 171 ) : item?.kind === 'svg' ? (