Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Almost perfect Masto HTML rendering

uwx f12e505d 3d95d307

+236 -86
+235 -85
src/components/Post/MastodonHtmlContent.tsx
··· 6 6 View, 7 7 type ViewStyle, 8 8 } from 'react-native' 9 + import {UITextView} from 'react-native-uitextview' 9 10 import {type AppBskyFeedPost} from '@atproto/api' 10 11 import {DOMParser, type XMLElement, type XMLNode} from '@journeyapps/domparser' 11 12 import {msg} from '@lingui/core/macro' ··· 13 14 import {Trans} from '@lingui/react/macro' 14 15 15 16 import {MathJaxSvgText} from '#/lib/mathjax' 17 + import {logger} from '#/logger' 16 18 import {useRenderMastodonHtml} from '#/state/preferences/render-mastodon-html' 17 - import {atoms as a, useAlf, web} from '#/alf' 19 + import {type Alf, applyFonts, atoms as a, flatten, useAlf, web} from '#/alf' 20 + import { 21 + childHasEmoji, 22 + renderChildrenWithEmoji, 23 + type TextProps, 24 + } from '#/alf/typography' 18 25 import {Button, ButtonText} from '#/components/Button' 19 26 import {InlineLinkText} from '#/components/Link' 20 - import {P, Text} from '#/components/Typography' 27 + import {numberOfLinesClippingFix, P} from '#/components/Typography' 28 + import {IS_NATIVE} from '#/env' 21 29 import {RichTextTag} from '../RichTextTag' 22 30 31 + //#region Alf typography re-exports with optional font override 32 + /** 33 + * normalizeTextStyles from #/alf with optional font override 34 + * 35 + * Ensures that `lineHeight` defaults to a relative value of `1`, or applies 36 + * other relative leading atoms. 37 + * 38 + * If the `lineHeight` value is > 2, we assume it's an absolute value and 39 + * returns it as-is. 40 + */ 41 + function normalizeTextStyles( 42 + styles: StyleProp<TextStyle>, 43 + { 44 + fontScale, 45 + fontFamily, 46 + }: { 47 + fontScale: number 48 + fontFamily: Alf['fonts']['family'] 49 + } & Pick<Alf, 'flags'>, 50 + ) { 51 + const s = flatten(styles) ?? {} 52 + 53 + // should always be defined on these components 54 + s.fontSize = (s.fontSize || a.text_md.fontSize) * fontScale 55 + 56 + if (s?.lineHeight) { 57 + if (s.lineHeight !== 0 && s.lineHeight <= 2) { 58 + s.lineHeight = Math.round(s.fontSize * s.lineHeight) 59 + } 60 + } else if (!IS_NATIVE) { 61 + s.lineHeight = s.fontSize 62 + } 63 + 64 + if (!s.fontFamily) { 65 + applyFonts(s, fontFamily) 66 + } 67 + 68 + return s 69 + } 70 + 71 + /** 72 + * Text from #/components/Typography with optional font override 73 + */ 74 + function Text({ 75 + children, 76 + emoji, 77 + style, 78 + selectable, 79 + title, 80 + dataSet, 81 + numberOfLines, 82 + ...rest 83 + }: TextProps) { 84 + const {fonts, flags, theme: t} = useAlf() 85 + const s = normalizeTextStyles( 86 + [ 87 + a.text_sm, 88 + t.atoms.text, 89 + web(numberOfLines === 1 && numberOfLinesClippingFix), 90 + style, 91 + ], 92 + { 93 + fontScale: fonts.scaleMultiplier, 94 + fontFamily: fonts.family, 95 + flags, 96 + }, 97 + ) 98 + 99 + if (__DEV__) { 100 + if (!emoji && childHasEmoji(children)) { 101 + logger.warn( 102 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string 103 + `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, 104 + ) 105 + } 106 + } 107 + 108 + const shared = { 109 + uiTextView: true, 110 + selectable, 111 + numberOfLines, 112 + style: s, 113 + dataSet: Object.assign({tooltip: title}, dataSet || {}), 114 + ...rest, 115 + } 116 + 117 + return ( 118 + <UITextView {...shared}> 119 + {renderChildrenWithEmoji(children, shared, emoji ?? false)} 120 + </UITextView> 121 + ) 122 + } 123 + //#endregion 124 + 125 + function CodeBlock({children}: {children: React.ReactNode}) { 126 + const {theme: t} = useAlf() 127 + 128 + return ( 129 + <View 130 + style={[ 131 + a.p_md, 132 + a.rounded_xs, 133 + a.my_sm, 134 + a.border, 135 + t.atoms.border_contrast_low, 136 + ]}> 137 + {children} 138 + </View> 139 + ) 140 + } 141 + 23 142 function toArray<T>(arrayLike: ArrayLike<T> | undefined): T[] | undefined { 24 143 if (!arrayLike) return undefined 25 144 const result: T[] = [] ··· 85 204 if (!rawHtml) return null 86 205 87 206 // Parse HTML once and sanitize/render in a single pass 88 - return sanitizeAndRenderHtml(rawHtml, numberOfLines, textStyle, [ 89 - t.atoms.text, 207 + return sanitizeAndRenderHtml( 208 + rawHtml, 209 + numberOfLines, 90 210 textStyle, 91 - ]) 92 - }, [record, renderMastodonHtml, numberOfLines, textStyle, t.atoms.text]) 211 + [t.atoms.text, textStyle], 212 + [ 213 + {color: t.palette.primary_500}, // should match <InlineLinkText> 214 + ], 215 + ) 216 + }, [ 217 + record, 218 + renderMastodonHtml, 219 + numberOfLines, 220 + textStyle, 221 + t.atoms.text, 222 + t.palette.primary_500, 223 + ]) 93 224 94 225 const handleLayout = (event: LayoutChangeEvent) => { 95 226 const height = event.nativeEvent.layout.height ··· 102 233 103 234 if (!renderedContent) return null 104 235 105 - const shouldCollapse = isTall && !isExpanded 106 - 107 236 return ( 108 237 <View style={style}> 109 238 <View 110 239 style={ 111 - shouldCollapse ? {maxHeight: 150, overflow: 'hidden'} : undefined 240 + isTall && !isExpanded 241 + ? {maxHeight: 150, overflow: 'hidden'} 242 + : undefined 112 243 } 113 244 onLayout={handleLayout}> 114 245 {renderedContent} 115 246 </View> 116 - {shouldCollapse && ( 247 + {isTall && isExpanded && ( 248 + <Button 249 + label={_(msg`Show less`)} 250 + onPress={() => setIsExpanded(false)} 251 + color="primary_subtle" 252 + size="small" 253 + style={[a.mt_xs]}> 254 + <ButtonText> 255 + <Trans>Show less</Trans> 256 + </ButtonText> 257 + </Button> 258 + )} 259 + {isTall && !isExpanded && ( 117 260 <Button 118 261 label={_(msg`Show more`)} 119 262 onPress={() => setIsExpanded(true)} 120 - variant="ghost" 121 - color="primary" 263 + color="primary_subtle" 122 264 size="small" 123 265 style={[a.mt_xs]}> 124 266 <ButtonText> ··· 193 335 _numberOfLines?: number, 194 336 inputTextStyle?: StyleProp<TextStyle>, 195 337 plainTextStyle?: StyleProp<TextStyle>, 338 + linkStyle?: StyleProp<TextStyle>, 196 339 ): React.ReactNode { 197 340 const doc = new DOMParser().parseFromString( 198 341 `<html>${html}</html>`, ··· 218 361 insidePre = false, 219 362 trimStart = false, 220 363 trimEnd = false, 364 + textStyle = inputTextStyle, 221 365 ): React.ReactNode => { 222 366 key = `${key}-${node.nodeName.toLowerCase()}` 223 367 if (node.nodeName === '#text') { ··· 234 378 return text 235 379 } 236 380 return ( 237 - <Text key={key} style={inputTextStyle} emoji> 381 + <Text key={key} style={textStyle} emoji> 238 382 {text} 239 383 </Text> 240 384 ) ··· 246 390 247 391 if (node.nodeName === '#cdata-section') { 248 392 return ( 249 - <Text key={key} style={inputTextStyle} emoji> 393 + <Text key={key} style={textStyle} emoji> 250 394 {node.nodeValue} 251 395 </Text> 252 396 ) ··· 290 434 } 291 435 } 292 436 293 - const renderChildren = (subInsideLink?: boolean, groupInline = false) => { 437 + const renderChildren = ( 438 + subInsideLink?: boolean, 439 + groupInline = false, 440 + textStyle = inputTextStyle, 441 + ) => { 294 442 if (!element.childNodes) return [] 295 443 296 444 const childRenderer = (child: XMLNode, i: number) => ··· 306 454 !childPre && 307 455 ((isBlock && i === lastContentIndex) || 308 456 (trimEnd && i === lastContentIndex)), 457 + textStyle, 309 458 ) 310 459 311 460 if (!groupInline) { ··· 321 470 const flushInline = () => { 322 471 if (inlineGroup.length === 0) return 323 472 result.push( 324 - <Text key={`ig-${groupKey}`} style={inputTextStyle} emoji> 473 + <Text key={`ig-${groupKey}`} style={textStyle} emoji> 325 474 {inlineGroup} 326 475 </Text>, 327 476 ) ··· 363 512 const children = renderChildren() 364 513 365 514 return ( 366 - <P key={key} style={inputTextStyle} emoji> 515 + <P key={key} style={textStyle} emoji> 367 516 {children} 368 517 </P> 369 518 ) ··· 388 537 ) 389 538 } 390 539 case 'pre': { 391 - const children = renderChildren() 540 + const children = renderChildren(false, false, [ 541 + textStyle, 542 + {fontFamily: 'monospace'}, 543 + ]) 392 544 393 545 return ( 394 - <View 395 - key={key} 396 - style={{ 397 - padding: 8, 398 - borderRadius: 4, 399 - marginVertical: 4, 400 - }}> 401 - <P style={[inputTextStyle, {fontFamily: 'monospace'}]} emoji> 546 + <CodeBlock key={key}> 547 + <P style={textStyle} emoji> 402 548 {children} 403 549 </P> 404 - </View> 550 + </CodeBlock> 405 551 ) 406 552 } 407 553 case 'code': { 408 - const children = renderChildren() 554 + const children = renderChildren(false, false, [ 555 + textStyle, 556 + {fontFamily: 'monospace'}, 557 + ]) 409 558 410 - return ( 411 - <Text 412 - key={key} 413 - style={[ 414 - inputTextStyle, 415 - { 416 - fontFamily: 'monospace', 417 - paddingHorizontal: 4, 418 - borderRadius: 2, 419 - }, 420 - ]} 421 - emoji> 422 - {children} 423 - </Text> 424 - ) 559 + return <>{children}</> 425 560 } 426 561 case 'strong': 427 562 case 'b': { 428 - const children = renderChildren() 563 + const children = renderChildren(false, false, [textStyle, a.font_bold]) 429 564 430 - return ( 431 - <Text key={key} style={[inputTextStyle, a.font_bold]} emoji> 432 - {children} 433 - </Text> 434 - ) 565 + return <>{children}</> 435 566 } 436 567 case 'em': 437 568 case 'i': { 438 - const children = renderChildren() 569 + const children = renderChildren(false, false, [textStyle, a.italic]) 439 570 440 - return ( 441 - <Text key={key} style={[inputTextStyle, a.italic]} emoji> 442 - {children} 443 - </Text> 444 - ) 571 + return <>{children}</> 445 572 } 446 573 case 'u': { 447 - const children = renderChildren() 574 + const children = renderChildren(false, false, [textStyle, a.underline]) 448 575 449 - return ( 450 - <Text key={key} style={[inputTextStyle, a.underline]} emoji> 451 - {children} 452 - </Text> 453 - ) 576 + return <>{children}</> 454 577 } 455 578 case 'del': 456 579 case 's': { 457 - const children = renderChildren() 580 + const children = renderChildren(false, false, [ 581 + textStyle, 582 + a.strike_through, 583 + ]) 458 584 459 - return ( 460 - <Text key={key} style={[inputTextStyle, a.strike_through]} emoji> 461 - {children} 462 - </Text> 463 - ) 585 + return <>{children}</> 464 586 } 465 587 case 'ul': { 466 588 const children = renderChildren() 467 589 468 590 return ( 469 - <View key={key} style={{marginVertical: 4}}> 591 + <View 592 + key={key} 593 + style={{marginVertical: 4}} 594 + {...(web({ 595 + role: 'list', 596 + }) || {})}> 470 597 {children} 471 598 </View> 472 599 ) ··· 474 601 case 'ol': { 475 602 const start = element.getAttribute('start') 476 603 const startNum = start ? parseInt(start, 10) : 1 604 + console.log(element.childNodes) 477 605 return ( 478 - <View key={key} style={{marginVertical: 4}}> 606 + <View 607 + key={key} 608 + style={{marginVertical: 4}} 609 + {...(web({ 610 + role: 'list', 611 + }) || {})}> 479 612 {toArray(element.childNodes) 480 - ?.filter(child => child.nodeName === 'LI') 613 + ?.filter( 614 + child => child.nodeName === 'LI' || child.nodeName === 'li', 615 + ) 481 616 .map((child, i) => 482 617 renderNode( 483 618 child, ··· 493 628 ) 494 629 } 495 630 case 'li': { 496 - const children = renderChildren() 631 + const children = renderChildren(undefined, true) 497 632 498 633 const marker = 499 634 listItemIndex !== undefined ? `${listItemIndex}.` : '\u2022' 500 635 return ( 501 - <View key={key} style={{flexDirection: 'row', marginVertical: 2}}> 502 - <Text style={[inputTextStyle, {marginRight: 8}]} emoji> 636 + <View 637 + key={key} 638 + style={{flexDirection: 'row', marginVertical: 2}} 639 + {...(web({ 640 + role: 'listitem', 641 + }) || {})}> 642 + <Text 643 + style={[textStyle, {marginRight: 8}]} 644 + emoji 645 + {...(web({'aria-hidden': true}) || {})}> 503 646 {marker} 504 647 </Text> 505 - <View style={[inputTextStyle, {flex: 1}]}>{children}</View> 648 + <View style={[textStyle, {flex: 1}]}>{children}</View> 506 649 </View> 507 650 ) 508 651 } ··· 515 658 case 'rp': 516 659 return null // TODO support ruby text rendering 517 660 case 'a': { 518 - const children = renderChildren(true) 661 + const children = renderChildren(true, false, [ 662 + inputTextStyle, 663 + linkStyle, 664 + ]) 519 665 520 666 const href = element.getAttribute('href') 521 667 if (href) { ··· 533 679 to={url.toString()} 534 680 label={linkText} 535 681 shouldProxy 536 - style={isInvisible ? {display: 'none'} : inputTextStyle}> 682 + style={isInvisible ? {display: 'none'} : textStyle}> 537 683 {children} 538 684 </InlineLinkText> 539 685 ) ··· 543 689 } 544 690 case 'br': 545 691 return ( 546 - <Text key={key} style={inputTextStyle} emoji> 692 + <Text key={key} style={textStyle} emoji> 547 693 {'\n'} 548 694 </Text> 549 695 ) ··· 564 710 return '\u2026' 565 711 } 566 712 return ( 567 - <Text key={key} style={inputTextStyle} emoji> 713 + <Text key={key} style={textStyle} emoji> 568 714 {'\u2026'} 569 715 </Text> 570 716 ) ··· 586 732 key={key} 587 733 display={tagText} 588 734 tag={tagText} 589 - textStyle={[inputTextStyle, a.underline]} 735 + textStyle={[textStyle, a.underline]} 590 736 /> 591 737 ) 592 738 } ··· 597 743 return children 598 744 } 599 745 return ( 600 - <Text key={key} style={inputTextStyle} emoji> 746 + <Text key={key} style={textStyle} emoji> 601 747 {children} 602 748 </Text> 603 749 ) ··· 607 753 return children 608 754 } 609 755 return ( 610 - <Text key={key} style={inputTextStyle} emoji> 756 + <Text key={key} style={textStyle} emoji> 611 757 {children} 612 758 </Text> 613 759 ) ··· 621 767 const children = renderChildren() 622 768 623 769 return ( 624 - <P key={key} style={inputTextStyle} emoji> 625 - <Text style={[inputTextStyle, {fontWeight: 'bold'}]} emoji> 626 - {children} 627 - </Text> 770 + <P 771 + key={key} 772 + style={[textStyle, {fontWeight: 'bold'}]} 773 + emoji 774 + {...(web({ 775 + role: 'heading', 776 + }) || {})}> 777 + {children} 628 778 </P> 629 779 ) 630 780 }
+1 -1
src/components/Typography.tsx
··· 105 105 * 106 106 * @see https://github.com/necolas/react-native-web/pull/2836 107 107 */ 108 - const numberOfLinesClippingFix = { 108 + export const numberOfLinesClippingFix = { 109 109 overflowY: 'visible', 110 110 overflowX: 'clip', 111 111 // mimic browser default behavior of `min-width: 0` on `overflow: hidden`