aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-02-20 16:21:00 +0100
committerGuillermo Serrahima <dev@vylion.com>2026-02-20 16:23:30 +0100
commit4a2a8c34e87c5dd25f6d01a1aafaa10799aa907f (patch)
treeb3b090a38a2e2748cd110a043e6369be9450e063
parente4c0caadc07b050ca827483263d15d2c118df3d2 (diff)
downloadllostre-4a2a8c34e87c5dd25f6d01a1aafaa10799aa907f.tar.xz
llostre-4a2a8c34e87c5dd25f6d01a1aafaa10799aa907f.zip
added facets and markdown rendering
-rw-r--r--backend/notes/app.bsky.feed.post.go50
-rw-r--r--frontend/bun.lock5
-rw-r--r--frontend/package.json3
-rw-r--r--frontend/src/main.ts63
-rw-r--r--frontend/src/style.css7
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
+}