add a custom fact renderer
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import {h} from 'preact'
|
import {ComponentChild, h} from 'preact'
|
||||||
|
|
||||||
import logo from '../../assets/logo_full_name.svg'
|
import logo from '../../assets/logo_full_name.svg'
|
||||||
import {Like as LikeIcon} from '../icons/Like'
|
import {Like as LikeIcon} from '../icons/Like'
|
||||||
@@ -7,6 +7,7 @@ import {Repost as RepostIcon} from '../icons/Repost'
|
|||||||
import {Robot as RobotIcon} from '../icons/Robot'
|
import {Robot as RobotIcon} from '../icons/Robot'
|
||||||
import {CONTENT_LABELS} from '../labels'
|
import {CONTENT_LABELS} from '../labels'
|
||||||
import * as app from '../lexicons/app'
|
import * as app from '../lexicons/app'
|
||||||
|
import {FacetRenderer} from '../util/facet-renderer'
|
||||||
import {niceDate} from '../util/nice-date'
|
import {niceDate} from '../util/nice-date'
|
||||||
import {prettyNumber} from '../util/pretty-number'
|
import {prettyNumber} from '../util/pretty-number'
|
||||||
import {getRkey} from '../util/rkey'
|
import {getRkey} from '../util/rkey'
|
||||||
@@ -146,65 +147,51 @@ function PostContent({record}: {record: app.bsky.feed.post.Main | null}) {
|
|||||||
// render an empty <p> that adds an extra flex gap above the embed
|
// render an empty <p> that adds an extra flex gap above the embed
|
||||||
if (!record?.text) return null
|
if (!record?.text) return null
|
||||||
|
|
||||||
// const rt = new RichText({
|
const rt = new FacetRenderer({text: record.text, facets: record.facets})
|
||||||
// text: record.text,
|
const richText: ComponentChild[] = []
|
||||||
// facets: record.facets,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const richText = []
|
let counter = 0
|
||||||
|
for (const segment of rt.segments()) {
|
||||||
// let counter = 0
|
if (segment.link) {
|
||||||
// for (const segment of rt.segments()) {
|
richText.push(
|
||||||
// if (
|
<Link
|
||||||
// segment.link &&
|
key={counter}
|
||||||
// app.bsky.richtext.facet.link.$safeValidate(segment.link).success
|
href={segment.link.uri}
|
||||||
// ) {
|
className="text-brand hover:underline"
|
||||||
// richText.push(
|
disableTracking={
|
||||||
// <Link
|
!segment.link.uri.startsWith('https://bsky.app') &&
|
||||||
// key={counter}
|
!segment.link.uri.startsWith('https://go.bsky.app')
|
||||||
// href={segment.link.uri}
|
}>
|
||||||
// className="text-brand hover:underline"
|
{segment.text}
|
||||||
// disableTracking={
|
</Link>,
|
||||||
// !segment.link.uri.startsWith('https://bsky.app') &&
|
)
|
||||||
// !segment.link.uri.startsWith('https://go.bsky.app')
|
} else if (segment.mention) {
|
||||||
// }>
|
richText.push(
|
||||||
// {segment.text}
|
<Link
|
||||||
// </Link>,
|
key={counter}
|
||||||
// )
|
href={`/profile/${segment.mention.did}`}
|
||||||
// } else if (
|
className="text-brand hover:underline">
|
||||||
// segment.mention &&
|
{segment.text}
|
||||||
// app.bsky.richtext.facet.mention.safeValidate(segment.mention).success
|
</Link>,
|
||||||
// ) {
|
)
|
||||||
// richText.push(
|
} else if (segment.tag) {
|
||||||
// <Link
|
richText.push(
|
||||||
// key={counter}
|
<Link
|
||||||
// href={`/profile/${segment.mention.did}`}
|
key={counter}
|
||||||
// className="text-brand hover:underline">
|
href={`/hashtag/${segment.tag.tag}`}
|
||||||
// {segment.text}
|
className="text-brand hover:underline">
|
||||||
// </Link>,
|
{segment.text}
|
||||||
// )
|
</Link>,
|
||||||
// } else if (
|
)
|
||||||
// segment.tag &&
|
} else {
|
||||||
// app.bsky.richtext.facet.tag.safeValidate(segment.tag).success
|
richText.push(segment.text)
|
||||||
// ) {
|
}
|
||||||
// richText.push(
|
counter++
|
||||||
// <Link
|
}
|
||||||
// key={counter}
|
|
||||||
// href={`/hashtag/${segment.tag.tag}`}
|
return (
|
||||||
// className="text-brand hover:underline">
|
<p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap">
|
||||||
// {segment.text}
|
{richText}
|
||||||
// </Link>,
|
</p>
|
||||||
// )
|
)
|
||||||
// } else {
|
|
||||||
// richText.push(segment.text)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// counter++
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap">
|
|
||||||
// {richText}
|
|
||||||
// </p>
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import * as facetDefs from '../lexicons/app/bsky/richtext/facet.defs'
|
||||||
|
|
||||||
|
type Facet = facetDefs.Main
|
||||||
|
type Link = facetDefs.Link
|
||||||
|
type Mention = facetDefs.Mention
|
||||||
|
type Tag = facetDefs.Tag
|
||||||
|
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
|
||||||
|
export class FacetSegment {
|
||||||
|
constructor(
|
||||||
|
public text: string,
|
||||||
|
public facet?: Facet,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get link(): Link | undefined {
|
||||||
|
return this.facet?.features.find(
|
||||||
|
f => f.$type === 'app.bsky.richtext.facet#link',
|
||||||
|
) as Link | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
get mention(): Mention | undefined {
|
||||||
|
return this.facet?.features.find(
|
||||||
|
f => f.$type === 'app.bsky.richtext.facet#mention',
|
||||||
|
) as Mention | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
get tag(): Tag | undefined {
|
||||||
|
return this.facet?.features.find(
|
||||||
|
f => f.$type === 'app.bsky.richtext.facet#tag',
|
||||||
|
) as Tag | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FacetRenderer {
|
||||||
|
private utf8: Uint8Array
|
||||||
|
private facets: Facet[]
|
||||||
|
|
||||||
|
constructor({text, facets}: {text: string; facets?: Facet[]}) {
|
||||||
|
this.utf8 = encoder.encode(text)
|
||||||
|
this.facets = (facets ?? [])
|
||||||
|
.filter(f => f.index.byteStart <= f.index.byteEnd)
|
||||||
|
.sort((a, b) => a.index.byteStart - b.index.byteStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
private slice(start: number, end: number): string {
|
||||||
|
return decoder.decode(this.utf8.slice(start, end))
|
||||||
|
}
|
||||||
|
|
||||||
|
*segments(): Generator<FacetSegment, void, void> {
|
||||||
|
if (!this.facets.length) {
|
||||||
|
yield new FacetSegment(decoder.decode(this.utf8))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let textCursor = 0
|
||||||
|
let facetCursor = 0
|
||||||
|
do {
|
||||||
|
const currFacet = this.facets[facetCursor]
|
||||||
|
if (textCursor < currFacet.index.byteStart) {
|
||||||
|
yield new FacetSegment(
|
||||||
|
this.slice(textCursor, currFacet.index.byteStart),
|
||||||
|
)
|
||||||
|
} else if (textCursor > currFacet.index.byteStart) {
|
||||||
|
facetCursor++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (currFacet.index.byteStart < currFacet.index.byteEnd) {
|
||||||
|
const subtext = this.slice(
|
||||||
|
currFacet.index.byteStart,
|
||||||
|
currFacet.index.byteEnd,
|
||||||
|
)
|
||||||
|
if (!subtext.trim()) {
|
||||||
|
yield new FacetSegment(subtext)
|
||||||
|
} else {
|
||||||
|
yield new FacetSegment(subtext, currFacet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textCursor = currFacet.index.byteEnd
|
||||||
|
facetCursor++
|
||||||
|
} while (facetCursor < this.facets.length)
|
||||||
|
if (textCursor < this.utf8.byteLength) {
|
||||||
|
yield new FacetSegment(this.slice(textCursor, this.utf8.byteLength))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-4
@@ -35,10 +35,7 @@
|
|||||||
// (https://embed.bsky.app)
|
// (https://embed.bsky.app)
|
||||||
window.BSKY_DEV_EMBED_URL = 'http://localhost:5173'
|
window.BSKY_DEV_EMBED_URL = 'http://localhost:5173'
|
||||||
</script>
|
</script>
|
||||||
<script
|
<script async src="http://localhost:3000/embed.js" charset="utf-8"></script>
|
||||||
async
|
|
||||||
src="http://localhost:3000/embed.js"
|
|
||||||
charset="utf-8"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="p12">
|
<body class="p12">
|
||||||
<!-- Base embeds -->
|
<!-- Base embeds -->
|
||||||
@@ -1048,6 +1045,34 @@
|
|||||||
|
|
||||||
<div>Verified quoted</div>
|
<div>Verified quoted</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="item">
|
||||||
|
<blockquote
|
||||||
|
class="bluesky-embed"
|
||||||
|
data-bluesky-uri="at://did:plc:mbsyz5d4psc7m5ri57ikievz/app.bsky.feed.post/3mljg3kmf4226"
|
||||||
|
data-bluesky-cid="bafyreidho2pt4sdq3bwgnb62pbnuvucjsxvgk3jq2t3iqzblbdb2pmjwpe"
|
||||||
|
data-bluesky-embed-color-mode="system">
|
||||||
|
<p lang="en">
|
||||||
|
#tangled is everything I wanted from a modern #git forge: -
|
||||||
|
#atproto based (this is HUGE!) - European - Decentralized -
|
||||||
|
Federated - Social with #vouching - Built with #golang and using a
|
||||||
|
#permissive #MIT license What not to like there?<br /><br /><a
|
||||||
|
href="https://bsky.app/profile/did:plc:mbsyz5d4psc7m5ri57ikievz/post/3mljg3kmf4226?ref_src=embed"
|
||||||
|
>[image or embed]</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
— perpetual screaming 🇺🇦 (<a
|
||||||
|
href="https://bsky.app/profile/did:plc:mbsyz5d4psc7m5ri57ikievz?ref_src=embed"
|
||||||
|
>@bazub.zubko.cc</a
|
||||||
|
>)
|
||||||
|
<a
|
||||||
|
href="https://bsky.app/profile/did:plc:mbsyz5d4psc7m5ri57ikievz/post/3mljg3kmf4226?ref_src=embed"
|
||||||
|
>10 May 2026 at 21:43</a
|
||||||
|
>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<div>Lots of hashtags</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user