aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-03-21 02:21:37 +0100
committerGuillermo Serrahima <dev@vylion.com>2026-03-21 02:21:37 +0100
commit41b2943dba9993bd4b8c5a6e4546d8e23dce7662 (patch)
tree29920b0a65dcf702dc60dbfd2d9b5122c97a1752
parentad8a4cdc3ea5040b7732bc9e6e3090690a1032b1 (diff)
downloadllostre-41b2943dba9993bd4b8c5a6e4546d8e23dce7662.tar.xz
llostre-41b2943dba9993bd4b8c5a6e4546d8e23dce7662.zip
Changed parents string array to Note array + rendering threads in frontend
-rw-r--r--backend/fetchers/app.bsky.feed.go20
-rw-r--r--backend/notes/note.go8
-rw-r--r--frontend/src/main.ts145
3 files changed, 129 insertions, 44 deletions
diff --git a/backend/fetchers/app.bsky.feed.go b/backend/fetchers/app.bsky.feed.go
index 5d87cca..4c6b92f 100644
--- a/backend/fetchers/app.bsky.feed.go
+++ b/backend/fetchers/app.bsky.feed.go
@@ -136,24 +136,18 @@ func parseAppBskyFeedPostCollection(logger *slog.Logger, slingshotURL string, se
return notes.DeduplicateThreads(posts), nil
}
-func FetchParents(logger *slog.Logger, replyToURI string, slingshotURL string, session *auth.OAuthState, cache *Cache, maxDepth int) ([]string, error) {
- parents := []string{}
- seen := make(map[string]bool)
+func FetchParents(logger *slog.Logger, replyToURI string, slingshotURL string, session *auth.OAuthState, cache *Cache, maxDepth int) ([]notes.Note, error) {
+ parents := []notes.Note{}
currentURI := replyToURI
for currentURI != "" {
- // check for circular reference
- if seen[currentURI] {
- logger.Warn("circular reference detected in thread", "uri", currentURI)
- break
- }
- seen[currentURI] = true
-
note, err := GetAppBskyFeedPostByURI(logger, currentURI, slingshotURL, session, cache)
if err != nil {
logger.Warn("failed to fetch parent", "uri", currentURI, "error", err)
- cache.Set(currentURI, notes.MissingNote(currentURI))
- parents = append(parents, currentURI)
+ note = notes.MissingNote(currentURI)
+
+ cache.Set(currentURI, note)
+ parents = append(parents, *note)
break
}
@@ -168,7 +162,7 @@ func FetchParents(logger *slog.Logger, replyToURI string, slingshotURL string, s
go FetchAppBskyFeedPosts(logger, note.AuthorDID, authorPDS, slingshotURL, session, cache)
}
- parents = append(parents, currentURI)
+ parents = append(parents, *note)
if note.ReplyTo == nil {
break
diff --git a/backend/notes/note.go b/backend/notes/note.go
index 569188c..36b9f2b 100644
--- a/backend/notes/note.go
+++ b/backend/notes/note.go
@@ -16,7 +16,7 @@ type Note struct {
CreatedAt time.Time `json:"createdAt"`
ReplyTo *string `json:"replyTo,omitempty"`
Facets []Facet `json:"facets,omitempty"`
- Parents []string `json:"parents,omitempty"`
+ Parents []Note `json:"parents,omitempty"`
}
type Facet struct {
@@ -35,15 +35,15 @@ func MissingNote(uri string) *Note {
}
}
-func isChainContained(smallChain, bigChain []string) bool {
+func isChainContained(smallChain, bigChain []Note) bool {
if len(smallChain) > len(bigChain) {
return false
}
// check if smallChain is a suffix of bigChain
offset := len(bigChain) - len(smallChain)
- for i, uri := range smallChain {
- if bigChain[offset+i] != uri {
+ for i, note := range smallChain {
+ if bigChain[offset+i].URI != note.URI {
return false
}
}
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index d0cff46..306578d 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -1,6 +1,25 @@
import { marked } from 'marked';
import './style.css'
+interface Note {
+ uri: string;
+ authorDID: string;
+ authorHandle: string;
+ authorName: string;
+ authorNoUnauth: boolean;
+ avatarURL: string;
+ text: string;
+ createdAt: string;
+ replyTo?: string;
+ parents?: Note[];
+ facets?: Array<{
+ byteStart: number;
+ byteEnd: number;
+ type: string;
+ value: string;
+ }>;
+}
+
// convert Facets to Markdown (before Markdown parsing) if Markdown passing is enabled
// convert them to HTML code otherwise
function convertFacets(text: string, facets: any[], toMarkown: boolean): string {
@@ -66,20 +85,98 @@ function convertFacets(text: string, facets: any[], toMarkown: boolean): string
return result;
}
-function renderAuthor(note: any): string {
+function createAuthorElement(note: Note): HTMLElement {
+ const container = document.createElement('div');
+
const hasDisplayName = note.authorName && note.authorName !== '';
const hasValidHandle = note.authorHandle !== 'handle.invalid';
- const pronouns = note.pronouns ? `<span class="muted> · ${note.pronouns}</span>` : '';
if (hasDisplayName && hasValidHandle) {
- return `<div><strong>${note.authorName}</strong>${pronouns}</div><div class="muted">@${note.authorHandle}</div>`;
+ const nameDiv = document.createElement('div');
+ nameDiv.innerHTML = `<strong>${note.authorName}</strong>`;
+ container.appendChild(nameDiv);
+
+ const handleDiv = document.createElement('div');
+ handleDiv.className = 'muted';
+ handleDiv.textContent = `@${note.authorHandle}`;
+ container.appendChild(handleDiv);
} else if (hasDisplayName && !hasValidHandle) {
- return `<div><strong>${note.authorName}</strong>${pronouns}</div><div class="muted">⚠️ InvalidHandle</div>`;
+ const nameDiv = document.createElement('div');
+ nameDiv.innerHTML = `<strong>${note.authorName}</strong>`;
+ container.appendChild(nameDiv);
+
+ const warningDiv = document.createElement('div');
+ warningDiv.className = 'muted';
+ warningDiv.textContent = '⚠️ InvalidHandle';
+ container.appendChild(warningDiv);
} else if (!hasDisplayName && hasValidHandle) {
- return `<div><strong>@${note.authorHandle}</strong>${pronouns}</div>`;
+ const handleDiv = document.createElement('div');
+ handleDiv.innerHTML = `<strong>@${note.authorHandle}</strong>`;
+ container.appendChild(handleDiv);
+ } else {
+ const warningDiv = document.createElement('div');
+ warningDiv.innerHTML = '<strong>⚠️ InvalidHandle</strong>';
+ container.appendChild(warningDiv);
+ }
+
+ return container;
+}
+
+function createNoteElement(note: Note): HTMLElement {
+ const noteDiv = document.createElement('div');
+ noteDiv.className = 'note';
+
+ const avatar = document.createElement('img');
+ avatar.className = 'note-avatar';
+ avatar.src = note.avatarURL || '';
+ avatar.alt = 'avatar';
+ noteDiv.appendChild(avatar);
+
+ const content = document.createElement('div');
+ content.className = 'note-content';
+
+ content.appendChild(createAuthorElement(note));
+
+ const textDiv = document.createElement('div');
+ textDiv.className = 'note-text';
+ const markdownEnabled = true; // TODO: create a toggleable setting
+ let parsedText = '';
+ if (note.facets && note.facets.length > 0) {
+ parsedText = convertFacets(note.text || '', note.facets || [], markdownEnabled);
+ } else {
+ parsedText = note.text;
+ }
+
+ if (markdownEnabled) {
+ textDiv.innerHTML = marked.parseInline(parsedText, { breaks: true }) as string;
} else {
- return `<div><strong>⚠️ InvalidHandle</strong>${pronouns}</div>`;
+ textDiv.innerHTML = parsedText;
}
+ content.appendChild(textDiv);
+
+ const timeDiv = document.createElement('div');
+ timeDiv.className = 'muted';
+ timeDiv.textContent = new Date(note.createdAt).toLocaleString();
+ content.appendChild(timeDiv);
+
+ noteDiv.appendChild(content);
+
+ return noteDiv;
+}
+
+function createThreadElement(notes: Note[]): HTMLElement {
+ if (notes.length === 1) {
+ return createNoteElement(notes[0]);
+ }
+
+ const threadDiv = document.createElement('div');
+ threadDiv.className = 'reply-chain';
+
+ notes.forEach(note => {
+ threadDiv.appendChild(createNoteElement(note));
+ });
+
+ return threadDiv;
}
async function main() {
@@ -105,32 +202,26 @@ async function main() {
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const notes = await response.json();
+ const notes: Note[] = await response.json();
+
app.innerHTML = '';
- for (const note of notes) {
- let parsedText = '';
- if(note.facets && note.facets.length > 0) {
- parsedText = convertFacets(note.text || '', note.facets || [], true);
+ notes.forEach(note => {
+ if (!note.parents || note.parents.length === 0) {
+ app.appendChild(createNoteElement(note));
} else {
- parsedText = note.text;
+ // parents were fetched bottom to top, reverse + add child note
+ const thread: Note[] = [note, ...note.parents].reverse();
+
+ app.appendChild(createThreadElement(thread));
}
- const noteText = marked.parseInline(parsedText, { breaks: true });
-
- const postHTML = `
- <div class="note">
- <img class="note-avatar" src="${note.avatarURL}" alt="avatar">
- <div class="note-content">
- ${renderAuthor(note)}
- <div class="note-text">${noteText}</div>
- <div class="muted">${new Date(note.createdAt).toLocaleString()}</div>
- </div>
- </div>
- `;
- app.innerHTML += postHTML;
- }
+ });
} catch (err) {
- app.innerHTML = `<div style="color: #f88">error loading posts: ${err}</div>`;
+ const errorDiv = document.createElement('div');
+ errorDiv.style.color = '#f88';
+ errorDiv.textContent = `error loading posts: ${err}`;
+ app.innerHTML = '';
+ app.appendChild(errorDiv);
}
}