diff options
| -rw-r--r-- | backend/notes/app.bsky.feed.post.go | 50 | ||||
| -rw-r--r-- | frontend/bun.lock | 5 | ||||
| -rw-r--r-- | frontend/package.json | 3 | ||||
| -rw-r--r-- | frontend/src/main.ts | 63 | ||||
| -rw-r--r-- | frontend/src/style.css | 7 |
5 files changed, 126 insertions, 2 deletions
diff --git a/backend/notes/app.bsky.feed.post.go b/backend/notes/app.bsky.feed.post.go index 43929b3..32e7854 100644 --- a/backend/notes/app.bsky.feed.post.go +++ b/backend/notes/app.bsky.feed.post.go @@ -13,6 +13,14 @@ type AppBskyFeedPostNote struct { Text string `json:"text"` CreatedAt time.Time `json:"createdAt"` ReplyTo *string `json:"replyTo,omitempty"` + Facets []Facet `json:"facets,omitempty"` +} + +type Facet struct { + ByteStart int `json:"byteStart"` + ByteEnd int `json:"byteEnd"` + Type string `json:"type"` + Value string `json:"value"` // the link destination, whether url, profile or tag } type AppBskyFeedPostRecord struct { @@ -25,6 +33,18 @@ type AppBskyFeedPostRecord struct { URI string `json:"uri"` } `json:"parent"` } `json:"reply,omitempty"` + Facets []struct { + Index struct { + ByteStart int `json:"byteStart"` + ByteEnd int `json:"byteEnd"` + } `json:"index"` + Features []struct { + Type string `json:"$type"` + URI string `json:"uri,omitempty"` + DID string `json:"did,omitempty"` + Tag string `json:"tag,omitempty"` + } `json:"features"` + } `json:"facets,omitempty"` } `json:"value"` } @@ -66,6 +86,35 @@ func AppBskyFeedPostFromRecord(record AppBskyFeedPostRecord) (*AppBskyFeedPostNo parts := strings.Split(record.URI, "/") // at://DID/collection/rkey did := parts[2] + var facets []Facet + for _, f := range record.Value.Facets { + for _, feature := range f.Features { + var value string + var facetType string + + switch feature.Type { + case "app.bsky.richtext.facet#link": + facetType = "link" + value = feature.URI + case "app.bsky.richtext.facet#mention": + facetType = "mention" + value = feature.DID + case "app.bsky.richtext.facet#tag": + facetType = "tag" + value = feature.Tag + default: + continue + } + + facets = append(facets, Facet{ + ByteStart: f.Index.ByteStart, + ByteEnd: f.Index.ByteEnd, + Type: facetType, + Value: value, + }) + } + } + return &AppBskyFeedPostNote{ URI: record.URI, AuthorDID: did, @@ -73,5 +122,6 @@ func AppBskyFeedPostFromRecord(record AppBskyFeedPostRecord) (*AppBskyFeedPostNo CreatedAt: createdAt, Text: record.Value.Text, ReplyTo: replyTo, + Facets: facets, }, nil } diff --git a/frontend/bun.lock b/frontend/bun.lock index bd161f5..4671154 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "frontend", + "dependencies": { + "marked": "^17.0.3", + }, "devDependencies": { "typescript": "~5.9.3", "vite": "^8.0.0-beta.13", @@ -86,6 +89,8 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], diff --git a/frontend/package.json b/frontend/package.json index 8c2ad49..db2c5e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,5 +14,8 @@ }, "overrides": { "vite": "^8.0.0-beta.13" + }, + "dependencies": { + "marked": "^17.0.3" } } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 099193a..f53632c 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,5 +1,58 @@ +import { marked } from 'marked'; import './style.css' +function renderTextWithFacets(text: string, facets: any[]): string { + if (!facets || facets.length === 0) { + return text; + } + + // convert text to UTF-8 bytes because Bluesky counts bytes in UTF-8 + const encoder = new TextEncoder(); + const postBytes = encoder.encode(text); + + // sort facets by start position + facets.sort((a, b) => a.byteStart - b.byteStart); + + let result = ''; + let lastPos = 0; + + const decoder = new TextDecoder(); + for (const facet of facets) { + // add text before this facet + const beforeBytes = postBytes.slice(lastPos, facet.byteStart); + result += decoder.decode(beforeBytes); + + // add the faceted text as a link + const facetBytes = postBytes.slice(facet.byteStart, facet.byteEnd); + const facetText = decoder.decode(facetBytes); + + switch (facet.type) { + case 'link': + result += `<a href="${facet.value}" target="_blank">${facetText}</a>`; + break; + case 'mention': + result += `<a href="https://bsky.app/profile/${facet.value}" target="_blank">${facetText}</a>`; + break; + case 'tag': + result += `<a href="https://bsky.app/hashtag/${facet.value}" target="_blank">#${facetText}</a>`; + break; + default: // in case Bluesky ever adds other fcaet types + result += facetText; // if we don't recognize the type, just add it as plain text + break; + } + + lastPos = facet.byteEnd; + } + + // add remaining text + const remainingBytes = postBytes.slice(lastPos); + result += decoder.decode(remainingBytes); + + console.log(result); + + return result; +} + async function main() { const app = document.querySelector('#app'); if (!app) { @@ -17,12 +70,20 @@ async function main() { app.innerHTML = ''; for (const post of posts) { + let parsedText = ''; + if(post.facets && post.facets.length > 0) { + parsedText = renderTextWithFacets(post.text || '', post.facets || []); + } else { + parsedText = post.text; + } + const postText = marked.parseInline(parsedText, { breaks: true }); + const postHTML = ` <div class="note"> <img class="note-avatar" src="https://cdn.bsky.app/img/avatar/plain/did:plc:hyyqbqqhpxt2bnn7s2e2iuxd/bafkreidoiynom7v2y5rpsm467l3d2cnr7ozvlfusrchjey3k2jtyquufq4@jpeg" alt="avatar"> <div class="note-content"> <div style="font-weight: bold">@${post.authorHandle}</div> - <div class="note-text">${post.text || '<br>'}</div> + <div class="note-text">${postText || '<br>'}</div> <div style="font-size: 12px; color: #888">${new Date(post.createdAt).toLocaleString()}</div> </div> </div> diff --git a/frontend/src/style.css b/frontend/src/style.css index 13d2217..549c596 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -60,6 +60,11 @@ h1 { white-space: pre-wrap; } +.muted { + opacity: 0.5; + font-size: 12px; +} + .spinner { border: 16px solid #f3f3f3; border-top: 16px solid #3498db; @@ -80,4 +85,4 @@ h1 { color: #213547; background-color: #ffffff; } -}
\ No newline at end of file +} |
